diff --git a/redisinsight/ui/src/assets/img/icons/cheer.svg b/redisinsight/ui/src/assets/img/icons/cheer.svg new file mode 100644 index 0000000000..95baa5b9d0 --- /dev/null +++ b/redisinsight/ui/src/assets/img/icons/cheer.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/redisinsight/ui/src/assets/img/icons/mobile_module_not_loaded.svg b/redisinsight/ui/src/assets/img/icons/mobile_module_not_loaded.svg new file mode 100644 index 0000000000..0508b5147e --- /dev/null +++ b/redisinsight/ui/src/assets/img/icons/mobile_module_not_loaded.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/redisinsight/ui/src/assets/img/icons/module_not_loaded.svg b/redisinsight/ui/src/assets/img/icons/module_not_loaded.svg new file mode 100644 index 0000000000..e43e747c93 --- /dev/null +++ b/redisinsight/ui/src/assets/img/icons/module_not_loaded.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/redisinsight/ui/src/components/query-card/QueryCard.tsx b/redisinsight/ui/src/components/query-card/QueryCard.tsx index 694ed6187b..d5a5e12997 100644 --- a/redisinsight/ui/src/components/query-card/QueryCard.tsx +++ b/redisinsight/ui/src/components/query-card/QueryCard.tsx @@ -153,13 +153,15 @@ const QueryCard = (props: Props) => { setSelectedViewValue(value) } - const commonError = CommonErrorResponse(command, result) + const commonError = CommonErrorResponse(id, command, result) return ( -
{ { - const commonError = CommonErrorResponse(item.command, item.response) + const commonError = CommonErrorResponse(item.id, item.command, item.response) if (React.isValidElement(commonError)) { return ([wbSummaryCommand(item.command), commonError]) } diff --git a/redisinsight/ui/src/components/query-card/QueryCardCommonResult/components/CommonErrorResponse/CommonErrorResponse.tsx b/redisinsight/ui/src/components/query-card/QueryCardCommonResult/components/CommonErrorResponse/CommonErrorResponse.tsx index 7ed1277fdb..c6564c7da4 100644 --- a/redisinsight/ui/src/components/query-card/QueryCardCommonResult/components/CommonErrorResponse/CommonErrorResponse.tsx +++ b/redisinsight/ui/src/components/query-card/QueryCardCommonResult/components/CommonErrorResponse/CommonErrorResponse.tsx @@ -12,15 +12,13 @@ import { import { cliTexts, SelectCommand } from 'uiSrc/constants/cliOutput' import { CommandMonitor, CommandPSubscribe, Pages } from 'uiSrc/constants' import { CommandExecutionStatus } from 'uiSrc/slices/interfaces/cli' -import { RedisDefaultModules } from 'uiSrc/slices/interfaces' -import { RSNotLoadedContent } from 'uiSrc/pages/workbench/constants' import { cliSettingsSelector } from 'uiSrc/slices/cli/cli-settings' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import ModuleNotLoaded from 'uiSrc/pages/workbench/components/module-not-loaded' import { showMonitor } from 'uiSrc/slices/cli/monitor' -const CommonErrorResponse = (command = '', result?: any) => { +const CommonErrorResponse = (id: string, command = '', result?: any) => { const { instanceId = '' } = useParams<{ instanceId: string }>() const { unsupportedCommands: cliUnsupportedCommands, blockingCommands } = useSelector(cliSettingsSelector) const { modules } = useSelector(connectedInstanceSelector) @@ -69,8 +67,8 @@ const CommonErrorResponse = (command = '', result?: any) => { } const unsupportedModule = checkUnsupportedModuleCommand(modules, commandLine) - if (unsupportedModule === RedisDefaultModules.Search) { - return + if (unsupportedModule) { + return } return null diff --git a/redisinsight/ui/src/constants/workbenchResults.ts b/redisinsight/ui/src/constants/workbenchResults.ts index affce8b9e2..75aac0ea1b 100644 --- a/redisinsight/ui/src/constants/workbenchResults.ts +++ b/redisinsight/ui/src/constants/workbenchResults.ts @@ -1,3 +1,65 @@ +import { RedisDefaultModules } from 'uiSrc/slices/interfaces' + export const bulkReplyCommands = ['LOLWUT', 'INFO', 'CLIENT', 'CLUSTER', 'MEMORY', 'MONITOR', 'PSUBSCRIBE'] export const EMPTY_COMMAND = 'Encrypted data' + +export const MODULE_NOT_LOADED_CONTENT: { [key in RedisDefaultModules]?: any } = { + [RedisDefaultModules.TimeSeries]: { + text: ['RedisTimeSeries adds a Time Series data structure to Redis. ', 'With this capability you can:'], + improvements: [ + 'Add sample data', + 'Perform cross-time-series range and aggregation queries', + 'Define compaction rules for economical retention of historical data' + ], + link: 'https://redis.io/docs/stack/timeseries/' + }, + [RedisDefaultModules.Search]: { + text: ['RediSearch adds the capability to:'], + improvements: [ + 'Query', + 'Secondary index', + 'Full-text search' + ], + additionalText: ['These features enable multi-field queries, aggregation, exact phrase matching, numeric filtering, ', 'geo filtering and vector similarity semantic search on top of text queries.'], + link: 'https://redis.io/docs/stack/search/' + }, + [RedisDefaultModules.Graph]: { + text: ['RedisGraph adds a Property Graph data structure to Redis. ', 'With this capability you can:'], + improvements: [ + 'Create graphs', + 'Query property graphs using the Cypher query language with proprietary extensions' + ], + link: 'https://redis.io/docs/stack/graph/' + }, + [RedisDefaultModules.ReJSON]: { + text: ['RedisJSON adds the capability to:'], + improvements: [ + 'Store JSON documents', + 'Update JSON documents', + 'Retrieve JSON documents' + ], + additionalText: ['RedisJSON also works seamlessly with RediSearch to let you index and query JSON documents.'], + link: 'https://redis.io/docs/stack/json/' + }, + [RedisDefaultModules.Bloom]: { + text: ['RedisBloom adds a set of probabilistic data structures to Redis, including:'], + improvements: [ + 'Bloom filter', + 'Cuckoo filter', + 'Count-min sketch', + 'Top-K', + 'T-digest' + ], + additionalText: ['With this capability you can query streaming data without needing to store all the elements of the stream.'], + link: 'https://redis.io/docs/stack/bloom/' + } +} + +export const MODULE_TEXT_VIEW: { [key in RedisDefaultModules]?: string } = { + [RedisDefaultModules.Bloom]: 'RedisBloom', + [RedisDefaultModules.Graph]: 'RedisGraph', + [RedisDefaultModules.ReJSON]: 'RedisJSON', + [RedisDefaultModules.Search]: 'RediSearch', + [RedisDefaultModules.TimeSeries]: 'RedisTimeSeries', +} diff --git a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/styles.module.scss b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/styles.module.scss index 4bf7e53131..a7f15eefa6 100644 --- a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/styles.module.scss +++ b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/styles.module.scss @@ -36,6 +36,7 @@ backface-visibility: hidden; transition: transform 0.4s ease-in-out; box-shadow: -5px 1px 10px rgba(0, 0, 0, 0.2); + z-index: 2; :global { .euiButton--small { diff --git a/redisinsight/ui/src/pages/workbench/components/module-not-loaded/ModuleNotLoaded.spec.tsx b/redisinsight/ui/src/pages/workbench/components/module-not-loaded/ModuleNotLoaded.spec.tsx index 6bfaedf219..2d0e19e96a 100644 --- a/redisinsight/ui/src/pages/workbench/components/module-not-loaded/ModuleNotLoaded.spec.tsx +++ b/redisinsight/ui/src/pages/workbench/components/module-not-loaded/ModuleNotLoaded.spec.tsx @@ -1,72 +1,12 @@ -import { cloneDeep } from 'lodash' import React from 'react' -import { cleanup, mockedStore, render } from 'uiSrc/utils/test-utils' -import { ThemeContext, defaultState } from 'uiSrc/contexts/themeContext' -import ModuleNotLoaded from './ModuleNotLoaded' -import { RSNotLoadedContent } from '../../constants' +import { instance, mock } from 'ts-mockito' +import { render } from 'uiSrc/utils/test-utils' +import ModuleNotLoaded, { IProps } from './ModuleNotLoaded' -let store: typeof mockedStore - -beforeEach(() => { - cleanup() - store = cloneDeep(mockedStore) - store.clearActions() -}) - -jest.mock('uiSrc/services', () => ({ - ...jest.requireActual('uiSrc/services'), - sessionStorageService: { - set: jest.fn(), - get: jest.fn(), - }, -})) - -jest.mock('uiSrc/slices/content/create-redis-buttons', () => ({ - ...jest.requireActual('uiSrc/slices/content/create-redis-buttons'), - contentSelector: jest.fn().mockReturnValue({ - loading: false, - data: { - cloud: { title: 'Limited offer', description: 'Try Redis cloud' } - } - }), -})) +const mockedProps = mock() describe('ModuleNotLoaded', () => { it('should render', () => { - expect(render()).toBeTruthy() - }) - - it('"output" prop should render', () => { - const { queryByTestId } = render() - - const outputEl = queryByTestId('query-card-no-module-output') - expect(outputEl).toBeInTheDocument() - }) - it('"table" prop should render', () => { - const { container } = render() - - const tableEl = container.querySelector('[data-test-subj="query-card-no-module-table"]') - expect(tableEl).toBeInTheDocument() - }) - it('"createCloudBtn" prop should render', () => { - const { queryByTestId } = render( - - - - ) - const btnEl = queryByTestId('query-card-no-module-button') - expect(btnEl).toBeInTheDocument() - }) - it('"summaryText" prop should render', () => { - const { queryByTestId } = render() - - const summaryTextEl = queryByTestId('query-card-no-module-summary-text') - expect(summaryTextEl).toBeInTheDocument() - }) - it('"summaryImgPath" prop should render', () => { - const { queryByTestId } = render() - - const summaryImgEl = queryByTestId('query-card-no-module-summary-img') - expect(summaryImgEl).toBeInTheDocument() + expect(render()).toBeTruthy() }) }) diff --git a/redisinsight/ui/src/pages/workbench/components/module-not-loaded/ModuleNotLoaded.tsx b/redisinsight/ui/src/pages/workbench/components/module-not-loaded/ModuleNotLoaded.tsx index 75ec496cc8..b19cd639a6 100644 --- a/redisinsight/ui/src/pages/workbench/components/module-not-loaded/ModuleNotLoaded.tsx +++ b/redisinsight/ui/src/pages/workbench/components/module-not-loaded/ModuleNotLoaded.tsx @@ -1,146 +1,123 @@ -import React, { useContext } from 'react' -import { useSelector } from 'react-redux' +import React, { useEffect, useState } from 'react' import cx from 'classnames' import { - EuiBasicTableColumn, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiInMemoryTable, EuiTextColor, + EuiText, + EuiTitle, + EuiLink, + EuiButton, } from '@elastic/eui' -import parse from 'html-react-parser' -import { ThemeContext } from 'uiSrc/contexts/themeContext' -import { contentSelector } from 'uiSrc/slices/content/create-redis-buttons' -import { Theme } from 'uiSrc/constants' -import PromoLink from 'uiSrc/components/promo-link/PromoLink' -import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import { getPathToResource } from 'uiSrc/services/resourcesService' -import { ContentCreateRedis } from 'uiSrc/slices/interfaces/content' +import { ReactComponent as MobileIcon } from 'uiSrc/assets/img/icons/mobile_module_not_loaded.svg' +import { ReactComponent as DesktopIcon } from 'uiSrc/assets/img/icons/module_not_loaded.svg' +import { ReactComponent as CheerIcon } from 'uiSrc/assets/img/icons/cheer.svg' +import { MODULE_NOT_LOADED_CONTENT as CONTENT, MODULE_TEXT_VIEW } from 'uiSrc/constants' +import { RedisDefaultModules } from 'uiSrc/slices/interfaces' import styles from './styles.module.scss' -interface IContentColumn { - title: string - text: string +export interface IProps { + moduleName: RedisDefaultModules + id: string } -export interface IModuleNotLoadedContent { - output?: string - createCloudBtnText?: string - createCloudBtnHref?: string - summaryText?: string - summaryImgDark?: string - summaryImgLight?: string - summaryImgPath?: string - columns?: IContentColumn[] -} +const MIN_ELEMENT_WIDTH = 1210 +const MAX_ELEMENT_WIDTH = 1440 -export interface Props { - content: IModuleNotLoadedContent -} +const renderTitle = (width: number, moduleName?: string) => ( + +

+ {`Looks like ${moduleName} is not available `} + {width > MAX_ELEMENT_WIDTH &&
} + for this database +

+
+) -const ModuleNotLoaded = ({ content = {} }: Props) => { - const { - output = '', - summaryText = '', - summaryImgDark = '', - summaryImgLight = '', - summaryImgPath = '', - columns = [] - } = content - const { loading, data: createDbContent } = useSelector(contentSelector) +const renderText = (moduleName?: string) => ( + + {`Create a free Redis Stack database with ${moduleName} which extends the core capabilities of open-source Redis`} + +) - const { theme } = useContext(ThemeContext) +const ListItem = ({ item }: { item: string }) => ( +
  • +
    + +
    + {item} +
  • +) - const columnsSettings: EuiBasicTableColumn[] = columns.map(({ title }, i) => ({ - name: title, - field: `text${i}`, - dataType: 'string', - truncateText: false, - render: (text: any) => parse(text) - })) +const ModuleNotLoaded = ({ moduleName, id }: IProps) => { + const [width, setWidth] = useState(0) - const item = columns.reduce((obj, { text }, i) => ({ ...obj, [`text${i}`]: text }), {}) + const module = MODULE_TEXT_VIEW[moduleName] - const handleClickLink = (event: TelemetryEvent, eventData: any = {}) => { - sendEventTelemetry({ - event, - eventData: { - ...eventData - } - }) - } - const CreateCloudBtn = ({ content }: { content: ContentCreateRedis }) => { - const { title, description, styles, links } = content - // @ts-ignore - const linkStyles = styles ? styles[theme] : {} - return ( - handleClickLink( - links?.redisearch?.event as TelemetryEvent, - { source: 'RediSearch is not loaded' } - )} - styles={{ - ...linkStyles, - backgroundImage: linkStyles?.backgroundImage - ? `url(${getPathToResource(linkStyles.backgroundImage)})` - : undefined - }} - /> - ) - } + useEffect(() => { + const parentEl = document?.getElementById(id) + if (parentEl) { + setWidth(parentEl.offsetWidth) + } + }) return ( -
    - - {!!output && ( - - - - - - {parse(output)} - - - )} - {!!columns?.length && ( - - - - )} - { !loading && createDbContent?.cloud && ( - - - - )} - {(!!summaryText || !!summaryImgPath || !!summaryImgDark || !!summaryImgLight) && ( - -
    - {(!!summaryImgPath || !!summaryImgDark || !!summaryImgLight) && ( - redisearch table - )} - {!!summaryText &&
    {parse(summaryText)}
    } -
    -
    - )} -
    +
    MAX_ELEMENT_WIDTH })}> +
    +
    + {width > MAX_ELEMENT_WIDTH + ? + : } +
    +
    + {renderTitle(width, module)} + + {CONTENT[moduleName]?.text.map((item: string) => ( + width > MIN_ELEMENT_WIDTH ? <>{item}
    : item + ))} +
    +
      + {CONTENT[moduleName]?.improvements.map((item: string) => ( + + ))} +
    + {!!CONTENT[moduleName]?.additionalText && ( + + {CONTENT[moduleName]?.additionalText.map((item: string) => ( + width > MIN_ELEMENT_WIDTH ? <>{item}
    : item + ))} +
    + )} + {renderText(module)} +
    +
    +
    + + Learn More + + + + Get Started For Free + + +
    ) } diff --git a/redisinsight/ui/src/pages/workbench/components/module-not-loaded/styles.module.scss b/redisinsight/ui/src/pages/workbench/components/module-not-loaded/styles.module.scss index 82e4e247d3..8268533ff4 100644 --- a/redisinsight/ui/src/pages/workbench/components/module-not-loaded/styles.module.scss +++ b/redisinsight/ui/src/pages/workbench/components/module-not-loaded/styles.module.scss @@ -1,97 +1,144 @@ .container { - font-family: 'Graphik', sans-serif !important; + padding: 41px 30px; + + .title { + font-family: 'Graphik', sans-serif; + font-size: 32px; + font-weight: 600; + word-break: break-word; + margin-bottom: 20px; + } - br { - display: block !important; - content: ""; - margin: 2em; - font-size: 34%; + .linksWrapper .text, + .text { + font-family: 'Graphik', sans-serif; + font-size: 14px; + line-height: 17px; + word-break: break-word; + color: var(--wbTextColor) !important; + text-decoration: none !important; } - ul { - list-style: disc !important; - padding-left: 20px !important; + .bigText { + font-family: 'Graphik', sans-serif; + font-size: 20px; + line-height: 24px; + word-break: break-word; + color: var(--wbTextColor); + margin-bottom: 20px; } - :global(.query-card-output-response-fail) { - padding: 6px 0; - color: var(--euiColorDanger) !important + .flex { + display: flex; + flex-direction: row-reverse; + align-items: center; + justify-content: space-between; + + .icon { + width: 173px; + margin-left: 15px; + } + + .bigIcon { + width: 317px; + margin-bottom: 42px; + } } -} -.table { - margin: 0 -20px !important; + .list.bloomList { + display: flex; + flex-wrap: wrap; - :global(.euiTableRowCell) { - background-color: var(--euiPageBackgroundColor) !important; + .listItem { + margin-right: 18px; + } } - br { - font-size: 18%; + .listItem { + display: flex; + margin-bottom: 20px; + + .iconWrapper { + display: flex; + flex: 0 0 16px; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + background: var(--wbActiveIconColor); + border-radius: 50%; + margin-right: 10px; + } + + .listIcon { + width: 10px; + height: 10px; + + path { + fill: var(--euiPageBackgroundColor); + } + } } - - a { - line-height: 24px; - text-align: left; - text-decoration: underline; - color: var(--buttonSecondaryTextColor); - - &:hover { - text-decoration: none; + + .linksWrapper { + display: flex; + justify-content: flex-end; + align-items: center; + margin-top: 20px; + + .link { + margin-right: 20px; + color: var(-wbTextColor); } } -} + + .marginBottom { + margin-bottom: 20px; + } -.summary { - display: flex; + &.fullScreen { + padding: 89px 77px; - > div { - padding-bottom: 10px; - } -} + .flex { + flex-direction: column; + } -.alertIconWrapper { - height: 19px; - display: inline-flex; - align-items: center; -} + .contentWrapper { + display: flex; + flex-direction: column; + align-items: center; + } -.summaryImg { - width: 468px !important; - height: 174px !important; - margin-right: 22px; -} + .title, + .text, + .bigText { + text-align: center; + } -@media only screen and (max-width: 700px) { - .summaryImg { - width: 312px !important; - height: 116px !important; - } -} + .list { + display: flex; + justify-content: center; + } -@media only screen and (max-width: 1200px) { - .summary { - display: block; - } -} + .listItem { + margin-right: 15px; -@media only screen and (max-width: 767px) { - .container :global(.euiFlexGroup--responsive .query-card-output-response-fail) { - margin: 10px 0 !important; - } + &:last-child { + margin-right: 0; + } + } - .table { - :global(.euiTableRowCell) { - background-color: initial !important; - max-width: 100% !important; - width: 100%; - border-top: 1px solid var(--euiColorLightShade) !important; + .linksWrapper { + justify-content: center; + flex-direction: column-reverse; } - :global(.euiTableRow) { - padding: 0 !important; - border-right: 0 !important; - background-color: var(--euiPageBackgroundColor) !important; + .link { + margin-right: 0; + + &:first-child { + margin-top: 13px; + } } } } diff --git a/redisinsight/ui/src/pages/workbench/constants.ts b/redisinsight/ui/src/pages/workbench/constants.ts index 673c0bce3d..0df68f333b 100644 --- a/redisinsight/ui/src/pages/workbench/constants.ts +++ b/redisinsight/ui/src/pages/workbench/constants.ts @@ -1,8 +1,5 @@ -import RSNotAvailableLightImg from 'uiSrc/assets/img/workbench/RediSearchNotAvailableLight.jpg' -import RSNotAvailableDarkImg from 'uiSrc/assets/img/workbench/RediSearchNotAvailableDark.jpg' import TextViewIconDark from 'uiSrc/assets/img/workbench/text_view_dark.svg' import TextViewIconLight from 'uiSrc/assets/img/workbench/text_view_light.svg' -import { IModuleNotLoadedContent } from './components/module-not-loaded' export const WORKBENCH_HISTORY_WRAPPER_NAME = 'WORKBENCH' export const WORKBENCH_HISTORY_MAX_LENGTH = 30 @@ -57,30 +54,14 @@ const PROFILE_VIEW_TYPE_OPTIONS = [ export const getProfileViewTypeOptions = () => [...PROFILE_VIEW_TYPE_OPTIONS] - export enum ModuleCommandPrefix { RediSearch = 'FT.', -} - -export const RSNotLoadedContent: IModuleNotLoadedContent = { - output: 'RediSearch module is not loaded for this database', - createCloudBtnText: 'Create your free Redis database with RediSearch on Redis Cloud​', - createCloudBtnHref: 'https://redis.com/try-free/?utm_source=redis&utm_medium=app&utm_campaign=redisinsight_redisearch_offer_jan', - summaryText: 'RedisInsight supports RediSearch and allows you to:

    ' - + '
    • Build and execute queries
    • Browse, analyse and export results

    ' - + 'As a benefit you get faster turnarounds when building your application using Redis and RediSearch.', - summaryImgDark: RSNotAvailableDarkImg, - summaryImgLight: RSNotAvailableLightImg, - // summaryImgPath: 'uiSrc/assets/img/workbench/RediSearchNotAvailable.jpg', - columns: [{ - title: 'What is RediSearch?', - text: 'RediSearch is a real-time search engine that enables you to query your Redis data. It implements a secondary index on top of Redis.
    This enables advanced features, such as multi-field queries, aggregation, auto-completion and full text search capabilities.
    These capabilities include exact phrase matching, fuzzy and prefix matching, numeric filtering, geo-spatial filtering, something that is neither possible or efficient with traditional Redis indexing approaches.', - }, { - title: 'Learn more about RediSearch module:', - text: 'RediSearch Quick Start tutorial' - + 'RediSearch Documentation' - + 'RediSearch Commands' - + 'RediSearch in Brief' - + 'RediSearch Benchmarks (blog post)' - }] + JSON = 'JSON.', + TimeSeries = 'TS.', + Graph = 'GRAPH.', + BF = 'BF.', + CF = 'CF.', + CMS = 'CMS.', + TOPK = 'TOPK.', + TDIGEST = 'TDIGEST.', } diff --git a/redisinsight/ui/src/slices/interfaces/instances.ts b/redisinsight/ui/src/slices/interfaces/instances.ts index e9ff40ed8d..6c052b6548 100644 --- a/redisinsight/ui/src/slices/interfaces/instances.ts +++ b/redisinsight/ui/src/slices/interfaces/instances.ts @@ -175,6 +175,14 @@ export const REDISEARCH_MODULES: string[] = [ RedisDefaultModules.FTL, ] +export const COMMAND_MODULES = { + [RedisDefaultModules.Search]: REDISEARCH_MODULES, + [RedisDefaultModules.ReJSON]: [RedisDefaultModules.ReJSON], + [RedisDefaultModules.TimeSeries]: [RedisDefaultModules.TimeSeries], + [RedisDefaultModules.Graph]: [RedisDefaultModules.Graph], + [RedisDefaultModules.Bloom]: [RedisDefaultModules.Bloom], +} + const RediSearchModulesText = [...REDISEARCH_MODULES].reduce((prev, next) => ({ ...prev, [next]: 'RediSearch' }), {}) // Enums don't allow to use dynamic key 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 2819808f43..1f1ffb713c 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 @@ -179,6 +179,7 @@ --wbRunResultsBg: #{$wbRunResultsBg}; --wbHoverIconColor: #{$wbHoverIconColor}; --wbActiveIconColor: #{$wbActiveIconColor}; + --wbTextColor: #{$wbTextColor}; // Pub/Sub --pubSubClientsBadge: #{$pubSubClientsBadge}; 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 e42473ee00..f596371d27 100644 --- a/redisinsight/ui/src/styles/themes/dark_theme/_theme_color.scss +++ b/redisinsight/ui/src/styles/themes/dark_theme/_theme_color.scss @@ -139,6 +139,7 @@ $rsSubmitBtn: #1ae26e; $wbRunResultsBg: #000; $wbHoverIconColor: #ffffff; $wbActiveIconColor: #8ba2ff; +$wbTextColor: #dfe5ef; // PubSub $pubSubClientsBadge: #008000; 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 ccc938f991..5fdef0de41 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 @@ -181,6 +181,7 @@ --wbRunResultsBg: #{$wbRunResultsBg}; --wbHoverIconColor: #{$wbHoverIconColor}; --wbActiveIconColor: #{$wbActiveIconColor}; + --wbTextColor: #{$wbTextColor}; // Pub/Sub --pubSubClientsBadge: #{$pubSubClientsBadge}; 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 20c09ec571..670e364b21 100644 --- a/redisinsight/ui/src/styles/themes/light_theme/_theme_color.scss +++ b/redisinsight/ui/src/styles/themes/light_theme/_theme_color.scss @@ -136,6 +136,7 @@ $rsInputWrapperColor: #fff; $wbRunResultsBg: #fff; $wbHoverIconColor: #415681; $wbActiveIconColor: #3163D8; +$wbTextColor: #415681; // Pub/Sub $pubSubClientsBadge: #b5cea8; diff --git a/redisinsight/ui/src/utils/cliHelper.tsx b/redisinsight/ui/src/utils/cliHelper.tsx index 2f24de7954..b65f8ea2d5 100644 --- a/redisinsight/ui/src/utils/cliHelper.tsx +++ b/redisinsight/ui/src/utils/cliHelper.tsx @@ -1,4 +1,4 @@ -import React, { Fragment } from 'react' +import React from 'react' import { Dispatch, PayloadAction } from '@reduxjs/toolkit' import parse from 'html-react-parser' @@ -8,10 +8,13 @@ import { resetOutput, updateCliCommandHistory } from 'uiSrc/slices/cli/cli-outpu import { BrowserStorageItem, ICommands } from 'uiSrc/constants' import { ModuleCommandPrefix } from 'uiSrc/pages/workbench/constants' import { SelectCommand } from 'uiSrc/constants/cliOutput' -import { ClusterNode, RedisDefaultModules, REDISEARCH_MODULES } from 'uiSrc/slices/interfaces' +import { + ClusterNode, + RedisDefaultModules, + COMMAND_MODULES, +} from 'uiSrc/slices/interfaces' import { AdditionalRedisModule } from 'apiSrc/modules/database/models/additional.redis.module' -import { Nullable } from './types' import formatToText from './transformers/cliTextFormatter' import { getDbIndex } from './longNames' @@ -139,18 +142,42 @@ const checkUnsupportedCommand = (unsupportedCommands: string[], commandLine: str const checkBlockingCommand = (blockingCommands: string[], commandLine: string) => blockingCommands?.find((command) => commandLine?.trim().toLowerCase().startsWith(command)) +const checkCommandModule = (command: string) => { + switch (true) { + case command.startsWith(ModuleCommandPrefix.RediSearch): { + return RedisDefaultModules.Search + } + case command.startsWith(ModuleCommandPrefix.JSON): { + return RedisDefaultModules.ReJSON + } + case command.startsWith(ModuleCommandPrefix.TimeSeries): { + return RedisDefaultModules.TimeSeries + } + case command.startsWith(ModuleCommandPrefix.Graph): { + return RedisDefaultModules.Graph + } + case command.startsWith(ModuleCommandPrefix.BF): + case command.startsWith(ModuleCommandPrefix.CF): + case command.startsWith(ModuleCommandPrefix.CMS): + case command.startsWith(ModuleCommandPrefix.TDIGEST): + case command.startsWith(ModuleCommandPrefix.TOPK): { + return RedisDefaultModules.Bloom + } + default: { + return null + } + } +} + const checkUnsupportedModuleCommand = (loadedModules: AdditionalRedisModule[], commandLine: string) => { const command = commandLine?.trim().toUpperCase() - let commandModule: Nullable = null - if (command.startsWith(ModuleCommandPrefix.RediSearch)) { - commandModule = RedisDefaultModules.Search + const commandModule = checkCommandModule(command) + if (!commandModule) { + return null } - - const isModuleLoaded = loadedModules?.some(({ name }) => name === commandModule) - // Redisearch has 4 names, need check all - || loadedModules?.some(({ name }) => - REDISEARCH_MODULES.some((search) => name === search)) + const isModuleLoaded = loadedModules?.some(({ name }) => + COMMAND_MODULES[commandModule].some((module) => name === module)) if (isModuleLoaded) { return null @@ -195,6 +222,7 @@ export { cliCommandWrapper, clearOutput, updateCliHistoryStorage, + checkCommandModule, checkUnsupportedCommand, checkBlockingCommand, checkUnsupportedModuleCommand, diff --git a/redisinsight/ui/src/utils/tests/cliHelper.spec.ts b/redisinsight/ui/src/utils/tests/cliHelper.spec.ts index 515fe6a891..961c7f185e 100644 --- a/redisinsight/ui/src/utils/tests/cliHelper.spec.ts +++ b/redisinsight/ui/src/utils/tests/cliHelper.spec.ts @@ -3,11 +3,16 @@ import { getCommandNameFromQuery, cliParseCommandsGroupResult, CliPrefix, - wbSummaryCommand + wbSummaryCommand, + checkUnsupportedModuleCommand, + checkCommandModule, + checkUnsupportedCommand, + checkBlockingCommand, } from 'uiSrc/utils' import { MOCK_COMMANDS_SPEC } from 'uiSrc/constants' import { render, screen } from 'uiSrc/utils/test-utils' import { CommandExecutionStatus } from 'uiSrc/slices/interfaces/cli' +import { RedisDefaultModules } from 'uiSrc/slices/interfaces' const getDbIndexFromSelectQueryTests = [ { input: 'select 0', expected: 0 }, @@ -21,6 +26,7 @@ const getDbIndexFromSelectQueryTests = [ { input: 'info', expected: new Error('Invalid command') }, { input: 'select "1 1231"', expected: new Error('Parsing error') }, { input: 'select abc', expected: new Error('Parsing error') }, + { input: 'select ', expected: new Error('Parsing error') }, ] describe('getDbIndexFromSelectQuery', () => { @@ -49,6 +55,41 @@ const getCommandNameFromQueryTests = [ input: [`${' '.repeat(20)} CLIENT ${' '.repeat(100)} KILL`, MOCK_COMMANDS_SPEC, 500], expected: 'CLIENT KILL' }, + { input: [1], expected: undefined }, +] + +const checkUnsupportedModuleCommandTests = [ + { input: [[], 'FT.foo bar'], expected: RedisDefaultModules.Search }, + { input: [[{ name: RedisDefaultModules.Search }], 'foo bar'], expected: null }, + { input: [[{ name: RedisDefaultModules.Search }], 'ft.foo bar'], expected: null }, + { input: [[{ name: RedisDefaultModules.SearchLight }], 'ft.foo bar'], expected: null }, + { input: [[{ name: RedisDefaultModules.FT }], ' FT.foo bar'], expected: null }, + { input: [[{ name: RedisDefaultModules.FTL }], ' ft.foo bar'], expected: null }, +] + +const checkCommandModuleTests = [ + { input: 'FT.foo bar', expected: RedisDefaultModules.Search }, + { input: 'JSON.foo bar', expected: RedisDefaultModules.ReJSON }, + { input: 'TS.foo bar', expected: RedisDefaultModules.TimeSeries }, + { input: 'GRAPH.foo bar', expected: RedisDefaultModules.Graph }, + { input: 'BF.foo bar', expected: RedisDefaultModules.Bloom }, + { input: 'CF.foo bar', expected: RedisDefaultModules.Bloom }, + { input: 'CMS.foo bar', expected: RedisDefaultModules.Bloom }, + { input: 'TDIGEST.foo bar', expected: RedisDefaultModules.Bloom }, + { input: 'TOPK.foo bar', expected: RedisDefaultModules.Bloom }, + { input: 'FOO.foo bar', expected: null }, +] + +const checkUnsupportedCommandTests = [ + { input: [['FT'], 'FT.foo bar'], expected: 'FT' }, + { input: [['FT'], ' ft.foo bar '], expected: 'FT' }, + { input: [['FOO', 'BAR'], 'FT.foo bar'], expected: undefined }, +] + +const checkBlockingCommandTests = [ + { input: [['ft'], 'FT.foo bar'], expected: 'ft' }, + { input: [['ft'], ' ft.foo bar '], expected: 'ft' }, + { input: [['foo', 'bar'], 'FT.foo bar'], expected: undefined }, ] describe('getCommandNameFromQuery', () => { @@ -99,3 +140,30 @@ describe('wbSummaryCommand', () => { expect(container).toHaveTextContent(expected) }) }) + +describe('checkUnsupportedModuleCommand', () => { + test.each(checkUnsupportedModuleCommandTests)('%j', ({ input, expected }) => { + // @ts-ignore + expect(checkUnsupportedModuleCommand(...input)).toEqual(expected) + }) +}) + +describe('checkCommandModule', () => { + test.each(checkCommandModuleTests)('%j', ({ input, expected }) => { + expect(checkCommandModule(input)).toEqual(expected) + }) +}) + +describe('checkUnsupportedCommand', () => { + test.each(checkUnsupportedCommandTests)('%j', ({ input, expected }) => { + // @ts-ignore + expect(checkUnsupportedCommand(...input)).toEqual(expected) + }) +}) + +describe('checkBlockingCommand', () => { + test.each(checkBlockingCommandTests)('%j', ({ input, expected }) => { + // @ts-ignore + expect(checkBlockingCommand(...input)).toEqual(expected) + }) +}) diff --git a/tests/e2e/pageObjects/workbench-page.ts b/tests/e2e/pageObjects/workbench-page.ts index bfc78ef5fe..f0e5675b10 100644 --- a/tests/e2e/pageObjects/workbench-page.ts +++ b/tests/e2e/pageObjects/workbench-page.ts @@ -29,6 +29,7 @@ export class WorkbenchPage { //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.). //------------------------------------------------------------------------------------------- //BUTTONS + welcomePageTitle = Selector('[data-testid=welcome-page-title]'); customTutorials = Selector('[data-testid=accordion-button-custom-tutorials]'); tutorialOpenUploadButton = Selector('[data-testid=open-upload-tutorial-btn]'); tutorialLinkField = Selector('[data-testid=tutorial-link-field]'); @@ -190,6 +191,7 @@ export class WorkbenchPage { */ async sendCommandInWorkbench(command: string, speed = 1, paste = true): Promise { await t + .click(this.queryInput) .typeText(this.queryInput, command, { replace: true, speed, paste }) .click(this.submitCommandButton); } diff --git a/tests/e2e/test-data/upload-tutorials/sample.zip b/tests/e2e/test-data/upload-tutorials/sample.zip deleted file mode 100644 index 4ec02b360b..0000000000 Binary files a/tests/e2e/test-data/upload-tutorials/sample.zip and /dev/null differ diff --git a/tests/e2e/test-data/upload-tutorials/testTutorials.zip b/tests/e2e/test-data/upload-tutorials/testTutorials.zip new file mode 100644 index 0000000000..60192fedb8 Binary files /dev/null and b/tests/e2e/test-data/upload-tutorials/testTutorials.zip differ diff --git a/tests/e2e/tests/critical-path/browser/bulk-delete.e2e.ts b/tests/e2e/tests/critical-path/browser/bulk-delete.e2e.ts index 59ad57e6f1..922d7c5761 100644 --- a/tests/e2e/tests/critical-path/browser/bulk-delete.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/bulk-delete.e2e.ts @@ -4,7 +4,6 @@ import { BrowserPage, BulkActionsPage, MyRedisDatabasePage } from '../../../page import { commonUrl, ossStandaloneRedisearch } from '../../../helpers/conf'; import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; -import { addHashKeyApi, addSetKeyApi } from '../../../helpers/api/api-keys'; import { deleteAllKeysFromDB, populateDBWithHashes } from '../../../helpers/keys'; const browserPage = new BrowserPage(); @@ -13,8 +12,6 @@ const common = new Common(); const myRedisDatabasePage = new MyRedisDatabasePage(); const keyNames = [common.generateWord(20), common.generateWord(20)]; -const hashKeyParameters = { keyName: keyNames[0], fields: [{ field: common.generateWord(20), value: common.generateWord(20) }] }; -const setKeyParameters = { keyName: keyNames[1], members: [common.generateWord(20)] }; const dbParameters = { host: ossStandaloneRedisearch.host, port: ossStandaloneRedisearch.port }; const keyToAddParameters = { keysCount: 10000, keyNameStartWith: 'hashKey'}; const keyToAddParameters2 = { keysCount: 500000, keyNameStartWith: 'hashKey'}; @@ -24,8 +21,8 @@ fixture `Bulk Delete` .page(commonUrl) .beforeEach(async() => { await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch, ossStandaloneRedisearch.databaseName); - await addHashKeyApi(hashKeyParameters, ossStandaloneRedisearch); - await addSetKeyApi(setKeyParameters, ossStandaloneRedisearch); + await browserPage.addHashKey(keyNames[0], '100000', common.generateWord(20), common.generateWord(20)); + await browserPage.addSetKey(keyNames[1], '100000', common.generateWord(20)); }) .afterEach(async() => { // Clear and delete database @@ -134,7 +131,7 @@ test('Verify that when bulk deletion is completed, status Action completed is di test .before(async() => { await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch, ossStandaloneRedisearch.databaseName); - await addSetKeyApi(setKeyParameters, ossStandaloneRedisearch); + await browserPage.addSetKey(keyNames[1], '100000', common.generateWord(20)); // Add 10000 Hash keys await populateDBWithHashes(dbParameters.host, dbParameters.port, keyToAddParameters); // Filter by Hash keys diff --git a/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts b/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts index 46b5113ce7..e22e28021b 100644 --- a/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts @@ -264,6 +264,7 @@ test await myRedisDatabasePage.clickOnDBByName(simpleDbName); // click standalone database await cliPage.sendCommandInCli(`FT.DROPINDEX ${indexNameSimpleDb}`); await t.click(browserPage.patternModeBtn); + await t.click(browserPage.browserViewButton); await browserPage.deleteKeysByNames(keyNames); //delete database diff --git a/tests/e2e/tests/critical-path/workbench/default-scripts-area.e2e.ts b/tests/e2e/tests/critical-path/workbench/default-scripts-area.e2e.ts index 60b230fe12..d2269a3041 100644 --- a/tests/e2e/tests/critical-path/workbench/default-scripts-area.e2e.ts +++ b/tests/e2e/tests/critical-path/workbench/default-scripts-area.e2e.ts @@ -114,7 +114,8 @@ test('Verify that user can edit and run automatically added "Aggregate" script i await t.expect(workbenchPage.queryTableResult.textContent).contains(aggregationResultField, 'The aggregation field name is not in the Search result'); await t.expect(workbenchPage.queryTableResult.textContent).contains('100', 'The aggregation max value is in not the Search result'); }); -test('Verify that when the “Manual” option clicked, user can see the Editor is automatically prepopulated with the information', async t => { +// Outdated after https://redislabs.atlassian.net/browse/RI-4279 +test.skip('Verify that when the “Manual” option clicked, user can see the Editor is automatically prepopulated with the information', async t => { const information = [ '// Workbench is the advanced Redis command-line interface that allows to send commands to Redis, read and visualize the replies sent by the server.', '// Enter multiple commands at different rows to run them at once.', diff --git a/tests/e2e/tests/critical-path/workbench/scripting-area.e2e.ts b/tests/e2e/tests/critical-path/workbench/scripting-area.e2e.ts index a8a71829c5..88fa4e0333 100644 --- a/tests/e2e/tests/critical-path/workbench/scripting-area.e2e.ts +++ b/tests/e2e/tests/critical-path/workbench/scripting-area.e2e.ts @@ -30,7 +30,7 @@ fixture `Scripting area at Workbench` // Update after resolving https://redislabs.atlassian.net/browse/RI-3299 test('Verify that user can resize scripting area in Workbench', async t => { const commandForSend = 'info'; - const offsetY = 100; + const offsetY = 130; await workbenchPage.sendCommandInWorkbench(commandForSend); // Verify that user can run any script from CLI in Workbench and see the results @@ -41,9 +41,9 @@ test('Verify that user can resize scripting area in Workbench', async t => { const inputHeightStart = await workbenchPage.queryInput.clientHeight; await t.hover(workbenchPage.resizeButtonForScriptingAndResults); - await t.drag(workbenchPage.resizeButtonForScriptingAndResults, 0, offsetY, { speed: 0.4 }); + await t.drag(workbenchPage.resizeButtonForScriptingAndResults, 0, offsetY, { speed: 0.1 }); // Verify that user can resize scripting area - const inputHeightEnd = inputHeightStart + 20; + const inputHeightEnd = inputHeightStart + 15; await t.expect(await workbenchPage.queryInput.clientHeight).gt(inputHeightEnd, 'Scripting area after resize has incorrect size'); }); test('Verify that user when he have more than 10 results can request to view more results in Workbench', async t => { diff --git a/tests/e2e/tests/regression/workbench/command-results.e2e.ts b/tests/e2e/tests/regression/workbench/command-results.e2e.ts index ae807aeb0d..a68bba1d54 100644 --- a/tests/e2e/tests/regression/workbench/command-results.e2e.ts +++ b/tests/e2e/tests/regression/workbench/command-results.e2e.ts @@ -2,7 +2,7 @@ import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; import { WorkbenchPage, MyRedisDatabasePage } from '../../../pageObjects'; import { commonUrl, - ossStandaloneRedisearch + ossStandaloneRedisearch, ossStandaloneV5Config } from '../../../helpers/conf'; import { env, rte } from '../../../helpers/constants'; import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; @@ -134,3 +134,21 @@ test.before(async t => { // verify table view row count match with text view after client list command await workBenchActions.verifyClientListTableViewRowCount(); }); +// https://redislabs.atlassian.net/browse/RI-4230 +test.before(async t => { + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneV5Config, ossStandaloneV5Config.databaseName); + await t.click(myRedisDatabasePage.workbenchButton); +}).after(async() => { + await deleteStandaloneDatabaseApi(ossStandaloneV5Config); +})('Verify that user can see options on what can be done to work with capabilities in Workbench for docker', async t => { + const commandJSON = 'JSON.ARRAPPEND key value'; + const commandFT = 'FT.LIST'; + await workbenchPage.sendCommandInWorkbench(commandJSON); + // Verify change screens when capability not available - 'Search' + await t.expect(workbenchPage.welcomePageTitle.withText('Looks like RedisJSON is not available').visible) + .ok('Missing RedisJSON title is not visible'); + await workbenchPage.sendCommandInWorkbench(commandFT); + // Verify change screens when capability not available - 'JSON' + await t.expect(workbenchPage.welcomePageTitle.withText('Looks like RediSearch is not available').visible) + .ok('Missing RedisJSON title is not visible'); +}); diff --git a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts index 88a7ce8840..89a171c057 100644 --- a/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts +++ b/tests/e2e/tests/regression/workbench/import-tutorials.e2e.ts @@ -6,14 +6,13 @@ import { import { MyRedisDatabasePage, WorkbenchPage } from '../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; -import {Common} from '../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); -const common = new Common(); -const filePath = path.join('..', '..', '..', 'test-data', 'upload-tutorials', 'sample.zip'); -const tutorialName = `tutorialName-${common.generateWord(10)}`; -const link = 'https://drive.google.com/uc?export=download&id=1mlyDKWLu12L02FblOPh15EwG2Vy_FhJ7'; +const filePath = path.join('..', '..', '..', 'test-data', 'upload-tutorials', 'testTutorials.zip'); +const tutorialName = 'testTutorials'; +const tutorialName2 = 'tutorialTestByLink'; +const link = 'https://drive.google.com/uc?id=1puRUoT8HmyZCekkeWNxBzXe_48TzXcJc&export=download'; let folder1 = 'folder-1'; let folder2 = 'folder-2'; let internalLinkName1 = 'probably-1'; @@ -29,7 +28,7 @@ fixture `Upload custom tutorials` .afterEach(async() => { await deleteStandaloneDatabaseApi(ossStandaloneConfig); }); -// https://redislabs.atlassian.net/browse/RI-4186, https://redislabs.atlassian.net/browse/RI-4198 +// https://redislabs.atlassian.net/browse/RI-4186, https://redislabs.atlassian.net/browse/RI-4198, https://redislabs.atlassian.net/browse/RI-4302 test('Verify that user can upload tutorial with local zip file without manifest.json', async t => { // Verify that user can upload custom tutorials on docker version folder1 = 'folder-1'; @@ -39,8 +38,6 @@ test('Verify that user can upload tutorial with local zip file without manifest. // Verify that user can see the “MY TUTORIALS” section in the Enablement area. await t.expect(workbenchPage.customTutorials.visible).ok('custom tutorials sections is not visible'); await t.click(workbenchPage.tutorialOpenUploadButton); - // Verify that User can enter a tutorial name - await t.typeText(workbenchPage.tutorialNameField, tutorialName); await t.expect(workbenchPage.tutorialSubmitButton.hasAttribute('disabled')).ok('submit button is not disabled'); // Verify that User can request to add a new custom Tutorial by uploading a .zip archive from a local folder await t.setFilesToUpload(workbenchPage.tutorialImport, [filePath]); @@ -50,10 +47,12 @@ test('Verify that user can upload tutorial with local zip file without manifest. await t.click(workbenchPage.tutorialAccordionButton.withText(tutorialName)); await t.expect((await workbenchPage.getAccordionButtonWithName(folder1)).visible).ok(`${folder1} is not visible`); await t.expect((await workbenchPage.getAccordionButtonWithName(folder2)).visible).ok(`${folder2} is not visible`); + await t.click(await workbenchPage.getAccordionButtonWithName(folder1)); await t.expect((await workbenchPage.getInternalLinkWithManifest(internalLinkName1)).visible) .ok(`${internalLinkName1} is not visible`); + await t.click(await workbenchPage.getAccordionButtonWithName(folder2)); await t.expect((await workbenchPage.getInternalLinkWithManifest(internalLinkName2)).visible) - .ok(`${internalLinkName2} is not visible`); + .ok(`${internalLinkName1} is not visible`); await t.expect(workbenchPage.scrolledEnablementArea.exists).notOk('enablement area is visible before clicked'); await t.click((await workbenchPage.getInternalLinkWithManifest(internalLinkName1))); await t.expect(workbenchPage.scrolledEnablementArea.visible).ok('enablement area is not visible after clicked'); @@ -66,21 +65,23 @@ test('Verify that user can upload tutorial with local zip file without manifest. await t.expect((workbenchPage.tutorialAccordionButton.withText(tutorialName).exists)) .notOk(`${tutorialName} tutorial is not uploaded`); }); -// https://redislabs.atlassian.net/browse/RI-4186, https://redislabs.atlassian.net/browse/RI-4213 +// https://redislabs.atlassian.net/browse/RI-4186, https://redislabs.atlassian.net/browse/RI-4213, https://redislabs.atlassian.net/browse/RI-4302 test('Verify that user can upload tutorial with URL with manifest.json', async t => { - internalLinkName1 = 'working_probably'; + const labelFromManifest = 'LabelFromManifest'; + internalLinkName1 = 'manifest-id'; await t.click(workbenchPage.tutorialOpenUploadButton); - await t.typeText(workbenchPage.tutorialNameField, tutorialName); // Verify that user can upload tutorials using a URL await t.typeText(workbenchPage.tutorialLinkField, link); await t.click(workbenchPage.tutorialSubmitButton); - await t.expect(workbenchPage.tutorialAccordionButton.withText(tutorialName).with({ timeout: 20000 }).visible) - .ok(`${tutorialName} tutorial is not uploaded`); - await t.click(workbenchPage.tutorialAccordionButton.withText(tutorialName)); + await t.expect(workbenchPage.tutorialAccordionButton.withText(tutorialName2).with({ timeout: 20000 }).visible) + .ok(`${tutorialName2} tutorial is not uploaded`); + await t.click(workbenchPage.tutorialAccordionButton.withText(tutorialName2)); // Verify that User can see the same structure in the tutorial uploaded as described in the .json manifest await t.expect((await workbenchPage.getInternalLinkWithoutManifest(internalLinkName1)).visible) - .ok(`${internalLinkName1} folder is not visible`); + .ok(`${internalLinkName1} folder specified in manifest is not visible`); + await t.expect(await (await workbenchPage.getInternalLinkWithoutManifest(internalLinkName1)).textContent) + .eql(labelFromManifest, `${labelFromManifest} tutorial specified in manifest is not visible`); await t.click((await workbenchPage.getInternalLinkWithoutManifest(internalLinkName1))); await t.expect(workbenchPage.scrolledEnablementArea.visible).ok('enablement area is not visible after clicked'); await t.click(workbenchPage.closeEnablementPage); @@ -89,6 +90,6 @@ test('Verify that user can upload tutorial with URL with manifest.json', async t await t.click(workbenchPage.tutorialDeleteButton); await t.expect(workbenchPage.tutorialDeleteButton.exists).notOk('Delete popup is still visible'); // Verify that when User delete the tutorial, then User can see this tutorial and relevant markdown files are deleted from: the Enablement area in Workbench - await t.expect((workbenchPage.tutorialAccordionButton.withText(tutorialName).exists)) - .notOk(`${tutorialName} tutorial is not uploaded`); + await t.expect((workbenchPage.tutorialAccordionButton.withText(tutorialName2).exists)) + .notOk(`${tutorialName2} tutorial is not uploaded`); });