From a439387857c2fede769f1921259cf55d293d55f1 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 10 Nov 2022 09:23:20 +0400 Subject: [PATCH 001/108] #RI-3527-add luascript recommendation --- .../database-analysis.module.ts | 2 + .../database-analysis.service.ts | 9 +- .../entities/database-analysis.entity.ts | 11 ++ .../models/database-analysis.ts | 10 ++ .../modules/database-analysis/models/index.ts | 1 + .../models/recommendation.ts | 12 ++ .../providers/database-analysis.provider.ts | 7 +- .../database-recommendations.module.ts | 9 ++ .../database-recommendations.provider.ts | 56 +++++++++ .../ui/src/assets/img/code-changes.svg | 3 + .../src/assets/img/configuration-changes.svg | 3 + redisinsight/ui/src/assets/img/upgrade.svg | 4 + .../constants/dbAnalisisRecomendations.json | 44 +++++++ .../recommendations-view/Recommendations.tsx | 117 ++++++++++++++++++ .../components/recommendations-view/index.ts | 3 + .../recommendations-view/styles.module.scss | 97 +++++++++++++++ .../sub-tabs/DatabaseAnalysisTabs.tsx | 69 +++++++++++ .../components/sub-tabs/constants.tsx | 23 ++++ .../components/sub-tabs/index.ts | 3 + .../components/sub-tabs/styles.module.scss | 11 ++ 20 files changed, 490 insertions(+), 4 deletions(-) create mode 100644 redisinsight/api/src/modules/database-analysis/models/recommendation.ts create mode 100644 redisinsight/api/src/modules/db-recommendations/database-recommendations.module.ts create mode 100644 redisinsight/api/src/modules/db-recommendations/providers/database-recommendations.provider.ts create mode 100644 redisinsight/ui/src/assets/img/code-changes.svg create mode 100644 redisinsight/ui/src/assets/img/configuration-changes.svg create mode 100644 redisinsight/ui/src/assets/img/upgrade.svg create mode 100644 redisinsight/ui/src/constants/dbAnalisisRecomendations.json create mode 100644 redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx create mode 100644 redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/index.ts create mode 100644 redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss create mode 100644 redisinsight/ui/src/pages/databaseAnalysis/components/sub-tabs/DatabaseAnalysisTabs.tsx create mode 100644 redisinsight/ui/src/pages/databaseAnalysis/components/sub-tabs/constants.tsx create mode 100644 redisinsight/ui/src/pages/databaseAnalysis/components/sub-tabs/index.ts create mode 100644 redisinsight/ui/src/pages/databaseAnalysis/components/sub-tabs/styles.module.scss diff --git a/redisinsight/api/src/modules/database-analysis/database-analysis.module.ts b/redisinsight/api/src/modules/database-analysis/database-analysis.module.ts index 90b09ff06c..452933eaff 100644 --- a/redisinsight/api/src/modules/database-analysis/database-analysis.module.ts +++ b/redisinsight/api/src/modules/database-analysis/database-analysis.module.ts @@ -5,8 +5,10 @@ import { DatabaseAnalyzer } from 'src/modules/database-analysis/providers/databa import { DatabaseAnalysisProvider } from 'src/modules/database-analysis/providers/database-analysis.provider'; import { KeysScanner } from 'src/modules/database-analysis/scanner/keys-scanner'; import { KeyInfoProvider } from 'src/modules/database-analysis/scanner/key-info/key-info.provider'; +import { DatabaseRecommendationsModule } from 'src/modules/db-recommendations/database-recommendations.module'; @Module({ + imports: [DatabaseRecommendationsModule], controllers: [DatabaseAnalysisController], providers: [ DatabaseAnalysisService, diff --git a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts index b1add19ae0..ac16c8f4ce 100644 --- a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts +++ b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts @@ -1,5 +1,6 @@ import { HttpException, Injectable, Logger } from '@nestjs/common'; import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; +import { RecommendationsService } from 'src/modules/db-recommendations/providers/database-recommendations.provider'; import { catchAclError } from 'src/utils'; import { DatabaseAnalyzer } from 'src/modules/database-analysis/providers/database-analyzer'; import { plainToClass } from 'class-transformer'; @@ -16,9 +17,10 @@ export class DatabaseAnalysisService { constructor( private readonly databaseConnectionService: DatabaseConnectionService, - private readonly analyzer: DatabaseAnalyzer, - private readonly databaseAnalysisProvider: DatabaseAnalysisProvider, - private readonly scanner: KeysScanner, + private recommendationsService: RecommendationsService, + private analyzer: DatabaseAnalyzer, + private databaseAnalysisProvider: DatabaseAnalysisProvider, + private scanner: KeysScanner, ) {} /** @@ -56,6 +58,7 @@ export class DatabaseAnalysisService { databaseId: clientOptions.instanceId, ...dto, progress, + recommendations: await this.recommendationsService.getRecommendations(clientOptions), }, [].concat(...scanResults.map((nodeResult) => nodeResult.keys)))); return this.databaseAnalysisProvider.create(analysis); diff --git a/redisinsight/api/src/modules/database-analysis/entities/database-analysis.entity.ts b/redisinsight/api/src/modules/database-analysis/entities/database-analysis.entity.ts index 1874261ad7..37a8000c25 100644 --- a/redisinsight/api/src/modules/database-analysis/entities/database-analysis.entity.ts +++ b/redisinsight/api/src/modules/database-analysis/entities/database-analysis.entity.ts @@ -118,6 +118,17 @@ export class DatabaseAnalysisEntity { @Column({ nullable: true }) encryption: string; + @Column({ nullable: true, type: 'blob' }) + @Transform((object) => JSON.stringify(object), { toClassOnly: true }) + @Transform((str) => { + try { + return JSON.parse(str); + } catch (e) { + return undefined; + } + }, { toPlainOnly: true }) + recommendations: string; + @CreateDateColumn() @Index() @Expose() diff --git a/redisinsight/api/src/modules/database-analysis/models/database-analysis.ts b/redisinsight/api/src/modules/database-analysis/models/database-analysis.ts index 9579ab2013..80e9479074 100644 --- a/redisinsight/api/src/modules/database-analysis/models/database-analysis.ts +++ b/redisinsight/api/src/modules/database-analysis/models/database-analysis.ts @@ -6,6 +6,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { ScanFilter } from 'src/modules/database-analysis/models/scan-filter'; import { AnalysisProgress } from 'src/modules/database-analysis/models/analysis-progress'; import { SumGroup } from 'src/modules/database-analysis/models/sum-group'; +import { Recommendation } from 'src/modules/database-analysis/models/recommendation'; export class DatabaseAnalysis { @ApiProperty({ @@ -114,4 +115,13 @@ export class DatabaseAnalysis { @Expose() @Type(() => SumGroup) expirationGroups: SumGroup[]; + + @ApiProperty({ + description: 'Expiration groups', + isArray: true, + type: () => Recommendation, + }) + @Expose() + @Type(() => Recommendation) + recommendations: Recommendation[]; } diff --git a/redisinsight/api/src/modules/database-analysis/models/index.ts b/redisinsight/api/src/modules/database-analysis/models/index.ts index 6eb8a3c250..7e2256b782 100644 --- a/redisinsight/api/src/modules/database-analysis/models/index.ts +++ b/redisinsight/api/src/modules/database-analysis/models/index.ts @@ -6,3 +6,4 @@ export * from './simple-summary'; export * from './database-analysis'; export * from './short-database-analysis'; export * from './sum-group'; +export * from './recommendation'; diff --git a/redisinsight/api/src/modules/database-analysis/models/recommendation.ts b/redisinsight/api/src/modules/database-analysis/models/recommendation.ts new file mode 100644 index 0000000000..e3910bbfe1 --- /dev/null +++ b/redisinsight/api/src/modules/database-analysis/models/recommendation.ts @@ -0,0 +1,12 @@ +import { Expose } from 'class-transformer'; +import { ApiProperty } from '@nestjs/swagger'; + +export class Recommendation { + @ApiProperty({ + description: 'Recommendation name', + type: String, + example: 'luaScript', + }) + @Expose() + name: string; +} diff --git a/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.ts b/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.ts index 8731ba4f97..388e8b5c05 100644 --- a/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.ts +++ b/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.ts @@ -26,6 +26,7 @@ export class DatabaseAnalysisProvider { 'filter', 'progress', 'expirationGroups', + 'recommendations', ]; constructor( @@ -40,7 +41,11 @@ export class DatabaseAnalysisProvider { * @param analysis */ async create(analysis: Partial): Promise { - const entity = await this.repository.save(await this.encryptEntity(plainToClass(DatabaseAnalysisEntity, analysis))); + const entity = await this.repository.save( + await this.encryptEntity(plainToClass(DatabaseAnalysisEntity, { + ...analysis, + })), + ); // cleanup history and ignore error if any try { diff --git a/redisinsight/api/src/modules/db-recommendations/database-recommendations.module.ts b/redisinsight/api/src/modules/db-recommendations/database-recommendations.module.ts new file mode 100644 index 0000000000..097526deae --- /dev/null +++ b/redisinsight/api/src/modules/db-recommendations/database-recommendations.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; + +import { RecommendationsService } from './providers/database-recommendations.provider'; + +@Module({ + providers: [RecommendationsService], + exports: [RecommendationsService], +}) +export class DatabaseRecommendationsModule {} diff --git a/redisinsight/api/src/modules/db-recommendations/providers/database-recommendations.provider.ts b/redisinsight/api/src/modules/db-recommendations/providers/database-recommendations.provider.ts new file mode 100644 index 0000000000..9c931c7636 --- /dev/null +++ b/redisinsight/api/src/modules/db-recommendations/providers/database-recommendations.provider.ts @@ -0,0 +1,56 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { Cluster, Command } from 'ioredis'; +import { get, max } from 'lodash'; +import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; +import { DatabaseConnectionService } from 'src/modules/database/database-connection.service'; +import { convertRedisInfoReplyToObject } from 'src/utils'; +import { AppTool } from 'src/models'; + +const minNumberOfCachedScripts = 10; + +@Injectable() +export class RecommendationsService { + private logger = new Logger('DatabaseRecommendationsService'); + + constructor( + private readonly databaseConnectionService: DatabaseConnectionService, + ) {} + + public async getRecommendations( + clientOptions: IFindRedisClientInstanceByOptions, + ) { + const recommendations = []; + if (await this.getLuaScriptRecommendation(clientOptions)) { + recommendations.push({ name: 'luaScript' }); + } + + return recommendations; + } + + async getLuaScriptRecommendation( + clientOptions: IFindRedisClientInstanceByOptions, + ): Promise { + let nodes = []; + const client = await this.databaseConnectionService.createClient({ + databaseId: clientOptions.instanceId, + namespace: AppTool.Common, + }); + + if (client instanceof Cluster) { + nodes = client.nodes('master'); + } else { + nodes = [client]; + } + + const nodesNumbersOfCachedScripts = Promise.all(nodes.map(async (node) => { + const info = convertRedisInfoReplyToObject( + await node.sendCommand( + new Command('info', ['memory'], { replyEncoding: 'utf8' }), + ) as string, + ); + return get(info, 'memory.number_of_cached_scripts', {}); + })); + + return max(await nodesNumbersOfCachedScripts) > minNumberOfCachedScripts; + } +} diff --git a/redisinsight/ui/src/assets/img/code-changes.svg b/redisinsight/ui/src/assets/img/code-changes.svg new file mode 100644 index 0000000000..d421faa639 --- /dev/null +++ b/redisinsight/ui/src/assets/img/code-changes.svg @@ -0,0 +1,3 @@ + + + diff --git a/redisinsight/ui/src/assets/img/configuration-changes.svg b/redisinsight/ui/src/assets/img/configuration-changes.svg new file mode 100644 index 0000000000..337938737e --- /dev/null +++ b/redisinsight/ui/src/assets/img/configuration-changes.svg @@ -0,0 +1,3 @@ + + + diff --git a/redisinsight/ui/src/assets/img/upgrade.svg b/redisinsight/ui/src/assets/img/upgrade.svg new file mode 100644 index 0000000000..9bd84c72bc --- /dev/null +++ b/redisinsight/ui/src/assets/img/upgrade.svg @@ -0,0 +1,4 @@ + + + + diff --git a/redisinsight/ui/src/constants/dbAnalisisRecomendations.json b/redisinsight/ui/src/constants/dbAnalisisRecomendations.json new file mode 100644 index 0000000000..dea76553d4 --- /dev/null +++ b/redisinsight/ui/src/constants/dbAnalisisRecomendations.json @@ -0,0 +1,44 @@ +{ + "luaScript": { + "id": "luaScript", + "title":"Avoid dynamic Lua script", + "content": [ + { + "id": "1", + "type": "span", + "value": "Refrain from generating dynamic scripts, which can cause your Lua cache to grow and get out of control. Memory is consumed as scripts are loaded. If you have to use dynamic Lua scripts, then remember to track your Lua memory consumption and flush the cache periodically with a SCRIPT FLUSH, also do not hardcode and/or programmatically generate key names in your Lua scripts because it makes them useless in a clustered Redis setup. " + }, + { + "id": "2", + "type": "link", + "value": { + "href": "https://docs.redis.com/latest/ri/memory-optimizations/", + "name": "Read more" + } + } + ], + "badges": ["configuration_changes", "code_changes", "upgrade"] + }, + "1": { + "id": "1", + "title":"Avoid", + "content": [ + { + "id": "1", + "type": "paragraph", + "value": "some text as paragraph" + }, + { + "id": "2", + "type": "span", + "value": "some text as span" + }, + { + "id": "3", + "type": "image", + "value": "uiSrc/assets/img/welcome_bg_dark.jpg" + } + ], + "badges": ["configuration_changes", "code_changes"] + } +} \ No newline at end of file diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx new file mode 100644 index 0000000000..f0da01639c --- /dev/null +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx @@ -0,0 +1,117 @@ +import React from 'react' +import { useSelector } from 'react-redux' +import cx from 'classnames' +import { + EuiAccordion, + EuiPanel, + EuiTitle, + EuiText, + EuiTextColor, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiLink, + EuiImage, +} from '@elastic/eui' +import { dbAnalysisSelector } from 'uiSrc/slices/analytics/dbAnalysis' +import recommendationsContent from 'uiSrc/constants/dbAnalisisRecomendations.json' +import { ReactComponent as CodeIcon } from 'uiSrc/assets/img/code-changes.svg' +import { ReactComponent as ConfigurationIcon } from 'uiSrc/assets/img/configuration-changes.svg' +import { ReactComponent as UpgradeIcon } from 'uiSrc/assets/img/upgrade.svg' +import logo from 'uiSrc/assets/img/welcome_bg_dark.jpg' + +import styles from './styles.module.scss' + +const badgesContent = [ + { id: 'code_changes', icon: , name: 'Code Changes' }, + { id: 'configuration_changes', icon: , name: 'Configuration Changes' }, + { id: 'upgrade', icon: , name: 'Upgrade' }, +] + +const renderBadges = (badges) => ( + + {badgesContent.map(({ id, icon, name }) => (badges.indexOf(id) === -1 + ? + : ( + +
+ {icon} + {name} +
+
+ )))} +
+) + +const parseContent = ({ type, value }: { type: string, value: any }) => { + switch (type) { + case 'paragraph': + return {value} + case 'span': + return {value} + case 'link': + return {value.name} + case 'image': + return + default: + return value + } +} + +const Recommendations = () => { + const { data, loading } = useSelector(dbAnalysisSelector) + const { recommendations = [] } = data ?? {} + + if (loading) { + return ( +
+ + Uploading recommendations... +
+ ) + } + + if (!recommendations.length) { + return ( +
+ No Recommendations at the moment +
+ ) + } + + return ( +
+ +

{`RECOMMENDATIONS (${recommendations.length}):`}

+
+ {recommendations.map(({ name }) => { + const { id, title = '', content = '', badges } = recommendationsContent[name] + return ( +
+ + + {content.map((item: { type: string, value: any, id: string }) => + ( + + {parseContent(item)} + + ))} + + +
+ {renderBadges(badges)} +
+
+ ) + })} +
+ ) +} + +export default Recommendations diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/index.ts b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/index.ts new file mode 100644 index 0000000000..5635179102 --- /dev/null +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/index.ts @@ -0,0 +1,3 @@ +import Recommendations from './Recommendations' + +export default Recommendations diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss new file mode 100644 index 0000000000..fd40b23db0 --- /dev/null +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss @@ -0,0 +1,97 @@ +@import "@elastic/eui/src/global_styling/mixins/helpers"; +@import "@elastic/eui/src/components/table/mixins"; +@import "@elastic/eui/src/global_styling/index"; + +.wrapper { + @include euiScrollBar; + overflow-y: auto; + overflow-x: hidden; + max-height: 100%; +} + +.title { + margin: 32px 0 24px; +} + +.container { + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; + height: 100%; + + .emptyMessage { + font: normal normal 500 16px/19px Graphik, sans-serif; + } + + .spinner { + width: 38px; + height: 38px; + margin-bottom: 12px; + border-top-color: var(--euiColorPrimary) !important; + } + + .spinnerText { + color: var(--htmlColor); + } +} + +.recommendation { + border-radius: 8px; + border: 1px solid #323232; + background-color: var(--euiColorLightestShade); + margin-bottom: 6px; + padding: 18px; + + .accordion { + margin-bottom: 18px; + } + + .accordionContent { + padding: 18px 0 17px !important; + } + + :global(.euiAccordion__triggerWrapper) { + background-color: transparent; + } + + :global(.euiPanel.euiPanel--subdued) { + background-color: transparent; + } + + :global(.euiAccordion.euiAccordion-isOpen .euiAccordion__triggerWrapper) { + border-bottom: none; + } + + :global(.euiAccordion .euiAccordion__triggerWrapper) { + border-bottom: 1px solid var(--separatorColor); + } + + .accordionBtn { + font: normal normal 500 16px/19px Graphik, sans-serif; + padding-bottom: 22px; + } + + .badgesContainer { + margin-right: 48px; + + .badge { + margin: 18px; + + .badgeIcon { + margin-right: 14px; + fill: var(--badgeIconColor); + } + + .badgeWrapper { + display: flex; + align-items: center; + } + } + } + + .span { + display: inline; + font: normal normal normal 14px/24px Graphik, sans-serif; + } +} \ No newline at end of file diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/sub-tabs/DatabaseAnalysisTabs.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/sub-tabs/DatabaseAnalysisTabs.tsx new file mode 100644 index 0000000000..ca609f19c7 --- /dev/null +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/sub-tabs/DatabaseAnalysisTabs.tsx @@ -0,0 +1,69 @@ +import React, { useMemo } from 'react' +import { EuiTab, EuiTabs } from '@elastic/eui' +import { isNull } from 'lodash' +import { useDispatch, useSelector } from 'react-redux' +import { EmptyMessage } from 'uiSrc/pages/databaseAnalysis/constants' +import { EmptyAnalysisMessage } from 'uiSrc/pages/databaseAnalysis/components' +import { setDatabaseAnalysisViewTab, dbAnalysisViewTabSelector } from 'uiSrc/slices/analytics/dbAnalysis' +import { DatabaseAnalysisViewTab } from 'uiSrc/slices/interfaces/analytics' +import { ShortDatabaseAnalysis, DatabaseAnalysis } from 'apiSrc/modules/database-analysis/models' + +import { databaseAnalysisTabs } from './constants' +import styles from './styles.module.scss' + +interface Props { + loading: boolean + reports: ShortDatabaseAnalysis[] + data: DatabaseAnalysis +} + +const DatabaseAnalysisTabs = (props: Props) => { + const { loading, reports, data } = props + + const viewTab = useSelector(dbAnalysisViewTabSelector) + + const dispatch = useDispatch() + + const selectedTabContent = useMemo(() => databaseAnalysisTabs.find((tab) => tab.id === viewTab)?.content, [viewTab]) + + const onSelectedTabChanged = (id: DatabaseAnalysisViewTab) => { + dispatch(setDatabaseAnalysisViewTab(id)) + } + + const renderTabs = () => ( + databaseAnalysisTabs.map(({ id, name }) => ( + onSelectedTabChanged(id)} + isSelected={id === viewTab} + > + {name} + + ))) + + if (!loading && !reports.length) { + return ( +
+ +
+ ) + } + if (!loading && !!reports.length && isNull(data?.totalKeys)) { + return ( +
+ +
+ ) + } + + return ( + <> + {renderTabs()} +
+ {selectedTabContent} +
+ + ) +} + +export default DatabaseAnalysisTabs diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/sub-tabs/constants.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/sub-tabs/constants.tsx new file mode 100644 index 0000000000..58b6df423a --- /dev/null +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/sub-tabs/constants.tsx @@ -0,0 +1,23 @@ +import React, { ReactNode } from 'react' +import { DatabaseAnalysisViewTab } from 'uiSrc/slices/interfaces/analytics' +import AnalysisDataView from '../analysis-data-view' +import Recommendations from '../recommendations-view' + +interface DatabaseAnalysisTabs { + id: DatabaseAnalysisViewTab, + name: string, + content: ReactNode +} + +export const databaseAnalysisTabs: DatabaseAnalysisTabs[] = [ + { + id: DatabaseAnalysisViewTab.DataSummary, + name: 'Data Summary', + content: + }, + { + id: DatabaseAnalysisViewTab.Recommendations, + name: 'Recommendations', + content: + }, +] diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/sub-tabs/index.ts b/redisinsight/ui/src/pages/databaseAnalysis/components/sub-tabs/index.ts new file mode 100644 index 0000000000..dbfeb488e6 --- /dev/null +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/sub-tabs/index.ts @@ -0,0 +1,3 @@ +import DatabaseAnalysisTabs from './DatabaseAnalysisTabs' + +export default DatabaseAnalysisTabs diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/sub-tabs/styles.module.scss b/redisinsight/ui/src/pages/databaseAnalysis/components/sub-tabs/styles.module.scss new file mode 100644 index 0000000000..a8b2530f68 --- /dev/null +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/sub-tabs/styles.module.scss @@ -0,0 +1,11 @@ +@import "@elastic/eui/src/global_styling/mixins/helpers"; +@import "@elastic/eui/src/components/table/mixins"; +@import "@elastic/eui/src/global_styling/index"; + +.container { + height: calc(100% - 126px); +} + +.emptyMessageWrapper { + height: calc(100% - 96px); +} \ No newline at end of file From 4cce0eea2852702fbd9cf897824d8d6a9e6923b6 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 10 Nov 2022 09:24:13 +0400 Subject: [PATCH 002/108] #RI-3527-add luascript recommendation --- .../databaseAnalysis/DatabaseAnalysisPage.tsx | 8 +++-- .../analysis-data-view/AnalysisDataView.tsx | 30 +++++++------------ .../analysis-data-view/styles.module.scss | 2 +- .../empty-analysis-message/styles.module.scss | 3 +- .../ui/src/slices/analytics/dbAnalysis.ts | 8 ++++- .../ui/src/slices/interfaces/analytics.ts | 6 ++++ .../themes/dark_theme/_dark_theme.lazy.scss | 3 ++ .../themes/dark_theme/_theme_color.scss | 3 ++ .../themes/light_theme/_light_theme.lazy.scss | 3 ++ .../themes/light_theme/_theme_color.scss | 3 ++ 10 files changed, 44 insertions(+), 25 deletions(-) diff --git a/redisinsight/ui/src/pages/databaseAnalysis/DatabaseAnalysisPage.tsx b/redisinsight/ui/src/pages/databaseAnalysis/DatabaseAnalysisPage.tsx index 61cb5a42ed..2dc4d1c0cc 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/DatabaseAnalysisPage.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/DatabaseAnalysisPage.tsx @@ -17,7 +17,7 @@ import { sendPageViewTelemetry, sendEventTelemetry, TelemetryPageView, Telemetry import { formatLongName, getDbIndex, setTitle } from 'uiSrc/utils' import Header from './components/header' -import AnalysisDataView from './components/analysis-data-view' +import DatabaseAnalysisTabs from './components/sub-tabs' import styles from './styles.module.scss' const DatabaseAnalysisPage = () => { @@ -89,7 +89,11 @@ const DatabaseAnalysisPage = () => { progress={data?.progress} analysisLoading={analysisLoading} /> - + ) } diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/analysis-data-view/AnalysisDataView.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/analysis-data-view/AnalysisDataView.tsx index 1c6e84cf7f..00aedda093 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/analysis-data-view/AnalysisDataView.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/analysis-data-view/AnalysisDataView.tsx @@ -1,9 +1,10 @@ import React, { useEffect, useState } from 'react' +import { useSelector } from 'react-redux' + import cx from 'classnames' -import { isNull } from 'lodash' import { useParams } from 'react-router-dom' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import { Nullable } from 'uiSrc/utils' +import { dbAnalysisSelector, dbAnalysisReportsSelector } from 'uiSrc/slices/analytics/dbAnalysis' import { DEFAULT_EXTRAPOLATION, EmptyMessage, SectionName } from 'uiSrc/pages/databaseAnalysis/constants' import { TopKeys, @@ -12,18 +13,14 @@ import { SummaryPerData, ExpirationGroupsView } from 'uiSrc/pages/databaseAnalysis/components' -import { ShortDatabaseAnalysis, DatabaseAnalysis } from 'apiSrc/modules/database-analysis/models' +// import { ShortDatabaseAnalysis, DatabaseAnalysis } from 'apiSrc/modules/database-analysis/models' import styles from './styles.module.scss' -export interface Props { - data: Nullable - reports: ShortDatabaseAnalysis[] - loading: boolean -} +const AnalysisDataView = () => { + const { loading, data } = useSelector(dbAnalysisSelector) + const { data: reports } = useSelector(dbAnalysisReportsSelector) -const AnalysisDataView = (props: Props) => { - const { loading, reports = [], data } = props const [extrapolation, setExtrapolation] = useState(DEFAULT_EXTRAPOLATION) const { instanceId } = useParams<{ instanceId: string }>() @@ -46,17 +43,12 @@ const AnalysisDataView = (props: Props) => { }) } + if (!loading && !!reports.length && data?.totalKeys?.total === 0) { + return () + } + return ( <> - {!loading && !reports.length && ( - - )} - {!loading && !!reports.length && data?.totalKeys?.total === 0 && ( - - )} - {!loading && !!reports.length && isNull(data?.totalKeys) && ( - - )}
) => { state.history.showNoExpiryGroup = payload }, + setDatabaseAnalysisViewTab: (state, { payload }: PayloadAction) => { + state.selectedViewTab = payload + }, } }) export const dbAnalysisSelector = (state: RootState) => state.analytics.databaseAnalysis export const dbAnalysisReportsSelector = (state: RootState) => state.analytics.databaseAnalysis.history +export const dbAnalysisViewTabSelector = (state: RootState) => state.analytics.databaseAnalysis.selectedViewTab export const { setDatabaseAnalysisInitialState, @@ -71,6 +76,7 @@ export const { loadDBAnalysisReportsError, setSelectedAnalysisId, setShowNoExpiryGroup, + setDatabaseAnalysisViewTab, } = databaseAnalysisSlice.actions // The reducer diff --git a/redisinsight/ui/src/slices/interfaces/analytics.ts b/redisinsight/ui/src/slices/interfaces/analytics.ts index a28cddd868..2e5b4b4b68 100644 --- a/redisinsight/ui/src/slices/interfaces/analytics.ts +++ b/redisinsight/ui/src/slices/interfaces/analytics.ts @@ -23,6 +23,7 @@ export interface StateDatabaseAnalysis { loading: boolean error: string data: Nullable + selectedViewTab: DatabaseAnalysisViewTab history: { loading: boolean error: string @@ -41,3 +42,8 @@ export enum AnalyticsViewTab { DatabaseAnalysis = 'DatabaseAnalysis', SlowLog = 'SlowLog', } + +export enum DatabaseAnalysisViewTab { + DataSummary = 'DataSummary', + Recommendations = 'Recommendations', +} diff --git a/redisinsight/ui/src/styles/themes/dark_theme/_dark_theme.lazy.scss b/redisinsight/ui/src/styles/themes/dark_theme/_dark_theme.lazy.scss index 2dee6e4865..39a11d6eeb 100644 --- a/redisinsight/ui/src/styles/themes/dark_theme/_dark_theme.lazy.scss +++ b/redisinsight/ui/src/styles/themes/dark_theme/_dark_theme.lazy.scss @@ -175,4 +175,7 @@ // Pub/Sub --pubSubClientsBadge: #{$pubSubClientsBadge}; + + // Database analysis + --badgeIconColor: #{$badgeIconColor}; } diff --git a/redisinsight/ui/src/styles/themes/dark_theme/_theme_color.scss b/redisinsight/ui/src/styles/themes/dark_theme/_theme_color.scss index bcfb32c4f2..7d7a086ee5 100644 --- a/redisinsight/ui/src/styles/themes/dark_theme/_theme_color.scss +++ b/redisinsight/ui/src/styles/themes/dark_theme/_theme_color.scss @@ -135,3 +135,6 @@ $wbActiveIconColor: #8ba2ff; // PubSub $pubSubClientsBadge: #008000; + +// Database analysis +$badgeIconColor : #D8AB52; diff --git a/redisinsight/ui/src/styles/themes/light_theme/_light_theme.lazy.scss b/redisinsight/ui/src/styles/themes/light_theme/_light_theme.lazy.scss index a9fbfa450e..b413a7a29a 100644 --- a/redisinsight/ui/src/styles/themes/light_theme/_light_theme.lazy.scss +++ b/redisinsight/ui/src/styles/themes/light_theme/_light_theme.lazy.scss @@ -177,4 +177,7 @@ // Pub/Sub --pubSubClientsBadge: #{$pubSubClientsBadge}; + + // Database analysis + --badgeIconColor: #{$badgeIconColor}; } diff --git a/redisinsight/ui/src/styles/themes/light_theme/_theme_color.scss b/redisinsight/ui/src/styles/themes/light_theme/_theme_color.scss index e925a43fe6..c7da8d8e30 100644 --- a/redisinsight/ui/src/styles/themes/light_theme/_theme_color.scss +++ b/redisinsight/ui/src/styles/themes/light_theme/_theme_color.scss @@ -132,3 +132,6 @@ $wbActiveIconColor: #3163D8; // Pub/Sub $pubSubClientsBadge: #b5cea8; + +// Database analysis +$badgeIconColor : #415681; From 74cbda3db968de4a6f43ab920bf8f504d66bdef6 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 10 Nov 2022 09:55:02 +0400 Subject: [PATCH 003/108] #RI-3527-remove deprecated code --- ...ns.json => dbAnalysisRecommendations.json} | 26 +--------- .../analysis-data-view/AnalysisDataView.tsx | 1 - .../recommendations-view/Recommendations.tsx | 48 +----------------- .../components/recommendations-view/utils.tsx | 49 +++++++++++++++++++ .../components/sub-tabs/styles.module.scss | 2 +- 5 files changed, 54 insertions(+), 72 deletions(-) rename redisinsight/ui/src/constants/{dbAnalisisRecomendations.json => dbAnalysisRecommendations.json} (61%) create mode 100644 redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx diff --git a/redisinsight/ui/src/constants/dbAnalisisRecomendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json similarity index 61% rename from redisinsight/ui/src/constants/dbAnalisisRecomendations.json rename to redisinsight/ui/src/constants/dbAnalysisRecommendations.json index dea76553d4..4a63e8d3c9 100644 --- a/redisinsight/ui/src/constants/dbAnalisisRecomendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -17,28 +17,6 @@ } } ], - "badges": ["configuration_changes", "code_changes", "upgrade"] - }, - "1": { - "id": "1", - "title":"Avoid", - "content": [ - { - "id": "1", - "type": "paragraph", - "value": "some text as paragraph" - }, - { - "id": "2", - "type": "span", - "value": "some text as span" - }, - { - "id": "3", - "type": "image", - "value": "uiSrc/assets/img/welcome_bg_dark.jpg" - } - ], - "badges": ["configuration_changes", "code_changes"] + "badges": ["upgrade"] } -} \ No newline at end of file +} diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/analysis-data-view/AnalysisDataView.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/analysis-data-view/AnalysisDataView.tsx index 00aedda093..804cfaa37e 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/analysis-data-view/AnalysisDataView.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/analysis-data-view/AnalysisDataView.tsx @@ -13,7 +13,6 @@ import { SummaryPerData, ExpirationGroupsView } from 'uiSrc/pages/databaseAnalysis/components' -// import { ShortDatabaseAnalysis, DatabaseAnalysis } from 'apiSrc/modules/database-analysis/models' import styles from './styles.module.scss' diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx index f0da01639c..846b81a568 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx @@ -6,58 +6,14 @@ import { EuiPanel, EuiTitle, EuiText, - EuiTextColor, - EuiFlexGroup, - EuiFlexItem, EuiLoadingSpinner, - EuiLink, - EuiImage, } from '@elastic/eui' import { dbAnalysisSelector } from 'uiSrc/slices/analytics/dbAnalysis' -import recommendationsContent from 'uiSrc/constants/dbAnalisisRecomendations.json' -import { ReactComponent as CodeIcon } from 'uiSrc/assets/img/code-changes.svg' -import { ReactComponent as ConfigurationIcon } from 'uiSrc/assets/img/configuration-changes.svg' -import { ReactComponent as UpgradeIcon } from 'uiSrc/assets/img/upgrade.svg' -import logo from 'uiSrc/assets/img/welcome_bg_dark.jpg' +import recommendationsContent from 'uiSrc/constants/dbAnalysisRecommendations.json' +import { parseContent, renderBadges } from './utils' import styles from './styles.module.scss' -const badgesContent = [ - { id: 'code_changes', icon: , name: 'Code Changes' }, - { id: 'configuration_changes', icon: , name: 'Configuration Changes' }, - { id: 'upgrade', icon: , name: 'Upgrade' }, -] - -const renderBadges = (badges) => ( - - {badgesContent.map(({ id, icon, name }) => (badges.indexOf(id) === -1 - ? - : ( - -
- {icon} - {name} -
-
- )))} -
-) - -const parseContent = ({ type, value }: { type: string, value: any }) => { - switch (type) { - case 'paragraph': - return {value} - case 'span': - return {value} - case 'link': - return {value.name} - case 'image': - return - default: - return value - } -} - const Recommendations = () => { const { data, loading } = useSelector(dbAnalysisSelector) const { recommendations = [] } = data ?? {} diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx new file mode 100644 index 0000000000..fb0ea72339 --- /dev/null +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx @@ -0,0 +1,49 @@ +import React from 'react' +import { + EuiTextColor, + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiImage, +} from '@elastic/eui' +import { ReactComponent as CodeIcon } from 'uiSrc/assets/img/code-changes.svg' +import { ReactComponent as ConfigurationIcon } from 'uiSrc/assets/img/configuration-changes.svg' +import { ReactComponent as UpgradeIcon } from 'uiSrc/assets/img/upgrade.svg' + +import styles from './styles.module.scss' + +const badgesContent = [ + { id: 'code_changes', icon: , name: 'Code Changes' }, + { id: 'configuration_changes', icon: , name: 'Configuration Changes' }, + { id: 'upgrade', icon: , name: 'Upgrade' }, +] + +export const renderBadges = (badges: string[]) => ( + + {badgesContent.map(({ id, icon, name }) => (badges.indexOf(id) === -1 + ? + : ( + +
+ {icon} + {name} +
+
+ )))} +
+) + +export const parseContent = ({ type, value }: { type: string, value: any }) => { + switch (type) { + case 'paragraph': + return {value} + case 'span': + return {value} + case 'link': + return {value.name} + case 'image': + return + default: + return value + } +} diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/sub-tabs/styles.module.scss b/redisinsight/ui/src/pages/databaseAnalysis/components/sub-tabs/styles.module.scss index a8b2530f68..7e288fa094 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/sub-tabs/styles.module.scss +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/sub-tabs/styles.module.scss @@ -8,4 +8,4 @@ .emptyMessageWrapper { height: calc(100% - 96px); -} \ No newline at end of file +} From af415056578e2f7e8465e2f030c04e4041b1de31 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 10 Nov 2022 10:01:11 +0400 Subject: [PATCH 004/108] #RI-3527-remove deprecated code --- .../components/recommendations-view/styles.module.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss index fd40b23db0..dab0d51d1c 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss @@ -94,4 +94,4 @@ display: inline; font: normal normal normal 14px/24px Graphik, sans-serif; } -} \ No newline at end of file +} From 5d018b4ab0772a622c0dfd98ac9e3998f7e61af3 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 10 Nov 2022 10:31:55 +0400 Subject: [PATCH 005/108] #RI-3527-fix recommendation be --- .../database-analysis/database-analysis.service.ts | 8 ++++---- .../entities/database-analysis.entity.ts | 10 ++-------- .../database-analysis/models/database-analysis.ts | 2 +- .../providers/database-analysis.provider.ts | 2 +- .../providers/database-recommendations.provider.ts | 12 +++++++++--- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts index ac16c8f4ce..86a10c6a99 100644 --- a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts +++ b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts @@ -17,10 +17,10 @@ export class DatabaseAnalysisService { constructor( private readonly databaseConnectionService: DatabaseConnectionService, - private recommendationsService: RecommendationsService, - private analyzer: DatabaseAnalyzer, - private databaseAnalysisProvider: DatabaseAnalysisProvider, - private scanner: KeysScanner, + private readonly recommendationsService: RecommendationsService, + private readonly analyzer: DatabaseAnalyzer, + private readonly databaseAnalysisProvider: DatabaseAnalysisProvider, + private readonly scanner: KeysScanner, ) {} /** diff --git a/redisinsight/api/src/modules/database-analysis/entities/database-analysis.entity.ts b/redisinsight/api/src/modules/database-analysis/entities/database-analysis.entity.ts index 37a8000c25..936b8bade3 100644 --- a/redisinsight/api/src/modules/database-analysis/entities/database-analysis.entity.ts +++ b/redisinsight/api/src/modules/database-analysis/entities/database-analysis.entity.ts @@ -119,14 +119,8 @@ export class DatabaseAnalysisEntity { encryption: string; @Column({ nullable: true, type: 'blob' }) - @Transform((object) => JSON.stringify(object), { toClassOnly: true }) - @Transform((str) => { - try { - return JSON.parse(str); - } catch (e) { - return undefined; - } - }, { toPlainOnly: true }) + @DataAsJsonString() + @Expose() recommendations: string; @CreateDateColumn() diff --git a/redisinsight/api/src/modules/database-analysis/models/database-analysis.ts b/redisinsight/api/src/modules/database-analysis/models/database-analysis.ts index 80e9479074..2ae523b071 100644 --- a/redisinsight/api/src/modules/database-analysis/models/database-analysis.ts +++ b/redisinsight/api/src/modules/database-analysis/models/database-analysis.ts @@ -117,7 +117,7 @@ export class DatabaseAnalysis { expirationGroups: SumGroup[]; @ApiProperty({ - description: 'Expiration groups', + description: 'Recommendations', isArray: true, type: () => Recommendation, }) diff --git a/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.ts b/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.ts index 388e8b5c05..c2fef9bec2 100644 --- a/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.ts +++ b/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.ts @@ -68,7 +68,7 @@ export class DatabaseAnalysisProvider { this.logger.error(`Database analysis with id:${id} was not Found`); throw new NotFoundException(ERROR_MESSAGES.DATABASE_ANALYSIS_NOT_FOUND); } - + console.log(classToClass(DatabaseAnalysis, await this.decryptEntity(entity, true))) return classToClass(DatabaseAnalysis, await this.decryptEntity(entity, true)); } diff --git a/redisinsight/api/src/modules/db-recommendations/providers/database-recommendations.provider.ts b/redisinsight/api/src/modules/db-recommendations/providers/database-recommendations.provider.ts index 9c931c7636..7e997c0e18 100644 --- a/redisinsight/api/src/modules/db-recommendations/providers/database-recommendations.provider.ts +++ b/redisinsight/api/src/modules/db-recommendations/providers/database-recommendations.provider.ts @@ -1,4 +1,4 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { Cluster, Command } from 'ioredis'; import { get, max } from 'lodash'; import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; @@ -10,12 +10,14 @@ const minNumberOfCachedScripts = 10; @Injectable() export class RecommendationsService { - private logger = new Logger('DatabaseRecommendationsService'); - constructor( private readonly databaseConnectionService: DatabaseConnectionService, ) {} + /** + * Get database recommendations + * @param clientOptions + */ public async getRecommendations( clientOptions: IFindRedisClientInstanceByOptions, ) { @@ -27,6 +29,10 @@ export class RecommendationsService { return recommendations; } + /** + * Check lua script recommendation + * @param clientOptions + */ async getLuaScriptRecommendation( clientOptions: IFindRedisClientInstanceByOptions, ): Promise { From d85f125bfe57cabe6d20e311bb3baa60411aa427 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Fri, 11 Nov 2022 15:45:57 +0400 Subject: [PATCH 006/108] #RI-3527-add tests, implement demo improvements --- .../constants/dbAnalysisRecommendations.json | 2 +- .../DatabaseAnalysisPage.spec.tsx | 11 -- .../databaseAnalysis/DatabaseAnalysisPage.tsx | 2 +- .../AnalysisDataView.spec.tsx | 160 ++++++++++++++---- .../analysis-data-view/AnalysisDataView.tsx | 2 +- .../DatabaseAnalysisTabs.spec.tsx | 83 +++++++++ .../DatabaseAnalysisTabs.tsx | 18 +- .../{sub-tabs => data-nav-tabs}/constants.tsx | 6 +- .../{sub-tabs => data-nav-tabs}/index.ts | 0 .../styles.module.scss | 0 .../Recommendations.spec.tsx | 135 +++++++++++++++ .../recommendations-view/Recommendations.tsx | 20 +-- .../recommendations-view/styles.module.scss | 33 ++-- .../components/recommendations-view/utils.tsx | 13 +- .../components/top-namespace/TopNamespace.tsx | 5 + 15 files changed, 395 insertions(+), 95 deletions(-) create mode 100644 redisinsight/ui/src/pages/databaseAnalysis/components/data-nav-tabs/DatabaseAnalysisTabs.spec.tsx rename redisinsight/ui/src/pages/databaseAnalysis/components/{sub-tabs => data-nav-tabs}/DatabaseAnalysisTabs.tsx (76%) rename redisinsight/ui/src/pages/databaseAnalysis/components/{sub-tabs => data-nav-tabs}/constants.tsx (77%) rename redisinsight/ui/src/pages/databaseAnalysis/components/{sub-tabs => data-nav-tabs}/index.ts (100%) rename redisinsight/ui/src/pages/databaseAnalysis/components/{sub-tabs => data-nav-tabs}/styles.module.scss (100%) create mode 100644 redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index 4a63e8d3c9..7cde7e7a48 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -17,6 +17,6 @@ } } ], - "badges": ["upgrade"] + "badges": ["code_changes"] } } diff --git a/redisinsight/ui/src/pages/databaseAnalysis/DatabaseAnalysisPage.spec.tsx b/redisinsight/ui/src/pages/databaseAnalysis/DatabaseAnalysisPage.spec.tsx index d7669a613b..6e066f47a8 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/DatabaseAnalysisPage.spec.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/DatabaseAnalysisPage.spec.tsx @@ -7,17 +7,6 @@ import DatabaseAnalysisPage from './DatabaseAnalysisPage' jest.mock('uiSrc/slices/analytics/dbAnalysis', () => ({ ...jest.requireActual('uiSrc/slices/analytics/dbAnalysis'), fetchDBAnalysisReportsHistory: jest.fn(), - dbAnalysisSelector: jest.fn().mockReturnValue({ - loading: false, - error: '', - data: null, - history: { - loading: false, - error: '', - data: [], - selectedAnalysis: null, - } - }), })) /** diff --git a/redisinsight/ui/src/pages/databaseAnalysis/DatabaseAnalysisPage.tsx b/redisinsight/ui/src/pages/databaseAnalysis/DatabaseAnalysisPage.tsx index 2dc4d1c0cc..1580584ad0 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/DatabaseAnalysisPage.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/DatabaseAnalysisPage.tsx @@ -17,7 +17,7 @@ import { sendPageViewTelemetry, sendEventTelemetry, TelemetryPageView, Telemetry import { formatLongName, getDbIndex, setTitle } from 'uiSrc/utils' import Header from './components/header' -import DatabaseAnalysisTabs from './components/sub-tabs' +import DatabaseAnalysisTabs from './components/data-nav-tabs' import styles from './styles.module.scss' const DatabaseAnalysisPage = () => { diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/analysis-data-view/AnalysisDataView.spec.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/analysis-data-view/AnalysisDataView.spec.tsx index a463a14a23..552eb718c6 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/analysis-data-view/AnalysisDataView.spec.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/analysis-data-view/AnalysisDataView.spec.tsx @@ -1,21 +1,44 @@ import React from 'react' -import { instance, mock } from 'ts-mockito' import { MOCK_ANALYSIS_REPORT_DATA } from 'uiSrc/mocks/data/analysis' import { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/analytics/clusterDetailsHandlers' +import { dbAnalysisSelector, dbAnalysisReportsSelector } from 'uiSrc/slices/analytics/dbAnalysis' import { SectionName } from 'uiSrc/pages/databaseAnalysis' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { formatBytes, getGroupTypeDisplay } from 'uiSrc/utils' import { numberWithSpaces } from 'uiSrc/utils/numbers' import { fireEvent, render, screen, within } from 'uiSrc/utils/test-utils' -import AnalysisDataView, { Props } from './AnalysisDataView' +import AnalysisDataView from './AnalysisDataView' jest.mock('uiSrc/telemetry', () => ({ ...jest.requireActual('uiSrc/telemetry'), sendEventTelemetry: jest.fn(), })) -const mockedProps = mock() +const mockdbAnalysisSelector = jest.requireActual('uiSrc/slices/analytics/dbAnalysis') +const mockdbAnalysisReportsSelector = jest.requireActual('uiSrc/slices/analytics/dbAnalysis') + +jest.mock('uiSrc/slices/analytics/dbAnalysis', () => ({ + ...jest.requireActual('uiSrc/slices/analytics/dbAnalysis'), + dbAnalysisSelector: jest.fn().mockReturnValue({ + loading: false, + error: '', + data: null, + history: { + loading: false, + error: '', + data: [], + selectedAnalysis: null, + } + }), + dbAnalysisReportsSelector: jest.fn().mockReturnValue({ + loading: false, + error: '', + data: [], + selectedAnalysis: null, + }), +})) + const mockReports = [ { id: MOCK_ANALYSIS_REPORT_DATA.id, @@ -34,34 +57,42 @@ const extrapolateResultsId = 'extrapolate-results' describe('AnalysisDataView', () => { it('should render', () => { - expect(render()).toBeTruthy() + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + })) + + expect(render()).toBeTruthy() }) it('should render only table when loading="true"', () => { - render() + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + loading: true + })) + + render() expect(screen.queryByTestId('empty-analysis-no-reports')).not.toBeInTheDocument() expect(screen.queryByTestId('empty-analysis-no-keys')).not.toBeInTheDocument() }) it('should render empty-data-message-no-keys when total=0 ', () => { - const mockedData = { totalKeys: { total: 0 } } + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: { totalKeys: { total: 0 } }, + })); + (dbAnalysisReportsSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisReportsSelector, + data: mockReports, + })) + render( - + ) expect(screen.queryByTestId('empty-analysis-no-reports')).not.toBeInTheDocument() expect(screen.queryByTestId('empty-analysis-no-keys')).toBeInTheDocument() }) - - it('should render empty-data-message-no-reports when reports=[] ', () => { - render( - - ) - - expect(screen.queryByTestId('empty-analysis-no-reports')).toBeInTheDocument() - expect(screen.queryByTestId('empty-analysis-no-keys')).not.toBeInTheDocument() - }) }) /** @@ -78,9 +109,18 @@ describe('AnalysisDataView', () => { scanned: 40, processed: 40 } - } + }; + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: mockedData, + })); + (dbAnalysisReportsSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisReportsSelector, + data: mockReports, + })) + render( - + ) expect(screen.getByTestId('total-memory-value')).toHaveTextContent(`~${formatBytes(mockedData.totalMemory.total * 2, 3)}`) @@ -111,9 +151,17 @@ describe('AnalysisDataView', () => { scanned: 40, processed: 40 } - } + }; + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: mockedData, + })); + (dbAnalysisReportsSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisReportsSelector, + data: mockReports, + })) render( - + ) fireEvent.click(within(screen.getByTestId(summaryContainerId)).getByTestId(extrapolateResultsId)) @@ -146,9 +194,17 @@ describe('AnalysisDataView', () => { scanned: 40, processed: 40 } - } + }; + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: mockedData, + })); + (dbAnalysisReportsSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisReportsSelector, + data: mockReports, + })) render( - + ) const expirationGroup = mockedData.expirationGroups[1] @@ -165,9 +221,17 @@ describe('AnalysisDataView', () => { scanned: 40, processed: 40 } - } + }; + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: mockedData, + })); + (dbAnalysisReportsSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisReportsSelector, + data: mockReports, + })) render( - + ) fireEvent.click(within(screen.getByTestId(analyticsTTLContainerId)).getByTestId(extrapolateResultsId)) @@ -185,9 +249,17 @@ describe('AnalysisDataView', () => { scanned: 40, processed: 40 } - } + }; + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: mockedData, + })); + (dbAnalysisReportsSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisReportsSelector, + data: mockReports, + })) render( - + ) const nspTopKeyItem = mockedData.topKeysNsp[0] @@ -206,9 +278,17 @@ describe('AnalysisDataView', () => { scanned: 40, processed: 40 } - } + }; + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: mockedData, + })); + (dbAnalysisReportsSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisReportsSelector, + data: mockReports, + })) render( - + ) fireEvent.click(within(screen.getByTestId(topNameSpacesContainerId)).getByTestId(extrapolateResultsId)) @@ -228,9 +308,17 @@ describe('AnalysisDataView', () => { scanned: 10000, processed: 80 } - } + }; + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: mockedData, + })); + (dbAnalysisReportsSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisReportsSelector, + data: mockReports, + })) render( - + ) expect(screen.queryByTestId(extrapolateResultsId)).not.toBeInTheDocument() @@ -259,12 +347,20 @@ describe('AnalysisDataView', () => { scanned: 10000, processed: 40 } - } + }; + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: mockedData, + })); + (dbAnalysisReportsSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisReportsSelector, + data: mockReports, + })) const sendEventTelemetryMock = jest.fn() sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) render( - + ) const clickAndCheckTelemetry = (el: HTMLInputElement, section: SectionName) => { diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/analysis-data-view/AnalysisDataView.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/analysis-data-view/AnalysisDataView.tsx index 804cfaa37e..b4b0403a87 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/analysis-data-view/AnalysisDataView.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/analysis-data-view/AnalysisDataView.tsx @@ -42,7 +42,7 @@ const AnalysisDataView = () => { }) } - if (!loading && !!reports.length && data?.totalKeys?.total === 0) { + if (!loading && !!reports?.length && data?.totalKeys?.total === 0) { return () } diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/data-nav-tabs/DatabaseAnalysisTabs.spec.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/data-nav-tabs/DatabaseAnalysisTabs.spec.tsx new file mode 100644 index 0000000000..18dd4fceea --- /dev/null +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/data-nav-tabs/DatabaseAnalysisTabs.spec.tsx @@ -0,0 +1,83 @@ +import React from 'react' +import { cloneDeep } from 'lodash' +import { instance, mock } from 'ts-mockito' +import { MOCK_ANALYSIS_REPORT_DATA } from 'uiSrc/mocks/data/analysis' +import { render, screen, mockedStore, cleanup, fireEvent } from 'uiSrc/utils/test-utils' +import { DatabaseAnalysisViewTab } from 'uiSrc/slices/interfaces/analytics' +import { setDatabaseAnalysisViewTab } from 'uiSrc/slices/analytics/dbAnalysis' + +import DatabaseAnalysisTabs, { Props } from './DatabaseAnalysisTabs' + +const mockedProps = mock() + +const mockReports = [ + { + id: MOCK_ANALYSIS_REPORT_DATA.id, + createdAt: '2022-09-23T05:30:23.000Z' + } +] + +let store: typeof mockedStore +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +describe('DatabaseAnalysisTabs', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should call setDatabaseAnalysisViewTab', () => { + render() + + fireEvent.click(screen.getByTestId(`${DatabaseAnalysisViewTab.Recommendations}-tab`)) + + const expectedActions = [setDatabaseAnalysisViewTab(DatabaseAnalysisViewTab.Recommendations)] + expect(store.getActions()).toEqual(expectedActions) + }) + + it('should render encrypt message', () => { + const mockData = { + totalKeys: null + } + render() + + expect(screen.queryByTestId('empty-encrypt-wrapper')).toBeTruthy() + }) + + describe('recommendations count', () => { + it('should render "Recommendation (3)" in the tab name', () => { + const mockData = { + recommendations: [ + { name: 'luaScript' }, + { name: 'luaScript' }, + { name: 'luaScript' }, + ] + } + + render() + + expect(screen.queryByTestId(`${DatabaseAnalysisViewTab.Recommendations}-tab`)).toHaveTextContent('Recommendations (3)') + }) + + it('should render "Recommendation (3)" in the tab name', () => { + const mockData = { + recommendations: [{ name: 'luaScript' }] + } + render() + + expect(screen.queryByTestId(`${DatabaseAnalysisViewTab.Recommendations}-tab`)).toHaveTextContent('Recommendations (1)') + }) + + it('should render "Recommendation" in the tab name', () => { + const mockData = { + recommendations: [] + } + render() + + expect(screen.queryByTestId(`${DatabaseAnalysisViewTab.Recommendations}-tab`)).toHaveTextContent('Recommendations') + }) + }) +}) diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/sub-tabs/DatabaseAnalysisTabs.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/data-nav-tabs/DatabaseAnalysisTabs.tsx similarity index 76% rename from redisinsight/ui/src/pages/databaseAnalysis/components/sub-tabs/DatabaseAnalysisTabs.tsx rename to redisinsight/ui/src/pages/databaseAnalysis/components/data-nav-tabs/DatabaseAnalysisTabs.tsx index ca609f19c7..b06f999fc0 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/sub-tabs/DatabaseAnalysisTabs.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/data-nav-tabs/DatabaseAnalysisTabs.tsx @@ -6,15 +6,16 @@ import { EmptyMessage } from 'uiSrc/pages/databaseAnalysis/constants' import { EmptyAnalysisMessage } from 'uiSrc/pages/databaseAnalysis/components' import { setDatabaseAnalysisViewTab, dbAnalysisViewTabSelector } from 'uiSrc/slices/analytics/dbAnalysis' import { DatabaseAnalysisViewTab } from 'uiSrc/slices/interfaces/analytics' +import { Nullable } from 'uiSrc/utils' import { ShortDatabaseAnalysis, DatabaseAnalysis } from 'apiSrc/modules/database-analysis/models' import { databaseAnalysisTabs } from './constants' import styles from './styles.module.scss' -interface Props { +export interface Props { loading: boolean reports: ShortDatabaseAnalysis[] - data: DatabaseAnalysis + data: Nullable } const DatabaseAnalysisTabs = (props: Props) => { @@ -36,21 +37,22 @@ const DatabaseAnalysisTabs = (props: Props) => { key={id} onClick={() => onSelectedTabChanged(id)} isSelected={id === viewTab} + data-testid={`${id}-tab`} > - {name} + {name(data?.recommendations?.length)} ))) - if (!loading && !reports.length) { + if (!loading && !reports?.length) { return ( -
+
) } - if (!loading && !!reports.length && isNull(data?.totalKeys)) { + if (!loading && !!reports?.length && isNull(data?.totalKeys)) { return ( -
+
) @@ -58,7 +60,7 @@ const DatabaseAnalysisTabs = (props: Props) => { return ( <> - {renderTabs()} + {renderTabs()}
{selectedTabContent}
diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/sub-tabs/constants.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/data-nav-tabs/constants.tsx similarity index 77% rename from redisinsight/ui/src/pages/databaseAnalysis/components/sub-tabs/constants.tsx rename to redisinsight/ui/src/pages/databaseAnalysis/components/data-nav-tabs/constants.tsx index 58b6df423a..25b5935871 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/sub-tabs/constants.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/data-nav-tabs/constants.tsx @@ -5,19 +5,19 @@ import Recommendations from '../recommendations-view' interface DatabaseAnalysisTabs { id: DatabaseAnalysisViewTab, - name: string, + name: (count?: number) => string, content: ReactNode } export const databaseAnalysisTabs: DatabaseAnalysisTabs[] = [ { id: DatabaseAnalysisViewTab.DataSummary, - name: 'Data Summary', + name: () => 'Data Summary', content: }, { id: DatabaseAnalysisViewTab.Recommendations, - name: 'Recommendations', + name: (count?: number) => (count ? `Recommendations (${count})` : 'Recommendations'), content: }, ] diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/sub-tabs/index.ts b/redisinsight/ui/src/pages/databaseAnalysis/components/data-nav-tabs/index.ts similarity index 100% rename from redisinsight/ui/src/pages/databaseAnalysis/components/sub-tabs/index.ts rename to redisinsight/ui/src/pages/databaseAnalysis/components/data-nav-tabs/index.ts diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/sub-tabs/styles.module.scss b/redisinsight/ui/src/pages/databaseAnalysis/components/data-nav-tabs/styles.module.scss similarity index 100% rename from redisinsight/ui/src/pages/databaseAnalysis/components/sub-tabs/styles.module.scss rename to redisinsight/ui/src/pages/databaseAnalysis/components/data-nav-tabs/styles.module.scss diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx new file mode 100644 index 0000000000..7c406c5031 --- /dev/null +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx @@ -0,0 +1,135 @@ +import React from 'react' +import { fireEvent, render, screen } from 'uiSrc/utils/test-utils' +import { dbAnalysisSelector } from 'uiSrc/slices/analytics/dbAnalysis' + +import Recommendations from './Recommendations' + +const mockdbAnalysisSelector = jest.requireActual('uiSrc/slices/analytics/dbAnalysis') + +jest.mock('uiSrc/slices/analytics/dbAnalysis', () => ({ + ...jest.requireActual('uiSrc/slices/analytics/dbAnalysis'), + dbAnalysisSelector: jest.fn().mockReturnValue({ + loading: false, + error: '', + data: null, + history: { + loading: false, + error: '', + data: [], + selectedAnalysis: null, + } + }), +})) + +describe('Recommendations', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should render loader', () => { + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + loading: true + })) + + render() + + expect(screen.queryByTestId('recommendations-loader')).toBeInTheDocument() + }) + + it('should not render loader', () => { + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + })) + + render() + + expect(screen.queryByTestId('recommendations-loader')).not.toBeInTheDocument() + }) + + describe('recommendations initial open', () => { + it('should render open recommendations', () => { + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: { + recommendations: [ + { name: 'luaScript' }, + { name: 'luaScript' }, + { name: 'luaScript' }, + { name: 'luaScript' }, + { name: 'luaScript' }, + ] + } + })) + + render() + + expect(screen.queryAllByTestId('luaScript-accordion')[0]?.classList.contains('euiAccordion-isOpen')).toBeTruthy() + expect(screen.queryAllByTestId('luaScript-accordion')[1]?.classList.contains('euiAccordion-isOpen')).toBeTruthy() + expect(screen.queryAllByTestId('luaScript-accordion')[2]?.classList.contains('euiAccordion-isOpen')).toBeTruthy() + expect(screen.queryAllByTestId('luaScript-accordion')[3]?.classList.contains('euiAccordion-isOpen')).toBeTruthy() + expect(screen.queryAllByTestId('luaScript-accordion')[4]?.classList.contains('euiAccordion-isOpen')).toBeTruthy() + }) + + it('should render closed recommendations', () => { + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: { + recommendations: [ + { name: 'luaScript' }, + { name: 'luaScript' }, + { name: 'luaScript' }, + { name: 'luaScript' }, + { name: 'luaScript' }, + { name: 'luaScript' }, + ] + } + })) + + render() + + expect(screen.queryAllByTestId('luaScript-accordion')[0]?.classList.contains('euiAccordion-isOpen')).not.toBeTruthy() + expect(screen.queryAllByTestId('luaScript-accordion')[1]?.classList.contains('euiAccordion-isOpen')).not.toBeTruthy() + expect(screen.queryAllByTestId('luaScript-accordion')[2]?.classList.contains('euiAccordion-isOpen')).not.toBeTruthy() + expect(screen.queryAllByTestId('luaScript-accordion')[3]?.classList.contains('euiAccordion-isOpen')).not.toBeTruthy() + expect(screen.queryAllByTestId('luaScript-accordion')[4]?.classList.contains('euiAccordion-isOpen')).not.toBeTruthy() + expect(screen.queryAllByTestId('luaScript-accordion')[5]?.classList.contains('euiAccordion-isOpen')).not.toBeTruthy() + }) + }) + + it('should render code changes badge in luaScript recommendation', () => { + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: { + recommendations: [{ name: 'luaScript' }] + } + })) + + render() + + expect(screen.queryByTestId('code_changes')).toBeInTheDocument() + expect(screen.queryByTestId('upgrade')).not.toBeInTheDocument() + expect(screen.queryByTestId('configuration_changes')).not.toBeInTheDocument() + }) + + it('should collapse/expand', () => { + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: { + recommendations: [{ name: 'luaScript' }] + } + })) + + const { container } = render() + + expect(screen.queryAllByTestId('luaScript-accordion')[0]?.classList.contains('euiAccordion-isOpen')).toBeTruthy() + + fireEvent.click(container.querySelector('[data-test-subj="luaScript-button"]') as HTMLInputElement) + + expect(screen.queryAllByTestId('luaScript-accordion')[0]?.classList.contains('euiAccordion-isOpen')).not.toBeTruthy() + + fireEvent.click(container.querySelector('[data-test-subj="luaScript-button"]') as HTMLInputElement) + + expect(screen.queryAllByTestId('luaScript-accordion')[0]?.classList.contains('euiAccordion-isOpen')).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx index 846b81a568..11c6f9feca 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx @@ -1,12 +1,9 @@ import React from 'react' import { useSelector } from 'react-redux' -import cx from 'classnames' import { EuiAccordion, EuiPanel, - EuiTitle, EuiText, - EuiLoadingSpinner, } from '@elastic/eui' import { dbAnalysisSelector } from 'uiSrc/slices/analytics/dbAnalysis' import recommendationsContent from 'uiSrc/constants/dbAnalysisRecommendations.json' @@ -14,34 +11,30 @@ import recommendationsContent from 'uiSrc/constants/dbAnalysisRecommendations.js import { parseContent, renderBadges } from './utils' import styles from './styles.module.scss' +const countToCollapseRecommendations = 6 + const Recommendations = () => { const { data, loading } = useSelector(dbAnalysisSelector) const { recommendations = [] } = data ?? {} if (loading) { return ( -
- - Uploading recommendations... -
+
) } if (!recommendations.length) { return (
- No Recommendations at the moment + No Recommendations at the moment.
) } return (
- -

{`RECOMMENDATIONS (${recommendations.length}):`}

-
{recommendations.map(({ name }) => { - const { id, title = '', content = '', badges } = recommendationsContent[name] + const { id = '', title = '', content = '', badges = [] } = recommendationsContent[name] return (
{ arrowDisplay="right" buttonContent={title} buttonClassName={styles.accordionBtn} + buttonProps={{ 'data-test-subj': `${id}-button` }} className={styles.accordion} + initialIsOpen={recommendations.length < countToCollapseRecommendations} + data-testId={`${id}-accordion`} > {content.map((item: { type: string, value: any, id: string }) => diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss index dab0d51d1c..35851a091c 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss @@ -7,10 +7,7 @@ overflow-y: auto; overflow-x: hidden; max-height: 100%; -} - -.title { - margin: 32px 0 24px; + padding-top: 30px; } .container { @@ -19,21 +16,14 @@ flex-direction: column; align-items: center; height: 100%; +} - .emptyMessage { - font: normal normal 500 16px/19px Graphik, sans-serif; - } - - .spinner { - width: 38px; - height: 38px; - margin-bottom: 12px; - border-top-color: var(--euiColorPrimary) !important; - } - - .spinnerText { - color: var(--htmlColor); - } +.loadingWrapper { + width: 100%; + height: 129px; + margin-top: 30px; + background-color: var(--euiColorLightestShade); + border-radius: 4px; } .recommendation { @@ -76,7 +66,7 @@ margin-right: 48px; .badge { - margin: 18px; + margin: 18px 18px 14px; .badgeIcon { margin-right: 14px; @@ -90,8 +80,11 @@ } } + .text { + font: normal normal normal 14px/24px Graphik, sans-serif; + } + .span { display: inline; - font: normal normal normal 14px/24px Graphik, sans-serif; } } diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx index fb0ea72339..efdd95543b 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx @@ -4,8 +4,9 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink, - EuiImage, + EuiSpacer, } from '@elastic/eui' +import cx from 'classnames' import { ReactComponent as CodeIcon } from 'uiSrc/assets/img/code-changes.svg' import { ReactComponent as ConfigurationIcon } from 'uiSrc/assets/img/configuration-changes.svg' import { ReactComponent as UpgradeIcon } from 'uiSrc/assets/img/upgrade.svg' @@ -24,7 +25,7 @@ export const renderBadges = (badges: string[]) => ( ? : ( -
+
{icon} {name}
@@ -36,13 +37,13 @@ export const renderBadges = (badges: string[]) => ( export const parseContent = ({ type, value }: { type: string, value: any }) => { switch (type) { case 'paragraph': - return {value} + return {value} case 'span': - return {value} + return {value} case 'link': return {value.name} - case 'image': - return + case 'spacer': + return default: return value } diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/top-namespace/TopNamespace.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/top-namespace/TopNamespace.tsx index 523bff42a7..b6bd110345 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/top-namespace/TopNamespace.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/top-namespace/TopNamespace.tsx @@ -1,4 +1,5 @@ import { EuiButton, EuiLink, EuiSwitch, EuiTitle } from '@elastic/eui' +import { isNull } from 'lodash' import cx from 'classnames' import React, { useEffect, useState } from 'react' import { useDispatch } from 'react-redux' @@ -38,6 +39,10 @@ const TopNamespace = (props: Props) => { return } + if (isNull(data)) { + return null + } + const handleTreeViewClick = (e: React.MouseEvent) => { e.preventDefault() From 97d8f795a41e17c4bd12c7023a8cbfc7d40a98e1 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 14 Nov 2022 14:23:32 +0400 Subject: [PATCH 007/108] #RI-3527-resolve comments --- ...50002-database-analysis-recommendations.ts | 28 +++++++++ redisinsight/api/migration/index.ts | 2 + .../database-analysis.module.ts | 4 +- .../database-analysis.service.ts | 30 +++++++-- .../providers/database-analysis.provider.ts | 1 - .../database-analysis/scanner/keys-scanner.ts | 1 + .../database-recommendations.module.ts | 9 --- .../database-recommendations.provider.ts | 62 ------------------- .../providers/recommendation.provider.ts | 50 +++++++++++++++ .../recommendation/recommendation.module.ts | 9 +++ redisinsight/api/src/utils/base.helper.ts | 5 ++ .../GET-databases-id-analysis-id.test.ts | 3 +- .../POST-databases-id-analysis.test.ts | 2 + .../test/api/database-analysis/constants.ts | 5 ++ redisinsight/api/test/helpers/constants.ts | 4 +- redisinsight/api/test/helpers/local-db.ts | 1 + 16 files changed, 136 insertions(+), 80 deletions(-) create mode 100644 redisinsight/api/migration/1668420950002-database-analysis-recommendations.ts delete mode 100644 redisinsight/api/src/modules/db-recommendations/database-recommendations.module.ts delete mode 100644 redisinsight/api/src/modules/db-recommendations/providers/database-recommendations.provider.ts create mode 100644 redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts create mode 100644 redisinsight/api/src/modules/recommendation/recommendation.module.ts diff --git a/redisinsight/api/migration/1668420950002-database-analysis-recommendations.ts b/redisinsight/api/migration/1668420950002-database-analysis-recommendations.ts new file mode 100644 index 0000000000..ccb61d2874 --- /dev/null +++ b/redisinsight/api/migration/1668420950002-database-analysis-recommendations.ts @@ -0,0 +1,28 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class databaseAnalysisRecommendations1668420950002 implements MigrationInterface { + name = 'databaseAnalysisRecommendations1668420950002' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_d174a8edc2201d6c5781f0126a"`); + await queryRunner.query(`DROP INDEX "IDX_fdd0daeb4d8f226cf1ff79bebb"`); + await queryRunner.query(`CREATE TABLE "temporary_database_analysis" ("id" varchar PRIMARY KEY NOT NULL, "databaseId" varchar NOT NULL, "filter" blob, "delimiter" varchar NOT NULL, "progress" blob, "totalKeys" blob, "totalMemory" blob, "topKeysNsp" blob, "topMemoryNsp" blob, "topKeysLength" blob, "topKeysMemory" blob, "encryption" varchar, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "expirationGroups" blob, "recommendations" blob, CONSTRAINT "FK_d174a8edc2201d6c5781f0126ae" FOREIGN KEY ("databaseId") REFERENCES "database_instance" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_database_analysis"("id", "databaseId", "filter", "delimiter", "progress", "totalKeys", "totalMemory", "topKeysNsp", "topMemoryNsp", "topKeysLength", "topKeysMemory", "encryption", "createdAt", "expirationGroups") SELECT "id", "databaseId", "filter", "delimiter", "progress", "totalKeys", "totalMemory", "topKeysNsp", "topMemoryNsp", "topKeysLength", "topKeysMemory", "encryption", "createdAt", "expirationGroups" FROM "database_analysis"`); + await queryRunner.query(`DROP TABLE "database_analysis"`); + await queryRunner.query(`ALTER TABLE "temporary_database_analysis" RENAME TO "database_analysis"`); + await queryRunner.query(`CREATE INDEX "IDX_d174a8edc2201d6c5781f0126a" ON "database_analysis" ("databaseId") `); + await queryRunner.query(`CREATE INDEX "IDX_fdd0daeb4d8f226cf1ff79bebb" ON "database_analysis" ("createdAt") `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_fdd0daeb4d8f226cf1ff79bebb"`); + await queryRunner.query(`DROP INDEX "IDX_d174a8edc2201d6c5781f0126a"`); + await queryRunner.query(`ALTER TABLE "database_analysis" RENAME TO "temporary_database_analysis"`); + await queryRunner.query(`CREATE TABLE "database_analysis" ("id" varchar PRIMARY KEY NOT NULL, "databaseId" varchar NOT NULL, "filter" blob, "delimiter" varchar NOT NULL, "progress" blob, "totalKeys" blob, "totalMemory" blob, "topKeysNsp" blob, "topMemoryNsp" blob, "topKeysLength" blob, "topKeysMemory" blob, "encryption" varchar, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "expirationGroups" blob, CONSTRAINT "FK_d174a8edc2201d6c5781f0126ae" FOREIGN KEY ("databaseId") REFERENCES "database_instance" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "database_analysis"("id", "databaseId", "filter", "delimiter", "progress", "totalKeys", "totalMemory", "topKeysNsp", "topMemoryNsp", "topKeysLength", "topKeysMemory", "encryption", "createdAt", "expirationGroups") SELECT "id", "databaseId", "filter", "delimiter", "progress", "totalKeys", "totalMemory", "topKeysNsp", "topMemoryNsp", "topKeysLength", "topKeysMemory", "encryption", "createdAt", "expirationGroups" FROM "temporary_database_analysis"`); + await queryRunner.query(`DROP TABLE "temporary_database_analysis"`); + await queryRunner.query(`CREATE INDEX "IDX_fdd0daeb4d8f226cf1ff79bebb" ON "database_analysis" ("createdAt") `); + await queryRunner.query(`CREATE INDEX "IDX_d174a8edc2201d6c5781f0126a" ON "database_analysis" ("databaseId") `); + } + +} diff --git a/redisinsight/api/migration/index.ts b/redisinsight/api/migration/index.ts index 954f1fd4e4..4c94f38241 100644 --- a/redisinsight/api/migration/index.ts +++ b/redisinsight/api/migration/index.ts @@ -21,6 +21,7 @@ import { databaseAnalysis1664785208236 } from './1664785208236-database-analysis import { databaseAnalysisExpirationGroups1664886479051 } from './1664886479051-database-analysis-expiration-groups'; import { workbenchExecutionTime1667368983699 } from './1667368983699-workbench-execution-time'; import { database1667477693934 } from './1667477693934-database'; +import { databaseAnalysisRecommendations1668420950002 } from './1668420950002-database-analysis-recommendations'; export default [ initialMigration1614164490968, @@ -46,4 +47,5 @@ export default [ databaseAnalysisExpirationGroups1664886479051, workbenchExecutionTime1667368983699, database1667477693934, + databaseAnalysisRecommendations1668420950002, ]; diff --git a/redisinsight/api/src/modules/database-analysis/database-analysis.module.ts b/redisinsight/api/src/modules/database-analysis/database-analysis.module.ts index 452933eaff..d55210eb11 100644 --- a/redisinsight/api/src/modules/database-analysis/database-analysis.module.ts +++ b/redisinsight/api/src/modules/database-analysis/database-analysis.module.ts @@ -5,10 +5,10 @@ import { DatabaseAnalyzer } from 'src/modules/database-analysis/providers/databa import { DatabaseAnalysisProvider } from 'src/modules/database-analysis/providers/database-analysis.provider'; import { KeysScanner } from 'src/modules/database-analysis/scanner/keys-scanner'; import { KeyInfoProvider } from 'src/modules/database-analysis/scanner/key-info/key-info.provider'; -import { DatabaseRecommendationsModule } from 'src/modules/db-recommendations/database-recommendations.module'; +import { RecommendationModule } from 'src/modules/recommendation/recommendation.module'; @Module({ - imports: [DatabaseRecommendationsModule], + imports: [RecommendationModule], controllers: [DatabaseAnalysisController], providers: [ DatabaseAnalysisService, diff --git a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts index 86a10c6a99..868740199d 100644 --- a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts +++ b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts @@ -1,6 +1,7 @@ import { HttpException, Injectable, Logger } from '@nestjs/common'; import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; -import { RecommendationsService } from 'src/modules/db-recommendations/providers/database-recommendations.provider'; +import { uniqWith, isEqual, flatten } from 'lodash'; +import { RecommendationService } from 'src/modules/recommendation/providers/recommendation.provider'; import { catchAclError } from 'src/utils'; import { DatabaseAnalyzer } from 'src/modules/database-analysis/providers/database-analyzer'; import { plainToClass } from 'class-transformer'; @@ -8,6 +9,7 @@ import { DatabaseAnalysis, ShortDatabaseAnalysis } from 'src/modules/database-an import { DatabaseAnalysisProvider } from 'src/modules/database-analysis/providers/database-analysis.provider'; import { CreateDatabaseAnalysisDto } from 'src/modules/database-analysis/dto'; import { KeysScanner } from 'src/modules/database-analysis/scanner/keys-scanner'; +import { Recommendation } from 'src/modules/database-analysis/models/recommendation'; import { DatabaseConnectionService } from 'src/modules/database/database-connection.service'; import { AppTool } from 'src/models'; @@ -17,7 +19,7 @@ export class DatabaseAnalysisService { constructor( private readonly databaseConnectionService: DatabaseConnectionService, - private readonly recommendationsService: RecommendationsService, + private readonly recommendationsService: RecommendationService, private readonly analyzer: DatabaseAnalyzer, private readonly databaseAnalysisProvider: DatabaseAnalysisProvider, private readonly scanner: KeysScanner, @@ -48,17 +50,27 @@ export class DatabaseAnalysisService { processed: 0, }; - scanResults.forEach((nodeResult) => { + scanResults.forEach(async (nodeResult) => { progress.scanned += nodeResult.progress.scanned; progress.processed += nodeResult.progress.processed; progress.total += nodeResult.progress.total; }); + const recommendations = DatabaseAnalysisService.getRecommendationsSummary( + flatten(await Promise.all( + scanResults.map(async (nodeResult) => ( + await this.recommendationsService.getRecommendations({ + client: nodeResult.client, + keys: nodeResult.keys, + }) + )), + )), + ); const analysis = plainToClass(DatabaseAnalysis, await this.analyzer.analyze({ databaseId: clientOptions.instanceId, ...dto, progress, - recommendations: await this.recommendationsService.getRecommendations(clientOptions), + recommendations, }, [].concat(...scanResults.map((nodeResult) => nodeResult.keys)))); return this.databaseAnalysisProvider.create(analysis); @@ -88,4 +100,14 @@ export class DatabaseAnalysisService { async list(databaseId: string): Promise { return this.databaseAnalysisProvider.list(databaseId); } + + /** + * Get recommendations summary + * @param recommendations + */ + + static getRecommendationsSummary(recommendations: Recommendation[]): Recommendation[] { + // if we will sent any values with recommendations, they will be calculate here + return uniqWith(recommendations, isEqual); + } } diff --git a/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.ts b/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.ts index c2fef9bec2..584342db6e 100644 --- a/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.ts +++ b/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.ts @@ -68,7 +68,6 @@ export class DatabaseAnalysisProvider { this.logger.error(`Database analysis with id:${id} was not Found`); throw new NotFoundException(ERROR_MESSAGES.DATABASE_ANALYSIS_NOT_FOUND); } - console.log(classToClass(DatabaseAnalysis, await this.decryptEntity(entity, true))) return classToClass(DatabaseAnalysis, await this.decryptEntity(entity, true)); } diff --git a/redisinsight/api/src/modules/database-analysis/scanner/keys-scanner.ts b/redisinsight/api/src/modules/database-analysis/scanner/keys-scanner.ts index bbe4090c73..67cfdc4e16 100644 --- a/redisinsight/api/src/modules/database-analysis/scanner/keys-scanner.ts +++ b/redisinsight/api/src/modules/database-analysis/scanner/keys-scanner.ts @@ -73,6 +73,7 @@ export class KeysScanner { scanned: opts.filter.count, processed: nodeKeys.length, }, + client, }; } diff --git a/redisinsight/api/src/modules/db-recommendations/database-recommendations.module.ts b/redisinsight/api/src/modules/db-recommendations/database-recommendations.module.ts deleted file mode 100644 index 097526deae..0000000000 --- a/redisinsight/api/src/modules/db-recommendations/database-recommendations.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { RecommendationsService } from './providers/database-recommendations.provider'; - -@Module({ - providers: [RecommendationsService], - exports: [RecommendationsService], -}) -export class DatabaseRecommendationsModule {} diff --git a/redisinsight/api/src/modules/db-recommendations/providers/database-recommendations.provider.ts b/redisinsight/api/src/modules/db-recommendations/providers/database-recommendations.provider.ts deleted file mode 100644 index 7e997c0e18..0000000000 --- a/redisinsight/api/src/modules/db-recommendations/providers/database-recommendations.provider.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { Cluster, Command } from 'ioredis'; -import { get, max } from 'lodash'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; -import { DatabaseConnectionService } from 'src/modules/database/database-connection.service'; -import { convertRedisInfoReplyToObject } from 'src/utils'; -import { AppTool } from 'src/models'; - -const minNumberOfCachedScripts = 10; - -@Injectable() -export class RecommendationsService { - constructor( - private readonly databaseConnectionService: DatabaseConnectionService, - ) {} - - /** - * Get database recommendations - * @param clientOptions - */ - public async getRecommendations( - clientOptions: IFindRedisClientInstanceByOptions, - ) { - const recommendations = []; - if (await this.getLuaScriptRecommendation(clientOptions)) { - recommendations.push({ name: 'luaScript' }); - } - - return recommendations; - } - - /** - * Check lua script recommendation - * @param clientOptions - */ - async getLuaScriptRecommendation( - clientOptions: IFindRedisClientInstanceByOptions, - ): Promise { - let nodes = []; - const client = await this.databaseConnectionService.createClient({ - databaseId: clientOptions.instanceId, - namespace: AppTool.Common, - }); - - if (client instanceof Cluster) { - nodes = client.nodes('master'); - } else { - nodes = [client]; - } - - const nodesNumbersOfCachedScripts = Promise.all(nodes.map(async (node) => { - const info = convertRedisInfoReplyToObject( - await node.sendCommand( - new Command('info', ['memory'], { replyEncoding: 'utf8' }), - ) as string, - ); - return get(info, 'memory.number_of_cached_scripts', {}); - })); - - return max(await nodesNumbersOfCachedScripts) > minNumberOfCachedScripts; - } -} diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts new file mode 100644 index 0000000000..fb99922c43 --- /dev/null +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -0,0 +1,50 @@ +import { Injectable } from '@nestjs/common'; +import { Redis, Command } from 'ioredis'; +import { get } from 'lodash'; +import { Recommendation } from 'src/modules/database-analysis/models/recommendation'; +import { convertRedisInfoReplyToObject } from 'src/utils'; +import { checkIsGreaterThan } from 'src/utils/base.helper'; + +const minNumberOfCachedScripts = 10; + +interface RecommendationInput { + client?: Redis, + keys?: any, + info?: any, +} + +@Injectable() +export class RecommendationService { + /** + * Get recommendations + * @param dto + */ + public async getRecommendations( + dto: RecommendationInput, + ): Promise { + // generic solution, if somewhere we will sent info, we don't need determined some recommendations + const { client, keys, info } = dto; + const recommendations = []; + if (await this.determineLuaScriptRecommendation(client)) { + recommendations.push({ name: 'luaScript' }); + } + return recommendations; + } + + /** + * Check lua script recommendation + * @param redisClient + */ + async determineLuaScriptRecommendation( + redisClient: Redis, + ): Promise { + const info = convertRedisInfoReplyToObject( + await redisClient.sendCommand( + new Command('info', ['memory'], { replyEncoding: 'utf8' }), + ) as string, + ); + const nodesNumbersOfCachedScripts = get(info, 'memory.number_of_cached_scripts', {}); + + return checkIsGreaterThan(minNumberOfCachedScripts, parseInt(await nodesNumbersOfCachedScripts, 10)); + } +} diff --git a/redisinsight/api/src/modules/recommendation/recommendation.module.ts b/redisinsight/api/src/modules/recommendation/recommendation.module.ts new file mode 100644 index 0000000000..14112eb9d5 --- /dev/null +++ b/redisinsight/api/src/modules/recommendation/recommendation.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; + +import { RecommendationService } from './providers/recommendation.provider'; + +@Module({ + providers: [RecommendationService], + exports: [RecommendationService], +}) +export class RecommendationModule {} diff --git a/redisinsight/api/src/utils/base.helper.ts b/redisinsight/api/src/utils/base.helper.ts index b72fc36c4e..fe49924383 100644 --- a/redisinsight/api/src/utils/base.helper.ts +++ b/redisinsight/api/src/utils/base.helper.ts @@ -4,3 +4,8 @@ export const sortByNumberField = ( items: T[], field: string, ): T[] => sortBy(items, (o) => (o && isNumber(o[field]) ? o[field] : -Infinity)); + +export const checkIsGreaterThan = ( + conditionNumber: number, + currentCount: number, +): boolean => currentCount > conditionNumber; diff --git a/redisinsight/api/test/api/database-analysis/GET-databases-id-analysis-id.test.ts b/redisinsight/api/test/api/database-analysis/GET-databases-id-analysis-id.test.ts index f6826fded6..9c4709a0cc 100644 --- a/redisinsight/api/test/api/database-analysis/GET-databases-id-analysis-id.test.ts +++ b/redisinsight/api/test/api/database-analysis/GET-databases-id-analysis-id.test.ts @@ -1,6 +1,6 @@ import { describe, deps, before, expect, getMainCheckFn } from '../deps'; import { analysisSchema } from './constants'; -const { localDb, request, server, constants, rte } = deps; +const { localDb, request, server, constants } = deps; const endpoint = ( instanceId = constants.TEST_INSTANCE_ID, @@ -38,6 +38,7 @@ describe('GET /databases/:id/analysis/:id', () => { topKeysLength: [constants.TEST_DATABASE_ANALYSIS_TOP_KEYS_1], topKeysMemory: [constants.TEST_DATABASE_ANALYSIS_TOP_KEYS_1], expirationGroups: [constants.TEST_DATABASE_ANALYSIS_EXPIRATION_GROUP_1], + recommendations: [constants.TEST_DATABASE_ANALYSIS_RECOMMENDATION_1], }); } }, diff --git a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts index 8d38826fff..17a69bf4f0 100644 --- a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts +++ b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts @@ -64,6 +64,7 @@ describe('POST /databases/:instanceId/analysis', () => { expect(body.topKeysLength.length).to.gt(0); expect(body.topKeysMemory.length).to.gt(0); expect(body.expirationGroups.length).to.gt(0); + expect(body.recommendations.length).to.eq(0); }, after: async () => { expect(await repository.count()).to.eq(5); @@ -142,6 +143,7 @@ describe('POST /databases/:instanceId/analysis', () => { expect(body.expirationGroups[0].label).to.eq('No Expiry'); expect(body.expirationGroups[0].total).to.gt(0); expect(body.expirationGroups[0].threshold).to.eq(0); + expect(body.recommendations.length).to.eq(0); }, after: async () => { expect(await repository.count()).to.eq(5); diff --git a/redisinsight/api/test/api/database-analysis/constants.ts b/redisinsight/api/test/api/database-analysis/constants.ts index a2f587db60..563a66a7d0 100644 --- a/redisinsight/api/test/api/database-analysis/constants.ts +++ b/redisinsight/api/test/api/database-analysis/constants.ts @@ -1,5 +1,9 @@ import { Joi } from '../../helpers/test'; +export const typedRecommendationSchema = Joi.object({ + name: Joi.string().required(), +}); + export const typedTotalSchema = Joi.object({ total: Joi.number().integer().required(), types: Joi.array().items(Joi.object({ @@ -55,4 +59,5 @@ export const analysisSchema = Joi.object().keys({ topKeysLength: Joi.array().items(keySchema).required().max(15), topKeysMemory: Joi.array().items(keySchema).required().max(15), expirationGroups: Joi.array().items(sumGroupSchema).required(), + recommendations: Joi.array().items(typedRecommendationSchema).required(), }).required(); diff --git a/redisinsight/api/test/helpers/constants.ts b/redisinsight/api/test/helpers/constants.ts index cf3b0b64c9..4cdc15f6be 100644 --- a/redisinsight/api/test/helpers/constants.ts +++ b/redisinsight/api/test/helpers/constants.ts @@ -451,6 +451,8 @@ export const constants = { threshold: 4 * 60 * 60 * 1000, }, - + TEST_DATABASE_ANALYSIS_RECOMMENDATION_1: { + name: 'luaScript', + }, // etc... } diff --git a/redisinsight/api/test/helpers/local-db.ts b/redisinsight/api/test/helpers/local-db.ts index 001e60ed5e..be3932addd 100644 --- a/redisinsight/api/test/helpers/local-db.ts +++ b/redisinsight/api/test/helpers/local-db.ts @@ -175,6 +175,7 @@ export const generateNDatabaseAnalysis = async ( expirationGroups: encryptData(JSON.stringify([ constants.TEST_DATABASE_ANALYSIS_EXPIRATION_GROUP_1, ])), + recommendations: encryptData(JSON.stringify([constants.TEST_DATABASE_ANALYSIS_RECOMMENDATION_1])), createdAt: new Date(), encryption: constants.TEST_ENCRYPTION_STRATEGY, ...partial, From 9328fa2ecdd32a55031b04849db1547e4c88f516 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 14 Nov 2022 14:31:56 +0400 Subject: [PATCH 008/108] #RI-3527-remove unnsessusary code --- .../modules/database-analysis/database-analysis.service.ts | 2 +- .../providers/database-analysis.provider.ts | 5 ++--- .../recommendation/providers/recommendation.provider.ts | 5 +++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts index 868740199d..dbe5d1e135 100644 --- a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts +++ b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts @@ -50,7 +50,7 @@ export class DatabaseAnalysisService { processed: 0, }; - scanResults.forEach(async (nodeResult) => { + scanResults.forEach((nodeResult) => { progress.scanned += nodeResult.progress.scanned; progress.processed += nodeResult.progress.processed; progress.total += nodeResult.progress.total; diff --git a/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.ts b/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.ts index 584342db6e..66d51e701b 100644 --- a/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.ts +++ b/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.ts @@ -42,9 +42,7 @@ export class DatabaseAnalysisProvider { */ async create(analysis: Partial): Promise { const entity = await this.repository.save( - await this.encryptEntity(plainToClass(DatabaseAnalysisEntity, { - ...analysis, - })), + await this.encryptEntity(plainToClass(DatabaseAnalysisEntity, analysis)), ); // cleanup history and ignore error if any @@ -68,6 +66,7 @@ export class DatabaseAnalysisProvider { this.logger.error(`Database analysis with id:${id} was not Found`); throw new NotFoundException(ERROR_MESSAGES.DATABASE_ANALYSIS_NOT_FOUND); } + return classToClass(DatabaseAnalysis, await this.decryptEntity(entity, true)); } diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index fb99922c43..ba1cb5e2d9 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { Redis, Command } from 'ioredis'; import { get } from 'lodash'; +import { RedisString } from 'src/common/constants'; import { Recommendation } from 'src/modules/database-analysis/models/recommendation'; import { convertRedisInfoReplyToObject } from 'src/utils'; import { checkIsGreaterThan } from 'src/utils/base.helper'; @@ -9,8 +10,8 @@ const minNumberOfCachedScripts = 10; interface RecommendationInput { client?: Redis, - keys?: any, - info?: any, + keys?: RedisString[], + info?: RedisString, } @Injectable() From 76eaf7f1cb9e3825839da7d7bf2a39e9cd03f55e Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 14 Nov 2022 14:35:06 +0400 Subject: [PATCH 009/108] #RI-3527-add null condition to recommendation view --- .../components/recommendations-view/Recommendations.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx index 11c6f9feca..ad48814435 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx @@ -1,5 +1,6 @@ import React from 'react' import { useSelector } from 'react-redux' +import { isNull } from 'lodash' import { EuiAccordion, EuiPanel, @@ -23,7 +24,7 @@ const Recommendations = () => { ) } - if (!recommendations.length) { + if (isNull(recommendations) || !recommendations.length) { return (
No Recommendations at the moment. From 84d0445ea28d98f867af0a432cb3d3f76e4cf19e Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 14 Nov 2022 14:41:20 +0400 Subject: [PATCH 010/108] #RI-3527-remove unnsessusary code --- .../modules/database-analysis/database-analysis.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts index dbe5d1e135..1e433dbdf3 100644 --- a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts +++ b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts @@ -19,7 +19,7 @@ export class DatabaseAnalysisService { constructor( private readonly databaseConnectionService: DatabaseConnectionService, - private readonly recommendationsService: RecommendationService, + private readonly RecommendationModule: RecommendationService, private readonly analyzer: DatabaseAnalyzer, private readonly databaseAnalysisProvider: DatabaseAnalysisProvider, private readonly scanner: KeysScanner, @@ -59,7 +59,7 @@ export class DatabaseAnalysisService { const recommendations = DatabaseAnalysisService.getRecommendationsSummary( flatten(await Promise.all( scanResults.map(async (nodeResult) => ( - await this.recommendationsService.getRecommendations({ + await this.RecommendationModule.getRecommendations({ client: nodeResult.client, keys: nodeResult.keys, }) From 7116488a294440bf653e004f5c99ef8b6a7543ad Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 14 Nov 2022 14:51:16 +0400 Subject: [PATCH 011/108] #RI-3527-remove commented code --- .../database-recommendations.provider.ts | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 redisinsight/api/src/modules/db-recommendations/providers/database-recommendations.provider.ts diff --git a/redisinsight/api/src/modules/db-recommendations/providers/database-recommendations.provider.ts b/redisinsight/api/src/modules/db-recommendations/providers/database-recommendations.provider.ts new file mode 100644 index 0000000000..eff64e7099 --- /dev/null +++ b/redisinsight/api/src/modules/db-recommendations/providers/database-recommendations.provider.ts @@ -0,0 +1,77 @@ +import { Injectable } from '@nestjs/common'; +import { Redis, Cluster, Command } from 'ioredis'; +import { get, max } from 'lodash'; +import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; +// import { DatabaseConnectionService } from 'src/modules/database/database-connection.service'; +import { convertRedisInfoReplyToObject } from 'src/utils'; +import { AppTool } from 'src/models'; + +const minNumberOfCachedScripts = 10; + +@Injectable() +export class RecommendationsService { + // number of cached, + // current cached scripts from info + + // second func determint + // redis client (or info) + // constructor( + // private readonly databaseConnectionService: DatabaseConnectionService, + // ) {} + + /** + * Get database recommendations + * @param redisClient + */ + public async getRecommendations( + redisClient: Redis, + // { client?, keys?, info?, etc } + // clientOptions: IFindRedisClientInstanceByOptions, + ) { + const recommendations = []; + if (await this.getLuaScriptRecommendation(redisClient)) { + recommendations.push({ name: 'luaScript' }); + } + + return recommendations; + } + + /** + * Check lua script recommendation + * @param redisClient + */ + async getLuaScriptRecommendation( + // clientOptions: IFindRedisClientInstanceByOptions, + redisClient: Redis, + ): Promise { + // let nodes = []; + // const client = await this.databaseConnectionService.createClient({ + // databaseId: clientOptions.instanceId, + // namespace: AppTool.Common, + // }); + + // if (client instanceof Cluster) { + // nodes = client.nodes('master'); + // } else { + // nodes = [client]; + // } + + // const nodesNumbersOfCachedScripts = Promise.all(nodes.map(async (node) => { + // const info = convertRedisInfoReplyToObject( + // await node.sendCommand( + // new Command('info', ['memory'], { replyEncoding: 'utf8' }), + // ) as string, + // ); + // return get(info, 'memory.number_of_cached_scripts', {}); + // })); + + const info = convertRedisInfoReplyToObject( + await redisClient.sendCommand( + new Command('info', ['memory'], { replyEncoding: 'utf8' }), + ) as string, + ); + const nodesNumbersOfCachedScripts = get(info, 'memory.number_of_cached_scripts', {}); + + return parseInt(await nodesNumbersOfCachedScripts, 10) < minNumberOfCachedScripts; + } +} From 34dafdf69c3d18da5b89e54a5ef6bf898e5cbeb8 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 14 Nov 2022 15:08:43 +0400 Subject: [PATCH 012/108] #RI-3527-remove o;d provider --- .../database-recommendations.provider.ts | 77 ------------------- 1 file changed, 77 deletions(-) delete mode 100644 redisinsight/api/src/modules/db-recommendations/providers/database-recommendations.provider.ts diff --git a/redisinsight/api/src/modules/db-recommendations/providers/database-recommendations.provider.ts b/redisinsight/api/src/modules/db-recommendations/providers/database-recommendations.provider.ts deleted file mode 100644 index eff64e7099..0000000000 --- a/redisinsight/api/src/modules/db-recommendations/providers/database-recommendations.provider.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { Redis, Cluster, Command } from 'ioredis'; -import { get, max } from 'lodash'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; -// import { DatabaseConnectionService } from 'src/modules/database/database-connection.service'; -import { convertRedisInfoReplyToObject } from 'src/utils'; -import { AppTool } from 'src/models'; - -const minNumberOfCachedScripts = 10; - -@Injectable() -export class RecommendationsService { - // number of cached, - // current cached scripts from info - - // second func determint - // redis client (or info) - // constructor( - // private readonly databaseConnectionService: DatabaseConnectionService, - // ) {} - - /** - * Get database recommendations - * @param redisClient - */ - public async getRecommendations( - redisClient: Redis, - // { client?, keys?, info?, etc } - // clientOptions: IFindRedisClientInstanceByOptions, - ) { - const recommendations = []; - if (await this.getLuaScriptRecommendation(redisClient)) { - recommendations.push({ name: 'luaScript' }); - } - - return recommendations; - } - - /** - * Check lua script recommendation - * @param redisClient - */ - async getLuaScriptRecommendation( - // clientOptions: IFindRedisClientInstanceByOptions, - redisClient: Redis, - ): Promise { - // let nodes = []; - // const client = await this.databaseConnectionService.createClient({ - // databaseId: clientOptions.instanceId, - // namespace: AppTool.Common, - // }); - - // if (client instanceof Cluster) { - // nodes = client.nodes('master'); - // } else { - // nodes = [client]; - // } - - // const nodesNumbersOfCachedScripts = Promise.all(nodes.map(async (node) => { - // const info = convertRedisInfoReplyToObject( - // await node.sendCommand( - // new Command('info', ['memory'], { replyEncoding: 'utf8' }), - // ) as string, - // ); - // return get(info, 'memory.number_of_cached_scripts', {}); - // })); - - const info = convertRedisInfoReplyToObject( - await redisClient.sendCommand( - new Command('info', ['memory'], { replyEncoding: 'utf8' }), - ) as string, - ); - const nodesNumbersOfCachedScripts = get(info, 'memory.number_of_cached_scripts', {}); - - return parseInt(await nodesNumbersOfCachedScripts, 10) < minNumberOfCachedScripts; - } -} From 73def34048178051363d302a8537934eeb07893a Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 14 Nov 2022 15:51:17 +0400 Subject: [PATCH 013/108] #RI-3527-resolve comments --- .../database-analysis.service.ts | 6 ++-- .../providers/recommendation.provider.ts | 28 ++------------- .../recommendation/recommendation.module.ts | 6 ++-- .../recommendation/recommendation.service.ts | 34 +++++++++++++++++++ 4 files changed, 42 insertions(+), 32 deletions(-) create mode 100644 redisinsight/api/src/modules/recommendation/recommendation.service.ts diff --git a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts index 1e433dbdf3..5e9eaab1d0 100644 --- a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts +++ b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts @@ -1,7 +1,7 @@ import { HttpException, Injectable, Logger } from '@nestjs/common'; import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { uniqWith, isEqual, flatten } from 'lodash'; -import { RecommendationService } from 'src/modules/recommendation/providers/recommendation.provider'; +import { RecommendationService } from 'src/modules/recommendation/recommendation.service'; import { catchAclError } from 'src/utils'; import { DatabaseAnalyzer } from 'src/modules/database-analysis/providers/database-analyzer'; import { plainToClass } from 'class-transformer'; @@ -19,7 +19,7 @@ export class DatabaseAnalysisService { constructor( private readonly databaseConnectionService: DatabaseConnectionService, - private readonly RecommendationModule: RecommendationService, + private readonly recommendationService: RecommendationService, private readonly analyzer: DatabaseAnalyzer, private readonly databaseAnalysisProvider: DatabaseAnalysisProvider, private readonly scanner: KeysScanner, @@ -59,7 +59,7 @@ export class DatabaseAnalysisService { const recommendations = DatabaseAnalysisService.getRecommendationsSummary( flatten(await Promise.all( scanResults.map(async (nodeResult) => ( - await this.RecommendationModule.getRecommendations({ + await this.recommendationService.getRecommendations({ client: nodeResult.client, keys: nodeResult.keys, }) diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index ba1cb5e2d9..871317acd3 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -1,37 +1,13 @@ import { Injectable } from '@nestjs/common'; import { Redis, Command } from 'ioredis'; import { get } from 'lodash'; -import { RedisString } from 'src/common/constants'; -import { Recommendation } from 'src/modules/database-analysis/models/recommendation'; import { convertRedisInfoReplyToObject } from 'src/utils'; import { checkIsGreaterThan } from 'src/utils/base.helper'; const minNumberOfCachedScripts = 10; -interface RecommendationInput { - client?: Redis, - keys?: RedisString[], - info?: RedisString, -} - @Injectable() -export class RecommendationService { - /** - * Get recommendations - * @param dto - */ - public async getRecommendations( - dto: RecommendationInput, - ): Promise { - // generic solution, if somewhere we will sent info, we don't need determined some recommendations - const { client, keys, info } = dto; - const recommendations = []; - if (await this.determineLuaScriptRecommendation(client)) { - recommendations.push({ name: 'luaScript' }); - } - return recommendations; - } - +export class RecommendationProvider { /** * Check lua script recommendation * @param redisClient @@ -41,7 +17,7 @@ export class RecommendationService { ): Promise { const info = convertRedisInfoReplyToObject( await redisClient.sendCommand( - new Command('info', ['memory'], { replyEncoding: 'utf8' }), + new Command('info', [], { replyEncoding: 'utf8' }), ) as string, ); const nodesNumbersOfCachedScripts = get(info, 'memory.number_of_cached_scripts', {}); diff --git a/redisinsight/api/src/modules/recommendation/recommendation.module.ts b/redisinsight/api/src/modules/recommendation/recommendation.module.ts index 14112eb9d5..9eb6c38863 100644 --- a/redisinsight/api/src/modules/recommendation/recommendation.module.ts +++ b/redisinsight/api/src/modules/recommendation/recommendation.module.ts @@ -1,9 +1,9 @@ import { Module } from '@nestjs/common'; - -import { RecommendationService } from './providers/recommendation.provider'; +import { RecommendationService } from './recommendation.service'; +import { RecommendationProvider } from './providers/recommendation.provider'; @Module({ - providers: [RecommendationService], + providers: [RecommendationService, RecommendationProvider], exports: [RecommendationService], }) export class RecommendationModule {} diff --git a/redisinsight/api/src/modules/recommendation/recommendation.service.ts b/redisinsight/api/src/modules/recommendation/recommendation.service.ts new file mode 100644 index 0000000000..54c2d456bf --- /dev/null +++ b/redisinsight/api/src/modules/recommendation/recommendation.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@nestjs/common'; +import { Redis } from 'ioredis'; +import { RecommendationProvider } from 'src/modules/recommendation/providers/recommendation.provider'; +import { Recommendation } from 'src/modules/database-analysis/models/recommendation'; +import { RedisString } from 'src/common/constants'; + +interface RecommendationInput { + client?: Redis, + keys?: RedisString[], + info?: RedisString, +} + +@Injectable() +export class RecommendationService { + constructor( + private readonly recommendationProvider: RecommendationProvider, + ) {} + + /** + * Get recommendations + * @param dto + */ + public async getRecommendations( + dto: RecommendationInput, + ): Promise { + // generic solution, if somewhere we will sent info, we don't need determined some recommendations + const { client, keys, info } = dto; + const recommendations = []; + if (await this.recommendationProvider.determineLuaScriptRecommendation(client)) { + recommendations.push({ name: 'luaScript' }); + } + return recommendations; + } +} From d1a60b7034038de37718052d22bbdc8d78c42c4d Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Mon, 14 Nov 2022 15:38:23 +0100 Subject: [PATCH 014/108] add tests for avoid dynamic lua script recommendation --- .../components/recommendations-view/utils.tsx | 2 +- tests/e2e/common-actions/cli-actions.ts | 20 ++++++- .../e2e/pageObjects/memory-efficiency-page.ts | 7 +++ .../memory-efficiency/recommendations.e2e.ts | 57 +++++++++++++++++++ 4 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx index efdd95543b..8ce97ac62e 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx @@ -41,7 +41,7 @@ export const parseContent = ({ type, value }: { type: string, value: any }) => { case 'span': return {value} case 'link': - return {value.name} + return {value.name} case 'spacer': return default: diff --git a/tests/e2e/common-actions/cli-actions.ts b/tests/e2e/common-actions/cli-actions.ts index 348a31ad5a..e2e82f79fa 100644 --- a/tests/e2e/common-actions/cli-actions.ts +++ b/tests/e2e/common-actions/cli-actions.ts @@ -1,16 +1,18 @@ import { t } from 'testcafe'; +import { Common } from '../helpers/common'; import { CliPage } from '../pageObjects'; const cliPage = new CliPage(); +const common = new Common(); export class CliActions { - + /** * Check list of commands searched * @param searchedCommand Searched command in Command Helper * @param listToCompare The list with commands to compare with opened in Command Helper */ - async checkSearchedCommandInCommandHelper(searchedCommand: string, listToCompare: string[]): Promise { + async checkSearchedCommandInCommandHelper(searchedCommand: string, listToCompare: string[]): Promise { await t.typeText(cliPage.cliHelperSearch, searchedCommand, { speed: 0.5 }); //Verify results in the output const commandsCount = await cliPage.cliHelperOutputTitles.count; @@ -30,4 +32,18 @@ export class CliActions { await t.expect(cliPage.cliHelperOutputTitles.nth(i).textContent).eql(listToCompare[i], 'Results in the output not contain searched value'); } } + + /** + * Add cached scripts + * @param numberOfScripts The number of cached scripts to add + */ + async addCachedScripts(numberOfScripts: number): Promise { + const scripts: string[] = []; + + for (let i = 0; i < numberOfScripts; i++) { + scripts.push(`EVAL "return '${common.generateWord(3)}'" 0`); + } + + await cliPage.sendCommandsInCli(scripts); + } } diff --git a/tests/e2e/pageObjects/memory-efficiency-page.ts b/tests/e2e/pageObjects/memory-efficiency-page.ts index 542235a88c..712c12ca0a 100644 --- a/tests/e2e/pageObjects/memory-efficiency-page.ts +++ b/tests/e2e/pageObjects/memory-efficiency-page.ts @@ -15,6 +15,8 @@ export class MemoryEfficiencyPage { reportItem = Selector('[data-test-subj^=items-report-]'); selectedReport = Selector('[data-testid=select-report]'); sortByLength = Selector('[data-testid=btn-change-table-keys]'); + recommendationsTab = Selector('[data-testid=Recommendations-tab]'); + luaScriptButton = Selector('[data-test-subj=luaScript-button]'); // ICONS reportTooltipIcon = Selector('[data-testid=db-new-reports-icon]'); // TEXT ELEMENTS @@ -26,6 +28,7 @@ export class MemoryEfficiencyPage { topKeysKeyName = Selector('[data-testid=top-keys-table-name]'); topNamespacesEmptyContainer = Selector('[data-testid=top-namespaces-empty]'); topNamespacesEmptyMessage = Selector('[data-testid=top-namespaces-message]'); + noRecommendationsMessage = Selector('[data-testid=empty-recommendations-message]'); // TABLE namespaceTable = Selector('[data-testid=nsp-table-memory]'); nameSpaceTableRows = this.namespaceTable.find('[data-testid^=row-]'); @@ -42,4 +45,8 @@ export class MemoryEfficiencyPage { noExpiryPoint = Selector('[data-testid*=bar-0-]:not(rect[data-testid=bar-0-0])'); // LINKS treeViewLink = Selector('[data-testid=tree-view-page-link]'); + readMoreLink = Selector('[data-testid=read-more-link]'); + // CONTAINERS + luaScriptAccordion = Selector('[data-testid=luaScript-accordion]'); + luaScriptTextContainer = Selector('#luaScript'); } diff --git a/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts b/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts new file mode 100644 index 0000000000..68d784ba7a --- /dev/null +++ b/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts @@ -0,0 +1,57 @@ +import { MyRedisDatabasePage, MemoryEfficiencyPage } from '../../../pageObjects'; +import { rte } from '../../../helpers/constants'; +import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { commonUrl, ossStandaloneBigConfig } from '../../../helpers/conf'; +import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { CliActions } from '../../../common-actions/cli-actions'; +import { Common } from '../../../helpers/common'; + +const memoryEfficiencyPage = new MemoryEfficiencyPage(); +const myRedisDatabasePage = new MyRedisDatabasePage(); +const cliActions = new CliActions(); +const common = new Common(); + +fixture `Memory Efficiency Recommendations` + .meta({ type: 'critical_path', rte: rte.standalone }) + .page(commonUrl); +test + .before(async t => { + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); + // Go to Analysis Tools page + await t.click(myRedisDatabasePage.analysisPageButton); + await t.click(memoryEfficiencyPage.newReportBtn); + }) + .after(async() => { + await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + })('Avoid dynamic Lua script recommendation', async t => { + const noRecommendationsMessage = 'No Recommendations at the moment.'; + const externalPageLink = 'https://docs.redis.com/latest/ri/memory-optimizations/'; + + // Go to Recommendations tab + await t.click(memoryEfficiencyPage.recommendationsTab); + // No recommendations message + await t.expect(memoryEfficiencyPage.noRecommendationsMessage.textContent).eql(noRecommendationsMessage, 'No recommendations message not displayed'); + // Add cached scripts and generate new report + await cliActions.addCachedScripts(10); + await t.click(memoryEfficiencyPage.newReportBtn); + // No recommendations message with 10 cached scripts + await t.expect(memoryEfficiencyPage.noRecommendationsMessage.textContent).eql(noRecommendationsMessage, 'No recommendations message not displayed'); + // Add the last cached script to see the recommendation + await cliActions.addCachedScripts(1); + await t.click(memoryEfficiencyPage.newReportBtn); + // Verify that user can see Avoid dynamic Lua script recommendation when number_of_cached_scripts> 10 + await t.expect(memoryEfficiencyPage.luaScriptAccordion.exists).ok('Avoid dynamic lua script recommendation not displayed'); + + // Verify that user can expand/collapse recommendation + const expandedTextConaiterSize = await memoryEfficiencyPage.luaScriptTextContainer.offsetHeight; + await t.click(memoryEfficiencyPage.luaScriptButton); + await t.expect(await memoryEfficiencyPage.luaScriptTextContainer.offsetHeight).lt(expandedTextConaiterSize, 'Recommendation not collapsed'); + await t.click(memoryEfficiencyPage.luaScriptButton); + await t.expect(await memoryEfficiencyPage.luaScriptTextContainer.offsetHeight).eql(expandedTextConaiterSize, 'Recommendation not expanded'); + + // Verify that user can navigate by link to see the recommendation + await t.click(memoryEfficiencyPage.readMoreLink); + await common.checkURL(externalPageLink); + // Close the window with external link to switch to the application window + await t.closeWindow(); + }); From 0807a4071b447a151b2b957037e12eff935c0083 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Tue, 15 Nov 2022 02:04:10 +0400 Subject: [PATCH 015/108] #RI-3527-resolve comments --- .../recommendation/providers/recommendation.provider.ts | 4 ++-- .../api/src/modules/recommendation/recommendation.service.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index 871317acd3..ce56773ec3 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -20,8 +20,8 @@ export class RecommendationProvider { new Command('info', [], { replyEncoding: 'utf8' }), ) as string, ); - const nodesNumbersOfCachedScripts = get(info, 'memory.number_of_cached_scripts', {}); + const nodesNumbersOfCachedScripts = get(info, 'memory.number_of_cached_scripts'); - return checkIsGreaterThan(minNumberOfCachedScripts, parseInt(await nodesNumbersOfCachedScripts, 10)); + return checkIsGreaterThan(minNumberOfCachedScripts, parseInt(nodesNumbersOfCachedScripts, 10)); } } diff --git a/redisinsight/api/src/modules/recommendation/recommendation.service.ts b/redisinsight/api/src/modules/recommendation/recommendation.service.ts index 54c2d456bf..67f1e29a9f 100644 --- a/redisinsight/api/src/modules/recommendation/recommendation.service.ts +++ b/redisinsight/api/src/modules/recommendation/recommendation.service.ts @@ -26,6 +26,7 @@ export class RecommendationService { // generic solution, if somewhere we will sent info, we don't need determined some recommendations const { client, keys, info } = dto; const recommendations = []; + // TODO refactor it if (await this.recommendationProvider.determineLuaScriptRecommendation(client)) { recommendations.push({ name: 'luaScript' }); } From 70101a207a5612b082b3776ab39cbea4587ed9eb Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Tue, 15 Nov 2022 15:23:36 +0400 Subject: [PATCH 016/108] #RI-3527-resolve comments --- .../recommendation/providers/recommendation.provider.ts | 3 +-- redisinsight/api/src/utils/base.helper.ts | 5 ----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index ce56773ec3..79a4cacdf6 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common'; import { Redis, Command } from 'ioredis'; import { get } from 'lodash'; import { convertRedisInfoReplyToObject } from 'src/utils'; -import { checkIsGreaterThan } from 'src/utils/base.helper'; const minNumberOfCachedScripts = 10; @@ -22,6 +21,6 @@ export class RecommendationProvider { ); const nodesNumbersOfCachedScripts = get(info, 'memory.number_of_cached_scripts'); - return checkIsGreaterThan(minNumberOfCachedScripts, parseInt(nodesNumbersOfCachedScripts, 10)); + return parseInt(nodesNumbersOfCachedScripts, 10) > minNumberOfCachedScripts; } } diff --git a/redisinsight/api/src/utils/base.helper.ts b/redisinsight/api/src/utils/base.helper.ts index fe49924383..b72fc36c4e 100644 --- a/redisinsight/api/src/utils/base.helper.ts +++ b/redisinsight/api/src/utils/base.helper.ts @@ -4,8 +4,3 @@ export const sortByNumberField = ( items: T[], field: string, ): T[] => sortBy(items, (o) => (o && isNumber(o[field]) ? o[field] : -Infinity)); - -export const checkIsGreaterThan = ( - conditionNumber: number, - currentCount: number, -): boolean => currentCount > conditionNumber; From b596d5f0438f5d0838fa64371839b309c753e693 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Tue, 15 Nov 2022 15:57:46 +0400 Subject: [PATCH 017/108] #RI-3527-fix unit tests --- .../src/modules/database-analysis/scanner/keys-scanner.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/redisinsight/api/src/modules/database-analysis/scanner/keys-scanner.spec.ts b/redisinsight/api/src/modules/database-analysis/scanner/keys-scanner.spec.ts index 743827564a..ee1b5f34f8 100644 --- a/redisinsight/api/src/modules/database-analysis/scanner/keys-scanner.spec.ts +++ b/redisinsight/api/src/modules/database-analysis/scanner/keys-scanner.spec.ts @@ -45,6 +45,7 @@ const mockScanResult = { scanned: 15, total: 1, }, + client: Object.assign(nodeClient), }; describe('KeysScanner', () => { From 49cebaa1b1166819456d2ec39bd3e920a82bd5b6 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Wed, 16 Nov 2022 10:13:35 +0400 Subject: [PATCH 018/108] #RI-3527-add more tests --- .../database-analysis.service.ts | 1 + .../database-analysis.provider.spec.ts | 4 +- .../providers/recommendation.provider.ts | 25 ++++++ .../recommendation/recommendation.service.ts | 18 ++++- .../POST-databases-id-analysis.test.ts | 79 +++++++++++++++++-- redisinsight/api/test/helpers/constants.ts | 1 + redisinsight/api/test/helpers/data/redis.ts | 14 ++++ 7 files changed, 132 insertions(+), 10 deletions(-) diff --git a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts index 5e9eaab1d0..a3442530a7 100644 --- a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts +++ b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts @@ -62,6 +62,7 @@ export class DatabaseAnalysisService { await this.recommendationService.getRecommendations({ client: nodeResult.client, keys: nodeResult.keys, + total: progress.total, }) )), )), diff --git a/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.spec.ts b/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.spec.ts index b4b38c45da..ba57e746ff 100644 --- a/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.spec.ts +++ b/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.spec.ts @@ -43,6 +43,7 @@ const mockDatabaseAnalysisEntity = new DatabaseAnalysisEntity({ topKeysLength: 'ENCRYPTED:topKeysLength', topKeysMemory: 'ENCRYPTED:topKeysMemory', expirationGroups: 'ENCRYPTED:expirationGroups', + recommendations: 'ENCRYPTED:recommendations', encryption: 'KEYTAR', createdAt: new Date(), }); @@ -146,6 +147,7 @@ const mockDatabaseAnalysis = { total: 0, }, ], + recommendations: [{ name: 'luaScript'}], } as DatabaseAnalysis; describe('DatabaseAnalysisProvider', () => { @@ -175,7 +177,7 @@ describe('DatabaseAnalysisProvider', () => { // encryption mocks [ 'filter', 'totalKeys', 'totalMemory', 'topKeysNsp', 'topMemoryNsp', - 'topKeysLength', 'topKeysMemory', 'expirationGroups', + 'topKeysLength', 'topKeysMemory', 'expirationGroups', 'recommendations', ].forEach((field) => { when(encryptionService.encrypt) .calledWith(JSON.stringify(mockDatabaseAnalysis[field])) diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index 79a4cacdf6..c677f66e90 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -2,8 +2,12 @@ import { Injectable } from '@nestjs/common'; import { Redis, Command } from 'ioredis'; import { get } from 'lodash'; import { convertRedisInfoReplyToObject } from 'src/utils'; +import { RedisDataType } from 'src/modules/browser/dto'; +import { Key } from 'src/modules/database-analysis/models'; const minNumberOfCachedScripts = 10; +const maxHashLength = 5000; +const maxDatabaseTotal = 1_000_000; @Injectable() export class RecommendationProvider { @@ -23,4 +27,25 @@ export class RecommendationProvider { return parseInt(nodesNumbersOfCachedScripts, 10) > minNumberOfCachedScripts; } + + /** + * Check big hashes recommendation + * @param keys + */ + async determineBigHashesRecommendation( + keys: Key[], + ): Promise { + const bigHashes = keys.filter((key) => key.type === RedisDataType.Hash && key.length > maxHashLength); + return bigHashes.length > 0; + } + + /** + * Check big hashes recommendation + * @param total + */ + async determineBigTotalRecommendation( + total: number, + ): Promise { + return total > maxDatabaseTotal; + } } diff --git a/redisinsight/api/src/modules/recommendation/recommendation.service.ts b/redisinsight/api/src/modules/recommendation/recommendation.service.ts index 67f1e29a9f..a8530688cf 100644 --- a/redisinsight/api/src/modules/recommendation/recommendation.service.ts +++ b/redisinsight/api/src/modules/recommendation/recommendation.service.ts @@ -3,11 +3,13 @@ import { Redis } from 'ioredis'; import { RecommendationProvider } from 'src/modules/recommendation/providers/recommendation.provider'; import { Recommendation } from 'src/modules/database-analysis/models/recommendation'; import { RedisString } from 'src/common/constants'; +import { Key } from 'src/modules/database-analysis/models'; interface RecommendationInput { client?: Redis, - keys?: RedisString[], + keys?: Key[], info?: RedisString, + total?: number, } @Injectable() @@ -24,12 +26,24 @@ export class RecommendationService { dto: RecommendationInput, ): Promise { // generic solution, if somewhere we will sent info, we don't need determined some recommendations - const { client, keys, info } = dto; + const { + client, + keys, + info, + total, + } = dto; + const recommendations = []; // TODO refactor it if (await this.recommendationProvider.determineLuaScriptRecommendation(client)) { recommendations.push({ name: 'luaScript' }); } + if (await this.recommendationProvider.determineBigHashesRecommendation(keys)) { + recommendations.push({ name: 'bigHashes' }); + } + if (await this.recommendationProvider.determineBigTotalRecommendation(total)) { + recommendations.push({ name: 'useSmallerKeys' }); + } return recommendations; } } diff --git a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts index 17a69bf4f0..4f07b395af 100644 --- a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts +++ b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts @@ -15,14 +15,14 @@ describe('POST /databases/:instanceId/analysis', () => { // todo: skip for RE for now since scan 0 count 10000 might return cursor and 0 keys multiple times requirements('!rte.re'); - before(async() => { - repository = await localDb.getRepository(localDb.repositories.DATABASE_ANALYSIS); + before(async () => { + repository = await localDb.getRepository(localDb.repositories.DATABASE_ANALYSIS); - await localDb.generateNDatabaseAnalysis({ - databaseId: constants.TEST_INSTANCE_ID, - }, 30, true); + await localDb.generateNDatabaseAnalysis({ + databaseId: constants.TEST_INSTANCE_ID, + }, 30, true); - await rte.data.generateKeys(true); + await rte.data.generateKeys(true); }); [ @@ -135,7 +135,7 @@ describe('POST /databases/:instanceId/analysis', () => { expect(body.topKeysLength[0].length).to.gt(0); expect(body.expirationGroups.length).to.eq(8); - for(let i = 1; i < 8; i++) { + for (let i = 1; i < 8; i++) { expect(body.expirationGroups[i].label).to.be.a('string'); expect(body.expirationGroups[i].total).to.eq(0); expect(body.expirationGroups[i].threshold).to.gt(0); @@ -149,5 +149,70 @@ describe('POST /databases/:instanceId/analysis', () => { expect(await repository.count()).to.eq(5); } }, + { + name: 'Should create new database analysis with useSmallerKeys recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + const KEYS_NUMBER = 1_000_001 + await rte.data.generateNKeys(KEYS_NUMBER, false); + }, + checkFn: async ({ body }) => { + expect(body.totalKeys.total).to.gt(0); + expect(body.totalMemory.total).to.gt(0); + expect(body.topKeysNsp.length).to.gt(0); + expect(body.topMemoryNsp.length).to.gt(0); + expect(body.topKeysLength.length).to.gt(0); + expect(body.topKeysMemory.length).to.gt(0); + expect(body.recommendations).to.deep.eq([{ name: 'useSmallerKeys'}]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } + }, + { + name: 'Should create new database analysis with bigHashes recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + const NUMBERS_OF_HASH_FIELDS = 5001 + await rte.data.generateHugeNumberOfFieldsForHashKey(NUMBERS_OF_HASH_FIELDS, true); + }, + checkFn: async ({ body }) => { + expect(body.totalKeys.total).to.gt(0); + expect(body.totalMemory.total).to.gt(0); + expect(body.topKeysNsp.length).to.gt(0); + expect(body.topMemoryNsp.length).to.gt(0); + expect(body.topKeysLength.length).to.gt(0); + expect(body.topKeysMemory.length).to.gt(0); + expect(body.recommendations).to.deep.eq([{ name: 'bigHashes'}]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } + }, + { + name: 'Should create new database analysis with luaScript recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + await rte.data.generateNCachedScripts(11, true); + }, + checkFn: async ({ body }) => { + expect(body.recommendations).to.deep.eq([{ name: 'luaScript'}]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } + }, ].map(mainCheckFn); }); diff --git a/redisinsight/api/test/helpers/constants.ts b/redisinsight/api/test/helpers/constants.ts index 4cdc15f6be..7668348f26 100644 --- a/redisinsight/api/test/helpers/constants.ts +++ b/redisinsight/api/test/helpers/constants.ts @@ -454,5 +454,6 @@ export const constants = { TEST_DATABASE_ANALYSIS_RECOMMENDATION_1: { name: 'luaScript', }, + // etc... } diff --git a/redisinsight/api/test/helpers/data/redis.ts b/redisinsight/api/test/helpers/data/redis.ts index 745ffbb841..d54e3eb56a 100644 --- a/redisinsight/api/test/helpers/data/redis.ts +++ b/redisinsight/api/test/helpers/data/redis.ts @@ -456,6 +456,19 @@ export const initDataHelper = (rte) => { } } + // scripts + const generateNCachedScripts = async (number: number = 10, clean: boolean) => { + if (clean) { + await truncate(); + } + + const pipeline = []; + for (let i = 0; i < number; i++) { + pipeline.push(['eval', `return ${i}`, '0']) + } + await insertKeysBasedOnEnv(pipeline); + }; + return { sendCommand, executeCommand, @@ -476,6 +489,7 @@ export const initDataHelper = (rte) => { generateStreamsWithoutStrictMode, generateNStreams, generateNGraphs, + generateNCachedScripts, getClientNodes, } } From f42c03f104406ad78cec459570a6302b2c40099a Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Wed, 16 Nov 2022 10:16:30 +0400 Subject: [PATCH 019/108] #RI-3527-add more tests --- .../database-analysis.provider.spec.ts | 2 +- .../constants/dbAnalysisRecommendations.json | 40 +++++++++++++++++++ .../Recommendations.spec.tsx | 30 ++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.spec.ts b/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.spec.ts index ba57e746ff..eb06385afa 100644 --- a/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.spec.ts +++ b/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.spec.ts @@ -147,7 +147,7 @@ const mockDatabaseAnalysis = { total: 0, }, ], - recommendations: [{ name: 'luaScript'}], + recommendations: [{ name: 'luaScript' }], } as DatabaseAnalysis; describe('DatabaseAnalysisProvider', () => { diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index 7cde7e7a48..d466f41583 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -18,5 +18,45 @@ } ], "badges": ["code_changes"] + }, + "useSmallerKeys": { + "id": "useSmallerKeys", + "title":"Use smaller keys", + "content": [ + { + "id": "1", + "type": "span", + "value": "Shorten key names to optimize memory usage. Though, in general, descriptive key names are always preferred, these large key names can eat a lot of the memory. " + }, + { + "id": "2", + "type": "link", + "value": { + "href": "https://docs.redis.com/latest/ri/memory-optimizations/", + "name": "Read more" + } + } + ], + "badges": ["code_changes"] + }, + "bigHashes": { + "id": "bigHashes", + "title": "Shard big hashes to small hashes", + "content": [ + { + "id": "1", + "type": "span", + "value": "If you have a hash with a large number of key, value pairs, and if each key, value pair is small enough - break it into smaller hashes to save memory. To shard a HASH table, choose a method of partitioning the data. Hashes themselves have keys that can be used for partitioning the keys into different shards. The number of shards is determined by the total number of keys to be stored and the shard size. Using this and the hash value you can determine the shard ID in which the key resides. Though converting big hashes to small hashes will increase the complexity of your code. " + }, + { + "id": "2", + "type": "link", + "value": { + "href": "https://docs.redis.com/latest/ri/memory-optimizations/", + "name": "Read more" + } + } + ], + "badges": ["code_changes", "configuration_changes"] } } diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx index 7c406c5031..cc044861d2 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx @@ -112,6 +112,36 @@ describe('Recommendations', () => { expect(screen.queryByTestId('configuration_changes')).not.toBeInTheDocument() }) + it('should render code changes badge in useSmallerKeys recommendation', () => { + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: { + recommendations: [{ name: 'useSmallerKeys' }] + } + })) + + render() + + expect(screen.queryByTestId('code_changes')).toBeInTheDocument() + expect(screen.queryByTestId('upgrade')).not.toBeInTheDocument() + expect(screen.queryByTestId('configuration_changes')).not.toBeInTheDocument() + }) + + it('should render code changes badge and configuration_changes in bigHashes recommendation', () => { + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: { + recommendations: [{ name: 'bigHashes' }] + } + })) + + render() + + expect(screen.queryByTestId('code_changes')).toBeInTheDocument() + expect(screen.queryByTestId('upgrade')).not.toBeInTheDocument() + expect(screen.queryByTestId('configuration_changes')).toBeInTheDocument() + }) + it('should collapse/expand', () => { (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ ...mockdbAnalysisSelector, From 2b7f2843f245c8e1a5f577a128b6fd7eedea9377 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Wed, 16 Nov 2022 11:48:42 +0400 Subject: [PATCH 020/108] #RI-3527-update recommendation merge --- .../database-analysis.service.ts | 27 ++++++++++++++----- .../models/recommendation.ts | 9 ++++++- .../providers/recommendation.provider.ts | 22 ++++++++++----- .../recommendation/recommendation.service.ts | 17 ++++-------- 4 files changed, 50 insertions(+), 25 deletions(-) diff --git a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts index a3442530a7..3059ab4eeb 100644 --- a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts +++ b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts @@ -1,6 +1,6 @@ import { HttpException, Injectable, Logger } from '@nestjs/common'; import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; -import { uniqWith, isEqual, flatten } from 'lodash'; +import { omit } from 'lodash'; import { RecommendationService } from 'src/modules/recommendation/recommendation.service'; import { catchAclError } from 'src/utils'; import { DatabaseAnalyzer } from 'src/modules/database-analysis/providers/database-analyzer'; @@ -57,7 +57,7 @@ export class DatabaseAnalysisService { }); const recommendations = DatabaseAnalysisService.getRecommendationsSummary( - flatten(await Promise.all( + await Promise.all( scanResults.map(async (nodeResult) => ( await this.recommendationService.getRecommendations({ client: nodeResult.client, @@ -65,7 +65,7 @@ export class DatabaseAnalysisService { total: progress.total, }) )), - )), + ), ); const analysis = plainToClass(DatabaseAnalysis, await this.analyzer.analyze({ databaseId: clientOptions.instanceId, @@ -107,8 +107,23 @@ export class DatabaseAnalysisService { * @param recommendations */ - static getRecommendationsSummary(recommendations: Recommendation[]): Recommendation[] { - // if we will sent any values with recommendations, they will be calculate here - return uniqWith(recommendations, isEqual); + static getRecommendationsSummary(recommendations: Recommendation[][]): Recommendation[] { + const mergedRecommendations = recommendations.reduce((acc, nodeRecommendations) => { + nodeRecommendations.forEach((recommendation) => { + if (!acc[recommendation.name]) { + acc[recommendation.name] = recommendation; + } else { + acc[recommendation.name] = { + name: recommendation.name, + isActual: recommendation.isActual || acc[recommendation.name].isActual, + // merge other fields here + }; + } + }); + return acc; + }, {}); + return Object.values(mergedRecommendations) + .filter((rec: Recommendation) => rec.isActual) + .map((recommendation: Recommendation) => omit(recommendation, 'isActual')); } } diff --git a/redisinsight/api/src/modules/database-analysis/models/recommendation.ts b/redisinsight/api/src/modules/database-analysis/models/recommendation.ts index e3910bbfe1..6e59156b0b 100644 --- a/redisinsight/api/src/modules/database-analysis/models/recommendation.ts +++ b/redisinsight/api/src/modules/database-analysis/models/recommendation.ts @@ -1,5 +1,5 @@ import { Expose } from 'class-transformer'; -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export class Recommendation { @ApiProperty({ @@ -9,4 +9,11 @@ export class Recommendation { }) @Expose() name: string; + + @ApiPropertyOptional({ + description: 'Is recommendation actual for database', + type: Boolean, + }) + @Expose() + isActual?: boolean; } diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index c677f66e90..bed35d6483 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -3,6 +3,7 @@ import { Redis, Command } from 'ioredis'; import { get } from 'lodash'; import { convertRedisInfoReplyToObject } from 'src/utils'; import { RedisDataType } from 'src/modules/browser/dto'; +import { Recommendation } from 'src/modules/database-analysis/models/recommendation'; import { Key } from 'src/modules/database-analysis/models'; const minNumberOfCachedScripts = 10; @@ -17,7 +18,7 @@ export class RecommendationProvider { */ async determineLuaScriptRecommendation( redisClient: Redis, - ): Promise { + ): Promise { const info = convertRedisInfoReplyToObject( await redisClient.sendCommand( new Command('info', [], { replyEncoding: 'utf8' }), @@ -25,7 +26,10 @@ export class RecommendationProvider { ); const nodesNumbersOfCachedScripts = get(info, 'memory.number_of_cached_scripts'); - return parseInt(nodesNumbersOfCachedScripts, 10) > minNumberOfCachedScripts; + return ({ + name: 'luaScript', + isActual: parseInt(nodesNumbersOfCachedScripts, 10) > minNumberOfCachedScripts, + }); } /** @@ -34,9 +38,12 @@ export class RecommendationProvider { */ async determineBigHashesRecommendation( keys: Key[], - ): Promise { + ): Promise { const bigHashes = keys.filter((key) => key.type === RedisDataType.Hash && key.length > maxHashLength); - return bigHashes.length > 0; + return ({ + name: 'bigHashes', + isActual: bigHashes.length > 0, + }); } /** @@ -45,7 +52,10 @@ export class RecommendationProvider { */ async determineBigTotalRecommendation( total: number, - ): Promise { - return total > maxDatabaseTotal; + ): Promise { + return ({ + name: 'useSmallerKeys', + isActual: total > maxDatabaseTotal, + }); } } diff --git a/redisinsight/api/src/modules/recommendation/recommendation.service.ts b/redisinsight/api/src/modules/recommendation/recommendation.service.ts index a8530688cf..8a50722cfa 100644 --- a/redisinsight/api/src/modules/recommendation/recommendation.service.ts +++ b/redisinsight/api/src/modules/recommendation/recommendation.service.ts @@ -33,17 +33,10 @@ export class RecommendationService { total, } = dto; - const recommendations = []; - // TODO refactor it - if (await this.recommendationProvider.determineLuaScriptRecommendation(client)) { - recommendations.push({ name: 'luaScript' }); - } - if (await this.recommendationProvider.determineBigHashesRecommendation(keys)) { - recommendations.push({ name: 'bigHashes' }); - } - if (await this.recommendationProvider.determineBigTotalRecommendation(total)) { - recommendations.push({ name: 'useSmallerKeys' }); - } - return recommendations; + return ([ + await this.recommendationProvider.determineLuaScriptRecommendation(client), + await this.recommendationProvider.determineBigHashesRecommendation(keys), + await this.recommendationProvider.determineBigTotalRecommendation(total), + ]); } } From 074998a1bcae16b01777f4773c6c89ac32111e81 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Wed, 16 Nov 2022 13:40:08 +0400 Subject: [PATCH 021/108] RI-3527-resolve comments --- .../database-analysis.service.ts | 29 +++++-------------- .../models/recommendation.ts | 9 +----- .../providers/recommendation.provider.ts | 19 ++++-------- .../GET-databases-id-analysis-id.test.ts | 2 +- .../POST-databases-id-analysis.test.ts | 6 ++-- redisinsight/api/test/helpers/constants.ts | 8 ++++- redisinsight/api/test/helpers/local-db.ts | 2 +- 7 files changed, 27 insertions(+), 48 deletions(-) diff --git a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts index 3059ab4eeb..2fa3069476 100644 --- a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts +++ b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts @@ -1,6 +1,6 @@ import { HttpException, Injectable, Logger } from '@nestjs/common'; import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; -import { omit } from 'lodash'; +import { isNull, flatten, uniqBy } from 'lodash'; import { RecommendationService } from 'src/modules/recommendation/recommendation.service'; import { catchAclError } from 'src/utils'; import { DatabaseAnalyzer } from 'src/modules/database-analysis/providers/database-analyzer'; @@ -57,7 +57,7 @@ export class DatabaseAnalysisService { }); const recommendations = DatabaseAnalysisService.getRecommendationsSummary( - await Promise.all( + flatten(await Promise.all( scanResults.map(async (nodeResult) => ( await this.recommendationService.getRecommendations({ client: nodeResult.client, @@ -65,7 +65,7 @@ export class DatabaseAnalysisService { total: progress.total, }) )), - ), + )), ); const analysis = plainToClass(DatabaseAnalysis, await this.analyzer.analyze({ databaseId: clientOptions.instanceId, @@ -107,23 +107,10 @@ export class DatabaseAnalysisService { * @param recommendations */ - static getRecommendationsSummary(recommendations: Recommendation[][]): Recommendation[] { - const mergedRecommendations = recommendations.reduce((acc, nodeRecommendations) => { - nodeRecommendations.forEach((recommendation) => { - if (!acc[recommendation.name]) { - acc[recommendation.name] = recommendation; - } else { - acc[recommendation.name] = { - name: recommendation.name, - isActual: recommendation.isActual || acc[recommendation.name].isActual, - // merge other fields here - }; - } - }); - return acc; - }, {}); - return Object.values(mergedRecommendations) - .filter((rec: Recommendation) => rec.isActual) - .map((recommendation: Recommendation) => omit(recommendation, 'isActual')); + static getRecommendationsSummary(recommendations: Recommendation[]): Recommendation[] { + return uniqBy( + recommendations.filter((recommendation) => !isNull(recommendation)), + 'name', + ); } } diff --git a/redisinsight/api/src/modules/database-analysis/models/recommendation.ts b/redisinsight/api/src/modules/database-analysis/models/recommendation.ts index 6e59156b0b..e3910bbfe1 100644 --- a/redisinsight/api/src/modules/database-analysis/models/recommendation.ts +++ b/redisinsight/api/src/modules/database-analysis/models/recommendation.ts @@ -1,5 +1,5 @@ import { Expose } from 'class-transformer'; -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiProperty } from '@nestjs/swagger'; export class Recommendation { @ApiProperty({ @@ -9,11 +9,4 @@ export class Recommendation { }) @Expose() name: string; - - @ApiPropertyOptional({ - description: 'Is recommendation actual for database', - type: Boolean, - }) - @Expose() - isActual?: boolean; } diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index bed35d6483..4966f153aa 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -26,10 +26,9 @@ export class RecommendationProvider { ); const nodesNumbersOfCachedScripts = get(info, 'memory.number_of_cached_scripts'); - return ({ - name: 'luaScript', - isActual: parseInt(nodesNumbersOfCachedScripts, 10) > minNumberOfCachedScripts, - }); + return parseInt(nodesNumbersOfCachedScripts, 10) > minNumberOfCachedScripts + ? { name: 'luaScript' } + : null; } /** @@ -39,11 +38,8 @@ export class RecommendationProvider { async determineBigHashesRecommendation( keys: Key[], ): Promise { - const bigHashes = keys.filter((key) => key.type === RedisDataType.Hash && key.length > maxHashLength); - return ({ - name: 'bigHashes', - isActual: bigHashes.length > 0, - }); + const bigHashes = keys.some((key) => key.type === RedisDataType.Hash && key.length > maxHashLength); + return bigHashes ? { name: 'bigHashes' } : null; } /** @@ -53,9 +49,6 @@ export class RecommendationProvider { async determineBigTotalRecommendation( total: number, ): Promise { - return ({ - name: 'useSmallerKeys', - isActual: total > maxDatabaseTotal, - }); + return total > maxDatabaseTotal ? { name: 'useSmallerKeys' } : null; } } diff --git a/redisinsight/api/test/api/database-analysis/GET-databases-id-analysis-id.test.ts b/redisinsight/api/test/api/database-analysis/GET-databases-id-analysis-id.test.ts index 9c4709a0cc..a6c6acb5cc 100644 --- a/redisinsight/api/test/api/database-analysis/GET-databases-id-analysis-id.test.ts +++ b/redisinsight/api/test/api/database-analysis/GET-databases-id-analysis-id.test.ts @@ -38,7 +38,7 @@ describe('GET /databases/:id/analysis/:id', () => { topKeysLength: [constants.TEST_DATABASE_ANALYSIS_TOP_KEYS_1], topKeysMemory: [constants.TEST_DATABASE_ANALYSIS_TOP_KEYS_1], expirationGroups: [constants.TEST_DATABASE_ANALYSIS_EXPIRATION_GROUP_1], - recommendations: [constants.TEST_DATABASE_ANALYSIS_RECOMMENDATION_1], + recommendations: [constants.TEST_LUA_DATABASE_ANALYSIS_RECOMMENDATION], }); } }, diff --git a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts index 4f07b395af..a800d7c27d 100644 --- a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts +++ b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts @@ -167,7 +167,7 @@ describe('POST /databases/:instanceId/analysis', () => { expect(body.topMemoryNsp.length).to.gt(0); expect(body.topKeysLength.length).to.gt(0); expect(body.topKeysMemory.length).to.gt(0); - expect(body.recommendations).to.deep.eq([{ name: 'useSmallerKeys'}]); + expect(body.recommendations).to.deep.eq([constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION]); }, after: async () => { expect(await repository.count()).to.eq(5); @@ -191,7 +191,7 @@ describe('POST /databases/:instanceId/analysis', () => { expect(body.topMemoryNsp.length).to.gt(0); expect(body.topKeysLength.length).to.gt(0); expect(body.topKeysMemory.length).to.gt(0); - expect(body.recommendations).to.deep.eq([{ name: 'bigHashes'}]); + expect(body.recommendations).to.deep.eq([constants.TEST_BIG_HASHES_DATABASE_ANALYSIS_RECOMMENDATION]); }, after: async () => { expect(await repository.count()).to.eq(5); @@ -208,7 +208,7 @@ describe('POST /databases/:instanceId/analysis', () => { await rte.data.generateNCachedScripts(11, true); }, checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([{ name: 'luaScript'}]); + expect(body.recommendations).to.deep.eq([constants.TEST_LUA_DATABASE_ANALYSIS_RECOMMENDATION]); }, after: async () => { expect(await repository.count()).to.eq(5); diff --git a/redisinsight/api/test/helpers/constants.ts b/redisinsight/api/test/helpers/constants.ts index 7668348f26..115662bb0c 100644 --- a/redisinsight/api/test/helpers/constants.ts +++ b/redisinsight/api/test/helpers/constants.ts @@ -451,9 +451,15 @@ export const constants = { threshold: 4 * 60 * 60 * 1000, }, - TEST_DATABASE_ANALYSIS_RECOMMENDATION_1: { + TEST_LUA_DATABASE_ANALYSIS_RECOMMENDATION: { name: 'luaScript', }, + TEST_BIG_HASHES_DATABASE_ANALYSIS_RECOMMENDATION: { + name: 'bigHashes', + }, + TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION: { + name: 'useSmallerKeys', + }, // etc... } diff --git a/redisinsight/api/test/helpers/local-db.ts b/redisinsight/api/test/helpers/local-db.ts index e7deea22a2..d2114ee2ff 100644 --- a/redisinsight/api/test/helpers/local-db.ts +++ b/redisinsight/api/test/helpers/local-db.ts @@ -176,7 +176,7 @@ export const generateNDatabaseAnalysis = async ( expirationGroups: encryptData(JSON.stringify([ constants.TEST_DATABASE_ANALYSIS_EXPIRATION_GROUP_1, ])), - recommendations: encryptData(JSON.stringify([constants.TEST_DATABASE_ANALYSIS_RECOMMENDATION_1])), + recommendations: encryptData(JSON.stringify([constants.TEST_LUA_DATABASE_ANALYSIS_RECOMMENDATION])), createdAt: new Date(), encryption: constants.TEST_ENCRYPTION_STRATEGY, ...partial, From 058878b30c93719038cf9f23fc601d8ecb5efd71 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 17 Nov 2022 13:52:21 +0400 Subject: [PATCH 022/108] #RI-3527-fix recommendation styles --- .../components/recommendations-view/styles.module.scss | 4 ++-- .../ui/src/styles/themes/dark_theme/_dark_theme.lazy.scss | 1 + .../ui/src/styles/themes/dark_theme/_theme_color.scss | 1 + .../ui/src/styles/themes/light_theme/_light_theme.lazy.scss | 1 + .../ui/src/styles/themes/light_theme/_theme_color.scss | 1 + 5 files changed, 6 insertions(+), 2 deletions(-) diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss index 35851a091c..4360fd75c3 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss @@ -28,7 +28,7 @@ .recommendation { border-radius: 8px; - border: 1px solid #323232; + border: 1px solid var(--recommendationBorderColor); background-color: var(--euiColorLightestShade); margin-bottom: 6px; padding: 18px; @@ -66,7 +66,7 @@ margin-right: 48px; .badge { - margin: 18px 18px 14px; + margin: 14px 18px; .badgeIcon { margin-right: 14px; diff --git a/redisinsight/ui/src/styles/themes/dark_theme/_dark_theme.lazy.scss b/redisinsight/ui/src/styles/themes/dark_theme/_dark_theme.lazy.scss index 39a11d6eeb..01ad94bc75 100644 --- a/redisinsight/ui/src/styles/themes/dark_theme/_dark_theme.lazy.scss +++ b/redisinsight/ui/src/styles/themes/dark_theme/_dark_theme.lazy.scss @@ -178,4 +178,5 @@ // Database analysis --badgeIconColor: #{$badgeIconColor}; + --recommendationBorderColor: #{$recommendationBorderColor}; } diff --git a/redisinsight/ui/src/styles/themes/dark_theme/_theme_color.scss b/redisinsight/ui/src/styles/themes/dark_theme/_theme_color.scss index 7d7a086ee5..1d178f1d72 100644 --- a/redisinsight/ui/src/styles/themes/dark_theme/_theme_color.scss +++ b/redisinsight/ui/src/styles/themes/dark_theme/_theme_color.scss @@ -138,3 +138,4 @@ $pubSubClientsBadge: #008000; // Database analysis $badgeIconColor : #D8AB52; +$recommendationBorderColor: #363636; diff --git a/redisinsight/ui/src/styles/themes/light_theme/_light_theme.lazy.scss b/redisinsight/ui/src/styles/themes/light_theme/_light_theme.lazy.scss index b413a7a29a..ec1ff06f06 100644 --- a/redisinsight/ui/src/styles/themes/light_theme/_light_theme.lazy.scss +++ b/redisinsight/ui/src/styles/themes/light_theme/_light_theme.lazy.scss @@ -180,4 +180,5 @@ // Database analysis --badgeIconColor: #{$badgeIconColor}; + --recommendationBorderColor: #{$recommendationBorderColor}; } diff --git a/redisinsight/ui/src/styles/themes/light_theme/_theme_color.scss b/redisinsight/ui/src/styles/themes/light_theme/_theme_color.scss index c7da8d8e30..dcf5134c1a 100644 --- a/redisinsight/ui/src/styles/themes/light_theme/_theme_color.scss +++ b/redisinsight/ui/src/styles/themes/light_theme/_theme_color.scss @@ -135,3 +135,4 @@ $pubSubClientsBadge: #b5cea8; // Database analysis $badgeIconColor : #415681; +$recommendationBorderColor: #3953c3; From 027308fb7a452bcf6a5d81c111b81ce6453d432a Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Thu, 17 Nov 2022 13:37:01 +0100 Subject: [PATCH 023/108] add tests for use smaller keys and big hash --- .../e2e/pageObjects/memory-efficiency-page.ts | 4 + .../memory-efficiency/recommendations.e2e.ts | 101 +++++++++++++----- 2 files changed, 77 insertions(+), 28 deletions(-) diff --git a/tests/e2e/pageObjects/memory-efficiency-page.ts b/tests/e2e/pageObjects/memory-efficiency-page.ts index 712c12ca0a..4cb4947193 100644 --- a/tests/e2e/pageObjects/memory-efficiency-page.ts +++ b/tests/e2e/pageObjects/memory-efficiency-page.ts @@ -29,6 +29,8 @@ export class MemoryEfficiencyPage { topNamespacesEmptyContainer = Selector('[data-testid=top-namespaces-empty]'); topNamespacesEmptyMessage = Selector('[data-testid=top-namespaces-message]'); noRecommendationsMessage = Selector('[data-testid=empty-recommendations-message]'); + codeChangesLabel = Selector('[data-testid=code_changes]'); + configurationChangesLabel = Selector('[data-testid=configuration_changes]'); // TABLE namespaceTable = Selector('[data-testid=nsp-table-memory]'); nameSpaceTableRows = this.namespaceTable.find('[data-testid^=row-]'); @@ -49,4 +51,6 @@ export class MemoryEfficiencyPage { // CONTAINERS luaScriptAccordion = Selector('[data-testid=luaScript-accordion]'); luaScriptTextContainer = Selector('#luaScript'); + useSmallKeysAccordion = Selector('[data-testid=useSmallerKeys-accordion]'); + bigHashesAccordion = Selector('[data-testid=bigHashes-accordion]'); } diff --git a/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts b/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts index 68d784ba7a..12cdbbfac3 100644 --- a/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts +++ b/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts @@ -1,57 +1,102 @@ -import { MyRedisDatabasePage, MemoryEfficiencyPage } from '../../../pageObjects'; +import { MyRedisDatabasePage, MemoryEfficiencyPage, BrowserPage, CliPage } from '../../../pageObjects'; import { rte } from '../../../helpers/constants'; import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; -import { commonUrl, ossStandaloneBigConfig } from '../../../helpers/conf'; +import { commonUrl, ossStandaloneBigConfig, ossStandaloneConfig } from '../../../helpers/conf'; import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; import { CliActions } from '../../../common-actions/cli-actions'; import { Common } from '../../../helpers/common'; +import { populateHashWithFields } from '../../../helpers/keys'; const memoryEfficiencyPage = new MemoryEfficiencyPage(); const myRedisDatabasePage = new MyRedisDatabasePage(); const cliActions = new CliActions(); const common = new Common(); +const browserPage = new BrowserPage(); +const cliPage = new CliPage(); + +const externalPageLink = 'https://docs.redis.com/latest/ri/memory-optimizations/'; +const keyName = `hugeHashKey-${common.generateWord(10)}`; fixture `Memory Efficiency Recommendations` .meta({ type: 'critical_path', rte: rte.standalone }) - .page(commonUrl); -test - .before(async t => { + .page(commonUrl) + .beforeEach(async t => { await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); // Go to Analysis Tools page await t.click(myRedisDatabasePage.analysisPageButton); await t.click(memoryEfficiencyPage.newReportBtn); }) - .after(async() => { + .afterEach(async() => { + await cliPage.sendCommandInCli(`SCRIPT FLUSH`); await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); - })('Avoid dynamic Lua script recommendation', async t => { + }); +test('Avoid dynamic Lua script recommendation', async t => { + // Go to Recommendations tab + await t.click(memoryEfficiencyPage.recommendationsTab); + // Add cached scripts and generate new report + await cliActions.addCachedScripts(10); + await t.click(memoryEfficiencyPage.newReportBtn); + // No recommendation with 10 cached scripts + await t.expect(memoryEfficiencyPage.luaScriptAccordion.exists).notOk('Avoid dynamic lua script recommendation displayed with 10 cached scripts'); + // Add the last cached script to see the recommendation + await cliActions.addCachedScripts(1); + await t.click(memoryEfficiencyPage.newReportBtn); + // Verify that user can see Avoid dynamic Lua script recommendation when number_of_cached_scripts> 10 + await t.expect(memoryEfficiencyPage.luaScriptAccordion.exists).ok('Avoid dynamic lua script recommendation not displayed'); + + // Verify that user can see Use smaller keys recommendation when database has 1M+ keys + await t.expect(memoryEfficiencyPage.useSmallKeysAccordion.exists).ok('Use smaller keys recommendation not displayed'); + + // Verify that user can expand/collapse recommendation + const expandedTextConaiterSize = await memoryEfficiencyPage.luaScriptTextContainer.offsetHeight; + await t.click(memoryEfficiencyPage.luaScriptButton); + await t.expect(memoryEfficiencyPage.luaScriptTextContainer.offsetHeight).lt(expandedTextConaiterSize, 'Recommendation not collapsed'); + await t.click(memoryEfficiencyPage.luaScriptButton); + await t.expect(memoryEfficiencyPage.luaScriptTextContainer.offsetHeight).eql(expandedTextConaiterSize, 'Recommendation not expanded'); + + // Verify that user can navigate by link to see the recommendation + await t.click(memoryEfficiencyPage.readMoreLink); + await common.checkURL(externalPageLink); + // Close the window with external link to switch to the application window + await t.closeWindow(); +}); +test + .before(async t => { + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await browserPage.addHashKey(keyName, '2147476121', 'field', 'value'); + // Go to Analysis Tools page + await t.click(myRedisDatabasePage.analysisPageButton); + await t.click(memoryEfficiencyPage.newReportBtn); + }) + .after(async t => { + // Clear and delete database + await t.click(myRedisDatabasePage.browserButton); + await browserPage.deleteKeyByName(keyName); + await deleteStandaloneDatabaseApi(ossStandaloneConfig); + })('Shard big hashes to small hashes recommendation', async t => { const noRecommendationsMessage = 'No Recommendations at the moment.'; - const externalPageLink = 'https://docs.redis.com/latest/ri/memory-optimizations/'; + // const dbParameters = { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port }; + const dbParameters = { host: 'localhost', port: '8100' }; + const keyToAddParameters = { fieldsCount: 4999, keyName, fieldStartWith: 'hashField', fieldValueStartWith: 'hashValue' }; + const keyToAddParameters2 = { fieldsCount: 1, keyName, fieldStartWith: 'hashFieldLast', fieldValueStartWith: 'hashValueLast' }; // Go to Recommendations tab await t.click(memoryEfficiencyPage.recommendationsTab); // No recommendations message await t.expect(memoryEfficiencyPage.noRecommendationsMessage.textContent).eql(noRecommendationsMessage, 'No recommendations message not displayed'); - // Add cached scripts and generate new report - await cliActions.addCachedScripts(10); + + // Add 5000 fields to the hash key + await populateHashWithFields(dbParameters.host, dbParameters.port, keyToAddParameters); + // Generate new report await t.click(memoryEfficiencyPage.newReportBtn); - // No recommendations message with 10 cached scripts + // Verify that big keys recommendation not displayed when hash has 5000 fields await t.expect(memoryEfficiencyPage.noRecommendationsMessage.textContent).eql(noRecommendationsMessage, 'No recommendations message not displayed'); - // Add the last cached script to see the recommendation - await cliActions.addCachedScripts(1); + // Add the last field in hash key + await populateHashWithFields(dbParameters.host, dbParameters.port, keyToAddParameters2); + // Generate new report await t.click(memoryEfficiencyPage.newReportBtn); - // Verify that user can see Avoid dynamic Lua script recommendation when number_of_cached_scripts> 10 - await t.expect(memoryEfficiencyPage.luaScriptAccordion.exists).ok('Avoid dynamic lua script recommendation not displayed'); - - // Verify that user can expand/collapse recommendation - const expandedTextConaiterSize = await memoryEfficiencyPage.luaScriptTextContainer.offsetHeight; - await t.click(memoryEfficiencyPage.luaScriptButton); - await t.expect(await memoryEfficiencyPage.luaScriptTextContainer.offsetHeight).lt(expandedTextConaiterSize, 'Recommendation not collapsed'); - await t.click(memoryEfficiencyPage.luaScriptButton); - await t.expect(await memoryEfficiencyPage.luaScriptTextContainer.offsetHeight).eql(expandedTextConaiterSize, 'Recommendation not expanded'); - - // Verify that user can navigate by link to see the recommendation - await t.click(memoryEfficiencyPage.readMoreLink); - await common.checkURL(externalPageLink); - // Close the window with external link to switch to the application window - await t.closeWindow(); + // Verify that user can see Shard big hashes to small hashes recommendation when Hash length > 5,000 + await t.expect(memoryEfficiencyPage.bigHashesAccordion.exists).ok('Shard big hashes to small hashes recommendation not displayed'); + await t.expect(memoryEfficiencyPage.codeChangesLabel.exists).ok('Big hashes recommendation not have Code Changes label'); + await t.expect(memoryEfficiencyPage.configurationChangesLabel.exists).ok('Big hashes recommendation not have Configuration Changes label'); }); From bff1701bb4d552269357f7243b79de97fdfc83a3 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Thu, 17 Nov 2022 13:38:54 +0100 Subject: [PATCH 024/108] upd --- .../critical-path/memory-efficiency/recommendations.e2e.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts b/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts index 12cdbbfac3..772355c16d 100644 --- a/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts +++ b/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts @@ -27,7 +27,7 @@ fixture `Memory Efficiency Recommendations` await t.click(memoryEfficiencyPage.newReportBtn); }) .afterEach(async() => { - await cliPage.sendCommandInCli(`SCRIPT FLUSH`); + await cliPage.sendCommandInCli('SCRIPT FLUSH'); await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); }); test('Avoid dynamic Lua script recommendation', async t => { From 14b9182a11fc68bfbee74890dd9decfbea31c82d Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Thu, 17 Nov 2022 13:39:29 +0100 Subject: [PATCH 025/108] change host and port --- .../critical-path/memory-efficiency/recommendations.e2e.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts b/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts index 772355c16d..9cf475a009 100644 --- a/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts +++ b/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts @@ -75,8 +75,7 @@ test await deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Shard big hashes to small hashes recommendation', async t => { const noRecommendationsMessage = 'No Recommendations at the moment.'; - // const dbParameters = { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port }; - const dbParameters = { host: 'localhost', port: '8100' }; + const dbParameters = { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port }; const keyToAddParameters = { fieldsCount: 4999, keyName, fieldStartWith: 'hashField', fieldValueStartWith: 'hashValue' }; const keyToAddParameters2 = { fieldsCount: 1, keyName, fieldStartWith: 'hashFieldLast', fieldValueStartWith: 'hashValueLast' }; From f6418ebbaee323a8ac01fe9bccea8659c346bb7b Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Tue, 29 Nov 2022 07:31:23 +0400 Subject: [PATCH 026/108] #RI-3571,3564,3560 - recommendations --- .../providers/recommendation.provider.ts | 60 ++++++++++++++++++- .../recommendation/recommendation.service.ts | 3 + .../POST-databases-id-analysis.test.ts | 43 ++++++++++++- redisinsight/api/test/helpers/constants.ts | 6 ++ redisinsight/api/test/helpers/data/redis.ts | 20 +++++++ 5 files changed, 127 insertions(+), 5 deletions(-) diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index 4966f153aa..11b7769273 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -1,13 +1,14 @@ import { Injectable } from '@nestjs/common'; -import { Redis, Command } from 'ioredis'; +import { Redis, Cluster, Command } from 'ioredis'; import { get } from 'lodash'; -import { convertRedisInfoReplyToObject } from 'src/utils'; +import { convertRedisInfoReplyToObject, convertBulkStringsToObject } from 'src/utils'; import { RedisDataType } from 'src/modules/browser/dto'; import { Recommendation } from 'src/modules/database-analysis/models/recommendation'; import { Key } from 'src/modules/database-analysis/models'; const minNumberOfCachedScripts = 10; const maxHashLength = 5000; +const maxStringLength = 200; const maxDatabaseTotal = 1_000_000; @Injectable() @@ -51,4 +52,59 @@ export class RecommendationProvider { ): Promise { return total > maxDatabaseTotal ? { name: 'useSmallerKeys' } : null; } + + /** + * Check logical databases recommendation + * @param redisClient + */ + async determineLogicalDatabasesRecommendation( + redisClient: Redis | Cluster, + ): Promise { + if (redisClient.isCluster) { + return null; + } + const info = convertRedisInfoReplyToObject( + await redisClient.info(), + ); + const keyspace = get(info, 'keyspace', {}); + const databasesWithKeys = Object.values(keyspace).filter((db) => { + const { keys } = convertBulkStringsToObject(db as string, ',', '='); + return keys > 0; + }); + return databasesWithKeys.length > 1 ? { name: 'avoidLogicalDatabases' } : null; + } + + /** + * Check combine small strings to hashes recommendation + * @param keys + */ + async determineCombineSmallStringsToHashesRecommendation( + keys: Key[], + ): Promise { + const smallString = keys.some((key) => key.type === RedisDataType.String && key.memory < maxStringLength); + return smallString ? { name: 'combineSmallStringsToHashes' } : null; + } + + /** + * Check increase set max intset entries recommendation + * @param keys + * @param redisClient + */ + async determineIncreaseSetMaxIntsetEntriesRecommendation( + redisClient: Redis | Cluster, + keys: Key[], + ): Promise { + const [, setMaxIntsetEntries] = await redisClient.sendCommand( + new Command('config', ['get', 'set-max-intset-entries'], { + replyEncoding: 'utf8', + }), + ) as string[]; + + if (!setMaxIntsetEntries) { + return null; + } + + const bigSet = keys.some((key) => key.type === RedisDataType.Set && key.length > parseInt(setMaxIntsetEntries, 10)); + return bigSet ? { name: 'increaseSetMaxIntsetEntries' } : null; + } } diff --git a/redisinsight/api/src/modules/recommendation/recommendation.service.ts b/redisinsight/api/src/modules/recommendation/recommendation.service.ts index 8a50722cfa..ece8b925b8 100644 --- a/redisinsight/api/src/modules/recommendation/recommendation.service.ts +++ b/redisinsight/api/src/modules/recommendation/recommendation.service.ts @@ -37,6 +37,9 @@ export class RecommendationService { await this.recommendationProvider.determineLuaScriptRecommendation(client), await this.recommendationProvider.determineBigHashesRecommendation(keys), await this.recommendationProvider.determineBigTotalRecommendation(total), + await this.recommendationProvider.determineLogicalDatabasesRecommendation(client), + await this.recommendationProvider.determineCombineSmallStringsToHashesRecommendation(keys), + await this.recommendationProvider.determineIncreaseSetMaxIntsetEntriesRecommendation(client, keys), ]); } } diff --git a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts index a800d7c27d..a038ec076c 100644 --- a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts +++ b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts @@ -64,7 +64,6 @@ describe('POST /databases/:instanceId/analysis', () => { expect(body.topKeysLength.length).to.gt(0); expect(body.topKeysMemory.length).to.gt(0); expect(body.expirationGroups.length).to.gt(0); - expect(body.recommendations.length).to.eq(0); }, after: async () => { expect(await repository.count()).to.eq(5); @@ -143,7 +142,6 @@ describe('POST /databases/:instanceId/analysis', () => { expect(body.expirationGroups[0].label).to.eq('No Expiry'); expect(body.expirationGroups[0].total).to.gt(0); expect(body.expirationGroups[0].threshold).to.eq(0); - expect(body.recommendations.length).to.eq(0); }, after: async () => { expect(await repository.count()).to.eq(5); @@ -167,7 +165,11 @@ describe('POST /databases/:instanceId/analysis', () => { expect(body.topMemoryNsp.length).to.gt(0); expect(body.topKeysLength.length).to.gt(0); expect(body.topKeysMemory.length).to.gt(0); - expect(body.recommendations).to.deep.eq([constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION]); + expect(body.recommendations).to.deep.eq([ + constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, + // generateNKeys generated small strings + constants.TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION, + ]); }, after: async () => { expect(await repository.count()).to.eq(5); @@ -197,6 +199,41 @@ describe('POST /databases/:instanceId/analysis', () => { expect(await repository.count()).to.eq(5); } }, + { + name: 'Should create new database analysis with increaseSetMaxIntsetEntries recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + const NUMBERS_OF_SET_MEMBERS = 513; + await rte.data.generateHugeNumberOfMembersForSetKey(NUMBERS_OF_SET_MEMBERS, true); + }, + checkFn: async ({ body }) => { + expect(body.recommendations).to.deep.eq([constants.TEST_INCREASE_SET_MAX_INTSET_ENTRIES_RECOMMENDATION]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } + }, + { + name: 'Should create new database analysis with combineSmallStringsToHashes recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + await rte.data.generateStrings(true); + }, + checkFn: async ({ body }) => { + expect(body.recommendations).to.deep.eq([constants.TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } + }, { name: 'Should create new database analysis with luaScript recommendation', data: { diff --git a/redisinsight/api/test/helpers/constants.ts b/redisinsight/api/test/helpers/constants.ts index 115662bb0c..a27b142223 100644 --- a/redisinsight/api/test/helpers/constants.ts +++ b/redisinsight/api/test/helpers/constants.ts @@ -460,6 +460,12 @@ export const constants = { TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION: { name: 'useSmallerKeys', }, + TEST_INCREASE_SET_MAX_INTSET_ENTRIES_RECOMMENDATION: { + name: 'increaseSetMaxIntsetEntries', + }, + TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION: { + name: 'combineSmallStringsToHashes', + }, // etc... } diff --git a/redisinsight/api/test/helpers/data/redis.ts b/redisinsight/api/test/helpers/data/redis.ts index d54e3eb56a..c6c93ae14b 100644 --- a/redisinsight/api/test/helpers/data/redis.ts +++ b/redisinsight/api/test/helpers/data/redis.ts @@ -386,6 +386,25 @@ export const initDataHelper = (rte) => { } while (inserted < number) }; + const generateHugeNumberOfMembersForSetKey = async (number: number = 100000, clean: boolean) => { + if (clean) { + await truncate(); + } + + const batchSize = 10000; + let inserted = 0; + do { + const pipeline = []; + const limit = inserted + batchSize; + for (inserted; inserted < limit && inserted < number; inserted++) { + pipeline.push(['sadd', constants.TEST_SET_KEY_1, inserted]); + } + + await insertKeysBasedOnEnv(pipeline, true); + } while (inserted < number) + }; + + const generateHugeNumberOfTinyStringKeys = async (number: number = 100000, clean: boolean) => { if (clean) { await truncate(); @@ -490,6 +509,7 @@ export const initDataHelper = (rte) => { generateNStreams, generateNGraphs, generateNCachedScripts, + generateHugeNumberOfMembersForSetKey, getClientNodes, } } From e79d94999e0dab208fad82a5c7d1f5a8ffa35411 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Tue, 29 Nov 2022 07:35:39 +0400 Subject: [PATCH 027/108] #RI-3571-3564-3560 - recommendations --- .../constants/dbAnalysisRecommendations.json | 95 +++++++++++++++++++ .../Recommendations.spec.tsx | 45 +++++++++ .../components/recommendations-view/utils.tsx | 2 +- 3 files changed, 141 insertions(+), 1 deletion(-) diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index d466f41583..c2306013bd 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -58,5 +58,100 @@ } ], "badges": ["code_changes", "configuration_changes"] + }, + "avoidLogicalDatabases": { + "id": "avoidLogicalDatabases", + "title": "Avoid using logical databases", + "content": [ + { + "id": "1", + "type": "paragraph", + "value": "Redis supports multiple logical databases within an instance, though these logical databases are neither independent nor isolated in any other way and can freeze each other." + }, + { + "id": "2", + "type": "span", + "value": "Also, they are not supported by any clustering system (open source or Redis Enterprise clustering), and some modules do not support numbered databases as well. " + }, + { + "id": "3", + "type": "link", + "value": { + "href": "https://docs.redis.com/latest/ri/memory-optimizations/", + "name": "Read more" + } + } + ], + "badges": ["code_changes"] + }, + "combineSmallStringsToHashes": { + "id": "combineSmallStringsToHashes", + "title": "Combine small strings to hashes", + "content": [ + { + "id": "1", + "type": "paragraph", + "value": "Strings data type has an overhead of about 90 bytes on a 64-bit machine, so if there is no need for different expiration values for these keys, combine small strings into a larger hash to optimize the memory usage." + }, + { + "id": "2", + "type": "paragraph", + "value": "Also, ensure that the hash has less than hash-max-ziplist-entries elements and the size of each element is within hash-max-ziplist-values bytes." + }, + { + "id": "3", + "type": "spacer", + "value": "l" + }, + { + "id": "4", + "type": "span", + "value": "Though this approach should not be used if you need different expiration values for String keys. " + }, + { + "id": "5", + "type": "link", + "value": { + "href": "https://docs.redis.com/latest/ri/memory-optimizations/", + "name": "Read more" + } + } + ], + "badges": ["code_changes"] + }, + "increaseSetMaxIntsetEntries": { + "id": "increaseSetMaxIntsetEntries", + "title": "Increase the set-max-intset-entries", + "content": [ + { + "id": "1", + "type": "paragraph", + "value": "Several set values with IntSet encoding exceed the set-max-intset-entries. Change the configuration in reds.conf to efficiently use the IntSet encoding." + }, + { + "id": "2", + "type": "paragraph", + "value": "Though increasing this value will lead to an increase in latency of set operations and CPU utilization." + }, + { + "id": "3", + "type": "spacer", + "value": "l" + }, + { + "id": "4", + "type": "span", + "value": "Run `INFO COMMANDSTATS` before and after making this change to verify the latency numbers. " + }, + { + "id": "5", + "type": "link", + "value": { + "href": "https://docs.redis.com/latest/ri/memory-optimizations/", + "name": "Read more" + } + } + ], + "badges": ["configuration_changes"] } } diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx index cc044861d2..9bcac116e3 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx @@ -142,6 +142,51 @@ describe('Recommendations', () => { expect(screen.queryByTestId('configuration_changes')).toBeInTheDocument() }) + it('should render code changes badge and configuration_changes in avoidLogicalDatabases recommendation', () => { + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: { + recommendations: [{ name: 'avoidLogicalDatabases' }] + } + })) + + render() + + expect(screen.queryByTestId('code_changes')).toBeInTheDocument() + expect(screen.queryByTestId('upgrade')).not.toBeInTheDocument() + expect(screen.queryByTestId('configuration_changes')).not.toBeInTheDocument() + }) + + it('should render code changes badge and configuration_changes in combineSmallStringsToHashes recommendation', () => { + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: { + recommendations: [{ name: 'combineSmallStringsToHashes' }] + } + })) + + render() + + expect(screen.queryByTestId('code_changes')).toBeInTheDocument() + expect(screen.queryByTestId('upgrade')).not.toBeInTheDocument() + expect(screen.queryByTestId('configuration_changes')).not.toBeInTheDocument() + }) + + it('should render code changes badge and configuration_changes in increaseSetMaxIntsetEntries recommendation', () => { + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: { + recommendations: [{ name: 'increaseSetMaxIntsetEntries' }] + } + })) + + render() + + expect(screen.queryByTestId('code_changes')).toBeInTheDocument() + expect(screen.queryByTestId('upgrade')).not.toBeInTheDocument() + expect(screen.queryByTestId('configuration_changes')).not.toBeInTheDocument() + }) + it('should collapse/expand', () => { (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ ...mockdbAnalysisSelector, diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx index 8ce97ac62e..4d6a08cbb7 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx @@ -37,7 +37,7 @@ export const renderBadges = (badges: string[]) => ( export const parseContent = ({ type, value }: { type: string, value: any }) => { switch (type) { case 'paragraph': - return {value} + return {value} case 'span': return {value} case 'link': From 7caa3d8e71e0f7a0985f6e28681bce70a19f7270 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Tue, 29 Nov 2022 13:52:41 +0400 Subject: [PATCH 028/108] #RI-3571-resolve conflicts --- redisinsight/api/test/helpers/data/redis.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/redisinsight/api/test/helpers/data/redis.ts b/redisinsight/api/test/helpers/data/redis.ts index 6e346e1c5a..520076a8f9 100644 --- a/redisinsight/api/test/helpers/data/redis.ts +++ b/redisinsight/api/test/helpers/data/redis.ts @@ -486,6 +486,7 @@ export const initDataHelper = (rte) => { pipeline.push(['eval', `return ${i}`, '0']) } await insertKeysBasedOnEnv(pipeline); + }; const setRedisearchConfig = async ( rule: string, From d81f1f90b4dbb326301c07825fdf459e89a08336 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Wed, 30 Nov 2022 13:02:18 +0400 Subject: [PATCH 029/108] resolve comments --- .../providers/recommendation.provider.spec.ts | 263 ++++++++++++++++++ .../providers/recommendation.provider.ts | 105 +++++-- .../recommendation/recommendation.service.ts | 19 +- .../POST-databases-id-analysis.test.ts | 65 ++++- redisinsight/api/test/helpers/constants.ts | 6 + .../constants/dbAnalysisRecommendations.json | 40 +++ 6 files changed, 458 insertions(+), 40 deletions(-) create mode 100644 redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts new file mode 100644 index 0000000000..6e823e43bb --- /dev/null +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts @@ -0,0 +1,263 @@ +import IORedis from 'ioredis'; +import { when } from 'jest-when'; +import { RecommendationProvider } from 'src/modules/recommendation/providers/recommendation.provider'; + +const nodeClient = Object.create(IORedis.prototype); +nodeClient.isCluster = false; +nodeClient.sendCommand = jest.fn(); + +const mockRedisMemoryInfoResponse_1: string = '# Memory\r\nnumber_of_cached_scripts:10\r\n'; +const mockRedisMemoryInfoResponse_2: string = '# Memory\r\nnumber_of_cached_scripts:11\r\n'; + +const mockRedisKeyspaceInfoResponse_1: string = '# Keyspace\r\ndb0:keys=2,expires=0,avg_ttl=0\r\n'; +const mockRedisKeyspaceInfoResponse_2: string = `# Keyspace\r\ndb0:keys=2,expires=0,avg_ttl=0\r\n + db1:keys=0,expires=0,avg_ttl=0\r\n`; +const mockRedisKeyspaceInfoResponse_3: string = `# Keyspace\r\ndb0:keys=2,expires=0,avg_ttl=0\r\n + db2:keys=20,expires=0,avg_ttl=0\r\n`; + +const mockRedisConfigResponse = ['name', '512']; + +const mockKeys = [ + { + name: Buffer.from('name'), type: 'string', length: 10, memory: 10, ttl: -1, + }, + { + name: Buffer.from('name'), type: 'hash', length: 10, memory: 10, ttl: -1, + }, + { + name: Buffer.from('name'), type: 'stream', length: 10, memory: 10, ttl: -1, + }, + { + name: Buffer.from('name'), type: 'set', length: 10, memory: 10, ttl: -1, + }, + { + name: Buffer.from('name'), type: 'zset', length: 10, memory: 10, ttl: -1, + }, + { + name: Buffer.from('name'), type: 'ReJSON-RL', length: 10, memory: 10, ttl: -1, + }, + { + name: Buffer.from('name'), type: 'graphdata', length: 10, memory: 10, ttl: -1, + }, + { + name: Buffer.from('name'), type: 'TSDB-TYPE', length: 10, memory: 10, ttl: -1, + }, +]; + +const mockBigHashKey = { + name: Buffer.from('name'), type: 'hash', length: 5001, memory: 10, ttl: -1, +}; + +const mockBigHashKey_2 = { + name: Buffer.from('name'), type: 'hash', length: 1001, memory: 10, ttl: -1, +}; + +const mockBigHashKey_3 = { + name: Buffer.from('name'), type: 'hash', length: 513, memory: 10, ttl: -1, +}; + +const mockBigStringKey = { + name: Buffer.from('name'), type: 'string', length: 10, memory: 201, ttl: -1, +}; + +const mockBigSet = { + name: Buffer.from('name'), type: 'set', length: 513, memory: 10, ttl: -1, +}; + +describe('RecommendationProvider', () => { + const service = new RecommendationProvider(); + + describe('determineLuaScriptRecommendation', () => { + it('should not return luaScript recommendation', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'info' })) + .mockResolvedValue(mockRedisMemoryInfoResponse_1); + + const luaScriptRecommendation = await service.determineLuaScriptRecommendation(nodeClient); + expect(luaScriptRecommendation).toEqual(null); + }); + + it('should return luaScript recommendation', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'info' })) + .mockResolvedValue(mockRedisMemoryInfoResponse_2); + + const luaScriptRecommendation = await service.determineLuaScriptRecommendation(nodeClient); + expect(luaScriptRecommendation).toEqual({ name: 'luaScript' }); + }); + + it('should not return luaScript recommendation when info command executed with error', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'info' })) + .mockRejectedValue('some error'); + + const luaScriptRecommendation = await service.determineLuaScriptRecommendation(nodeClient); + expect(luaScriptRecommendation).toEqual(null); + }); + }); + + describe('determineBigHashesRecommendation', () => { + it('should not return bigHashes recommendation', async () => { + const bigHashesRecommendation = await service.determineBigHashesRecommendation(mockKeys); + expect(bigHashesRecommendation).toEqual(null); + }); + it('should return bigHashes recommendation', async () => { + const bigHashesRecommendation = await service.determineBigHashesRecommendation( + [...mockKeys, mockBigHashKey], + ); + expect(bigHashesRecommendation).toEqual({ name: 'bigHashes' }); + }); + }); + + describe('determineBigTotalRecommendation', () => { + it('should not return useSmallerKeys recommendation', async () => { + const bigTotalRecommendation = await service.determineBigTotalRecommendation(1); + expect(bigTotalRecommendation).toEqual(null); + }); + it('should return useSmallerKeys recommendation', async () => { + const bigTotalRecommendation = await service.determineBigTotalRecommendation(1_000_001); + expect(bigTotalRecommendation).toEqual({ name: 'useSmallerKeys' }); + }); + }); + + describe('determineLogicalDatabasesRecommendation', () => { + it('should not return avoidLogicalDatabases recommendation when only one logical db', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'info' })) + .mockResolvedValue(mockRedisKeyspaceInfoResponse_1); + + const avoidLogicalDatabasesRecommendation = await service.determineLogicalDatabasesRecommendation(nodeClient); + expect(avoidLogicalDatabasesRecommendation).toEqual(null); + }); + + it('should not return avoidLogicalDatabases recommendation when only on logical db with keys', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'info' })) + .mockResolvedValue(mockRedisKeyspaceInfoResponse_2); + + const avoidLogicalDatabasesRecommendation = await service.determineLogicalDatabasesRecommendation(nodeClient); + expect(avoidLogicalDatabasesRecommendation).toEqual(null); + }); + + it('should return avoidLogicalDatabases recommendation', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'info' })) + .mockResolvedValue(mockRedisKeyspaceInfoResponse_3); + + const avoidLogicalDatabasesRecommendation = await service.determineLogicalDatabasesRecommendation(nodeClient); + expect(avoidLogicalDatabasesRecommendation).toEqual({ name: 'avoidLogicalDatabases' }); + }); + + it('should not return avoidLogicalDatabases recommendation when info command executed with error', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'info' })) + .mockRejectedValue('some error'); + + const avoidLogicalDatabasesRecommendation = await service.determineLogicalDatabasesRecommendation(nodeClient); + expect(avoidLogicalDatabasesRecommendation).toEqual(null); + }); + + it('should not return avoidLogicalDatabases recommendation when isCluster', async () => { + nodeClient.isCluster = true; + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'info' })) + .mockResolvedValue(mockRedisKeyspaceInfoResponse_3); + + const avoidLogicalDatabasesRecommendation = await service.determineLogicalDatabasesRecommendation(nodeClient); + expect(avoidLogicalDatabasesRecommendation).toEqual(null); + nodeClient.isCluster = false; + }); + }); + + describe('determineCombineSmallStringsToHashesRecommendation', () => { + it('should not return combineSmallStringsToHashes recommendation', async () => { + const smallStringRecommendation = await service.determineCombineSmallStringsToHashesRecommendation([ + mockBigStringKey, + ]); + expect(smallStringRecommendation).toEqual(null); + }); + it('should return combineSmallStringsToHashes recommendation', async () => { + const smallStringRecommendation = await service.determineCombineSmallStringsToHashesRecommendation(mockKeys); + expect(smallStringRecommendation).toEqual({ name: 'combineSmallStringsToHashes' }); + }); + }); + + describe('determineIncreaseSetMaxIntsetEntriesRecommendation', () => { + it('should not return increaseSetMaxIntsetEntries', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'config' })) + .mockResolvedValue(mockRedisConfigResponse); + + const increaseSetMaxIntsetEntriesRecommendation = await service + .determineIncreaseSetMaxIntsetEntriesRecommendation(nodeClient, mockKeys); + expect(increaseSetMaxIntsetEntriesRecommendation).toEqual(null); + }); + + it('should return increaseSetMaxIntsetEntries recommendation', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'config' })) + .mockResolvedValue(mockRedisConfigResponse); + + const increaseSetMaxIntsetEntriesRecommendation = await service + .determineIncreaseSetMaxIntsetEntriesRecommendation(nodeClient, [...mockKeys, mockBigSet]); + expect(increaseSetMaxIntsetEntriesRecommendation).toEqual({ name: 'increaseSetMaxIntsetEntries' }); + }); + + it('should not return increaseSetMaxIntsetEntries recommendation when config command executed with error', + async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'config' })) + .mockRejectedValue('some error'); + + const increaseSetMaxIntsetEntriesRecommendation = await service + .determineIncreaseSetMaxIntsetEntriesRecommendation(nodeClient, mockKeys); + expect(increaseSetMaxIntsetEntriesRecommendation).toEqual(null); + }); + }); + + describe('determineConvertHashtableToZiplistRecommendation', () => { + it('should not return convertHashtableToZiplist recommendation', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'config' })) + .mockResolvedValue(mockRedisConfigResponse); + + const convertHashtableToZiplistRecommendation = await service + .determineConvertHashtableToZiplistRecommendation(nodeClient, mockKeys); + expect(convertHashtableToZiplistRecommendation).toEqual(null); + }); + + it('should return convertHashtableToZiplist recommendation', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'config' })) + .mockResolvedValue(mockRedisConfigResponse); + + const convertHashtableToZiplistRecommendation = await service + .determineConvertHashtableToZiplistRecommendation(nodeClient, [...mockKeys, mockBigHashKey_3]); + expect(convertHashtableToZiplistRecommendation).toEqual({ name: 'convertHashtableToZiplist' }); + }); + + it('should not return convertHashtableToZiplist recommendation when config command executed with error', + async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'config' })) + .mockRejectedValue('some error'); + + const convertHashtableToZiplistRecommendation = await service + .determineConvertHashtableToZiplistRecommendation(nodeClient, mockKeys); + expect(convertHashtableToZiplistRecommendation).toEqual(null); + }); + }); + + describe('determineCompressHashFieldNamesRecommendation', () => { + it('should not return compressHashFieldNames recommendation', async () => { + const compressHashFieldNamesRecommendation = await service + .determineCompressHashFieldNamesRecommendation(mockKeys); + expect(compressHashFieldNamesRecommendation).toEqual(null); + }); + it('should return compressHashFieldNames recommendation', async () => { + const compressHashFieldNamesRecommendation = await service + .determineCompressHashFieldNamesRecommendation([mockBigHashKey_2]); + expect(compressHashFieldNamesRecommendation).toEqual({ name: 'compressHashFieldNames' }); + }); + }); +}); diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index 11b7769273..bc02df89d6 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -8,8 +8,9 @@ import { Key } from 'src/modules/database-analysis/models'; const minNumberOfCachedScripts = 10; const maxHashLength = 5000; -const maxStringLength = 200; +const maxStringMemory = 200; const maxDatabaseTotal = 1_000_000; +const maxCompressHashLength = 1000; @Injectable() export class RecommendationProvider { @@ -20,16 +21,20 @@ export class RecommendationProvider { async determineLuaScriptRecommendation( redisClient: Redis, ): Promise { - const info = convertRedisInfoReplyToObject( - await redisClient.sendCommand( - new Command('info', [], { replyEncoding: 'utf8' }), - ) as string, - ); - const nodesNumbersOfCachedScripts = get(info, 'memory.number_of_cached_scripts'); + try { + const info = convertRedisInfoReplyToObject( + await redisClient.sendCommand( + new Command('info', [], { replyEncoding: 'utf8' }), + ) as string, + ); + const nodesNumbersOfCachedScripts = get(info, 'memory.number_of_cached_scripts'); - return parseInt(nodesNumbersOfCachedScripts, 10) > minNumberOfCachedScripts - ? { name: 'luaScript' } - : null; + return parseInt(nodesNumbersOfCachedScripts, 10) > minNumberOfCachedScripts + ? { name: 'luaScript' } + : null; + } catch (err) { + return null; + } } /** @@ -63,15 +68,21 @@ export class RecommendationProvider { if (redisClient.isCluster) { return null; } - const info = convertRedisInfoReplyToObject( - await redisClient.info(), - ); - const keyspace = get(info, 'keyspace', {}); - const databasesWithKeys = Object.values(keyspace).filter((db) => { - const { keys } = convertBulkStringsToObject(db as string, ',', '='); - return keys > 0; - }); - return databasesWithKeys.length > 1 ? { name: 'avoidLogicalDatabases' } : null; + try { + const info = convertRedisInfoReplyToObject( + await redisClient.sendCommand( + new Command('info', ['keyspace'], { replyEncoding: 'utf8' }), + ) as string, + ); + const keyspace = get(info, 'keyspace', {}); + const databasesWithKeys = Object.values(keyspace).filter((db) => { + const { keys } = convertBulkStringsToObject(db as string, ',', '='); + return keys > 0; + }); + return databasesWithKeys.length > 1 ? { name: 'avoidLogicalDatabases' } : null; + } catch (err) { + return null; + } } /** @@ -81,7 +92,7 @@ export class RecommendationProvider { async determineCombineSmallStringsToHashesRecommendation( keys: Key[], ): Promise { - const smallString = keys.some((key) => key.type === RedisDataType.String && key.memory < maxStringLength); + const smallString = keys.some((key) => key.type === RedisDataType.String && key.memory < maxStringMemory); return smallString ? { name: 'combineSmallStringsToHashes' } : null; } @@ -94,17 +105,55 @@ export class RecommendationProvider { redisClient: Redis | Cluster, keys: Key[], ): Promise { - const [, setMaxIntsetEntries] = await redisClient.sendCommand( - new Command('config', ['get', 'set-max-intset-entries'], { - replyEncoding: 'utf8', - }), - ) as string[]; + try { + const [, setMaxIntsetEntries] = await redisClient.sendCommand( + new Command('config', ['get', 'set-max-intset-entries'], { + replyEncoding: 'utf8', + }), + ) as string[]; - if (!setMaxIntsetEntries) { + if (!setMaxIntsetEntries) { + return null; + } + const setMaxIntsetEntriesNumber = parseInt(setMaxIntsetEntries, 10); + const bigSet = keys.some((key) => key.type === RedisDataType.Set && key.length > setMaxIntsetEntriesNumber); + return bigSet ? { name: 'increaseSetMaxIntsetEntries' } : null; + } catch (err) { return null; } + } + /** + * Check convert hashtable to ziplist recommendation + * @param keys + * @param redisClient + */ - const bigSet = keys.some((key) => key.type === RedisDataType.Set && key.length > parseInt(setMaxIntsetEntries, 10)); - return bigSet ? { name: 'increaseSetMaxIntsetEntries' } : null; + async determineConvertHashtableToZiplistRecommendation( + redisClient: Redis | Cluster, + keys: Key[], + ): Promise { + try { + const [, hashMaxZiplistEntries] = await redisClient.sendCommand( + new Command('config', ['get', 'hash-max-ziplist-entries'], { + replyEncoding: 'utf8', + }), + ) as string[]; + const hashMaxZiplistEntriesNumber = parseInt(hashMaxZiplistEntries, 10); + const bigHash = keys.some((key) => key.type === RedisDataType.Hash && key.length > hashMaxZiplistEntriesNumber); + return bigHash ? { name: 'convertHashtableToZiplist' } : null; + } catch (err) { + return null; + } + } + + /** + * Check compress hash field names recommendation + * @param keys + */ + async determineCompressHashFieldNamesRecommendation( + keys: Key[], + ): Promise { + const bigHash = keys.some((key) => key.type === RedisDataType.Hash && key.length > maxCompressHashLength); + return bigHash ? { name: 'compressHashFieldNames' } : null; } } diff --git a/redisinsight/api/src/modules/recommendation/recommendation.service.ts b/redisinsight/api/src/modules/recommendation/recommendation.service.ts index ece8b925b8..3dab87a31a 100644 --- a/redisinsight/api/src/modules/recommendation/recommendation.service.ts +++ b/redisinsight/api/src/modules/recommendation/recommendation.service.ts @@ -33,13 +33,16 @@ export class RecommendationService { total, } = dto; - return ([ - await this.recommendationProvider.determineLuaScriptRecommendation(client), - await this.recommendationProvider.determineBigHashesRecommendation(keys), - await this.recommendationProvider.determineBigTotalRecommendation(total), - await this.recommendationProvider.determineLogicalDatabasesRecommendation(client), - await this.recommendationProvider.determineCombineSmallStringsToHashesRecommendation(keys), - await this.recommendationProvider.determineIncreaseSetMaxIntsetEntriesRecommendation(client, keys), - ]); + return ( + Promise.all([ + await this.recommendationProvider.determineLuaScriptRecommendation(client), + await this.recommendationProvider.determineBigHashesRecommendation(keys), + await this.recommendationProvider.determineBigTotalRecommendation(total), + await this.recommendationProvider.determineLogicalDatabasesRecommendation(client), + await this.recommendationProvider.determineCombineSmallStringsToHashesRecommendation(keys), + await this.recommendationProvider.determineIncreaseSetMaxIntsetEntriesRecommendation(client, keys), + await this.recommendationProvider.determineConvertHashtableToZiplistRecommendation(client, keys), + await this.recommendationProvider.determineCompressHashFieldNamesRecommendation(keys), + ])); } } diff --git a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts index a038ec076c..712234f749 100644 --- a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts +++ b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts @@ -155,7 +155,7 @@ describe('POST /databases/:instanceId/analysis', () => { statusCode: 201, responseSchema, before: async () => { - const KEYS_NUMBER = 1_000_001 + const KEYS_NUMBER = 1_000_001; await rte.data.generateNKeys(KEYS_NUMBER, false); }, checkFn: async ({ body }) => { @@ -167,7 +167,6 @@ describe('POST /databases/:instanceId/analysis', () => { expect(body.topKeysMemory.length).to.gt(0); expect(body.recommendations).to.deep.eq([ constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, - // generateNKeys generated small strings constants.TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION, ]); }, @@ -183,7 +182,7 @@ describe('POST /databases/:instanceId/analysis', () => { statusCode: 201, responseSchema, before: async () => { - const NUMBERS_OF_HASH_FIELDS = 5001 + const NUMBERS_OF_HASH_FIELDS = 5001; await rte.data.generateHugeNumberOfFieldsForHashKey(NUMBERS_OF_HASH_FIELDS, true); }, checkFn: async ({ body }) => { @@ -193,7 +192,11 @@ describe('POST /databases/:instanceId/analysis', () => { expect(body.topMemoryNsp.length).to.gt(0); expect(body.topKeysLength.length).to.gt(0); expect(body.topKeysMemory.length).to.gt(0); - expect(body.recommendations).to.deep.eq([constants.TEST_BIG_HASHES_DATABASE_ANALYSIS_RECOMMENDATION]); + expect(body.recommendations).to.deep.eq([ + constants.TEST_BIG_HASHES_DATABASE_ANALYSIS_RECOMMENDATION, + constants.TEST_CONVERT_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, + constants.TEST_COMPRESS_HASH_FIELD_NAMES_RECOMMENDATION, + ]); }, after: async () => { expect(await repository.count()).to.eq(5); @@ -234,6 +237,60 @@ describe('POST /databases/:instanceId/analysis', () => { expect(await repository.count()).to.eq(5); } }, + { + name: 'Should create new database analysis with convertHashtableToZiplist recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + const NUMBERS_OF_HASH_FIELDS = 513; + await rte.data.generateHugeNumberOfFieldsForHashKey(NUMBERS_OF_HASH_FIELDS, true); + }, + checkFn: async ({ body }) => { + expect(body.totalKeys.total).to.gt(0); + expect(body.totalMemory.total).to.gt(0); + expect(body.topKeysNsp.length).to.gt(0); + expect(body.topMemoryNsp.length).to.gt(0); + expect(body.topKeysLength.length).to.gt(0); + expect(body.topKeysMemory.length).to.gt(0); + expect(body.recommendations).to.deep.eq([ + constants.TEST_CONVERT_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, + ]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } + }, + { + name: 'Should create new database analysis with compressHashFieldNames recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + const NUMBERS_OF_HASH_FIELDS = 1001; + await rte.data.generateHugeNumberOfFieldsForHashKey(NUMBERS_OF_HASH_FIELDS, true); + }, + checkFn: async ({ body }) => { + expect(body.totalKeys.total).to.gt(0); + expect(body.totalMemory.total).to.gt(0); + expect(body.topKeysNsp.length).to.gt(0); + expect(body.topMemoryNsp.length).to.gt(0); + expect(body.topKeysLength.length).to.gt(0); + expect(body.topKeysMemory.length).to.gt(0); + expect(body.recommendations).to.deep.eq([ + constants.TEST_CONVERT_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, + constants.TEST_COMPRESS_HASH_FIELD_NAMES_RECOMMENDATION, + + ]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } + }, { name: 'Should create new database analysis with luaScript recommendation', data: { diff --git a/redisinsight/api/test/helpers/constants.ts b/redisinsight/api/test/helpers/constants.ts index a27b142223..1673bbc6b9 100644 --- a/redisinsight/api/test/helpers/constants.ts +++ b/redisinsight/api/test/helpers/constants.ts @@ -466,6 +466,12 @@ export const constants = { TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION: { name: 'combineSmallStringsToHashes', }, + TEST_CONVERT_HASHTABLE_TO_ZIPLIST_RECOMMENDATION: { + name: 'convertHashtableToZiplist', + }, + TEST_COMPRESS_HASH_FIELD_NAMES_RECOMMENDATION: { + name: 'compressHashFieldNames', + }, // etc... } diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index c2306013bd..64799dc079 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -153,5 +153,45 @@ } ], "badges": ["configuration_changes"] + }, + "convertHashtableToZiplist": { + "id": "convertHashtableToZiplist", + "title": "Increase zset-max-ziplist-entries", + "content": [ + { + "id": "1", + "type": "span", + "value": "If any value for a key exceeds zset-max-ziplist-entries, it is stored automatically as a Hashtable instead of a Ziplist, which consumes almost double the memory. So to save memory, increase the configurations and convert your hashtables to ziplist. The trade-off can be an increase in latency and possibly an increase in CPU utilization. " + }, + { + "id": "2", + "type": "link", + "value": { + "href": "https://docs.redis.com/latest/ri/memory-optimizations/", + "name": "Read more" + } + } + ], + "badges": ["configuration_changes"] +}, + "compressHashFieldNames": { + "id": "compressHashFieldNames", + "title": "Compress Hash field names", + "content": [ + { + "id": "1", + "type": "span", + "value": "Hash field name also consumes memory, so use smaller or shortened field names to reduce memory usage. " + }, + { + "id": "2", + "type": "link", + "value": { + "href": "https://docs.redis.com/latest/ri/memory-optimizations/", + "name": "Read more" + } + } + ], + "badges": ["configuration_changes"] } } From 7113bb342aa4006d71136bf83c4259ef088b5195 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Wed, 30 Nov 2022 13:07:15 +0400 Subject: [PATCH 030/108] resolve comments --- .../modules/recommendation/providers/recommendation.provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index bc02df89d6..a1908c9d39 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -24,7 +24,7 @@ export class RecommendationProvider { try { const info = convertRedisInfoReplyToObject( await redisClient.sendCommand( - new Command('info', [], { replyEncoding: 'utf8' }), + new Command('info', ['memory'], { replyEncoding: 'utf8' }), ) as string, ); const nodesNumbersOfCachedScripts = get(info, 'memory.number_of_cached_scripts'); From 92a6fffbec19a6b094e33b551fac19999e099fbf Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Wed, 30 Nov 2022 13:51:35 +0400 Subject: [PATCH 031/108] add badges test --- .../Recommendations.spec.tsx | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx index 9bcac116e3..7cde5742db 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx @@ -172,7 +172,7 @@ describe('Recommendations', () => { expect(screen.queryByTestId('configuration_changes')).not.toBeInTheDocument() }) - it('should render code changes badge and configuration_changes in increaseSetMaxIntsetEntries recommendation', () => { + it('should render configuration_changes badge and configuration_changes in increaseSetMaxIntsetEntries recommendation', () => { (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ ...mockdbAnalysisSelector, data: { @@ -182,11 +182,42 @@ describe('Recommendations', () => { render() - expect(screen.queryByTestId('code_changes')).toBeInTheDocument() + expect(screen.queryByTestId('code_changes')).not.toBeInTheDocument() expect(screen.queryByTestId('upgrade')).not.toBeInTheDocument() - expect(screen.queryByTestId('configuration_changes')).not.toBeInTheDocument() + expect(screen.queryByTestId('configuration_changes')).toBeInTheDocument() + }) + + it('should render code changes badge and configuration_changes in convertHashtableToZiplist recommendation', () => { + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: { + recommendations: [{ name: 'convertHashtableToZiplist' }] + } + })) + + render() + + expect(screen.queryByTestId('code_changes')).not.toBeInTheDocument() + expect(screen.queryByTestId('upgrade')).not.toBeInTheDocument() + expect(screen.queryByTestId('configuration_changes')).toBeInTheDocument() + }) + + it('should render configuration_changes badge and configuration_changes in compressHashFieldNames recommendation', () => { + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: { + recommendations: [{ name: 'compressHashFieldNames' }] + } + })) + + render() + + expect(screen.queryByTestId('code_changes')).not.toBeInTheDocument() + expect(screen.queryByTestId('upgrade')).not.toBeInTheDocument() + expect(screen.queryByTestId('configuration_changes')).toBeInTheDocument() }) + it('should collapse/expand', () => { (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ ...mockdbAnalysisSelector, From 9d5815f82d7cfc3f4183bb5e6664b8174581a1b5 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Wed, 30 Nov 2022 11:04:19 +0100 Subject: [PATCH 032/108] add initial tests --- .../memory-efficiency/recommendations.e2e.ts | 84 ++++++++++++++++++- 1 file changed, 82 insertions(+), 2 deletions(-) diff --git a/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts b/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts index 9cf475a009..7ffdc0820a 100644 --- a/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts +++ b/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts @@ -1,6 +1,6 @@ -import { MyRedisDatabasePage, MemoryEfficiencyPage, BrowserPage, CliPage } from '../../../pageObjects'; +import { MyRedisDatabasePage, MemoryEfficiencyPage, BrowserPage, CliPage, AddRedisDatabasePage } from '../../../pageObjects'; import { rte } from '../../../helpers/constants'; -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { acceptLicenseTermsAndAddDatabaseApi, deleteCustomDatabase } from '../../../helpers/database'; import { commonUrl, ossStandaloneBigConfig, ossStandaloneConfig } from '../../../helpers/conf'; import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; import { CliActions } from '../../../common-actions/cli-actions'; @@ -13,9 +13,13 @@ const cliActions = new CliActions(); const common = new Common(); const browserPage = new BrowserPage(); const cliPage = new CliPage(); +const addRedisDatabasePage = new AddRedisDatabasePage(); const externalPageLink = 'https://docs.redis.com/latest/ri/memory-optimizations/'; const keyName = `hugeHashKey-${common.generateWord(10)}`; +const stringKeyName = `smallStringKey-${common.generateWord(10)}`; +const stringBigKeyName = `bigStringKey-${common.generateWord(10)}`; +const index = '1'; fixture `Memory Efficiency Recommendations` .meta({ type: 'critical_path', rte: rte.standalone }) @@ -99,3 +103,79 @@ test await t.expect(memoryEfficiencyPage.codeChangesLabel.exists).ok('Big hashes recommendation not have Code Changes label'); await t.expect(memoryEfficiencyPage.configurationChangesLabel.exists).ok('Big hashes recommendation not have Configuration Changes label'); }); + test + .before(async t => { + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await browserPage.addStringKey(stringKeyName, '2147476121', 'field'); + // Go to Analysis Tools page + await t.click(myRedisDatabasePage.analysisPageButton); + await t.click(memoryEfficiencyPage.newReportBtn); + // Go to Recommendations tab + await t.click(memoryEfficiencyPage.recommendationsTab); + }) + .after(async t => { + // Clear and delete database + await t.click(myRedisDatabasePage.browserButton); + await browserPage.deleteKeysByNames([stringKeyName, stringBigKeyName]); + await deleteStandaloneDatabaseApi(ossStandaloneConfig); + })('Combine small strings to hashes recommendation', async t => { + const command = `SET ${stringBigKeyName} "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed accumsan lectus sed diam suscipit, eu ullamcorper ligula pulvinar."`; + + // Verify that user can see Combine small strings to hashes recommendation when there are strings that are less than 200 bytes + await t.expect(memoryEfficiencyPage.bigHashesAccordion.exists).ok('Combine small strings to hashes recommendation not displayed'); + await t.expect(memoryEfficiencyPage.codeChangesLabel.exists).ok('Combine small strings to hashes recommendation not have Code Changes label'); + + // Add String key with more than 200 bytes + await cliPage.sendCommandInCli(command); + await t.click(memoryEfficiencyPage.newReportBtn); + // Verify that user can not see recommendation when there is at least one string that are more than 200 bytes + await t.expect(memoryEfficiencyPage.bigHashesAccordion.exists).notOk('Combine small strings to hashes recommendation not displayed'); + }); +test + .before(async t => { + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await browserPage.addStringKey(stringKeyName, '2147476121', 'field'); + // Go to Analysis Tools page + await t.click(myRedisDatabasePage.analysisPageButton); + await t.click(memoryEfficiencyPage.newReportBtn); + // Go to Recommendations tab + await t.click(memoryEfficiencyPage.recommendationsTab); + }) + .after(async t => { + // Clear and delete database + await t.click(myRedisDatabasePage.browserButton); + await browserPage.deleteKeyByName(stringKeyName); + await deleteStandaloneDatabaseApi(ossStandaloneConfig); + })('Increase the set-max-intset-entries recommendation', async t => { + // Verify that user can see Increase the set-max-intset-entries recommendation when Found sets with length > set-max-intset-entries + await t.expect(memoryEfficiencyPage.bigHashesAccordion.exists).ok('Combine small strings to hashes recommendation not displayed'); + await t.expect(memoryEfficiencyPage.configurationChangesLabel.exists).ok('Combine small strings to hashes recommendation not have Configuration Changes label'); + }); +test + .before(async t => { + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await browserPage.addStringKey(stringKeyName, '2147476121', 'field'); + await t.click(myRedisDatabasePage.myRedisDBButton); + await addRedisDatabasePage.addLogicalRedisDatabase(ossStandaloneConfig, index); + await myRedisDatabasePage.clickOnDBByName(`${ossStandaloneConfig.databaseName} [${index}]`); + await browserPage.addHashKey(keyName, '2147476121', 'field', 'value'); + // Go to Analysis Tools page + await t.click(myRedisDatabasePage.analysisPageButton); + await t.click(memoryEfficiencyPage.newReportBtn); + // Go to Recommendations tab + await t.click(memoryEfficiencyPage.recommendationsTab); + }) + .after(async t => { + // Clear and delete database + await t.click(myRedisDatabasePage.browserButton); + await browserPage.deleteKeyByName(keyName); + await deleteCustomDatabase(`${ossStandaloneConfig.databaseName} [${index}]`); + await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName); + await browserPage.deleteKeyByName(stringKeyName); + await deleteStandaloneDatabaseApi(ossStandaloneConfig); + })('Avoid using logical databases', async t => { + + // Verify that user can see Avoid using logical databases recommendation when the database supports logical databases and there are keys in more than 1 logical database + await t.expect(memoryEfficiencyPage.bigHashesAccordion.exists).ok('Avoid using logical databases recommendation not displayed'); + await t.expect(memoryEfficiencyPage.codeChangesLabel.exists).ok('Avoid using logical databases recommendation not have Code Changes label'); + }); From 52a66af21bcacb8dd6f5eb0d036699d1cc18e3d0 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Wed, 30 Nov 2022 15:00:05 +0100 Subject: [PATCH 033/108] add recommendations tests --- tests/e2e/helpers/keys.ts | 34 ++++++++ .../e2e/pageObjects/memory-efficiency-page.ts | 3 + .../memory-efficiency/recommendations.e2e.ts | 80 +++++++++++++++---- 3 files changed, 103 insertions(+), 14 deletions(-) diff --git a/tests/e2e/helpers/keys.ts b/tests/e2e/helpers/keys.ts index d277db769e..de7c465d75 100644 --- a/tests/e2e/helpers/keys.ts +++ b/tests/e2e/helpers/keys.ts @@ -6,6 +6,7 @@ import { BrowserPage, CliPage } from '../pageObjects'; import { KeyData, AddKeyArguments } from '../pageObjects/browser-page'; import { KeyTypesTexts } from './constants'; import { Common } from './common'; +import { random } from 'lodash'; const common = new Common(); const cliPage = new CliPage(); @@ -193,6 +194,39 @@ export async function populateSetWithMembers(host: string, port: string, keyArgu }); } +/** + * Populate Zset key with members + * @param host The host of database + * @param port The port of database + * @param keyArguments The arguments of key and its members + */ + export async function populateZSetWithMembers(host: string, port: string, keyArguments: AddKeyArguments): Promise { + const dbConf = { host, port: Number(port) }; + let minScoreValue: -10; + let maxScoreValue: 10; + const client = createClient(dbConf); + const members: string[] = []; + + await client.on('error', async function(error: string) { + throw new Error(error); + }); + await client.on('connect', async function() { + if (keyArguments.membersCount != undefined) { + for (let i = 0; i < keyArguments.membersCount; i++) { + const memberName = `${keyArguments.memberStartWith}${common.generateWord(10)}`; + const scoreValue = random(minScoreValue, maxScoreValue).toString(2); + members.push(scoreValue, memberName); + } + } + await client.zadd(keyArguments.keyName, members, async(error: string) => { + if (error) { + throw error; + } + }); + await client.quit(); + }); +} + /** * Delete all keys from database * @param host The host of database diff --git a/tests/e2e/pageObjects/memory-efficiency-page.ts b/tests/e2e/pageObjects/memory-efficiency-page.ts index 309384b88d..38cd0b6a5c 100644 --- a/tests/e2e/pageObjects/memory-efficiency-page.ts +++ b/tests/e2e/pageObjects/memory-efficiency-page.ts @@ -55,4 +55,7 @@ export class MemoryEfficiencyPage { luaScriptTextContainer = Selector('#luaScript'); useSmallKeysAccordion = Selector('[data-testid=useSmallerKeys-accordion]'); bigHashesAccordion = Selector('[data-testid=bigHashes-accordion]'); + combineStringsAccordion = Selector('[data-testid=combineSmallStringsToHashes-accordion]'); + increaseSetAccordion = Selector('[data-testid=increaseSetMaxIntsetEntries-accordion]'); + avoidLogicalDbAccordion = Selector('[data-testid=avoidLogicalDatabases-accordion]'); } diff --git a/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts b/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts index 7ffdc0820a..fed05fe07e 100644 --- a/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts +++ b/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts @@ -5,7 +5,7 @@ import { commonUrl, ossStandaloneBigConfig, ossStandaloneConfig } from '../../.. import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; import { CliActions } from '../../../common-actions/cli-actions'; import { Common } from '../../../helpers/common'; -import { populateHashWithFields } from '../../../helpers/keys'; +import { populateHashWithFields, populateSetWithMembers, populateZSetWithMembers } from '../../../helpers/keys'; const memoryEfficiencyPage = new MemoryEfficiencyPage(); const myRedisDatabasePage = new MyRedisDatabasePage(); @@ -16,10 +16,15 @@ const cliPage = new CliPage(); const addRedisDatabasePage = new AddRedisDatabasePage(); const externalPageLink = 'https://docs.redis.com/latest/ri/memory-optimizations/'; -const keyName = `hugeHashKey-${common.generateWord(10)}`; -const stringKeyName = `smallStringKey-${common.generateWord(10)}`; -const stringBigKeyName = `bigStringKey-${common.generateWord(10)}`; +const keyName = `hugeHashKey-${common.generateWord(5)}`; +const stringKeyName = `smallStringKey-${common.generateWord(5)}`; +const setKeyName = `hugeSetKey-${common.generateWord(4)}`; +const stringBigKeyName = `bigStringKey-${common.generateWord(5)}`; const index = '1'; +// const dbParameters = { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port }; +const dbParameters = { host: 'localhost', port: '8100' }; +const setKeyParameters = { membersCount: 512, keyName: setKeyName, memberStartWith: 'setMember' }; +const zsetKeyParameters = { membersCount: 128, keyName: setKeyName, memberStartWith: 'zsetMember' }; fixture `Memory Efficiency Recommendations` .meta({ type: 'critical_path', rte: rte.standalone }) @@ -103,7 +108,10 @@ test await t.expect(memoryEfficiencyPage.codeChangesLabel.exists).ok('Big hashes recommendation not have Code Changes label'); await t.expect(memoryEfficiencyPage.configurationChangesLabel.exists).ok('Big hashes recommendation not have Configuration Changes label'); }); - test + + + +test .before(async t => { await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); await browserPage.addStringKey(stringKeyName, '2147476121', 'field'); @@ -116,25 +124,30 @@ test .after(async t => { // Clear and delete database await t.click(myRedisDatabasePage.browserButton); - await browserPage.deleteKeysByNames([stringKeyName, stringBigKeyName]); + await browserPage.deleteKeyByName(stringBigKeyName); await deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Combine small strings to hashes recommendation', async t => { const command = `SET ${stringBigKeyName} "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed accumsan lectus sed diam suscipit, eu ullamcorper ligula pulvinar."`; // Verify that user can see Combine small strings to hashes recommendation when there are strings that are less than 200 bytes - await t.expect(memoryEfficiencyPage.bigHashesAccordion.exists).ok('Combine small strings to hashes recommendation not displayed'); + await t.expect(memoryEfficiencyPage.combineStringsAccordion.exists).ok('Combine small strings to hashes recommendation not displayed for small string'); await t.expect(memoryEfficiencyPage.codeChangesLabel.exists).ok('Combine small strings to hashes recommendation not have Code Changes label'); // Add String key with more than 200 bytes await cliPage.sendCommandInCli(command); + // Delete small String key + await cliPage.sendCommandInCli(`DEL ${stringKeyName}`); + await t.click(memoryEfficiencyPage.newReportBtn); // Verify that user can not see recommendation when there is at least one string that are more than 200 bytes - await t.expect(memoryEfficiencyPage.bigHashesAccordion.exists).notOk('Combine small strings to hashes recommendation not displayed'); + await t.expect(memoryEfficiencyPage.combineStringsAccordion.exists).notOk('Combine small strings to hashes recommendation is displayed for huge string'); }); test .before(async t => { await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); - await browserPage.addStringKey(stringKeyName, '2147476121', 'field'); + await browserPage.addSetKey(setKeyName, '2147476121', 'member'); + // Add 512 members to the set key + await populateSetWithMembers(dbParameters.host, dbParameters.port, setKeyParameters); // Go to Analysis Tools page await t.click(myRedisDatabasePage.analysisPageButton); await t.click(memoryEfficiencyPage.newReportBtn); @@ -144,12 +157,21 @@ test .after(async t => { // Clear and delete database await t.click(myRedisDatabasePage.browserButton); - await browserPage.deleteKeyByName(stringKeyName); + await browserPage.deleteKeyByName(setKeyName); + await cliPage.sendCommandInCli('config set set-max-intset-entries 512'); await deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Increase the set-max-intset-entries recommendation', async t => { + const command = 'config set set-max-intset-entries 1024'; + // Verify that user can see Increase the set-max-intset-entries recommendation when Found sets with length > set-max-intset-entries - await t.expect(memoryEfficiencyPage.bigHashesAccordion.exists).ok('Combine small strings to hashes recommendation not displayed'); - await t.expect(memoryEfficiencyPage.configurationChangesLabel.exists).ok('Combine small strings to hashes recommendation not have Configuration Changes label'); + await t.expect(memoryEfficiencyPage.increaseSetAccordion.exists).ok('Increase the set-max-intset-entries recommendation not displayed'); + await t.expect(memoryEfficiencyPage.configurationChangesLabel.exists).ok('Increase the set-max-intset-entries recommendation not have Configuration Changes label'); + + // Change config max entries to 1024 + await cliPage.sendCommandInCli(command); + await t.click(memoryEfficiencyPage.newReportBtn); + // Verify that user can not see Increase the set-max-intset-entries recommendation when Found sets with length < set-max-intset-entries + await t.expect(memoryEfficiencyPage.increaseSetAccordion.exists).notOk('Increase the set-max-intset-entries recommendation is displayed'); }); test .before(async t => { @@ -174,8 +196,38 @@ test await browserPage.deleteKeyByName(stringKeyName); await deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Avoid using logical databases', async t => { - // Verify that user can see Avoid using logical databases recommendation when the database supports logical databases and there are keys in more than 1 logical database - await t.expect(memoryEfficiencyPage.bigHashesAccordion.exists).ok('Avoid using logical databases recommendation not displayed'); + await t.expect(memoryEfficiencyPage.avoidLogicalDbAccordion.exists).ok('Avoid using logical databases recommendation not displayed'); await t.expect(memoryEfficiencyPage.codeChangesLabel.exists).ok('Avoid using logical databases recommendation not have Code Changes label'); }); +test.only + .before(async t => { + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + await browserPage.addZSetKey(setKeyName, '2147476121', 'member'); + // Add 128 members to the zset key + await populateZSetWithMembers(dbParameters.host, dbParameters.port, zsetKeyParameters); + // Go to Analysis Tools page + await t.click(myRedisDatabasePage.analysisPageButton); + await t.click(memoryEfficiencyPage.newReportBtn); + // Go to Recommendations tab + await t.click(memoryEfficiencyPage.recommendationsTab); + }) + .after(async t => { + // Clear and delete database + await t.click(myRedisDatabasePage.browserButton); + await browserPage.deleteKeyByName(setKeyName); + await cliPage.sendCommandInCli('config set zset-max-ziplist-entries 128'); + await deleteStandaloneDatabaseApi(ossStandaloneConfig); + })('Convert hashtable to ziplist for hashes recommendation', async t => { + const command = 'config set zset-max-ziplist-entries 256'; + + // Verify that user can see Convert hashtable to ziplist for hashes recommendation when the number of hash entries exceeds hash-max-ziplist-entries + await t.expect(memoryEfficiencyPage.increaseSetAccordion.exists).ok('Increase the set-max-intset-entries recommendation not displayed'); + await t.expect(memoryEfficiencyPage.configurationChangesLabel.exists).ok('Increase the set-max-intset-entries recommendation not have Configuration Changes label'); + + // Change config max entries to 256 + await cliPage.sendCommandInCli(command); + await t.click(memoryEfficiencyPage.newReportBtn); + // Verify that user can not see Convert hashtable to ziplist for hashes recommendation when the number of hash entries not exceeds hash-max-ziplist-entries + await t.expect(memoryEfficiencyPage.increaseSetAccordion.exists).notOk('Increase the set-max-intset-entries recommendation is displayed'); + }); \ No newline at end of file From 23d4797fef7ec5df075190b0b8edc2aaed30db69 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Wed, 30 Nov 2022 18:20:16 +0400 Subject: [PATCH 034/108] #RI-3562- update recommendation text --- redisinsight/ui/src/constants/dbAnalysisRecommendations.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index 64799dc079..bd05082176 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -156,12 +156,12 @@ }, "convertHashtableToZiplist": { "id": "convertHashtableToZiplist", - "title": "Increase zset-max-ziplist-entries", + "title": "Increase hash-max-ziplist-entries", "content": [ { "id": "1", "type": "span", - "value": "If any value for a key exceeds zset-max-ziplist-entries, it is stored automatically as a Hashtable instead of a Ziplist, which consumes almost double the memory. So to save memory, increase the configurations and convert your hashtables to ziplist. The trade-off can be an increase in latency and possibly an increase in CPU utilization. " + "value": "If any value for a key exceeds hash-max-ziplist-entries, it is stored automatically as a Hashtable instead of a Ziplist, which consumes almost double the memory. So to save memory, increase the configurations and convert your hashtables to ziplist. The trade-off can be an increase in latency and possibly an increase in CPU utilization. " }, { "id": "2", From fc7cb7777742cb51d45ab894a238b74a29218b32 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Wed, 30 Nov 2022 17:17:35 +0100 Subject: [PATCH 035/108] last tests --- .../e2e/pageObjects/memory-efficiency-page.ts | 2 + .../memory-efficiency/recommendations.e2e.ts | 273 +++++++++--------- 2 files changed, 134 insertions(+), 141 deletions(-) diff --git a/tests/e2e/pageObjects/memory-efficiency-page.ts b/tests/e2e/pageObjects/memory-efficiency-page.ts index 38cd0b6a5c..9b544d40ff 100644 --- a/tests/e2e/pageObjects/memory-efficiency-page.ts +++ b/tests/e2e/pageObjects/memory-efficiency-page.ts @@ -58,4 +58,6 @@ export class MemoryEfficiencyPage { combineStringsAccordion = Selector('[data-testid=combineSmallStringsToHashes-accordion]'); increaseSetAccordion = Selector('[data-testid=increaseSetMaxIntsetEntries-accordion]'); avoidLogicalDbAccordion = Selector('[data-testid=avoidLogicalDatabases-accordion]'); + convertHashToZipAccordion = Selector('[data-testid=convertHashtableToZiplist-accordion]'); + compressHashAccordion = Selector('[data-testid=compressHashFieldNames-accordion]'); } diff --git a/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts b/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts index fed05fe07e..9377ff4f4d 100644 --- a/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts +++ b/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts @@ -5,7 +5,7 @@ import { commonUrl, ossStandaloneBigConfig, ossStandaloneConfig } from '../../.. import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; import { CliActions } from '../../../common-actions/cli-actions'; import { Common } from '../../../helpers/common'; -import { populateHashWithFields, populateSetWithMembers, populateZSetWithMembers } from '../../../helpers/keys'; +import { populateHashWithFields, populateSetWithMembers } from '../../../helpers/keys'; const memoryEfficiencyPage = new MemoryEfficiencyPage(); const myRedisDatabasePage = new MyRedisDatabasePage(); @@ -16,153 +16,138 @@ const cliPage = new CliPage(); const addRedisDatabasePage = new AddRedisDatabasePage(); const externalPageLink = 'https://docs.redis.com/latest/ri/memory-optimizations/'; -const keyName = `hugeHashKey-${common.generateWord(5)}`; +let keyName = `recomKey-${common.generateWord(10)}`; const stringKeyName = `smallStringKey-${common.generateWord(5)}`; -const setKeyName = `hugeSetKey-${common.generateWord(4)}`; -const stringBigKeyName = `bigStringKey-${common.generateWord(5)}`; const index = '1'; -// const dbParameters = { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port }; -const dbParameters = { host: 'localhost', port: '8100' }; -const setKeyParameters = { membersCount: 512, keyName: setKeyName, memberStartWith: 'setMember' }; -const zsetKeyParameters = { membersCount: 128, keyName: setKeyName, memberStartWith: 'zsetMember' }; +const dbParameters = { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port }; fixture `Memory Efficiency Recommendations` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); // Go to Analysis Tools page await t.click(myRedisDatabasePage.analysisPageButton); - await t.click(memoryEfficiencyPage.newReportBtn); }) - .afterEach(async() => { - await cliPage.sendCommandInCli('SCRIPT FLUSH'); - await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + .afterEach(async t => { + // Clear and delete database + await t.click(myRedisDatabasePage.browserButton); + await browserPage.deleteKeyByName(keyName); + await deleteStandaloneDatabaseApi(ossStandaloneConfig); }); -test('Avoid dynamic Lua script recommendation', async t => { - // Go to Recommendations tab - await t.click(memoryEfficiencyPage.recommendationsTab); - // Add cached scripts and generate new report - await cliActions.addCachedScripts(10); - await t.click(memoryEfficiencyPage.newReportBtn); - // No recommendation with 10 cached scripts - await t.expect(memoryEfficiencyPage.luaScriptAccordion.exists).notOk('Avoid dynamic lua script recommendation displayed with 10 cached scripts'); - // Add the last cached script to see the recommendation - await cliActions.addCachedScripts(1); - await t.click(memoryEfficiencyPage.newReportBtn); - // Verify that user can see Avoid dynamic Lua script recommendation when number_of_cached_scripts> 10 - await t.expect(memoryEfficiencyPage.luaScriptAccordion.exists).ok('Avoid dynamic lua script recommendation not displayed'); - - // Verify that user can see Use smaller keys recommendation when database has 1M+ keys - await t.expect(memoryEfficiencyPage.useSmallKeysAccordion.exists).ok('Use smaller keys recommendation not displayed'); - - // Verify that user can expand/collapse recommendation - const expandedTextConaiterSize = await memoryEfficiencyPage.luaScriptTextContainer.offsetHeight; - await t.click(memoryEfficiencyPage.luaScriptButton); - await t.expect(memoryEfficiencyPage.luaScriptTextContainer.offsetHeight).lt(expandedTextConaiterSize, 'Recommendation not collapsed'); - await t.click(memoryEfficiencyPage.luaScriptButton); - await t.expect(memoryEfficiencyPage.luaScriptTextContainer.offsetHeight).eql(expandedTextConaiterSize, 'Recommendation not expanded'); - - // Verify that user can navigate by link to see the recommendation - await t.click(memoryEfficiencyPage.readMoreLink); - await common.checkURL(externalPageLink); - // Close the window with external link to switch to the application window - await t.closeWindow(); -}); test .before(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); - await browserPage.addHashKey(keyName, '2147476121', 'field', 'value'); + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); // Go to Analysis Tools page await t.click(myRedisDatabasePage.analysisPageButton); await t.click(memoryEfficiencyPage.newReportBtn); }) - .after(async t => { - // Clear and delete database - await t.click(myRedisDatabasePage.browserButton); - await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); - })('Shard big hashes to small hashes recommendation', async t => { - const noRecommendationsMessage = 'No Recommendations at the moment.'; - const dbParameters = { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port }; - const keyToAddParameters = { fieldsCount: 4999, keyName, fieldStartWith: 'hashField', fieldValueStartWith: 'hashValue' }; - const keyToAddParameters2 = { fieldsCount: 1, keyName, fieldStartWith: 'hashFieldLast', fieldValueStartWith: 'hashValueLast' }; - + .after(async() => { + await cliPage.sendCommandInCli('SCRIPT FLUSH'); + await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + })('Avoid dynamic Lua script recommendation', async t => { // Go to Recommendations tab await t.click(memoryEfficiencyPage.recommendationsTab); - // No recommendations message - await t.expect(memoryEfficiencyPage.noRecommendationsMessage.textContent).eql(noRecommendationsMessage, 'No recommendations message not displayed'); - - // Add 5000 fields to the hash key - await populateHashWithFields(dbParameters.host, dbParameters.port, keyToAddParameters); - // Generate new report + // Add cached scripts and generate new report + await cliActions.addCachedScripts(10); await t.click(memoryEfficiencyPage.newReportBtn); - // Verify that big keys recommendation not displayed when hash has 5000 fields - await t.expect(memoryEfficiencyPage.noRecommendationsMessage.textContent).eql(noRecommendationsMessage, 'No recommendations message not displayed'); - // Add the last field in hash key - await populateHashWithFields(dbParameters.host, dbParameters.port, keyToAddParameters2); - // Generate new report + // No recommendation with 10 cached scripts + await t.expect(memoryEfficiencyPage.luaScriptAccordion.exists).notOk('Avoid dynamic lua script recommendation displayed with 10 cached scripts'); + // Add the last cached script to see the recommendation + await cliActions.addCachedScripts(1); await t.click(memoryEfficiencyPage.newReportBtn); - // Verify that user can see Shard big hashes to small hashes recommendation when Hash length > 5,000 - await t.expect(memoryEfficiencyPage.bigHashesAccordion.exists).ok('Shard big hashes to small hashes recommendation not displayed'); - await t.expect(memoryEfficiencyPage.codeChangesLabel.exists).ok('Big hashes recommendation not have Code Changes label'); - await t.expect(memoryEfficiencyPage.configurationChangesLabel.exists).ok('Big hashes recommendation not have Configuration Changes label'); + // Verify that user can see Avoid dynamic Lua script recommendation when number_of_cached_scripts> 10 + await t.expect(memoryEfficiencyPage.luaScriptAccordion.exists).ok('Avoid dynamic lua script recommendation not displayed'); + + // Verify that user can see Use smaller keys recommendation when database has 1M+ keys + await t.expect(memoryEfficiencyPage.useSmallKeysAccordion.exists).ok('Use smaller keys recommendation not displayed'); + + // Verify that user can expand/collapse recommendation + const expandedTextConaiterSize = await memoryEfficiencyPage.luaScriptTextContainer.offsetHeight; + await t.click(memoryEfficiencyPage.luaScriptButton); + await t.expect(memoryEfficiencyPage.luaScriptTextContainer.offsetHeight).lt(expandedTextConaiterSize, 'Recommendation not collapsed'); + await t.click(memoryEfficiencyPage.luaScriptButton); + await t.expect(memoryEfficiencyPage.luaScriptTextContainer.offsetHeight).eql(expandedTextConaiterSize, 'Recommendation not expanded'); + + // Verify that user can navigate by link to see the recommendation + await t.click(memoryEfficiencyPage.readMoreLink); + await common.checkURL(externalPageLink); + // Close the window with external link to switch to the application window + await t.closeWindow(); }); +test('Shard big hashes to small hashes recommendation', async t => { + keyName = `recomKey-${common.generateWord(10)}`; + const noRecommendationsMessage = 'No Recommendations at the moment.'; + const keyToAddParameters = { fieldsCount: 4999, keyName, fieldStartWith: 'hashField', fieldValueStartWith: 'hashValue' }; + const keyToAddParameters2 = { fieldsCount: 1, keyName, fieldStartWith: 'hashFieldLast', fieldValueStartWith: 'hashValueLast' }; + const command = `HSET ${keyName} field value`; + + // Create Hash key and create report + await cliPage.sendCommandInCli(command); + await t.click(memoryEfficiencyPage.newReportBtn); + // Go to Recommendations tab + await t.click(memoryEfficiencyPage.recommendationsTab); + // No recommendations message + await t.expect(memoryEfficiencyPage.noRecommendationsMessage.textContent).eql(noRecommendationsMessage, 'No recommendations message not displayed'); + // Add 5000 fields to the hash key + await populateHashWithFields(dbParameters.host, dbParameters.port, keyToAddParameters); + // Generate new report + await t.click(memoryEfficiencyPage.newReportBtn); + // Verify that big keys recommendation not displayed when hash has 5000 fields + await t.expect(memoryEfficiencyPage.noRecommendationsMessage.textContent).eql(noRecommendationsMessage, 'No recommendations message not displayed'); + // Add the last field in hash key + await populateHashWithFields(dbParameters.host, dbParameters.port, keyToAddParameters2); + // Generate new report + await t.click(memoryEfficiencyPage.newReportBtn); + // Verify that user can see Shard big hashes to small hashes recommendation when Hash length > 5,000 + await t.expect(memoryEfficiencyPage.bigHashesAccordion.exists).ok('Shard big hashes to small hashes recommendation not displayed'); + await t.expect(memoryEfficiencyPage.codeChangesLabel.exists).ok('Big hashes recommendation not have Code Changes label'); + await t.expect(memoryEfficiencyPage.configurationChangesLabel.exists).ok('Big hashes recommendation not have Configuration Changes label'); +}); +test('Combine small strings to hashes recommendation', async t => { + keyName = `recomKey-${common.generateWord(10)}`; + const commandToAddKey = `SET ${stringKeyName} value`; + const command = `SET ${keyName} "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed accumsan lectus sed diam suscipit, eu ullamcorper ligula pulvinar."`; + // Create small String key and create report + await cliPage.sendCommandInCli(commandToAddKey); + await t.click(memoryEfficiencyPage.newReportBtn); + // Go to Recommendations tab + await t.click(memoryEfficiencyPage.recommendationsTab); + // Verify that user can see Combine small strings to hashes recommendation when there are strings that are less than 200 bytes + await t.expect(memoryEfficiencyPage.combineStringsAccordion.exists).ok('Combine small strings to hashes recommendation not displayed for small string'); + await t.expect(memoryEfficiencyPage.codeChangesLabel.exists).ok('Combine small strings to hashes recommendation not have Code Changes label'); + + // Add String key with more than 200 bytes + await cliPage.sendCommandInCli(command); + // Delete small String key + await cliPage.sendCommandInCli(`DEL ${stringKeyName}`); + await t.click(memoryEfficiencyPage.newReportBtn); + // Verify that user can not see recommendation when there is no string that is less than 200 bytes + await t.expect(memoryEfficiencyPage.combineStringsAccordion.exists).notOk('Combine small strings to hashes recommendation is displayed for huge string'); +}); test - .before(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); - await browserPage.addStringKey(stringKeyName, '2147476121', 'field'); - // Go to Analysis Tools page - await t.click(myRedisDatabasePage.analysisPageButton); - await t.click(memoryEfficiencyPage.newReportBtn); - // Go to Recommendations tab - await t.click(memoryEfficiencyPage.recommendationsTab); - }) .after(async t => { // Clear and delete database await t.click(myRedisDatabasePage.browserButton); - await browserPage.deleteKeyByName(stringBigKeyName); + await browserPage.deleteKeyByName(keyName); + // Return back set-max-intset-entries value + await cliPage.sendCommandInCli('config set set-max-intset-entries 512'); await deleteStandaloneDatabaseApi(ossStandaloneConfig); - })('Combine small strings to hashes recommendation', async t => { - const command = `SET ${stringBigKeyName} "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed accumsan lectus sed diam suscipit, eu ullamcorper ligula pulvinar."`; - - // Verify that user can see Combine small strings to hashes recommendation when there are strings that are less than 200 bytes - await t.expect(memoryEfficiencyPage.combineStringsAccordion.exists).ok('Combine small strings to hashes recommendation not displayed for small string'); - await t.expect(memoryEfficiencyPage.codeChangesLabel.exists).ok('Combine small strings to hashes recommendation not have Code Changes label'); - - // Add String key with more than 200 bytes - await cliPage.sendCommandInCli(command); - // Delete small String key - await cliPage.sendCommandInCli(`DEL ${stringKeyName}`); + })('Increase the set-max-intset-entries recommendation', async t => { + keyName = `recomKey-${common.generateWord(10)}`; + const commandToAddKey = `SADD ${keyName} member`; + const command = 'config set set-max-intset-entries 1024'; + const setKeyParameters = { membersCount: 512, keyName, memberStartWith: 'setMember' }; - await t.click(memoryEfficiencyPage.newReportBtn); - // Verify that user can not see recommendation when there is at least one string that are more than 200 bytes - await t.expect(memoryEfficiencyPage.combineStringsAccordion.exists).notOk('Combine small strings to hashes recommendation is displayed for huge string'); - }); -test - .before(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); - await browserPage.addSetKey(setKeyName, '2147476121', 'member'); - // Add 512 members to the set key + // Create Set with 513 members and create report + await cliPage.sendCommandInCli(commandToAddKey); await populateSetWithMembers(dbParameters.host, dbParameters.port, setKeyParameters); - // Go to Analysis Tools page - await t.click(myRedisDatabasePage.analysisPageButton); await t.click(memoryEfficiencyPage.newReportBtn); // Go to Recommendations tab await t.click(memoryEfficiencyPage.recommendationsTab); - }) - .after(async t => { - // Clear and delete database - await t.click(myRedisDatabasePage.browserButton); - await browserPage.deleteKeyByName(setKeyName); - await cliPage.sendCommandInCli('config set set-max-intset-entries 512'); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); - })('Increase the set-max-intset-entries recommendation', async t => { - const command = 'config set set-max-intset-entries 1024'; - // Verify that user can see Increase the set-max-intset-entries recommendation when Found sets with length > set-max-intset-entries await t.expect(memoryEfficiencyPage.increaseSetAccordion.exists).ok('Increase the set-max-intset-entries recommendation not displayed'); await t.expect(memoryEfficiencyPage.configurationChangesLabel.exists).ok('Increase the set-max-intset-entries recommendation not have Configuration Changes label'); @@ -176,16 +161,12 @@ test test .before(async t => { await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + keyName = `recomKey-${common.generateWord(10)}`; await browserPage.addStringKey(stringKeyName, '2147476121', 'field'); await t.click(myRedisDatabasePage.myRedisDBButton); await addRedisDatabasePage.addLogicalRedisDatabase(ossStandaloneConfig, index); await myRedisDatabasePage.clickOnDBByName(`${ossStandaloneConfig.databaseName} [${index}]`); await browserPage.addHashKey(keyName, '2147476121', 'field', 'value'); - // Go to Analysis Tools page - await t.click(myRedisDatabasePage.analysisPageButton); - await t.click(memoryEfficiencyPage.newReportBtn); - // Go to Recommendations tab - await t.click(memoryEfficiencyPage.recommendationsTab); }) .after(async t => { // Clear and delete database @@ -196,38 +177,48 @@ test await browserPage.deleteKeyByName(stringKeyName); await deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Avoid using logical databases', async t => { - // Verify that user can see Avoid using logical databases recommendation when the database supports logical databases and there are keys in more than 1 logical database - await t.expect(memoryEfficiencyPage.avoidLogicalDbAccordion.exists).ok('Avoid using logical databases recommendation not displayed'); - await t.expect(memoryEfficiencyPage.codeChangesLabel.exists).ok('Avoid using logical databases recommendation not have Code Changes label'); - }); -test.only - .before(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); - await browserPage.addZSetKey(setKeyName, '2147476121', 'member'); - // Add 128 members to the zset key - await populateZSetWithMembers(dbParameters.host, dbParameters.port, zsetKeyParameters); // Go to Analysis Tools page await t.click(myRedisDatabasePage.analysisPageButton); await t.click(memoryEfficiencyPage.newReportBtn); // Go to Recommendations tab await t.click(memoryEfficiencyPage.recommendationsTab); - }) + // Verify that user can see Avoid using logical databases recommendation when the database supports logical databases and there are keys in more than 1 logical database + await t.expect(memoryEfficiencyPage.avoidLogicalDbAccordion.exists).ok('Avoid using logical databases recommendation not displayed'); + await t.expect(memoryEfficiencyPage.codeChangesLabel.exists).ok('Avoid using logical databases recommendation not have Code Changes label'); + }); +test .after(async t => { // Clear and delete database await t.click(myRedisDatabasePage.browserButton); - await browserPage.deleteKeyByName(setKeyName); - await cliPage.sendCommandInCli('config set zset-max-ziplist-entries 128'); + await browserPage.deleteKeyByName(keyName); + await cliPage.sendCommandInCli('config set hash-max-ziplist-entries 512'); await deleteStandaloneDatabaseApi(ossStandaloneConfig); - })('Convert hashtable to ziplist for hashes recommendation', async t => { - const command = 'config set zset-max-ziplist-entries 256'; + })('Compress Hash field names', async t => { + keyName = `recomKey-${common.generateWord(10)}`; + const command = 'config set hash-max-ziplist-entries 1001'; + const commandToAddKey = `HSET ${keyName} field value`; + const hashKeyParameters = { fieldsCount: 1000, keyName, fieldStartWith: 'hashField', fieldValueStartWith: 'hashValue' }; + + // Create Hash key + await cliPage.sendCommandInCli(commandToAddKey); + // Add 1000 fields to Hash key and create report + await populateHashWithFields(dbParameters.host, dbParameters.port, hashKeyParameters); + await t.click(memoryEfficiencyPage.newReportBtn); + // Go to Recommendations tab + await t.click(memoryEfficiencyPage.recommendationsTab); + + // Verify that user can see Compress Hash field names recommendation when Hash length > 1,000 + await t.expect(memoryEfficiencyPage.compressHashAccordion.exists).ok('Compress Hash field names recommendation not displayed'); + await t.expect(memoryEfficiencyPage.configurationChangesLabel.exists).ok('Compress Hash field names recommendation not have Configuration Changes label'); + // Convert hashtable to ziplist for hashes recommendation // Verify that user can see Convert hashtable to ziplist for hashes recommendation when the number of hash entries exceeds hash-max-ziplist-entries - await t.expect(memoryEfficiencyPage.increaseSetAccordion.exists).ok('Increase the set-max-intset-entries recommendation not displayed'); - await t.expect(memoryEfficiencyPage.configurationChangesLabel.exists).ok('Increase the set-max-intset-entries recommendation not have Configuration Changes label'); + await t.expect(memoryEfficiencyPage.convertHashToZipAccordion.exists).ok('Convert hashtable to ziplist for hashes recommendation not displayed'); + await t.expect(memoryEfficiencyPage.configurationChangesLabel.exists).ok('Convert hashtable to ziplist for hashes recommendation not have Configuration Changes label'); - // Change config max entries to 256 - await cliPage.sendCommandInCli(command); - await t.click(memoryEfficiencyPage.newReportBtn); - // Verify that user can not see Convert hashtable to ziplist for hashes recommendation when the number of hash entries not exceeds hash-max-ziplist-entries - await t.expect(memoryEfficiencyPage.increaseSetAccordion.exists).notOk('Increase the set-max-intset-entries recommendation is displayed'); - }); \ No newline at end of file + // Change config max entries to 1001 + await cliPage.sendCommandInCli(command); + await t.click(memoryEfficiencyPage.newReportBtn); + // Verify that user can not see Convert hashtable to ziplist for hashes recommendation when the number of hash entries not exceeds hash-max-ziplist-entries + await t.expect(memoryEfficiencyPage.convertHashToZipAccordion.exists).notOk('Convert hashtable to ziplist for hashes recommendation is displayed'); + }); From 19008eb903739f6a8f63df9160994779fbe38f4d Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Wed, 30 Nov 2022 20:20:09 +0400 Subject: [PATCH 036/108] update badge styles --- .../components/recommendations-view/styles.module.scss | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss index 4360fd75c3..8d1b49428e 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss @@ -63,10 +63,14 @@ } .badgesContainer { - margin-right: 48px; - .badge { + width: 180px; margin: 14px 18px; + + &:last-child { + padding-right: 48px; + align-items: flex-end; + } .badgeIcon { margin-right: 14px; From fd674dfb2293f99549a7a4bde70204277e999d05 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Thu, 1 Dec 2022 10:47:30 +0100 Subject: [PATCH 037/108] fix --- .../critical-path/memory-efficiency/recommendations.e2e.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts b/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts index 9377ff4f4d..3957b26d5e 100644 --- a/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts +++ b/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts @@ -95,7 +95,7 @@ test('Shard big hashes to small hashes recommendation', async t => { // Generate new report await t.click(memoryEfficiencyPage.newReportBtn); // Verify that big keys recommendation not displayed when hash has 5000 fields - await t.expect(memoryEfficiencyPage.noRecommendationsMessage.textContent).eql(noRecommendationsMessage, 'No recommendations message not displayed'); + await t.expect(memoryEfficiencyPage.bigHashesAccordion.exists).notOk('Shard big hashes to small hashes recommendation is displayed when hash has 5000 fields'); // Add the last field in hash key await populateHashWithFields(dbParameters.host, dbParameters.port, keyToAddParameters2); // Generate new report @@ -125,7 +125,7 @@ test('Combine small strings to hashes recommendation', async t => { // Delete small String key await cliPage.sendCommandInCli(`DEL ${stringKeyName}`); await t.click(memoryEfficiencyPage.newReportBtn); - // Verify that user can not see recommendation when there is no string that is less than 200 bytes + // Verify that user can not see recommendation when there are only strings with more than 200 bytes await t.expect(memoryEfficiencyPage.combineStringsAccordion.exists).notOk('Combine small strings to hashes recommendation is displayed for huge string'); }); test From e17fa553b4b74b5f4059aa77ba5e02621f2bb104 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Fri, 2 Dec 2022 13:16:52 +0400 Subject: [PATCH 038/108] #RI-3569 - add comression for list recommendation --- redisinsight/api/src/constants/index.ts | 1 + .../api/src/constants/recommendations.ts | 11 +++ .../providers/recommendation.provider.spec.ts | 34 +++++++-- .../providers/recommendation.provider.ts | 65 ++++++++++++---- .../recommendation/recommendation.service.ts | 1 + .../POST-databases-id-analysis.test.ts | 27 ++++++- redisinsight/api/test/helpers/constants.ts | 18 +++-- redisinsight/api/test/helpers/data/redis.ts | 20 +++++ .../constants/dbAnalysisRecommendations.json | 35 +++++++++ .../Recommendations.spec.tsx | 74 +++++-------------- .../recommendations-view/Recommendations.tsx | 3 - 11 files changed, 203 insertions(+), 86 deletions(-) create mode 100644 redisinsight/api/src/constants/recommendations.ts diff --git a/redisinsight/api/src/constants/index.ts b/redisinsight/api/src/constants/index.ts index 29b0a50e30..bba5b52d42 100644 --- a/redisinsight/api/src/constants/index.ts +++ b/redisinsight/api/src/constants/index.ts @@ -9,3 +9,4 @@ export * from './redis-commands'; export * from './telemetry-events'; export * from './app-events'; export * from './redis-connection'; +export * from './recommendations'; diff --git a/redisinsight/api/src/constants/recommendations.ts b/redisinsight/api/src/constants/recommendations.ts new file mode 100644 index 0000000000..15468b0363 --- /dev/null +++ b/redisinsight/api/src/constants/recommendations.ts @@ -0,0 +1,11 @@ +export const RECOMMENDATION_NAMES = Object.freeze({ + LUA_SCRIPT: 'luaScript', + BIG_HASHES: 'bigHashes', + USE_SMALLER_KEYS: 'useSmallerKeys', + AVOID_LOGICAL_DATABASES: 'avoidLogicalDatabases', + COMBINE_SMALL_STRINGS_TO_HASHES: 'combineSmallStringsToHashes', + INCREASE_SET_MAX_INTSET_ENTRIES: 'increaseSetMaxIntsetEntries', + CONVERT_HASHTABLE_TO_ZIPLIST: 'convertHashtableToZiplist', + COMPRESS_HASH_FIELD_NAMES: 'compressHashFieldNames', + COMPRESSION_FOR_LIST: 'compressionForList', +}); diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts index 6e823e43bb..5fd9ec8523 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts @@ -1,5 +1,6 @@ import IORedis from 'ioredis'; import { when } from 'jest-when'; +import { RECOMMENDATION_NAMES } from 'src/constants'; import { RecommendationProvider } from 'src/modules/recommendation/providers/recommendation.provider'; const nodeClient = Object.create(IORedis.prototype); @@ -64,6 +65,10 @@ const mockBigSet = { name: Buffer.from('name'), type: 'set', length: 513, memory: 10, ttl: -1, }; +const mockBigListKey = { + name: Buffer.from('name'), type: 'list', length: 1001, memory: 10, ttl: -1, +}; + describe('RecommendationProvider', () => { const service = new RecommendationProvider(); @@ -83,7 +88,7 @@ describe('RecommendationProvider', () => { .mockResolvedValue(mockRedisMemoryInfoResponse_2); const luaScriptRecommendation = await service.determineLuaScriptRecommendation(nodeClient); - expect(luaScriptRecommendation).toEqual({ name: 'luaScript' }); + expect(luaScriptRecommendation).toEqual({ name: RECOMMENDATION_NAMES.LUA_SCRIPT }); }); it('should not return luaScript recommendation when info command executed with error', async () => { @@ -105,7 +110,7 @@ describe('RecommendationProvider', () => { const bigHashesRecommendation = await service.determineBigHashesRecommendation( [...mockKeys, mockBigHashKey], ); - expect(bigHashesRecommendation).toEqual({ name: 'bigHashes' }); + expect(bigHashesRecommendation).toEqual({ name: RECOMMENDATION_NAMES.BIG_HASHES }); }); }); @@ -116,7 +121,7 @@ describe('RecommendationProvider', () => { }); it('should return useSmallerKeys recommendation', async () => { const bigTotalRecommendation = await service.determineBigTotalRecommendation(1_000_001); - expect(bigTotalRecommendation).toEqual({ name: 'useSmallerKeys' }); + expect(bigTotalRecommendation).toEqual({ name: RECOMMENDATION_NAMES.USE_SMALLER_KEYS }); }); }); @@ -178,7 +183,7 @@ describe('RecommendationProvider', () => { }); it('should return combineSmallStringsToHashes recommendation', async () => { const smallStringRecommendation = await service.determineCombineSmallStringsToHashesRecommendation(mockKeys); - expect(smallStringRecommendation).toEqual({ name: 'combineSmallStringsToHashes' }); + expect(smallStringRecommendation).toEqual({ name: RECOMMENDATION_NAMES.COMBINE_SMALL_STRINGS_TO_HASHES }); }); }); @@ -200,7 +205,8 @@ describe('RecommendationProvider', () => { const increaseSetMaxIntsetEntriesRecommendation = await service .determineIncreaseSetMaxIntsetEntriesRecommendation(nodeClient, [...mockKeys, mockBigSet]); - expect(increaseSetMaxIntsetEntriesRecommendation).toEqual({ name: 'increaseSetMaxIntsetEntries' }); + expect(increaseSetMaxIntsetEntriesRecommendation) + .toEqual({ name: RECOMMENDATION_NAMES.INCREASE_SET_MAX_INTSET_ENTRIES }); }); it('should not return increaseSetMaxIntsetEntries recommendation when config command executed with error', @@ -233,7 +239,8 @@ describe('RecommendationProvider', () => { const convertHashtableToZiplistRecommendation = await service .determineConvertHashtableToZiplistRecommendation(nodeClient, [...mockKeys, mockBigHashKey_3]); - expect(convertHashtableToZiplistRecommendation).toEqual({ name: 'convertHashtableToZiplist' }); + expect(convertHashtableToZiplistRecommendation) + .toEqual({ name: RECOMMENDATION_NAMES.CONVERT_HASHTABLE_TO_ZIPLIST }); }); it('should not return convertHashtableToZiplist recommendation when config command executed with error', @@ -257,7 +264,20 @@ describe('RecommendationProvider', () => { it('should return compressHashFieldNames recommendation', async () => { const compressHashFieldNamesRecommendation = await service .determineCompressHashFieldNamesRecommendation([mockBigHashKey_2]); - expect(compressHashFieldNamesRecommendation).toEqual({ name: 'compressHashFieldNames' }); + expect(compressHashFieldNamesRecommendation).toEqual({ name: RECOMMENDATION_NAMES.COMPRESS_HASH_FIELD_NAMES }); + }); + }); + + describe('determineCompressionForListRecommendation', () => { + it('should not return compressionForList recommendation', async () => { + const compressHashFieldNamesRecommendation = await service + .determineCompressionForListRecommendation(mockKeys); + expect(compressHashFieldNamesRecommendation).toEqual(null); + }); + it('should return compressionForList recommendation', async () => { + const compressHashFieldNamesRecommendation = await service + .determineCompressionForListRecommendation([mockBigListKey]); + expect(compressHashFieldNamesRecommendation).toEqual({ name: RECOMMENDATION_NAMES.COMPRESSION_FOR_LIST }); }); }); }); diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index a1908c9d39..ff7b5d1d45 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -1,7 +1,8 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { Redis, Cluster, Command } from 'ioredis'; import { get } from 'lodash'; import { convertRedisInfoReplyToObject, convertBulkStringsToObject } from 'src/utils'; +import { RECOMMENDATION_NAMES } from 'src/constants'; import { RedisDataType } from 'src/modules/browser/dto'; import { Recommendation } from 'src/modules/database-analysis/models/recommendation'; import { Key } from 'src/modules/database-analysis/models'; @@ -11,9 +12,12 @@ const maxHashLength = 5000; const maxStringMemory = 200; const maxDatabaseTotal = 1_000_000; const maxCompressHashLength = 1000; +const maxListLength = 1000; @Injectable() export class RecommendationProvider { + private logger = new Logger('RecommendationProvider'); + /** * Check lua script recommendation * @param redisClient @@ -30,9 +34,10 @@ export class RecommendationProvider { const nodesNumbersOfCachedScripts = get(info, 'memory.number_of_cached_scripts'); return parseInt(nodesNumbersOfCachedScripts, 10) > minNumberOfCachedScripts - ? { name: 'luaScript' } + ? { name: RECOMMENDATION_NAMES.LUA_SCRIPT } : null; } catch (err) { + this.logger.error('Can not determine Lua script recommendation', err); return null; } } @@ -44,18 +49,23 @@ export class RecommendationProvider { async determineBigHashesRecommendation( keys: Key[], ): Promise { - const bigHashes = keys.some((key) => key.type === RedisDataType.Hash && key.length > maxHashLength); - return bigHashes ? { name: 'bigHashes' } : null; + try { + const bigHashes = keys.some((key) => key.type === RedisDataType.Hash && key.length > maxHashLength); + return bigHashes ? { name: RECOMMENDATION_NAMES.BIG_HASHES } : null; + } catch (err) { + this.logger.error('Can not determine Big Hashes recommendation', err); + return null; + } } /** - * Check big hashes recommendation + * Check use smaller keys recommendation * @param total */ async determineBigTotalRecommendation( total: number, ): Promise { - return total > maxDatabaseTotal ? { name: 'useSmallerKeys' } : null; + return total > maxDatabaseTotal ? { name: RECOMMENDATION_NAMES.USE_SMALLER_KEYS } : null; } /** @@ -79,8 +89,9 @@ export class RecommendationProvider { const { keys } = convertBulkStringsToObject(db as string, ',', '='); return keys > 0; }); - return databasesWithKeys.length > 1 ? { name: 'avoidLogicalDatabases' } : null; + return databasesWithKeys.length > 1 ? { name: RECOMMENDATION_NAMES.AVOID_LOGICAL_DATABASES } : null; } catch (err) { + this.logger.error('Can not determine Logical database recommendation', err); return null; } } @@ -92,8 +103,13 @@ export class RecommendationProvider { async determineCombineSmallStringsToHashesRecommendation( keys: Key[], ): Promise { - const smallString = keys.some((key) => key.type === RedisDataType.String && key.memory < maxStringMemory); - return smallString ? { name: 'combineSmallStringsToHashes' } : null; + try { + const smallString = keys.some((key) => key.type === RedisDataType.String && key.memory < maxStringMemory); + return smallString ? { name: RECOMMENDATION_NAMES.COMBINE_SMALL_STRINGS_TO_HASHES } : null; + } catch (err) { + this.logger.error('Can not determine Combine small strings to hashes recommendation', err); + return null; + } } /** @@ -117,8 +133,9 @@ export class RecommendationProvider { } const setMaxIntsetEntriesNumber = parseInt(setMaxIntsetEntries, 10); const bigSet = keys.some((key) => key.type === RedisDataType.Set && key.length > setMaxIntsetEntriesNumber); - return bigSet ? { name: 'increaseSetMaxIntsetEntries' } : null; + return bigSet ? { name: RECOMMENDATION_NAMES.INCREASE_SET_MAX_INTSET_ENTRIES } : null; } catch (err) { + this.logger.error('Can not determine Increase set max intset entries recommendation', err); return null; } } @@ -140,8 +157,9 @@ export class RecommendationProvider { ) as string[]; const hashMaxZiplistEntriesNumber = parseInt(hashMaxZiplistEntries, 10); const bigHash = keys.some((key) => key.type === RedisDataType.Hash && key.length > hashMaxZiplistEntriesNumber); - return bigHash ? { name: 'convertHashtableToZiplist' } : null; + return bigHash ? { name: RECOMMENDATION_NAMES.CONVERT_HASHTABLE_TO_ZIPLIST } : null; } catch (err) { + this.logger.error('Can not determine Convert hashtable to ziplist recommendation', err); return null; } } @@ -153,7 +171,28 @@ export class RecommendationProvider { async determineCompressHashFieldNamesRecommendation( keys: Key[], ): Promise { - const bigHash = keys.some((key) => key.type === RedisDataType.Hash && key.length > maxCompressHashLength); - return bigHash ? { name: 'compressHashFieldNames' } : null; + try { + const bigHash = keys.some((key) => key.type === RedisDataType.Hash && key.length > maxCompressHashLength); + return bigHash ? { name: RECOMMENDATION_NAMES.COMPRESS_HASH_FIELD_NAMES } : null; + } catch (err) { + this.logger.error('Can not determine Compress hash field names recommendation', err); + return null; + } + } + + /** + * Check compression for list recommendation + * @param keys + */ + async determineCompressionForListRecommendation( + keys: Key[], + ): Promise { + try { + const bugList = keys.some((key) => key.type === RedisDataType.List && key.length > maxListLength); + return bugList ? { name: RECOMMENDATION_NAMES.COMPRESSION_FOR_LIST } : null; + } catch (err) { + this.logger.error('Can not determine Compression for list recommendation', err); + return null; + } } } diff --git a/redisinsight/api/src/modules/recommendation/recommendation.service.ts b/redisinsight/api/src/modules/recommendation/recommendation.service.ts index 3dab87a31a..3a405f012a 100644 --- a/redisinsight/api/src/modules/recommendation/recommendation.service.ts +++ b/redisinsight/api/src/modules/recommendation/recommendation.service.ts @@ -43,6 +43,7 @@ export class RecommendationService { await this.recommendationProvider.determineIncreaseSetMaxIntsetEntriesRecommendation(client, keys), await this.recommendationProvider.determineConvertHashtableToZiplistRecommendation(client, keys), await this.recommendationProvider.determineCompressHashFieldNamesRecommendation(keys), + await this.recommendationProvider.determineCompressionForListRecommendation(keys), ])); } } diff --git a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts index 712234f749..5f470a2fe7 100644 --- a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts +++ b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts @@ -284,7 +284,32 @@ describe('POST /databases/:instanceId/analysis', () => { expect(body.recommendations).to.deep.eq([ constants.TEST_CONVERT_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, constants.TEST_COMPRESS_HASH_FIELD_NAMES_RECOMMENDATION, - + ]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } + }, + { + name: 'Should create new database analysis with compressionForList recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + const NUMBERS_OF_LIST_ELEMENTS = 1001; + await rte.data.generateHugeElementsForListKey(NUMBERS_OF_LIST_ELEMENTS, true); + }, + checkFn: async ({ body }) => { + expect(body.totalKeys.total).to.gt(0); + expect(body.totalMemory.total).to.gt(0); + expect(body.topKeysNsp.length).to.gt(0); + expect(body.topMemoryNsp.length).to.gt(0); + expect(body.topKeysLength.length).to.gt(0); + expect(body.topKeysMemory.length).to.gt(0); + expect(body.recommendations).to.deep.eq([ + constants.TEST_COMPRESSION_FOR_LIST_RECOMMENDATION, ]); }, after: async () => { diff --git a/redisinsight/api/test/helpers/constants.ts b/redisinsight/api/test/helpers/constants.ts index 1673bbc6b9..71b835694b 100644 --- a/redisinsight/api/test/helpers/constants.ts +++ b/redisinsight/api/test/helpers/constants.ts @@ -1,6 +1,7 @@ import { v4 as uuidv4 } from 'uuid'; import { randomBytes } from 'crypto'; import { getASCIISafeStringFromBuffer, getBufferFromSafeASCIIString } from "src/utils/cli-helper"; +import { RECOMMENDATION_NAMES } from 'src/constants'; const API = { DATABASES: 'databases', @@ -452,25 +453,28 @@ export const constants = { }, TEST_LUA_DATABASE_ANALYSIS_RECOMMENDATION: { - name: 'luaScript', + name: RECOMMENDATION_NAMES.LUA_SCRIPT, }, TEST_BIG_HASHES_DATABASE_ANALYSIS_RECOMMENDATION: { - name: 'bigHashes', + name: RECOMMENDATION_NAMES.BIG_HASHES, }, TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION: { - name: 'useSmallerKeys', + name: RECOMMENDATION_NAMES.USE_SMALLER_KEYS, }, TEST_INCREASE_SET_MAX_INTSET_ENTRIES_RECOMMENDATION: { - name: 'increaseSetMaxIntsetEntries', + name: RECOMMENDATION_NAMES.INCREASE_SET_MAX_INTSET_ENTRIES, }, TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION: { - name: 'combineSmallStringsToHashes', + name: RECOMMENDATION_NAMES.COMBINE_SMALL_STRINGS_TO_HASHES, }, TEST_CONVERT_HASHTABLE_TO_ZIPLIST_RECOMMENDATION: { - name: 'convertHashtableToZiplist', + name: RECOMMENDATION_NAMES.CONVERT_HASHTABLE_TO_ZIPLIST, }, TEST_COMPRESS_HASH_FIELD_NAMES_RECOMMENDATION: { - name: 'compressHashFieldNames', + name: RECOMMENDATION_NAMES.COMPRESS_HASH_FIELD_NAMES, + }, + TEST_COMPRESSION_FOR_LIST_RECOMMENDATION: { + name: RECOMMENDATION_NAMES.COMPRESSION_FOR_LIST, }, // etc... diff --git a/redisinsight/api/test/helpers/data/redis.ts b/redisinsight/api/test/helpers/data/redis.ts index 520076a8f9..1bf6d8a60e 100644 --- a/redisinsight/api/test/helpers/data/redis.ts +++ b/redisinsight/api/test/helpers/data/redis.ts @@ -218,6 +218,25 @@ export const initDataHelper = (rte) => { ); }; + + const generateHugeElementsForListKey = async (number: number = 100000, clean: boolean) => { + if (clean) { + await truncate(); + } + + const batchSize = 10000; + let inserted = 0; + do { + const pipeline = []; + const limit = inserted + batchSize; + for (inserted; inserted < limit && inserted < number; inserted++) { + pipeline.push(['lpush', constants.TEST_LIST_KEY_1, inserted]); + } + + await insertKeysBasedOnEnv(pipeline, true); + } while (inserted < number) + }; + // Set const generateSets = async (clean: boolean = false) => { if (clean) { @@ -507,6 +526,7 @@ export const initDataHelper = (rte) => { generateKeys, generateHugeNumberOfFieldsForHashKey, generateHugeNumberOfTinyStringKeys, + generateHugeElementsForListKey, generateHugeStream, generateNKeys, generateRedisearchIndexes, diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index bd05082176..ad033fbf39 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -193,5 +193,40 @@ } ], "badges": ["configuration_changes"] + }, + "compressionForList": { + "id": "compressionForList", + "title": "Enable compression for the list", + "content": [ + { + "id": "1", + "type": "paragraph", + "value": "If you use long lists, and mostly access elements from the head and tail only, then you can enable compression." + }, + { + "id": "2", + "type": "paragraph", + "value": "Set list-compression-depth=1 in redis.conf to compress every list node except the head and tail of the list. Though list operations that involve elements in the center of the list will get slower, the compression can increase CPU utilization." + }, + { + "id": "3", + "type": "spacer", + "value": "l" + }, + { + "id": "4", + "type": "span", + "value": "Run `INFO COMMANDSTATS` before and after making this change to verify the latency numbers. " + }, + { + "id": "5", + "type": "link", + "value": { + "href": "https://docs.redis.com/latest/ri/memory-optimizations/", + "name": "Read more" + } + } + ], + "badges": ["configuration_changes"] } } diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx index 7cde5742db..c5a868c35c 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx @@ -47,56 +47,6 @@ describe('Recommendations', () => { expect(screen.queryByTestId('recommendations-loader')).not.toBeInTheDocument() }) - describe('recommendations initial open', () => { - it('should render open recommendations', () => { - (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ - ...mockdbAnalysisSelector, - data: { - recommendations: [ - { name: 'luaScript' }, - { name: 'luaScript' }, - { name: 'luaScript' }, - { name: 'luaScript' }, - { name: 'luaScript' }, - ] - } - })) - - render() - - expect(screen.queryAllByTestId('luaScript-accordion')[0]?.classList.contains('euiAccordion-isOpen')).toBeTruthy() - expect(screen.queryAllByTestId('luaScript-accordion')[1]?.classList.contains('euiAccordion-isOpen')).toBeTruthy() - expect(screen.queryAllByTestId('luaScript-accordion')[2]?.classList.contains('euiAccordion-isOpen')).toBeTruthy() - expect(screen.queryAllByTestId('luaScript-accordion')[3]?.classList.contains('euiAccordion-isOpen')).toBeTruthy() - expect(screen.queryAllByTestId('luaScript-accordion')[4]?.classList.contains('euiAccordion-isOpen')).toBeTruthy() - }) - - it('should render closed recommendations', () => { - (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ - ...mockdbAnalysisSelector, - data: { - recommendations: [ - { name: 'luaScript' }, - { name: 'luaScript' }, - { name: 'luaScript' }, - { name: 'luaScript' }, - { name: 'luaScript' }, - { name: 'luaScript' }, - ] - } - })) - - render() - - expect(screen.queryAllByTestId('luaScript-accordion')[0]?.classList.contains('euiAccordion-isOpen')).not.toBeTruthy() - expect(screen.queryAllByTestId('luaScript-accordion')[1]?.classList.contains('euiAccordion-isOpen')).not.toBeTruthy() - expect(screen.queryAllByTestId('luaScript-accordion')[2]?.classList.contains('euiAccordion-isOpen')).not.toBeTruthy() - expect(screen.queryAllByTestId('luaScript-accordion')[3]?.classList.contains('euiAccordion-isOpen')).not.toBeTruthy() - expect(screen.queryAllByTestId('luaScript-accordion')[4]?.classList.contains('euiAccordion-isOpen')).not.toBeTruthy() - expect(screen.queryAllByTestId('luaScript-accordion')[5]?.classList.contains('euiAccordion-isOpen')).not.toBeTruthy() - }) - }) - it('should render code changes badge in luaScript recommendation', () => { (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ ...mockdbAnalysisSelector, @@ -142,7 +92,7 @@ describe('Recommendations', () => { expect(screen.queryByTestId('configuration_changes')).toBeInTheDocument() }) - it('should render code changes badge and configuration_changes in avoidLogicalDatabases recommendation', () => { + it('should render code changes badge in avoidLogicalDatabases recommendation', () => { (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ ...mockdbAnalysisSelector, data: { @@ -157,7 +107,7 @@ describe('Recommendations', () => { expect(screen.queryByTestId('configuration_changes')).not.toBeInTheDocument() }) - it('should render code changes badge and configuration_changes in combineSmallStringsToHashes recommendation', () => { + it('should render code changes badge in combineSmallStringsToHashes recommendation', () => { (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ ...mockdbAnalysisSelector, data: { @@ -172,7 +122,7 @@ describe('Recommendations', () => { expect(screen.queryByTestId('configuration_changes')).not.toBeInTheDocument() }) - it('should render configuration_changes badge and configuration_changes in increaseSetMaxIntsetEntries recommendation', () => { + it('should render configuration_changes badge in increaseSetMaxIntsetEntries recommendation', () => { (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ ...mockdbAnalysisSelector, data: { @@ -187,7 +137,7 @@ describe('Recommendations', () => { expect(screen.queryByTestId('configuration_changes')).toBeInTheDocument() }) - it('should render code changes badge and configuration_changes in convertHashtableToZiplist recommendation', () => { + it('should render code changes badge in convertHashtableToZiplist recommendation', () => { (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ ...mockdbAnalysisSelector, data: { @@ -202,7 +152,7 @@ describe('Recommendations', () => { expect(screen.queryByTestId('configuration_changes')).toBeInTheDocument() }) - it('should render configuration_changes badge and configuration_changes in compressHashFieldNames recommendation', () => { + it('should render configuration_changes badge in compressHashFieldNames recommendation', () => { (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ ...mockdbAnalysisSelector, data: { @@ -217,6 +167,20 @@ describe('Recommendations', () => { expect(screen.queryByTestId('configuration_changes')).toBeInTheDocument() }) + it('should render configuration_changes badge in compressionForList recommendation', () => { + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: { + recommendations: [{ name: 'compressionForList' }] + } + })) + + render() + + expect(screen.queryByTestId('code_changes')).not.toBeInTheDocument() + expect(screen.queryByTestId('upgrade')).not.toBeInTheDocument() + expect(screen.queryByTestId('configuration_changes')).toBeInTheDocument() + }) it('should collapse/expand', () => { (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx index ad48814435..8baf5567d9 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx @@ -12,8 +12,6 @@ import recommendationsContent from 'uiSrc/constants/dbAnalysisRecommendations.js import { parseContent, renderBadges } from './utils' import styles from './styles.module.scss' -const countToCollapseRecommendations = 6 - const Recommendations = () => { const { data, loading } = useSelector(dbAnalysisSelector) const { recommendations = [] } = data ?? {} @@ -45,7 +43,6 @@ const Recommendations = () => { buttonClassName={styles.accordionBtn} buttonProps={{ 'data-test-subj': `${id}-button` }} className={styles.accordion} - initialIsOpen={recommendations.length < countToCollapseRecommendations} data-testId={`${id}-accordion`} > From ce466e7659ccb156aa5154e5331974a88dc1d37e Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Fri, 2 Dec 2022 17:25:13 +0400 Subject: [PATCH 039/108] #RI-3569-fix big data tests --- .../POST-databases-id-analysis.test.ts | 367 +++++++++--------- .../Recommendations.spec.tsx | 6 +- 2 files changed, 190 insertions(+), 183 deletions(-) diff --git a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts index 5f470a2fe7..a56c18d091 100644 --- a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts +++ b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts @@ -147,191 +147,198 @@ describe('POST /databases/:instanceId/analysis', () => { expect(await repository.count()).to.eq(5); } }, - { - name: 'Should create new database analysis with useSmallerKeys recommendation', - data: { - delimiter: '-', - }, - statusCode: 201, - responseSchema, - before: async () => { - const KEYS_NUMBER = 1_000_001; - await rte.data.generateNKeys(KEYS_NUMBER, false); - }, - checkFn: async ({ body }) => { - expect(body.totalKeys.total).to.gt(0); - expect(body.totalMemory.total).to.gt(0); - expect(body.topKeysNsp.length).to.gt(0); - expect(body.topMemoryNsp.length).to.gt(0); - expect(body.topKeysLength.length).to.gt(0); - expect(body.topKeysMemory.length).to.gt(0); - expect(body.recommendations).to.deep.eq([ - constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, - constants.TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION, - ]); - }, - after: async () => { - expect(await repository.count()).to.eq(5); - } - }, - { - name: 'Should create new database analysis with bigHashes recommendation', - data: { - delimiter: '-', - }, - statusCode: 201, - responseSchema, - before: async () => { - const NUMBERS_OF_HASH_FIELDS = 5001; - await rte.data.generateHugeNumberOfFieldsForHashKey(NUMBERS_OF_HASH_FIELDS, true); - }, - checkFn: async ({ body }) => { - expect(body.totalKeys.total).to.gt(0); - expect(body.totalMemory.total).to.gt(0); - expect(body.topKeysNsp.length).to.gt(0); - expect(body.topMemoryNsp.length).to.gt(0); - expect(body.topKeysLength.length).to.gt(0); - expect(body.topKeysMemory.length).to.gt(0); - expect(body.recommendations).to.deep.eq([ - constants.TEST_BIG_HASHES_DATABASE_ANALYSIS_RECOMMENDATION, - constants.TEST_CONVERT_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, - constants.TEST_COMPRESS_HASH_FIELD_NAMES_RECOMMENDATION, - ]); - }, - after: async () => { - expect(await repository.count()).to.eq(5); - } - }, - { - name: 'Should create new database analysis with increaseSetMaxIntsetEntries recommendation', - data: { - delimiter: '-', - }, - statusCode: 201, - responseSchema, - before: async () => { - const NUMBERS_OF_SET_MEMBERS = 513; - await rte.data.generateHugeNumberOfMembersForSetKey(NUMBERS_OF_SET_MEMBERS, true); - }, - checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([constants.TEST_INCREASE_SET_MAX_INTSET_ENTRIES_RECOMMENDATION]); - }, - after: async () => { - expect(await repository.count()).to.eq(5); - } - }, - { - name: 'Should create new database analysis with combineSmallStringsToHashes recommendation', - data: { - delimiter: '-', - }, - statusCode: 201, - responseSchema, - before: async () => { - await rte.data.generateStrings(true); - }, - checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([constants.TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION]); - }, - after: async () => { - expect(await repository.count()).to.eq(5); - } - }, - { - name: 'Should create new database analysis with convertHashtableToZiplist recommendation', - data: { - delimiter: '-', - }, - statusCode: 201, - responseSchema, - before: async () => { - const NUMBERS_OF_HASH_FIELDS = 513; - await rte.data.generateHugeNumberOfFieldsForHashKey(NUMBERS_OF_HASH_FIELDS, true); - }, - checkFn: async ({ body }) => { - expect(body.totalKeys.total).to.gt(0); - expect(body.totalMemory.total).to.gt(0); - expect(body.topKeysNsp.length).to.gt(0); - expect(body.topMemoryNsp.length).to.gt(0); - expect(body.topKeysLength.length).to.gt(0); - expect(body.topKeysMemory.length).to.gt(0); - expect(body.recommendations).to.deep.eq([ - constants.TEST_CONVERT_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, - ]); - }, - after: async () => { - expect(await repository.count()).to.eq(5); - } - }, - { - name: 'Should create new database analysis with compressHashFieldNames recommendation', - data: { - delimiter: '-', - }, - statusCode: 201, - responseSchema, - before: async () => { - const NUMBERS_OF_HASH_FIELDS = 1001; - await rte.data.generateHugeNumberOfFieldsForHashKey(NUMBERS_OF_HASH_FIELDS, true); - }, - checkFn: async ({ body }) => { - expect(body.totalKeys.total).to.gt(0); - expect(body.totalMemory.total).to.gt(0); - expect(body.topKeysNsp.length).to.gt(0); - expect(body.topMemoryNsp.length).to.gt(0); - expect(body.topKeysLength.length).to.gt(0); - expect(body.topKeysMemory.length).to.gt(0); - expect(body.recommendations).to.deep.eq([ - constants.TEST_CONVERT_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, - constants.TEST_COMPRESS_HASH_FIELD_NAMES_RECOMMENDATION, - ]); + ].map(mainCheckFn); + + describe('recommendations', () => { + // big data always has useSmallerKeys + requirements('!rte.bigData'); + [ + // { + // name: 'Should create new database analysis with useSmallerKeys recommendation', + // data: { + // delimiter: '-', + // }, + // statusCode: 201, + // responseSchema, + // before: async () => { + // const KEYS_NUMBER = 1_000_001; + // await rte.data.generateNKeys(KEYS_NUMBER, false); + // }, + // checkFn: async ({ body }) => { + // expect(body.totalKeys.total).to.gt(0); + // expect(body.totalMemory.total).to.gt(0); + // expect(body.topKeysNsp.length).to.gt(0); + // expect(body.topMemoryNsp.length).to.gt(0); + // expect(body.topKeysLength.length).to.gt(0); + // expect(body.topKeysMemory.length).to.gt(0); + // expect(body.recommendations).to.deep.eq([ + // constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, + // constants.TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION, + // ]); + // }, + // after: async () => { + // expect(await repository.count()).to.eq(5); + // } + // }, + { + name: 'Should create new database analysis with bigHashes recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + const NUMBERS_OF_HASH_FIELDS = 5001; + await rte.data.generateHugeNumberOfFieldsForHashKey(NUMBERS_OF_HASH_FIELDS, true); + }, + checkFn: async ({ body }) => { + expect(body.totalKeys.total).to.gt(0); + expect(body.totalMemory.total).to.gt(0); + expect(body.topKeysNsp.length).to.gt(0); + expect(body.topMemoryNsp.length).to.gt(0); + expect(body.topKeysLength.length).to.gt(0); + expect(body.topKeysMemory.length).to.gt(0); + expect(body.recommendations).to.deep.eq([ + constants.TEST_BIG_HASHES_DATABASE_ANALYSIS_RECOMMENDATION, + constants.TEST_CONVERT_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, + constants.TEST_COMPRESS_HASH_FIELD_NAMES_RECOMMENDATION, + ]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } }, - after: async () => { - expect(await repository.count()).to.eq(5); - } - }, - { - name: 'Should create new database analysis with compressionForList recommendation', - data: { - delimiter: '-', + { + name: 'Should create new database analysis with increaseSetMaxIntsetEntries recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + const NUMBERS_OF_SET_MEMBERS = 513; + await rte.data.generateHugeNumberOfMembersForSetKey(NUMBERS_OF_SET_MEMBERS, true); + }, + checkFn: async ({ body }) => { + expect(body.recommendations).to.deep.eq([constants.TEST_INCREASE_SET_MAX_INTSET_ENTRIES_RECOMMENDATION]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } }, - statusCode: 201, - responseSchema, - before: async () => { - const NUMBERS_OF_LIST_ELEMENTS = 1001; - await rte.data.generateHugeElementsForListKey(NUMBERS_OF_LIST_ELEMENTS, true); + { + name: 'Should create new database analysis with combineSmallStringsToHashes recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + await rte.data.generateStrings(true); + }, + checkFn: async ({ body }) => { + expect(body.recommendations).to.deep.eq([constants.TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } }, - checkFn: async ({ body }) => { - expect(body.totalKeys.total).to.gt(0); - expect(body.totalMemory.total).to.gt(0); - expect(body.topKeysNsp.length).to.gt(0); - expect(body.topMemoryNsp.length).to.gt(0); - expect(body.topKeysLength.length).to.gt(0); - expect(body.topKeysMemory.length).to.gt(0); - expect(body.recommendations).to.deep.eq([ - constants.TEST_COMPRESSION_FOR_LIST_RECOMMENDATION, - ]); + { + name: 'Should create new database analysis with convertHashtableToZiplist recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + const NUMBERS_OF_HASH_FIELDS = 513; + await rte.data.generateHugeNumberOfFieldsForHashKey(NUMBERS_OF_HASH_FIELDS, true); + }, + checkFn: async ({ body }) => { + expect(body.totalKeys.total).to.gt(0); + expect(body.totalMemory.total).to.gt(0); + expect(body.topKeysNsp.length).to.gt(0); + expect(body.topMemoryNsp.length).to.gt(0); + expect(body.topKeysLength.length).to.gt(0); + expect(body.topKeysMemory.length).to.gt(0); + expect(body.recommendations).to.deep.eq([ + constants.TEST_CONVERT_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, + ]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } }, - after: async () => { - expect(await repository.count()).to.eq(5); - } - }, - { - name: 'Should create new database analysis with luaScript recommendation', - data: { - delimiter: '-', + { + name: 'Should create new database analysis with compressHashFieldNames recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + const NUMBERS_OF_HASH_FIELDS = 1001; + await rte.data.generateHugeNumberOfFieldsForHashKey(NUMBERS_OF_HASH_FIELDS, true); + }, + checkFn: async ({ body }) => { + expect(body.totalKeys.total).to.gt(0); + expect(body.totalMemory.total).to.gt(0); + expect(body.topKeysNsp.length).to.gt(0); + expect(body.topMemoryNsp.length).to.gt(0); + expect(body.topKeysLength.length).to.gt(0); + expect(body.topKeysMemory.length).to.gt(0); + expect(body.recommendations).to.deep.eq([ + constants.TEST_CONVERT_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, + constants.TEST_COMPRESS_HASH_FIELD_NAMES_RECOMMENDATION, + ]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } }, - statusCode: 201, - responseSchema, - before: async () => { - await rte.data.generateNCachedScripts(11, true); + { + name: 'Should create new database analysis with compressionForList recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + const NUMBERS_OF_LIST_ELEMENTS = 1001; + await rte.data.generateHugeElementsForListKey(NUMBERS_OF_LIST_ELEMENTS, true); + }, + checkFn: async ({ body }) => { + expect(body.totalKeys.total).to.gt(0); + expect(body.totalMemory.total).to.gt(0); + expect(body.topKeysNsp.length).to.gt(0); + expect(body.topMemoryNsp.length).to.gt(0); + expect(body.topKeysLength.length).to.gt(0); + expect(body.topKeysMemory.length).to.gt(0); + expect(body.recommendations).to.deep.eq([ + constants.TEST_COMPRESSION_FOR_LIST_RECOMMENDATION, + ]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } }, - checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([constants.TEST_LUA_DATABASE_ANALYSIS_RECOMMENDATION]); + { + name: 'Should create new database analysis with luaScript recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + await rte.data.generateNCachedScripts(11, true); + }, + checkFn: async ({ body }) => { + expect(body.recommendations).to.deep.eq([constants.TEST_LUA_DATABASE_ANALYSIS_RECOMMENDATION]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } }, - after: async () => { - expect(await repository.count()).to.eq(5); - } - }, - ].map(mainCheckFn); + ].map(mainCheckFn); + }); }); diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx index c5a868c35c..926b712ae8 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx @@ -192,14 +192,14 @@ describe('Recommendations', () => { const { container } = render() - expect(screen.queryAllByTestId('luaScript-accordion')[0]?.classList.contains('euiAccordion-isOpen')).toBeTruthy() + expect(screen.queryAllByTestId('luaScript-accordion')[0]?.classList.contains('euiAccordion-isOpen')).not.toBeTruthy() fireEvent.click(container.querySelector('[data-test-subj="luaScript-button"]') as HTMLInputElement) - expect(screen.queryAllByTestId('luaScript-accordion')[0]?.classList.contains('euiAccordion-isOpen')).not.toBeTruthy() + expect(screen.queryAllByTestId('luaScript-accordion')[0]?.classList.contains('euiAccordion-isOpen')).toBeTruthy() fireEvent.click(container.querySelector('[data-test-subj="luaScript-button"]') as HTMLInputElement) - expect(screen.queryAllByTestId('luaScript-accordion')[0]?.classList.contains('euiAccordion-isOpen')).toBeTruthy() + expect(screen.queryAllByTestId('luaScript-accordion')[0]?.classList.contains('euiAccordion-isOpen')).not.toBeTruthy() }) }) From 1a7de58cfd5f37ea6c5ba4d0765ef0086b3296fa Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 5 Dec 2022 14:10:37 +0400 Subject: [PATCH 040/108] #RI-3573 - add big strings recommendation --- .../api/src/constants/recommendations.ts | 1 + .../providers/recommendation.provider.spec.ts | 17 + .../providers/recommendation.provider.ts | 21 +- .../recommendation/recommendation.service.ts | 1 + .../POST-databases-id-analysis.test.ts | 321 +++++++++++++++--- redisinsight/api/test/helpers/constants.ts | 3 + .../constants/dbAnalysisRecommendations.json | 20 ++ .../Recommendations.spec.tsx | 21 +- .../recommendations-view/Recommendations.tsx | 1 + 9 files changed, 350 insertions(+), 56 deletions(-) diff --git a/redisinsight/api/src/constants/recommendations.ts b/redisinsight/api/src/constants/recommendations.ts index 15468b0363..42b84031c4 100644 --- a/redisinsight/api/src/constants/recommendations.ts +++ b/redisinsight/api/src/constants/recommendations.ts @@ -1,6 +1,7 @@ export const RECOMMENDATION_NAMES = Object.freeze({ LUA_SCRIPT: 'luaScript', BIG_HASHES: 'bigHashes', + BIG_STRINGS: 'bigStrings', USE_SMALLER_KEYS: 'useSmallerKeys', AVOID_LOGICAL_DATABASES: 'avoidLogicalDatabases', COMBINE_SMALL_STRINGS_TO_HASHES: 'combineSmallStringsToHashes', diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts index 5fd9ec8523..e805865ee3 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts @@ -61,6 +61,10 @@ const mockBigStringKey = { name: Buffer.from('name'), type: 'string', length: 10, memory: 201, ttl: -1, }; +const mockHugeStringKey = { + name: Buffer.from('name'), type: 'string', length: 10, memory: 5_000_001, ttl: -1, +}; + const mockBigSet = { name: Buffer.from('name'), type: 'set', length: 513, memory: 10, ttl: -1, }; @@ -280,4 +284,17 @@ describe('RecommendationProvider', () => { expect(compressHashFieldNamesRecommendation).toEqual({ name: RECOMMENDATION_NAMES.COMPRESSION_FOR_LIST }); }); }); + + describe('determineBigStringsRecommendation', () => { + it('should not return bigStrings recommendation', async () => { + const bigStringsRecommendation = await service + .determineBigStringsRecommendation(mockKeys); + expect(bigStringsRecommendation).toEqual(null); + }); + it('should return bigStrings recommendation', async () => { + const bigStringsRecommendation = await service + .determineBigStringsRecommendation([mockHugeStringKey]); + expect(bigStringsRecommendation).toEqual({ name: RECOMMENDATION_NAMES.BIG_STRINGS }); + }); + }); }); diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index ff7b5d1d45..9626313fa1 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -13,6 +13,7 @@ const maxStringMemory = 200; const maxDatabaseTotal = 1_000_000; const maxCompressHashLength = 1000; const maxListLength = 1000; +const bigStringMemory = 5_000_000; @Injectable() export class RecommendationProvider { @@ -188,11 +189,27 @@ export class RecommendationProvider { keys: Key[], ): Promise { try { - const bugList = keys.some((key) => key.type === RedisDataType.List && key.length > maxListLength); - return bugList ? { name: RECOMMENDATION_NAMES.COMPRESSION_FOR_LIST } : null; + const bigList = keys.some((key) => key.type === RedisDataType.List && key.length > maxListLength); + return bigList ? { name: RECOMMENDATION_NAMES.COMPRESSION_FOR_LIST } : null; } catch (err) { this.logger.error('Can not determine Compression for list recommendation', err); return null; } } + + /** + * Check big strings recommendation + * @param keys + */ + async determineBigStringsRecommendation( + keys: Key[], + ): Promise { + try { + const bigString = keys.some((key) => key.type === RedisDataType.String && key.memory > bigStringMemory); + return bigString ? { name: RECOMMENDATION_NAMES.BIG_STRINGS } : null; + } catch (err) { + this.logger.error('Can not determine Big strings recommendation', err); + return null; + } + } } diff --git a/redisinsight/api/src/modules/recommendation/recommendation.service.ts b/redisinsight/api/src/modules/recommendation/recommendation.service.ts index 3a405f012a..23f1e6bf8b 100644 --- a/redisinsight/api/src/modules/recommendation/recommendation.service.ts +++ b/redisinsight/api/src/modules/recommendation/recommendation.service.ts @@ -44,6 +44,7 @@ export class RecommendationService { await this.recommendationProvider.determineConvertHashtableToZiplistRecommendation(client, keys), await this.recommendationProvider.determineCompressHashFieldNamesRecommendation(keys), await this.recommendationProvider.determineCompressionForListRecommendation(keys), + await this.recommendationProvider.determineBigStringsRecommendation(keys), ])); } } diff --git a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts index a56c18d091..490923ee27 100644 --- a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts +++ b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts @@ -153,33 +153,6 @@ describe('POST /databases/:instanceId/analysis', () => { // big data always has useSmallerKeys requirements('!rte.bigData'); [ - // { - // name: 'Should create new database analysis with useSmallerKeys recommendation', - // data: { - // delimiter: '-', - // }, - // statusCode: 201, - // responseSchema, - // before: async () => { - // const KEYS_NUMBER = 1_000_001; - // await rte.data.generateNKeys(KEYS_NUMBER, false); - // }, - // checkFn: async ({ body }) => { - // expect(body.totalKeys.total).to.gt(0); - // expect(body.totalMemory.total).to.gt(0); - // expect(body.topKeysNsp.length).to.gt(0); - // expect(body.topMemoryNsp.length).to.gt(0); - // expect(body.topKeysLength.length).to.gt(0); - // expect(body.topKeysMemory.length).to.gt(0); - // expect(body.recommendations).to.deep.eq([ - // constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, - // constants.TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION, - // ]); - // }, - // after: async () => { - // expect(await repository.count()).to.eq(5); - // } - // }, { name: 'Should create new database analysis with bigHashes recommendation', data: { @@ -192,12 +165,6 @@ describe('POST /databases/:instanceId/analysis', () => { await rte.data.generateHugeNumberOfFieldsForHashKey(NUMBERS_OF_HASH_FIELDS, true); }, checkFn: async ({ body }) => { - expect(body.totalKeys.total).to.gt(0); - expect(body.totalMemory.total).to.gt(0); - expect(body.topKeysNsp.length).to.gt(0); - expect(body.topMemoryNsp.length).to.gt(0); - expect(body.topKeysLength.length).to.gt(0); - expect(body.topKeysMemory.length).to.gt(0); expect(body.recommendations).to.deep.eq([ constants.TEST_BIG_HASHES_DATABASE_ANALYSIS_RECOMMENDATION, constants.TEST_CONVERT_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, @@ -255,12 +222,6 @@ describe('POST /databases/:instanceId/analysis', () => { await rte.data.generateHugeNumberOfFieldsForHashKey(NUMBERS_OF_HASH_FIELDS, true); }, checkFn: async ({ body }) => { - expect(body.totalKeys.total).to.gt(0); - expect(body.totalMemory.total).to.gt(0); - expect(body.topKeysNsp.length).to.gt(0); - expect(body.topMemoryNsp.length).to.gt(0); - expect(body.topKeysLength.length).to.gt(0); - expect(body.topKeysMemory.length).to.gt(0); expect(body.recommendations).to.deep.eq([ constants.TEST_CONVERT_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, ]); @@ -281,12 +242,6 @@ describe('POST /databases/:instanceId/analysis', () => { await rte.data.generateHugeNumberOfFieldsForHashKey(NUMBERS_OF_HASH_FIELDS, true); }, checkFn: async ({ body }) => { - expect(body.totalKeys.total).to.gt(0); - expect(body.totalMemory.total).to.gt(0); - expect(body.topKeysNsp.length).to.gt(0); - expect(body.topMemoryNsp.length).to.gt(0); - expect(body.topKeysLength.length).to.gt(0); - expect(body.topKeysMemory.length).to.gt(0); expect(body.recommendations).to.deep.eq([ constants.TEST_CONVERT_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, constants.TEST_COMPRESS_HASH_FIELD_NAMES_RECOMMENDATION, @@ -308,16 +263,31 @@ describe('POST /databases/:instanceId/analysis', () => { await rte.data.generateHugeElementsForListKey(NUMBERS_OF_LIST_ELEMENTS, true); }, checkFn: async ({ body }) => { - expect(body.totalKeys.total).to.gt(0); - expect(body.totalMemory.total).to.gt(0); - expect(body.topKeysNsp.length).to.gt(0); - expect(body.topMemoryNsp.length).to.gt(0); - expect(body.topKeysLength.length).to.gt(0); - expect(body.topKeysMemory.length).to.gt(0); expect(body.recommendations).to.deep.eq([ constants.TEST_COMPRESSION_FOR_LIST_RECOMMENDATION, ]); }, + after: async () => { + await rte.data.truncate(); + expect(await repository.count()).to.eq(5); + } + }, + { + name: 'Should create new database analysis with bigStrings recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + const BIG_STRING_MEMORY = 5_000_001; + const bigStringValue = Buffer.alloc(BIG_STRING_MEMORY, 'a').toString(); + + await rte.data.sendCommand('set', [constants.TEST_STRING_KEY_1, bigStringValue]); + }, + checkFn: async ({ body }) => { + expect(body.recommendations).to.deep.eq([constants.TEST_BIG_STRINGS_RECOMMENDATION]); + }, after: async () => { expect(await repository.count()).to.eq(5); } @@ -335,10 +305,259 @@ describe('POST /databases/:instanceId/analysis', () => { checkFn: async ({ body }) => { expect(body.recommendations).to.deep.eq([constants.TEST_LUA_DATABASE_ANALYSIS_RECOMMENDATION]); }, + after: async () => { + await rte.data.sendCommand('script', ['flush']); + expect(await repository.count()).to.eq(5); + } + }, + { + name: 'Should create new database analysis with useSmallerKeys recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + const KEYS_NUMBER = 1_000_001; + await rte.data.generateNKeys(KEYS_NUMBER, false); + }, + checkFn: async ({ body }) => { + expect(body.recommendations).to.deep.eq([ + constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, + constants.TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION, + ]); + }, + after: async () => { + await rte.data.truncate(); + expect(await repository.count()).to.eq(5); + } + }, + ].map(mainCheckFn); + }); + + describe('recommendations', () => { + // big data always has useSmallerKeys recommendation + requirements('rte.bigData'); + [ + { + name: 'Should create new database analysis with bigHashes recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + const NUMBERS_OF_HASH_FIELDS = 5001; + await rte.data.generateHugeNumberOfFieldsForHashKey(NUMBERS_OF_HASH_FIELDS, true); + }, + checkFn: async ({ body }) => { + expect(body.recommendations).to.deep.eq([ + constants.TEST_BIG_HASHES_DATABASE_ANALYSIS_RECOMMENDATION, + constants.TEST_CONVERT_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, + constants.TEST_COMPRESS_HASH_FIELD_NAMES_RECOMMENDATION, + constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, + ]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } + }, + { + name: 'Should create new database analysis with increaseSetMaxIntsetEntries recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + const NUMBERS_OF_SET_MEMBERS = 513; + await rte.data.generateHugeNumberOfMembersForSetKey(NUMBERS_OF_SET_MEMBERS, true); + }, + checkFn: async ({ body }) => { + expect(body.recommendations).to.deep.eq([ + constants.TEST_INCREASE_SET_MAX_INTSET_ENTRIES_RECOMMENDATION, + constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, + ]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } + }, + { + name: 'Should create new database analysis with combineSmallStringsToHashes recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + await rte.data.generateStrings(true); + }, + checkFn: async ({ body }) => { + expect(body.recommendations).to.deep.eq([ + constants.TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION, + constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, + ]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } + }, + { + name: 'Should create new database analysis with convertHashtableToZiplist recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + const NUMBERS_OF_HASH_FIELDS = 513; + await rte.data.generateHugeNumberOfFieldsForHashKey(NUMBERS_OF_HASH_FIELDS, true); + }, + checkFn: async ({ body }) => { + expect(body.recommendations).to.deep.eq([ + constants.TEST_CONVERT_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, + constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, + ]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } + }, + { + name: 'Should create new database analysis with compressHashFieldNames recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + const NUMBERS_OF_HASH_FIELDS = 1001; + await rte.data.generateHugeNumberOfFieldsForHashKey(NUMBERS_OF_HASH_FIELDS, true); + }, + checkFn: async ({ body }) => { + expect(body.recommendations).to.deep.eq([ + constants.TEST_CONVERT_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, + constants.TEST_COMPRESS_HASH_FIELD_NAMES_RECOMMENDATION, + constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, + ]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } + }, + { + name: 'Should create new database analysis with compressionForList recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + const NUMBERS_OF_LIST_ELEMENTS = 1001; + await rte.data.generateHugeElementsForListKey(NUMBERS_OF_LIST_ELEMENTS, true); + }, + checkFn: async ({ body }) => { + expect(body.recommendations).to.deep.eq([ + constants.TEST_COMPRESSION_FOR_LIST_RECOMMENDATION, + constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, + ]); + }, + after: async () => { + await rte.data.truncate(); + expect(await repository.count()).to.eq(5); + } + }, + { + name: 'Should create new database analysis with bigStrings recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + const BIG_STRING_MEMORY = 5_000_001; + const bigStringValue = Buffer.alloc(BIG_STRING_MEMORY, 'a').toString(); + + await rte.data.sendCommand('set', [constants.TEST_STRING_KEY_1, bigStringValue]); + }, + checkFn: async ({ body }) => { + expect(body.recommendations).to.deep.eq([ + constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, + constants.TEST_BIG_STRINGS_RECOMMENDATION, + ]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } + }, + { + name: 'Should create new database analysis with luaScript recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + await rte.data.generateNCachedScripts(11, true); + }, + checkFn: async ({ body }) => { + expect(body.recommendations).to.deep.eq([ + constants.TEST_LUA_DATABASE_ANALYSIS_RECOMMENDATION, + constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, + ]); + }, + after: async () => { + await rte.data.sendCommand('script', ['flush']); + expect(await repository.count()).to.eq(5); + } + }, + { + name: 'Should create new database analysis with useSmallerKeys recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + checkFn: async ({ body }) => { + expect(body.recommendations).to.deep.eq([ + constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, + constants.TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION, + ]); + }, after: async () => { expect(await repository.count()).to.eq(5); } }, ].map(mainCheckFn); }); + + // TODO + // describe('useSmallerKeys recommendation', () => { + // // generate 1M keys in cloud take a lot of time + // requirements('!rte.re'); + // [ + // { + // name: 'Should create new database analysis with useSmallerKeys recommendation', + // data: { + // delimiter: '-', + // }, + // statusCode: 201, + // responseSchema, + // before: async () => { + // const KEYS_NUMBER = 1_000_001; + // await rte.data.generateNKeys(KEYS_NUMBER, false); + // }, + // checkFn: async ({ body }) => { + // expect(body.recommendations).to.deep.eq([ + // constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, + // constants.TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION, + // ]); + // }, + // after: async () => { + // expect(await repository.count()).to.eq(5); + // } + // }, + // ].map(mainCheckFn) + // }); }); diff --git a/redisinsight/api/test/helpers/constants.ts b/redisinsight/api/test/helpers/constants.ts index 71b835694b..f2368f5a74 100644 --- a/redisinsight/api/test/helpers/constants.ts +++ b/redisinsight/api/test/helpers/constants.ts @@ -476,6 +476,9 @@ export const constants = { TEST_COMPRESSION_FOR_LIST_RECOMMENDATION: { name: RECOMMENDATION_NAMES.COMPRESSION_FOR_LIST, }, + TEST_BIG_STRINGS_RECOMMENDATION: { + name: RECOMMENDATION_NAMES.BIG_STRINGS, + }, // etc... } diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index ad033fbf39..b42008ec2e 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -228,5 +228,25 @@ } ], "badges": ["configuration_changes"] + }, + "bigStrings": { + "id": "bigStrings", + "title": "Do not store large strings", + "content": [ + { + "id": "1", + "type": "span", + "value": "Avoid storing large strings, since transferring them takes time and consumes the network bandwidth. Large keys are acceptable only to read/write portions of the string. " + }, + { + "id": "2", + "type": "link", + "value": { + "href": "https://docs.redis.com/latest/ri/memory-optimizations/", + "name": "Read more" + } + } + ], + "badges": ["configuration_changes"] } } diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx index 926b712ae8..48562884de 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx @@ -182,6 +182,21 @@ describe('Recommendations', () => { expect(screen.queryByTestId('configuration_changes')).toBeInTheDocument() }) + it('should render configuration_changes badge in bigStrings recommendation', () => { + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: { + recommendations: [{ name: 'bigStrings' }] + } + })) + + render() + + expect(screen.queryByTestId('code_changes')).not.toBeInTheDocument() + expect(screen.queryByTestId('upgrade')).not.toBeInTheDocument() + expect(screen.queryByTestId('configuration_changes')).toBeInTheDocument() + }) + it('should collapse/expand', () => { (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ ...mockdbAnalysisSelector, @@ -192,14 +207,14 @@ describe('Recommendations', () => { const { container } = render() - expect(screen.queryAllByTestId('luaScript-accordion')[0]?.classList.contains('euiAccordion-isOpen')).not.toBeTruthy() + expect(screen.queryAllByTestId('luaScript-accordion')[0]?.classList.contains('euiAccordion-isOpen')).toBeTruthy() fireEvent.click(container.querySelector('[data-test-subj="luaScript-button"]') as HTMLInputElement) - expect(screen.queryAllByTestId('luaScript-accordion')[0]?.classList.contains('euiAccordion-isOpen')).toBeTruthy() + expect(screen.queryAllByTestId('luaScript-accordion')[0]?.classList.contains('euiAccordion-isOpen')).not.toBeTruthy() fireEvent.click(container.querySelector('[data-test-subj="luaScript-button"]') as HTMLInputElement) - expect(screen.queryAllByTestId('luaScript-accordion')[0]?.classList.contains('euiAccordion-isOpen')).not.toBeTruthy() + expect(screen.queryAllByTestId('luaScript-accordion')[0]?.classList.contains('euiAccordion-isOpen')).toBeTruthy() }) }) diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx index 8baf5567d9..4250327689 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx @@ -43,6 +43,7 @@ const Recommendations = () => { buttonClassName={styles.accordionBtn} buttonProps={{ 'data-test-subj': `${id}-button` }} className={styles.accordion} + initialIsOpen data-testId={`${id}-accordion`} > From e7bc9fd2097d3a1f27d9ed8f5206f3f60bae1dda Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 5 Dec 2022 15:23:47 +0400 Subject: [PATCH 041/108] #RI-3569 - fix IT test --- .../POST-databases-id-analysis.test.ts | 240 +----------------- 1 file changed, 7 insertions(+), 233 deletions(-) diff --git a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts index 490923ee27..6060fd92d3 100644 --- a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts +++ b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts @@ -150,8 +150,10 @@ describe('POST /databases/:instanceId/analysis', () => { ].map(mainCheckFn); describe('recommendations', () => { - // big data always has useSmallerKeys - requirements('!rte.bigData'); + beforeEach(async () => { + await rte.data.truncate() + }); + [ { name: 'Should create new database analysis with bigHashes recommendation', @@ -251,65 +253,6 @@ describe('POST /databases/:instanceId/analysis', () => { expect(await repository.count()).to.eq(5); } }, - { - name: 'Should create new database analysis with compressionForList recommendation', - data: { - delimiter: '-', - }, - statusCode: 201, - responseSchema, - before: async () => { - const NUMBERS_OF_LIST_ELEMENTS = 1001; - await rte.data.generateHugeElementsForListKey(NUMBERS_OF_LIST_ELEMENTS, true); - }, - checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([ - constants.TEST_COMPRESSION_FOR_LIST_RECOMMENDATION, - ]); - }, - after: async () => { - await rte.data.truncate(); - expect(await repository.count()).to.eq(5); - } - }, - { - name: 'Should create new database analysis with bigStrings recommendation', - data: { - delimiter: '-', - }, - statusCode: 201, - responseSchema, - before: async () => { - const BIG_STRING_MEMORY = 5_000_001; - const bigStringValue = Buffer.alloc(BIG_STRING_MEMORY, 'a').toString(); - - await rte.data.sendCommand('set', [constants.TEST_STRING_KEY_1, bigStringValue]); - }, - checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([constants.TEST_BIG_STRINGS_RECOMMENDATION]); - }, - after: async () => { - expect(await repository.count()).to.eq(5); - } - }, - { - name: 'Should create new database analysis with luaScript recommendation', - data: { - delimiter: '-', - }, - statusCode: 201, - responseSchema, - before: async () => { - await rte.data.generateNCachedScripts(11, true); - }, - checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([constants.TEST_LUA_DATABASE_ANALYSIS_RECOMMENDATION]); - }, - after: async () => { - await rte.data.sendCommand('script', ['flush']); - expect(await repository.count()).to.eq(5); - } - }, { name: 'Should create new database analysis with useSmallerKeys recommendation', data: { @@ -318,7 +261,7 @@ describe('POST /databases/:instanceId/analysis', () => { statusCode: 201, responseSchema, before: async () => { - const KEYS_NUMBER = 1_000_001; + const KEYS_NUMBER = 1_000_006; await rte.data.generateNKeys(KEYS_NUMBER, false); }, checkFn: async ({ body }) => { @@ -327,121 +270,6 @@ describe('POST /databases/:instanceId/analysis', () => { constants.TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION, ]); }, - after: async () => { - await rte.data.truncate(); - expect(await repository.count()).to.eq(5); - } - }, - ].map(mainCheckFn); - }); - - describe('recommendations', () => { - // big data always has useSmallerKeys recommendation - requirements('rte.bigData'); - [ - { - name: 'Should create new database analysis with bigHashes recommendation', - data: { - delimiter: '-', - }, - statusCode: 201, - responseSchema, - before: async () => { - const NUMBERS_OF_HASH_FIELDS = 5001; - await rte.data.generateHugeNumberOfFieldsForHashKey(NUMBERS_OF_HASH_FIELDS, true); - }, - checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([ - constants.TEST_BIG_HASHES_DATABASE_ANALYSIS_RECOMMENDATION, - constants.TEST_CONVERT_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, - constants.TEST_COMPRESS_HASH_FIELD_NAMES_RECOMMENDATION, - constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, - ]); - }, - after: async () => { - expect(await repository.count()).to.eq(5); - } - }, - { - name: 'Should create new database analysis with increaseSetMaxIntsetEntries recommendation', - data: { - delimiter: '-', - }, - statusCode: 201, - responseSchema, - before: async () => { - const NUMBERS_OF_SET_MEMBERS = 513; - await rte.data.generateHugeNumberOfMembersForSetKey(NUMBERS_OF_SET_MEMBERS, true); - }, - checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([ - constants.TEST_INCREASE_SET_MAX_INTSET_ENTRIES_RECOMMENDATION, - constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, - ]); - }, - after: async () => { - expect(await repository.count()).to.eq(5); - } - }, - { - name: 'Should create new database analysis with combineSmallStringsToHashes recommendation', - data: { - delimiter: '-', - }, - statusCode: 201, - responseSchema, - before: async () => { - await rte.data.generateStrings(true); - }, - checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([ - constants.TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION, - constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, - ]); - }, - after: async () => { - expect(await repository.count()).to.eq(5); - } - }, - { - name: 'Should create new database analysis with convertHashtableToZiplist recommendation', - data: { - delimiter: '-', - }, - statusCode: 201, - responseSchema, - before: async () => { - const NUMBERS_OF_HASH_FIELDS = 513; - await rte.data.generateHugeNumberOfFieldsForHashKey(NUMBERS_OF_HASH_FIELDS, true); - }, - checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([ - constants.TEST_CONVERT_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, - constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, - ]); - }, - after: async () => { - expect(await repository.count()).to.eq(5); - } - }, - { - name: 'Should create new database analysis with compressHashFieldNames recommendation', - data: { - delimiter: '-', - }, - statusCode: 201, - responseSchema, - before: async () => { - const NUMBERS_OF_HASH_FIELDS = 1001; - await rte.data.generateHugeNumberOfFieldsForHashKey(NUMBERS_OF_HASH_FIELDS, true); - }, - checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([ - constants.TEST_CONVERT_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, - constants.TEST_COMPRESS_HASH_FIELD_NAMES_RECOMMENDATION, - constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, - ]); - }, after: async () => { expect(await repository.count()).to.eq(5); } @@ -460,11 +288,9 @@ describe('POST /databases/:instanceId/analysis', () => { checkFn: async ({ body }) => { expect(body.recommendations).to.deep.eq([ constants.TEST_COMPRESSION_FOR_LIST_RECOMMENDATION, - constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, ]); }, after: async () => { - await rte.data.truncate(); expect(await repository.count()).to.eq(5); } }, @@ -482,10 +308,7 @@ describe('POST /databases/:instanceId/analysis', () => { await rte.data.sendCommand('set', [constants.TEST_STRING_KEY_1, bigStringValue]); }, checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([ - constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, - constants.TEST_BIG_STRINGS_RECOMMENDATION, - ]); + expect(body.recommendations).to.deep.eq([constants.TEST_BIG_STRINGS_RECOMMENDATION]); }, after: async () => { expect(await repository.count()).to.eq(5); @@ -502,62 +325,13 @@ describe('POST /databases/:instanceId/analysis', () => { await rte.data.generateNCachedScripts(11, true); }, checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([ - constants.TEST_LUA_DATABASE_ANALYSIS_RECOMMENDATION, - constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, - ]); + expect(body.recommendations).to.deep.eq([constants.TEST_LUA_DATABASE_ANALYSIS_RECOMMENDATION]); }, after: async () => { await rte.data.sendCommand('script', ['flush']); expect(await repository.count()).to.eq(5); } }, - { - name: 'Should create new database analysis with useSmallerKeys recommendation', - data: { - delimiter: '-', - }, - statusCode: 201, - responseSchema, - checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([ - constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, - constants.TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION, - ]); - }, - after: async () => { - expect(await repository.count()).to.eq(5); - } - }, ].map(mainCheckFn); }); - - // TODO - // describe('useSmallerKeys recommendation', () => { - // // generate 1M keys in cloud take a lot of time - // requirements('!rte.re'); - // [ - // { - // name: 'Should create new database analysis with useSmallerKeys recommendation', - // data: { - // delimiter: '-', - // }, - // statusCode: 201, - // responseSchema, - // before: async () => { - // const KEYS_NUMBER = 1_000_001; - // await rte.data.generateNKeys(KEYS_NUMBER, false); - // }, - // checkFn: async ({ body }) => { - // expect(body.recommendations).to.deep.eq([ - // constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, - // constants.TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION, - // ]); - // }, - // after: async () => { - // expect(await repository.count()).to.eq(5); - // } - // }, - // ].map(mainCheckFn) - // }); }); From 6052cc688be057e77a59fe741ba03d5eb6dfe6c7 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 5 Dec 2022 16:00:33 +0400 Subject: [PATCH 042/108] #RI-3569 - fix big data test --- .../database-analysis/POST-databases-id-analysis.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts index 6060fd92d3..a0a68d2399 100644 --- a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts +++ b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts @@ -261,13 +261,13 @@ describe('POST /databases/:instanceId/analysis', () => { statusCode: 201, responseSchema, before: async () => { - const KEYS_NUMBER = 1_000_006; - await rte.data.generateNKeys(KEYS_NUMBER, false); + const KEYS_NUMBER = 1_000_001; + // to avoid another recommendations + await rte.data.generateNGraphs(KEYS_NUMBER, false); }, checkFn: async ({ body }) => { expect(body.recommendations).to.deep.eq([ constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, - constants.TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION, ]); }, after: async () => { From 796b418f30c39e00ba8a218bfb8ce2eecef0b605 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 5 Dec 2022 16:12:10 +0400 Subject: [PATCH 043/108] #RI-3569 - update test --- redisinsight/api/test/api/.mocharc.yml | 2 +- .../api/database-analysis/POST-databases-id-analysis.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/redisinsight/api/test/api/.mocharc.yml b/redisinsight/api/test/api/.mocharc.yml index 6185c06241..52bb8bfd9b 100644 --- a/redisinsight/api/test/api/.mocharc.yml +++ b/redisinsight/api/test/api/.mocharc.yml @@ -1,5 +1,5 @@ spec: - - 'test/**/*.test.ts' + - 'test/api/database-analysis/**/*.test.ts' require: 'test/api/api.deps.init.ts' timeout: 60000 exit: true diff --git a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts index a0a68d2399..b2bb4b48a3 100644 --- a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts +++ b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts @@ -262,12 +262,12 @@ describe('POST /databases/:instanceId/analysis', () => { responseSchema, before: async () => { const KEYS_NUMBER = 1_000_001; - // to avoid another recommendations - await rte.data.generateNGraphs(KEYS_NUMBER, false); + await rte.data.generateHugeNumberOfTinyStringKeys(KEYS_NUMBER, false); }, checkFn: async ({ body }) => { expect(body.recommendations).to.deep.eq([ constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, + constants.TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION, ]); }, after: async () => { From c47949360e2a829f2d8853c6a807ce9701383388 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 5 Dec 2022 16:29:36 +0400 Subject: [PATCH 044/108] #RI-3569 - fix cloud IT --- redisinsight/api/test/api/.mocharc.yml | 2 +- .../POST-databases-id-analysis.test.ts | 41 +++++++++++++++++-- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/redisinsight/api/test/api/.mocharc.yml b/redisinsight/api/test/api/.mocharc.yml index 52bb8bfd9b..6185c06241 100644 --- a/redisinsight/api/test/api/.mocharc.yml +++ b/redisinsight/api/test/api/.mocharc.yml @@ -1,5 +1,5 @@ spec: - - 'test/api/database-analysis/**/*.test.ts' + - 'test/**/*.test.ts' require: 'test/api/api.deps.init.ts' timeout: 60000 exit: true diff --git a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts index b2bb4b48a3..d61e696663 100644 --- a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts +++ b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts @@ -150,8 +150,10 @@ describe('POST /databases/:instanceId/analysis', () => { ].map(mainCheckFn); describe('recommendations', () => { + requirements('!rte.bigData'); + beforeEach(async () => { - await rte.data.truncate() + await rte.data.truncate(); }); [ @@ -261,8 +263,8 @@ describe('POST /databases/:instanceId/analysis', () => { statusCode: 201, responseSchema, before: async () => { - const KEYS_NUMBER = 1_000_001; - await rte.data.generateHugeNumberOfTinyStringKeys(KEYS_NUMBER, false); + const KEYS_NUMBER = 1_000_006; + await rte.data.generateNKeys(KEYS_NUMBER, false); }, checkFn: async ({ body }) => { expect(body.recommendations).to.deep.eq([ @@ -333,5 +335,38 @@ describe('POST /databases/:instanceId/analysis', () => { } }, ].map(mainCheckFn); + + describe('useSmallerKeys recommendation', () => { + // generate 1M keys take a lot of time + requirements('!rte.cloud'); + + beforeEach(async () => { + await rte.data.truncate(); + }); + + [ + { + name: 'Should create new database analysis with useSmallerKeys recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + const KEYS_NUMBER = 1_000_006; + await rte.data.generateNKeys(KEYS_NUMBER, false); + }, + checkFn: async ({ body }) => { + expect(body.recommendations).to.deep.eq([ + constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, + constants.TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION, + ]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } + }, + ].map(mainCheckFn); + }); }); }); From 06eb860b5b62062858753a314df41966c4824a14 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 5 Dec 2022 16:50:21 +0400 Subject: [PATCH 045/108] #RI-3569 - fix cloud IT --- redisinsight/api/test/api/.mocharc.yml | 2 +- .../POST-databases-id-analysis.test.ts | 28 ++----------------- 2 files changed, 3 insertions(+), 27 deletions(-) diff --git a/redisinsight/api/test/api/.mocharc.yml b/redisinsight/api/test/api/.mocharc.yml index 6185c06241..52bb8bfd9b 100644 --- a/redisinsight/api/test/api/.mocharc.yml +++ b/redisinsight/api/test/api/.mocharc.yml @@ -1,5 +1,5 @@ spec: - - 'test/**/*.test.ts' + - 'test/api/database-analysis/**/*.test.ts' require: 'test/api/api.deps.init.ts' timeout: 60000 exit: true diff --git a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts index d61e696663..bef463a5a4 100644 --- a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts +++ b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts @@ -255,27 +255,6 @@ describe('POST /databases/:instanceId/analysis', () => { expect(await repository.count()).to.eq(5); } }, - { - name: 'Should create new database analysis with useSmallerKeys recommendation', - data: { - delimiter: '-', - }, - statusCode: 201, - responseSchema, - before: async () => { - const KEYS_NUMBER = 1_000_006; - await rte.data.generateNKeys(KEYS_NUMBER, false); - }, - checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([ - constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, - constants.TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION, - ]); - }, - after: async () => { - expect(await repository.count()).to.eq(5); - } - }, { name: 'Should create new database analysis with compressionForList recommendation', data: { @@ -338,11 +317,7 @@ describe('POST /databases/:instanceId/analysis', () => { describe('useSmallerKeys recommendation', () => { // generate 1M keys take a lot of time - requirements('!rte.cloud'); - - beforeEach(async () => { - await rte.data.truncate(); - }); + requirements('!rte.type=CLUSTER'); [ { @@ -353,6 +328,7 @@ describe('POST /databases/:instanceId/analysis', () => { statusCode: 201, responseSchema, before: async () => { + await rte.data.truncate(); const KEYS_NUMBER = 1_000_006; await rte.data.generateNKeys(KEYS_NUMBER, false); }, From 38b47c4d646b8aed0eb92281e28a9d9425c1bdeb Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 5 Dec 2022 17:01:52 +0400 Subject: [PATCH 046/108] #RI-3569 - remove unused code --- redisinsight/api/test/api/.mocharc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisinsight/api/test/api/.mocharc.yml b/redisinsight/api/test/api/.mocharc.yml index 52bb8bfd9b..6185c06241 100644 --- a/redisinsight/api/test/api/.mocharc.yml +++ b/redisinsight/api/test/api/.mocharc.yml @@ -1,5 +1,5 @@ spec: - - 'test/api/database-analysis/**/*.test.ts' + - 'test/**/*.test.ts' require: 'test/api/api.deps.init.ts' timeout: 60000 exit: true From 79ed366d073e767d6563d209d5cf529c696997fc Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Tue, 6 Dec 2022 18:35:22 +0100 Subject: [PATCH 047/108] updates for recommendations --- .../e2e/pageObjects/memory-efficiency-page.ts | 4 + .../memory-efficiency/recommendations.e2e.ts | 143 +++--------------- 2 files changed, 24 insertions(+), 123 deletions(-) diff --git a/tests/e2e/pageObjects/memory-efficiency-page.ts b/tests/e2e/pageObjects/memory-efficiency-page.ts index 9b544d40ff..376172ca8c 100644 --- a/tests/e2e/pageObjects/memory-efficiency-page.ts +++ b/tests/e2e/pageObjects/memory-efficiency-page.ts @@ -7,6 +7,9 @@ export class MemoryEfficiencyPage { //*Target any element/component via data-id, if possible! //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.). //------------------------------------------------------------------------------------------- + // CSS Selectors + cssCodeChangesLabel = '[data-testid=code_changes]'; + cssConfigurationChangesLabel = '[data-testid=configuration_changes]'; // BUTTONS newReportBtn = Selector('[data-testid=start-database-analysis-btn]'); expandArrowBtn = Selector('[data-testid^=expand-arrow-]'); @@ -17,6 +20,7 @@ export class MemoryEfficiencyPage { sortByLength = Selector('[data-testid=btn-change-table-keys]'); recommendationsTab = Selector('[data-testid=Recommendations-tab]'); luaScriptButton = Selector('[data-test-subj=luaScript-button]'); + useSmallKeysButton = Selector('[data-test-subj=useSmallerKeys-button]'); // ICONS reportTooltipIcon = Selector('[data-testid=db-new-reports-icon]'); // TEXT ELEMENTS diff --git a/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts b/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts index 3957b26d5e..9ea4f2c7cd 100644 --- a/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts +++ b/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts @@ -5,7 +5,6 @@ import { commonUrl, ossStandaloneBigConfig, ossStandaloneConfig } from '../../.. import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; import { CliActions } from '../../../common-actions/cli-actions'; import { Common } from '../../../helpers/common'; -import { populateHashWithFields, populateSetWithMembers } from '../../../helpers/keys'; const memoryEfficiencyPage = new MemoryEfficiencyPage(); const myRedisDatabasePage = new MyRedisDatabasePage(); @@ -19,7 +18,6 @@ const externalPageLink = 'https://docs.redis.com/latest/ri/memory-optimizations/ let keyName = `recomKey-${common.generateWord(10)}`; const stringKeyName = `smallStringKey-${common.generateWord(5)}`; const index = '1'; -const dbParameters = { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port }; fixture `Memory Efficiency Recommendations` .meta({ type: 'critical_path', rte: rte.standalone }) @@ -40,34 +38,39 @@ test await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); // Go to Analysis Tools page await t.click(myRedisDatabasePage.analysisPageButton); + // Add cached scripts and generate new report + await cliActions.addCachedScripts(11); await t.click(memoryEfficiencyPage.newReportBtn); + // Go to Recommendations tab + await t.click(memoryEfficiencyPage.recommendationsTab); }) .after(async() => { await cliPage.sendCommandInCli('SCRIPT FLUSH'); await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); - })('Avoid dynamic Lua script recommendation', async t => { - // Go to Recommendations tab - await t.click(memoryEfficiencyPage.recommendationsTab); - // Add cached scripts and generate new report - await cliActions.addCachedScripts(10); - await t.click(memoryEfficiencyPage.newReportBtn); - // No recommendation with 10 cached scripts - await t.expect(memoryEfficiencyPage.luaScriptAccordion.exists).notOk('Avoid dynamic lua script recommendation displayed with 10 cached scripts'); - // Add the last cached script to see the recommendation - await cliActions.addCachedScripts(1); + })('Recommendations displaying', async t => { + const luaScriptCodeChangesLabel = memoryEfficiencyPage.luaScriptAccordion.parent().find(memoryEfficiencyPage.cssCodeChangesLabel); + const luaScriptConfigurationChangesLabel = memoryEfficiencyPage.luaScriptAccordion.parent().find(memoryEfficiencyPage.cssConfigurationChangesLabel); + await t.click(memoryEfficiencyPage.newReportBtn); // Verify that user can see Avoid dynamic Lua script recommendation when number_of_cached_scripts> 10 await t.expect(memoryEfficiencyPage.luaScriptAccordion.exists).ok('Avoid dynamic lua script recommendation not displayed'); + // Verify that user can see type of recommendation badge + await t.expect(luaScriptCodeChangesLabel.exists).ok('Avoid dynamic lua script recommendation not have Code Changes label'); + await t.expect(luaScriptConfigurationChangesLabel.exists).notOk('Avoid dynamic lua script recommendation have Configuration Changes label'); // Verify that user can see Use smaller keys recommendation when database has 1M+ keys await t.expect(memoryEfficiencyPage.useSmallKeysAccordion.exists).ok('Use smaller keys recommendation not displayed'); + // Verify that user can see all the recommendations expanded by default + await t.expect(memoryEfficiencyPage.luaScriptButton.getAttribute('aria-expanded')).eql('true', 'Avoid dynamic lua script recommendation not expanded'); + await t.expect(memoryEfficiencyPage.useSmallKeysButton.getAttribute('aria-expanded')).eql('true', 'Use smaller keys recommendation not expanded'); + // Verify that user can expand/collapse recommendation - const expandedTextConaiterSize = await memoryEfficiencyPage.luaScriptTextContainer.offsetHeight; + const expandedTextContaiterSize = await memoryEfficiencyPage.luaScriptTextContainer.offsetHeight; await t.click(memoryEfficiencyPage.luaScriptButton); - await t.expect(memoryEfficiencyPage.luaScriptTextContainer.offsetHeight).lt(expandedTextConaiterSize, 'Recommendation not collapsed'); + await t.expect(memoryEfficiencyPage.luaScriptTextContainer.offsetHeight).lt(expandedTextContaiterSize, 'Lua script recommendation not collapsed'); await t.click(memoryEfficiencyPage.luaScriptButton); - await t.expect(memoryEfficiencyPage.luaScriptTextContainer.offsetHeight).eql(expandedTextConaiterSize, 'Recommendation not expanded'); + await t.expect(memoryEfficiencyPage.luaScriptTextContainer.offsetHeight).eql(expandedTextContaiterSize, 'Lua script recommendation not expanded'); // Verify that user can navigate by link to see the recommendation await t.click(memoryEfficiencyPage.readMoreLink); @@ -75,11 +78,9 @@ test // Close the window with external link to switch to the application window await t.closeWindow(); }); -test('Shard big hashes to small hashes recommendation', async t => { +test('No recommendations message', async t => { keyName = `recomKey-${common.generateWord(10)}`; const noRecommendationsMessage = 'No Recommendations at the moment.'; - const keyToAddParameters = { fieldsCount: 4999, keyName, fieldStartWith: 'hashField', fieldValueStartWith: 'hashValue' }; - const keyToAddParameters2 = { fieldsCount: 1, keyName, fieldStartWith: 'hashFieldLast', fieldValueStartWith: 'hashValueLast' }; const command = `HSET ${keyName} field value`; // Create Hash key and create report @@ -89,75 +90,7 @@ test('Shard big hashes to small hashes recommendation', async t => { await t.click(memoryEfficiencyPage.recommendationsTab); // No recommendations message await t.expect(memoryEfficiencyPage.noRecommendationsMessage.textContent).eql(noRecommendationsMessage, 'No recommendations message not displayed'); - - // Add 5000 fields to the hash key - await populateHashWithFields(dbParameters.host, dbParameters.port, keyToAddParameters); - // Generate new report - await t.click(memoryEfficiencyPage.newReportBtn); - // Verify that big keys recommendation not displayed when hash has 5000 fields - await t.expect(memoryEfficiencyPage.bigHashesAccordion.exists).notOk('Shard big hashes to small hashes recommendation is displayed when hash has 5000 fields'); - // Add the last field in hash key - await populateHashWithFields(dbParameters.host, dbParameters.port, keyToAddParameters2); - // Generate new report - await t.click(memoryEfficiencyPage.newReportBtn); - // Verify that user can see Shard big hashes to small hashes recommendation when Hash length > 5,000 - await t.expect(memoryEfficiencyPage.bigHashesAccordion.exists).ok('Shard big hashes to small hashes recommendation not displayed'); - await t.expect(memoryEfficiencyPage.codeChangesLabel.exists).ok('Big hashes recommendation not have Code Changes label'); - await t.expect(memoryEfficiencyPage.configurationChangesLabel.exists).ok('Big hashes recommendation not have Configuration Changes label'); -}); - -test('Combine small strings to hashes recommendation', async t => { - keyName = `recomKey-${common.generateWord(10)}`; - const commandToAddKey = `SET ${stringKeyName} value`; - const command = `SET ${keyName} "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed accumsan lectus sed diam suscipit, eu ullamcorper ligula pulvinar."`; - - // Create small String key and create report - await cliPage.sendCommandInCli(commandToAddKey); - await t.click(memoryEfficiencyPage.newReportBtn); - // Go to Recommendations tab - await t.click(memoryEfficiencyPage.recommendationsTab); - // Verify that user can see Combine small strings to hashes recommendation when there are strings that are less than 200 bytes - await t.expect(memoryEfficiencyPage.combineStringsAccordion.exists).ok('Combine small strings to hashes recommendation not displayed for small string'); - await t.expect(memoryEfficiencyPage.codeChangesLabel.exists).ok('Combine small strings to hashes recommendation not have Code Changes label'); - - // Add String key with more than 200 bytes - await cliPage.sendCommandInCli(command); - // Delete small String key - await cliPage.sendCommandInCli(`DEL ${stringKeyName}`); - await t.click(memoryEfficiencyPage.newReportBtn); - // Verify that user can not see recommendation when there are only strings with more than 200 bytes - await t.expect(memoryEfficiencyPage.combineStringsAccordion.exists).notOk('Combine small strings to hashes recommendation is displayed for huge string'); }); -test - .after(async t => { - // Clear and delete database - await t.click(myRedisDatabasePage.browserButton); - await browserPage.deleteKeyByName(keyName); - // Return back set-max-intset-entries value - await cliPage.sendCommandInCli('config set set-max-intset-entries 512'); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); - })('Increase the set-max-intset-entries recommendation', async t => { - keyName = `recomKey-${common.generateWord(10)}`; - const commandToAddKey = `SADD ${keyName} member`; - const command = 'config set set-max-intset-entries 1024'; - const setKeyParameters = { membersCount: 512, keyName, memberStartWith: 'setMember' }; - - // Create Set with 513 members and create report - await cliPage.sendCommandInCli(commandToAddKey); - await populateSetWithMembers(dbParameters.host, dbParameters.port, setKeyParameters); - await t.click(memoryEfficiencyPage.newReportBtn); - // Go to Recommendations tab - await t.click(memoryEfficiencyPage.recommendationsTab); - // Verify that user can see Increase the set-max-intset-entries recommendation when Found sets with length > set-max-intset-entries - await t.expect(memoryEfficiencyPage.increaseSetAccordion.exists).ok('Increase the set-max-intset-entries recommendation not displayed'); - await t.expect(memoryEfficiencyPage.configurationChangesLabel.exists).ok('Increase the set-max-intset-entries recommendation not have Configuration Changes label'); - - // Change config max entries to 1024 - await cliPage.sendCommandInCli(command); - await t.click(memoryEfficiencyPage.newReportBtn); - // Verify that user can not see Increase the set-max-intset-entries recommendation when Found sets with length < set-max-intset-entries - await t.expect(memoryEfficiencyPage.increaseSetAccordion.exists).notOk('Increase the set-max-intset-entries recommendation is displayed'); - }); test .before(async t => { await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); @@ -176,7 +109,7 @@ test await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName); await browserPage.deleteKeyByName(stringKeyName); await deleteStandaloneDatabaseApi(ossStandaloneConfig); - })('Avoid using logical databases', async t => { + })('Avoid using logical databases recommendation', async t => { // Go to Analysis Tools page await t.click(myRedisDatabasePage.analysisPageButton); await t.click(memoryEfficiencyPage.newReportBtn); @@ -186,39 +119,3 @@ test await t.expect(memoryEfficiencyPage.avoidLogicalDbAccordion.exists).ok('Avoid using logical databases recommendation not displayed'); await t.expect(memoryEfficiencyPage.codeChangesLabel.exists).ok('Avoid using logical databases recommendation not have Code Changes label'); }); -test - .after(async t => { - // Clear and delete database - await t.click(myRedisDatabasePage.browserButton); - await browserPage.deleteKeyByName(keyName); - await cliPage.sendCommandInCli('config set hash-max-ziplist-entries 512'); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); - })('Compress Hash field names', async t => { - keyName = `recomKey-${common.generateWord(10)}`; - const command = 'config set hash-max-ziplist-entries 1001'; - const commandToAddKey = `HSET ${keyName} field value`; - const hashKeyParameters = { fieldsCount: 1000, keyName, fieldStartWith: 'hashField', fieldValueStartWith: 'hashValue' }; - - // Create Hash key - await cliPage.sendCommandInCli(commandToAddKey); - // Add 1000 fields to Hash key and create report - await populateHashWithFields(dbParameters.host, dbParameters.port, hashKeyParameters); - await t.click(memoryEfficiencyPage.newReportBtn); - // Go to Recommendations tab - await t.click(memoryEfficiencyPage.recommendationsTab); - - // Verify that user can see Compress Hash field names recommendation when Hash length > 1,000 - await t.expect(memoryEfficiencyPage.compressHashAccordion.exists).ok('Compress Hash field names recommendation not displayed'); - await t.expect(memoryEfficiencyPage.configurationChangesLabel.exists).ok('Compress Hash field names recommendation not have Configuration Changes label'); - - // Convert hashtable to ziplist for hashes recommendation - // Verify that user can see Convert hashtable to ziplist for hashes recommendation when the number of hash entries exceeds hash-max-ziplist-entries - await t.expect(memoryEfficiencyPage.convertHashToZipAccordion.exists).ok('Convert hashtable to ziplist for hashes recommendation not displayed'); - await t.expect(memoryEfficiencyPage.configurationChangesLabel.exists).ok('Convert hashtable to ziplist for hashes recommendation not have Configuration Changes label'); - - // Change config max entries to 1001 - await cliPage.sendCommandInCli(command); - await t.click(memoryEfficiencyPage.newReportBtn); - // Verify that user can not see Convert hashtable to ziplist for hashes recommendation when the number of hash entries not exceeds hash-max-ziplist-entries - await t.expect(memoryEfficiencyPage.convertHashToZipAccordion.exists).notOk('Convert hashtable to ziplist for hashes recommendation is displayed'); - }); From 0c8adb405b653b1579aa49d60e4cc1f70926ecc3 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 8 Dec 2022 14:57:49 +0400 Subject: [PATCH 048/108] #RI-3563-3568 - add bigSets and zsetHashtableToZiplist recommendations --- .../api/src/constants/recommendations.ts | 4 +- .../providers/recommendation.provider.spec.ts | 71 ++++++++++++++++--- .../providers/recommendation.provider.ts | 47 +++++++++++- .../recommendation/recommendation.service.ts | 4 +- .../POST-databases-id-analysis.test.ts | 48 +++++++++++-- redisinsight/api/test/helpers/constants.ts | 12 +++- redisinsight/api/test/helpers/data/redis.ts | 38 ++++++++++ 7 files changed, 206 insertions(+), 18 deletions(-) diff --git a/redisinsight/api/src/constants/recommendations.ts b/redisinsight/api/src/constants/recommendations.ts index 42b84031c4..9aff07a24e 100644 --- a/redisinsight/api/src/constants/recommendations.ts +++ b/redisinsight/api/src/constants/recommendations.ts @@ -2,11 +2,13 @@ export const RECOMMENDATION_NAMES = Object.freeze({ LUA_SCRIPT: 'luaScript', BIG_HASHES: 'bigHashes', BIG_STRINGS: 'bigStrings', + BIG_SETS: 'bigSets', USE_SMALLER_KEYS: 'useSmallerKeys', AVOID_LOGICAL_DATABASES: 'avoidLogicalDatabases', COMBINE_SMALL_STRINGS_TO_HASHES: 'combineSmallStringsToHashes', INCREASE_SET_MAX_INTSET_ENTRIES: 'increaseSetMaxIntsetEntries', - CONVERT_HASHTABLE_TO_ZIPLIST: 'convertHashtableToZiplist', + HASH_HASHTABLE_TO_ZIPLIST: 'hashHashtableToZiplist', COMPRESS_HASH_FIELD_NAMES: 'compressHashFieldNames', COMPRESSION_FOR_LIST: 'compressionForList', + ZSET_HASHTABLE_TO_ZIPLIST: 'zSetHashtableToZiplist', }); diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts index e805865ee3..02db94a93d 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts @@ -69,6 +69,14 @@ const mockBigSet = { name: Buffer.from('name'), type: 'set', length: 513, memory: 10, ttl: -1, }; +const mockHugeSet = { + name: Buffer.from('name'), type: 'set', length: 5001, memory: 10, ttl: -1, +}; + +const mockBigZSetKey = { + name: Buffer.from('name'), type: 'zset', length: 513, memory: 10, ttl: -1, +}; + const mockBigListKey = { name: Buffer.from('name'), type: 'list', length: 1001, memory: 10, ttl: -1, }; @@ -225,36 +233,36 @@ describe('RecommendationProvider', () => { }); }); - describe('determineConvertHashtableToZiplistRecommendation', () => { - it('should not return convertHashtableToZiplist recommendation', async () => { + describe('determineHashHashtableToZiplistRecommendation', () => { + it('should not return hashHashtableToZiplist recommendation', async () => { when(nodeClient.sendCommand) .calledWith(jasmine.objectContaining({ name: 'config' })) .mockResolvedValue(mockRedisConfigResponse); const convertHashtableToZiplistRecommendation = await service - .determineConvertHashtableToZiplistRecommendation(nodeClient, mockKeys); + .determineHashHashtableToZiplistRecommendation(nodeClient, mockKeys); expect(convertHashtableToZiplistRecommendation).toEqual(null); }); - it('should return convertHashtableToZiplist recommendation', async () => { + it('should return hashHashtableToZiplist recommendation', async () => { when(nodeClient.sendCommand) .calledWith(jasmine.objectContaining({ name: 'config' })) .mockResolvedValue(mockRedisConfigResponse); const convertHashtableToZiplistRecommendation = await service - .determineConvertHashtableToZiplistRecommendation(nodeClient, [...mockKeys, mockBigHashKey_3]); + .determineHashHashtableToZiplistRecommendation(nodeClient, [...mockKeys, mockBigHashKey_3]); expect(convertHashtableToZiplistRecommendation) - .toEqual({ name: RECOMMENDATION_NAMES.CONVERT_HASHTABLE_TO_ZIPLIST }); + .toEqual({ name: RECOMMENDATION_NAMES.HASH_HASHTABLE_TO_ZIPLIST }); }); - it('should not return convertHashtableToZiplist recommendation when config command executed with error', + it('should not return hashHashtableToZiplist recommendation when config command executed with error', async () => { when(nodeClient.sendCommand) .calledWith(jasmine.objectContaining({ name: 'config' })) .mockRejectedValue('some error'); const convertHashtableToZiplistRecommendation = await service - .determineConvertHashtableToZiplistRecommendation(nodeClient, mockKeys); + .determineHashHashtableToZiplistRecommendation(nodeClient, mockKeys); expect(convertHashtableToZiplistRecommendation).toEqual(null); }); }); @@ -297,4 +305,51 @@ describe('RecommendationProvider', () => { expect(bigStringsRecommendation).toEqual({ name: RECOMMENDATION_NAMES.BIG_STRINGS }); }); }); + + describe('determineZSetHashtableToZiplistRecommendation', () => { + it('should not return zSetHashtableToZiplist recommendation', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'config' })) + .mockResolvedValue(mockRedisConfigResponse); + + const zSetHashtableToZiplistRecommendation = await service + .determineZSetHashtableToZiplistRecommendation(nodeClient, mockKeys); + expect(zSetHashtableToZiplistRecommendation).toEqual(null); + }); + + it('should return zSetHashtableToZiplist recommendation', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'config' })) + .mockResolvedValue(mockRedisConfigResponse); + + const zSetHashtableToZiplistRecommendation = await service + .determineZSetHashtableToZiplistRecommendation(nodeClient, [...mockKeys, mockBigZSetKey]); + expect(zSetHashtableToZiplistRecommendation) + .toEqual({ name: RECOMMENDATION_NAMES.ZSET_HASHTABLE_TO_ZIPLIST }); + }); + + it('should not return zSetHashtableToZiplist recommendation when config command executed with error', + async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'config' })) + .mockRejectedValue('some error'); + + const zSetHashtableToZiplistRecommendation = await service + .determineZSetHashtableToZiplistRecommendation(nodeClient, mockKeys); + expect(zSetHashtableToZiplistRecommendation).toEqual(null); + }); + }); + + describe('determineBigSetsRecommendation', () => { + it('should not return bigSets recommendation', async () => { + const bigSetsRecommendation = await service + .determineBigSetsRecommendation(mockKeys); + expect(bigSetsRecommendation).toEqual(null); + }); + it('should return bigSets recommendation', async () => { + const bigSetsRecommendation = await service + .determineBigSetsRecommendation([mockHugeSet]); + expect(bigSetsRecommendation).toEqual({ name: RECOMMENDATION_NAMES.BIG_SETS }); + }); + }); }); diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index 9626313fa1..a94d5cf8bb 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -13,6 +13,7 @@ const maxStringMemory = 200; const maxDatabaseTotal = 1_000_000; const maxCompressHashLength = 1000; const maxListLength = 1000; +const maxSetLength = 5000; const bigStringMemory = 5_000_000; @Injectable() @@ -146,7 +147,7 @@ export class RecommendationProvider { * @param redisClient */ - async determineConvertHashtableToZiplistRecommendation( + async determineHashHashtableToZiplistRecommendation( redisClient: Redis | Cluster, keys: Key[], ): Promise { @@ -158,7 +159,7 @@ export class RecommendationProvider { ) as string[]; const hashMaxZiplistEntriesNumber = parseInt(hashMaxZiplistEntries, 10); const bigHash = keys.some((key) => key.type === RedisDataType.Hash && key.length > hashMaxZiplistEntriesNumber); - return bigHash ? { name: RECOMMENDATION_NAMES.CONVERT_HASHTABLE_TO_ZIPLIST } : null; + return bigHash ? { name: RECOMMENDATION_NAMES.HASH_HASHTABLE_TO_ZIPLIST } : null; } catch (err) { this.logger.error('Can not determine Convert hashtable to ziplist recommendation', err); return null; @@ -212,4 +213,46 @@ export class RecommendationProvider { return null; } } + + /** + * Check zSet hashtable to ziplist recommendation + * @param keys + * @param redisClient + */ + + async determineZSetHashtableToZiplistRecommendation( + redisClient: Redis | Cluster, + keys: Key[], + ): Promise { + try { + const [, zSetMaxZiplistEntries] = await redisClient.sendCommand( + new Command('config', ['get', 'zset-max-ziplist-entries'], { + replyEncoding: 'utf8', + }), + ) as string[]; + const zSetMaxZiplistEntriesNumber = parseInt(zSetMaxZiplistEntries, 10); + const bigHash = keys.some((key) => key.type === RedisDataType.ZSet && key.length > zSetMaxZiplistEntriesNumber); + return bigHash ? { name: RECOMMENDATION_NAMES.ZSET_HASHTABLE_TO_ZIPLIST } : null; + } catch (err) { + this.logger.error('Can not determine ZSet hashtable to ziplist recommendation', err); + return null; + } + } + + /** + * Check big sets recommendation + * @param keys + */ + + async determineBigSetsRecommendation( + keys: Key[], + ): Promise { + try { + const bigSet = keys.some((key) => key.type === RedisDataType.Set && key.length > maxSetLength); + return bigSet ? { name: RECOMMENDATION_NAMES.BIG_SETS } : null; + } catch (err) { + this.logger.error('Can not determine Big sets recommendation', err); + return null; + } + } } diff --git a/redisinsight/api/src/modules/recommendation/recommendation.service.ts b/redisinsight/api/src/modules/recommendation/recommendation.service.ts index 23f1e6bf8b..9b2f0fc1f2 100644 --- a/redisinsight/api/src/modules/recommendation/recommendation.service.ts +++ b/redisinsight/api/src/modules/recommendation/recommendation.service.ts @@ -41,10 +41,12 @@ export class RecommendationService { await this.recommendationProvider.determineLogicalDatabasesRecommendation(client), await this.recommendationProvider.determineCombineSmallStringsToHashesRecommendation(keys), await this.recommendationProvider.determineIncreaseSetMaxIntsetEntriesRecommendation(client, keys), - await this.recommendationProvider.determineConvertHashtableToZiplistRecommendation(client, keys), + await this.recommendationProvider.determineHashHashtableToZiplistRecommendation(client, keys), await this.recommendationProvider.determineCompressHashFieldNamesRecommendation(keys), await this.recommendationProvider.determineCompressionForListRecommendation(keys), await this.recommendationProvider.determineBigStringsRecommendation(keys), + await this.recommendationProvider.determineZSetHashtableToZiplistRecommendation(client, keys), + await this.recommendationProvider.determineBigSetsRecommendation(keys), ])); } } diff --git a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts index bef463a5a4..9000f9141f 100644 --- a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts +++ b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts @@ -171,7 +171,7 @@ describe('POST /databases/:instanceId/analysis', () => { checkFn: async ({ body }) => { expect(body.recommendations).to.deep.eq([ constants.TEST_BIG_HASHES_DATABASE_ANALYSIS_RECOMMENDATION, - constants.TEST_CONVERT_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, + constants.TEST_HASH_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, constants.TEST_COMPRESS_HASH_FIELD_NAMES_RECOMMENDATION, ]); }, @@ -215,7 +215,7 @@ describe('POST /databases/:instanceId/analysis', () => { } }, { - name: 'Should create new database analysis with convertHashtableToZiplist recommendation', + name: 'Should create new database analysis with hashHashtableToZiplist recommendation', data: { delimiter: '-', }, @@ -227,7 +227,7 @@ describe('POST /databases/:instanceId/analysis', () => { }, checkFn: async ({ body }) => { expect(body.recommendations).to.deep.eq([ - constants.TEST_CONVERT_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, + constants.TEST_HASH_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, ]); }, after: async () => { @@ -247,7 +247,7 @@ describe('POST /databases/:instanceId/analysis', () => { }, checkFn: async ({ body }) => { expect(body.recommendations).to.deep.eq([ - constants.TEST_CONVERT_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, + constants.TEST_HASH_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, constants.TEST_COMPRESS_HASH_FIELD_NAMES_RECOMMENDATION, ]); }, @@ -313,6 +313,46 @@ describe('POST /databases/:instanceId/analysis', () => { expect(await repository.count()).to.eq(5); } }, + { + name: 'Should create new database analysis with zSetHashtableToZiplist recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + const NUMBERS_OF_ZSET_MEMBERS = 129; + await rte.data.generateHugeMembersForSortedListKey(NUMBERS_OF_ZSET_MEMBERS, true); + }, + checkFn: async ({ body }) => { + expect(body.recommendations).to.deep.eq([ + constants.TEST_ZSET_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, + ]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } + }, + { + name: 'Should create new database analysis with bigSets recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + const NUMBERS_OF_SET_MEMBERS = 5001; + await rte.data.generateHugeMembersSetKey(NUMBERS_OF_SET_MEMBERS, true); + }, + checkFn: async ({ body }) => { + expect(body.recommendations).to.deep.eq([ + constants.TEST_BIG_SETS_RECOMMENDATION, + ]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } + }, ].map(mainCheckFn); describe('useSmallerKeys recommendation', () => { diff --git a/redisinsight/api/test/helpers/constants.ts b/redisinsight/api/test/helpers/constants.ts index f2368f5a74..60335cfec2 100644 --- a/redisinsight/api/test/helpers/constants.ts +++ b/redisinsight/api/test/helpers/constants.ts @@ -467,8 +467,8 @@ export const constants = { TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION: { name: RECOMMENDATION_NAMES.COMBINE_SMALL_STRINGS_TO_HASHES, }, - TEST_CONVERT_HASHTABLE_TO_ZIPLIST_RECOMMENDATION: { - name: RECOMMENDATION_NAMES.CONVERT_HASHTABLE_TO_ZIPLIST, + TEST_HASH_HASHTABLE_TO_ZIPLIST_RECOMMENDATION: { + name: RECOMMENDATION_NAMES.HASH_HASHTABLE_TO_ZIPLIST, }, TEST_COMPRESS_HASH_FIELD_NAMES_RECOMMENDATION: { name: RECOMMENDATION_NAMES.COMPRESS_HASH_FIELD_NAMES, @@ -480,5 +480,13 @@ export const constants = { name: RECOMMENDATION_NAMES.BIG_STRINGS, }, + TEST_ZSET_HASHTABLE_TO_ZIPLIST_RECOMMENDATION: { + name: RECOMMENDATION_NAMES.ZSET_HASHTABLE_TO_ZIPLIST, + }, + + TEST_BIG_SETS_RECOMMENDATION: { + name: RECOMMENDATION_NAMES.BIG_SETS, + }, + // etc... } diff --git a/redisinsight/api/test/helpers/data/redis.ts b/redisinsight/api/test/helpers/data/redis.ts index 1bf6d8a60e..2691704d43 100644 --- a/redisinsight/api/test/helpers/data/redis.ts +++ b/redisinsight/api/test/helpers/data/redis.ts @@ -250,6 +250,24 @@ export const initDataHelper = (rte) => { ); }; + const generateHugeMembersSetKey = async (number: number = 100000, clean: boolean) => { + if (clean) { + await truncate(); + } + + const batchSize = 10000; + let inserted = 0; + do { + const pipeline = []; + const limit = inserted + batchSize; + for (inserted; inserted < limit && inserted < number; inserted++) { + pipeline.push(['sadd', constants.TEST_SET_KEY_1, inserted]); + } + + await insertKeysBasedOnEnv(pipeline, true); + } while (inserted < number) + }; + // ZSet const generateZSets = async (clean: boolean = false) => { if (clean) { @@ -286,6 +304,24 @@ export const initDataHelper = (rte) => { ); }; + const generateHugeMembersForSortedListKey = async (number: number = 100000, clean: boolean) => { + if (clean) { + await truncate(); + } + + const batchSize = 10000; + let inserted = 0; + do { + const pipeline = []; + const limit = inserted + batchSize; + for (inserted; inserted < limit && inserted < number; inserted++) { + pipeline.push(['zadd', constants.TEST_ZSET_KEY_1, inserted, inserted]); + } + + await insertKeysBasedOnEnv(pipeline, true); + } while (inserted < number) + }; + // Hash const generateHashes = async (clean: boolean = false) => { if (clean) { @@ -527,6 +563,8 @@ export const initDataHelper = (rte) => { generateHugeNumberOfFieldsForHashKey, generateHugeNumberOfTinyStringKeys, generateHugeElementsForListKey, + generateHugeMembersForSortedListKey, + generateHugeMembersSetKey, generateHugeStream, generateNKeys, generateRedisearchIndexes, From 8b3f4b4ce20804485fd2a33b3f49bceccb830837 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 8 Dec 2022 15:15:58 +0400 Subject: [PATCH 049/108] #RI-3563-3568 - fix IT --- .../POST-databases-id-analysis.test.ts | 4 +++- redisinsight/api/test/helpers/data/redis.ts | 19 ------------------- 2 files changed, 3 insertions(+), 20 deletions(-) diff --git a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts index 9000f9141f..f4eb8a83cc 100644 --- a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts +++ b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts @@ -342,10 +342,12 @@ describe('POST /databases/:instanceId/analysis', () => { responseSchema, before: async () => { const NUMBERS_OF_SET_MEMBERS = 5001; - await rte.data.generateHugeMembersSetKey(NUMBERS_OF_SET_MEMBERS, true); + await rte.data.generateHugeNumberOfMembersForSetKey(NUMBERS_OF_SET_MEMBERS, true); }, checkFn: async ({ body }) => { expect(body.recommendations).to.deep.eq([ + // by default max_intset_entries = 512 + constants.TEST_INCREASE_SET_MAX_INTSET_ENTRIES_RECOMMENDATION, constants.TEST_BIG_SETS_RECOMMENDATION, ]); }, diff --git a/redisinsight/api/test/helpers/data/redis.ts b/redisinsight/api/test/helpers/data/redis.ts index 2691704d43..d4f1bb6432 100644 --- a/redisinsight/api/test/helpers/data/redis.ts +++ b/redisinsight/api/test/helpers/data/redis.ts @@ -250,24 +250,6 @@ export const initDataHelper = (rte) => { ); }; - const generateHugeMembersSetKey = async (number: number = 100000, clean: boolean) => { - if (clean) { - await truncate(); - } - - const batchSize = 10000; - let inserted = 0; - do { - const pipeline = []; - const limit = inserted + batchSize; - for (inserted; inserted < limit && inserted < number; inserted++) { - pipeline.push(['sadd', constants.TEST_SET_KEY_1, inserted]); - } - - await insertKeysBasedOnEnv(pipeline, true); - } while (inserted < number) - }; - // ZSet const generateZSets = async (clean: boolean = false) => { if (clean) { @@ -564,7 +546,6 @@ export const initDataHelper = (rte) => { generateHugeNumberOfTinyStringKeys, generateHugeElementsForListKey, generateHugeMembersForSortedListKey, - generateHugeMembersSetKey, generateHugeStream, generateNKeys, generateRedisearchIndexes, From 78228eee3d90cfbb1bd570782a0742134ec82100 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 8 Dec 2022 15:19:16 +0400 Subject: [PATCH 050/108] #RI-3563-3568 - add bigSets and ZsetHastableToZiplist recommendations --- .../constants/dbAnalysisRecommendations.json | 158 +++++++++++++++++- .../Recommendations.spec.tsx | 34 +++- .../recommendations-view/Recommendations.tsx | 9 +- .../recommendations-view/styles.module.scss | 9 + .../components/recommendations-view/utils.tsx | 18 +- 5 files changed, 219 insertions(+), 9 deletions(-) diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index b42008ec2e..b63b8cd138 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -154,8 +154,8 @@ ], "badges": ["configuration_changes"] }, - "convertHashtableToZiplist": { - "id": "convertHashtableToZiplist", + "hashHashtableToZiplist": { + "id": "hashHashtableToZiplist", "title": "Increase hash-max-ziplist-entries", "content": [ { @@ -248,5 +248,159 @@ } ], "badges": ["configuration_changes"] + }, + "zSetHashtableToZiplist": { + "id": "zSetHashtableToZiplist", + "title": "the number of sorted set members exceeds zset-max-ziplist-entries", + "content": [ + { + "id": "1", + "type": "span", + "value": "If any value for a key exceeds zset-max-ziplist-entries, it is stored automatically as a Hashtable instead of a Ziplist, which consumes almost double the memory. So to save memory, increase the configurations and convert your hashtables to ziplist. The trade-off can be an increase in latency and possibly an increase in CPU utilization. " + }, + { + "id": "2", + "type": "link", + "value": { + "href": "https://docs.redis.com/latest/ri/memory-optimizations/", + "name": "Read more" + } + } + ], + "badges": ["configuration_changes"] + }, + "bigSets": { + "id": "bigSets", + "title": "Switch to Bloom filter, cuckoo filter, or HyperLogLog", + "content": [ + { + "id": "1", + "type": "span", + "value": "If you are using large " + }, + { + "id": "2", + "type": "link", + "value": { + "href": "https://redis.io/docs/data-types/sets/", + "name": "sets" + } + }, + { + "id": "3", + "type": "span", + "value": " to solve one of the following problems:" + }, + { + "id": "4", + "type": "list", + "value": [ + [ + { + "id": "1", + "type": "span", + "value": "Count the number of unique observations in a stream" + } + ], + [ + { + "id": "1", + "type": "span", + "value": "Check if an observation already appeared in the stream" + } + ], + [ + { + "id": "1", + "type": "span", + "value": "Find the fraction or the number of observations in the stream that are smaller or larger than a given value" + } + ] + ] + }, + { + "id": "5", + "type": "span", + "value": "and you are ready to trade accuracy with speed and memory usage, consider using one of the following probabilistic data structures:" + }, + { + "id": "6", + "type": "list", + "value": [ + [ + { + "id": "1", + "type": "link", + "value": { + "href": "https://redis.io/docs/data-types/hyperloglogs/", + "name": "HyperLogLog" + } + }, + { + "id": "2", + "type": "span", + "value": " can be used for estimating the number of unique observations in a set." + } + ], + [ + { + "id": "1", + "type": "link", + "value": { + "href": "https://redis.io/docs/stack/bloom/", + "name": "Bloom filter or cuckoo filter" + } + }, + { + "id": "2", + "type": "span", + "value": " can be used for checking if an observation has already appeared in the stream (false positive matches are possible, but false negatives are not)." + } + ], + [ + { + "id": "1", + "type": "link", + "value": { + "href": "https://redis.io/docs/stack/bloom/", + "name": "t-digest" + } + }, + { + "id": "2", + "type": "span", + "value": " can be used for estimating the fraction or the number of observations in the stream that are smaller or larger than a given value." + } + ] + ] + }, + { + "id": "5", + "type": "span", + "value": "Bloom filter and cuckoo filter require " + }, + { + "id": "6", + "type": "link", + "value": { + "href": "https://redis.com/modules/redis-bloom/", + "name": "RedisBloom" + } + }, + { + "id": "7", + "type": "span", + "value": ". " + }, + { + "id": "8", + "type": "link", + "value": { + "href": "https://docs.redis.com/latest/ri/memory-optimizations/", + "name": "Read more" + } + } + ], + "badges": ["configuration_changes"] } } diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx index 48562884de..50b7d348b6 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx @@ -137,11 +137,11 @@ describe('Recommendations', () => { expect(screen.queryByTestId('configuration_changes')).toBeInTheDocument() }) - it('should render code changes badge in convertHashtableToZiplist recommendation', () => { + it('should render code changes badge in hashHashtableToZiplist recommendation', () => { (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ ...mockdbAnalysisSelector, data: { - recommendations: [{ name: 'convertHashtableToZiplist' }] + recommendations: [{ name: 'hashHashtableToZiplist' }] } })) @@ -197,6 +197,36 @@ describe('Recommendations', () => { expect(screen.queryByTestId('configuration_changes')).toBeInTheDocument() }) + it('should render configuration_changes badge in zSetHashtableToZiplist recommendation', () => { + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: { + recommendations: [{ name: 'zSetHashtableToZiplist' }] + } + })) + + render() + + expect(screen.queryByTestId('code_changes')).not.toBeInTheDocument() + expect(screen.queryByTestId('upgrade')).not.toBeInTheDocument() + expect(screen.queryByTestId('configuration_changes')).toBeInTheDocument() + }) + + it('should render configuration_changes badge in bigSets recommendation', () => { + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: { + recommendations: [{ name: 'bigSets' }] + } + })) + + render() + + expect(screen.queryByTestId('code_changes')).not.toBeInTheDocument() + expect(screen.queryByTestId('upgrade')).not.toBeInTheDocument() + expect(screen.queryByTestId('configuration_changes')).toBeInTheDocument() + }) + it('should collapse/expand', () => { (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ ...mockdbAnalysisSelector, diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx index 4250327689..e59b7b65aa 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx @@ -9,7 +9,7 @@ import { import { dbAnalysisSelector } from 'uiSrc/slices/analytics/dbAnalysis' import recommendationsContent from 'uiSrc/constants/dbAnalysisRecommendations.json' -import { parseContent, renderBadges } from './utils' +import { renderContent, renderBadges } from './utils' import styles from './styles.module.scss' const Recommendations = () => { @@ -47,12 +47,13 @@ const Recommendations = () => { data-testId={`${id}-accordion`} > - {content.map((item: { type: string, value: any, id: string }) => + {renderContent(content)} + {/* {content.map((item: { type: string, value: any, id: string }) => ( - {parseContent(item)} + {renderContent(item)} - ))} + ))} */}
diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss index 8d1b49428e..481f546141 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss @@ -33,6 +33,15 @@ margin-bottom: 6px; padding: 18px; + ul { + list-style: initial; + padding-left: 21px; + + li::marker { + color: var(--euiTextSubduedColor); + } + } + .accordion { margin-bottom: 18px; } diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx index 4d6a08cbb7..ec9704942f 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx @@ -13,6 +13,11 @@ import { ReactComponent as UpgradeIcon } from 'uiSrc/assets/img/upgrade.svg' import styles from './styles.module.scss' +interface IContentElement { + type: string + value: any[] +} + const badgesContent = [ { id: 'code_changes', icon: , name: 'Code Changes' }, { id: 'configuration_changes', icon: , name: 'Configuration Changes' }, @@ -34,7 +39,7 @@ export const renderBadges = (badges: string[]) => ( ) -export const parseContent = ({ type, value }: { type: string, value: any }) => { +const renderContentElement = ({ type, value }: IContentElement) => { switch (type) { case 'paragraph': return {value} @@ -44,7 +49,18 @@ export const parseContent = ({ type, value }: { type: string, value: any }) => { return {value.name} case 'spacer': return + case 'list': + return ( +
    + {value.map((listElement: IContentElement[]) => ( +
  • {renderContent(listElement)}
  • + ))} +
+ ) default: return value } } + +export const renderContent = (elements: IContentElement[]) => ( + elements?.map((item) => renderContentElement(item))) From e34e07fbe2a5f0b22a1bcac9775a595db53163be Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 8 Dec 2022 15:40:42 +0400 Subject: [PATCH 051/108] #RI-3563-3568 - fix integration test --- .../POST-databases-id-analysis.test.ts | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts index f4eb8a83cc..023d1cf006 100644 --- a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts +++ b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts @@ -296,37 +296,41 @@ describe('POST /databases/:instanceId/analysis', () => { } }, { - name: 'Should create new database analysis with luaScript recommendation', + name: 'Should create new database analysis with zSetHashtableToZiplist recommendation', data: { delimiter: '-', }, statusCode: 201, responseSchema, before: async () => { - await rte.data.generateNCachedScripts(11, true); + const NUMBERS_OF_ZSET_MEMBERS = 129; + await rte.data.generateHugeMembersForSortedListKey(NUMBERS_OF_ZSET_MEMBERS, true); }, checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([constants.TEST_LUA_DATABASE_ANALYSIS_RECOMMENDATION]); + expect(body.recommendations).to.deep.eq([ + constants.TEST_ZSET_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, + ]); }, after: async () => { - await rte.data.sendCommand('script', ['flush']); expect(await repository.count()).to.eq(5); } }, { - name: 'Should create new database analysis with zSetHashtableToZiplist recommendation', + name: 'Should create new database analysis with bigSets recommendation', data: { delimiter: '-', }, statusCode: 201, responseSchema, before: async () => { - const NUMBERS_OF_ZSET_MEMBERS = 129; - await rte.data.generateHugeMembersForSortedListKey(NUMBERS_OF_ZSET_MEMBERS, true); + const NUMBERS_OF_SET_MEMBERS = 5001; + await rte.data.generateHugeNumberOfMembersForSetKey(NUMBERS_OF_SET_MEMBERS, true); }, checkFn: async ({ body }) => { expect(body.recommendations).to.deep.eq([ - constants.TEST_ZSET_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, + // by default max_intset_entries = 512 + constants.TEST_INCREASE_SET_MAX_INTSET_ENTRIES_RECOMMENDATION, + constants.TEST_BIG_SETS_RECOMMENDATION, ]); }, after: async () => { @@ -334,24 +338,20 @@ describe('POST /databases/:instanceId/analysis', () => { } }, { - name: 'Should create new database analysis with bigSets recommendation', + name: 'Should create new database analysis with luaScript recommendation', data: { delimiter: '-', }, statusCode: 201, responseSchema, before: async () => { - const NUMBERS_OF_SET_MEMBERS = 5001; - await rte.data.generateHugeNumberOfMembersForSetKey(NUMBERS_OF_SET_MEMBERS, true); + await rte.data.generateNCachedScripts(11, true); }, checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([ - // by default max_intset_entries = 512 - constants.TEST_INCREASE_SET_MAX_INTSET_ENTRIES_RECOMMENDATION, - constants.TEST_BIG_SETS_RECOMMENDATION, - ]); + expect(body.recommendations).to.deep.eq([constants.TEST_LUA_DATABASE_ANALYSIS_RECOMMENDATION]); }, after: async () => { + await rte.data.sendCommand('script', ['flush']); expect(await repository.count()).to.eq(5); } }, From 254e72e4b03f7d27a887a1927629c685c4313365 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 8 Dec 2022 17:11:52 +0400 Subject: [PATCH 052/108] #RI-3563-3568 0 remove commented code --- .../components/recommendations-view/Recommendations.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx index e59b7b65aa..d4be0e39d6 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx @@ -48,12 +48,6 @@ const Recommendations = () => { > {renderContent(content)} - {/* {content.map((item: { type: string, value: any, id: string }) => - ( - - {renderContent(item)} - - ))} */}
From d1bf1596521746d970044ba1ef6ee76a3d363ac8 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 8 Dec 2022 20:54:13 +0400 Subject: [PATCH 053/108] #RI-3563 - change recommendation title --- .../src/constants/dbAnalysisRecommendations.json | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index b63b8cd138..a05244ff1b 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -251,15 +251,25 @@ }, "zSetHashtableToZiplist": { "id": "zSetHashtableToZiplist", - "title": "the number of sorted set members exceeds zset-max-ziplist-entries", + "title": "Convert hashtable to ziplist for sorted sets", "content": [ { "id": "1", + "type": "paragraph", + "value": "Increase zset-max-ziplist-entries" + }, + { + "id": "2", + "type": "spacer", + "value": "l" + }, + { + "id": "3", "type": "span", "value": "If any value for a key exceeds zset-max-ziplist-entries, it is stored automatically as a Hashtable instead of a Ziplist, which consumes almost double the memory. So to save memory, increase the configurations and convert your hashtables to ziplist. The trade-off can be an increase in latency and possibly an increase in CPU utilization. " }, { - "id": "2", + "id": "4", "type": "link", "value": { "href": "https://docs.redis.com/latest/ri/memory-optimizations/", From 3948df8a4c6894e434af21b1fa4eda2685d84162 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Sun, 11 Dec 2022 12:39:09 +0400 Subject: [PATCH 054/108] #RI-3883 - add telemtry in recommendations --- .../DatabaseAnalysisTabs.spec.tsx | 71 +++++++++++++++++++ .../data-nav-tabs/DatabaseAnalysisTabs.tsx | 21 ++++++ .../Recommendations.spec.tsx | 29 +++++++- .../recommendations-view/Recommendations.tsx | 13 ++++ redisinsight/ui/src/telemetry/events.ts | 4 ++ 5 files changed, 137 insertions(+), 1 deletion(-) diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/data-nav-tabs/DatabaseAnalysisTabs.spec.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/data-nav-tabs/DatabaseAnalysisTabs.spec.tsx index 18dd4fceea..624fa17d32 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/data-nav-tabs/DatabaseAnalysisTabs.spec.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/data-nav-tabs/DatabaseAnalysisTabs.spec.tsx @@ -2,12 +2,19 @@ import React from 'react' import { cloneDeep } from 'lodash' import { instance, mock } from 'ts-mockito' import { MOCK_ANALYSIS_REPORT_DATA } from 'uiSrc/mocks/data/analysis' +import { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/analytics/clusterDetailsHandlers' import { render, screen, mockedStore, cleanup, fireEvent } from 'uiSrc/utils/test-utils' import { DatabaseAnalysisViewTab } from 'uiSrc/slices/interfaces/analytics' import { setDatabaseAnalysisViewTab } from 'uiSrc/slices/analytics/dbAnalysis' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import DatabaseAnalysisTabs, { Props } from './DatabaseAnalysisTabs' +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) + const mockedProps = mock() const mockReports = [ @@ -80,4 +87,68 @@ describe('DatabaseAnalysisTabs', () => { expect(screen.queryByTestId(`${DatabaseAnalysisViewTab.Recommendations}-tab`)).toHaveTextContent('Recommendations') }) }) + + describe('Telemetry', () => { + it('should call DATABASE_ANALYSIS_DATA_SUMMARY_CLICKED telemetry event with 0 count', () => { + const sendEventTelemetryMock = jest.fn() + sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) + + const mockData = { + recommendations: [] + } + render() + + fireEvent.click(screen.getByTestId(`${DatabaseAnalysisViewTab.DataSummary}-tab`)) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.DATABASE_ANALYSIS_DATA_SUMMARY_CLICKED, + eventData: { + databaseId: INSTANCE_ID_MOCK, + } + }) + sendEventTelemetry.mockRestore() + }) + + it('should call DATABASE_ANALYSIS_RECOMMENDATIONS_CLICKED telemetry event with 0 count', () => { + const sendEventTelemetryMock = jest.fn() + sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) + + const mockData = { + recommendations: [] + } + render() + + fireEvent.click(screen.getByTestId(`${DatabaseAnalysisViewTab.Recommendations}-tab`)) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.DATABASE_ANALYSIS_RECOMMENDATIONS_CLICKED, + eventData: { + databaseId: INSTANCE_ID_MOCK, + recommendationsCount: 0, + } + }) + sendEventTelemetry.mockRestore() + }) + + it('should call DATABASE_ANALYSIS_RECOMMENDATIONS_CLICKED telemetry event with 2 count', () => { + const sendEventTelemetryMock = jest.fn() + sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) + + const mockData = { + recommendations: [{ name: 'luaScript' }, { name: 'luaScript' }] + } + render() + + fireEvent.click(screen.getByTestId(`${DatabaseAnalysisViewTab.Recommendations}-tab`)) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.DATABASE_ANALYSIS_RECOMMENDATIONS_CLICKED, + eventData: { + databaseId: INSTANCE_ID_MOCK, + recommendationsCount: 2, + } + }) + sendEventTelemetry.mockRestore() + }) + }) }) diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/data-nav-tabs/DatabaseAnalysisTabs.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/data-nav-tabs/DatabaseAnalysisTabs.tsx index b06f999fc0..6e41ba5f57 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/data-nav-tabs/DatabaseAnalysisTabs.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/data-nav-tabs/DatabaseAnalysisTabs.tsx @@ -1,4 +1,5 @@ import React, { useMemo } from 'react' +import { useParams } from 'react-router-dom' import { EuiTab, EuiTabs } from '@elastic/eui' import { isNull } from 'lodash' import { useDispatch, useSelector } from 'react-redux' @@ -7,6 +8,7 @@ import { EmptyAnalysisMessage } from 'uiSrc/pages/databaseAnalysis/components' import { setDatabaseAnalysisViewTab, dbAnalysisViewTabSelector } from 'uiSrc/slices/analytics/dbAnalysis' import { DatabaseAnalysisViewTab } from 'uiSrc/slices/interfaces/analytics' import { Nullable } from 'uiSrc/utils' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { ShortDatabaseAnalysis, DatabaseAnalysis } from 'apiSrc/modules/database-analysis/models' import { databaseAnalysisTabs } from './constants' @@ -23,11 +25,30 @@ const DatabaseAnalysisTabs = (props: Props) => { const viewTab = useSelector(dbAnalysisViewTabSelector) + const { instanceId } = useParams<{ instanceId: string }>() + const dispatch = useDispatch() const selectedTabContent = useMemo(() => databaseAnalysisTabs.find((tab) => tab.id === viewTab)?.content, [viewTab]) const onSelectedTabChanged = (id: DatabaseAnalysisViewTab) => { + if (id === DatabaseAnalysisViewTab.DataSummary) { + sendEventTelemetry({ + event: TelemetryEvent.DATABASE_ANALYSIS_DATA_SUMMARY_CLICKED, + eventData: { + databaseId: instanceId, + } + }) + } + if (id === DatabaseAnalysisViewTab.Recommendations) { + sendEventTelemetry({ + event: TelemetryEvent.DATABASE_ANALYSIS_RECOMMENDATIONS_CLICKED, + eventData: { + databaseId: instanceId, + recommendationsCount: data?.recommendations?.length, + } + }) + } dispatch(setDatabaseAnalysisViewTab(id)) } diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx index 50b7d348b6..807a847f35 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx @@ -1,11 +1,18 @@ import React from 'react' import { fireEvent, render, screen } from 'uiSrc/utils/test-utils' import { dbAnalysisSelector } from 'uiSrc/slices/analytics/dbAnalysis' +import { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/analytics/clusterDetailsHandlers' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import Recommendations from './Recommendations' const mockdbAnalysisSelector = jest.requireActual('uiSrc/slices/analytics/dbAnalysis') +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) + jest.mock('uiSrc/slices/analytics/dbAnalysis', () => ({ ...jest.requireActual('uiSrc/slices/analytics/dbAnalysis'), dbAnalysisSelector: jest.fn().mockReturnValue({ @@ -227,7 +234,7 @@ describe('Recommendations', () => { expect(screen.queryByTestId('configuration_changes')).toBeInTheDocument() }) - it('should collapse/expand', () => { + it('should collapse/expand and sent proper telemetry event', () => { (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ ...mockdbAnalysisSelector, data: { @@ -235,6 +242,10 @@ describe('Recommendations', () => { } })) + const sendEventTelemetryMock = jest.fn() + + sendEventTelemetry.mockImplementation(() => sendEventTelemetryMock) + const { container } = render() expect(screen.queryAllByTestId('luaScript-accordion')[0]?.classList.contains('euiAccordion-isOpen')).toBeTruthy() @@ -242,9 +253,25 @@ describe('Recommendations', () => { fireEvent.click(container.querySelector('[data-test-subj="luaScript-button"]') as HTMLInputElement) expect(screen.queryAllByTestId('luaScript-accordion')[0]?.classList.contains('euiAccordion-isOpen')).not.toBeTruthy() + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.DATABASE_ANALYSIS_RECOMMENDATIONS_COLLAPSED, + eventData: { + databaseId: INSTANCE_ID_MOCK, + recommendation: 'luaScript', + } + }) + sendEventTelemetry.mockRestore() fireEvent.click(container.querySelector('[data-test-subj="luaScript-button"]') as HTMLInputElement) expect(screen.queryAllByTestId('luaScript-accordion')[0]?.classList.contains('euiAccordion-isOpen')).toBeTruthy() + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.DATABASE_ANALYSIS_RECOMMENDATIONS_EXPANDED, + eventData: { + databaseId: INSTANCE_ID_MOCK, + recommendation: 'luaScript', + } + }) + sendEventTelemetry.mockRestore() }) }) diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx index d4be0e39d6..8f34b119b4 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx @@ -1,5 +1,6 @@ import React from 'react' import { useSelector } from 'react-redux' +import { useParams } from 'react-router-dom' import { isNull } from 'lodash' import { EuiAccordion, @@ -8,6 +9,7 @@ import { } from '@elastic/eui' import { dbAnalysisSelector } from 'uiSrc/slices/analytics/dbAnalysis' import recommendationsContent from 'uiSrc/constants/dbAnalysisRecommendations.json' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { renderContent, renderBadges } from './utils' import styles from './styles.module.scss' @@ -16,6 +18,8 @@ const Recommendations = () => { const { data, loading } = useSelector(dbAnalysisSelector) const { recommendations = [] } = data ?? {} + const { instanceId } = useParams<{ instanceId: string }>() + if (loading) { return (
@@ -44,6 +48,15 @@ const Recommendations = () => { buttonProps={{ 'data-test-subj': `${id}-button` }} className={styles.accordion} initialIsOpen + onToggle={(isOpen) => sendEventTelemetry({ + event: isOpen + ? TelemetryEvent.DATABASE_ANALYSIS_RECOMMENDATIONS_EXPANDED + : TelemetryEvent.DATABASE_ANALYSIS_RECOMMENDATIONS_COLLAPSED, + eventData: { + databaseId: instanceId, + recommendation: id, + } + })} data-testId={`${id}-accordion`} > diff --git a/redisinsight/ui/src/telemetry/events.ts b/redisinsight/ui/src/telemetry/events.ts index ea9b2bf88f..31fb5f97ab 100644 --- a/redisinsight/ui/src/telemetry/events.ts +++ b/redisinsight/ui/src/telemetry/events.ts @@ -185,6 +185,10 @@ export enum TelemetryEvent { DATABASE_ANALYSIS_STARTED = 'DATABASE_ANALYSIS_STARTED', DATABASE_ANALYSIS_HISTORY_VIEWED = 'DATABASE_ANALYSIS_HISTORY_VIEWED', DATABASE_ANALYSIS_EXTRAPOLATION_CHANGED = 'DATABASE_ANALYSIS_EXTRAPOLATION_CHANGED', + DATABASE_ANALYSIS_RECOMMENDATIONS_CLICKED = 'DATABASE_ANALYSIS_RECOMMENDATIONS_CLICKED', + DATABASE_ANALYSIS_DATA_SUMMARY_CLICKED = 'DATABASE_ANALYSIS_DATA_SUMMARY_CLICKED', + DATABASE_ANALYSIS_RECOMMENDATIONS_EXPANDED = 'DATABASE_ANALYSIS_RECOMMENDATIONS_EXPANDED', + DATABASE_ANALYSIS_RECOMMENDATIONS_COLLAPSED = 'DATABASE_ANALYSIS_RECOMMENDATIONS_COLLAPSED', USER_SURVEY_LINK_CLICKED = 'USER_SURVEY_LINK_CLICKED', From e46737fc90efe0728f14575d418e3c6d52ee06de Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 12 Dec 2022 11:17:21 +0400 Subject: [PATCH 055/108] #RI-3883 - resolve comments --- .../recommendations-view/Recommendations.tsx | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx index 8f34b119b4..7b42dd9686 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx @@ -20,6 +20,16 @@ const Recommendations = () => { const { instanceId } = useParams<{ instanceId: string }>() + const handleToggle = (isOpen: boolean, id: string) => sendEventTelemetry({ + event: isOpen + ? TelemetryEvent.DATABASE_ANALYSIS_RECOMMENDATIONS_EXPANDED + : TelemetryEvent.DATABASE_ANALYSIS_RECOMMENDATIONS_COLLAPSED, + eventData: { + databaseId: instanceId, + recommendation: id, + } + }) + if (loading) { return (
@@ -48,15 +58,7 @@ const Recommendations = () => { buttonProps={{ 'data-test-subj': `${id}-button` }} className={styles.accordion} initialIsOpen - onToggle={(isOpen) => sendEventTelemetry({ - event: isOpen - ? TelemetryEvent.DATABASE_ANALYSIS_RECOMMENDATIONS_EXPANDED - : TelemetryEvent.DATABASE_ANALYSIS_RECOMMENDATIONS_COLLAPSED, - eventData: { - databaseId: instanceId, - recommendation: id, - } - })} + onToggle={(isOpen) => handleToggle(isOpen, id)} data-testId={`${id}-accordion`} > From e63cf91638a15a141b616435f4a08398a8893b77 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Tue, 13 Dec 2022 10:31:17 +0400 Subject: [PATCH 056/108] #RI-3570 - add big connected clients recommendation --- .../api/src/constants/recommendations.ts | 1 + .../providers/recommendation.provider.spec.ts | 37 +++++++++++++++++++ .../providers/recommendation.provider.ts | 25 +++++++++++++ .../recommendation/recommendation.service.ts | 1 + .../constants/dbAnalysisRecommendations.json | 22 +++++++++++ .../Recommendations.spec.tsx | 15 ++++++++ 6 files changed, 101 insertions(+) diff --git a/redisinsight/api/src/constants/recommendations.ts b/redisinsight/api/src/constants/recommendations.ts index 9aff07a24e..05973a330c 100644 --- a/redisinsight/api/src/constants/recommendations.ts +++ b/redisinsight/api/src/constants/recommendations.ts @@ -3,6 +3,7 @@ export const RECOMMENDATION_NAMES = Object.freeze({ BIG_HASHES: 'bigHashes', BIG_STRINGS: 'bigStrings', BIG_SETS: 'bigSets', + BIG_CONNECTED_CLIENTS: 'bigConnectedClients', USE_SMALLER_KEYS: 'useSmallerKeys', AVOID_LOGICAL_DATABASES: 'avoidLogicalDatabases', COMBINE_SMALL_STRINGS_TO_HASHES: 'combineSmallStringsToHashes', diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts index 02db94a93d..c635e90ec3 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts @@ -18,6 +18,9 @@ const mockRedisKeyspaceInfoResponse_3: string = `# Keyspace\r\ndb0:keys=2,expire const mockRedisConfigResponse = ['name', '512']; +const mockRedisClientsResponse_1: string = '# Clients\r\nconnected_clients:100\r\n'; +const mockRedisClientsResponse_2: string = '# Clients\r\nconnected_clients:101\r\n'; + const mockKeys = [ { name: Buffer.from('name'), type: 'string', length: 10, memory: 10, ttl: -1, @@ -352,4 +355,38 @@ describe('RecommendationProvider', () => { expect(bigSetsRecommendation).toEqual({ name: RECOMMENDATION_NAMES.BIG_SETS }); }); }); + + describe('determineConnectionClientsRecommendation', () => { + it('should not return connectionClients recommendation', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'info' })) + .mockResolvedValue(mockRedisClientsResponse_1); + + const connectionClientsRecommendation = await service + .determineConnectionClientsRecommendation(nodeClient); + expect(connectionClientsRecommendation).toEqual(null); + }); + + it('should return connectionClients recommendation', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'info' })) + .mockResolvedValue(mockRedisClientsResponse_2); + + const connectionClientsRecommendation = await service + .determineConnectionClientsRecommendation(nodeClient); + expect(connectionClientsRecommendation) + .toEqual({ name: RECOMMENDATION_NAMES.BIG_CONNECTED_CLIENTS }); + }); + + it('should not return connectionClients recommendation when info command executed with error', + async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'info' })) + .mockRejectedValue('some error'); + + const connectionClientsRecommendation = await service + .determineConnectionClientsRecommendation(nodeClient); + expect(connectionClientsRecommendation).toEqual(null); + }); + }); }); diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index a94d5cf8bb..f973811457 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -14,6 +14,7 @@ const maxDatabaseTotal = 1_000_000; const maxCompressHashLength = 1000; const maxListLength = 1000; const maxSetLength = 5000; +const maxConnectedClients = 100; const bigStringMemory = 5_000_000; @Injectable() @@ -255,4 +256,28 @@ export class RecommendationProvider { return null; } } + + /** + * Check big connected clients recommendation + * @param redisClient + */ + + async determineConnectionClientsRecommendation( + redisClient: Redis | Cluster, + ): Promise { + try { + const info = convertRedisInfoReplyToObject( + await redisClient.sendCommand( + new Command('info', ['clients'], { replyEncoding: 'utf8' }), + ) as string, + ); + const connectedClients = parseInt(get(info, 'clients.connected_clients'), 10); + + return connectedClients > maxConnectedClients + ? { name: RECOMMENDATION_NAMES.BIG_CONNECTED_CLIENTS } : null; + } catch (err) { + this.logger.error('Can not determine Connection clients recommendation', err); + return null; + } + } } diff --git a/redisinsight/api/src/modules/recommendation/recommendation.service.ts b/redisinsight/api/src/modules/recommendation/recommendation.service.ts index 9b2f0fc1f2..4cee7de155 100644 --- a/redisinsight/api/src/modules/recommendation/recommendation.service.ts +++ b/redisinsight/api/src/modules/recommendation/recommendation.service.ts @@ -47,6 +47,7 @@ export class RecommendationService { await this.recommendationProvider.determineBigStringsRecommendation(keys), await this.recommendationProvider.determineZSetHashtableToZiplistRecommendation(client, keys), await this.recommendationProvider.determineBigSetsRecommendation(keys), + await this.recommendationProvider.determineConnectionClientsRecommendation(client), ])); } } diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index a05244ff1b..3a2abb8225 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -412,5 +412,27 @@ } ], "badges": ["configuration_changes"] + }, + "bigConnectedClients": { + "id": "bigConnectedClients", + "title":"Don't open a new connection for every request / every command", + "content": [ + { + "id": "1", + "type": "paragraph", + "value": "When the value of your connected_clients is high, it usually means that your application is opening and closing a connection for every request it makes. Opening a connection is an expensive operation that adds to both client and server latency." + }, + { + "id": "2", + "type": "spacer", + "value": "l" + }, + { + "id": "2", + "type": "paragraph", + "value": "To rectify this, consult your Redis client’s documentation and configure it to use persistent connections." + } + ], + "badges": ["code_changes"] } } diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx index 50b7d348b6..f6d40e6c2b 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx @@ -227,6 +227,21 @@ describe('Recommendations', () => { expect(screen.queryByTestId('configuration_changes')).toBeInTheDocument() }) + it('should render code_changes badge in bigConnectedClients recommendation', () => { + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: { + recommendations: [{ name: 'bigConnectedClients' }] + } + })) + + render() + + expect(screen.queryByTestId('code_changes')).toBeInTheDocument() + expect(screen.queryByTestId('upgrade')).not.toBeInTheDocument() + expect(screen.queryByTestId('configuration_changes')).not.toBeInTheDocument() + }) + it('should collapse/expand', () => { (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ ...mockdbAnalysisSelector, From 389ef2e963890e6ff27fd984f23451050c907488 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Wed, 14 Dec 2022 13:03:25 +0400 Subject: [PATCH 057/108] #RI-3570 - resolve comments --- redisinsight/api/src/constants/recommendations.ts | 2 +- .../recommendation/providers/recommendation.provider.spec.ts | 2 +- .../recommendation/providers/recommendation.provider.ts | 2 +- redisinsight/ui/src/constants/dbAnalysisRecommendations.json | 4 ++-- .../components/recommendations-view/Recommendations.spec.tsx | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/redisinsight/api/src/constants/recommendations.ts b/redisinsight/api/src/constants/recommendations.ts index 05973a330c..a728ecd6d0 100644 --- a/redisinsight/api/src/constants/recommendations.ts +++ b/redisinsight/api/src/constants/recommendations.ts @@ -3,7 +3,7 @@ export const RECOMMENDATION_NAMES = Object.freeze({ BIG_HASHES: 'bigHashes', BIG_STRINGS: 'bigStrings', BIG_SETS: 'bigSets', - BIG_CONNECTED_CLIENTS: 'bigConnectedClients', + BIG_AMOUNT_OF_CONNECTED_CLIENTS: 'bigAmountOfConnectedClients', USE_SMALLER_KEYS: 'useSmallerKeys', AVOID_LOGICAL_DATABASES: 'avoidLogicalDatabases', COMBINE_SMALL_STRINGS_TO_HASHES: 'combineSmallStringsToHashes', diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts index c635e90ec3..66bdb734b2 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts @@ -375,7 +375,7 @@ describe('RecommendationProvider', () => { const connectionClientsRecommendation = await service .determineConnectionClientsRecommendation(nodeClient); expect(connectionClientsRecommendation) - .toEqual({ name: RECOMMENDATION_NAMES.BIG_CONNECTED_CLIENTS }); + .toEqual({ name: RECOMMENDATION_NAMES.BIG_AMOUNT_OF_CONNECTED_CLIENTS }); }); it('should not return connectionClients recommendation when info command executed with error', diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index f973811457..8cb761ef5c 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -274,7 +274,7 @@ export class RecommendationProvider { const connectedClients = parseInt(get(info, 'clients.connected_clients'), 10); return connectedClients > maxConnectedClients - ? { name: RECOMMENDATION_NAMES.BIG_CONNECTED_CLIENTS } : null; + ? { name: RECOMMENDATION_NAMES.BIG_AMOUNT_OF_CONNECTED_CLIENTS } : null; } catch (err) { this.logger.error('Can not determine Connection clients recommendation', err); return null; diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index 3a2abb8225..00cd5887d3 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -413,8 +413,8 @@ ], "badges": ["configuration_changes"] }, - "bigConnectedClients": { - "id": "bigConnectedClients", + "bigAmountOfConnectedClients": { + "id": "bigAmountOfConnectedClients", "title":"Don't open a new connection for every request / every command", "content": [ { diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx index 4965dbfe04..f984bf9791 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx @@ -234,11 +234,11 @@ describe('Recommendations', () => { expect(screen.queryByTestId('configuration_changes')).toBeInTheDocument() }) - it('should render code_changes badge in bigConnectedClients recommendation', () => { + it('should render code_changes badge in bigAmountOfConnectedClients recommendation', () => { (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ ...mockdbAnalysisSelector, data: { - recommendations: [{ name: 'bigConnectedClients' }] + recommendations: [{ name: 'bigAmountOfConnectedClients' }] } })) From f7c9831ceb9a821dcedc91640260cdd9cd93c706 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Wed, 14 Dec 2022 19:52:32 +0400 Subject: [PATCH 058/108] #RI-3574 - add set password recommendation --- redisinsight/api/src/__mocks__/errors.ts | 6 ++ .../api/src/constants/recommendations.ts | 1 + .../providers/recommendation.provider.spec.ts | 66 +++++++++++++++++++ .../providers/recommendation.provider.ts | 39 +++++++++++ .../recommendation/recommendation.service.ts | 1 + .../constants/dbAnalysisRecommendations.json | 33 ++++++++++ .../Recommendations.spec.tsx | 15 +++++ 7 files changed, 161 insertions(+) diff --git a/redisinsight/api/src/__mocks__/errors.ts b/redisinsight/api/src/__mocks__/errors.ts index bd1fe1f44c..040c707e0b 100644 --- a/redisinsight/api/src/__mocks__/errors.ts +++ b/redisinsight/api/src/__mocks__/errors.ts @@ -6,6 +6,12 @@ export const mockRedisNoAuthError: ReplyError = { message: 'NOAUTH authentication is required', }; +export const mockRedisNoPasswordError: ReplyError = { + name: 'ReplyError', + command: 'AUTH', + message: 'ERR Client sent AUTH, but no password is set', +}; + export const mockRedisNoPermError: ReplyError = { name: 'ReplyError', command: 'GET', diff --git a/redisinsight/api/src/constants/recommendations.ts b/redisinsight/api/src/constants/recommendations.ts index a728ecd6d0..557b0c69f8 100644 --- a/redisinsight/api/src/constants/recommendations.ts +++ b/redisinsight/api/src/constants/recommendations.ts @@ -12,4 +12,5 @@ export const RECOMMENDATION_NAMES = Object.freeze({ COMPRESS_HASH_FIELD_NAMES: 'compressHashFieldNames', COMPRESSION_FOR_LIST: 'compressionForList', ZSET_HASHTABLE_TO_ZIPLIST: 'zSetHashtableToZiplist', + SET_PASSWORD: 'setPassword', }); diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts index 66bdb734b2..6a3aa37794 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts @@ -1,6 +1,7 @@ import IORedis from 'ioredis'; import { when } from 'jest-when'; import { RECOMMENDATION_NAMES } from 'src/constants'; +import { mockRedisNoAuthError, mockRedisNoPasswordError } from 'src/__mocks__'; import { RecommendationProvider } from 'src/modules/recommendation/providers/recommendation.provider'; const nodeClient = Object.create(IORedis.prototype); @@ -21,6 +22,16 @@ const mockRedisConfigResponse = ['name', '512']; const mockRedisClientsResponse_1: string = '# Clients\r\nconnected_clients:100\r\n'; const mockRedisClientsResponse_2: string = '# Clients\r\nconnected_clients:101\r\n'; +const mockRedisAclListResponse_1: string[] = [ + 'user { expect(connectionClientsRecommendation).toEqual(null); }); }); + + describe('determineSetPasswordRecommendation', () => { + it('should not return setPassword recommendation', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'acl' })) + .mockResolvedValue(mockRedisAclListResponse_1); + + const setPasswordRecommendation = await service + .determineSetPasswordRecommendation(nodeClient); + expect(setPasswordRecommendation).toEqual(null); + }); + + it('should return setPassword recommendation', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'acl' })) + .mockResolvedValue(mockRedisAclListResponse_2); + + const setPasswordRecommendation = await service + .determineSetPasswordRecommendation(nodeClient); + expect(setPasswordRecommendation).toEqual({ name: RECOMMENDATION_NAMES.SET_PASSWORD }); + }); + + it('should not return setPassword recommendation when acl command executed with error', + async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'acl' })) + .mockRejectedValue('some error'); + + const setPasswordRecommendation = await service + .determineSetPasswordRecommendation(nodeClient); + expect(setPasswordRecommendation).toEqual(null); + }); + + it('should not return setPassword recommendation when acl command executed with error', + async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'auth' })) + .mockRejectedValue(mockRedisNoAuthError); + + const setPasswordRecommendation = await service + .determineSetPasswordRecommendation(nodeClient); + expect(setPasswordRecommendation).toEqual(null); + }); + + it('should return setPassword recommendation when acl command executed with no password error', + async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'auth' })) + .mockRejectedValue(mockRedisNoPasswordError); + + const setPasswordRecommendation = await service + .determineSetPasswordRecommendation(nodeClient); + expect(setPasswordRecommendation).toEqual({ name: RECOMMENDATION_NAMES.SET_PASSWORD }); + }); + }); }); diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index 8cb761ef5c..b2feb189c3 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -280,4 +280,43 @@ export class RecommendationProvider { return null; } } + + /** + * Check set password recommendation + * @param redisClient + */ + + async determineSetPasswordRecommendation( + redisClient: Redis | Cluster, + ): Promise { + if (await this.checkAuth(redisClient)) { + return { name: RECOMMENDATION_NAMES.SET_PASSWORD }; + } + + try { + const users = await redisClient.sendCommand( + new Command('acl', ['list'], { replyEncoding: 'utf8' }), + ) as string[]; + + const nopassUser = users.some((user) => user.split(' ')[3] === 'nopass'); + + return nopassUser ? { name: RECOMMENDATION_NAMES.SET_PASSWORD } : null; + } catch (err) { + this.logger.error('Can not determine set password recommendation', err); + return null; + } + } + + public async checkAuth(redisClient: Redis | Cluster): Promise { + try { + await redisClient.sendCommand( + new Command('auth', ['pass']), + ); + } catch (err) { + if (err.message.includes('Client sent AUTH, but no password is set')) { + return true; + } + } + return false; + } } diff --git a/redisinsight/api/src/modules/recommendation/recommendation.service.ts b/redisinsight/api/src/modules/recommendation/recommendation.service.ts index 4cee7de155..8c0a0ea2a1 100644 --- a/redisinsight/api/src/modules/recommendation/recommendation.service.ts +++ b/redisinsight/api/src/modules/recommendation/recommendation.service.ts @@ -48,6 +48,7 @@ export class RecommendationService { await this.recommendationProvider.determineZSetHashtableToZiplistRecommendation(client, keys), await this.recommendationProvider.determineBigSetsRecommendation(keys), await this.recommendationProvider.determineConnectionClientsRecommendation(client), + await this.recommendationProvider.determineSetPasswordRecommendation(client), ])); } } diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index 00cd5887d3..589f715b93 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -434,5 +434,38 @@ } ], "badges": ["code_changes"] + }, + "setPassword": { + "id": "setPassword", + "title":"Set the password", + "content": [ + { + "id": "1", + "type": "span", + "value": "Protect your database by setting a password and using the " + }, + { + "id": "2", + "type": "link", + "value": { + "href": "https://redis.io/commands/auth/", + "name": "AUTH" + } + }, + { + "id": "3", + "type": "span", + "value": " command to authenticate the connection. " + }, + { + "id": "4", + "type": "link", + "value": { + "href": "https://redis.io/docs/management/security/", + "name": "Read more" + } + } + ], + "badges": ["configuration_changes"] } } diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx index f984bf9791..585d160ce8 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx @@ -249,6 +249,21 @@ describe('Recommendations', () => { expect(screen.queryByTestId('configuration_changes')).not.toBeInTheDocument() }) + it('should render configuration_changes badge in setPassword recommendation', () => { + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: { + recommendations: [{ name: 'setPassword' }] + } + })) + + render() + + expect(screen.queryByTestId('code_changes')).not.toBeInTheDocument() + expect(screen.queryByTestId('upgrade')).not.toBeInTheDocument() + expect(screen.queryByTestId('configuration_changes')).toBeInTheDocument() + }) + it('should collapse/expand and sent proper telemetry event', () => { (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ ...mockdbAnalysisSelector, From e90956b7145fe23b80dd09ada5fab41e19a03e92 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Wed, 14 Dec 2022 19:55:34 +0400 Subject: [PATCH 059/108] #RI-3574 - change public to private --- .../modules/recommendation/providers/recommendation.provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index b2feb189c3..3ca1cf8a8d 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -307,7 +307,7 @@ export class RecommendationProvider { } } - public async checkAuth(redisClient: Redis | Cluster): Promise { + private async checkAuth(redisClient: Redis | Cluster): Promise { try { await redisClient.sendCommand( new Command('auth', ['pass']), From 1b4154edb33def69d74ae4172423da24cca6bb76 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 15 Dec 2022 10:55:17 +0400 Subject: [PATCH 060/108] #RI-3574 - fix IT --- redisinsight/api/test/api/.mocharc.yml | 2 +- .../POST-databases-id-analysis.test.ts | 89 +++++++++++-------- redisinsight/api/test/helpers/constants.ts | 4 + 3 files changed, 59 insertions(+), 36 deletions(-) diff --git a/redisinsight/api/test/api/.mocharc.yml b/redisinsight/api/test/api/.mocharc.yml index 6185c06241..52bb8bfd9b 100644 --- a/redisinsight/api/test/api/.mocharc.yml +++ b/redisinsight/api/test/api/.mocharc.yml @@ -1,5 +1,5 @@ spec: - - 'test/**/*.test.ts' + - 'test/api/database-analysis/**/*.test.ts' require: 'test/api/api.deps.init.ts' timeout: 60000 exit: true diff --git a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts index 023d1cf006..c2c00cfff5 100644 --- a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts +++ b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts @@ -150,12 +150,43 @@ describe('POST /databases/:instanceId/analysis', () => { ].map(mainCheckFn); describe('recommendations', () => { - requirements('!rte.bigData'); + requirements('!rte.bigData', '!rte.pass'); beforeEach(async () => { await rte.data.truncate(); }); + describe('useSmallerKeys recommendation', () => { + // generate 1M keys take a lot of time + requirements('!rte.type=CLUSTER'); + + [ + { + name: 'Should create new database analysis with useSmallerKeys recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + await rte.data.truncate(); + const KEYS_NUMBER = 1_000_006; + await rte.data.generateNKeys(KEYS_NUMBER, false); + }, + checkFn: async ({ body }) => { + expect(body.recommendations).to.deep.eq([ + constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, + constants.TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION, + constants.TEST_SET_PASSWORD_RECOMMENDATION, + ]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } + }, + ].map(mainCheckFn); + }); + [ { name: 'Should create new database analysis with bigHashes recommendation', @@ -173,6 +204,7 @@ describe('POST /databases/:instanceId/analysis', () => { constants.TEST_BIG_HASHES_DATABASE_ANALYSIS_RECOMMENDATION, constants.TEST_HASH_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, constants.TEST_COMPRESS_HASH_FIELD_NAMES_RECOMMENDATION, + constants.TEST_SET_PASSWORD_RECOMMENDATION, ]); }, after: async () => { @@ -191,7 +223,10 @@ describe('POST /databases/:instanceId/analysis', () => { await rte.data.generateHugeNumberOfMembersForSetKey(NUMBERS_OF_SET_MEMBERS, true); }, checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([constants.TEST_INCREASE_SET_MAX_INTSET_ENTRIES_RECOMMENDATION]); + expect(body.recommendations).to.deep.eq([ + constants.TEST_INCREASE_SET_MAX_INTSET_ENTRIES_RECOMMENDATION, + constants.TEST_SET_PASSWORD_RECOMMENDATION, + ]); }, after: async () => { expect(await repository.count()).to.eq(5); @@ -208,7 +243,10 @@ describe('POST /databases/:instanceId/analysis', () => { await rte.data.generateStrings(true); }, checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([constants.TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION]); + expect(body.recommendations).to.deep.eq([ + constants.TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION, + constants.TEST_SET_PASSWORD_RECOMMENDATION, + ]); }, after: async () => { expect(await repository.count()).to.eq(5); @@ -228,6 +266,7 @@ describe('POST /databases/:instanceId/analysis', () => { checkFn: async ({ body }) => { expect(body.recommendations).to.deep.eq([ constants.TEST_HASH_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, + constants.TEST_SET_PASSWORD_RECOMMENDATION, ]); }, after: async () => { @@ -249,6 +288,7 @@ describe('POST /databases/:instanceId/analysis', () => { expect(body.recommendations).to.deep.eq([ constants.TEST_HASH_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, constants.TEST_COMPRESS_HASH_FIELD_NAMES_RECOMMENDATION, + constants.TEST_SET_PASSWORD_RECOMMENDATION, ]); }, after: async () => { @@ -269,6 +309,7 @@ describe('POST /databases/:instanceId/analysis', () => { checkFn: async ({ body }) => { expect(body.recommendations).to.deep.eq([ constants.TEST_COMPRESSION_FOR_LIST_RECOMMENDATION, + constants.TEST_SET_PASSWORD_RECOMMENDATION, ]); }, after: async () => { @@ -289,7 +330,10 @@ describe('POST /databases/:instanceId/analysis', () => { await rte.data.sendCommand('set', [constants.TEST_STRING_KEY_1, bigStringValue]); }, checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([constants.TEST_BIG_STRINGS_RECOMMENDATION]); + expect(body.recommendations).to.deep.eq([ + constants.TEST_BIG_STRINGS_RECOMMENDATION, + constants.TEST_SET_PASSWORD_RECOMMENDATION, + ]); }, after: async () => { expect(await repository.count()).to.eq(5); @@ -309,6 +353,7 @@ describe('POST /databases/:instanceId/analysis', () => { checkFn: async ({ body }) => { expect(body.recommendations).to.deep.eq([ constants.TEST_ZSET_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, + constants.TEST_SET_PASSWORD_RECOMMENDATION, ]); }, after: async () => { @@ -331,6 +376,7 @@ describe('POST /databases/:instanceId/analysis', () => { // by default max_intset_entries = 512 constants.TEST_INCREASE_SET_MAX_INTSET_ENTRIES_RECOMMENDATION, constants.TEST_BIG_SETS_RECOMMENDATION, + constants.TEST_SET_PASSWORD_RECOMMENDATION, ]); }, after: async () => { @@ -348,7 +394,10 @@ describe('POST /databases/:instanceId/analysis', () => { await rte.data.generateNCachedScripts(11, true); }, checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([constants.TEST_LUA_DATABASE_ANALYSIS_RECOMMENDATION]); + expect(body.recommendations).to.deep.eq([ + constants.TEST_LUA_DATABASE_ANALYSIS_RECOMMENDATION, + constants.TEST_SET_PASSWORD_RECOMMENDATION, + ]); }, after: async () => { await rte.data.sendCommand('script', ['flush']); @@ -356,35 +405,5 @@ describe('POST /databases/:instanceId/analysis', () => { } }, ].map(mainCheckFn); - - describe('useSmallerKeys recommendation', () => { - // generate 1M keys take a lot of time - requirements('!rte.type=CLUSTER'); - - [ - { - name: 'Should create new database analysis with useSmallerKeys recommendation', - data: { - delimiter: '-', - }, - statusCode: 201, - responseSchema, - before: async () => { - await rte.data.truncate(); - const KEYS_NUMBER = 1_000_006; - await rte.data.generateNKeys(KEYS_NUMBER, false); - }, - checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([ - constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, - constants.TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION, - ]); - }, - after: async () => { - expect(await repository.count()).to.eq(5); - } - }, - ].map(mainCheckFn); - }); }); }); diff --git a/redisinsight/api/test/helpers/constants.ts b/redisinsight/api/test/helpers/constants.ts index 60335cfec2..87087f24e6 100644 --- a/redisinsight/api/test/helpers/constants.ts +++ b/redisinsight/api/test/helpers/constants.ts @@ -488,5 +488,9 @@ export const constants = { name: RECOMMENDATION_NAMES.BIG_SETS, }, + TEST_SET_PASSWORD_RECOMMENDATION: { + name: RECOMMENDATION_NAMES.SET_PASSWORD, + }, + // etc... } From ee095e9eb4267856193307431fa911e1b64c5dc0 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 15 Dec 2022 11:37:46 +0400 Subject: [PATCH 061/108] #RI-3574 - fix IT --- redisinsight/api/test/api/.mocharc.yml | 2 +- .../POST-databases-id-analysis.test.ts | 56 +++++++++++-------- 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/redisinsight/api/test/api/.mocharc.yml b/redisinsight/api/test/api/.mocharc.yml index 52bb8bfd9b..6185c06241 100644 --- a/redisinsight/api/test/api/.mocharc.yml +++ b/redisinsight/api/test/api/.mocharc.yml @@ -1,5 +1,5 @@ spec: - - 'test/api/database-analysis/**/*.test.ts' + - 'test/**/*.test.ts' require: 'test/api/api.deps.init.ts' timeout: 60000 exit: true diff --git a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts index c2c00cfff5..f57124e6b2 100644 --- a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts +++ b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts @@ -150,7 +150,7 @@ describe('POST /databases/:instanceId/analysis', () => { ].map(mainCheckFn); describe('recommendations', () => { - requirements('!rte.bigData', '!rte.pass'); + requirements('!rte.bigData'); beforeEach(async () => { await rte.data.truncate(); @@ -174,9 +174,31 @@ describe('POST /databases/:instanceId/analysis', () => { await rte.data.generateNKeys(KEYS_NUMBER, false); }, checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([ + expect(body.recommendations).to.include.deep.members([ constants.TEST_SMALLER_KEYS_DATABASE_ANALYSIS_RECOMMENDATION, constants.TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION, + ]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } + }, + ].map(mainCheckFn); + }); + + + describe('setPassword recommendation', () => { + requirements('!rte.pass'); + [ + { + name: 'Should create new database analysis with useSmallerKeys recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + checkFn: async ({ body }) => { + expect(body.recommendations).to.include.deep.members([ constants.TEST_SET_PASSWORD_RECOMMENDATION, ]); }, @@ -200,11 +222,10 @@ describe('POST /databases/:instanceId/analysis', () => { await rte.data.generateHugeNumberOfFieldsForHashKey(NUMBERS_OF_HASH_FIELDS, true); }, checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([ + expect(body.recommendations).to.include.deep.members([ constants.TEST_BIG_HASHES_DATABASE_ANALYSIS_RECOMMENDATION, constants.TEST_HASH_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, constants.TEST_COMPRESS_HASH_FIELD_NAMES_RECOMMENDATION, - constants.TEST_SET_PASSWORD_RECOMMENDATION, ]); }, after: async () => { @@ -223,9 +244,8 @@ describe('POST /databases/:instanceId/analysis', () => { await rte.data.generateHugeNumberOfMembersForSetKey(NUMBERS_OF_SET_MEMBERS, true); }, checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([ + expect(body.recommendations).to.include.deep.members([ constants.TEST_INCREASE_SET_MAX_INTSET_ENTRIES_RECOMMENDATION, - constants.TEST_SET_PASSWORD_RECOMMENDATION, ]); }, after: async () => { @@ -243,9 +263,8 @@ describe('POST /databases/:instanceId/analysis', () => { await rte.data.generateStrings(true); }, checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([ + expect(body.recommendations).to.include.deep.members([ constants.TEST_COMBINE_SMALL_STRING_TO_HASHES_RECOMMENDATION, - constants.TEST_SET_PASSWORD_RECOMMENDATION, ]); }, after: async () => { @@ -264,9 +283,8 @@ describe('POST /databases/:instanceId/analysis', () => { await rte.data.generateHugeNumberOfFieldsForHashKey(NUMBERS_OF_HASH_FIELDS, true); }, checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([ + expect(body.recommendations).to.include.deep.members([ constants.TEST_HASH_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, - constants.TEST_SET_PASSWORD_RECOMMENDATION, ]); }, after: async () => { @@ -285,10 +303,9 @@ describe('POST /databases/:instanceId/analysis', () => { await rte.data.generateHugeNumberOfFieldsForHashKey(NUMBERS_OF_HASH_FIELDS, true); }, checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([ + expect(body.recommendations).to.include.deep.members([ constants.TEST_HASH_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, constants.TEST_COMPRESS_HASH_FIELD_NAMES_RECOMMENDATION, - constants.TEST_SET_PASSWORD_RECOMMENDATION, ]); }, after: async () => { @@ -307,9 +324,8 @@ describe('POST /databases/:instanceId/analysis', () => { await rte.data.generateHugeElementsForListKey(NUMBERS_OF_LIST_ELEMENTS, true); }, checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([ + expect(body.recommendations).to.include.deep.members([ constants.TEST_COMPRESSION_FOR_LIST_RECOMMENDATION, - constants.TEST_SET_PASSWORD_RECOMMENDATION, ]); }, after: async () => { @@ -330,9 +346,8 @@ describe('POST /databases/:instanceId/analysis', () => { await rte.data.sendCommand('set', [constants.TEST_STRING_KEY_1, bigStringValue]); }, checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([ + expect(body.recommendations).to.include.deep.members([ constants.TEST_BIG_STRINGS_RECOMMENDATION, - constants.TEST_SET_PASSWORD_RECOMMENDATION, ]); }, after: async () => { @@ -351,9 +366,8 @@ describe('POST /databases/:instanceId/analysis', () => { await rte.data.generateHugeMembersForSortedListKey(NUMBERS_OF_ZSET_MEMBERS, true); }, checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([ + expect(body.recommendations).to.include.deep.members([ constants.TEST_ZSET_HASHTABLE_TO_ZIPLIST_RECOMMENDATION, - constants.TEST_SET_PASSWORD_RECOMMENDATION, ]); }, after: async () => { @@ -372,11 +386,10 @@ describe('POST /databases/:instanceId/analysis', () => { await rte.data.generateHugeNumberOfMembersForSetKey(NUMBERS_OF_SET_MEMBERS, true); }, checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([ + expect(body.recommendations).to.include.deep.members([ // by default max_intset_entries = 512 constants.TEST_INCREASE_SET_MAX_INTSET_ENTRIES_RECOMMENDATION, constants.TEST_BIG_SETS_RECOMMENDATION, - constants.TEST_SET_PASSWORD_RECOMMENDATION, ]); }, after: async () => { @@ -394,9 +407,8 @@ describe('POST /databases/:instanceId/analysis', () => { await rte.data.generateNCachedScripts(11, true); }, checkFn: async ({ body }) => { - expect(body.recommendations).to.deep.eq([ + expect(body.recommendations).to.include.deep.members([ constants.TEST_LUA_DATABASE_ANALYSIS_RECOMMENDATION, - constants.TEST_SET_PASSWORD_RECOMMENDATION, ]); }, after: async () => { From 5cb805a3fc3bbe7af0f044f5a6397d166ae5243c Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Tue, 20 Dec 2022 17:09:05 +0400 Subject: [PATCH 062/108] #RI-3899 - change badges icon location --- .../constants/dbAnalysisRecommendations.json | 12 ++-- .../Recommendations.spec.tsx | 26 +++++++ .../recommendations-view/Recommendations.tsx | 23 ++++-- .../recommendations-view/styles.module.scss | 70 +++++++++++-------- .../components/recommendations-view/utils.tsx | 43 +++++++++--- 5 files changed, 120 insertions(+), 54 deletions(-) diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index 589f715b93..f9269b8562 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -314,14 +314,14 @@ ], [ { - "id": "1", + "id": "2", "type": "span", "value": "Check if an observation already appeared in the stream" } ], [ { - "id": "1", + "id": "3", "type": "span", "value": "Find the fraction or the number of observations in the stream that are smaller or larger than a given value" } @@ -385,12 +385,12 @@ ] }, { - "id": "5", + "id": "7", "type": "span", "value": "Bloom filter and cuckoo filter require " }, { - "id": "6", + "id": "8", "type": "link", "value": { "href": "https://redis.com/modules/redis-bloom/", @@ -398,12 +398,12 @@ } }, { - "id": "7", + "id": "9", "type": "span", "value": ". " }, { - "id": "8", + "id": "10", "type": "link", "value": { "href": "https://docs.redis.com/latest/ri/memory-optimizations/", diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx index 585d160ce8..f386fd446e 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx @@ -304,4 +304,30 @@ describe('Recommendations', () => { }) sendEventTelemetry.mockRestore() }) + + it('should not render badges legend', () => { + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: { + recommendations: [] + } + })) + + render() + + expect(screen.queryByTestId('badges-legend')).not.toBeInTheDocument() + }) + + it('should render badges legend', () => { + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: { + recommendations: [{ name: 'luaScript' }] + } + })) + + render() + + expect(screen.queryByTestId('badges-legend')).toBeInTheDocument() + }) }) diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx index 7b42dd9686..c8dec8bf53 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx @@ -6,12 +6,14 @@ import { EuiAccordion, EuiPanel, EuiText, + EuiFlexGroup, + EuiFlexItem, } from '@elastic/eui' import { dbAnalysisSelector } from 'uiSrc/slices/analytics/dbAnalysis' import recommendationsContent from 'uiSrc/constants/dbAnalysisRecommendations.json' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import { renderContent, renderBadges } from './utils' +import { renderContent, renderBadges, renderBadgesLegend } from './utils' import styles from './styles.module.scss' const Recommendations = () => { @@ -46,28 +48,37 @@ const Recommendations = () => { return (
+
+ {renderBadgesLegend()} +
{recommendations.map(({ name }) => { const { id = '', title = '', content = '', badges = [] } = recommendationsContent[name] + + const buttonContent = ( + + {title} + + {renderBadges(badges)} + + + ) return (
handleToggle(isOpen, id)} - data-testId={`${id}-accordion`} + data-testid={`${id}-accordion`} > {renderContent(content)} -
- {renderBadges(badges)} -
) })} diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss index 481f546141..a1b6df58b5 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss @@ -8,6 +8,40 @@ overflow-x: hidden; max-height: 100%; padding-top: 30px; + + .badgesLegend { + margin: 0 22px 14px 0 !important; + + .badge { + margin: 0 0 0 24px; + } + + .badgeIcon { + margin-right: 14px; + } + } + + .badgeIcon { + margin-right: 24px; + fill: var(--badgeIconColor); + } + + .badgeWrapper { + display: flex; + align-items: center; + } + + .accordionButton :global(.euiFlexItem) { + margin: 0; + } + + :global(.euiAccordion__buttonReverse .euiAccordion__iconWrapper) { + margin-left: 0; + } + + :global(.euiFlexGroup--gutterLarge) { + margin: 0; + } } .container { @@ -31,7 +65,7 @@ border: 1px solid var(--recommendationBorderColor); background-color: var(--euiColorLightestShade); margin-bottom: 6px; - padding: 18px; + padding: 30px 18px; ul { list-style: initial; @@ -42,12 +76,8 @@ } } - .accordion { - margin-bottom: 18px; - } - .accordionContent { - padding: 18px 0 17px !important; + padding: 18px 0 0 !important; } :global(.euiAccordion__triggerWrapper) { @@ -62,35 +92,13 @@ border-bottom: none; } - :global(.euiAccordion .euiAccordion__triggerWrapper) { - border-bottom: 1px solid var(--separatorColor); + :global(.euiIEFlexWrapFix) { + display: block; + width: 100%; } .accordionBtn { font: normal normal 500 16px/19px Graphik, sans-serif; - padding-bottom: 22px; - } - - .badgesContainer { - .badge { - width: 180px; - margin: 14px 18px; - - &:last-child { - padding-right: 48px; - align-items: flex-end; - } - - .badgeIcon { - margin-right: 14px; - fill: var(--badgeIconColor); - } - - .badgeWrapper { - display: flex; - align-items: center; - } - } } .text { diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx index ec9704942f..d1d5fe20b4 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx @@ -1,6 +1,7 @@ import React from 'react' import { EuiTextColor, + EuiToolTip, EuiFlexGroup, EuiFlexItem, EuiLink, @@ -14,6 +15,7 @@ import { ReactComponent as UpgradeIcon } from 'uiSrc/assets/img/upgrade.svg' import styles from './styles.module.scss' interface IContentElement { + id: string type: string value: any[] } @@ -25,33 +27,52 @@ const badgesContent = [ ] export const renderBadges = (badges: string[]) => ( - - {badgesContent.map(({ id, icon, name }) => (badges.indexOf(id) === -1 - ? + + {badgesContent.map(({ id, name, icon }) => (badges.indexOf(id) === -1 + ? null : (
- {icon} - {name} + + {icon} +
)))}
) -const renderContentElement = ({ type, value }: IContentElement) => { +export const renderBadgesLegend = () => ( + + {badgesContent.map(({ id, icon, name }) => ( + +
+ {icon} + {name} +
+
+ ))} +
+) + +const renderContentElement = ({ id, type, value }: IContentElement) => { switch (type) { case 'paragraph': - return {value} + return {value} case 'span': - return {value} + return {value} case 'link': - return {value.name} + return {value.name} case 'spacer': - return + return case 'list': return ( -
    +
      {value.map((listElement: IContentElement[]) => (
    • {renderContent(listElement)}
    • ))} From 09205a5a0531ca12585f191f42f11dde24fc8e3b Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Tue, 20 Dec 2022 17:24:46 +0400 Subject: [PATCH 063/108] #RI-3899 - resolve comments --- .../recommendations-view/styles.module.scss | 6 +++- .../components/recommendations-view/utils.tsx | 30 +++++++++---------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss index a1b6df58b5..46ec75adb3 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss @@ -11,6 +11,10 @@ .badgesLegend { margin: 0 22px 14px 0 !important; + + .badgeWrapper { + margin-right: 0; + } .badge { margin: 0 0 0 24px; @@ -22,13 +26,13 @@ } .badgeIcon { - margin-right: 24px; fill: var(--badgeIconColor); } .badgeWrapper { display: flex; align-items: center; + margin-right: 24px; } .accordionButton :global(.euiFlexItem) { diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx index d1d5fe20b4..67e47706bb 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx @@ -28,22 +28,20 @@ const badgesContent = [ export const renderBadges = (badges: string[]) => ( - {badgesContent.map(({ id, name, icon }) => (badges.indexOf(id) === -1 - ? null - : ( - -
      - - {icon} - -
      -
      - )))} + {badgesContent.map(({ id, name, icon }) => (badges.indexOf(id) > -1 && ( + +
      + + {icon} + +
      +
      + )))}
      ) From b654af733719ffc748737bf044a6b5830922e038 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Fri, 23 Dec 2022 11:54:12 +0400 Subject: [PATCH 064/108] #RI-3942 - add rts recommendation --- .../api/src/constants/recommendations.ts | 1 + redisinsight/api/src/constants/regex.ts | 1 + .../providers/recommendation.provider.spec.ts | 74 ++++++++++++++++++- .../providers/recommendation.provider.ts | 45 ++++++++++- .../recommendation/recommendation.service.ts | 1 + .../POST-databases-id-analysis.test.ts | 21 +++++- redisinsight/api/test/helpers/constants.ts | 7 ++ 7 files changed, 146 insertions(+), 4 deletions(-) diff --git a/redisinsight/api/src/constants/recommendations.ts b/redisinsight/api/src/constants/recommendations.ts index 557b0c69f8..640cb330a9 100644 --- a/redisinsight/api/src/constants/recommendations.ts +++ b/redisinsight/api/src/constants/recommendations.ts @@ -13,4 +13,5 @@ export const RECOMMENDATION_NAMES = Object.freeze({ COMPRESSION_FOR_LIST: 'compressionForList', ZSET_HASHTABLE_TO_ZIPLIST: 'zSetHashtableToZiplist', SET_PASSWORD: 'setPassword', + RTS: 'RTS', }); diff --git a/redisinsight/api/src/constants/regex.ts b/redisinsight/api/src/constants/regex.ts index ef68cca6cc..8506d70ccd 100644 --- a/redisinsight/api/src/constants/regex.ts +++ b/redisinsight/api/src/constants/regex.ts @@ -3,3 +3,4 @@ export const IS_INTEGER_NUMBER_REGEX = /^\d+$/; export const IS_NON_PRINTABLE_ASCII_CHARACTER = /[^ -~\u0007\b\t\n\r]/; export const IP_ADDRESS_REGEX = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; export const PRIVATE_IP_ADDRESS_REGEX = /(^127\.)|(^10\.)|(^172\.1[6-9]\.)|(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^192\.168\.)/; +export const IS_TIMESTAMP = /^\d{10,}$/; diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts index 6a3aa37794..f36d2a019e 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts @@ -1,5 +1,5 @@ import IORedis from 'ioredis'; -import { when } from 'jest-when'; +import { when, resetAllWhenMocks } from 'jest-when'; import { RECOMMENDATION_NAMES } from 'src/constants'; import { mockRedisNoAuthError, mockRedisNoPasswordError } from 'src/__mocks__'; import { RecommendationProvider } from 'src/modules/recommendation/providers/recommendation.provider'; @@ -26,12 +26,20 @@ const mockRedisAclListResponse_1: string[] = [ 'user { const service = new RecommendationProvider(); @@ -455,4 +469,60 @@ describe('RecommendationProvider', () => { expect(setPasswordRecommendation).toEqual({ name: RECOMMENDATION_NAMES.SET_PASSWORD }); }); }); + + describe('determineRTSRecommendation', () => { + it('should not return RTS recommendation', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'zscan' })) + .mockResolvedValue(mockZScanResponse_1); + + const RTSRecommendation = await service + .determineRTSRecommendation(nodeClient, mockKeys); + expect(RTSRecommendation).toEqual(null); + }); + + it('should return RTS recommendation', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'zscan' })) + .mockResolvedValueOnce(mockZScanResponse_1); + + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'zscan' })) + .mockResolvedValue(mockZScanResponse_2); + + const RTSRecommendation = await service + .determineRTSRecommendation(nodeClient, mockSortedSets); + expect(RTSRecommendation).toEqual({ name: RECOMMENDATION_NAMES.RTS }); + }); + + it('should not return RTS recommendation when only 101 sorted set contain timestamp', async () => { + let counter = 0; + while (counter <= 100) { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'zscan' })) + .mockResolvedValueOnce(mockZScanResponse_1); + counter += 1; + } + + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'zscan' })) + .mockResolvedValueOnce(mockZScanResponse_2); + + const RTSRecommendation = await service + .determineRTSRecommendation(nodeClient, mockSortedSets); + expect(RTSRecommendation).toEqual(null); + }); + + it('should not return RTS recommendation when zscan command executed with error', + async () => { + resetAllWhenMocks(); + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'zscan' })) + .mockRejectedValue('some error'); + + const RTSRecommendation = await service + .determineRTSRecommendation(nodeClient, mockKeys); + expect(RTSRecommendation).toEqual(null); + }); + }); }); diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index 3ca1cf8a8d..6de9d275ce 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -2,7 +2,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { Redis, Cluster, Command } from 'ioredis'; import { get } from 'lodash'; import { convertRedisInfoReplyToObject, convertBulkStringsToObject } from 'src/utils'; -import { RECOMMENDATION_NAMES } from 'src/constants'; +import { RECOMMENDATION_NAMES, IS_TIMESTAMP } from 'src/constants'; import { RedisDataType } from 'src/modules/browser/dto'; import { Recommendation } from 'src/modules/database-analysis/models/recommendation'; import { Key } from 'src/modules/database-analysis/models'; @@ -16,6 +16,7 @@ const maxListLength = 1000; const maxSetLength = 5000; const maxConnectedClients = 100; const bigStringMemory = 5_000_000; +const sortedSetCountForCheck = 100; @Injectable() export class RecommendationProvider { @@ -307,6 +308,48 @@ export class RecommendationProvider { } } + /** + * Check set password recommendation + * @param redisClient + * @param keys + */ + + async determineRTSRecommendation( + redisClient: Redis | Cluster, + keys: Key[], + ): Promise { + try { + let processedKeysNumber = 0; + let isTimeSeries = false; + let sortedSetNumber = 0; + while ( + processedKeysNumber < keys.length + && !isTimeSeries + && sortedSetNumber <= sortedSetCountForCheck + ) { + if (keys[processedKeysNumber].type !== RedisDataType.ZSet) { + processedKeysNumber += 1; + } else { + const [, membersArray] = await redisClient.sendCommand( + // get first member-score pair + new Command('zscan', [keys[processedKeysNumber].name, '0', 'COUNT', 2], { replyEncoding: 'utf8' }), + ) as string[]; + // check is pair member-score is timestamp + if (IS_TIMESTAMP.test(membersArray[0]) && IS_TIMESTAMP.test(membersArray[1])) { + isTimeSeries = true; + } + processedKeysNumber += 1; + sortedSetNumber += 1; + } + } + + return isTimeSeries ? { name: RECOMMENDATION_NAMES.RTS } : null; + } catch (err) { + this.logger.error('Can not determine RTS recommendation', err); + return null; + } + } + private async checkAuth(redisClient: Redis | Cluster): Promise { try { await redisClient.sendCommand( diff --git a/redisinsight/api/src/modules/recommendation/recommendation.service.ts b/redisinsight/api/src/modules/recommendation/recommendation.service.ts index 8c0a0ea2a1..ea8351557a 100644 --- a/redisinsight/api/src/modules/recommendation/recommendation.service.ts +++ b/redisinsight/api/src/modules/recommendation/recommendation.service.ts @@ -49,6 +49,7 @@ export class RecommendationService { await this.recommendationProvider.determineBigSetsRecommendation(keys), await this.recommendationProvider.determineConnectionClientsRecommendation(client), await this.recommendationProvider.determineSetPasswordRecommendation(client), + await this.recommendationProvider.determineRTSRecommendation(client, keys), ])); } } diff --git a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts index f57124e6b2..a8d1f413c7 100644 --- a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts +++ b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts @@ -219,7 +219,7 @@ describe('POST /databases/:instanceId/analysis', () => { responseSchema, before: async () => { const NUMBERS_OF_HASH_FIELDS = 5001; - await rte.data.generateHugeNumberOfFieldsForHashKey(NUMBERS_OF_HASH_FIELDS, true); + await rte.data.sendCommand(NUMBERS_OF_HASH_FIELDS, true); }, checkFn: async ({ body }) => { expect(body.recommendations).to.include.deep.members([ @@ -416,6 +416,25 @@ describe('POST /databases/:instanceId/analysis', () => { expect(await repository.count()).to.eq(5); } }, + { + name: 'Should create new database analysis with RTS recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + before: async () => { + await rte.data.sendCommand('zadd', [constants.TEST_ZSET_TIMESTAMP_KEY, constants.TEST_ZSET_TIMESTAMP_MEMBER, constants.TEST_ZSET_TIMESTAMP_SCORE]); + }, + checkFn: async ({ body }) => { + expect(body.recommendations).to.include.deep.members([ + constants.TEST_RTS_RECOMMENDATION, + ]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } + }, ].map(mainCheckFn); }); }); diff --git a/redisinsight/api/test/helpers/constants.ts b/redisinsight/api/test/helpers/constants.ts index 87087f24e6..ef77aa1b32 100644 --- a/redisinsight/api/test/helpers/constants.ts +++ b/redisinsight/api/test/helpers/constants.ts @@ -186,6 +186,9 @@ export const constants = { TEST_ZSET_HUGE_KEY: 'big zset 1M', TEST_ZSET_HUGE_MEMBER: ' 356897', TEST_ZSET_HUGE_SCORE: 356897, + TEST_ZSET_TIMESTAMP_KEY: TEST_RUN_ID + '_zset_timestamp' + CLUSTER_HASH_SLOT, + TEST_ZSET_TIMESTAMP_MEMBER: '12345678910', + TEST_ZSET_TIMESTAMP_SCORE: 12345678910, TEST_ZSET_KEY_BIN_BUFFER_1: Buffer.concat([Buffer.from(TEST_RUN_ID), Buffer.from('zsetk'), unprintableBuf]), get TEST_ZSET_KEY_BIN_BUF_OBJ_1() { return { type: 'Buffer', data: [...this.TEST_ZSET_KEY_BIN_BUFFER_1] } }, get TEST_ZSET_KEY_BIN_ASCII_1() { return getASCIISafeStringFromBuffer(this.TEST_ZSET_KEY_BIN_BUFFER_1) }, @@ -492,5 +495,9 @@ export const constants = { name: RECOMMENDATION_NAMES.SET_PASSWORD, }, + TEST_RTS_RECOMMENDATION: { + name: RECOMMENDATION_NAMES.RTS, + }, + // etc... } From 7aef85359c036d8200b4d5d1f7a1230aa8bfd101 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Fri, 23 Dec 2022 11:57:25 +0400 Subject: [PATCH 065/108] #RI-3942 - add rts recommendation --- .../constants/dbAnalysisRecommendations.json | 36 ++++++++ .../recommendations-view/Recommendations.tsx | 91 ++++++++++++------- .../recommendations-view/styles.module.scss | 6 ++ 3 files changed, 101 insertions(+), 32 deletions(-) diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index f9269b8562..9074620fca 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -467,5 +467,41 @@ } ], "badges": ["configuration_changes"] + }, + "RTS": { + "id": "RTS", + "title":"Optimize the use of time series", + "redisStack": true, + "content": [ + { + "id": "1", + "type": "paragraph", + "value": "If you are using sorted sets to work with time series data, consider using RedisTimeSeries to optimize the memory usage while having extraordinary query performance and small overhead during ingestion." + }, + { + "id": "2", + "type": "spacer", + "value": "l" + }, + { + "id": "3", + "type": "span", + "value": "Create a " + }, + { + "id": "4", + "type": "link", + "value": { + "href": "https://redis.com/try-free/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight/", + "name": "free Redis Stack database" + } + }, + { + "id": "5", + "type": "span", + "value": " to use modern data models and processing engines." + } + ], + "badges": ["configuration_changes"] } } diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx index c8dec8bf53..6ff1b2796b 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx @@ -1,17 +1,22 @@ -import React from 'react' +import React, { useContext } from 'react' import { useSelector } from 'react-redux' import { useParams } from 'react-router-dom' -import { isNull } from 'lodash' +import { isNull, sortBy } from 'lodash' import { EuiAccordion, EuiPanel, EuiText, EuiFlexGroup, EuiFlexItem, + EuiIcon, } from '@elastic/eui' +import { ThemeContext } from 'uiSrc/contexts/themeContext' import { dbAnalysisSelector } from 'uiSrc/slices/analytics/dbAnalysis' import recommendationsContent from 'uiSrc/constants/dbAnalysisRecommendations.json' +import { Theme } from 'uiSrc/constants' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import RediStackDarkMin from 'uiSrc/assets/img/modules/redistack/RediStackDark-min.svg' +import RediStackLightMin from 'uiSrc/assets/img/modules/redistack/RediStackLight-min.svg' import { renderContent, renderBadges, renderBadgesLegend } from './utils' import styles from './styles.module.scss' @@ -20,6 +25,7 @@ const Recommendations = () => { const { data, loading } = useSelector(dbAnalysisSelector) const { recommendations = [] } = data ?? {} + const { theme } = useContext(ThemeContext) const { instanceId } = useParams<{ instanceId: string }>() const handleToggle = (isOpen: boolean, id: string) => sendEventTelemetry({ @@ -51,37 +57,58 @@ const Recommendations = () => {
      {renderBadgesLegend()}
      - {recommendations.map(({ name }) => { - const { id = '', title = '', content = '', badges = [] } = recommendationsContent[name] + {sortBy(recommendations, ({ name }) => + (recommendationsContent[name]?.redisStack ? -1 : 0)) + ?.map(({ name }) => { + const { + id = '', + title = '', + content = '', + badges = [], + redisStack = false + } = recommendationsContent[name] - const buttonContent = ( - - {title} - - {renderBadges(badges)} - - - ) - return ( -
      - handleToggle(isOpen, id)} - data-testid={`${id}-accordion`} - > - - {renderContent(content)} - - -
      - ) - })} + const buttonContent = ( + + + + {redisStack && ( + + )} + + + {title} + + + + {renderBadges(badges)} + + + ) + return ( +
      + handleToggle(isOpen, id)} + data-testid={`${id}-accordion`} + > + + {renderContent(content)} + + +
      + ) + })}
) } diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss index 46ec75adb3..32de0b16c5 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss @@ -112,4 +112,10 @@ .span { display: inline; } + + .redisStack { + width: 20px; + height: 20px; + margin-right: 16px; + } } From 979a0ea47868799464f8f69a55d8d2b56e6481f7 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Wed, 28 Dec 2022 13:06:04 +0400 Subject: [PATCH 066/108] #RI-3942 - update link --- redisinsight/ui/src/constants/dbAnalysisRecommendations.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index 9074620fca..1d419d20de 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -492,7 +492,7 @@ "id": "4", "type": "link", "value": { - "href": "https://redis.com/try-free/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight/", + "href": "https://redis.com/try-free/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight_recommendations/", "name": "free Redis Stack database" } }, From 44738515326594974cb125285eb78ea6270fdceb Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Wed, 28 Dec 2022 13:49:36 +0400 Subject: [PATCH 067/108] #RI-3942 - fix IT --- .../api/database-analysis/POST-databases-id-analysis.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts index a8d1f413c7..9bdd4cf7a9 100644 --- a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts +++ b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts @@ -219,7 +219,7 @@ describe('POST /databases/:instanceId/analysis', () => { responseSchema, before: async () => { const NUMBERS_OF_HASH_FIELDS = 5001; - await rte.data.sendCommand(NUMBERS_OF_HASH_FIELDS, true); + await rte.data.generateHugeNumberOfFieldsForHashKey(NUMBERS_OF_HASH_FIELDS, true); }, checkFn: async ({ body }) => { expect(body.recommendations).to.include.deep.members([ From 5307ba835b0c3c059c12fe94c755583e5f5a3b61 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 29 Dec 2022 11:45:37 +0400 Subject: [PATCH 068/108] #RI-3942 - resolve comments --- .../recommendations-view/Recommendations.tsx | 104 +++++++++--------- 1 file changed, 53 insertions(+), 51 deletions(-) diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx index 6ff1b2796b..4d7c9344c4 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx @@ -38,6 +38,31 @@ const Recommendations = () => { } }) + const sortedRecommendations = sortBy(recommendations, ({ name }) => + (recommendationsContent[name]?.redisStack ? -1 : 0)) + + const renderButtonContent = (redisStack: boolean, title: string, badges: string[]) => ( + + + + {redisStack && ( + + )} + + + {title} + + + + {renderBadges(badges)} + + + ) + if (loading) { return (
@@ -57,58 +82,35 @@ const Recommendations = () => {
{renderBadgesLegend()}
- {sortBy(recommendations, ({ name }) => - (recommendationsContent[name]?.redisStack ? -1 : 0)) - ?.map(({ name }) => { - const { - id = '', - title = '', - content = '', - badges = [], - redisStack = false - } = recommendationsContent[name] + {sortedRecommendations.map(({ name }) => { + const { + id = '', + title = '', + content = '', + badges = [], + redisStack = false + } = recommendationsContent[name] - const buttonContent = ( - - - - {redisStack && ( - - )} - - - {title} - - - - {renderBadges(badges)} - - - ) - return ( -
- handleToggle(isOpen, id)} - data-testid={`${id}-accordion`} - > - - {renderContent(content)} - - -
- ) - })} + return ( +
+ handleToggle(isOpen, id)} + data-testid={`${id}-accordion`} + > + + {renderContent(content)} + + +
+ ) + })}
) } From 36545b78038d392418f590a615ff6c8f5bcb2db8 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 29 Dec 2022 15:14:49 +0400 Subject: [PATCH 069/108] #RI-3969 - fix styles --- .../recommendations-view/Recommendations.tsx | 62 ++++++++++--------- .../recommendations-view/styles.module.scss | 15 +++-- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx index c8dec8bf53..66a870a708 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx @@ -51,37 +51,39 @@ const Recommendations = () => {
{renderBadgesLegend()}
- {recommendations.map(({ name }) => { - const { id = '', title = '', content = '', badges = [] } = recommendationsContent[name] +
+ {recommendations.map(({ name }) => { + const { id = '', title = '', content = '', badges = [] } = recommendationsContent[name] - const buttonContent = ( - - {title} - - {renderBadges(badges)} - - - ) - return ( -
- handleToggle(isOpen, id)} - data-testid={`${id}-accordion`} - > - - {renderContent(content)} - - -
- ) - })} + const buttonContent = ( + + {title} + + {renderBadges(badges)} + + + ) + return ( +
+ handleToggle(isOpen, id)} + data-testid={`${id}-accordion`} + > + + {renderContent(content)} + + +
+ ) + })} +
) } diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss index 46ec75adb3..c7b1726a73 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss @@ -3,14 +3,10 @@ @import "@elastic/eui/src/global_styling/index"; .wrapper { - @include euiScrollBar; - overflow-y: auto; - overflow-x: hidden; - max-height: 100%; - padding-top: 30px; + height: 100%; .badgesLegend { - margin: 0 22px 14px 0 !important; + margin: 20px 22px 14px 0 !important; .badgeWrapper { margin-right: 0; @@ -34,6 +30,13 @@ align-items: center; margin-right: 24px; } +} + +.recommendationsContainer { + @include euiScrollBar; + overflow-y: auto; + overflow-x: hidden; + max-height: 100%; .accordionButton :global(.euiFlexItem) { margin: 0; From 153f99f7f44eebed1fd646310789f7a7f56d0e2e Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 29 Dec 2022 16:06:48 +0400 Subject: [PATCH 070/108] #RI-3899 - fix styles --- .../components/recommendations-view/styles.module.scss | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss index c7b1726a73..3471358ff0 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss @@ -6,7 +6,8 @@ height: 100%; .badgesLegend { - margin: 20px 22px 14px 0 !important; + margin: 0 22px 14px 0 !important; + padding-top: 20px; .badgeWrapper { margin-right: 0; @@ -36,7 +37,7 @@ @include euiScrollBar; overflow-y: auto; overflow-x: hidden; - max-height: 100%; + max-height: calc(100% - 51px); .accordionButton :global(.euiFlexItem) { margin: 0; From 70408ee7afcc3349790e2712d45652e1e1fd13fd Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Fri, 30 Dec 2022 08:17:33 +0400 Subject: [PATCH 071/108] #RI-3942 - workaround determine only on first shard --- .../modules/database-analysis/database-analysis.service.ts | 5 ++++- .../src/modules/recommendation/recommendation.service.ts | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts index 2fa3069476..4c70d05feb 100644 --- a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts +++ b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts @@ -3,6 +3,7 @@ import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.servi import { isNull, flatten, uniqBy } from 'lodash'; import { RecommendationService } from 'src/modules/recommendation/recommendation.service'; import { catchAclError } from 'src/utils'; +import { RECOMMENDATION_NAMES } from 'src/constants'; import { DatabaseAnalyzer } from 'src/modules/database-analysis/providers/database-analyzer'; import { plainToClass } from 'class-transformer'; import { DatabaseAnalysis, ShortDatabaseAnalysis } from 'src/modules/database-analysis/models'; @@ -58,11 +59,13 @@ export class DatabaseAnalysisService { const recommendations = DatabaseAnalysisService.getRecommendationsSummary( flatten(await Promise.all( - scanResults.map(async (nodeResult) => ( + scanResults.map(async (nodeResult, idx) => ( await this.recommendationService.getRecommendations({ client: nodeResult.client, keys: nodeResult.keys, total: progress.total, + // TODO: create generic solution to exclude recommendations + exclude: idx !== 0 ? [RECOMMENDATION_NAMES.RTS] : [], }) )), )), diff --git a/redisinsight/api/src/modules/recommendation/recommendation.service.ts b/redisinsight/api/src/modules/recommendation/recommendation.service.ts index ea8351557a..a6a25460e0 100644 --- a/redisinsight/api/src/modules/recommendation/recommendation.service.ts +++ b/redisinsight/api/src/modules/recommendation/recommendation.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common'; import { Redis } from 'ioredis'; import { RecommendationProvider } from 'src/modules/recommendation/providers/recommendation.provider'; import { Recommendation } from 'src/modules/database-analysis/models/recommendation'; +import { RECOMMENDATION_NAMES } from 'src/constants'; import { RedisString } from 'src/common/constants'; import { Key } from 'src/modules/database-analysis/models'; @@ -10,6 +11,7 @@ interface RecommendationInput { keys?: Key[], info?: RedisString, total?: number, + exclude?: string[], } @Injectable() @@ -31,6 +33,7 @@ export class RecommendationService { keys, info, total, + exclude, } = dto; return ( @@ -49,7 +52,8 @@ export class RecommendationService { await this.recommendationProvider.determineBigSetsRecommendation(keys), await this.recommendationProvider.determineConnectionClientsRecommendation(client), await this.recommendationProvider.determineSetPasswordRecommendation(client), - await this.recommendationProvider.determineRTSRecommendation(client, keys), + // TODO rework, need better solution to do not start determine recommendation + exclude.includes(RECOMMENDATION_NAMES.RTS) ? null : await this.recommendationProvider.determineRTSRecommendation(client, keys), ])); } } From 97775613796ed4a72dea22b21d9670186ddb0c71 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Wed, 4 Jan 2023 17:27:53 +0400 Subject: [PATCH 072/108] #RI-3941 - add redisstack link --- .../constants/dbAnalysisRecommendations.json | 24 +++++++++++++++++++ .../Recommendations.spec.tsx | 14 +++++++++++ .../recommendations-view/Recommendations.tsx | 19 +++++++++++---- .../recommendations-view/styles.module.scss | 7 ++++-- 4 files changed, 57 insertions(+), 7 deletions(-) diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index 1d419d20de..0ae71775d3 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -282,6 +282,7 @@ "bigSets": { "id": "bigSets", "title": "Switch to Bloom filter, cuckoo filter, or HyperLogLog", + "redisStack": true, "content": [ { "id": "1", @@ -409,6 +410,29 @@ "href": "https://docs.redis.com/latest/ri/memory-optimizations/", "name": "Read more" } + }, + { + "id": "11", + "type": "spacer", + "value": "l" + }, + { + "id": "12", + "type": "span", + "value": "Create a " + }, + { + "id": "13", + "type": "link", + "value": { + "href": "https://redis.com/try-free/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight_recommendations/", + "name": "free Redis Stack database" + } + }, + { + "id": "14", + "type": "span", + "value": " to use modern data models and processing engines." } ], "badges": ["configuration_changes"] diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx index f386fd446e..3081259c54 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx @@ -330,4 +330,18 @@ describe('Recommendations', () => { expect(screen.queryByTestId('badges-legend')).toBeInTheDocument() }) + + it('should render redisstack link', () => { + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: { + recommendations: [{ name: 'bigSets' }] + } + })) + + render() + + expect(screen.queryByTestId('redis-stack-link')).toBeInTheDocument() + expect(screen.queryByTestId('redis-stack-link')).toHaveAttribute('href', 'https://redis.io/docs/stack/') + }) }) diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx index d675772682..c8beccd173 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx @@ -9,6 +9,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, + EuiLink, } from '@elastic/eui' import { ThemeContext } from 'uiSrc/contexts/themeContext' import { dbAnalysisSelector } from 'uiSrc/slices/analytics/dbAnalysis' @@ -46,11 +47,19 @@ const Recommendations = () => { {redisStack && ( - + + + )} diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss index 4766c72e5c..6977235530 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss @@ -117,9 +117,12 @@ display: inline; } - .redisStack { + .redisStackLink { + margin-right: 16px; + } + + .redisStackIcon { width: 20px; height: 20px; - margin-right: 16px; } } From a02461a34f05edc4e918b6682f9940c37a13ba52 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 5 Jan 2023 16:11:51 +0400 Subject: [PATCH 073/108] #RI-3941 - resolve comments --- .../constants/dbAnalysisRecommendations.json | 4 ++-- .../recommendations-view/Recommendations.tsx | 21 ++++++++++++++----- .../recommendations-view/styles.module.scss | 19 +++++++++-------- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index 0ae71775d3..b55c1169fc 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -425,7 +425,7 @@ "id": "13", "type": "link", "value": { - "href": "https://redis.com/try-free/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight_recommendations/", + "href": "https://redis.com/try-free/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight/", "name": "free Redis Stack database" } }, @@ -516,7 +516,7 @@ "id": "4", "type": "link", "value": { - "href": "https://redis.com/try-free/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight_recommendations/", + "href": "https://redis.com/try-free/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight/", "name": "free Redis Stack database" } }, diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx index c8beccd173..a790d4b30c 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx @@ -6,6 +6,7 @@ import { EuiAccordion, EuiPanel, EuiText, + EuiToolTip, EuiFlexGroup, EuiFlexItem, EuiIcon, @@ -39,6 +40,8 @@ const Recommendations = () => { } }) + const onRedisStackClick = (event: React.MouseEvent) => event.stopPropagation() + const sortedRecommendations = sortBy(recommendations, ({ name }) => (recommendationsContent[name]?.redisStack ? -1 : 0)) @@ -53,12 +56,20 @@ const Recommendations = () => { href="https://redis.io/docs/stack/" className={styles.redisStackLink} data-testid="redis-stack-link" + onClick={onRedisStackClick} > - + + + )} diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss index 6977235530..e9eb391bbf 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss @@ -41,6 +41,16 @@ .accordionButton :global(.euiFlexItem) { margin: 0; + + .redisStackLink { + margin-right: 16px; + animation: none !important; + } + + .redisStackIcon { + width: 20px; + height: 20px; + } } :global(.euiAccordion__buttonReverse .euiAccordion__iconWrapper) { @@ -116,13 +126,4 @@ .span { display: inline; } - - .redisStackLink { - margin-right: 16px; - } - - .redisStackIcon { - width: 20px; - height: 20px; - } } From 9e0f0745663b61552a4dd68b91ccd5da47c23d5b Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 5 Jan 2023 16:35:27 +0400 Subject: [PATCH 074/108] #RI-3941 - resolve comments --- .../recommendations-view/Recommendations.spec.tsx | 4 ++-- .../recommendations-view/Recommendations.tsx | 13 ++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx index 3081259c54..7bb6bde41a 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx @@ -341,7 +341,7 @@ describe('Recommendations', () => { render() - expect(screen.queryByTestId('redis-stack-link')).toBeInTheDocument() - expect(screen.queryByTestId('redis-stack-link')).toHaveAttribute('href', 'https://redis.io/docs/stack/') + expect(screen.queryByTestId('bigSets-redis-stack-link')).toBeInTheDocument() + expect(screen.queryByTestId('bigSets-redis-stack-link')).toHaveAttribute('href', 'https://redis.io/docs/stack/') }) }) diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx index a790d4b30c..6b0b37d417 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx @@ -40,23 +40,22 @@ const Recommendations = () => { } }) - const onRedisStackClick = (event: React.MouseEvent) => event.stopPropagation() + const onRedisStackClick = (event: React.MouseEvent) => event.stopPropagation() const sortedRecommendations = sortBy(recommendations, ({ name }) => (recommendationsContent[name]?.redisStack ? -1 : 0)) - const renderButtonContent = (redisStack: boolean, title: string, badges: string[]) => ( + const renderButtonContent = (redisStack: boolean, title: string, badges: string[], id: string) => ( - + {redisStack && ( { @@ -117,7 +116,7 @@ const Recommendations = () => { Date: Mon, 9 Jan 2023 10:04:23 +0400 Subject: [PATCH 075/108] #RI-3955-3972 - add redis version recommendation --- redisinsight/api/package.json | 2 +- .../api/src/constants/recommendations.ts | 2 + .../providers/recommendation.provider.spec.ts | 37 ++++++++ .../providers/recommendation.provider.ts | 68 ++++++++++++++- .../recommendation/recommendation.service.ts | 2 + .../POST-databases-id-analysis.test.ts | 64 +++++++++----- redisinsight/api/test/helpers/constants.ts | 3 + .../constants/dbAnalysisRecommendations.json | 85 +++++++++++++++++++ .../Recommendations.spec.tsx | 30 +++++++ 9 files changed, 271 insertions(+), 22 deletions(-) diff --git a/redisinsight/api/package.json b/redisinsight/api/package.json index 3cfc7ae5f6..bc280f0444 100644 --- a/redisinsight/api/package.json +++ b/redisinsight/api/package.json @@ -62,6 +62,7 @@ "lodash": "^4.17.20", "nest-router": "^1.0.9", "nest-winston": "^1.4.0", + "node-version-compare": "^1.0.3", "reflect-metadata": "^0.1.13", "rxjs": "^7.5.6", "socket.io": "^4.4.0", @@ -102,7 +103,6 @@ "mocha": "^8.4.0", "mocha-junit-reporter": "^2.0.0", "mocha-multi-reporters": "^1.5.1", - "node-version-compare": "^1.0.3", "nyc": "^15.1.0", "object-diff": "^0.0.4", "rimraf": "^3.0.2", diff --git a/redisinsight/api/src/constants/recommendations.ts b/redisinsight/api/src/constants/recommendations.ts index 640cb330a9..80b50e5b66 100644 --- a/redisinsight/api/src/constants/recommendations.ts +++ b/redisinsight/api/src/constants/recommendations.ts @@ -14,4 +14,6 @@ export const RECOMMENDATION_NAMES = Object.freeze({ ZSET_HASHTABLE_TO_ZIPLIST: 'zSetHashtableToZiplist', SET_PASSWORD: 'setPassword', RTS: 'RTS', + REDIS_VERSION: 'redisVersion', + REDIS_SEARCH: 'redisSearch', }); diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts index f36d2a019e..87ebf81567 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts @@ -22,6 +22,9 @@ const mockRedisConfigResponse = ['name', '512']; const mockRedisClientsResponse_1: string = '# Clients\r\nconnected_clients:100\r\n'; const mockRedisClientsResponse_2: string = '# Clients\r\nconnected_clients:101\r\n'; +const mockRedisServerResponse_1: string = '# Server\r\nredis_version:6.0.0\r\n'; +const mockRedisServerResponse_2: string = '# Server\r\nredis_version:5.1.1\r\n'; + const mockRedisAclListResponse_1: string[] = [ 'user { expect(RTSRecommendation).toEqual(null); }); }); + + describe('determineRedisVersionRecommendation', () => { + it('should not return redis version recommendation', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'info' })) + .mockResolvedValue(mockRedisServerResponse_1); + + const redisServerRecommendation = await service + .determineRedisVersionRecommendation(nodeClient); + expect(redisServerRecommendation).toEqual(null); + }); + + it('should return redis version recommendation', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'info' })) + .mockResolvedValueOnce(mockRedisServerResponse_2); + + const redisServerRecommendation = await service + .determineRedisVersionRecommendation(nodeClient); + expect(redisServerRecommendation).toEqual({ name: RECOMMENDATION_NAMES.REDIS_VERSION }); + }); + + it('should not return redis version recommendation when info command executed with error', + async () => { + resetAllWhenMocks(); + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'info' })) + .mockRejectedValue('some error'); + + const redisServerRecommendation = await service + .determineRedisVersionRecommendation(nodeClient); + expect(redisServerRecommendation).toEqual(null); + }); + }); }); diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index 6de9d275ce..d9d92e607c 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -1,6 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { Redis, Cluster, Command } from 'ioredis'; import { get } from 'lodash'; +import * as semverCompare from 'node-version-compare'; import { convertRedisInfoReplyToObject, convertBulkStringsToObject } from 'src/utils'; import { RECOMMENDATION_NAMES, IS_TIMESTAMP } from 'src/constants'; import { RedisDataType } from 'src/modules/browser/dto'; @@ -17,6 +18,7 @@ const maxSetLength = 5000; const maxConnectedClients = 100; const bigStringMemory = 5_000_000; const sortedSetCountForCheck = 100; +const minRedisVersion = '6'; @Injectable() export class RecommendationProvider { @@ -309,7 +311,7 @@ export class RecommendationProvider { } /** - * Check set password recommendation + * Check RTS recommendation * @param redisClient * @param keys */ @@ -350,6 +352,70 @@ export class RecommendationProvider { } } + /** + * Check redis search recommendation + * @param redisClient + * @param keys + */ + + async determineRedisSearchRecommendation( + redisClient: Redis | Cluster, + keys: Key[], + ): Promise { + try { + let processedKeysNumber = 0; + let isTimeSeries = false; + let sortedSetNumber = 0; + while ( + processedKeysNumber < keys.length + && !isTimeSeries + && sortedSetNumber <= sortedSetCountForCheck + ) { + if (keys[processedKeysNumber].type !== RedisDataType.ZSet) { + processedKeysNumber += 1; + } else { + const [, membersArray] = await redisClient.sendCommand( + // get first member-score pair + new Command('zscan', [keys[processedKeysNumber].name, '0', 'COUNT', 2], { replyEncoding: 'utf8' }), + ) as string[]; + // check is pair member-score is timestamp + if (IS_TIMESTAMP.test(membersArray[0]) && IS_TIMESTAMP.test(membersArray[1])) { + isTimeSeries = true; + } + processedKeysNumber += 1; + sortedSetNumber += 1; + } + } + + return isTimeSeries ? { name: RECOMMENDATION_NAMES.RTS } : null; + } catch (err) { + this.logger.error('Can not determine RTS recommendation', err); + return null; + } + } + + /** + * Check redis version recommendation + * @param redisClient + */ + + async determineRedisVersionRecommendation( + redisClient: Redis | Cluster, + ): Promise { + try { + const info = convertRedisInfoReplyToObject( + await redisClient.sendCommand( + new Command('info', ['server'], { replyEncoding: 'utf8' }), + ) as string, + ); + const version = get(info, 'server.redis_version'); + return semverCompare(version, minRedisVersion) >= 0 ? null : { name: RECOMMENDATION_NAMES.REDIS_VERSION }; + } catch (err) { + this.logger.error('Can not determine redis version recommendation', err); + return null; + } + } + private async checkAuth(redisClient: Redis | Cluster): Promise { try { await redisClient.sendCommand( diff --git a/redisinsight/api/src/modules/recommendation/recommendation.service.ts b/redisinsight/api/src/modules/recommendation/recommendation.service.ts index a6a25460e0..e850105d92 100644 --- a/redisinsight/api/src/modules/recommendation/recommendation.service.ts +++ b/redisinsight/api/src/modules/recommendation/recommendation.service.ts @@ -54,6 +54,8 @@ export class RecommendationService { await this.recommendationProvider.determineSetPasswordRecommendation(client), // TODO rework, need better solution to do not start determine recommendation exclude.includes(RECOMMENDATION_NAMES.RTS) ? null : await this.recommendationProvider.determineRTSRecommendation(client, keys), + await this.recommendationProvider.determineRedisSearchRecommendation(client, keys), + await this.recommendationProvider.determineRedisVersionRecommendation(client), ])); } } diff --git a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts index 9bdd4cf7a9..4ba9854b68 100644 --- a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts +++ b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts @@ -191,7 +191,7 @@ describe('POST /databases/:instanceId/analysis', () => { requirements('!rte.pass'); [ { - name: 'Should create new database analysis with useSmallerKeys recommendation', + name: 'Should create new database analysis with setPassword recommendation', data: { delimiter: '-', }, @@ -209,6 +209,29 @@ describe('POST /databases/:instanceId/analysis', () => { ].map(mainCheckFn); }); + + describe('redisVersion recommendation', () => { + requirements('rte.version <= 6'); + [ + { + name: 'Should create new database analysis with redisVersion recommendation', + data: { + delimiter: '-', + }, + statusCode: 201, + responseSchema, + checkFn: async ({ body }) => { + expect(body.recommendations).to.include.deep.members([ + constants.TEST_REDIS_VERSION_RECOMMENDATION, + ]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } + }, + ].map(mainCheckFn); + }); + [ { name: 'Should create new database analysis with bigHashes recommendation', @@ -416,25 +439,26 @@ describe('POST /databases/:instanceId/analysis', () => { expect(await repository.count()).to.eq(5); } }, - { - name: 'Should create new database analysis with RTS recommendation', - data: { - delimiter: '-', - }, - statusCode: 201, - responseSchema, - before: async () => { - await rte.data.sendCommand('zadd', [constants.TEST_ZSET_TIMESTAMP_KEY, constants.TEST_ZSET_TIMESTAMP_MEMBER, constants.TEST_ZSET_TIMESTAMP_SCORE]); - }, - checkFn: async ({ body }) => { - expect(body.recommendations).to.include.deep.members([ - constants.TEST_RTS_RECOMMENDATION, - ]); - }, - after: async () => { - expect(await repository.count()).to.eq(5); - } - }, + // update with new requirements + // { + // name: 'Should create new database analysis with RTS recommendation', + // data: { + // delimiter: '-', + // }, + // statusCode: 201, + // responseSchema, + // before: async () => { + // await rte.data.sendCommand('zadd', [constants.TEST_ZSET_TIMESTAMP_KEY, constants.TEST_ZSET_TIMESTAMP_MEMBER, constants.TEST_ZSET_TIMESTAMP_SCORE]); + // }, + // checkFn: async ({ body }) => { + // expect(body.recommendations).to.include.deep.members([ + // constants.TEST_RTS_RECOMMENDATION, + // ]); + // }, + // after: async () => { + // expect(await repository.count()).to.eq(5); + // } + // }, ].map(mainCheckFn); }); }); diff --git a/redisinsight/api/test/helpers/constants.ts b/redisinsight/api/test/helpers/constants.ts index b250b63745..6e306e2b06 100644 --- a/redisinsight/api/test/helpers/constants.ts +++ b/redisinsight/api/test/helpers/constants.ts @@ -506,5 +506,8 @@ export const constants = { name: RECOMMENDATION_NAMES.RTS, }, + TEST_REDIS_VERSION_RECOMMENDATION: { + name: RECOMMENDATION_NAMES.REDIS_VERSION, + }, // etc... } diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index b55c1169fc..963ef1a1a4 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -527,5 +527,90 @@ } ], "badges": ["configuration_changes"] + }, + "redisVersion": { + "id": "redisVersion", + "title":"Update Redis database", + "redisStack": true, + "content": [ + { + "id": "1", + "type": "paragraph", + "value": "Newer versions of Redis (starting from 6.0) have performance and resource utilization improvements, as well as the improved active, expire cycle to evict the keys faster." + }, + { + "id": "2", + "type": "spacer", + "value": "l" + }, + { + "id": "3", + "type": "span", + "value": "Create a " + }, + { + "id": "4", + "type": "link", + "value": { + "href": "https://redis.com/try-free/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight/", + "name": "free Redis Stack database" + } + }, + { + "id": "5", + "type": "span", + "value": " which extends the core capabilities of Redis OSS and provides a complete developer experience for debugging and more." + } + ], + "badges": ["upgrade"] + }, + "redisSearch": { + "id": "redisSearch", + "title":"Optimize your search and query experience", + "redisStack": true, + "content": [ + { + "id": "1", + "type": "span", + "value": "RediSearch was designed to help address your query needs and support a better development experience when dealing with complex data scenarios. Take a look at the powerful API options here (" + }, + { + "id": "2", + "type": "link", + "value": { + "href": "https://redis.io/commands/?name=Ft", + "name": "Commands" + } + }, + { + "id": "3", + "type": "span", + "value": ") and try it. Supports full-text search, wildcards, fuzzy logic, and more." + }, + { + "id": "4", + "type": "spacer", + "value": "l" + }, + { + "id": "5", + "type": "span", + "value": "Create a " + }, + { + "id": "4", + "type": "link", + "value": { + "href": "https://redis.com/try-free/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight/", + "name": "free Redis Stack database" + } + }, + { + "id": "5", + "type": "span", + "value": " which extends the core capabilities of Redis OSS and uses modern data models and processing engines." + } + ], + "badges": ["upgrade"] } } diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx index 7bb6bde41a..99e978fd51 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx @@ -264,6 +264,36 @@ describe('Recommendations', () => { expect(screen.queryByTestId('configuration_changes')).toBeInTheDocument() }) + it('should render upgrade badge in redisSearch recommendation', () => { + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: { + recommendations: [{ name: 'redisSearch' }] + } + })) + + render() + + expect(screen.queryByTestId('code_changes')).not.toBeInTheDocument() + expect(screen.queryByTestId('upgrade')).toBeInTheDocument() + expect(screen.queryByTestId('configuration_changes')).not.toBeInTheDocument() + }) + + it('should render upgrade badge in redisVersion recommendation', () => { + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: { + recommendations: [{ name: 'redisVersion' }] + } + })) + + render() + + expect(screen.queryByTestId('code_changes')).not.toBeInTheDocument() + expect(screen.queryByTestId('upgrade')).toBeInTheDocument() + expect(screen.queryByTestId('configuration_changes')).not.toBeInTheDocument() + }) + it('should collapse/expand and sent proper telemetry event', () => { (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ ...mockdbAnalysisSelector, From bcfcf1a371874810e53471c6723f849c255306fc Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 9 Jan 2023 12:01:23 +0400 Subject: [PATCH 076/108] #RI-3955-3972 - add redis search recommendation --- .../api/src/constants/redis-modules.ts | 4 + .../providers/recommendation.provider.spec.ts | 122 ++++++++++++++++++ .../providers/recommendation.provider.ts | 50 ++++--- .../recommendation/recommendation.service.ts | 2 +- 4 files changed, 150 insertions(+), 28 deletions(-) diff --git a/redisinsight/api/src/constants/redis-modules.ts b/redisinsight/api/src/constants/redis-modules.ts index 85bac9a523..ecf34cf7cf 100644 --- a/redisinsight/api/src/constants/redis-modules.ts +++ b/redisinsight/api/src/constants/redis-modules.ts @@ -8,6 +8,10 @@ export enum AdditionalRedisModuleName { RedisTimeSeries = 'timeseries', } +export const REDIS_SEARCH_MODULES = [ + AdditionalRedisModuleName.RediSearch, +]; + export const SUPPORTED_REDIS_MODULES = Object.freeze({ ai: AdditionalRedisModuleName.RedisAI, graph: AdditionalRedisModuleName.RedisGraph, diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts index 87ebf81567..1c9e91a2e5 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts @@ -34,6 +34,30 @@ const mockRedisAclListResponse_2: string[] = [ 'user test_2 on nopass ~* &* +@all', ]; +const mockRedisModulesResponse_1 = [ + { name: 'ai', ver: 10000 }, + { name: 'graph', ver: 10000 }, + { name: 'rg', ver: 10000 }, + { name: 'bf', ver: 10000 }, + { name: 'ReJSON', ver: 10000 }, + { name: 'search', ver: 10000 }, + { name: 'timeseries', ver: 10000 }, + { name: 'customModule', ver: 10000 }, +].map((item) => ([].concat(...Object.entries(item)))); + +const mockRedisModulesResponse_2 = [ + { name: 'ai', ver: 10000 }, + { name: 'graph', ver: 10000 }, + { name: 'rg', ver: 10000 }, + { name: 'bf', ver: 10000 }, + { name: 'ReJSON', ver: 10000 }, + { name: 'timeseries', ver: 10000 }, + { name: 'customModule', ver: 10000 }, +].map((item) => ([].concat(...Object.entries(item)))); + +const mockFTListResponse_1 = []; +const mockFTListResponse_2 = ['idx']; + const mockZScanResponse_1 = [ '0', [123456789, 123456789, 12345678910, 12345678910], @@ -106,6 +130,18 @@ const mockBigListKey = { name: Buffer.from('name'), type: 'list', length: 1001, memory: 10, ttl: -1, }; +const mockJSONKey = { + name: Buffer.from('name'), type: 'ReJSON-RL', length: 1, memory: 10, ttl: -1, +}; + +const mockRediSearchStringKey_1 = { + name: Buffer.from('name'), type: 'string', length: 1, memory: 512 * 1024 + 1, ttl: -1, +}; + +const mockRediSearchStringKey_2 = { + name: Buffer.from('name'), type: 'string', length: 1, memory: 512 * 1024, ttl: -1, +}; + const mockSortedSets = new Array(101).fill( { name: Buffer.from('name'), type: 'zset', length: 10, memory: 10, ttl: -1, @@ -529,6 +565,92 @@ describe('RecommendationProvider', () => { }); }); + describe('determineRediSearchRecommendation', () => { + it('should not return rediSearch recommendation when rediSearch module was download with indexes', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'module' })) + .mockResolvedValue(mockRedisModulesResponse_1); + + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'FT._LIST' })) + .mockResolvedValue(mockFTListResponse_2); + + const redisServerRecommendation = await service + .determineRediSearchRecommendation(nodeClient, [mockJSONKey]); + expect(redisServerRecommendation).toEqual(null); + }); + + it('should return rediSearch recommendation when there is JSON key', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'module' })) + .mockResolvedValue(mockRedisModulesResponse_1); + + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'FT._LIST' })) + .mockResolvedValue(mockFTListResponse_1); + + const redisServerRecommendation = await service + .determineRediSearchRecommendation(nodeClient, [mockJSONKey]); + expect(redisServerRecommendation).toEqual({ name: RECOMMENDATION_NAMES.REDIS_SEARCH }); + }); + + it('should return rediSearch recommendation when there is huge string key', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'module' })) + .mockResolvedValue(mockRedisModulesResponse_1); + + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'FT._LIST' })) + .mockResolvedValue(mockFTListResponse_1); + + const redisServerRecommendation = await service + .determineRediSearchRecommendation(nodeClient, [mockRediSearchStringKey_1]); + expect(redisServerRecommendation).toEqual({ name: RECOMMENDATION_NAMES.REDIS_SEARCH }); + }); + + it('should not return rediSearch recommendation when there is small string key', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'module' })) + .mockResolvedValue(mockRedisModulesResponse_1); + + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'FT._LIST' })) + .mockResolvedValue(mockFTListResponse_1); + + const redisServerRecommendation = await service + .determineRediSearchRecommendation(nodeClient, [mockRediSearchStringKey_2]); + expect(redisServerRecommendation).toEqual(null); + }); + + it('should not return rediSearch recommendation when ft command execute with error', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'module' })) + .mockResolvedValue(mockRedisModulesResponse_1); + + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'FT._LIST' })) + .mockRejectedValue("some error"); + + const redisServerRecommendation = await service + .determineRediSearchRecommendation(nodeClient, [mockJSONKey]); + expect(redisServerRecommendation).toEqual(null); + }); + + it('should not return rediSearch recommendation when module command execute with error', async () => { + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'module' })) + .mockResolvedValue("some error"); + + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'FT._LIST' })) + .mockResolvedValue(mockFTListResponse_1); + + const redisServerRecommendation = await service + .determineRediSearchRecommendation(nodeClient, [mockJSONKey]); + expect(redisServerRecommendation).toEqual(null); + }); + }); + describe('determineRedisVersionRecommendation', () => { it('should not return redis version recommendation', async () => { when(nodeClient.sendCommand) diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index d9d92e607c..b87db6b27d 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -2,8 +2,8 @@ import { Injectable, Logger } from '@nestjs/common'; import { Redis, Cluster, Command } from 'ioredis'; import { get } from 'lodash'; import * as semverCompare from 'node-version-compare'; -import { convertRedisInfoReplyToObject, convertBulkStringsToObject } from 'src/utils'; -import { RECOMMENDATION_NAMES, IS_TIMESTAMP } from 'src/constants'; +import { convertRedisInfoReplyToObject, convertBulkStringsToObject, convertStringsArrayToObject } from 'src/utils'; +import { RECOMMENDATION_NAMES, IS_TIMESTAMP, REDIS_SEARCH_MODULES } from 'src/constants'; import { RedisDataType } from 'src/modules/browser/dto'; import { Recommendation } from 'src/modules/database-analysis/models/recommendation'; import { Key } from 'src/modules/database-analysis/models'; @@ -16,6 +16,7 @@ const maxCompressHashLength = 1000; const maxListLength = 1000; const maxSetLength = 5000; const maxConnectedClients = 100; +const maxRediSearchStringMemory = 512 * 1024; const bigStringMemory = 5_000_000; const sortedSetCountForCheck = 100; const minRedisVersion = '6'; @@ -358,38 +359,33 @@ export class RecommendationProvider { * @param keys */ - async determineRedisSearchRecommendation( + async determineRediSearchRecommendation( redisClient: Redis | Cluster, keys: Key[], ): Promise { try { - let processedKeysNumber = 0; - let isTimeSeries = false; - let sortedSetNumber = 0; - while ( - processedKeysNumber < keys.length - && !isTimeSeries - && sortedSetNumber <= sortedSetCountForCheck - ) { - if (keys[processedKeysNumber].type !== RedisDataType.ZSet) { - processedKeysNumber += 1; - } else { - const [, membersArray] = await redisClient.sendCommand( - // get first member-score pair - new Command('zscan', [keys[processedKeysNumber].name, '0', 'COUNT', 2], { replyEncoding: 'utf8' }), - ) as string[]; - // check is pair member-score is timestamp - if (IS_TIMESTAMP.test(membersArray[0]) && IS_TIMESTAMP.test(membersArray[1])) { - isTimeSeries = true; - } - processedKeysNumber += 1; - sortedSetNumber += 1; - } + const reply = await redisClient.sendCommand( + new Command('module', ['list'], { replyEncoding: 'utf8' }), + ) as any[]; + const modules = reply.map((module: any[]) => convertStringsArrayToObject(module)); + const isRediSearchModule = modules.some(({ name }) => REDIS_SEARCH_MODULES + .some((search: string) => name === search)); + + const indexes = await redisClient.sendCommand( + new Command('FT._LIST', [], { replyEncoding: 'utf8' }), + ) as any[]; + if (isRediSearchModule && indexes.length) { + return null; } - return isTimeSeries ? { name: RECOMMENDATION_NAMES.RTS } : null; + const isBigStringOrJSON = keys.some((key) => ( + key.type === RedisDataType.String && key.memory > maxRediSearchStringMemory + ) + || key.type === RedisDataType.JSON); + + return isBigStringOrJSON ? { name: RECOMMENDATION_NAMES.REDIS_SEARCH } : null; } catch (err) { - this.logger.error('Can not determine RTS recommendation', err); + this.logger.error('Can not determine redis search recommendation', err); return null; } } diff --git a/redisinsight/api/src/modules/recommendation/recommendation.service.ts b/redisinsight/api/src/modules/recommendation/recommendation.service.ts index e850105d92..a5bc8d6728 100644 --- a/redisinsight/api/src/modules/recommendation/recommendation.service.ts +++ b/redisinsight/api/src/modules/recommendation/recommendation.service.ts @@ -54,7 +54,7 @@ export class RecommendationService { await this.recommendationProvider.determineSetPasswordRecommendation(client), // TODO rework, need better solution to do not start determine recommendation exclude.includes(RECOMMENDATION_NAMES.RTS) ? null : await this.recommendationProvider.determineRTSRecommendation(client, keys), - await this.recommendationProvider.determineRedisSearchRecommendation(client, keys), + await this.recommendationProvider.determineRediSearchRecommendation(client, keys), await this.recommendationProvider.determineRedisVersionRecommendation(client), ])); } From c87c862c9206d6183e1e767e8b4cc46f42294667 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 9 Jan 2023 12:15:07 +0400 Subject: [PATCH 077/108] #RI-3972 - add IT test --- .../POST-databases-id-analysis.test.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts index 4ba9854b68..d512fae9ed 100644 --- a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts +++ b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts @@ -232,6 +232,30 @@ describe('POST /databases/:instanceId/analysis', () => { ].map(mainCheckFn); }); + describe('rediSearch recommendation', () => { + [ + { + name: 'Should create new database analysis with rediSearch recommendation', + data: { + delimiter: '-', + }, + before: async () => { + await rte.data.sendCommand('SET', [constants.TEST_STRING_KEY_1, Buffer.alloc(513 * 1024, 'a').toString()]); + }, + statusCode: 201, + responseSchema, + checkFn: async ({ body }) => { + expect(body.recommendations).to.include.deep.members([ + constants.TEST_REDIS_VERSION_RECOMMENDATION, + ]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } + }, + ].map(mainCheckFn); + }); + [ { name: 'Should create new database analysis with bigHashes recommendation', From 35b0bbff9175dba175c114dc3f3d951e81ef6bfb Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 9 Jan 2023 12:21:44 +0400 Subject: [PATCH 078/108] #RI-3972 - add IT reJson test --- .../POST-databases-id-analysis.test.ts | 28 ++++++++++++++++++- redisinsight/api/test/helpers/constants.ts | 4 +++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts index d512fae9ed..48404ce13e 100644 --- a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts +++ b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts @@ -246,7 +246,33 @@ describe('POST /databases/:instanceId/analysis', () => { responseSchema, checkFn: async ({ body }) => { expect(body.recommendations).to.include.deep.members([ - constants.TEST_REDIS_VERSION_RECOMMENDATION, + constants.TEST_REDISEARCH_RECOMMENDATION, + ]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } + }, + ].map(mainCheckFn); + }); + + describe('rediSearch recommendation with ReJSON', () => { + requirements('rte.modules.rejson'); + [ + { + name: 'Should create new database analysis with rediSearch recommendation', + data: { + delimiter: '-', + }, + before: async () => { + const NUMBERS_REJSONS = 1; + await rte.data.generateNReJSONs(NUMBERS_REJSONS, true); + }, + statusCode: 201, + responseSchema, + checkFn: async ({ body }) => { + expect(body.recommendations).to.include.deep.members([ + constants.TEST_REDISEARCH_RECOMMENDATION, ]); }, after: async () => { diff --git a/redisinsight/api/test/helpers/constants.ts b/redisinsight/api/test/helpers/constants.ts index 6e306e2b06..fb1a1ab585 100644 --- a/redisinsight/api/test/helpers/constants.ts +++ b/redisinsight/api/test/helpers/constants.ts @@ -509,5 +509,9 @@ export const constants = { TEST_REDIS_VERSION_RECOMMENDATION: { name: RECOMMENDATION_NAMES.REDIS_VERSION, }, + + TEST_REDISEARCH_RECOMMENDATION: { + name: RECOMMENDATION_NAMES.REDIS_SEARCH, + }, // etc... } From aeffc00c326272e06c8a252dd04e1454d510deff Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 9 Jan 2023 12:40:15 +0400 Subject: [PATCH 079/108] #RI-3972 - fix test --- .../providers/recommendation.provider.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index b87db6b27d..5c0569a3b9 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -371,11 +371,13 @@ export class RecommendationProvider { const isRediSearchModule = modules.some(({ name }) => REDIS_SEARCH_MODULES .some((search: string) => name === search)); - const indexes = await redisClient.sendCommand( - new Command('FT._LIST', [], { replyEncoding: 'utf8' }), - ) as any[]; - if (isRediSearchModule && indexes.length) { - return null; + if (isRediSearchModule) { + const indexes = await redisClient.sendCommand( + new Command('FT._LIST', [], { replyEncoding: 'utf8' }), + ) as any[]; + if (indexes.length) { + return null; + } } const isBigStringOrJSON = keys.some((key) => ( From 6b1ba844a42cbc23302a01b30af8c0b6a90167f1 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 9 Jan 2023 13:01:42 +0400 Subject: [PATCH 080/108] #RI-3972 - fix test --- .../providers/recommendation.provider.ts | 52 +++++++++---------- .../POST-databases-id-analysis.test.ts | 1 - 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index 5c0569a3b9..4a22923545 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -285,32 +285,6 @@ export class RecommendationProvider { } } - /** - * Check set password recommendation - * @param redisClient - */ - - async determineSetPasswordRecommendation( - redisClient: Redis | Cluster, - ): Promise { - if (await this.checkAuth(redisClient)) { - return { name: RECOMMENDATION_NAMES.SET_PASSWORD }; - } - - try { - const users = await redisClient.sendCommand( - new Command('acl', ['list'], { replyEncoding: 'utf8' }), - ) as string[]; - - const nopassUser = users.some((user) => user.split(' ')[3] === 'nopass'); - - return nopassUser ? { name: RECOMMENDATION_NAMES.SET_PASSWORD } : null; - } catch (err) { - this.logger.error('Can not determine set password recommendation', err); - return null; - } - } - /** * Check RTS recommendation * @param redisClient @@ -414,6 +388,32 @@ export class RecommendationProvider { } } + /** + * Check set password recommendation + * @param redisClient + */ + + async determineSetPasswordRecommendation( + redisClient: Redis | Cluster, + ): Promise { + if (await this.checkAuth(redisClient)) { + return { name: RECOMMENDATION_NAMES.SET_PASSWORD }; + } + + try { + const users = await redisClient.sendCommand( + new Command('acl', ['list'], { replyEncoding: 'utf8' }), + ) as string[]; + + const nopassUser = users.some((user) => user.split(' ')[3] === 'nopass'); + + return nopassUser ? { name: RECOMMENDATION_NAMES.SET_PASSWORD } : null; + } catch (err) { + this.logger.error('Can not determine set password recommendation', err); + return null; + } + } + private async checkAuth(redisClient: Redis | Cluster): Promise { try { await redisClient.sendCommand( diff --git a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts index 48404ce13e..c33d39c3a1 100644 --- a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts +++ b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts @@ -209,7 +209,6 @@ describe('POST /databases/:instanceId/analysis', () => { ].map(mainCheckFn); }); - describe('redisVersion recommendation', () => { requirements('rte.version <= 6'); [ From 38ef44de3ee6d453395422a347265ad818dfbe06 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 9 Jan 2023 13:28:59 +0400 Subject: [PATCH 081/108] #RI-3972 - fix pass IT --- .../api/src/modules/recommendation/recommendation.service.ts | 4 ++-- .../api/database-analysis/POST-databases-id-analysis.test.ts | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/redisinsight/api/src/modules/recommendation/recommendation.service.ts b/redisinsight/api/src/modules/recommendation/recommendation.service.ts index a5bc8d6728..50517b2437 100644 --- a/redisinsight/api/src/modules/recommendation/recommendation.service.ts +++ b/redisinsight/api/src/modules/recommendation/recommendation.service.ts @@ -51,11 +51,11 @@ export class RecommendationService { await this.recommendationProvider.determineZSetHashtableToZiplistRecommendation(client, keys), await this.recommendationProvider.determineBigSetsRecommendation(keys), await this.recommendationProvider.determineConnectionClientsRecommendation(client), - await this.recommendationProvider.determineSetPasswordRecommendation(client), // TODO rework, need better solution to do not start determine recommendation exclude.includes(RECOMMENDATION_NAMES.RTS) ? null : await this.recommendationProvider.determineRTSRecommendation(client, keys), await this.recommendationProvider.determineRediSearchRecommendation(client, keys), - await this.recommendationProvider.determineRedisVersionRecommendation(client), + await this.recommendationProvider.determineRedisVersionRecommendation(client), + await this.recommendationProvider.determineSetPasswordRecommendation(client), ])); } } diff --git a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts index c33d39c3a1..bc978abe7d 100644 --- a/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts +++ b/redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts @@ -186,7 +186,6 @@ describe('POST /databases/:instanceId/analysis', () => { ].map(mainCheckFn); }); - describe('setPassword recommendation', () => { requirements('!rte.pass'); [ From d341e9689ff56bda959c7335b8ec03b514dbc967 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 9 Jan 2023 13:58:44 +0400 Subject: [PATCH 082/108] #RI-3972 - update search modules --- redisinsight/api/src/constants/redis-modules.ts | 14 ++++++++++++-- .../providers/recommendation.provider.ts | 4 ++-- .../src/constants/dbAnalysisRecommendations.json | 8 ++++---- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/redisinsight/api/src/constants/redis-modules.ts b/redisinsight/api/src/constants/redis-modules.ts index ecf34cf7cf..0fd0cbaef2 100644 --- a/redisinsight/api/src/constants/redis-modules.ts +++ b/redisinsight/api/src/constants/redis-modules.ts @@ -8,8 +8,18 @@ export enum AdditionalRedisModuleName { RedisTimeSeries = 'timeseries', } -export const REDIS_SEARCH_MODULES = [ - AdditionalRedisModuleName.RediSearch, +enum RediSearchModuleName { + Search = 'search', + SearchLight = 'searchlight', + FT = 'ft', + FTL = 'ftl', +} + +export const REDISEARCH_MODULES = [ + RediSearchModuleName.Search, + RediSearchModuleName.SearchLight, + RediSearchModuleName.FT, + RediSearchModuleName.FTL, ]; export const SUPPORTED_REDIS_MODULES = Object.freeze({ diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index 4a22923545..2656b49cc5 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -3,7 +3,7 @@ import { Redis, Cluster, Command } from 'ioredis'; import { get } from 'lodash'; import * as semverCompare from 'node-version-compare'; import { convertRedisInfoReplyToObject, convertBulkStringsToObject, convertStringsArrayToObject } from 'src/utils'; -import { RECOMMENDATION_NAMES, IS_TIMESTAMP, REDIS_SEARCH_MODULES } from 'src/constants'; +import { RECOMMENDATION_NAMES, IS_TIMESTAMP, REDISEARCH_MODULES } from 'src/constants'; import { RedisDataType } from 'src/modules/browser/dto'; import { Recommendation } from 'src/modules/database-analysis/models/recommendation'; import { Key } from 'src/modules/database-analysis/models'; @@ -342,7 +342,7 @@ export class RecommendationProvider { new Command('module', ['list'], { replyEncoding: 'utf8' }), ) as any[]; const modules = reply.map((module: any[]) => convertStringsArrayToObject(module)); - const isRediSearchModule = modules.some(({ name }) => REDIS_SEARCH_MODULES + const isRediSearchModule = modules.some(({ name }) => REDISEARCH_MODULES .some((search: string) => name === search)); if (isRediSearchModule) { diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index 963ef1a1a4..c4fea11619 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -425,7 +425,7 @@ "id": "13", "type": "link", "value": { - "href": "https://redis.com/try-free/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight/", + "href": "https://redis.com/try-free/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight_recommendations/", "name": "free Redis Stack database" } }, @@ -516,7 +516,7 @@ "id": "4", "type": "link", "value": { - "href": "https://redis.com/try-free/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight/", + "href": "https://redis.com/try-free/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight_recommendations/", "name": "free Redis Stack database" } }, @@ -552,7 +552,7 @@ "id": "4", "type": "link", "value": { - "href": "https://redis.com/try-free/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight/", + "href": "https://redis.com/try-free/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight_recommendations/", "name": "free Redis Stack database" } }, @@ -601,7 +601,7 @@ "id": "4", "type": "link", "value": { - "href": "https://redis.com/try-free/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight/", + "href": "https://redis.com/try-free/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight_recommendations/", "name": "free Redis Stack database" } }, From e094714d7cb04245c1afe16585b0ee0a5a5be91c Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 9 Jan 2023 15:06:01 +0400 Subject: [PATCH 083/108] #RI-3972 - resolve comments --- .../api/src/constants/redis-modules.ts | 14 ---- .../providers/recommendation.provider.spec.ts | 71 ++++--------------- .../providers/recommendation.provider.ts | 13 ++-- 3 files changed, 17 insertions(+), 81 deletions(-) diff --git a/redisinsight/api/src/constants/redis-modules.ts b/redisinsight/api/src/constants/redis-modules.ts index 0fd0cbaef2..85bac9a523 100644 --- a/redisinsight/api/src/constants/redis-modules.ts +++ b/redisinsight/api/src/constants/redis-modules.ts @@ -8,20 +8,6 @@ export enum AdditionalRedisModuleName { RedisTimeSeries = 'timeseries', } -enum RediSearchModuleName { - Search = 'search', - SearchLight = 'searchlight', - FT = 'ft', - FTL = 'ftl', -} - -export const REDISEARCH_MODULES = [ - RediSearchModuleName.Search, - RediSearchModuleName.SearchLight, - RediSearchModuleName.FT, - RediSearchModuleName.FTL, -]; - export const SUPPORTED_REDIS_MODULES = Object.freeze({ ai: AdditionalRedisModuleName.RedisAI, graph: AdditionalRedisModuleName.RedisGraph, diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts index 1c9e91a2e5..760a5a25a1 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts @@ -34,27 +34,6 @@ const mockRedisAclListResponse_2: string[] = [ 'user test_2 on nopass ~* &* +@all', ]; -const mockRedisModulesResponse_1 = [ - { name: 'ai', ver: 10000 }, - { name: 'graph', ver: 10000 }, - { name: 'rg', ver: 10000 }, - { name: 'bf', ver: 10000 }, - { name: 'ReJSON', ver: 10000 }, - { name: 'search', ver: 10000 }, - { name: 'timeseries', ver: 10000 }, - { name: 'customModule', ver: 10000 }, -].map((item) => ([].concat(...Object.entries(item)))); - -const mockRedisModulesResponse_2 = [ - { name: 'ai', ver: 10000 }, - { name: 'graph', ver: 10000 }, - { name: 'rg', ver: 10000 }, - { name: 'bf', ver: 10000 }, - { name: 'ReJSON', ver: 10000 }, - { name: 'timeseries', ver: 10000 }, - { name: 'customModule', ver: 10000 }, -].map((item) => ([].concat(...Object.entries(item)))); - const mockFTListResponse_1 = []; const mockFTListResponse_2 = ['idx']; @@ -566,25 +545,7 @@ describe('RecommendationProvider', () => { }); describe('determineRediSearchRecommendation', () => { - it('should not return rediSearch recommendation when rediSearch module was download with indexes', async () => { - when(nodeClient.sendCommand) - .calledWith(jasmine.objectContaining({ name: 'module' })) - .mockResolvedValue(mockRedisModulesResponse_1); - - when(nodeClient.sendCommand) - .calledWith(jasmine.objectContaining({ name: 'FT._LIST' })) - .mockResolvedValue(mockFTListResponse_2); - - const redisServerRecommendation = await service - .determineRediSearchRecommendation(nodeClient, [mockJSONKey]); - expect(redisServerRecommendation).toEqual(null); - }); - it('should return rediSearch recommendation when there is JSON key', async () => { - when(nodeClient.sendCommand) - .calledWith(jasmine.objectContaining({ name: 'module' })) - .mockResolvedValue(mockRedisModulesResponse_1); - when(nodeClient.sendCommand) .calledWith(jasmine.objectContaining({ name: 'FT._LIST' })) .mockResolvedValue(mockFTListResponse_1); @@ -595,10 +556,6 @@ describe('RecommendationProvider', () => { }); it('should return rediSearch recommendation when there is huge string key', async () => { - when(nodeClient.sendCommand) - .calledWith(jasmine.objectContaining({ name: 'module' })) - .mockResolvedValue(mockRedisModulesResponse_1); - when(nodeClient.sendCommand) .calledWith(jasmine.objectContaining({ name: 'FT._LIST' })) .mockResolvedValue(mockFTListResponse_1); @@ -609,10 +566,6 @@ describe('RecommendationProvider', () => { }); it('should not return rediSearch recommendation when there is small string key', async () => { - when(nodeClient.sendCommand) - .calledWith(jasmine.objectContaining({ name: 'module' })) - .mockResolvedValue(mockRedisModulesResponse_1); - when(nodeClient.sendCommand) .calledWith(jasmine.objectContaining({ name: 'FT._LIST' })) .mockResolvedValue(mockFTListResponse_1); @@ -622,31 +575,33 @@ describe('RecommendationProvider', () => { expect(redisServerRecommendation).toEqual(null); }); - it('should not return rediSearch recommendation when ft command execute with error', async () => { - when(nodeClient.sendCommand) - .calledWith(jasmine.objectContaining({ name: 'module' })) - .mockResolvedValue(mockRedisModulesResponse_1); - + it('should not return rediSearch recommendation when there are no indexes', async () => { when(nodeClient.sendCommand) .calledWith(jasmine.objectContaining({ name: 'FT._LIST' })) - .mockRejectedValue("some error"); + .mockResolvedValue(mockFTListResponse_2); const redisServerRecommendation = await service .determineRediSearchRecommendation(nodeClient, [mockJSONKey]); expect(redisServerRecommendation).toEqual(null); }); - it('should not return rediSearch recommendation when module command execute with error', async () => { + it('should ignore errors when ft command execute with error', async () => { when(nodeClient.sendCommand) - .calledWith(jasmine.objectContaining({ name: 'module' })) - .mockResolvedValue("some error"); + .calledWith(jasmine.objectContaining({ name: 'FT._LIST' })) + .mockRejectedValue("some error"); + + const redisServerRecommendation = await service + .determineRediSearchRecommendation(nodeClient, [mockJSONKey]); + expect(redisServerRecommendation).toEqual({ name: RECOMMENDATION_NAMES.REDIS_SEARCH }); + }); + it('should ignore errors when ft command execute with error', async () => { when(nodeClient.sendCommand) .calledWith(jasmine.objectContaining({ name: 'FT._LIST' })) - .mockResolvedValue(mockFTListResponse_1); + .mockRejectedValue("some error"); const redisServerRecommendation = await service - .determineRediSearchRecommendation(nodeClient, [mockJSONKey]); + .determineRediSearchRecommendation(nodeClient, [mockRediSearchStringKey_2]); expect(redisServerRecommendation).toEqual(null); }); }); diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index 2656b49cc5..291578e88b 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -3,7 +3,7 @@ import { Redis, Cluster, Command } from 'ioredis'; import { get } from 'lodash'; import * as semverCompare from 'node-version-compare'; import { convertRedisInfoReplyToObject, convertBulkStringsToObject, convertStringsArrayToObject } from 'src/utils'; -import { RECOMMENDATION_NAMES, IS_TIMESTAMP, REDISEARCH_MODULES } from 'src/constants'; +import { RECOMMENDATION_NAMES, IS_TIMESTAMP } from 'src/constants'; import { RedisDataType } from 'src/modules/browser/dto'; import { Recommendation } from 'src/modules/database-analysis/models/recommendation'; import { Key } from 'src/modules/database-analysis/models'; @@ -338,20 +338,15 @@ export class RecommendationProvider { keys: Key[], ): Promise { try { - const reply = await redisClient.sendCommand( - new Command('module', ['list'], { replyEncoding: 'utf8' }), - ) as any[]; - const modules = reply.map((module: any[]) => convertStringsArrayToObject(module)); - const isRediSearchModule = modules.some(({ name }) => REDISEARCH_MODULES - .some((search: string) => name === search)); - - if (isRediSearchModule) { + try { const indexes = await redisClient.sendCommand( new Command('FT._LIST', [], { replyEncoding: 'utf8' }), ) as any[]; if (indexes.length) { return null; } + } catch (err) { + // Ignore errors } const isBigStringOrJSON = keys.some((key) => ( From 9fdf09278f881c7591bdefd78787d7dc05478522 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 9 Jan 2023 16:00:00 +0400 Subject: [PATCH 084/108] #RI-3527 - fix recommendation indexes --- redisinsight/ui/src/constants/dbAnalysisRecommendations.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index c4fea11619..3737dc96ef 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -598,7 +598,7 @@ "value": "Create a " }, { - "id": "4", + "id": "6", "type": "link", "value": { "href": "https://redis.com/try-free/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight_recommendations/", @@ -606,7 +606,7 @@ } }, { - "id": "5", + "id": "7", "type": "span", "value": " which extends the core capabilities of Redis OSS and uses modern data models and processing engines." } From d9eb141d546af04a94ccac8fee2841f97525d2f2 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Tue, 10 Jan 2023 14:24:47 +0400 Subject: [PATCH 085/108] #RI-3973 - update rts recommendation --- redisinsight/api/package.json | 1 + redisinsight/api/src/constants/regex.ts | 3 +- .../providers/recommendation.provider.spec.ts | 50 ++++++++++++------- .../providers/recommendation.provider.ts | 33 ++++++++++-- redisinsight/api/yarn.lock | 5 ++ 5 files changed, 67 insertions(+), 25 deletions(-) diff --git a/redisinsight/api/package.json b/redisinsight/api/package.json index 865b996805..65f9e7d0ed 100644 --- a/redisinsight/api/package.json +++ b/redisinsight/api/package.json @@ -53,6 +53,7 @@ "body-parser": "^1.19.0", "class-transformer": "^0.2.3", "class-validator": "^0.12.2", + "date-fns": "^2.29.3", "dotenv": "^16.0.0", "express": "^4.17.1", "fs-extra": "^10.0.0", diff --git a/redisinsight/api/src/constants/regex.ts b/redisinsight/api/src/constants/regex.ts index 8506d70ccd..08207685e3 100644 --- a/redisinsight/api/src/constants/regex.ts +++ b/redisinsight/api/src/constants/regex.ts @@ -1,6 +1,7 @@ export const ARG_IN_QUOTATION_MARKS_REGEX = /"[^"]*|'[^']*'|"+/g; export const IS_INTEGER_NUMBER_REGEX = /^\d+$/; +export const IS_NUMBER_REGEX = /^-?\d*(\.\d+)?$/; export const IS_NON_PRINTABLE_ASCII_CHARACTER = /[^ -~\u0007\b\t\n\r]/; export const IP_ADDRESS_REGEX = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; export const PRIVATE_IP_ADDRESS_REGEX = /(^127\.)|(^10\.)|(^172\.1[6-9]\.)|(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^192\.168\.)/; -export const IS_TIMESTAMP = /^\d{10,}$/; +export const IS_TIMESTAMP = /^(\d{10}|\d{13}|\d{16}|\d{19})$/; diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts index 760a5a25a1..0a347eea03 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts @@ -37,13 +37,39 @@ const mockRedisAclListResponse_2: string[] = [ const mockFTListResponse_1 = []; const mockFTListResponse_2 = ['idx']; +const generateRTSRecommendationTests = [ + { input: ['0', ['123', 123]], expected: null }, + { input: ['0', ['1234567891', 3]], expected: { name: RECOMMENDATION_NAMES.RTS } }, + { input: ['0', ['1234567891', 1234567891]], expected: { name: RECOMMENDATION_NAMES.RTS } }, + { input: ['0', ['123', 1234567891]], expected: { name: RECOMMENDATION_NAMES.RTS } }, + { input: ['0', ['123', 12345678911]], expected: null }, + { input: ['0', ['123', 1234567891234]], expected: { name: RECOMMENDATION_NAMES.RTS } }, + { input: ['0', ['123', 12345678912345]], expected: null }, + { input: ['0', ['123', 1234567891234567]], expected: { name: RECOMMENDATION_NAMES.RTS } }, + { input: ['0', ['12345678912345678', 1]], expected: null }, + { input: ['0', ['1234567891234567891', 1]], expected: { name: RECOMMENDATION_NAMES.RTS } }, + { input: ['0', ['1', 1234567891.2]], expected: { name: RECOMMENDATION_NAMES.RTS } }, + { input: ['0', ['1234567891.2', 1]], expected: { name: RECOMMENDATION_NAMES.RTS } }, + { input: ['0', ['1234567891:12', 1]], expected: { name: RECOMMENDATION_NAMES.RTS } }, + { input: ['0', ['1234567891a12', 1]], expected: { name: RECOMMENDATION_NAMES.RTS } }, + { input: ['0', ['1234567891.2.2', 1]], expected: null }, + { input: ['0', ['1234567891asd', 1]], expected: null }, + { input: ['0', ['10-10-2020', 1]], expected: { name: RECOMMENDATION_NAMES.RTS } }, + { input: ['0', ['', 1]], expected: null }, + { input: ['0', ['1', -12]], expected: null }, + { input: ['0', ['1', -1234567891]], expected: null }, + { input: ['0', ['1', -1234567891.123]], expected: null }, + { input: ['0', ['1', -1234567891.123]], expected: null }, + { input: ['0', ['1234567891.-123', 1]], expected: null }, +]; + const mockZScanResponse_1 = [ '0', - [123456789, 123456789, 12345678910, 12345678910], + ['1', 1, '12345678910', 12345678910], ]; const mockZScanResponse_2 = [ '0', - [12345678910, 12345678910, 1, 1], + ['12345678910', 12345678910, 1, 1], ]; const mockKeys = [ @@ -489,28 +515,14 @@ describe('RecommendationProvider', () => { }); describe('determineRTSRecommendation', () => { - it('should not return RTS recommendation', async () => { + test.each(generateRTSRecommendationTests)('%j', async ({ input, expected }) => { when(nodeClient.sendCommand) .calledWith(jasmine.objectContaining({ name: 'zscan' })) - .mockResolvedValue(mockZScanResponse_1); + .mockResolvedValue(input); const RTSRecommendation = await service .determineRTSRecommendation(nodeClient, mockKeys); - expect(RTSRecommendation).toEqual(null); - }); - - it('should return RTS recommendation', async () => { - when(nodeClient.sendCommand) - .calledWith(jasmine.objectContaining({ name: 'zscan' })) - .mockResolvedValueOnce(mockZScanResponse_1); - - when(nodeClient.sendCommand) - .calledWith(jasmine.objectContaining({ name: 'zscan' })) - .mockResolvedValue(mockZScanResponse_2); - - const RTSRecommendation = await service - .determineRTSRecommendation(nodeClient, mockSortedSets); - expect(RTSRecommendation).toEqual({ name: RECOMMENDATION_NAMES.RTS }); + expect(RTSRecommendation).toEqual(expected); }); it('should not return RTS recommendation when only 101 sorted set contain timestamp', async () => { diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index 291578e88b..b79f301ef5 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -1,9 +1,12 @@ import { Injectable, Logger } from '@nestjs/common'; import { Redis, Cluster, Command } from 'ioredis'; -import { get } from 'lodash'; +import { get, isNumber } from 'lodash'; +import { isValid } from 'date-fns'; import * as semverCompare from 'node-version-compare'; -import { convertRedisInfoReplyToObject, convertBulkStringsToObject, convertStringsArrayToObject } from 'src/utils'; -import { RECOMMENDATION_NAMES, IS_TIMESTAMP } from 'src/constants'; +import { convertRedisInfoReplyToObject, convertBulkStringsToObject } from 'src/utils'; +import { + RECOMMENDATION_NAMES, IS_TIMESTAMP, IS_INTEGER_NUMBER_REGEX, IS_NUMBER_REGEX, +} from 'src/constants'; import { RedisDataType } from 'src/modules/browser/dto'; import { Recommendation } from 'src/modules/database-analysis/models/recommendation'; import { Key } from 'src/modules/database-analysis/models'; @@ -311,8 +314,7 @@ export class RecommendationProvider { // get first member-score pair new Command('zscan', [keys[processedKeysNumber].name, '0', 'COUNT', 2], { replyEncoding: 'utf8' }), ) as string[]; - // check is pair member-score is timestamp - if (IS_TIMESTAMP.test(membersArray[0]) && IS_TIMESTAMP.test(membersArray[1])) { + if (this.checkTimestamp(membersArray[0]) || this.checkTimestamp(membersArray[1])) { isTimeSeries = true; } processedKeysNumber += 1; @@ -421,4 +423,25 @@ export class RecommendationProvider { } return false; } + + private checkTimestamp(value: string): boolean { + try { + if (!IS_NUMBER_REGEX.test(value) && isValid(new Date(value))) { + return true; + } + const integerPart = parseInt(value, 10); + if (!IS_TIMESTAMP.test(integerPart.toString())) { + return false; + } + if (isNumber(value) || integerPart.toString().length === value.length) { + return true; + } + // check part after separator + const subPart = value.replace(integerPart.toString(), ''); + return IS_INTEGER_NUMBER_REGEX.test(subPart.substring(1, subPart.length)); + } catch (err) { + // ignore errors + return false; + } + } } diff --git a/redisinsight/api/yarn.lock b/redisinsight/api/yarn.lock index beaebe7663..0c4a40f848 100644 --- a/redisinsight/api/yarn.lock +++ b/redisinsight/api/yarn.lock @@ -2626,6 +2626,11 @@ date-fns@^2.28.0: resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.2.tgz#0d4b3d0f3dff0f920820a070920f0d9662c51931" integrity sha512-0VNbwmWJDS/G3ySwFSJA3ayhbURMTJLtwM2DTxf9CWondCnh6DTNlO9JgRSq6ibf4eD0lfMJNBxUdEAHHix+bA== +date-fns@^2.29.3: + version "2.29.3" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" + integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA== + debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" From 110acd704c650ede998171250a57afddc292dda1 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Wed, 11 Jan 2023 09:46:19 +0400 Subject: [PATCH 086/108] #RI-3971 - add search indexes recommnedation --- .../api/src/constants/recommendations.ts | 1 + .../database-analysis.service.ts | 56 ++++++++---- .../providers/recommendation.provider.ts | 88 +++++++++++++++++++ .../recommendation/recommendation.service.ts | 5 +- .../constants/dbAnalysisRecommendations.json | 54 ++++++++++++ .../Recommendations.spec.tsx | 15 ++++ 6 files changed, 202 insertions(+), 17 deletions(-) diff --git a/redisinsight/api/src/constants/recommendations.ts b/redisinsight/api/src/constants/recommendations.ts index 80b50e5b66..bb698e06dc 100644 --- a/redisinsight/api/src/constants/recommendations.ts +++ b/redisinsight/api/src/constants/recommendations.ts @@ -16,4 +16,5 @@ export const RECOMMENDATION_NAMES = Object.freeze({ RTS: 'RTS', REDIS_VERSION: 'redisVersion', REDIS_SEARCH: 'redisSearch', + SEARCH_INDEXES: 'searchIndexes', }); diff --git a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts index a919e7b660..25f5720b20 100644 --- a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts +++ b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts @@ -1,5 +1,5 @@ import { HttpException, Injectable, Logger } from '@nestjs/common'; -import { isNull, flatten, uniqBy } from 'lodash'; +import { isNull, flatten, concat } from 'lodash'; import { RecommendationService } from 'src/modules/recommendation/recommendation.service'; import { catchAclError } from 'src/utils'; import { RECOMMENDATION_NAMES } from 'src/constants'; @@ -56,18 +56,41 @@ export class DatabaseAnalysisService { }); const recommendations = DatabaseAnalysisService.getRecommendationsSummary( - flatten(await Promise.all( - scanResults.map(async (nodeResult, idx) => ( - await this.recommendationService.getRecommendations({ - client: nodeResult.client, - keys: nodeResult.keys, - total: progress.total, - // TODO: create generic solution to exclude recommendations - exclude: idx !== 0 ? [RECOMMENDATION_NAMES.RTS] : [], - }) - )), - )), + // flatten(await Promise.all( + // scanResults.map(async (nodeResult, idx) => ( + // await this.recommendationService.getRecommendations({ + // client: nodeResult.client, + // keys: nodeResult.keys, + // total: progress.total, + // globalClient: client, + // // TODO: create generic solution to exclude recommendations + // exclude: idx !== 0 ? [RECOMMENDATION_NAMES.RTS] : [], + // }) + // )), + // )), + await scanResults.reduce(async (previousPromise, nodeResult) => { + const jobsArray = await previousPromise; + let recommendationToExclude = []; + const nodeRecommendations = await this.recommendationService.getRecommendations({ + client: nodeResult.client, + keys: nodeResult.keys, + total: progress.total, + globalClient: client, + exclude: recommendationToExclude, + // TODO: create generic solution to exclude recommendations + // exclude: idx !== 0 ? [RECOMMENDATION_NAMES.RTS] : [], + }); + recommendationToExclude = concat(recommendationToExclude, [RECOMMENDATION_NAMES.RTS]); + const foundedRecommendations = nodeRecommendations.filter((recommendation) => !isNull(recommendation)); + const foundedRecommendationNames = foundedRecommendations.map(({ name }) => name); + recommendationToExclude = concat(recommendationToExclude, foundedRecommendationNames); + jobsArray.push(foundedRecommendations); + console.log(recommendationToExclude); + console.log(foundedRecommendations); + return flatten(jobsArray); + }, Promise.resolve([])), ); + const analysis = plainToClass(DatabaseAnalysis, await this.analyzer.analyze({ databaseId: clientMetadata.databaseId, ...dto, @@ -111,9 +134,10 @@ export class DatabaseAnalysisService { */ static getRecommendationsSummary(recommendations: Recommendation[]): Recommendation[] { - return uniqBy( - recommendations.filter((recommendation) => !isNull(recommendation)), - 'name', - ); + // return uniqBy( + // recommendations, + // 'name', + // ); + return recommendations; } } diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index b79f301ef5..6c6d346146 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -7,6 +7,7 @@ import { convertRedisInfoReplyToObject, convertBulkStringsToObject } from 'src/u import { RECOMMENDATION_NAMES, IS_TIMESTAMP, IS_INTEGER_NUMBER_REGEX, IS_NUMBER_REGEX, } from 'src/constants'; +import { checkRedirectionError, parseRedirectionError } from 'src/utils/cli-helper'; import { RedisDataType } from 'src/modules/browser/dto'; import { Recommendation } from 'src/modules/database-analysis/models/recommendation'; import { Key } from 'src/modules/database-analysis/models'; @@ -410,6 +411,93 @@ export class RecommendationProvider { return null; } } + /** + * Check search indexes recommendation + * @param redisClient + * @param keys + * @param isCluster + */ + + async determineSearchIndexesRecommendation( + redisClient: Redis, + keys: Key[], + client: any, + ): Promise { + if (client.isCluster) { + let processedKeysNumber = 0; + let isJSONOrHash = false; + let sortedSetNumber = 0; + while ( + processedKeysNumber < keys.length + && !isJSONOrHash + && sortedSetNumber <= sortedSetCountForCheck + ) { + if (keys[processedKeysNumber].type !== RedisDataType.ZSet) { + processedKeysNumber += 1; + } else { + let keyBySortedSetMember; + const sortedSetMember = await redisClient.sendCommand( + new Command('zrange', [keys[processedKeysNumber].name, 0, 0], { replyEncoding: 'utf8' }), + ) as string[]; + + try { + keyBySortedSetMember = await redisClient.sendCommand( + new Command('type', [sortedSetMember[0]], { replyEncoding: 'utf8' }), + ) as string; + } catch (err) { + if (err && checkRedirectionError(err)) { + const { address } = parseRedirectionError(err); + const nodes = client.nodes('master'); + + const node: any = nodes.find(({ options: { host, port } }: Redis) => `${host}:${port}` === address); + // if (!node) { + // node = nodeRole === ClusterNodeRole.All + // ? nodeAddress + // : `${nodeAddress} [${nodeRole.toLowerCase()}]`; + // // throw new ClusterNodeNotFoundError( + // // ERROR_MESSAGES.CLUSTER_NODE_NOT_FOUND(node), + // // ); + // } + + keyBySortedSetMember = await node.sendCommand( + new Command('type', [sortedSetMember[0]], { replyEncoding: 'utf8' }), + ) as string; + } + } + if (keyBySortedSetMember === RedisDataType.JSON || keyBySortedSetMember === RedisDataType.Hash) { + isJSONOrHash = true; + } + processedKeysNumber += 1; + sortedSetNumber += 1; + } + } + + return isJSONOrHash ? { name: RECOMMENDATION_NAMES.SEARCH_INDEXES } : null; + } + try { + const sortedSets = keys + .filter(({ type }) => type === RedisDataType.ZSet) + .slice(0, 100); + const res = await redisClient.pipeline(sortedSets.map(({ name }) => ([ + 'zrange', + name, + 0, + 0, + ]))).exec(); + + const types = await redisClient.pipeline(res.map(([, member]) => ([ + 'type', + member, + ]))).exec(); + + const isHashOrJSONName = types.some(([, type]) => type === RedisDataType.JSON || type === RedisDataType.Hash) + return isHashOrJSONName ? { name: RECOMMENDATION_NAMES.SEARCH_INDEXES } : null; + } catch (err) { + this.logger.error('Can not determine search indexes recommendation', err); + return null; + } + } + private async checkAuth(redisClient: Redis | Cluster): Promise { try { diff --git a/redisinsight/api/src/modules/recommendation/recommendation.service.ts b/redisinsight/api/src/modules/recommendation/recommendation.service.ts index 50517b2437..fed7151ce7 100644 --- a/redisinsight/api/src/modules/recommendation/recommendation.service.ts +++ b/redisinsight/api/src/modules/recommendation/recommendation.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { Redis } from 'ioredis'; +import { Redis, Cluster } from 'ioredis'; import { RecommendationProvider } from 'src/modules/recommendation/providers/recommendation.provider'; import { Recommendation } from 'src/modules/database-analysis/models/recommendation'; import { RECOMMENDATION_NAMES } from 'src/constants'; @@ -11,6 +11,7 @@ interface RecommendationInput { keys?: Key[], info?: RedisString, total?: number, + globalClient?: Redis | Cluster, exclude?: string[], } @@ -33,6 +34,7 @@ export class RecommendationService { keys, info, total, + globalClient, exclude, } = dto; @@ -55,6 +57,7 @@ export class RecommendationService { exclude.includes(RECOMMENDATION_NAMES.RTS) ? null : await this.recommendationProvider.determineRTSRecommendation(client, keys), await this.recommendationProvider.determineRediSearchRecommendation(client, keys), await this.recommendationProvider.determineRedisVersionRecommendation(client), + await this.recommendationProvider.determineSearchIndexesRecommendation(client, keys, globalClient), await this.recommendationProvider.determineSetPasswordRecommendation(client), ])); } diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index 3737dc96ef..ead17c1329 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -612,5 +612,59 @@ } ], "badges": ["upgrade"] + }, + "searchIndexes": { + "id": "searchIndexes", + "title":"Enhance your search indexes", + "redisStack": true, + "content": [ + { + "id": "1", + "type": "paragraph", + "value": "Creating your own index structure manually? Consider the out-of-box option of FT.CREATE on RediSearch." + }, + { + "id": "2", + "type": "span", + "value": "RediSearch was designed to help meet your query needs and support a better development experience when dealing with complex data scenarios. Take a look at the powerful API options " + }, + { + "id": "3", + "type": "link", + "value": { + "href": "https://redis.io/commands/?name=Ft", + "name": "here" + } + }, + { + "id": "4", + "type": "span", + "value": " and try it." + }, + { + "id": "5", + "type": "spacer", + "value": "l" + }, + { + "id": "6", + "type": "span", + "value": "Create a " + }, + { + "id": "7", + "type": "link", + "value": { + "href": "https://redis.com/try-free/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight_recommendations/", + "name": "free Redis Stack database" + } + }, + { + "id": "8", + "type": "span", + "value": " to use modern data models and processing engines." + } + ], + "badges": ["upgrade"] } } diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx index 99e978fd51..2c0fb8d906 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx @@ -294,6 +294,21 @@ describe('Recommendations', () => { expect(screen.queryByTestId('configuration_changes')).not.toBeInTheDocument() }) + it('should render upgrade badge in searchIndexes recommendation', () => { + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: { + recommendations: [{ name: 'searchIndexes' }] + } + })) + + render() + + expect(screen.queryByTestId('code_changes')).not.toBeInTheDocument() + expect(screen.queryByTestId('upgrade')).toBeInTheDocument() + expect(screen.queryByTestId('configuration_changes')).not.toBeInTheDocument() + }) + it('should collapse/expand and sent proper telemetry event', () => { (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ ...mockdbAnalysisSelector, From cdd74a99d3fa20e7b4ebe1c819199c9b496e45fb Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Wed, 11 Jan 2023 13:08:05 +0300 Subject: [PATCH 087/108] #RI-3927 - add recommendations tab highlighting --- .../components/analytics-tabs/constants.ts | 21 --------- .../components/analytics-tabs/constants.tsx | 44 ++++++++++++++++++ .../HighlightedFeature.tsx | 5 +- .../navigation-menu/styles.module.scss | 4 +- .../ui/src/constants/featuresHighlighting.tsx | 8 ++-- .../components/data-nav-tabs/constants.tsx | 31 +++++++++++-- .../home/components/HomeHeader/HomeHeader.tsx | 46 ++++++------------- .../ui/src/styles/components/_tabs.scss | 8 +++- 8 files changed, 102 insertions(+), 65 deletions(-) delete mode 100644 redisinsight/ui/src/components/analytics-tabs/constants.ts create mode 100644 redisinsight/ui/src/components/analytics-tabs/constants.tsx diff --git a/redisinsight/ui/src/components/analytics-tabs/constants.ts b/redisinsight/ui/src/components/analytics-tabs/constants.ts deleted file mode 100644 index b813398d40..0000000000 --- a/redisinsight/ui/src/components/analytics-tabs/constants.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { AnalyticsViewTab } from 'uiSrc/slices/interfaces/analytics' - -interface AnalyticsTabs { - id: AnalyticsViewTab, - label: string, -} - -export const analyticsViewTabs: AnalyticsTabs[] = [ - { - id: AnalyticsViewTab.ClusterDetails, - label: 'Overview', - }, - { - id: AnalyticsViewTab.DatabaseAnalysis, - label: 'Database Analysis', - }, - { - id: AnalyticsViewTab.SlowLog, - label: 'Slow Log', - }, -] diff --git a/redisinsight/ui/src/components/analytics-tabs/constants.tsx b/redisinsight/ui/src/components/analytics-tabs/constants.tsx new file mode 100644 index 0000000000..66922f4acb --- /dev/null +++ b/redisinsight/ui/src/components/analytics-tabs/constants.tsx @@ -0,0 +1,44 @@ +import React, { ReactNode } from 'react' +import { AnalyticsViewTab } from 'uiSrc/slices/interfaces/analytics' +import { useSelector } from 'react-redux' +import { appFeaturesToHighlightSelector } from 'uiSrc/slices/app/features-highlighting' +import HighlightedFeature from 'uiSrc/components/hightlighted-feature/HighlightedFeature' +import { BUILD_FEATURES } from 'uiSrc/constants/featuresHighlighting' + +interface AnalyticsTabs { + id: AnalyticsViewTab + label: string | ReactNode +} + +const DatabaseAnalyticsTab = () => { + const { recommendations: recommendationsHighlighting } = useSelector(appFeaturesToHighlightSelector) ?? {} + + return ( + <> + + Database Analysis + + + ) +} + +export const analyticsViewTabs: AnalyticsTabs[] = [ + { + id: AnalyticsViewTab.ClusterDetails, + label: 'Overview', + }, + { + id: AnalyticsViewTab.DatabaseAnalysis, + label: , + }, + { + id: AnalyticsViewTab.SlowLog, + label: 'Slow Log', + }, +] diff --git a/redisinsight/ui/src/components/hightlighted-feature/HighlightedFeature.tsx b/redisinsight/ui/src/components/hightlighted-feature/HighlightedFeature.tsx index f26c1f5532..4e237b37bb 100644 --- a/redisinsight/ui/src/components/hightlighted-feature/HighlightedFeature.tsx +++ b/redisinsight/ui/src/components/hightlighted-feature/HighlightedFeature.tsx @@ -1,3 +1,4 @@ +import { isString } from 'lodash' import { EuiToolTip } from '@elastic/eui' import { ToolTipPositions } from '@elastic/eui/src/components/tool_tip/tool_tip' import cx from 'classnames' @@ -8,7 +9,7 @@ import styles from './styles.modules.scss' export interface Props { isHighlight?: boolean - children: React.ReactElement + children: React.ReactElement | string title?: string | React.ReactElement content?: string | React.ReactElement type?: FeaturesHighlightingType @@ -36,7 +37,7 @@ const HighlightedFeature = (props: Props) => { dataTestPostfix = '' } = props - const innerContent = hideFirstChild ? children.props.children : children + const innerContent = hideFirstChild && !isString(children) ? children.props.children : children const DotHighlighting = () => ( <> diff --git a/redisinsight/ui/src/components/navigation-menu/styles.module.scss b/redisinsight/ui/src/components/navigation-menu/styles.module.scss index 98e62a61d5..a0863d287d 100644 --- a/redisinsight/ui/src/components/navigation-menu/styles.module.scss +++ b/redisinsight/ui/src/components/navigation-menu/styles.module.scss @@ -207,8 +207,8 @@ $sideBarWidth: 60px; } .highlightDot { - top: 12px !important; - right: 12px !important; + top: 11px !important; + right: 11px !important; &.activePage { background-color: #465282 !important; diff --git a/redisinsight/ui/src/constants/featuresHighlighting.tsx b/redisinsight/ui/src/constants/featuresHighlighting.tsx index 6dd60756de..14f491dbbb 100644 --- a/redisinsight/ui/src/constants/featuresHighlighting.tsx +++ b/redisinsight/ui/src/constants/featuresHighlighting.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { PageNames } from 'uiSrc/constants/pages' export type FeaturesHighlightingType = 'plain' | 'tooltip' | 'popover' @@ -9,9 +10,10 @@ interface BuildHighlightingFeature { page?: string } export const BUILD_FEATURES: { [key: string]: BuildHighlightingFeature } = { - importDatabases: { + recommendations: { type: 'tooltip', - title: 'Import Database Connections', - content: 'Import your database connections from other Redis UIs' + title: 'Database Recommendations', + content: 'Run database analysis to get recommendations for optimizing your database.', + page: PageNames.analytics } } diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/data-nav-tabs/constants.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/data-nav-tabs/constants.tsx index 25b5935871..85bb925c00 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/data-nav-tabs/constants.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/data-nav-tabs/constants.tsx @@ -1,14 +1,39 @@ import React, { ReactNode } from 'react' +import { useDispatch, useSelector } from 'react-redux' + import { DatabaseAnalysisViewTab } from 'uiSrc/slices/interfaces/analytics' -import AnalysisDataView from '../analysis-data-view' +import { appFeaturesToHighlightSelector, removeFeatureFromHighlighting } from 'uiSrc/slices/app/features-highlighting' +import { BUILD_FEATURES } from 'uiSrc/constants/featuresHighlighting' +import HighlightedFeature from 'uiSrc/components/hightlighted-feature/HighlightedFeature' + import Recommendations from '../recommendations-view' +import AnalysisDataView from '../analysis-data-view' interface DatabaseAnalysisTabs { id: DatabaseAnalysisViewTab, - name: (count?: number) => string, + name: (count?: number) => string | ReactNode, content: ReactNode } +const RecommendationsTab = ({ count }: { count?: number }) => { + const { recommendations: recommendationsHighlighting } = useSelector(appFeaturesToHighlightSelector) ?? {} + + const dispatch = useDispatch() + + return count ? ( + <> + dispatch(removeFeatureFromHighlighting('recommendations'))} + dotClassName="tab-highlighting-dot" + > + <>Recommendations ({count}) + + + ) : (<>Recommendations) +} + export const databaseAnalysisTabs: DatabaseAnalysisTabs[] = [ { id: DatabaseAnalysisViewTab.DataSummary, @@ -17,7 +42,7 @@ export const databaseAnalysisTabs: DatabaseAnalysisTabs[] = [ }, { id: DatabaseAnalysisViewTab.Recommendations, - name: (count?: number) => (count ? `Recommendations (${count})` : 'Recommendations'), + name: (count) => , content: }, ] diff --git a/redisinsight/ui/src/pages/home/components/HomeHeader/HomeHeader.tsx b/redisinsight/ui/src/pages/home/components/HomeHeader/HomeHeader.tsx index de6aa94762..959ffc6da9 100644 --- a/redisinsight/ui/src/pages/home/components/HomeHeader/HomeHeader.tsx +++ b/redisinsight/ui/src/pages/home/components/HomeHeader/HomeHeader.tsx @@ -9,15 +9,9 @@ import { EuiToolTip, } from '@elastic/eui' import { isEmpty } from 'lodash' -import { useDispatch, useSelector } from 'react-redux' +import { useSelector } from 'react-redux' import cx from 'classnames' import { ImportDatabasesDialog } from 'uiSrc/components' -import HighlightedFeature from 'uiSrc/components/hightlighted-feature/HighlightedFeature' -import { BUILD_FEATURES } from 'uiSrc/constants/featuresHighlighting' -import { - appFeaturesToHighlightSelector, - removeFeatureFromHighlighting -} from 'uiSrc/slices/app/features-highlighting' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import HelpLinksMenu from 'uiSrc/pages/home/components/HelpLinksMenu' import PromoLink from 'uiSrc/components/promo-link/PromoLink' @@ -48,10 +42,6 @@ const HomeHeader = ({ onAddInstance, direction, welcomePage = false }: Props) => const [guides, setGuides] = useState([]) const [isImportDialogOpen, setIsImportDialogOpen] = useState(false) - const { importDatabases: importDatabasesHighlighting } = useSelector(appFeaturesToHighlightSelector) ?? {} - - const dispatch = useDispatch() - useEffect(() => { if (loading || !data || isEmpty(data)) { return @@ -126,30 +116,20 @@ const HomeHeader = ({ onAddInstance, direction, welcomePage = false }: Props) => ) const ImportDatabasesBtn = () => ( - dispatch(removeFeatureFromHighlighting('importDatabases'))} - transformOnHover - hideFirstChild + - - - - - - + + + ) const Guides = () => ( diff --git a/redisinsight/ui/src/styles/components/_tabs.scss b/redisinsight/ui/src/styles/components/_tabs.scss index b03e7f503e..4c22c64a14 100644 --- a/redisinsight/ui/src/styles/components/_tabs.scss +++ b/redisinsight/ui/src/styles/components/_tabs.scss @@ -49,7 +49,7 @@ .tabs-active-borders { .euiTab { border-radius: 0; - padding: 8px 12px !important; + padding: 0 !important; border-bottom: 1px solid var(--separatorColor); color: var(--euiTextSubduedColor) !important; @@ -63,9 +63,15 @@ font-size: 13px !important; line-height: 18px !important; font-weight: 500 !important; + padding: 8px 12px; } } + .tab-highlighting-dot { + top: -6px; + right: -12px; + } + .euiTab + .euiTab { margin-left: 0 !important; From 9588cc101c840cc60236e3d57179941a21c28924 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 12 Jan 2023 09:32:30 +0400 Subject: [PATCH 088/108] #RI-3572-dangerous commands recommendation --- .../api/src/constants/recommendations.ts | 1 + .../database-analysis.service.ts | 69 ++++-------- .../models/recommendation.ts | 9 +- .../providers/recommendation.provider.ts | 50 +++++++-- .../recommendation/recommendation.service.ts | 104 ++++++++++++++---- .../constants/dbAnalysisRecommendations.json | 51 +++++++-- .../Recommendations.spec.tsx | 14 +++ .../recommendations-view/Recommendations.tsx | 4 +- .../components/recommendations-view/utils.tsx | 43 ++++++-- 9 files changed, 245 insertions(+), 100 deletions(-) diff --git a/redisinsight/api/src/constants/recommendations.ts b/redisinsight/api/src/constants/recommendations.ts index bb698e06dc..a53422829c 100644 --- a/redisinsight/api/src/constants/recommendations.ts +++ b/redisinsight/api/src/constants/recommendations.ts @@ -17,4 +17,5 @@ export const RECOMMENDATION_NAMES = Object.freeze({ REDIS_VERSION: 'redisVersion', REDIS_SEARCH: 'redisSearch', SEARCH_INDEXES: 'searchIndexes', + DANGEROUS_COMMANDS: 'dangerousCommands', }); diff --git a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts index 25f5720b20..cf36a38a4c 100644 --- a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts +++ b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts @@ -2,14 +2,12 @@ import { HttpException, Injectable, Logger } from '@nestjs/common'; import { isNull, flatten, concat } from 'lodash'; import { RecommendationService } from 'src/modules/recommendation/recommendation.service'; import { catchAclError } from 'src/utils'; -import { RECOMMENDATION_NAMES } from 'src/constants'; import { DatabaseAnalyzer } from 'src/modules/database-analysis/providers/database-analyzer'; import { plainToClass } from 'class-transformer'; import { DatabaseAnalysis, ShortDatabaseAnalysis } from 'src/modules/database-analysis/models'; import { DatabaseAnalysisProvider } from 'src/modules/database-analysis/providers/database-analysis.provider'; import { CreateDatabaseAnalysisDto } from 'src/modules/database-analysis/dto'; import { KeysScanner } from 'src/modules/database-analysis/scanner/keys-scanner'; -import { Recommendation } from 'src/modules/database-analysis/models/recommendation'; import { DatabaseConnectionService } from 'src/modules/database/database-connection.service'; import { ClientMetadata } from 'src/common/models'; @@ -55,41 +53,25 @@ export class DatabaseAnalysisService { progress.total += nodeResult.progress.total; }); - const recommendations = DatabaseAnalysisService.getRecommendationsSummary( - // flatten(await Promise.all( - // scanResults.map(async (nodeResult, idx) => ( - // await this.recommendationService.getRecommendations({ - // client: nodeResult.client, - // keys: nodeResult.keys, - // total: progress.total, - // globalClient: client, - // // TODO: create generic solution to exclude recommendations - // exclude: idx !== 0 ? [RECOMMENDATION_NAMES.RTS] : [], - // }) - // )), - // )), - await scanResults.reduce(async (previousPromise, nodeResult) => { - const jobsArray = await previousPromise; - let recommendationToExclude = []; - const nodeRecommendations = await this.recommendationService.getRecommendations({ - client: nodeResult.client, - keys: nodeResult.keys, - total: progress.total, - globalClient: client, - exclude: recommendationToExclude, - // TODO: create generic solution to exclude recommendations - // exclude: idx !== 0 ? [RECOMMENDATION_NAMES.RTS] : [], - }); - recommendationToExclude = concat(recommendationToExclude, [RECOMMENDATION_NAMES.RTS]); - const foundedRecommendations = nodeRecommendations.filter((recommendation) => !isNull(recommendation)); - const foundedRecommendationNames = foundedRecommendations.map(({ name }) => name); - recommendationToExclude = concat(recommendationToExclude, foundedRecommendationNames); - jobsArray.push(foundedRecommendations); - console.log(recommendationToExclude); - console.log(foundedRecommendations); - return flatten(jobsArray); - }, Promise.resolve([])), - ); + let recommendationToExclude = []; + + const recommendations = await scanResults.reduce(async (previousPromise, nodeResult) => { + const jobsArray = await previousPromise; + const nodeRecommendations = await this.recommendationService.getRecommendations({ + client: nodeResult.client, + keys: nodeResult.keys, + total: progress.total, + globalClient: client, + exclude: recommendationToExclude, + }); + // recommendationToExclude = concat(recommendationToExclude, [RECOMMENDATION_NAMES.RTS]); + const foundedRecommendations = nodeRecommendations.filter((recommendation) => !isNull(recommendation)); + const foundedRecommendationNames = foundedRecommendations.map(({ name }) => name); + recommendationToExclude = concat(recommendationToExclude, foundedRecommendationNames); + recommendationToExclude.push(...foundedRecommendationNames); + jobsArray.push(foundedRecommendations); + return flatten(jobsArray); + }, Promise.resolve([])); const analysis = plainToClass(DatabaseAnalysis, await this.analyzer.analyze({ databaseId: clientMetadata.databaseId, @@ -127,17 +109,4 @@ export class DatabaseAnalysisService { async list(databaseId: string): Promise { return this.databaseAnalysisProvider.list(databaseId); } - - /** - * Get recommendations summary - * @param recommendations - */ - - static getRecommendationsSummary(recommendations: Recommendation[]): Recommendation[] { - // return uniqBy( - // recommendations, - // 'name', - // ); - return recommendations; - } } diff --git a/redisinsight/api/src/modules/database-analysis/models/recommendation.ts b/redisinsight/api/src/modules/database-analysis/models/recommendation.ts index e3910bbfe1..9cd1b8ab3f 100644 --- a/redisinsight/api/src/modules/database-analysis/models/recommendation.ts +++ b/redisinsight/api/src/modules/database-analysis/models/recommendation.ts @@ -1,5 +1,5 @@ import { Expose } from 'class-transformer'; -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export class Recommendation { @ApiProperty({ @@ -9,4 +9,11 @@ export class Recommendation { }) @Expose() name: string; + + @ApiPropertyOptional({ + description: 'Additional recommendation params', + example: 'luaScript', + }) + @Expose() + params?: any; } diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index 6c6d346146..2164d2854d 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -7,6 +7,8 @@ import { convertRedisInfoReplyToObject, convertBulkStringsToObject } from 'src/u import { RECOMMENDATION_NAMES, IS_TIMESTAMP, IS_INTEGER_NUMBER_REGEX, IS_NUMBER_REGEX, } from 'src/constants'; +import ERROR_MESSAGES from 'src/constants/error-messages'; +import { ClusterNodeNotFoundError } from 'src/modules/cli/constants/errors'; import { checkRedirectionError, parseRedirectionError } from 'src/utils/cli-helper'; import { RedisDataType } from 'src/modules/browser/dto'; import { Recommendation } from 'src/modules/database-analysis/models/recommendation'; @@ -25,6 +27,8 @@ const bigStringMemory = 5_000_000; const sortedSetCountForCheck = 100; const minRedisVersion = '6'; +const redisInsightCommands = ['info', 'monitor', 'slowlog', 'acl', 'config', 'module']; + @Injectable() export class RecommendationProvider { private logger = new Logger('RecommendationProvider'); @@ -411,13 +415,14 @@ export class RecommendationProvider { return null; } } + /** * Check search indexes recommendation * @param redisClient * @param keys - * @param isCluster + * @param client */ - + // eslint-disable-next-line async determineSearchIndexesRecommendation( redisClient: Redis, keys: Key[], @@ -450,14 +455,11 @@ export class RecommendationProvider { const nodes = client.nodes('master'); const node: any = nodes.find(({ options: { host, port } }: Redis) => `${host}:${port}` === address); - // if (!node) { - // node = nodeRole === ClusterNodeRole.All - // ? nodeAddress - // : `${nodeAddress} [${nodeRole.toLowerCase()}]`; - // // throw new ClusterNodeNotFoundError( - // // ERROR_MESSAGES.CLUSTER_NODE_NOT_FOUND(node), - // // ); - // } + if (!node) { + throw new ClusterNodeNotFoundError( + ERROR_MESSAGES.CLUSTER_NODE_NOT_FOUND(node), + ); + } keyBySortedSetMember = await node.sendCommand( new Command('type', [sortedSetMember[0]], { replyEncoding: 'utf8' }), @@ -490,7 +492,7 @@ export class RecommendationProvider { member, ]))).exec(); - const isHashOrJSONName = types.some(([, type]) => type === RedisDataType.JSON || type === RedisDataType.Hash) + const isHashOrJSONName = types.some(([, type]) => type === RedisDataType.JSON || type === RedisDataType.Hash); return isHashOrJSONName ? { name: RECOMMENDATION_NAMES.SEARCH_INDEXES } : null; } catch (err) { this.logger.error('Can not determine search indexes recommendation', err); @@ -498,6 +500,32 @@ export class RecommendationProvider { } } + /* + * Check dangerous commands recommendation + * @param redisClient + */ + + async determineDangerousCommandsRecommendation( + redisClient: Redis | Cluster, + ): Promise { + try { + const dangerousCommands = await redisClient.sendCommand( + new Command('command', ['LIST', 'FILTERBY', 'aclcat', 'dangerous'], { replyEncoding: 'utf8' }), + ) as string[]; + + const filteredDangerousCommands = dangerousCommands.filter((command) => { + const commandName = command.split('|')[0]; + return !redisInsightCommands.includes(commandName); + }); + const commands = filteredDangerousCommands.join('\r\n').toUpperCase(); + return filteredDangerousCommands + ? { name: RECOMMENDATION_NAMES.DANGEROUS_COMMANDS, params: { commands } } + : null; + } catch (err) { + this.logger.error('Can not determine dangerous commands recommendation', err); + return null; + } + } private async checkAuth(redisClient: Redis | Cluster): Promise { try { diff --git a/redisinsight/api/src/modules/recommendation/recommendation.service.ts b/redisinsight/api/src/modules/recommendation/recommendation.service.ts index fed7151ce7..7562b5bb38 100644 --- a/redisinsight/api/src/modules/recommendation/recommendation.service.ts +++ b/redisinsight/api/src/modules/recommendation/recommendation.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; import { Redis, Cluster } from 'ioredis'; +import { difference } from 'lodash'; import { RecommendationProvider } from 'src/modules/recommendation/providers/recommendation.provider'; import { Recommendation } from 'src/modules/database-analysis/models/recommendation'; import { RECOMMENDATION_NAMES } from 'src/constants'; @@ -38,27 +39,88 @@ export class RecommendationService { exclude, } = dto; + const recommendations = new Map([ + [ + RECOMMENDATION_NAMES.LUA_SCRIPT, + async () => await this.recommendationProvider.determineLuaScriptRecommendation(client), + ], + [ + RECOMMENDATION_NAMES.BIG_HASHES, + async () => await this.recommendationProvider.determineBigHashesRecommendation(keys), + ], + [ + RECOMMENDATION_NAMES.USE_SMALLER_KEYS, + async () => await this.recommendationProvider.determineBigTotalRecommendation(total), + ], + [ + RECOMMENDATION_NAMES.AVOID_LOGICAL_DATABASES, + async () => await this.recommendationProvider.determineLogicalDatabasesRecommendation(client), + ], + [ + RECOMMENDATION_NAMES.COMBINE_SMALL_STRINGS_TO_HASHES, + async () => await this.recommendationProvider.determineCombineSmallStringsToHashesRecommendation(keys), + ], + [ + RECOMMENDATION_NAMES.INCREASE_SET_MAX_INTSET_ENTRIES, + async () => await this.recommendationProvider.determineIncreaseSetMaxIntsetEntriesRecommendation(client, keys), + ], + [ + RECOMMENDATION_NAMES.HASH_HASHTABLE_TO_ZIPLIST, + async () => await this.recommendationProvider.determineHashHashtableToZiplistRecommendation(client, keys), + ], + [ + RECOMMENDATION_NAMES.COMPRESS_HASH_FIELD_NAMES, + async () => await this.recommendationProvider.determineCompressHashFieldNamesRecommendation(keys), + ], + [ + RECOMMENDATION_NAMES.COMPRESSION_FOR_LIST, + async () => await this.recommendationProvider.determineCompressionForListRecommendation(keys), + ], + [ + RECOMMENDATION_NAMES.BIG_STRINGS, + async () => await this.recommendationProvider.determineBigStringsRecommendation(keys), + ], + [ + RECOMMENDATION_NAMES.ZSET_HASHTABLE_TO_ZIPLIST, + async () => await this.recommendationProvider.determineZSetHashtableToZiplistRecommendation(client, keys), + ], + [ + RECOMMENDATION_NAMES.BIG_SETS, + async () => await this.recommendationProvider.determineBigSetsRecommendation(keys), + ], + [ + RECOMMENDATION_NAMES.BIG_AMOUNT_OF_CONNECTED_CLIENTS, + async () => await this.recommendationProvider.determineConnectionClientsRecommendation(client), + ], + [ + RECOMMENDATION_NAMES.RTS, + async () => await this.recommendationProvider.determineRTSRecommendation(client, keys), + ], + [ + RECOMMENDATION_NAMES.REDIS_SEARCH, + async () => await this.recommendationProvider.determineRediSearchRecommendation(client, keys), + ], + [ + RECOMMENDATION_NAMES.REDIS_VERSION, + async () => await this.recommendationProvider.determineRedisVersionRecommendation(client), + ], + [ + RECOMMENDATION_NAMES.SEARCH_INDEXES, + async () => await this.recommendationProvider.determineSearchIndexesRecommendation(client, keys, globalClient), + ], + [ + RECOMMENDATION_NAMES.DANGEROUS_COMMANDS, + async () => await this.recommendationProvider.determineDangerousCommandsRecommendation(client), + ], + [ + RECOMMENDATION_NAMES.SET_PASSWORD, + async () => await this.recommendationProvider.determineSetPasswordRecommendation(client), + ], + ]); + + const recommendationsToDetermine = difference(Object.values(RECOMMENDATION_NAMES), exclude); + return ( - Promise.all([ - await this.recommendationProvider.determineLuaScriptRecommendation(client), - await this.recommendationProvider.determineBigHashesRecommendation(keys), - await this.recommendationProvider.determineBigTotalRecommendation(total), - await this.recommendationProvider.determineLogicalDatabasesRecommendation(client), - await this.recommendationProvider.determineCombineSmallStringsToHashesRecommendation(keys), - await this.recommendationProvider.determineIncreaseSetMaxIntsetEntriesRecommendation(client, keys), - await this.recommendationProvider.determineHashHashtableToZiplistRecommendation(client, keys), - await this.recommendationProvider.determineCompressHashFieldNamesRecommendation(keys), - await this.recommendationProvider.determineCompressionForListRecommendation(keys), - await this.recommendationProvider.determineBigStringsRecommendation(keys), - await this.recommendationProvider.determineZSetHashtableToZiplistRecommendation(client, keys), - await this.recommendationProvider.determineBigSetsRecommendation(keys), - await this.recommendationProvider.determineConnectionClientsRecommendation(client), - // TODO rework, need better solution to do not start determine recommendation - exclude.includes(RECOMMENDATION_NAMES.RTS) ? null : await this.recommendationProvider.determineRTSRecommendation(client, keys), - await this.recommendationProvider.determineRediSearchRecommendation(client, keys), - await this.recommendationProvider.determineRedisVersionRecommendation(client), - await this.recommendationProvider.determineSearchIndexesRecommendation(client, keys, globalClient), - await this.recommendationProvider.determineSetPasswordRecommendation(client), - ])); + Promise.all(recommendationsToDetermine.map((recommendation) => recommendations.get(recommendation)()))); } } diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index ead17c1329..17e7dc11d4 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -439,7 +439,7 @@ }, "bigAmountOfConnectedClients": { "id": "bigAmountOfConnectedClients", - "title":"Don't open a new connection for every request / every command", + "title": "Don't open a new connection for every request / every command", "content": [ { "id": "1", @@ -461,7 +461,7 @@ }, "setPassword": { "id": "setPassword", - "title":"Set the password", + "title": "Set the password", "content": [ { "id": "1", @@ -502,11 +502,6 @@ "type": "paragraph", "value": "If you are using sorted sets to work with time series data, consider using RedisTimeSeries to optimize the memory usage while having extraordinary query performance and small overhead during ingestion." }, - { - "id": "2", - "type": "spacer", - "value": "l" - }, { "id": "3", "type": "span", @@ -666,5 +661,47 @@ } ], "badges": ["upgrade"] + }, + "dangerousCommands": { + "id": "dangerousCommands", + "title": "Rename or disable dangerous commands", + "content": [ + { + "id": "2", + "type": "pre", + "parameter": ["commands"], + "value": "${0} " + }, + { + "id": "2", + "type": "span", + "parameter": ["commands"], + "value": " are currently not renamed or disabled for your Instance." + }, + { + "id": "2", + "type": "paragraph", + "value": "These commands are powerful and dangerous if not managed properly." + }, + { + "id": "3", + "type": "spacer", + "value": "l" + }, + { + "id": "4", + "type": "span", + "value": "Rename or disable them, especially for the production environment. " + }, + { + "id": "5", + "type": "link", + "value": { + "href": "https://redis.io/download/", + "name": "Read more" + } + } + ], + "badges": ["code_changes"] } } diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx index 2c0fb8d906..e1b73d4188 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.spec.tsx @@ -309,6 +309,20 @@ describe('Recommendations', () => { expect(screen.queryByTestId('configuration_changes')).not.toBeInTheDocument() }) + it('should render configuration_changes badge in dangerousCommands recommendation', () => { + (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ + ...mockdbAnalysisSelector, + data: { + recommendations: [{ name: 'dangerousCommands', params: { commands: 'some commands' } }] + } + })) + + render() + expect(screen.queryByTestId('code_changes')).toBeInTheDocument() + expect(screen.queryByTestId('upgrade')).not.toBeInTheDocument() + expect(screen.queryByTestId('configuration_changes')).not.toBeInTheDocument() + }) + it('should collapse/expand and sent proper telemetry event', () => { (dbAnalysisSelector as jest.Mock).mockImplementation(() => ({ ...mockdbAnalysisSelector, diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx index 6b0b37d417..68470e835e 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx @@ -102,7 +102,7 @@ const Recommendations = () => { {renderBadgesLegend()}
- {sortedRecommendations.map(({ name }) => { + {sortedRecommendations.map(({ name, params }) => { const { id = '', title = '', @@ -125,7 +125,7 @@ const Recommendations = () => { data-testid={`${id}-accordion`} > - {renderContent(content)} + {renderContent(content, params)}
diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx index 67e47706bb..168b26210d 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx @@ -1,5 +1,7 @@ import React from 'react' +import { isString, isArray } from 'lodash' import { + EuiText, EuiTextColor, EuiToolTip, EuiFlexGroup, @@ -7,6 +9,7 @@ import { EuiLink, EuiSpacer, } from '@elastic/eui' +import { SpacerSize } from '@elastic/eui/src/components/spacer/spacer' import cx from 'classnames' import { ReactComponent as CodeIcon } from 'uiSrc/assets/img/code-changes.svg' import { ReactComponent as ConfigurationIcon } from 'uiSrc/assets/img/configuration-changes.svg' @@ -17,7 +20,8 @@ import styles from './styles.module.scss' interface IContentElement { id: string type: string - value: any[] + value: any[] | any + parameter: string[] } const badgesContent = [ @@ -58,21 +62,44 @@ export const renderBadgesLegend = () => ( ) -const renderContentElement = ({ id, type, value }: IContentElement) => { +const replaceVariables = (value: any[] | any, parameter: string[], params: any) => ( + parameter && isString(value) ? value.replace(/\$\{\d}/i, (matched) => { + const parameterIndex: string = matched.substring( + matched.indexOf('{') + 1, + matched.lastIndexOf('}') + ) + return params[parameter[+parameterIndex]] + }) : value +) + +const renderContentElement = ({ id, type, value: jsonValue, parameter }: IContentElement, params: any) => { + const value = replaceVariables(jsonValue, parameter, params) switch (type) { case 'paragraph': - return {value} + return ( + + {value} + + ) + case 'pre': + return ( + +
+            {value}
+          
+
+ ) case 'span': return {value} case 'link': return {value.name} case 'spacer': - return + return case 'list': return (
    - {value.map((listElement: IContentElement[]) => ( -
  • {renderContent(listElement)}
  • + {isArray(jsonValue) && jsonValue.map((listElement: IContentElement[]) => ( +
  • {renderContent(listElement, params)}
  • ))}
) @@ -81,5 +108,5 @@ const renderContentElement = ({ id, type, value }: IContentElement) => { } } -export const renderContent = (elements: IContentElement[]) => ( - elements?.map((item) => renderContentElement(item))) +export const renderContent = (elements: IContentElement[], params: any) => ( + elements?.map((item) => renderContentElement(item, params))) From 84ba9b9641351d2bd170ec2dc26deea101f8cf28 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 12 Jan 2023 09:42:06 +0400 Subject: [PATCH 089/108] #RI-3971 - add one node recommendations --- redisinsight/api/src/constants/recommendations.ts | 8 ++++++++ .../database-analysis/database-analysis.service.ts | 7 +++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/redisinsight/api/src/constants/recommendations.ts b/redisinsight/api/src/constants/recommendations.ts index a53422829c..2fef5729c3 100644 --- a/redisinsight/api/src/constants/recommendations.ts +++ b/redisinsight/api/src/constants/recommendations.ts @@ -19,3 +19,11 @@ export const RECOMMENDATION_NAMES = Object.freeze({ SEARCH_INDEXES: 'searchIndexes', DANGEROUS_COMMANDS: 'dangerousCommands', }); + +export const ONE_NODE_RECOMMENDATIONS = [ + RECOMMENDATION_NAMES.LUA_SCRIPT, + RECOMMENDATION_NAMES.DANGEROUS_COMMANDS, + RECOMMENDATION_NAMES.AVOID_LOGICAL_DATABASES, + RECOMMENDATION_NAMES.RTS, + RECOMMENDATION_NAMES.REDIS_VERSION, +]; diff --git a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts index cf36a38a4c..7b2918d26f 100644 --- a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts +++ b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts @@ -2,6 +2,7 @@ import { HttpException, Injectable, Logger } from '@nestjs/common'; import { isNull, flatten, concat } from 'lodash'; import { RecommendationService } from 'src/modules/recommendation/recommendation.service'; import { catchAclError } from 'src/utils'; +import { ONE_NODE_RECOMMENDATIONS } from 'src/constants'; import { DatabaseAnalyzer } from 'src/modules/database-analysis/providers/database-analyzer'; import { plainToClass } from 'class-transformer'; import { DatabaseAnalysis, ShortDatabaseAnalysis } from 'src/modules/database-analysis/models'; @@ -55,7 +56,7 @@ export class DatabaseAnalysisService { let recommendationToExclude = []; - const recommendations = await scanResults.reduce(async (previousPromise, nodeResult) => { + const recommendations = await scanResults.reduce(async (previousPromise, nodeResult, idx) => { const jobsArray = await previousPromise; const nodeRecommendations = await this.recommendationService.getRecommendations({ client: nodeResult.client, @@ -64,7 +65,9 @@ export class DatabaseAnalysisService { globalClient: client, exclude: recommendationToExclude, }); - // recommendationToExclude = concat(recommendationToExclude, [RECOMMENDATION_NAMES.RTS]); + if (idx === 0) { + recommendationToExclude = concat(recommendationToExclude, ONE_NODE_RECOMMENDATIONS); + } const foundedRecommendations = nodeRecommendations.filter((recommendation) => !isNull(recommendation)); const foundedRecommendationNames = foundedRecommendations.map(({ name }) => name); recommendationToExclude = concat(recommendationToExclude, foundedRecommendationNames); From f732f165c23e030dfa2680578e0bf5b5b71105d3 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 12 Jan 2023 10:49:05 +0400 Subject: [PATCH 090/108] #RI-3572 - add check renamed command function --- .../providers/recommendation.provider.spec.ts | 50 ++++++++++++------- .../providers/recommendation.provider.ts | 34 ++++++++++--- 2 files changed, 59 insertions(+), 25 deletions(-) diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts index 0a347eea03..bd9dca469d 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.spec.ts @@ -562,9 +562,9 @@ describe('RecommendationProvider', () => { .calledWith(jasmine.objectContaining({ name: 'FT._LIST' })) .mockResolvedValue(mockFTListResponse_1); - const redisServerRecommendation = await service + const redisSearchRecommendation = await service .determineRediSearchRecommendation(nodeClient, [mockJSONKey]); - expect(redisServerRecommendation).toEqual({ name: RECOMMENDATION_NAMES.REDIS_SEARCH }); + expect(redisSearchRecommendation).toEqual({ name: RECOMMENDATION_NAMES.REDIS_SEARCH }); }); it('should return rediSearch recommendation when there is huge string key', async () => { @@ -572,9 +572,9 @@ describe('RecommendationProvider', () => { .calledWith(jasmine.objectContaining({ name: 'FT._LIST' })) .mockResolvedValue(mockFTListResponse_1); - const redisServerRecommendation = await service + const redisSearchRecommendation = await service .determineRediSearchRecommendation(nodeClient, [mockRediSearchStringKey_1]); - expect(redisServerRecommendation).toEqual({ name: RECOMMENDATION_NAMES.REDIS_SEARCH }); + expect(redisSearchRecommendation).toEqual({ name: RECOMMENDATION_NAMES.REDIS_SEARCH }); }); it('should not return rediSearch recommendation when there is small string key', async () => { @@ -582,9 +582,9 @@ describe('RecommendationProvider', () => { .calledWith(jasmine.objectContaining({ name: 'FT._LIST' })) .mockResolvedValue(mockFTListResponse_1); - const redisServerRecommendation = await service + const redisSearchRecommendation = await service .determineRediSearchRecommendation(nodeClient, [mockRediSearchStringKey_2]); - expect(redisServerRecommendation).toEqual(null); + expect(redisSearchRecommendation).toEqual(null); }); it('should not return rediSearch recommendation when there are no indexes', async () => { @@ -592,9 +592,9 @@ describe('RecommendationProvider', () => { .calledWith(jasmine.objectContaining({ name: 'FT._LIST' })) .mockResolvedValue(mockFTListResponse_2); - const redisServerRecommendation = await service + const redisSearchRecommendation = await service .determineRediSearchRecommendation(nodeClient, [mockJSONKey]); - expect(redisServerRecommendation).toEqual(null); + expect(redisSearchRecommendation).toEqual(null); }); it('should ignore errors when ft command execute with error', async () => { @@ -602,9 +602,9 @@ describe('RecommendationProvider', () => { .calledWith(jasmine.objectContaining({ name: 'FT._LIST' })) .mockRejectedValue("some error"); - const redisServerRecommendation = await service + const redisSearchRecommendation = await service .determineRediSearchRecommendation(nodeClient, [mockJSONKey]); - expect(redisServerRecommendation).toEqual({ name: RECOMMENDATION_NAMES.REDIS_SEARCH }); + expect(redisSearchRecommendation).toEqual({ name: RECOMMENDATION_NAMES.REDIS_SEARCH }); }); it('should ignore errors when ft command execute with error', async () => { @@ -612,9 +612,9 @@ describe('RecommendationProvider', () => { .calledWith(jasmine.objectContaining({ name: 'FT._LIST' })) .mockRejectedValue("some error"); - const redisServerRecommendation = await service + const redisSearchRecommendation = await service .determineRediSearchRecommendation(nodeClient, [mockRediSearchStringKey_2]); - expect(redisServerRecommendation).toEqual(null); + expect(redisSearchRecommendation).toEqual(null); }); }); @@ -624,9 +624,9 @@ describe('RecommendationProvider', () => { .calledWith(jasmine.objectContaining({ name: 'info' })) .mockResolvedValue(mockRedisServerResponse_1); - const redisServerRecommendation = await service + const redisVersionRecommendation = await service .determineRedisVersionRecommendation(nodeClient); - expect(redisServerRecommendation).toEqual(null); + expect(redisVersionRecommendation).toEqual(null); }); it('should return redis version recommendation', async () => { @@ -634,9 +634,9 @@ describe('RecommendationProvider', () => { .calledWith(jasmine.objectContaining({ name: 'info' })) .mockResolvedValueOnce(mockRedisServerResponse_2); - const redisServerRecommendation = await service + const redisVersionRecommendation = await service .determineRedisVersionRecommendation(nodeClient); - expect(redisServerRecommendation).toEqual({ name: RECOMMENDATION_NAMES.REDIS_VERSION }); + expect(redisVersionRecommendation).toEqual({ name: RECOMMENDATION_NAMES.REDIS_VERSION }); }); it('should not return redis version recommendation when info command executed with error', @@ -646,9 +646,23 @@ describe('RecommendationProvider', () => { .calledWith(jasmine.objectContaining({ name: 'info' })) .mockRejectedValue('some error'); - const redisServerRecommendation = await service + const redisVersionRecommendation = await service .determineRedisVersionRecommendation(nodeClient); - expect(redisServerRecommendation).toEqual(null); + expect(redisVersionRecommendation).toEqual(null); + }); + }); + + describe('determineDangerousCommandsRecommendation', () => { + it('should not return dangerous commands recommendation when "command" command executed with error', + async () => { + resetAllWhenMocks(); + when(nodeClient.sendCommand) + .calledWith(jasmine.objectContaining({ name: 'command' })) + .mockRejectedValue('some error'); + + const dangerousCommandsRecommendation = await service + .determineDangerousCommandsRecommendation(nodeClient); + expect(dangerousCommandsRecommendation).toEqual(null); }); }); }); diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index 2164d2854d..c938546357 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -1,6 +1,6 @@ import { Injectable, Logger } from '@nestjs/common'; import { Redis, Cluster, Command } from 'ioredis'; -import { get, isNumber } from 'lodash'; +import { get, isNull, isNumber } from 'lodash'; import { isValid } from 'date-fns'; import * as semverCompare from 'node-version-compare'; import { convertRedisInfoReplyToObject, convertBulkStringsToObject } from 'src/utils'; @@ -440,13 +440,13 @@ export class RecommendationProvider { if (keys[processedKeysNumber].type !== RedisDataType.ZSet) { processedKeysNumber += 1; } else { - let keyBySortedSetMember; + let keyType: string; const sortedSetMember = await redisClient.sendCommand( new Command('zrange', [keys[processedKeysNumber].name, 0, 0], { replyEncoding: 'utf8' }), ) as string[]; try { - keyBySortedSetMember = await redisClient.sendCommand( + keyType = await redisClient.sendCommand( new Command('type', [sortedSetMember[0]], { replyEncoding: 'utf8' }), ) as string; } catch (err) { @@ -461,12 +461,12 @@ export class RecommendationProvider { ); } - keyBySortedSetMember = await node.sendCommand( + keyType = await node.sendCommand( new Command('type', [sortedSetMember[0]], { replyEncoding: 'utf8' }), ) as string; } } - if (keyBySortedSetMember === RedisDataType.JSON || keyBySortedSetMember === RedisDataType.Hash) { + if (keyType === RedisDataType.JSON || keyType === RedisDataType.Hash) { isJSONOrHash = true; } processedKeysNumber += 1; @@ -517,8 +517,14 @@ export class RecommendationProvider { const commandName = command.split('|')[0]; return !redisInsightCommands.includes(commandName); }); - const commands = filteredDangerousCommands.join('\r\n').toUpperCase(); - return filteredDangerousCommands + + const activeDangerousCommands = await Promise.all( + filteredDangerousCommands.map(async (command) => await this.checkCommandInfo(redisClient, command)), + ); + const commands = activeDangerousCommands + .filter((command) => !isNull(command)) + .join('\r\n').toUpperCase(); + return activeDangerousCommands.length ? { name: RECOMMENDATION_NAMES.DANGEROUS_COMMANDS, params: { commands } } : null; } catch (err) { @@ -540,6 +546,20 @@ export class RecommendationProvider { return false; } + private async checkCommandInfo(redisClient: Redis | Cluster, command: string): Promise { + try { + const result = await redisClient.sendCommand( + new Command('command', ['info', command]), + ); + if (isNull(result[0])) { + return null; + } + } catch (err) { + return null; + } + return command; + } + private checkTimestamp(value: string): boolean { try { if (!IS_NUMBER_REGEX.test(value) && isValid(new Date(value))) { From 7434698a11325a79ec05ae5ca07c2904d70344ec Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 12 Jan 2023 10:54:34 +0400 Subject: [PATCH 091/108] #RI-3971 - update json file --- .../src/constants/dbAnalysisRecommendations.json | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index 17e7dc11d4..0a6b074738 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -502,6 +502,11 @@ "type": "paragraph", "value": "If you are using sorted sets to work with time series data, consider using RedisTimeSeries to optimize the memory usage while having extraordinary query performance and small overhead during ingestion." }, + { + "id": "2", + "type": "spacer", + "value": "l" + }, { "id": "3", "type": "span", @@ -667,7 +672,7 @@ "title": "Rename or disable dangerous commands", "content": [ { - "id": "2", + "id": "1", "type": "pre", "parameter": ["commands"], "value": "${0} " @@ -679,22 +684,22 @@ "value": " are currently not renamed or disabled for your Instance." }, { - "id": "2", + "id": "3", "type": "paragraph", "value": "These commands are powerful and dangerous if not managed properly." }, { - "id": "3", + "id": "4", "type": "spacer", "value": "l" }, { - "id": "4", + "id": "5", "type": "span", "value": "Rename or disable them, especially for the production environment. " }, { - "id": "5", + "id": "6", "type": "link", "value": { "href": "https://redis.io/download/", From 5bf736631b0dbe3f46b726a91574ba752f779c79 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 12 Jan 2023 11:00:59 +0400 Subject: [PATCH 092/108] #RI-3971 - remove unused code --- redisinsight/ui/src/constants/dbAnalysisRecommendations.json | 1 - .../databaseAnalysis/components/recommendations-view/utils.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index 0a6b074738..9a5790335d 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -680,7 +680,6 @@ { "id": "2", "type": "span", - "parameter": ["commands"], "value": " are currently not renamed or disabled for your Instance." }, { diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx index 168b26210d..fad8f5aca2 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/utils.tsx @@ -1,7 +1,6 @@ import React from 'react' import { isString, isArray } from 'lodash' import { - EuiText, EuiTextColor, EuiToolTip, EuiFlexGroup, From 653fd018d5b85fcdf2493e07d3096d037fa56b70 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 12 Jan 2023 12:37:30 +0400 Subject: [PATCH 093/108] #RI-3572 - resolve comments --- .../providers/recommendation.provider.ts | 85 +++++++++---------- .../constants/dbAnalysisRecommendations.json | 24 ++---- 2 files changed, 49 insertions(+), 60 deletions(-) diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index c938546357..9ca90028e3 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -428,55 +428,54 @@ export class RecommendationProvider { keys: Key[], client: any, ): Promise { - if (client.isCluster) { - let processedKeysNumber = 0; - let isJSONOrHash = false; - let sortedSetNumber = 0; - while ( - processedKeysNumber < keys.length - && !isJSONOrHash - && sortedSetNumber <= sortedSetCountForCheck - ) { - if (keys[processedKeysNumber].type !== RedisDataType.ZSet) { - processedKeysNumber += 1; - } else { - let keyType: string; - const sortedSetMember = await redisClient.sendCommand( - new Command('zrange', [keys[processedKeysNumber].name, 0, 0], { replyEncoding: 'utf8' }), - ) as string[]; - - try { - keyType = await redisClient.sendCommand( - new Command('type', [sortedSetMember[0]], { replyEncoding: 'utf8' }), - ) as string; - } catch (err) { - if (err && checkRedirectionError(err)) { - const { address } = parseRedirectionError(err); - const nodes = client.nodes('master'); - - const node: any = nodes.find(({ options: { host, port } }: Redis) => `${host}:${port}` === address); - if (!node) { - throw new ClusterNodeNotFoundError( - ERROR_MESSAGES.CLUSTER_NODE_NOT_FOUND(node), - ); - } - - keyType = await node.sendCommand( + try { + if (client.isCluster) { + let processedKeysNumber = 0; + let isJSONOrHash = false; + let sortedSetNumber = 0; + while ( + processedKeysNumber < keys.length + && !isJSONOrHash + && sortedSetNumber <= sortedSetCountForCheck + ) { + if (keys[processedKeysNumber].type !== RedisDataType.ZSet) { + processedKeysNumber += 1; + } else { + let keyType: string; + const sortedSetMember = await redisClient.sendCommand( + new Command('zrange', [keys[processedKeysNumber].name, 0, 0], { replyEncoding: 'utf8' }), + ) as string[]; + try { + keyType = await redisClient.sendCommand( new Command('type', [sortedSetMember[0]], { replyEncoding: 'utf8' }), ) as string; + } catch (err) { + if (err && checkRedirectionError(err)) { + const { address } = parseRedirectionError(err); + const nodes = client.nodes('master'); + + const node: any = nodes.find(({ options: { host, port } }: Redis) => `${host}:${port}` === address); + if (!node) { + throw new ClusterNodeNotFoundError( + ERROR_MESSAGES.CLUSTER_NODE_NOT_FOUND(node), + ); + } + + keyType = await node.sendCommand( + new Command('type', [sortedSetMember[0]], { replyEncoding: 'utf8' }), + ) as string; + } } + if (keyType === RedisDataType.JSON || keyType === RedisDataType.Hash) { + isJSONOrHash = true; + } + processedKeysNumber += 1; + sortedSetNumber += 1; } - if (keyType === RedisDataType.JSON || keyType === RedisDataType.Hash) { - isJSONOrHash = true; - } - processedKeysNumber += 1; - sortedSetNumber += 1; } - } - return isJSONOrHash ? { name: RECOMMENDATION_NAMES.SEARCH_INDEXES } : null; - } - try { + return isJSONOrHash ? { name: RECOMMENDATION_NAMES.SEARCH_INDEXES } : null; + } const sortedSets = keys .filter(({ type }) => type === RedisDataType.ZSet) .slice(0, 100); diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index 9a5790335d..e8eae1a338 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -673,32 +673,22 @@ "content": [ { "id": "1", - "type": "pre", - "parameter": ["commands"], - "value": "${0} " + "type": "paragraph", + "value": "The following commands are currently not renamed or disabled for your Instance. These commands are powerful and dangerous if not managed properly. Rename or disable them, especially for the production environment" }, { "id": "2", - "type": "span", - "value": " are currently not renamed or disabled for your Instance." + "type": "pre", + "parameter": ["commands"], + "value": "${0} " }, { "id": "3", - "type": "paragraph", - "value": "These commands are powerful and dangerous if not managed properly." - }, - { - "id": "4", "type": "spacer", - "value": "l" + "value": "s" }, { - "id": "5", - "type": "span", - "value": "Rename or disable them, especially for the production environment. " - }, - { - "id": "6", + "id": "4", "type": "link", "value": { "href": "https://redis.io/download/", From dde336047d77abe607645ff4c028ecab91722e7f Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Thu, 12 Jan 2023 12:08:16 +0300 Subject: [PATCH 094/108] #RI-4014 - fix re-rendering, fix highlighting if no count, fix click space for tab highlighting --- .../components/analytics-tabs/constants.tsx | 10 ++++-- .../DatabaseOverviewWrapper.tsx | 2 +- .../components/data-nav-tabs/constants.tsx | 32 +++++++++++-------- .../src/slices/app/features-highlighting.ts | 3 -- .../ui/src/styles/components/_tabs.scss | 10 ++++++ redisinsight/ui/src/utils/highlighting.ts | 3 ++ .../ui/src/utils/tests/highlighting.spec.ts | 10 +++++- 7 files changed, 48 insertions(+), 22 deletions(-) diff --git a/redisinsight/ui/src/components/analytics-tabs/constants.tsx b/redisinsight/ui/src/components/analytics-tabs/constants.tsx index 66922f4acb..6131253503 100644 --- a/redisinsight/ui/src/components/analytics-tabs/constants.tsx +++ b/redisinsight/ui/src/components/analytics-tabs/constants.tsx @@ -1,9 +1,11 @@ import React, { ReactNode } from 'react' -import { AnalyticsViewTab } from 'uiSrc/slices/interfaces/analytics' import { useSelector } from 'react-redux' -import { appFeaturesToHighlightSelector } from 'uiSrc/slices/app/features-highlighting' + +import { AnalyticsViewTab } from 'uiSrc/slices/interfaces/analytics' +import { appFeatureHighlightingSelector } from 'uiSrc/slices/app/features-highlighting' import HighlightedFeature from 'uiSrc/components/hightlighted-feature/HighlightedFeature' import { BUILD_FEATURES } from 'uiSrc/constants/featuresHighlighting' +import { getHighlightingFeatures } from 'uiSrc/utils/highlighting' interface AnalyticsTabs { id: AnalyticsViewTab @@ -11,7 +13,8 @@ interface AnalyticsTabs { } const DatabaseAnalyticsTab = () => { - const { recommendations: recommendationsHighlighting } = useSelector(appFeaturesToHighlightSelector) ?? {} + const { features } = useSelector(appFeatureHighlightingSelector) + const { recommendations: recommendationsHighlighting } = getHighlightingFeatures(features) return ( <> @@ -21,6 +24,7 @@ const DatabaseAnalyticsTab = () => { type={BUILD_FEATURES.recommendations?.type} isHighlight={BUILD_FEATURES.recommendations && recommendationsHighlighting} dotClassName="tab-highlighting-dot" + wrapperClassName="inner-highlighting-wrapper" > Database Analysis diff --git a/redisinsight/ui/src/components/database-overview/DatabaseOverviewWrapper.tsx b/redisinsight/ui/src/components/database-overview/DatabaseOverviewWrapper.tsx index a8998451cc..6033b5c41f 100644 --- a/redisinsight/ui/src/components/database-overview/DatabaseOverviewWrapper.tsx +++ b/redisinsight/ui/src/components/database-overview/DatabaseOverviewWrapper.tsx @@ -10,7 +10,7 @@ import { ThemeContext } from 'uiSrc/contexts/themeContext' import { getOverviewMetrics } from './components/OverviewMetrics' -const TIMEOUT_TO_GET_INFO = process.env.NODE_ENV !== 'development' ? 5000 : 10000000 +const TIMEOUT_TO_GET_INFO = process.env.NODE_ENV !== 'development' ? 5000 : 60_000 interface IProps { windowDimensions: number } diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/data-nav-tabs/constants.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/data-nav-tabs/constants.tsx index 85bb925c00..82ea803159 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/data-nav-tabs/constants.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/data-nav-tabs/constants.tsx @@ -2,10 +2,14 @@ import React, { ReactNode } from 'react' import { useDispatch, useSelector } from 'react-redux' import { DatabaseAnalysisViewTab } from 'uiSrc/slices/interfaces/analytics' -import { appFeaturesToHighlightSelector, removeFeatureFromHighlighting } from 'uiSrc/slices/app/features-highlighting' +import { + appFeatureHighlightingSelector, + removeFeatureFromHighlighting +} from 'uiSrc/slices/app/features-highlighting' import { BUILD_FEATURES } from 'uiSrc/constants/featuresHighlighting' import HighlightedFeature from 'uiSrc/components/hightlighted-feature/HighlightedFeature' +import { getHighlightingFeatures } from 'uiSrc/utils/highlighting' import Recommendations from '../recommendations-view' import AnalysisDataView from '../analysis-data-view' @@ -16,22 +20,22 @@ interface DatabaseAnalysisTabs { } const RecommendationsTab = ({ count }: { count?: number }) => { - const { recommendations: recommendationsHighlighting } = useSelector(appFeaturesToHighlightSelector) ?? {} + const { features } = useSelector(appFeatureHighlightingSelector) + const { recommendations: recommendationsHighlighting } = getHighlightingFeatures(features) const dispatch = useDispatch() - return count ? ( - <> - dispatch(removeFeatureFromHighlighting('recommendations'))} - dotClassName="tab-highlighting-dot" - > - <>Recommendations ({count}) - - - ) : (<>Recommendations) + return ( + dispatch(removeFeatureFromHighlighting('recommendations'))} + dotClassName="tab-highlighting-dot" + wrapperClassName="inner-highlighting-wrapper" + > + {count ? <>Recommendations ({count}) : <>Recommendations} + + ) } export const databaseAnalysisTabs: DatabaseAnalysisTabs[] = [ diff --git a/redisinsight/ui/src/slices/app/features-highlighting.ts b/redisinsight/ui/src/slices/app/features-highlighting.ts index 9d2d2d37cb..93de48534e 100644 --- a/redisinsight/ui/src/slices/app/features-highlighting.ts +++ b/redisinsight/ui/src/slices/app/features-highlighting.ts @@ -44,9 +44,6 @@ export const { } = appFeaturesHighlightingSlice.actions export const appFeatureHighlightingSelector = (state: RootState) => state.app.featuresHighlighting -export const appFeaturesToHighlightSelector = (state: RootState): { [key: string]: boolean } => - state.app.featuresHighlighting.features - .reduce((prev, next) => ({ ...prev, [next]: true }), {}) export const appFeaturePagesHighlightingSelector = (state: RootState) => state.app.featuresHighlighting.pages export default appFeaturesHighlightingSlice.reducer diff --git a/redisinsight/ui/src/styles/components/_tabs.scss b/redisinsight/ui/src/styles/components/_tabs.scss index 4c22c64a14..166250e16e 100644 --- a/redisinsight/ui/src/styles/components/_tabs.scss +++ b/redisinsight/ui/src/styles/components/_tabs.scss @@ -67,6 +67,16 @@ } } + .inner-highlighting-wrapper { + margin: -8px -12px; + padding: 8px 12px; + + .tab-highlighting-dot { + top: 2px; + right: 2px; + } + } + .tab-highlighting-dot { top: -6px; right: -12px; diff --git a/redisinsight/ui/src/utils/highlighting.ts b/redisinsight/ui/src/utils/highlighting.ts index 95479d32cf..6609f59db4 100644 --- a/redisinsight/ui/src/utils/highlighting.ts +++ b/redisinsight/ui/src/utils/highlighting.ts @@ -17,3 +17,6 @@ export const getPagesForFeatures = (features: string[] = []) => { return result } + +export const getHighlightingFeatures = (features: string[]): { [key: string]: boolean } => features + .reduce((prev, next) => ({ ...prev, [next]: true }), {}) diff --git a/redisinsight/ui/src/utils/tests/highlighting.spec.ts b/redisinsight/ui/src/utils/tests/highlighting.spec.ts index df999bd68a..082dc7a582 100644 --- a/redisinsight/ui/src/utils/tests/highlighting.spec.ts +++ b/redisinsight/ui/src/utils/tests/highlighting.spec.ts @@ -1,4 +1,4 @@ -import { getPagesForFeatures } from 'uiSrc/utils/highlighting' +import { getHighlightingFeatures, getPagesForFeatures } from 'uiSrc/utils/highlighting' import { MOCKED_HIGHLIGHTING_FEATURES } from 'uiSrc/utils/test-utils' describe('getPagesForFeatures', () => { @@ -10,3 +10,11 @@ describe('getPagesForFeatures', () => { expect(getPagesForFeatures(MOCKED_HIGHLIGHTING_FEATURES)).toEqual({ browser: MOCKED_HIGHLIGHTING_FEATURES }) }) }) + +describe('getPagesForFeatures', () => { + it('should return proper pages for features', () => { + expect(getHighlightingFeatures([])).toEqual({}) + expect(getHighlightingFeatures(['feature1'])).toEqual({ feature1: true }) + expect(getHighlightingFeatures(['f1', 'f2'])).toEqual({ f1: true, f2: true }) + }) +}) From 3ab1be1f3787cdda6581d723378b57d5478f4804 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Mon, 16 Jan 2023 19:50:35 +0400 Subject: [PATCH 095/108] #RI-4023 - update no recommendations message --- .../assets/img/icons/recommendations_dark.svg | 24 +++++++++ .../img/icons/recommendations_light.svg | 24 +++++++++ .../constants/dbAnalysisRecommendations.json | 51 +++++++++++++------ .../recommendations-view/Recommendations.tsx | 12 ++++- .../recommendations-view/styles.module.scss | 10 ++++ 5 files changed, 105 insertions(+), 16 deletions(-) create mode 100644 redisinsight/ui/src/assets/img/icons/recommendations_dark.svg create mode 100644 redisinsight/ui/src/assets/img/icons/recommendations_light.svg diff --git a/redisinsight/ui/src/assets/img/icons/recommendations_dark.svg b/redisinsight/ui/src/assets/img/icons/recommendations_dark.svg new file mode 100644 index 0000000000..99219b30d5 --- /dev/null +++ b/redisinsight/ui/src/assets/img/icons/recommendations_dark.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/redisinsight/ui/src/assets/img/icons/recommendations_light.svg b/redisinsight/ui/src/assets/img/icons/recommendations_light.svg new file mode 100644 index 0000000000..23264787c4 --- /dev/null +++ b/redisinsight/ui/src/assets/img/icons/recommendations_light.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index e8eae1a338..ea073e1f02 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -499,21 +499,34 @@ "content": [ { "id": "1", - "type": "paragraph", - "value": "If you are using sorted sets to work with time series data, consider using RedisTimeSeries to optimize the memory usage while having extraordinary query performance and small overhead during ingestion." + "type": "span", + "value": "If you are using sorted sets to work with time series data, consider using " }, { "id": "2", + "type": "link", + "value": { + "href": "https://redis.io/docs/stack/timeseries/", + "name": "RedisTimeSeries" + } + }, + { + "id": "3", + "type": "span", + "value": " to optimize the memory usage while having extraordinary query performance and small overhead during ingestion." + }, + { + "id": "4", "type": "spacer", "value": "l" }, { - "id": "3", + "id": "5", "type": "span", "value": "Create a " }, { - "id": "4", + "id": "6", "type": "link", "value": { "href": "https://redis.com/try-free/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight_recommendations/", @@ -521,7 +534,7 @@ } }, { - "id": "5", + "id": "7", "type": "span", "value": " to use modern data models and processing engines." } @@ -566,39 +579,47 @@ }, "redisSearch": { "id": "redisSearch", - "title":"Optimize your search and query experience", + "title":"Optimize your query and search experience", "redisStack": true, "content": [ { "id": "1", - "type": "span", - "value": "RediSearch was designed to help address your query needs and support a better development experience when dealing with complex data scenarios. Take a look at the powerful API options here (" + "type": "link", + "value": { + "href": "https://redis.io/docs/stack/search/", + "name": "RediSearch" + } }, { "id": "2", + "type": "span", + "value": "was designed to help address your query needs and support a better development experience when dealing with complex data scenarios. Take a look at the " + }, + { + "id": "3", "type": "link", "value": { "href": "https://redis.io/commands/?name=Ft", - "name": "Commands" + "name": "powerful API options" } }, { - "id": "3", + "id": "4", "type": "span", - "value": ") and try it. Supports full-text search, wildcards, fuzzy logic, and more." + "value": " and try them. Supports full-text search, wildcards, fuzzy logic, and more." }, { - "id": "4", + "id": "5", "type": "spacer", "value": "l" }, { - "id": "5", + "id": "6", "type": "span", "value": "Create a " }, { - "id": "6", + "id": "7", "type": "link", "value": { "href": "https://redis.com/try-free/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight_recommendations/", @@ -606,7 +627,7 @@ } }, { - "id": "7", + "id": "8", "type": "span", "value": " which extends the core capabilities of Redis OSS and uses modern data models and processing engines." } diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx index 68470e835e..7363b4a538 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx @@ -19,6 +19,8 @@ import { Theme } from 'uiSrc/constants' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import RediStackDarkMin from 'uiSrc/assets/img/modules/redistack/RediStackDark-min.svg' import RediStackLightMin from 'uiSrc/assets/img/modules/redistack/RediStackLight-min.svg' +import NoRecommendationsDark from 'uiSrc/assets/img/icons/recommendations_dark.svg' +import NoRecommendationsLight from 'uiSrc/assets/img/icons/recommendations_light.svg' import { renderContent, renderBadges, renderBadgesLegend } from './utils' import styles from './styles.module.scss' @@ -91,7 +93,15 @@ const Recommendations = () => { if (isNull(recommendations) || !recommendations.length) { return (
- No Recommendations at the moment. + + AMAZING JOB! + No Recommendations at the moment, +
+ keep up the good work!
) } diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss index e9eb391bbf..9ae556f001 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss @@ -68,6 +68,16 @@ flex-direction: column; align-items: center; height: 100%; + + .noRecommendationsIcon { + width: 154px; + height: 127px; + } +} + +.bigText { + font: normal normal 600 18px/22px Graphik, sans-serif !important; + margin: 16px 0 12px; } .loadingWrapper { From d808d7cd5bcdcebffd350e33b4513c42153c8b96 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Tue, 17 Jan 2023 11:52:30 +0400 Subject: [PATCH 096/108] #RI-4023 - update styles --- .../components/recommendations-view/styles.module.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss index 9ae556f001..f63ba897fa 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss @@ -68,6 +68,7 @@ flex-direction: column; align-items: center; height: 100%; + padding-bottom: 162px; .noRecommendationsIcon { width: 154px; From 03aada0063d2bf01f87e57e51c233ff00747393c Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Tue, 17 Jan 2023 12:06:06 +0400 Subject: [PATCH 097/108] #RI-4023 - resolve comments --- redisinsight/ui/src/constants/dbAnalysisRecommendations.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index ea073e1f02..c06f56181d 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -500,7 +500,7 @@ { "id": "1", "type": "span", - "value": "If you are using sorted sets to work with time series data, consider using " + "value": "If you are using sorted sets to work with time series data, consider using " }, { "id": "2", From af9e00df54d6fec27f88ea985d0cb866da7642b2 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Wed, 18 Jan 2023 13:09:47 +0400 Subject: [PATCH 098/108] #RI-4027 - fix redis version --- .../modules/recommendation/providers/recommendation.provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts index 9ca90028e3..115eb70897 100644 --- a/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts +++ b/redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts @@ -509,7 +509,7 @@ export class RecommendationProvider { ): Promise { try { const dangerousCommands = await redisClient.sendCommand( - new Command('command', ['LIST', 'FILTERBY', 'aclcat', 'dangerous'], { replyEncoding: 'utf8' }), + new Command('ACL', ['CAT', 'dangerous'], { replyEncoding: 'utf8' }), ) as string[]; const filteredDangerousCommands = dangerousCommands.filter((command) => { From 623030dba218a5574d12b665c3aee256eeacd1d7 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 19 Jan 2023 13:53:03 +0400 Subject: [PATCH 099/108] #RI-3977 - add recommendation voting --- .../ui/src/assets/img/icons/dislike.svg | 3 + .../ui/src/assets/img/icons/double_like.svg | 4 + redisinsight/ui/src/assets/img/icons/like.svg | 3 + .../constants/dbAnalysisRecommendations.json | 2 +- redisinsight/ui/src/constants/links.ts | 1 + .../ui/src/constants/recommendations.ts | 5 + .../databaseAnalysis/components/index.ts | 2 + .../RecommendationVoting.spec.tsx | 84 ++++++++++ .../RecommendationVoting.tsx | 144 ++++++++++++++++++ .../components/recommendation-voting/index.ts | 3 + .../recommendation-voting/styles.module.scss | 64 ++++++++ .../recommendations-view/Recommendations.tsx | 5 +- .../recommendations-view/styles.module.scss | 2 +- .../ui/src/slices/analytics/dbAnalysis.ts | 50 ++++++ .../slices/tests/analytics/dbAnalysis.spec.ts | 92 +++++++++++ redisinsight/ui/src/telemetry/events.ts | 3 +- 16 files changed, 462 insertions(+), 5 deletions(-) create mode 100644 redisinsight/ui/src/assets/img/icons/dislike.svg create mode 100644 redisinsight/ui/src/assets/img/icons/double_like.svg create mode 100644 redisinsight/ui/src/assets/img/icons/like.svg create mode 100644 redisinsight/ui/src/constants/recommendations.ts create mode 100644 redisinsight/ui/src/pages/databaseAnalysis/components/recommendation-voting/RecommendationVoting.spec.tsx create mode 100644 redisinsight/ui/src/pages/databaseAnalysis/components/recommendation-voting/RecommendationVoting.tsx create mode 100644 redisinsight/ui/src/pages/databaseAnalysis/components/recommendation-voting/index.ts create mode 100644 redisinsight/ui/src/pages/databaseAnalysis/components/recommendation-voting/styles.module.scss diff --git a/redisinsight/ui/src/assets/img/icons/dislike.svg b/redisinsight/ui/src/assets/img/icons/dislike.svg new file mode 100644 index 0000000000..9bc0db8362 --- /dev/null +++ b/redisinsight/ui/src/assets/img/icons/dislike.svg @@ -0,0 +1,3 @@ + + + diff --git a/redisinsight/ui/src/assets/img/icons/double_like.svg b/redisinsight/ui/src/assets/img/icons/double_like.svg new file mode 100644 index 0000000000..336288eb9e --- /dev/null +++ b/redisinsight/ui/src/assets/img/icons/double_like.svg @@ -0,0 +1,4 @@ + + + + diff --git a/redisinsight/ui/src/assets/img/icons/like.svg b/redisinsight/ui/src/assets/img/icons/like.svg new file mode 100644 index 0000000000..4eea43fc0d --- /dev/null +++ b/redisinsight/ui/src/assets/img/icons/like.svg @@ -0,0 +1,3 @@ + + + diff --git a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json index c06f56181d..4097052fc1 100644 --- a/redisinsight/ui/src/constants/dbAnalysisRecommendations.json +++ b/redisinsight/ui/src/constants/dbAnalysisRecommendations.json @@ -593,7 +593,7 @@ { "id": "2", "type": "span", - "value": "was designed to help address your query needs and support a better development experience when dealing with complex data scenarios. Take a look at the " + "value": " was designed to help address your query needs and support a better development experience when dealing with complex data scenarios. Take a look at the " }, { "id": "3", diff --git a/redisinsight/ui/src/constants/links.ts b/redisinsight/ui/src/constants/links.ts index 506be9f976..1e22edb2b2 100644 --- a/redisinsight/ui/src/constants/links.ts +++ b/redisinsight/ui/src/constants/links.ts @@ -3,4 +3,5 @@ export const EXTERNAL_LINKS = { githubIssues: 'https://github.com/RedisInsight/RedisInsight/issues', releaseNotes: 'https://github.com/RedisInsight/RedisInsight/releases', userSurvey: 'https://www.surveymonkey.com/r/redisinsight', + recommendationFeedback: 'https://github.com/RedisInsight/RedisInsight/issues/new/choose', } diff --git a/redisinsight/ui/src/constants/recommendations.ts b/redisinsight/ui/src/constants/recommendations.ts new file mode 100644 index 0000000000..513cf24a30 --- /dev/null +++ b/redisinsight/ui/src/constants/recommendations.ts @@ -0,0 +1,5 @@ +export enum Vote { + DoubleLike = 'amazing', + Like = 'useful', + Dislike = 'not useful' +} diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/index.ts b/redisinsight/ui/src/pages/databaseAnalysis/components/index.ts index 368b008a47..1221f0ccf1 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/index.ts +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/index.ts @@ -2,6 +2,7 @@ import AnalysisDataView from './analysis-data-view' import ExpirationGroupsView from './analysis-ttl-view' import EmptyAnalysisMessage from './empty-analysis-message' import Header from './header' +import RecommendationVoting from './recommendation-voting' import SummaryPerData from './summary-per-data' import TableLoader from './table-loader' import TopKeys from './top-keys' @@ -12,6 +13,7 @@ export { ExpirationGroupsView, EmptyAnalysisMessage, Header, + RecommendationVoting, SummaryPerData, TableLoader, TopKeys, diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendation-voting/RecommendationVoting.spec.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendation-voting/RecommendationVoting.spec.tsx new file mode 100644 index 0000000000..8a114ee32c --- /dev/null +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendation-voting/RecommendationVoting.spec.tsx @@ -0,0 +1,84 @@ +import React from 'react' +import { cloneDeep } from 'lodash' +import { instance, mock } from 'ts-mockito' +import { setRecommendationVote } from 'uiSrc/slices/analytics/dbAnalysis' + +import { + cleanup, + mockedStore, + fireEvent, + render, + screen, + waitForEuiPopoverVisible, +} from 'uiSrc/utils/test-utils' + +import RecommendationVoting, { Props } from './RecommendationVoting' + +const mockedProps = mock() + +let store: typeof mockedStore + +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: jest.fn(), +})) + +describe('RecommendationVoting', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should call "setRecommendationVote" action be called after click "amazing-vote-btn"', () => { + render() + fireEvent.click(screen.getByTestId('amazing-vote-btn')) + + const expectedActions = [setRecommendationVote()] + expect(store.getActions()).toEqual(expectedActions) + }) + + it('should call "setRecommendationVote" action be called after click "useful-vote-btn"', () => { + render() + fireEvent.click(screen.getByTestId('useful-vote-btn')) + + const expectedActions = [setRecommendationVote()] + expect(store.getActions()).toEqual(expectedActions) + }) + + it('should call "setRecommendationVote" action be called after click "not-useful-vote-btn"', () => { + render() + fireEvent.click(screen.getByTestId('not-useful-vote-btn')) + + const expectedActions = [setRecommendationVote()] + expect(store.getActions()).toEqual(expectedActions) + }) + + it('should render popover after click "not-useful-vote-btn"', async () => { + render() + + expect(document.querySelector('[data-test-subj="github-repo-link"]')).not.toBeInTheDocument() + + fireEvent.click(screen.getByTestId('not-useful-vote-btn')) + await waitForEuiPopoverVisible() + + expect(document.querySelector('[data-test-subj="github-repo-link"]')).toHaveAttribute('href', 'https://github.com/RedisInsight/RedisInsight/issues/new/choose') + }) + + it('should render component where all buttons are disabled"', async () => { + render() + + expect(screen.getByTestId('amazing-vote-btn')).toBeDisabled() + expect(screen.getByTestId('useful-vote-btn')).toBeDisabled() + expect(screen.getByTestId('not-useful-vote-btn')).toBeDisabled() + }) +}) diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendation-voting/RecommendationVoting.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendation-voting/RecommendationVoting.tsx new file mode 100644 index 0000000000..e029cb2b52 --- /dev/null +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendation-voting/RecommendationVoting.tsx @@ -0,0 +1,144 @@ +import React, { useState } from 'react' +import { useDispatch } from 'react-redux' +import cx from 'classnames' +import { + EuiButton, + EuiButtonIcon, + EuiPopover, + EuiText, + EuiToolTip, + EuiFlexGroup, + EuiIcon, + EuiLink, +} from '@elastic/eui' +import { putRecommendationVote } from 'uiSrc/slices/analytics/dbAnalysis' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { EXTERNAL_LINKS } from 'uiSrc/constants/links' +import { Vote } from 'uiSrc/constants/recommendations' + +import { ReactComponent as LikeIcon } from 'uiSrc/assets/img/icons/like.svg' +import { ReactComponent as DoubleLikeIcon } from 'uiSrc/assets/img/icons/double_like.svg' +import { ReactComponent as DislikeIcon } from 'uiSrc/assets/img/icons/dislike.svg' +import GithubSVG from 'uiSrc/assets/img/sidebar/github.svg' +import styles from './styles.module.scss' + +export interface Props { vote?: Vote, name: string } + +const RecommendationVoting = ({ vote, name }: Props) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false) + const dispatch = useDispatch() + + const onSuccessVoted = (instanceId: string, name: string, vote: Vote) => { + sendEventTelemetry({ + event: TelemetryEvent.DATABASE_ANALYSIS_RECOMMENDATIONS_VOTED, + eventData: { + databaseId: instanceId, + name, + vote, + } + }) + } + + const handleClick = (name: string, vote: Vote) => { + if (vote === Vote.Dislike) { + setIsPopoverOpen(true) + } + dispatch(putRecommendationVote(name, vote, onSuccessVoted)) + } + + return ( + + Rate Recommendation +
+ + handleClick(name, Vote.DoubleLike)} + /> + + + handleClick(name, Vote.Like)} + /> + + + setIsPopoverOpen(false)} + anchorClassName={styles.popoverAnchor} + panelClassName={cx('euiToolTip', 'popoverLikeTooltip', styles.popover)} + button={( + handleClick(name, Vote.Dislike)} + /> + )} + > +
+ Thank you for your feedback, Tell us how we can improve + + + + To Github + + + setIsPopoverOpen(false)} + /> +
+
+
+
+
+ ) +} + +export default RecommendationVoting diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendation-voting/index.ts b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendation-voting/index.ts new file mode 100644 index 0000000000..7b77eec7fb --- /dev/null +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendation-voting/index.ts @@ -0,0 +1,3 @@ +import RecommendationVoting from './RecommendationVoting' + +export default RecommendationVoting diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendation-voting/styles.module.scss b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendation-voting/styles.module.scss new file mode 100644 index 0000000000..b0fc89120f --- /dev/null +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendation-voting/styles.module.scss @@ -0,0 +1,64 @@ +.votingContainer { + padding-top: 15px; + margin-top: 15px !important; + border-top: 1px solid var(--separatorColor); + height: 49px; + + .vote { + margin-left: 10px; + } + + .vote :global(.euiIcon) { + width: 34px; + height: 34px; + fill: none; + + path { + stroke: var(--buttonSecondaryTextColor); + } + } + + .vote .voteBtn { + width: 34px; + height: 34px; + border-radius: 50%; + + &:hover, + &:focus, + &.selected { + transform: none; + background-color: var(--separatorColor); + } + } +} + +:global(.euiPanel).popover { + max-width: none !important; + box-shadow: none !important; + padding: 10px 15px !important; + color: var(--buttonSecondaryTextColor) !important; + + .feedbackBtn { + padding: 4px 8px 4px 4px; + margin: 0 10px; + height: 22px !important; + + :global(.euiButtonContent.euiButton__content) { + padding: 0; + } + + .link { + display: flex; + align-items: center; + color: var(--euiColorPrimaryText) !important; + text-decoration: none !important; + font: normal normal normal 12px/14px Graphik, sans-serif; + } + + .link .githubIcon { + width: 12px; + height: 12px; + margin-right: 2px; + } + } +} diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx index 7363b4a538..b056b6d446 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/Recommendations.tsx @@ -13,6 +13,7 @@ import { EuiLink, } from '@elastic/eui' import { ThemeContext } from 'uiSrc/contexts/themeContext' +import { RecommendationVoting } from 'uiSrc/pages/databaseAnalysis/components' import { dbAnalysisSelector } from 'uiSrc/slices/analytics/dbAnalysis' import recommendationsContent from 'uiSrc/constants/dbAnalysisRecommendations.json' import { Theme } from 'uiSrc/constants' @@ -21,7 +22,6 @@ import RediStackDarkMin from 'uiSrc/assets/img/modules/redistack/RediStackDark-m import RediStackLightMin from 'uiSrc/assets/img/modules/redistack/RediStackLight-min.svg' import NoRecommendationsDark from 'uiSrc/assets/img/icons/recommendations_dark.svg' import NoRecommendationsLight from 'uiSrc/assets/img/icons/recommendations_light.svg' - import { renderContent, renderBadges, renderBadgesLegend } from './utils' import styles from './styles.module.scss' @@ -112,7 +112,7 @@ const Recommendations = () => { {renderBadgesLegend()}
- {sortedRecommendations.map(({ name, params }) => { + {sortedRecommendations.map(({ name, params, vote }) => { const { id = '', title = '', @@ -138,6 +138,7 @@ const Recommendations = () => { {renderContent(content, params)} +
) })} diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss index f63ba897fa..88a4cf30c8 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendations-view/styles.module.scss @@ -94,7 +94,7 @@ border: 1px solid var(--recommendationBorderColor); background-color: var(--euiColorLightestShade); margin-bottom: 6px; - padding: 30px 18px; + padding: 30px 18px 11px; ul { list-style: initial; diff --git a/redisinsight/ui/src/slices/analytics/dbAnalysis.ts b/redisinsight/ui/src/slices/analytics/dbAnalysis.ts index 3e05801c0f..b569213bc9 100644 --- a/redisinsight/ui/src/slices/analytics/dbAnalysis.ts +++ b/redisinsight/ui/src/slices/analytics/dbAnalysis.ts @@ -1,6 +1,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { AxiosError } from 'axios' import { ApiEndpoints } from 'uiSrc/constants' +import { Vote } from 'uiSrc/constants/recommendations' import { apiService, } from 'uiSrc/services' import { addErrorNotification } from 'uiSrc/slices/app/notifications' import { StateDatabaseAnalysis, DatabaseAnalysisViewTab } from 'uiSrc/slices/interfaces/analytics' @@ -39,6 +40,15 @@ const databaseAnalysisSlice = createSlice({ state.loading = false state.error = payload }, + setRecommendationVote: () => { + // we don't have any loading here + }, + setRecommendationVoteSuccess: (state, { payload }: PayloadAction) => { + state.data = payload + }, + setRecommendationVoteError: (state, { payload }) => { + state.error = payload + }, loadDBAnalysisReports: (state) => { state.history.loading = true }, @@ -77,6 +87,9 @@ export const { setSelectedAnalysisId, setShowNoExpiryGroup, setDatabaseAnalysisViewTab, + setRecommendationVote, + setRecommendationVoteSuccess, + setRecommendationVoteError, } = databaseAnalysisSlice.actions // The reducer @@ -116,6 +129,43 @@ export function fetchDBAnalysisAction( } } +// Asynchronous thunk action +export function putRecommendationVote( + recommendationName: string, + vote: Vote, + onSuccessAction?: (instanceId: string, name: string, vote: Vote) => void, + onFailAction?: () => void, +) { + return async (dispatch: AppDispatch, stateInit: () => RootState) => { + try { + dispatch(setRecommendationVote()) + const state = stateInit() + const instanceId = state.connections.instances.connectedInstance?.id + + const { data, status } = await apiService.patch( + getUrl( + instanceId, + ApiEndpoints.DATABASE_ANALYSIS, + state.analytics.databaseAnalysis.history.selectedAnalysis ?? '', + ), + { name: recommendationName, vote }, + ) + + if (isStatusSuccessful(status)) { + dispatch(setRecommendationVoteSuccess(data)) + + onSuccessAction?.(instanceId, recommendationName, vote) + } + } catch (_err) { + const error = _err as AxiosError + const errorMessage = getApiErrorMessage(error) + dispatch(addErrorNotification(error)) + dispatch(setRecommendationVoteError(errorMessage)) + onFailAction?.() + } + } +} + export function fetchDBAnalysisReportsHistory( instanceId: string, onSuccessAction?: (data: ShortDatabaseAnalysis[]) => void, diff --git a/redisinsight/ui/src/slices/tests/analytics/dbAnalysis.spec.ts b/redisinsight/ui/src/slices/tests/analytics/dbAnalysis.spec.ts index 122f66975c..0ea90411cb 100644 --- a/redisinsight/ui/src/slices/tests/analytics/dbAnalysis.spec.ts +++ b/redisinsight/ui/src/slices/tests/analytics/dbAnalysis.spec.ts @@ -18,7 +18,12 @@ import reducer, { dbAnalysisReportsSelector, dbAnalysisSelector, setShowNoExpiryGroup, + setRecommendationVote, + setRecommendationVoteSuccess, + setRecommendationVoteError, + putRecommendationVote, } from 'uiSrc/slices/analytics/dbAnalysis' +import { Vote } from 'uiSrc/constants/recommendations' import { cleanup, initialStateDefault, mockedStore } from 'uiSrc/utils/test-utils' let store: typeof mockedStore @@ -159,6 +164,26 @@ describe('db analysis slice', () => { expect(dbAnalysisSelector(rootState)).toEqual(state) }) }) + describe('setRecommendationVoteError', () => { + it('should properly set error', () => { + // Arrange + const error = 'Some error' + const state = { + ...initialState, + error, + loading: false, + } + + // Act + const nextState = reducer(initialState, setRecommendationVoteError(error)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + analytics: { databaseAnalysis: nextState }, + }) + expect(dbAnalysisSelector(rootState)).toEqual(state) + }) + }) describe('getDBAnalysis', () => { it('should properly set loading: true', () => { // Arrange @@ -218,6 +243,26 @@ describe('db analysis slice', () => { expect(dbAnalysisSelector(rootState)).toEqual(state) }) }) + describe('setRecommendationVoteSuccess', () => { + it('should properly set data', () => { + const payload = mockAnalysis + // Arrange + const state = { + ...initialState, + loading: false, + data: mockAnalysis + } + + // Act + const nextState = reducer(initialState, setRecommendationVoteSuccess(payload)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + analytics: { databaseAnalysis: nextState }, + }) + expect(dbAnalysisSelector(rootState)).toEqual(state) + }) + }) describe('loadDBAnalysisReportsSuccess', () => { it('should properly set data to history', () => { const payload = [mockHistoryReport] @@ -408,6 +453,53 @@ describe('db analysis slice', () => { loadDBAnalysisReportsError(errorMessage) ] + expect(store.getActions()).toEqual(expectedActions) + }) + }) + describe('putRecommendationVote', () => { + it('succeed to put recommendation vote', async () => { + const data = mockAnalysis + const responsePayload = { data, status: 200 } + + apiService.patch = jest.fn().mockResolvedValue(responsePayload) + + // Act + await store.dispatch( + putRecommendationVote('name', Vote.Like) + ) + + // Assert + const expectedActions = [ + setRecommendationVote(), + setRecommendationVoteSuccess(data), + ] + + expect(store.getActions()).toEqual(expectedActions) + }) + + it('failed to put recommendation vote', async () => { + const errorMessage = 'Something was wrong!' + const responsePayload = { + response: { + status: 500, + data: { message: errorMessage }, + }, + } + + apiService.patch = jest.fn().mockRejectedValue(responsePayload) + + // Act + await store.dispatch( + putRecommendationVote('name', Vote.Like) + ) + + // Assert + const expectedActions = [ + setRecommendationVote(), + addErrorNotification(responsePayload as AxiosError), + setRecommendationVoteError(errorMessage) + ] + expect(store.getActions()).toEqual(expectedActions) }) }) diff --git a/redisinsight/ui/src/telemetry/events.ts b/redisinsight/ui/src/telemetry/events.ts index 1099682ff4..35acaeffdb 100644 --- a/redisinsight/ui/src/telemetry/events.ts +++ b/redisinsight/ui/src/telemetry/events.ts @@ -72,7 +72,7 @@ export enum TelemetryEvent { BROWSER_KEY_FIELD_VALUE_COLLAPSED = 'BROWSER_KEY_FIELD_VALUE_COLLAPSED', BROWSER_KEY_DETAILS_FORMATTER_CHANGED = 'BROWSER_KEY_DETAILS_FORMATTER_CHANGED', BROWSER_WORKBENCH_LINK_CLICKED = 'BROWSER_WORKBENCH_LINK_CLICKED', - BROWSER_DATABASE_INDEX_CHANGED= 'BROWSER_DATABASE_INDEX_CHANGED', + BROWSER_DATABASE_INDEX_CHANGED = 'BROWSER_DATABASE_INDEX_CHANGED', CLI_OPENED = 'CLI_OPENED', CLI_CLOSED = 'CLI_CLOSED', @@ -194,6 +194,7 @@ export enum TelemetryEvent { DATABASE_ANALYSIS_DATA_SUMMARY_CLICKED = 'DATABASE_ANALYSIS_DATA_SUMMARY_CLICKED', DATABASE_ANALYSIS_RECOMMENDATIONS_EXPANDED = 'DATABASE_ANALYSIS_RECOMMENDATIONS_EXPANDED', DATABASE_ANALYSIS_RECOMMENDATIONS_COLLAPSED = 'DATABASE_ANALYSIS_RECOMMENDATIONS_COLLAPSED', + DATABASE_ANALYSIS_RECOMMENDATIONS_VOTED = 'DATABASE_ANALYSIS_RECOMMENDATIONS_VOTED', USER_SURVEY_LINK_CLICKED = 'USER_SURVEY_LINK_CLICKED', From edbc3630a508ff89d8472e4469613afd6ebf116e Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 19 Jan 2023 18:59:13 +0400 Subject: [PATCH 100/108] #RI-3977 - add recommendation voting --- .../database-analysis.controller.ts | 30 +++++++- .../database-analysis.service.ts | 11 ++- .../modules/database-analysis/dto/index.ts | 1 + .../dto/recommendation-vote.dto.ts | 18 +++++ .../models/recommendation.ts | 7 ++ .../database-analysis.provider.spec.ts | 35 ++++++++- .../providers/database-analysis.provider.ts | 26 +++++++ .../PATCH-databases-id-analysis.test.ts | 72 +++++++++++++++++++ .../test/api/database-analysis/constants.ts | 2 + redisinsight/api/test/helpers/constants.ts | 5 ++ 10 files changed, 203 insertions(+), 4 deletions(-) create mode 100644 redisinsight/api/src/modules/database-analysis/dto/recommendation-vote.dto.ts create mode 100644 redisinsight/api/test/api/database-analysis/PATCH-databases-id-analysis.test.ts diff --git a/redisinsight/api/src/modules/database-analysis/database-analysis.controller.ts b/redisinsight/api/src/modules/database-analysis/database-analysis.controller.ts index fa9e8d675d..fdc79fe1ce 100644 --- a/redisinsight/api/src/modules/database-analysis/database-analysis.controller.ts +++ b/redisinsight/api/src/modules/database-analysis/database-analysis.controller.ts @@ -1,6 +1,6 @@ import { Body, - Controller, Get, Param, Post, UseInterceptors, UsePipes, ValidationPipe, + Controller, Get, Param, Post, Patch, UseInterceptors, UsePipes, ValidationPipe, } from '@nestjs/common'; import { ApiEndpoint } from 'src/decorators/api-endpoint.decorator'; import { ApiTags } from '@nestjs/swagger'; @@ -8,7 +8,7 @@ import { DatabaseAnalysisService } from 'src/modules/database-analysis/database- import { DatabaseAnalysis, ShortDatabaseAnalysis } from 'src/modules/database-analysis/models'; import { BrowserSerializeInterceptor } from 'src/common/interceptors'; import { ApiQueryRedisStringEncoding, ClientMetadataParam } from 'src/common/decorators'; -import { CreateDatabaseAnalysisDto } from 'src/modules/database-analysis/dto'; +import { CreateDatabaseAnalysisDto, RecommendationVoteDto } from 'src/modules/database-analysis/dto'; import { ClientMetadata } from 'src/common/models'; @UseInterceptors(BrowserSerializeInterceptor) @@ -72,4 +72,30 @@ export class DatabaseAnalysisController { ): Promise { return this.service.list(databaseId); } + + @Patch(':id') + @ApiEndpoint({ + description: 'Update database instance by id', + statusCode: 200, + responses: [ + { + status: 200, + description: 'Updated database instance\' response', + type: DatabaseAnalysis, + }, + ], + }) + @UsePipes( + new ValidationPipe({ + transform: true, + whitelist: true, + forbidNonWhitelisted: true, + }), + ) + async modify( + @Param('id') id: string, + @Body() dto: RecommendationVoteDto, + ): Promise { + return await this.service.vote(id, dto); + } } diff --git a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts index 7b2918d26f..4d04c4a8b4 100644 --- a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts +++ b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts @@ -7,7 +7,7 @@ import { DatabaseAnalyzer } from 'src/modules/database-analysis/providers/databa import { plainToClass } from 'class-transformer'; import { DatabaseAnalysis, ShortDatabaseAnalysis } from 'src/modules/database-analysis/models'; import { DatabaseAnalysisProvider } from 'src/modules/database-analysis/providers/database-analysis.provider'; -import { CreateDatabaseAnalysisDto } from 'src/modules/database-analysis/dto'; +import { CreateDatabaseAnalysisDto, RecommendationVoteDto } from 'src/modules/database-analysis/dto'; import { KeysScanner } from 'src/modules/database-analysis/scanner/keys-scanner'; import { DatabaseConnectionService } from 'src/modules/database/database-connection.service'; import { ClientMetadata } from 'src/common/models'; @@ -112,4 +112,13 @@ export class DatabaseAnalysisService { async list(databaseId: string): Promise { return this.databaseAnalysisProvider.list(databaseId); } + + /** + * Set user vote for recommendation + * @param id + * @param recommendation + */ + async vote(id: string, recommendation: RecommendationVoteDto): Promise { + return this.databaseAnalysisProvider.recommendationVote(id, recommendation); + } } diff --git a/redisinsight/api/src/modules/database-analysis/dto/index.ts b/redisinsight/api/src/modules/database-analysis/dto/index.ts index b7e8392483..70aa99912a 100644 --- a/redisinsight/api/src/modules/database-analysis/dto/index.ts +++ b/redisinsight/api/src/modules/database-analysis/dto/index.ts @@ -1 +1,2 @@ export * from './create-database-analysis.dto'; +export * from './recommendation-vote.dto'; diff --git a/redisinsight/api/src/modules/database-analysis/dto/recommendation-vote.dto.ts b/redisinsight/api/src/modules/database-analysis/dto/recommendation-vote.dto.ts new file mode 100644 index 0000000000..11f2bce39e --- /dev/null +++ b/redisinsight/api/src/modules/database-analysis/dto/recommendation-vote.dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; + +export class RecommendationVoteDto { + @ApiProperty({ + description: 'Recommendation name', + type: String, + }) + @IsString() + name: string; + + @ApiProperty({ + description: 'User vote', + type: String, + }) + @IsString() + vote: string; +} diff --git a/redisinsight/api/src/modules/database-analysis/models/recommendation.ts b/redisinsight/api/src/modules/database-analysis/models/recommendation.ts index 9cd1b8ab3f..74f43de895 100644 --- a/redisinsight/api/src/modules/database-analysis/models/recommendation.ts +++ b/redisinsight/api/src/modules/database-analysis/models/recommendation.ts @@ -16,4 +16,11 @@ export class Recommendation { }) @Expose() params?: any; + + @ApiPropertyOptional({ + description: 'User vote', + example: 'Amazing', + }) + @Expose() + vote?: string; } diff --git a/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.spec.ts b/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.spec.ts index eb06385afa..22a15f68d7 100644 --- a/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.spec.ts +++ b/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.spec.ts @@ -13,7 +13,7 @@ import { Repository } from 'typeorm'; import { getRepositoryToken } from '@nestjs/typeorm'; import { DatabaseAnalysisProvider } from 'src/modules/database-analysis/providers/database-analysis.provider'; import { DatabaseAnalysis } from 'src/modules/database-analysis/models'; -import { CreateDatabaseAnalysisDto } from 'src/modules/database-analysis/dto'; +import { CreateDatabaseAnalysisDto, RecommendationVoteDto } from 'src/modules/database-analysis/dto'; import { RedisDataType } from 'src/modules/browser/dto'; import { plainToClass } from 'class-transformer'; import { ScanFilter } from 'src/modules/database-analysis/models/scan-filter'; @@ -150,6 +150,16 @@ const mockDatabaseAnalysis = { recommendations: [{ name: 'luaScript' }], } as DatabaseAnalysis; +const mockDatabaseAnalysisWithVote = { + ...mockDatabaseAnalysis, + recommendations: [{ name: 'luaScript', vote: 'amazing' }], +} as DatabaseAnalysis; + +const mockRecommendationVoteDto: RecommendationVoteDto = { + name: 'luaScript', + vote: 'amazing', +}; + describe('DatabaseAnalysisProvider', () => { let service: DatabaseAnalysisProvider; let repository: MockType>; @@ -254,4 +264,27 @@ describe('DatabaseAnalysisProvider', () => { ); }); }); + + describe('recommendationVote', () => { + it('should return updated database analysis', async () => { + repository.findOneBy.mockReturnValueOnce(mockDatabaseAnalysisEntity); + repository.update.mockReturnValueOnce(true); + await encryptionService.encrypt.mockReturnValue(mockEncryptResult); + + expect(await service.recommendationVote(mockDatabaseAnalysis.id, mockRecommendationVoteDto)) + .toEqual(mockDatabaseAnalysisWithVote); + }); + + it('should throw an error', async () => { + repository.findOneBy.mockReturnValueOnce(null); + + try { + await service.recommendationVote(mockDatabaseAnalysis.id, mockRecommendationVoteDto); + fail(); + } catch (e) { + expect(e).toBeInstanceOf(NotFoundException); + expect(e.message).toEqual(ERROR_MESSAGES.DATABASE_ANALYSIS_NOT_FOUND); + } + }); + }); }); diff --git a/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.ts b/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.ts index 66d51e701b..695e4e88de 100644 --- a/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.ts +++ b/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.ts @@ -6,6 +6,7 @@ import { Repository } from 'typeorm'; import { EncryptionService } from 'src/modules/encryption/encryption.service'; import { plainToClass } from 'class-transformer'; import { DatabaseAnalysis, ShortDatabaseAnalysis } from 'src/modules/database-analysis/models'; +import { RecommendationVoteDto } from 'src/modules/database-analysis/dto'; import { classToClass } from 'src/utils'; import config from 'src/utils/config'; import ERROR_MESSAGES from 'src/constants/error-messages'; @@ -70,6 +71,31 @@ export class DatabaseAnalysisProvider { return classToClass(DatabaseAnalysis, await this.decryptEntity(entity, true)); } + /** + * Fetches entity, decrypt, update and return updated DatabaseAnalysis model + * @param id + * @param dto + */ + async recommendationVote(id: string, dto: RecommendationVoteDto): Promise { + this.logger.log('Updating database analysis with recommendation vote'); + const { name, vote } = dto; + const oldDatabaseAnalysis = await this.repository.findOneBy({ id }); + + if (!oldDatabaseAnalysis) { + this.logger.error(`Database analysis with id:${id} was not Found`); + throw new NotFoundException(ERROR_MESSAGES.DATABASE_ANALYSIS_NOT_FOUND); + } + + const entity = classToClass(DatabaseAnalysis, await this.decryptEntity(oldDatabaseAnalysis, true)); + + entity.recommendations = entity.recommendations.map((recommendation) => ( + recommendation.name === name ? { ...recommendation, vote } : recommendation)); + + await this.repository.update(id, await this.encryptEntity(plainToClass(DatabaseAnalysisEntity, entity))); + + return entity; + } + /** * Return list of database analysis with several fields only * @param databaseId diff --git a/redisinsight/api/test/api/database-analysis/PATCH-databases-id-analysis.test.ts b/redisinsight/api/test/api/database-analysis/PATCH-databases-id-analysis.test.ts new file mode 100644 index 0000000000..54c240e43e --- /dev/null +++ b/redisinsight/api/test/api/database-analysis/PATCH-databases-id-analysis.test.ts @@ -0,0 +1,72 @@ +import { + expect, + describe, + deps, + before, + getMainCheckFn, + Joi, + generateInvalidDataTestCases, + validateInvalidDataTestCase, +} from '../deps'; +import { analysisSchema } from './constants'; +const { localDb, request, server, constants, rte } = deps; + +const endpoint = ( + instanceId = constants.TEST_INSTANCE_ID, + id = constants.TEST_DATABASE_ANALYSIS_ID_1, +) => + request(server).patch(`/${constants.API.DATABASES}/${instanceId}/analysis/${id}`); + + // input data schema +const dataSchema = Joi.object({ + name: Joi.string(), + vote: Joi.string(), +}).strict(); + +const validInputData = { + name: constants.getRandomString(), + vote: constants.getRandomString(), +}; + +const responseSchema = analysisSchema; +const mainCheckFn = getMainCheckFn(endpoint); +let repository; + +describe('PATCH /databases/:instanceId/analysis/:id', () => { + before(async () => await localDb.generateNDatabaseAnalysis({ + databaseId: constants.TEST_INSTANCE_ID, + id: constants.TEST_DATABASE_ANALYSIS_ID_1, + createdAt: constants.TEST_DATABASE_ANALYSIS_CREATED_AT_1, + }, 1, true), + ); + + describe('Validation', () => { + generateInvalidDataTestCases(dataSchema, validInputData).map( + validateInvalidDataTestCase(endpoint, dataSchema), + ); + }); + + describe('recommendations', () => { + describe('recommendation vote', () => { + [ + { + name: 'Should add vote for RTS recommendation', + data: { + name: 'luaScript', + vote: 'amazing', + }, + statusCode: 200, + responseSchema, + checkFn: async ({ body }) => { + expect(body.recommendations).to.include.deep.members([ + constants.TEST_LUA_SCRIPT_VOTE_RECOMMENDATION + ]); + }, + after: async () => { + expect(await repository.count()).to.eq(5); + } + }, + ].map(mainCheckFn); + }); + }); +}); diff --git a/redisinsight/api/test/api/database-analysis/constants.ts b/redisinsight/api/test/api/database-analysis/constants.ts index 563a66a7d0..22048a0d21 100644 --- a/redisinsight/api/test/api/database-analysis/constants.ts +++ b/redisinsight/api/test/api/database-analysis/constants.ts @@ -2,6 +2,8 @@ import { Joi } from '../../helpers/test'; export const typedRecommendationSchema = Joi.object({ name: Joi.string().required(), + vote: Joi.string(), + params: Joi.any(), }); export const typedTotalSchema = Joi.object({ diff --git a/redisinsight/api/test/helpers/constants.ts b/redisinsight/api/test/helpers/constants.ts index fb1a1ab585..99cf2fad48 100644 --- a/redisinsight/api/test/helpers/constants.ts +++ b/redisinsight/api/test/helpers/constants.ts @@ -513,5 +513,10 @@ export const constants = { TEST_REDISEARCH_RECOMMENDATION: { name: RECOMMENDATION_NAMES.REDIS_SEARCH, }, + + TEST_LUA_SCRIPT_VOTE_RECOMMENDATION: { + name: RECOMMENDATION_NAMES.LUA_SCRIPT, + vote: 'amazing', + }, // etc... } From 07d183f36205395cb092cfa3710fd405cf379b90 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Thu, 19 Jan 2023 22:51:42 +0400 Subject: [PATCH 101/108] #RI-3977 - fix IT --- .../api/database-analysis/PATCH-databases-id-analysis.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/redisinsight/api/test/api/database-analysis/PATCH-databases-id-analysis.test.ts b/redisinsight/api/test/api/database-analysis/PATCH-databases-id-analysis.test.ts index 54c240e43e..98e72f179a 100644 --- a/redisinsight/api/test/api/database-analysis/PATCH-databases-id-analysis.test.ts +++ b/redisinsight/api/test/api/database-analysis/PATCH-databases-id-analysis.test.ts @@ -62,9 +62,6 @@ describe('PATCH /databases/:instanceId/analysis/:id', () => { constants.TEST_LUA_SCRIPT_VOTE_RECOMMENDATION ]); }, - after: async () => { - expect(await repository.count()).to.eq(5); - } }, ].map(mainCheckFn); }); From b5ccb093e93d81c57a05759e90a7c38eb1e193a5 Mon Sep 17 00:00:00 2001 From: Amir Allayarov Date: Fri, 20 Jan 2023 11:54:19 +0400 Subject: [PATCH 102/108] #3977 - demo comments resolve --- .../models/recommendation.ts | 2 +- .../database-analysis.provider.spec.ts | 4 ++-- .../PATCH-databases-id-analysis.test.ts | 2 +- redisinsight/api/test/helpers/constants.ts | 2 +- .../ui/src/constants/recommendations.ts | 2 +- .../RecommendationVoting.spec.tsx | 8 +++---- .../RecommendationVoting.tsx | 24 ++++++++++++------- 7 files changed, 25 insertions(+), 19 deletions(-) diff --git a/redisinsight/api/src/modules/database-analysis/models/recommendation.ts b/redisinsight/api/src/modules/database-analysis/models/recommendation.ts index 74f43de895..e98007998f 100644 --- a/redisinsight/api/src/modules/database-analysis/models/recommendation.ts +++ b/redisinsight/api/src/modules/database-analysis/models/recommendation.ts @@ -19,7 +19,7 @@ export class Recommendation { @ApiPropertyOptional({ description: 'User vote', - example: 'Amazing', + example: 'useful', }) @Expose() vote?: string; diff --git a/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.spec.ts b/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.spec.ts index 22a15f68d7..4cfe3bd067 100644 --- a/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.spec.ts +++ b/redisinsight/api/src/modules/database-analysis/providers/database-analysis.provider.spec.ts @@ -152,12 +152,12 @@ const mockDatabaseAnalysis = { const mockDatabaseAnalysisWithVote = { ...mockDatabaseAnalysis, - recommendations: [{ name: 'luaScript', vote: 'amazing' }], + recommendations: [{ name: 'luaScript', vote: 'useful' }], } as DatabaseAnalysis; const mockRecommendationVoteDto: RecommendationVoteDto = { name: 'luaScript', - vote: 'amazing', + vote: 'useful', }; describe('DatabaseAnalysisProvider', () => { diff --git a/redisinsight/api/test/api/database-analysis/PATCH-databases-id-analysis.test.ts b/redisinsight/api/test/api/database-analysis/PATCH-databases-id-analysis.test.ts index 98e72f179a..2da67f5ff3 100644 --- a/redisinsight/api/test/api/database-analysis/PATCH-databases-id-analysis.test.ts +++ b/redisinsight/api/test/api/database-analysis/PATCH-databases-id-analysis.test.ts @@ -53,7 +53,7 @@ describe('PATCH /databases/:instanceId/analysis/:id', () => { name: 'Should add vote for RTS recommendation', data: { name: 'luaScript', - vote: 'amazing', + vote: 'useful', }, statusCode: 200, responseSchema, diff --git a/redisinsight/api/test/helpers/constants.ts b/redisinsight/api/test/helpers/constants.ts index 99cf2fad48..e38535b145 100644 --- a/redisinsight/api/test/helpers/constants.ts +++ b/redisinsight/api/test/helpers/constants.ts @@ -516,7 +516,7 @@ export const constants = { TEST_LUA_SCRIPT_VOTE_RECOMMENDATION: { name: RECOMMENDATION_NAMES.LUA_SCRIPT, - vote: 'amazing', + vote: 'useful', }, // etc... } diff --git a/redisinsight/ui/src/constants/recommendations.ts b/redisinsight/ui/src/constants/recommendations.ts index 513cf24a30..2ed717788b 100644 --- a/redisinsight/ui/src/constants/recommendations.ts +++ b/redisinsight/ui/src/constants/recommendations.ts @@ -1,5 +1,5 @@ export enum Vote { - DoubleLike = 'amazing', + DoubleLike = 'very useful', Like = 'useful', Dislike = 'not useful' } diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendation-voting/RecommendationVoting.spec.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendation-voting/RecommendationVoting.spec.tsx index 8a114ee32c..9c479d6175 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendation-voting/RecommendationVoting.spec.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendation-voting/RecommendationVoting.spec.tsx @@ -39,9 +39,9 @@ describe('RecommendationVoting', () => { expect(render()).toBeTruthy() }) - it('should call "setRecommendationVote" action be called after click "amazing-vote-btn"', () => { + it('should call "setRecommendationVote" action be called after click "very-useful-vote-btn"', () => { render() - fireEvent.click(screen.getByTestId('amazing-vote-btn')) + fireEvent.click(screen.getByTestId('very-useful-vote-btn')) const expectedActions = [setRecommendationVote()] expect(store.getActions()).toEqual(expectedActions) @@ -75,9 +75,9 @@ describe('RecommendationVoting', () => { }) it('should render component where all buttons are disabled"', async () => { - render() + render() - expect(screen.getByTestId('amazing-vote-btn')).toBeDisabled() + expect(screen.getByTestId('very-useful-vote-btn')).toBeDisabled() expect(screen.getByTestId('useful-vote-btn')).toBeDisabled() expect(screen.getByTestId('not-useful-vote-btn')).toBeDisabled() }) diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendation-voting/RecommendationVoting.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendation-voting/RecommendationVoting.tsx index e029cb2b52..4b46364b70 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/recommendation-voting/RecommendationVoting.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/recommendation-voting/RecommendationVoting.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react' -import { useDispatch } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import cx from 'classnames' import { EuiButton, @@ -11,6 +11,7 @@ import { EuiIcon, EuiLink, } from '@elastic/eui' +import { userSettingsConfigSelector } from 'uiSrc/slices/user/user-settings' import { putRecommendationVote } from 'uiSrc/slices/analytics/dbAnalysis' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { EXTERNAL_LINKS } from 'uiSrc/constants/links' @@ -25,6 +26,7 @@ import styles from './styles.module.scss' export interface Props { vote?: Vote, name: string } const RecommendationVoting = ({ vote, name }: Props) => { + const config = useSelector(userSettingsConfigSelector) const [isPopoverOpen, setIsPopoverOpen] = useState(false) const dispatch = useDispatch() @@ -46,29 +48,33 @@ const RecommendationVoting = ({ vote, name }: Props) => { dispatch(putRecommendationVote(name, vote, onSuccessVoted)) } + const getTooltipContent = (content: string) => (config?.agreements?.analytics + ? content + : 'Enable Analytics on the Settings page to vote for a recommendation') + return ( Rate Recommendation
handleClick(name, Vote.DoubleLike)} /> { /> { panelClassName={cx('euiToolTip', 'popoverLikeTooltip', styles.popover)} button={( Date: Mon, 23 Jan 2023 11:46:30 +0100 Subject: [PATCH 103/108] Add upvote recommendations test and page object for recommendations --- tests/e2e/pageObjects/recommendations-page.ts | 34 ++++++++++++++ .../database-recommendations.e2e.ts | 44 +++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 tests/e2e/pageObjects/recommendations-page.ts create mode 100644 tests/e2e/tests/critical-path/database-overview/database-recommendations.e2e.ts diff --git a/tests/e2e/pageObjects/recommendations-page.ts b/tests/e2e/pageObjects/recommendations-page.ts new file mode 100644 index 0000000000..4027da79b3 --- /dev/null +++ b/tests/e2e/pageObjects/recommendations-page.ts @@ -0,0 +1,34 @@ +import {Selector, t} from 'testcafe'; + +export class RecommendationsPage { + //CSS Selectors + veryUsefulVoteBtn = Selector('[data-testid=very-useful-vote-btn]').nth(0); + usefulVoteBtn = Selector('[data-testid=useful-vote-btn]').nth(0); + notUsefulVoteBtn = Selector('[data-testid=not-useful-vote-btn]').nth(0); + recommendationsFeedbackBtn = Selector('[data-testid=recommendation-feedback-btn]'); + + async voteForVeryUsefulAndVerifyDisabled(): Promise { + await t.click(this.veryUsefulVoteBtn); + await this.verifyVoteDisabled(); + } + + async voteForUsefulAndVerifyDisabled(): Promise { + await t.click(this.usefulVoteBtn); + await this.verifyVoteDisabled(); + } + + async voteForNotUsefulAndVerifyDisabled(): Promise { + await t.click(this.notUsefulVoteBtn); + await this.verifyVoteDisabled(); + } + + async verifyVoteDisabled(): Promise{ + // Verify that user can rate recommendations with one of 3 existing types at the same time + await t.expect(this.veryUsefulVoteBtn.nth(0) + .hasAttribute('disabled')).ok(); + await t.expect(this.usefulVoteBtn.nth(0) + .hasAttribute('disabled')).ok(); + await t.expect(this.notUsefulVoteBtn.nth(0) + .hasAttribute('disabled')).ok(); + } +} diff --git a/tests/e2e/tests/critical-path/database-overview/database-recommendations.e2e.ts b/tests/e2e/tests/critical-path/database-overview/database-recommendations.e2e.ts new file mode 100644 index 0000000000..d4d169bfe9 --- /dev/null +++ b/tests/e2e/tests/critical-path/database-overview/database-recommendations.e2e.ts @@ -0,0 +1,44 @@ +import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { rte } from '../../../helpers/constants'; +import { + commonUrl, + ossStandaloneConfig +} from '../../../helpers/conf'; +import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import {MemoryEfficiencyPage, MyRedisDatabasePage} from '../../../pageObjects'; +import {RecommendationsPage} from '../../../pageObjects/recommendations-page'; +import {Common} from '../../../helpers/common'; + +const myRedisDatabasePage = new MyRedisDatabasePage(); +const memoryEfficiencyPage = new MemoryEfficiencyPage(); +const recommendationPage = new RecommendationsPage(); +const common = new Common(); + +fixture `Upvote recommendations` + .meta({ type: 'critical_path', rte: rte.standalone }) + .page(commonUrl) + .beforeEach(async t => { + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + // Go to Analysis Tools page and create new report and open recommendations + await t.click(myRedisDatabasePage.analysisPageButton); + await t.click(memoryEfficiencyPage.newReportBtn); + await t.click(memoryEfficiencyPage.recommendationsTab); + }) + .afterEach(async() => { + await deleteStandaloneDatabaseApi(ossStandaloneConfig); + }); +test.only('Verify that user can upvote recommendations', async t => { + await recommendationPage.voteForVeryUsefulAndVerifyDisabled(); + // Verify that user can see previous votes when reload the page + await common.reloadPage(); + await t.click(memoryEfficiencyPage.recommendationsTab); + await recommendationPage.verifyVoteDisabled(); + + await t.click(memoryEfficiencyPage.newReportBtn); + await recommendationPage.voteForUsefulAndVerifyDisabled(); + + await t.click(memoryEfficiencyPage.newReportBtn); + await recommendationPage.voteForNotUsefulAndVerifyDisabled(); + // Verify that user can see the popup with link when he votes for “Not useful” + await t.expect(recommendationPage.recommendationsFeedbackBtn.visible).ok(); +}); From 0aabc97f885a69acd6bf46bb09bcd97aa0af9cd4 Mon Sep 17 00:00:00 2001 From: nmammadli Date: Mon, 23 Jan 2023 12:02:19 +0100 Subject: [PATCH 104/108] Update recommendations-page.ts Delete unused nth from verifyVoteDisabled --- tests/e2e/pageObjects/recommendations-page.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/e2e/pageObjects/recommendations-page.ts b/tests/e2e/pageObjects/recommendations-page.ts index 4027da79b3..9b5c850dee 100644 --- a/tests/e2e/pageObjects/recommendations-page.ts +++ b/tests/e2e/pageObjects/recommendations-page.ts @@ -1,7 +1,6 @@ import {Selector, t} from 'testcafe'; export class RecommendationsPage { - //CSS Selectors veryUsefulVoteBtn = Selector('[data-testid=very-useful-vote-btn]').nth(0); usefulVoteBtn = Selector('[data-testid=useful-vote-btn]').nth(0); notUsefulVoteBtn = Selector('[data-testid=not-useful-vote-btn]').nth(0); @@ -24,11 +23,11 @@ export class RecommendationsPage { async verifyVoteDisabled(): Promise{ // Verify that user can rate recommendations with one of 3 existing types at the same time - await t.expect(this.veryUsefulVoteBtn.nth(0) + await t.expect(this.veryUsefulVoteBtn .hasAttribute('disabled')).ok(); - await t.expect(this.usefulVoteBtn.nth(0) + await t.expect(this.usefulVoteBtn .hasAttribute('disabled')).ok(); - await t.expect(this.notUsefulVoteBtn.nth(0) + await t.expect(this.notUsefulVoteBtn .hasAttribute('disabled')).ok(); } } From d9c1d918c49f1cfce4aa38061621edd98d0f074c Mon Sep 17 00:00:00 2001 From: nmammadli Date: Mon, 23 Jan 2023 12:39:15 +0100 Subject: [PATCH 105/108] Update recommendations-page.ts Add assert message --- tests/e2e/pageObjects/recommendations-page.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/e2e/pageObjects/recommendations-page.ts b/tests/e2e/pageObjects/recommendations-page.ts index 9b5c850dee..0be344b64c 100644 --- a/tests/e2e/pageObjects/recommendations-page.ts +++ b/tests/e2e/pageObjects/recommendations-page.ts @@ -24,10 +24,10 @@ export class RecommendationsPage { async verifyVoteDisabled(): Promise{ // Verify that user can rate recommendations with one of 3 existing types at the same time await t.expect(this.veryUsefulVoteBtn - .hasAttribute('disabled')).ok(); + .hasAttribute('disabled')).ok('very useful vote button is not disabled'); await t.expect(this.usefulVoteBtn - .hasAttribute('disabled')).ok(); + .hasAttribute('disabled')).ok('useful vote button is not disabled'); await t.expect(this.notUsefulVoteBtn - .hasAttribute('disabled')).ok(); + .hasAttribute('disabled')).ok('not useful vote button is not disabled'); } } From af09d55d98370a31be06c33a126a10b7507197e6 Mon Sep 17 00:00:00 2001 From: nmammadli Date: Mon, 23 Jan 2023 14:16:38 +0100 Subject: [PATCH 106/108] Update database-recommendations.e2e.ts Delete .only --- .../database-overview/database-recommendations.e2e.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/tests/critical-path/database-overview/database-recommendations.e2e.ts b/tests/e2e/tests/critical-path/database-overview/database-recommendations.e2e.ts index d4d169bfe9..eaad806d13 100644 --- a/tests/e2e/tests/critical-path/database-overview/database-recommendations.e2e.ts +++ b/tests/e2e/tests/critical-path/database-overview/database-recommendations.e2e.ts @@ -27,7 +27,7 @@ fixture `Upvote recommendations` .afterEach(async() => { await deleteStandaloneDatabaseApi(ossStandaloneConfig); }); -test.only('Verify that user can upvote recommendations', async t => { +test('Verify that user can upvote recommendations', async t => { await recommendationPage.voteForVeryUsefulAndVerifyDisabled(); // Verify that user can see previous votes when reload the page await common.reloadPage(); From e4b40fed841175f007e15384f68d160dfc42c31f Mon Sep 17 00:00:00 2001 From: nmammadli Date: Tue, 24 Jan 2023 09:21:15 +0100 Subject: [PATCH 107/108] Refactor Refactor for page objects --- .../memory-efficiency-actions.ts} | 24 +++++----- .../e2e/pageObjects/memory-efficiency-page.ts | 6 ++- .../database-recommendations.e2e.ts | 44 ------------------- .../memory-efficiency/recommendations.e2e.ts | 26 +++++++++++ 4 files changed, 41 insertions(+), 59 deletions(-) rename tests/e2e/{pageObjects/recommendations-page.ts => common-actions/memory-efficiency-actions.ts} (52%) delete mode 100644 tests/e2e/tests/critical-path/database-overview/database-recommendations.e2e.ts diff --git a/tests/e2e/pageObjects/recommendations-page.ts b/tests/e2e/common-actions/memory-efficiency-actions.ts similarity index 52% rename from tests/e2e/pageObjects/recommendations-page.ts rename to tests/e2e/common-actions/memory-efficiency-actions.ts index 0be344b64c..49dabfd61a 100644 --- a/tests/e2e/pageObjects/recommendations-page.ts +++ b/tests/e2e/common-actions/memory-efficiency-actions.ts @@ -1,33 +1,29 @@ -import {Selector, t} from 'testcafe'; - -export class RecommendationsPage { - veryUsefulVoteBtn = Selector('[data-testid=very-useful-vote-btn]').nth(0); - usefulVoteBtn = Selector('[data-testid=useful-vote-btn]').nth(0); - notUsefulVoteBtn = Selector('[data-testid=not-useful-vote-btn]').nth(0); - recommendationsFeedbackBtn = Selector('[data-testid=recommendation-feedback-btn]'); +import {t} from 'testcafe'; +import { MemoryEfficiencyPage } from '../pageObjects'; +const memoryEfficiencyPage = new MemoryEfficiencyPage(); +export class MemoryEfficiencyActions { async voteForVeryUsefulAndVerifyDisabled(): Promise { - await t.click(this.veryUsefulVoteBtn); + await t.click(memoryEfficiencyPage.veryUsefulVoteBtn); await this.verifyVoteDisabled(); } async voteForUsefulAndVerifyDisabled(): Promise { - await t.click(this.usefulVoteBtn); + await t.click(memoryEfficiencyPage.usefulVoteBtn); await this.verifyVoteDisabled(); } async voteForNotUsefulAndVerifyDisabled(): Promise { - await t.click(this.notUsefulVoteBtn); + await t.click(memoryEfficiencyPage.notUsefulVoteBtn); await this.verifyVoteDisabled(); } - async verifyVoteDisabled(): Promise{ // Verify that user can rate recommendations with one of 3 existing types at the same time - await t.expect(this.veryUsefulVoteBtn + await t.expect(memoryEfficiencyPage.veryUsefulVoteBtn .hasAttribute('disabled')).ok('very useful vote button is not disabled'); - await t.expect(this.usefulVoteBtn + await t.expect(memoryEfficiencyPage.usefulVoteBtn .hasAttribute('disabled')).ok('useful vote button is not disabled'); - await t.expect(this.notUsefulVoteBtn + await t.expect(memoryEfficiencyPage.notUsefulVoteBtn .hasAttribute('disabled')).ok('not useful vote button is not disabled'); } } diff --git a/tests/e2e/pageObjects/memory-efficiency-page.ts b/tests/e2e/pageObjects/memory-efficiency-page.ts index 376172ca8c..6a70527dbf 100644 --- a/tests/e2e/pageObjects/memory-efficiency-page.ts +++ b/tests/e2e/pageObjects/memory-efficiency-page.ts @@ -1,4 +1,4 @@ -import { Selector } from 'testcafe'; +import {Selector} from 'testcafe'; export class MemoryEfficiencyPage { //------------------------------------------------------------------------------------------- @@ -64,4 +64,8 @@ export class MemoryEfficiencyPage { avoidLogicalDbAccordion = Selector('[data-testid=avoidLogicalDatabases-accordion]'); convertHashToZipAccordion = Selector('[data-testid=convertHashtableToZiplist-accordion]'); compressHashAccordion = Selector('[data-testid=compressHashFieldNames-accordion]'); + veryUsefulVoteBtn = Selector('[data-testid=very-useful-vote-btn]').nth(0); + usefulVoteBtn = Selector('[data-testid=useful-vote-btn]').nth(0); + notUsefulVoteBtn = Selector('[data-testid=not-useful-vote-btn]').nth(0); + recommendationsFeedbackBtn = Selector('[data-testid=recommendation-feedback-btn]'); } diff --git a/tests/e2e/tests/critical-path/database-overview/database-recommendations.e2e.ts b/tests/e2e/tests/critical-path/database-overview/database-recommendations.e2e.ts deleted file mode 100644 index eaad806d13..0000000000 --- a/tests/e2e/tests/critical-path/database-overview/database-recommendations.e2e.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; -import { rte } from '../../../helpers/constants'; -import { - commonUrl, - ossStandaloneConfig -} from '../../../helpers/conf'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; -import {MemoryEfficiencyPage, MyRedisDatabasePage} from '../../../pageObjects'; -import {RecommendationsPage} from '../../../pageObjects/recommendations-page'; -import {Common} from '../../../helpers/common'; - -const myRedisDatabasePage = new MyRedisDatabasePage(); -const memoryEfficiencyPage = new MemoryEfficiencyPage(); -const recommendationPage = new RecommendationsPage(); -const common = new Common(); - -fixture `Upvote recommendations` - .meta({ type: 'critical_path', rte: rte.standalone }) - .page(commonUrl) - .beforeEach(async t => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); - // Go to Analysis Tools page and create new report and open recommendations - await t.click(myRedisDatabasePage.analysisPageButton); - await t.click(memoryEfficiencyPage.newReportBtn); - await t.click(memoryEfficiencyPage.recommendationsTab); - }) - .afterEach(async() => { - await deleteStandaloneDatabaseApi(ossStandaloneConfig); - }); -test('Verify that user can upvote recommendations', async t => { - await recommendationPage.voteForVeryUsefulAndVerifyDisabled(); - // Verify that user can see previous votes when reload the page - await common.reloadPage(); - await t.click(memoryEfficiencyPage.recommendationsTab); - await recommendationPage.verifyVoteDisabled(); - - await t.click(memoryEfficiencyPage.newReportBtn); - await recommendationPage.voteForUsefulAndVerifyDisabled(); - - await t.click(memoryEfficiencyPage.newReportBtn); - await recommendationPage.voteForNotUsefulAndVerifyDisabled(); - // Verify that user can see the popup with link when he votes for “Not useful” - await t.expect(recommendationPage.recommendationsFeedbackBtn.visible).ok(); -}); diff --git a/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts b/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts index 9ea4f2c7cd..4b010ed580 100644 --- a/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts +++ b/tests/e2e/tests/critical-path/memory-efficiency/recommendations.e2e.ts @@ -4,6 +4,7 @@ import { acceptLicenseTermsAndAddDatabaseApi, deleteCustomDatabase } from '../.. import { commonUrl, ossStandaloneBigConfig, ossStandaloneConfig } from '../../../helpers/conf'; import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; import { CliActions } from '../../../common-actions/cli-actions'; +import { MemoryEfficiencyActions } from '../../../common-actions/memory-efficiency-actions'; import { Common } from '../../../helpers/common'; const memoryEfficiencyPage = new MemoryEfficiencyPage(); @@ -13,6 +14,7 @@ const common = new Common(); const browserPage = new BrowserPage(); const cliPage = new CliPage(); const addRedisDatabasePage = new AddRedisDatabasePage(); +const memoryEfficiencyActions = new MemoryEfficiencyActions(); const externalPageLink = 'https://docs.redis.com/latest/ri/memory-optimizations/'; let keyName = `recomKey-${common.generateWord(10)}`; @@ -119,3 +121,27 @@ test await t.expect(memoryEfficiencyPage.avoidLogicalDbAccordion.exists).ok('Avoid using logical databases recommendation not displayed'); await t.expect(memoryEfficiencyPage.codeChangesLabel.exists).ok('Avoid using logical databases recommendation not have Code Changes label'); }); +test + .before(async t => { + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + // Go to Analysis Tools page and create new report and open recommendations + await t.click(myRedisDatabasePage.analysisPageButton); + await t.click(memoryEfficiencyPage.newReportBtn); + await t.click(memoryEfficiencyPage.recommendationsTab); + }).after(async() => { + await deleteStandaloneDatabaseApi(ossStandaloneConfig); + })('Verify that user can upvote recommendations', async t => { + await memoryEfficiencyActions.voteForVeryUsefulAndVerifyDisabled(); + // Verify that user can see previous votes when reload the page + await common.reloadPage(); + await t.click(memoryEfficiencyPage.recommendationsTab); + await memoryEfficiencyActions.verifyVoteDisabled(); + + await t.click(memoryEfficiencyPage.newReportBtn); + await memoryEfficiencyActions.voteForUsefulAndVerifyDisabled(); + + await t.click(memoryEfficiencyPage.newReportBtn); + await memoryEfficiencyActions.voteForNotUsefulAndVerifyDisabled(); + // Verify that user can see the popup with link when he votes for “Not useful” + await t.expect(memoryEfficiencyPage.recommendationsFeedbackBtn.visible).ok('popup did not appear after voting for not useful'); + }); From ccf980c30e67754c1307ba80affed63bedf4010c Mon Sep 17 00:00:00 2001 From: nmammadli Date: Tue, 24 Jan 2023 12:45:31 +0100 Subject: [PATCH 108/108] Update memory-efficiency-actions.ts Add comments to methods --- tests/e2e/common-actions/memory-efficiency-actions.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/e2e/common-actions/memory-efficiency-actions.ts b/tests/e2e/common-actions/memory-efficiency-actions.ts index 49dabfd61a..edb67920ee 100644 --- a/tests/e2e/common-actions/memory-efficiency-actions.ts +++ b/tests/e2e/common-actions/memory-efficiency-actions.ts @@ -3,16 +3,23 @@ import { MemoryEfficiencyPage } from '../pageObjects'; const memoryEfficiencyPage = new MemoryEfficiencyPage(); export class MemoryEfficiencyActions { + /* + vote for very useful and verify others are disabled + */ async voteForVeryUsefulAndVerifyDisabled(): Promise { await t.click(memoryEfficiencyPage.veryUsefulVoteBtn); await this.verifyVoteDisabled(); } - + /* + vote for useful and verify others are disabled + */ async voteForUsefulAndVerifyDisabled(): Promise { await t.click(memoryEfficiencyPage.usefulVoteBtn); await this.verifyVoteDisabled(); } - + /* + vote for not useful and verify others are disabled + */ async voteForNotUsefulAndVerifyDisabled(): Promise { await t.click(memoryEfficiencyPage.notUsefulVoteBtn); await this.verifyVoteDisabled();