From 7693923a363baaf8ac5449127a37117b2326f22b Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Mon, 24 Jun 2024 14:48:30 +0200 Subject: [PATCH 001/112] #RI-5842 - add tabs to browser --- .../main-router/constants/defaultRoutes.ts | 21 +++-- .../main-router/constants/redisStackRoutes.ts | 41 ++++++--- .../constants/sub-routes/browserRoutes.ts | 22 +++++ .../main-router/constants/sub-routes/index.ts | 2 + .../navigation-menu/NavigationMenu.spec.tsx | 29 ++++--- .../navigation-menu/NavigationMenu.tsx | 29 ++----- .../insights-trigger/InsightsTrigger.spec.tsx | 2 +- redisinsight/ui/src/constants/pages.ts | 11 ++- .../ui/src/pages/browser/BrowserPage.spec.tsx | 2 - .../ui/src/pages/browser/BrowserPage.tsx | 2 - .../ui/src/pages/browser/styles.module.scss | 19 +---- .../top-namespace/TopNamespace.spec.tsx | 2 +- .../database-alias/DatabaseAlias.tsx | 11 +-- .../DatabasesListWrapper.tsx | 10 +-- .../ui/src/pages/keys/KeysPage.spec.tsx | 85 +++++++++++++++++++ redisinsight/ui/src/pages/keys/KeysPage.tsx | 62 ++++++++++++++ .../browser-tabs/BrowserTabs.spec.tsx | 40 +++++++++ .../components/browser-tabs/BrowserTabs.tsx | 67 +++++++++++++++ .../keys/components/browser-tabs/index.ts | 3 + .../browser-tabs/styles.module.scss | 44 ++++++++++ redisinsight/ui/src/pages/keys/index.ts | 3 + .../ui/src/pages/search/SearchPage.spec.tsx | 10 +++ .../ui/src/pages/search/SearchPage.tsx | 14 +++ redisinsight/ui/src/pages/search/index.ts | 3 + .../ui/src/pages/workbench/WorkbenchPage.tsx | 6 -- .../components/wb-view/WBViewWrapper.tsx | 2 +- redisinsight/ui/src/slices/app/context.ts | 4 +- redisinsight/ui/src/slices/interfaces/app.ts | 2 +- .../ui/src/slices/tests/app/context.spec.ts | 2 +- .../ui/src/utils/routerWithSubRoutes.tsx | 20 +++-- redisinsight/ui/src/utils/routing.ts | 7 +- .../ui/src/utils/tests/routing.spec.ts | 6 +- 32 files changed, 469 insertions(+), 114 deletions(-) create mode 100644 redisinsight/ui/src/components/main-router/constants/sub-routes/browserRoutes.ts create mode 100644 redisinsight/ui/src/pages/keys/KeysPage.spec.tsx create mode 100644 redisinsight/ui/src/pages/keys/KeysPage.tsx create mode 100644 redisinsight/ui/src/pages/keys/components/browser-tabs/BrowserTabs.spec.tsx create mode 100644 redisinsight/ui/src/pages/keys/components/browser-tabs/BrowserTabs.tsx create mode 100644 redisinsight/ui/src/pages/keys/components/browser-tabs/index.ts create mode 100644 redisinsight/ui/src/pages/keys/components/browser-tabs/styles.module.scss create mode 100644 redisinsight/ui/src/pages/keys/index.ts create mode 100644 redisinsight/ui/src/pages/search/SearchPage.spec.tsx create mode 100644 redisinsight/ui/src/pages/search/SearchPage.tsx create mode 100644 redisinsight/ui/src/pages/search/index.ts diff --git a/redisinsight/ui/src/components/main-router/constants/defaultRoutes.ts b/redisinsight/ui/src/components/main-router/constants/defaultRoutes.ts index 030df4cf24..3e42509952 100644 --- a/redisinsight/ui/src/components/main-router/constants/defaultRoutes.ts +++ b/redisinsight/ui/src/components/main-router/constants/defaultRoutes.ts @@ -1,6 +1,5 @@ import { IRoute, FeatureFlags, PageNames, Pages } from 'uiSrc/constants' import { - BrowserPage, HomePage, InstancePage, RedisCloudDatabasesPage, @@ -9,27 +8,23 @@ import { RedisCloudSubscriptionsPage, RedisClusterDatabasesPage, } from 'uiSrc/pages' -import WorkbenchPage from 'uiSrc/pages/workbench' +import KeysPage from 'uiSrc/pages/keys' import PubSubPage from 'uiSrc/pages/pub-sub' import AnalyticsPage from 'uiSrc/pages/analytics' import RdiPage from 'uiSrc/pages/rdi/home' import RdiInstancePage from 'uiSrc/pages/rdi/instance' import RdiStatisticsPage from 'uiSrc/pages/rdi/statistics' import PipelineManagementPage from 'uiSrc/pages/rdi/pipeline-management' -import { ANALYTICS_ROUTES, RDI_PIPELINE_MANAGEMENT_ROUTES } from './sub-routes' + +import { ANALYTICS_ROUTES, RDI_PIPELINE_MANAGEMENT_ROUTES, BROWSER_ROUTES } from './sub-routes' import COMMON_ROUTES from './commonRoutes' const INSTANCE_ROUTES: IRoute[] = [ { - pageName: PageNames.browser, - path: Pages.browser(':instanceId'), - component: BrowserPage, - }, - { - pageName: PageNames.workbench, - path: Pages.workbench(':instanceId'), - component: WorkbenchPage, + path: Pages.keys(':instanceId'), + component: KeysPage, + routes: BROWSER_ROUTES, }, { pageName: PageNames.pubSub, @@ -41,6 +36,10 @@ const INSTANCE_ROUTES: IRoute[] = [ component: AnalyticsPage, routes: ANALYTICS_ROUTES, }, + { + path: '/:instanceId/workbench', + redirect: (params) => Pages.workbench(params?.instanceId || '') + } ] const RDI_INSTANCE_ROUTES: IRoute[] = [ diff --git a/redisinsight/ui/src/components/main-router/constants/redisStackRoutes.ts b/redisinsight/ui/src/components/main-router/constants/redisStackRoutes.ts index 88502af3bc..043937c1ca 100644 --- a/redisinsight/ui/src/components/main-router/constants/redisStackRoutes.ts +++ b/redisinsight/ui/src/components/main-router/constants/redisStackRoutes.ts @@ -2,6 +2,7 @@ import { PageNames, Pages, IRoute } from 'uiSrc/constants' import { BrowserPage, InstancePage, } from 'uiSrc/pages' +import KeysPage from 'uiSrc/pages/keys' import WorkbenchPage from 'uiSrc/pages/workbench' import SlowLogPage from 'uiSrc/pages/slow-log' import PubSubPage from 'uiSrc/pages/pub-sub' @@ -9,8 +10,30 @@ import EditConnection from 'uiSrc/pages/redis-stack/components/edit-connection' import ClusterDetailsPage from 'uiSrc/pages/cluster-details' import AnalyticsPage from 'uiSrc/pages/analytics' import DatabaseAnalysisPage from 'uiSrc/pages/database-analysis' +import SearchPage from 'uiSrc/pages/search' import COMMON_ROUTES from './commonRoutes' +const BROWSER_ROUTES: IRoute[] = [ + { + pageName: PageNames.browser, + protected: true, + path: Pages.browser(':instanceId'), + component: BrowserPage, + }, + { + pageName: PageNames.search, + protected: true, + path: Pages.search(':instanceId'), + component: SearchPage, + }, + { + pageName: PageNames.workbench, + protected: true, + path: Pages.workbench(':instanceId'), + component: WorkbenchPage, + }, +] + const ANALYTICS_ROUTES: IRoute[] = [ { pageName: PageNames.slowLog, @@ -34,16 +57,9 @@ const ANALYTICS_ROUTES: IRoute[] = [ const INSTANCE_ROUTES: IRoute[] = [ { - pageName: PageNames.browser, - protected: true, - path: Pages.browser(':instanceId'), - component: BrowserPage, - }, - { - pageName: PageNames.workbench, - protected: true, - path: Pages.workbench(':instanceId'), - component: WorkbenchPage, + path: Pages.keys(':instanceId'), + component: KeysPage, + routes: BROWSER_ROUTES, }, { pageName: PageNames.pubSub, @@ -57,6 +73,11 @@ const INSTANCE_ROUTES: IRoute[] = [ component: AnalyticsPage, routes: ANALYTICS_ROUTES, }, + // redirect to the new workbench path + { + path: ':instanceId/workbench', + redirect: (params) => Pages.workbench(params?.instanceId || '') + } ] const ROUTES: IRoute[] = [ diff --git a/redisinsight/ui/src/components/main-router/constants/sub-routes/browserRoutes.ts b/redisinsight/ui/src/components/main-router/constants/sub-routes/browserRoutes.ts new file mode 100644 index 0000000000..16c739a1be --- /dev/null +++ b/redisinsight/ui/src/components/main-router/constants/sub-routes/browserRoutes.ts @@ -0,0 +1,22 @@ +import { IRoute, PageNames, Pages } from 'uiSrc/constants' +import BrowserPage from 'uiSrc/pages/browser' +import SearchPage from 'uiSrc/pages/search' +import WorkbenchPage from 'uiSrc/pages/workbench' + +export const BROWSER_ROUTES: IRoute[] = [ + { + pageName: PageNames.browser, + path: Pages.browser(':instanceId'), + component: BrowserPage, + }, + { + pageName: PageNames.search, + path: Pages.search(':instanceId'), + component: SearchPage, + }, + { + pageName: PageNames.workbench, + path: Pages.workbench(':instanceId'), + component: WorkbenchPage, + } +] diff --git a/redisinsight/ui/src/components/main-router/constants/sub-routes/index.ts b/redisinsight/ui/src/components/main-router/constants/sub-routes/index.ts index 68a3732539..5770a6d4ed 100644 --- a/redisinsight/ui/src/components/main-router/constants/sub-routes/index.ts +++ b/redisinsight/ui/src/components/main-router/constants/sub-routes/index.ts @@ -1,7 +1,9 @@ import { ANALYTICS_ROUTES } from './analyticsRoutes' import { RDI_PIPELINE_MANAGEMENT_ROUTES } from './rdiPipelineManagementRoutes' +import { BROWSER_ROUTES } from './browserRoutes' export { ANALYTICS_ROUTES, RDI_PIPELINE_MANAGEMENT_ROUTES, + BROWSER_ROUTES } diff --git a/redisinsight/ui/src/components/navigation-menu/NavigationMenu.spec.tsx b/redisinsight/ui/src/components/navigation-menu/NavigationMenu.spec.tsx index 118c11783d..2dfa3c3e57 100644 --- a/redisinsight/ui/src/components/navigation-menu/NavigationMenu.spec.tsx +++ b/redisinsight/ui/src/components/navigation-menu/NavigationMenu.spec.tsx @@ -5,6 +5,7 @@ import { EXTERNAL_LINKS } from 'uiSrc/constants/links' import { appInfoSelector } from 'uiSrc/slices/app/info' import { cleanup, mockedStore, render, screen, fireEvent } from 'uiSrc/utils/test-utils' +import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import NavigationMenu from './NavigationMenu' let store: typeof mockedStore @@ -23,6 +24,13 @@ jest.mock('uiSrc/slices/app/info', () => ({ }) })) +jest.mock('uiSrc/slices/instances/instances', () => ({ + ...jest.requireActual('uiSrc/slices/instances/instances'), + connectedInstanceSelector: jest.fn().mockReturnValue({ + id: '' + }) +})) + describe('NavigationMenu', () => { describe('without connectedInstance', () => { it('should render', () => { @@ -45,7 +53,6 @@ describe('NavigationMenu', () => { render() expect(screen.queryByTestId('browser-page-btn"')).not.toBeInTheDocument() - expect(screen.queryByTestId('workbench-page-btn')).not.toBeInTheDocument() }) it('should render help menu', () => { @@ -84,15 +91,12 @@ describe('NavigationMenu', () => { }) describe('with connectedInstance', () => { - beforeAll(() => { - jest.mock('uiSrc/slices/instances/instances', () => ({ - ...jest.requireActual('uiSrc/slices/instances/instances'), - connectedInstanceSelector: jest.fn().mockReturnValue({ - id: '123', - connectionType: 'STANDALONE', - db: 0, - }) - })) + beforeEach(() => { + (connectedInstanceSelector as jest.Mock).mockReturnValue({ + id: '123', + connectionType: 'STANDALONE', + db: 0, + }) }) it('should render', () => { @@ -114,8 +118,9 @@ describe('NavigationMenu', () => { })) render() - expect(screen.findByTestId('browser-page-btn')).toBeTruthy() - expect(screen.findByTestId('workbench-page-btn')).toBeTruthy() + screen.debug(undefined, 100_000) + + expect(screen.getByTestId('browser-page-btn')).toBeTruthy() }) it('should render public routes', () => { diff --git a/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx b/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx index e3a0d24f29..f77a84689f 100644 --- a/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx +++ b/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx @@ -13,7 +13,7 @@ import { EuiToolTip } from '@elastic/eui' import HighlightedFeature, { Props as HighlightedFeatureProps } from 'uiSrc/components/hightlighted-feature/HighlightedFeature' -import { ANALYTICS_ROUTES } from 'uiSrc/components/main-router/constants/sub-routes' +import { ANALYTICS_ROUTES, BROWSER_ROUTES } from 'uiSrc/components/main-router/constants/sub-routes' import { PageNames, Pages } from 'uiSrc/constants' import { EXTERNAL_LINKS } from 'uiSrc/constants/links' @@ -29,8 +29,6 @@ import SettingsSVG from 'uiSrc/assets/img/sidebar/settings.svg' import SettingsActiveSVG from 'uiSrc/assets/img/sidebar/settings_active.svg' import BrowserSVG from 'uiSrc/assets/img/sidebar/browser.svg' import BrowserActiveSVG from 'uiSrc/assets/img/sidebar/browser_active.svg' -import WorkbenchSVG from 'uiSrc/assets/img/sidebar/workbench.svg' -import WorkbenchActiveSVG from 'uiSrc/assets/img/sidebar/workbench_active.svg' import SlowLogSVG from 'uiSrc/assets/img/sidebar/slowlog.svg' import SlowLogActiveSVG from 'uiSrc/assets/img/sidebar/slowlog_active.svg' import PubSubSVG from 'uiSrc/assets/img/sidebar/pubsub.svg' @@ -53,8 +51,6 @@ import NotificationMenu from './components/notifications-center' import styles from './styles.module.scss' -const workbenchPath = `/${PageNames.workbench}` -const browserPath = `/${PageNames.browser}` const pubSubPath = `/${PageNames.pubSub}` interface INavigations { @@ -96,6 +92,10 @@ const NavigationMenu = () => { ({ path }) => (`/${last(path.split('/'))}` === activePage) ) + const isBrowserPath = (activePage: string) => !!BROWSER_ROUTES.find( + ({ path }) => (`/${last(path.split('/'))}` === activePage) + ) + const isPipelineManagementPath = () => location.pathname?.startsWith(Pages.rdiPipelineManagement(connectedRdiInstanceId)) @@ -115,7 +115,7 @@ const NavigationMenu = () => { { tooltipText: 'Browser', pageName: PageNames.browser, - isActivePage: activePage === browserPath, + isActivePage: isBrowserPath(activePage), ariaLabel: 'Browser page button', onClick: () => handleGoPage(Pages.browser(connectedInstanceId)), dataTestId: 'browser-page-btn', @@ -126,23 +126,6 @@ const NavigationMenu = () => { getIconType() { return this.isActivePage ? BrowserSVG : BrowserActiveSVG }, - onboard: ONBOARDING_FEATURES.BROWSER_PAGE - }, - { - tooltipText: 'Workbench', - pageName: PageNames.workbench, - ariaLabel: 'Workbench page button', - onClick: () => handleGoPage(Pages.workbench(connectedInstanceId)), - dataTestId: 'workbench-page-btn', - connectedInstanceId, - isActivePage: activePage === workbenchPath, - getClassName() { - return cx(styles.navigationButton, { [styles.active]: this.isActivePage }) - }, - getIconType() { - return this.isActivePage ? WorkbenchSVG : WorkbenchActiveSVG - }, - onboard: ONBOARDING_FEATURES.WORKBENCH_PAGE }, { tooltipText: 'Analysis Tools', diff --git a/redisinsight/ui/src/components/triggers/insights-trigger/InsightsTrigger.spec.tsx b/redisinsight/ui/src/components/triggers/insights-trigger/InsightsTrigger.spec.tsx index 2c4dc23b9f..2619b6a388 100644 --- a/redisinsight/ui/src/components/triggers/insights-trigger/InsightsTrigger.spec.tsx +++ b/redisinsight/ui/src/components/triggers/insights-trigger/InsightsTrigger.spec.tsx @@ -86,7 +86,7 @@ describe('InsightsTrigger', () => { databaseId: 'instanceId', provider: 'RE_CLOUD', source: 'overview', - page: '/browser', + page: '/browser/browser', tab: 'tips' }, }); diff --git a/redisinsight/ui/src/constants/pages.ts b/redisinsight/ui/src/constants/pages.ts index 29cc5c1f4b..17b6d7a35c 100644 --- a/redisinsight/ui/src/constants/pages.ts +++ b/redisinsight/ui/src/constants/pages.ts @@ -1,12 +1,14 @@ import { FeatureFlags } from 'uiSrc/constants' +import { Maybe } from 'uiSrc/utils' export interface IRoute { path: any - component: (routes: any) => JSX.Element | Element | null + component?: (routes: any) => JSX.Element | Element | null pageName?: PageNames exact?: boolean routes?: any protected?: boolean + redirect?: (params: Record>) => string isAvailableWithoutAgreements?: boolean featureFlag?: FeatureFlags } @@ -14,6 +16,7 @@ export interface IRoute { export enum PageNames { workbench = 'workbench', browser = 'browser', + search = 'search', slowLog = 'slowlog', pubSub = 'pub-sub', analytics = 'analytics', @@ -44,8 +47,10 @@ export const Pages = { sentinel, sentinelDatabases: `${sentinel}/databases`, sentinelDatabasesResult: `${sentinel}/databases-result`, - browser: (instanceId: string) => `/${instanceId}/${PageNames.browser}`, - workbench: (instanceId: string) => `/${instanceId}/${PageNames.workbench}`, + keys: (instanceId: string) => `/${instanceId}/${PageNames.browser}`, + browser: (instanceId: string) => `/${instanceId}/${PageNames.browser}/${PageNames.browser}`, + search: (instanceId: string) => `/${instanceId}/${PageNames.browser}/${PageNames.search}`, + workbench: (instanceId: string) => `/${instanceId}/${PageNames.browser}/${PageNames.workbench}`, pubSub: (instanceId: string) => `/${instanceId}/${PageNames.pubSub}`, analytics: (instanceId: string) => `/${instanceId}/${PageNames.analytics}`, slowLog: (instanceId: string) => `/${instanceId}/${PageNames.analytics}/${PageNames.slowLog}`, diff --git a/redisinsight/ui/src/pages/browser/BrowserPage.spec.tsx b/redisinsight/ui/src/pages/browser/BrowserPage.spec.tsx index 9279901966..079291de0e 100644 --- a/redisinsight/ui/src/pages/browser/BrowserPage.spec.tsx +++ b/redisinsight/ui/src/pages/browser/BrowserPage.spec.tsx @@ -9,7 +9,6 @@ import { setBrowserBulkActionOpen, setBrowserPanelSizes, setBrowserSelectedKey, - setLastPageContext } from 'uiSrc/slices/app/context' import BrowserPage from './BrowserPage' import KeyList, { Props as KeyListProps } from './components/key-list/KeyList' @@ -161,7 +160,6 @@ describe('BrowserPage', () => { setBrowserPanelSizes(expect.any(Object)), setBrowserBulkActionOpen(expect.any(Boolean)), setBrowserSelectedKey(null), - setLastPageContext('browser'), toggleBrowserFullScreen(false) ] diff --git a/redisinsight/ui/src/pages/browser/BrowserPage.tsx b/redisinsight/ui/src/pages/browser/BrowserPage.tsx index cba22499be..02c6a26d6d 100644 --- a/redisinsight/ui/src/pages/browser/BrowserPage.tsx +++ b/redisinsight/ui/src/pages/browser/BrowserPage.tsx @@ -31,7 +31,6 @@ import { setBrowserSelectedKey, appContextBrowser, setBrowserPanelSizes, - setLastPageContext, setBrowserBulkActionOpen, } from 'uiSrc/slices/app/context' import { resetErrors } from 'uiSrc/slices/app/notifications' @@ -105,7 +104,6 @@ const BrowserPage = () => { }) dispatch(setBrowserBulkActionOpen(isBulkActionsPanelOpenRef.current)) dispatch(setBrowserSelectedKey(selectedKeyRef.current)) - dispatch(setLastPageContext('browser')) if (!selectedKeyRef.current) { dispatch(toggleBrowserFullScreen(false)) diff --git a/redisinsight/ui/src/pages/browser/styles.module.scss b/redisinsight/ui/src/pages/browser/styles.module.scss index 78257fd4fb..99b9613820 100644 --- a/redisinsight/ui/src/pages/browser/styles.module.scss +++ b/redisinsight/ui/src/pages/browser/styles.module.scss @@ -1,29 +1,18 @@ $breakpoint-to-hide-resize-panel: 1280px; .container { - height: 100%; max-width: 100vw; display: flex; flex-direction: column; + flex-grow: 1; + overflow: hidden; } .main { display: flex; - flex: 1; + flex-grow: 1; padding: 0 16px; - height: calc(100% - 280px); - - &.mainWithBackBtn { - height: calc(100% - 338px); - } - - @media only screen and (min-width: 768px) { - max-width: calc(100vw - 60px); - - &.mainWithBackBtn { - height: calc(100% - 118px); - } - } + overflow: hidden; } .resizableButton { diff --git a/redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespace.spec.tsx b/redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespace.spec.tsx index 9a8a0c1c14..88d050d31e 100644 --- a/redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespace.spec.tsx +++ b/redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespace.spec.tsx @@ -166,6 +166,6 @@ describe('TopNamespace', () => { expect(store.getActions()).toEqual(expectedActions) expect(pushMock).toHaveBeenCalledTimes(1) - expect(pushMock).toHaveBeenCalledWith('/instanceId/browser') + expect(pushMock).toHaveBeenCalledWith('/instanceId/browser/browser') }) }) diff --git a/redisinsight/ui/src/pages/home/components/database-alias/DatabaseAlias.tsx b/redisinsight/ui/src/pages/home/components/database-alias/DatabaseAlias.tsx index 24b2a8a3aa..c3510d03ee 100644 --- a/redisinsight/ui/src/pages/home/components/database-alias/DatabaseAlias.tsx +++ b/redisinsight/ui/src/pages/home/components/database-alias/DatabaseAlias.tsx @@ -18,7 +18,7 @@ import { useHistory } from 'react-router' import { BuildType } from 'uiSrc/constants/env' import { appInfoSelector } from 'uiSrc/slices/app/info' import { Nullable, getDbIndex } from 'uiSrc/utils' -import { PageNames, Pages, Theme } from 'uiSrc/constants' +import { Pages, Theme } from 'uiSrc/constants' import { ThemeContext } from 'uiSrc/contexts/themeContext' import InlineItemEditor from 'uiSrc/components/inline-item-editor/InlineItemEditor' import RediStackDarkMin from 'uiSrc/assets/img/modules/redistack/RediStackDark-min.svg' @@ -65,7 +65,7 @@ const DatabaseAlias = (props: Props) => { } = props const { server } = useSelector(appInfoSelector) - const { contextInstanceId, lastPage } = useSelector(appContextSelector) + const { contextInstanceId } = useSelector(appContextSelector) const [isEditing, setIsEditing] = useState(false) const [value, setValue] = useState(alias) @@ -95,12 +95,7 @@ const DatabaseAlias = (props: Props) => { dispatch(setAppContextInitialState()) } dispatch(setConnectedInstanceId(id ?? '')) - - if (lastPage === PageNames.workbench && contextInstanceId === id) { - history.push(Pages.workbench(id)) - return - } - history.push(Pages.browser(id ?? '')) + history.push(Pages.keys(id ?? '')) } const handleOpen = (event: any) => { diff --git a/redisinsight/ui/src/pages/home/components/database-list-component/DatabasesListWrapper.tsx b/redisinsight/ui/src/pages/home/components/database-list-component/DatabasesListWrapper.tsx index 5faac08598..cc57d6ea18 100644 --- a/redisinsight/ui/src/pages/home/components/database-list-component/DatabasesListWrapper.tsx +++ b/redisinsight/ui/src/pages/home/components/database-list-component/DatabasesListWrapper.tsx @@ -25,7 +25,7 @@ import CloudLinkIcon from 'uiSrc/assets/img/oauth/cloud_link.svg?react' import { ShowChildByCondition } from 'uiSrc/components' import DatabaseListModules from 'uiSrc/components/database-list-modules/DatabaseListModules' import ItemList from 'uiSrc/components/item-list' -import { BrowserStorageItem, PageNames, Pages, Theme } from 'uiSrc/constants' +import { BrowserStorageItem, Pages, Theme } from 'uiSrc/constants' import { EXTERNAL_LINKS } from 'uiSrc/constants/links' import { ThemeContext } from 'uiSrc/contexts/themeContext' import PopoverDelete from 'uiSrc/pages/browser/components/popover-delete/PopoverDelete' @@ -63,7 +63,7 @@ const DatabasesListWrapper = ({ width, onEditInstance, editedInstance, onDeleteI const { search } = useLocation() const { theme } = useContext(ThemeContext) - const { contextInstanceId, lastPage } = useSelector(appContextSelector) + const { contextInstanceId } = useSelector(appContextSelector) const instances = useSelector(instancesSelector) const [, forceRerender] = useState({}) @@ -125,11 +125,7 @@ const DatabasesListWrapper = ({ width, onEditInstance, editedInstance, onDeleteI } dispatch(setConnectedInstanceId(id)) - if (lastPage === PageNames.workbench && contextInstanceId === id) { - history.push(Pages.workbench(id)) - return - } - history.push(Pages.browser(id)) + history.push(Pages.keys(id)) } const handleCheckConnectToInstance = ( event: React.MouseEvent | React.KeyboardEvent, diff --git a/redisinsight/ui/src/pages/keys/KeysPage.spec.tsx b/redisinsight/ui/src/pages/keys/KeysPage.spec.tsx new file mode 100644 index 0000000000..b3b4982efe --- /dev/null +++ b/redisinsight/ui/src/pages/keys/KeysPage.spec.tsx @@ -0,0 +1,85 @@ +import React from 'react' +import reactRouterDom, { BrowserRouter } from 'react-router-dom' +import { cleanup } from '@testing-library/react' +import { cloneDeep } from 'lodash' +import { mockedStore, render } from 'uiSrc/utils/test-utils' + +import { Pages } from 'uiSrc/constants' +import { appContextSelector, setLastPageContext } from 'uiSrc/slices/app/context' +import KeysPage from './KeysPage' + +jest.mock('uiSrc/slices/app/context', () => ({ + ...jest.requireActual('uiSrc/slices/app/context'), + appContextSelector: jest.fn().mockReturnValue({ + lastBrowserPage: '', + }), +})) + +let store: typeof mockedStore +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +const mockedRoutes = [ + { + path: '/123/browser', + }, +] + +describe('KeysPage', () => { + it('should render', () => { + expect( + render( + + + + ) + ).toBeTruthy() + }) + + it('should redirect to the browser by default', () => { + const pushMock = jest.fn() + reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock }) + reactRouterDom.useLocation = jest.fn().mockReturnValue({ pathname: Pages.keys('instanceId') }) + + render( + + + + ) + + expect(pushMock).toBeCalledWith(Pages.browser('instanceId')) + }) + + it('should redirect to the prev page from context', () => { + (appContextSelector as jest.Mock).mockReturnValueOnce({ + lastBrowserPage: Pages.workbench('instanceId') + }) + const pushMock = jest.fn() + reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock }) + reactRouterDom.useLocation = jest.fn().mockReturnValue({ pathname: Pages.keys('instanceId') }) + + render( + + + + ) + + expect(pushMock).toBeCalledWith(Pages.workbench('instanceId')) + }) + + it('should save proper page on unmount', () => { + reactRouterDom.useLocation = jest.fn().mockReturnValue({ pathname: Pages.workbench('instanceId') }) + + const { unmount } = render( + + + + ) + + unmount() + expect(store.getActions()).toEqual([setLastPageContext('/instanceId/browser/workbench')]) + }) +}) diff --git a/redisinsight/ui/src/pages/keys/KeysPage.tsx b/redisinsight/ui/src/pages/keys/KeysPage.tsx new file mode 100644 index 0000000000..9ac514c88d --- /dev/null +++ b/redisinsight/ui/src/pages/keys/KeysPage.tsx @@ -0,0 +1,62 @@ +import React, { useEffect, useRef } from 'react' +import { Switch, useHistory, useLocation, useParams } from 'react-router-dom' +import { useDispatch, useSelector } from 'react-redux' +import { IRoute, Pages } from 'uiSrc/constants' +import { appContextSelector, setLastPageContext } from 'uiSrc/slices/app/context' +import RouteWithSubRoutes from 'uiSrc/utils/routerWithSubRoutes' + +import BrowserTabs from './components/browser-tabs' + +export interface Props { + routes: IRoute[] +} + +const KeysPage = (props: Props) => { + const { routes } = props + + const { lastBrowserPage } = useSelector(appContextSelector) + const { instanceId } = useParams<{ instanceId: string }>() + const { pathname } = useLocation() + const pathnameRef = useRef('') + + const history = useHistory() + const dispatch = useDispatch() + + useEffect(() => () => { + dispatch(setLastPageContext(pathnameRef.current)) + }, []) + + useEffect(() => { + if (pathname === Pages.keys(instanceId)) { + // restore current inner page and ignore context (as we store context on unmount) + if (pathnameRef.current && pathnameRef.current !== lastBrowserPage) { + history.push(pathnameRef.current) + return + } + + // restore from context + if (lastBrowserPage) { + history.push(lastBrowserPage) + return + } + + history.push(Pages.browser(instanceId)) + } + + pathnameRef.current = pathname === Pages.keys(instanceId) ? '' : pathname + }, [pathname]) + + return ( + <> + + + {routes.map((route, i) => ( + // eslint-disable-next-line react/no-array-index-key + + ))} + + + ) +} + +export default KeysPage diff --git a/redisinsight/ui/src/pages/keys/components/browser-tabs/BrowserTabs.spec.tsx b/redisinsight/ui/src/pages/keys/components/browser-tabs/BrowserTabs.spec.tsx new file mode 100644 index 0000000000..a85228d2e4 --- /dev/null +++ b/redisinsight/ui/src/pages/keys/components/browser-tabs/BrowserTabs.spec.tsx @@ -0,0 +1,40 @@ +import React from 'react' +import reactRouterDom from 'react-router-dom' +import { render, screen, fireEvent } from 'uiSrc/utils/test-utils' + +import BrowserTabs from './BrowserTabs' + +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useHistory: () => ({ + push: jest.fn, + }), +})) + +const MOCKED_INSTANCE_ID = 'instanceId' + +describe('BrowserTabs', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should call proper history push after click on tabs', () => { + const pushMock = jest.fn() + reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock }) + + render() + + fireEvent.click(screen.getByTestId('browser-tab-workbench')) + expect(pushMock).toBeCalledWith('/instanceId/browser/workbench') + + pushMock.mockRestore() + + fireEvent.click(screen.getByTestId('browser-tab-browser')) + expect(pushMock).toBeCalledWith('/instanceId/browser/browser') + }) +}) diff --git a/redisinsight/ui/src/pages/keys/components/browser-tabs/BrowserTabs.tsx b/redisinsight/ui/src/pages/keys/components/browser-tabs/BrowserTabs.tsx new file mode 100644 index 0000000000..6e34e2e159 --- /dev/null +++ b/redisinsight/ui/src/pages/keys/components/browser-tabs/BrowserTabs.tsx @@ -0,0 +1,67 @@ +import React from 'react' +import { EuiBadge, EuiTab, EuiTabs } from '@elastic/eui' +import { useHistory } from 'react-router-dom' +import { Pages } from 'uiSrc/constants/pages' + +import { renderOnboardingTourWithChild } from 'uiSrc/utils/onboarding' +import { ONBOARDING_FEATURES } from 'uiSrc/components/onboarding-features' +import styles from './styles.module.scss' + +export interface Props { + instanceId: string + pathname: string +} + +const BrowserTabs = (props: Props) => { + const { instanceId, pathname } = props + + const history = useHistory() + + const tabs = [ + { + id: 'browser', + title: 'Browse and filter', + page: Pages.browser(instanceId), + onboard: ONBOARDING_FEATURES.BROWSER_PAGE + }, + { + id: 'search', + title: 'Search and query', + page: Pages.search(instanceId), + isBeta: true + }, + { + id: 'workbench', + title: 'Workbench', + page: Pages.workbench(instanceId), + onboard: ONBOARDING_FEATURES.WORKBENCH_PAGE, + } + ] + + const onClickTab = (page: string) => { + history.push(page) + } + + return ( + + {tabs.map(({ id, title, page, onboard, isBeta }) => renderOnboardingTourWithChild( + ( + onClickTab(page)} + data-testid={`browser-tab-${id}`} + > + {title} + {isBeta && ((New!))} + + ), + { options: onboard, anchorPosition: 'downLeft' }, + pathname === page + ))} + + ) +} + +export default BrowserTabs diff --git a/redisinsight/ui/src/pages/keys/components/browser-tabs/index.ts b/redisinsight/ui/src/pages/keys/components/browser-tabs/index.ts new file mode 100644 index 0000000000..e1617ed596 --- /dev/null +++ b/redisinsight/ui/src/pages/keys/components/browser-tabs/index.ts @@ -0,0 +1,3 @@ +import BrowserTabs from './BrowserTabs' + +export default BrowserTabs diff --git a/redisinsight/ui/src/pages/keys/components/browser-tabs/styles.module.scss b/redisinsight/ui/src/pages/keys/components/browser-tabs/styles.module.scss new file mode 100644 index 0000000000..3072523b71 --- /dev/null +++ b/redisinsight/ui/src/pages/keys/components/browser-tabs/styles.module.scss @@ -0,0 +1,44 @@ +.tabs { + margin: 0 16px 16px; + background: var(--euiColorEmptyShade); + + .tab { + padding: 8px 16px !important; + margin: 0 !important; + border-radius: 0 !important; + color: var(--euiTextSubduedColor) !important; + border-right: 1px solid var(--separatorColor); + + &:hover { + background: var(--tableLightBorderColor); + } + + &:global(.euiTab-isSelected) { + color: var(--euiTextColor) !important; + background: var(--insightsTriggerBgColor) !important; + } + + &:after { + display: none !important; + } + } + + .betaLabel { + margin-left: 12px; + font-size: 8px !important; + line-height: 12px !important; + background-color: var(--recommendationLiveBorderColor) !important; + border: 1px solid var(--triggerIconActiveColor) !important; + color: #FFF7EA !important; + border-radius: 2px !important; + padding: 0 3px !important; + margin-bottom: 2px; + + transition: transform 250ms ease-in-out; + pointer-events: none; + + :global(.euiBadge__content) { + min-height: 12px !important; + } + } +} diff --git a/redisinsight/ui/src/pages/keys/index.ts b/redisinsight/ui/src/pages/keys/index.ts new file mode 100644 index 0000000000..06362669d9 --- /dev/null +++ b/redisinsight/ui/src/pages/keys/index.ts @@ -0,0 +1,3 @@ +import KeysPage from './KeysPage' + +export default KeysPage diff --git a/redisinsight/ui/src/pages/search/SearchPage.spec.tsx b/redisinsight/ui/src/pages/search/SearchPage.spec.tsx new file mode 100644 index 0000000000..f48f5729ff --- /dev/null +++ b/redisinsight/ui/src/pages/search/SearchPage.spec.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import { render, screen } from 'uiSrc/utils/test-utils' + +import SearchPage from './SearchPage' + +describe('SearchPage', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/search/SearchPage.tsx b/redisinsight/ui/src/pages/search/SearchPage.tsx new file mode 100644 index 0000000000..aef47b2781 --- /dev/null +++ b/redisinsight/ui/src/pages/search/SearchPage.tsx @@ -0,0 +1,14 @@ +import React from 'react' + +export interface Props { + +} + +const SearchPage = (props: Props) => { + const {} = props + return ( +
+ ) +} + +export default SearchPage diff --git a/redisinsight/ui/src/pages/search/index.ts b/redisinsight/ui/src/pages/search/index.ts new file mode 100644 index 0000000000..2f6b199b65 --- /dev/null +++ b/redisinsight/ui/src/pages/search/index.ts @@ -0,0 +1,3 @@ +import SearchPage from './SearchPage' + +export default SearchPage diff --git a/redisinsight/ui/src/pages/workbench/WorkbenchPage.tsx b/redisinsight/ui/src/pages/workbench/WorkbenchPage.tsx index 8217ec18e4..8f142d8923 100644 --- a/redisinsight/ui/src/pages/workbench/WorkbenchPage.tsx +++ b/redisinsight/ui/src/pages/workbench/WorkbenchPage.tsx @@ -3,9 +3,7 @@ import { useDispatch, useSelector } from 'react-redux' import { useParams } from 'react-router-dom' import { formatLongName, getDbIndex, setTitle } from 'uiSrc/utils' -import { PageNames } from 'uiSrc/constants' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' -import { setLastPageContext } from 'uiSrc/slices/app/context' import { loadPluginsAction } from 'uiSrc/slices/app/plugins' import { sendPageViewTelemetry, TelemetryPageView } from 'uiSrc/telemetry' import WBViewWrapper from './components/wb-view' @@ -38,10 +36,6 @@ const WorkbenchPage = () => { dispatch(loadPluginsAction()) }, []) - useEffect(() => () => { - dispatch(setLastPageContext(PageNames.workbench)) - }) - return () } diff --git a/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.tsx b/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.tsx index 921c67b96f..0c3e94acb0 100644 --- a/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.tsx +++ b/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.tsx @@ -44,7 +44,7 @@ interface IState { let state: IState = { loading: false, - instance: instanceInitState.connectedInstance, + instance: instanceInitState?.connectedInstance, unsupportedCommands: [], blockingCommands: [], visualizations: [], diff --git a/redisinsight/ui/src/slices/app/context.ts b/redisinsight/ui/src/slices/app/context.ts index d00dfff572..12928d204c 100644 --- a/redisinsight/ui/src/slices/app/context.ts +++ b/redisinsight/ui/src/slices/app/context.ts @@ -35,7 +35,7 @@ export const initialState: StateAppContext = { : AppWorkspace.Databases, contextInstanceId: '', contextRdiInstanceId: '', - lastPage: '', + lastBrowserPage: '', dbConfig: { treeViewDelimiter: DEFAULT_DELIMITER, treeViewSort: DEFAULT_TREE_SORTING, @@ -179,7 +179,7 @@ const appContextSlice = createSlice({ state.workbench.panelSizes.vertical = payload }, setLastPageContext: (state, { payload }: { payload: string }) => { - state.lastPage = payload + state.lastBrowserPage = payload }, resetBrowserTree: (state) => { state.browser.tree.selectedLeaf = null diff --git a/redisinsight/ui/src/slices/interfaces/app.ts b/redisinsight/ui/src/slices/interfaces/app.ts index 3948d9ba4e..68510f468c 100644 --- a/redisinsight/ui/src/slices/interfaces/app.ts +++ b/redisinsight/ui/src/slices/interfaces/app.ts @@ -54,7 +54,7 @@ export interface StateAppContext { workspace: AppWorkspace contextInstanceId: string contextRdiInstanceId: string - lastPage: string + lastBrowserPage: string dbConfig: { treeViewDelimiter: string treeViewSort: SortOrder diff --git a/redisinsight/ui/src/slices/tests/app/context.spec.ts b/redisinsight/ui/src/slices/tests/app/context.spec.ts index 874ac9da0b..91b8898d17 100644 --- a/redisinsight/ui/src/slices/tests/app/context.spec.ts +++ b/redisinsight/ui/src/slices/tests/app/context.spec.ts @@ -370,7 +370,7 @@ describe('slices', () => { const lastPage = 'workbench' const state = { ...initialState, - lastPage + lastBrowserPage: lastPage } // Act diff --git a/redisinsight/ui/src/utils/routerWithSubRoutes.tsx b/redisinsight/ui/src/utils/routerWithSubRoutes.tsx index a934bfcf2e..690ad23ff3 100644 --- a/redisinsight/ui/src/utils/routerWithSubRoutes.tsx +++ b/redisinsight/ui/src/utils/routerWithSubRoutes.tsx @@ -1,13 +1,12 @@ import React from 'react' import { Redirect, Route } from 'react-router-dom' import { useSelector } from 'react-redux' -import { isUndefined } from 'lodash' import { userSettingsSelector } from 'uiSrc/slices/user/user-settings' import { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features' import { IRoute, FeatureFlags } from 'uiSrc/constants' const PrivateRoute = (route: IRoute) => { - const { path, exact, routes, featureFlag } = route + const { path, exact, routes, featureFlag, redirect } = route const { [featureFlag as FeatureFlags]: feature, } = useSelector(appFeatureFlagsFeaturesSelector) @@ -17,15 +16,26 @@ const PrivateRoute = (route: IRoute) => { - haveToAcceptAgreements || feature?.flag === false + render={(props) => { + if (redirect) { + return ( + + ) + } + + return haveToAcceptAgreements || feature?.flag === false ? : ( // pass the sub-routes down to keep nesting // @ts-ignore ) - } + }} /> ) } diff --git a/redisinsight/ui/src/utils/routing.ts b/redisinsight/ui/src/utils/routing.ts index 78ea0d2ec2..341b33e01a 100644 --- a/redisinsight/ui/src/utils/routing.ts +++ b/redisinsight/ui/src/utils/routing.ts @@ -1,4 +1,4 @@ -import { IRoute } from 'uiSrc/constants' +import { IRoute, Pages } from 'uiSrc/constants' import { Maybe, Nullable } from 'uiSrc/utils' import DEFAULT_ROUTES from 'uiSrc/components/main-router/constants/defaultRoutes' @@ -43,6 +43,11 @@ export const getRedirectionPage = ( page += '&insights=open' } + // old page - temp redirection + if (page === 'workbench' && databaseId) { + return `${Pages.keys(databaseId)}/${page}` + } + const foundRoute = findRouteByPathname(DEFAULT_ROUTES, pathname) if (!foundRoute) return undefined diff --git a/redisinsight/ui/src/utils/tests/routing.spec.ts b/redisinsight/ui/src/utils/tests/routing.spec.ts index 6d4e88443f..ab019d61b5 100644 --- a/redisinsight/ui/src/utils/tests/routing.spec.ts +++ b/redisinsight/ui/src/utils/tests/routing.spec.ts @@ -14,8 +14,10 @@ Object.defineProperty(window, 'location', { const databaseId = '1' const getRedirectionPageTests = [ { input: ['settings'], expected: '/settings' }, - { input: ['workbench', databaseId], expected: '/1/workbench' }, - { input: ['/workbench', databaseId], expected: '/1/workbench' }, + { input: ['workbench', databaseId], expected: '/1/browser/workbench' }, + { input: ['/workbench', databaseId], expected: '/1/browser/workbench' }, + { input: ['browser/workbench', databaseId], expected: '/1/browser/workbench' }, + { input: ['/browser/workbench', databaseId], expected: '/1/browser/workbench' }, { input: ['/analytics/slowlog', databaseId], expected: '/1/analytics/slowlog' }, { input: ['/analytics/slowlog'], expected: null }, { input: ['/analytics', databaseId], expected: '/1/analytics' }, From 633dc9ceb477f04cc3556560d6d5a850dea92011 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Thu, 27 Jun 2024 15:32:39 +0200 Subject: [PATCH 002/112] #RI-5682 - update workbench editor --- .../components/query/Query/styles.module.scss | 140 ------------------ .../ui/src/constants/keyboardShortcuts.tsx | 4 +- .../query-actions/QueryActions.spec.tsx | 37 +++++ .../components/query-actions/QueryActions.tsx | 127 ++++++++++++++++ .../components/query-actions/index.ts | 3 + .../query-actions/styles.module.scss | 82 ++++++++++ .../query-tutorials/QueryTutorials.spec.tsx | 58 ++++++++ .../query-tutorials/QueryTutorials.tsx | 66 +++++++++ .../components/query-tutorials/index.ts | 3 + .../query-tutorials/styles.module.scss | 48 ++++++ .../components/query/Query/Query.spec.tsx | 0 .../components/query/Query/Query.tsx | 97 ++---------- .../components/query/Query/index.ts | 0 .../components/query/Query/styles.module.scss | 76 ++++++++++ .../components/query/QueryWrapper.spec.tsx | 0 .../components/query/QueryWrapper.tsx | 3 - .../workbench}/components/query/index.ts | 0 .../wb-results/WBResults/WBResults.tsx | 3 +- .../components/wb-view/WBView/WBView.tsx | 16 +- .../components/wb-view/WBViewWrapper.tsx | 1 - 20 files changed, 524 insertions(+), 240 deletions(-) delete mode 100644 redisinsight/ui/src/components/query/Query/styles.module.scss create mode 100644 redisinsight/ui/src/pages/workbench/components/query-actions/QueryActions.spec.tsx create mode 100644 redisinsight/ui/src/pages/workbench/components/query-actions/QueryActions.tsx create mode 100644 redisinsight/ui/src/pages/workbench/components/query-actions/index.ts create mode 100644 redisinsight/ui/src/pages/workbench/components/query-actions/styles.module.scss create mode 100644 redisinsight/ui/src/pages/workbench/components/query-tutorials/QueryTutorials.spec.tsx create mode 100644 redisinsight/ui/src/pages/workbench/components/query-tutorials/QueryTutorials.tsx create mode 100644 redisinsight/ui/src/pages/workbench/components/query-tutorials/index.ts create mode 100644 redisinsight/ui/src/pages/workbench/components/query-tutorials/styles.module.scss rename redisinsight/ui/src/{ => pages/workbench}/components/query/Query/Query.spec.tsx (100%) rename redisinsight/ui/src/{ => pages/workbench}/components/query/Query/Query.tsx (82%) rename redisinsight/ui/src/{ => pages/workbench}/components/query/Query/index.ts (100%) create mode 100644 redisinsight/ui/src/pages/workbench/components/query/Query/styles.module.scss rename redisinsight/ui/src/{ => pages/workbench}/components/query/QueryWrapper.spec.tsx (100%) rename redisinsight/ui/src/{ => pages/workbench}/components/query/QueryWrapper.tsx (92%) rename redisinsight/ui/src/{ => pages/workbench}/components/query/index.ts (100%) diff --git a/redisinsight/ui/src/components/query/Query/styles.module.scss b/redisinsight/ui/src/components/query/Query/styles.module.scss deleted file mode 100644 index bb80636b81..0000000000 --- a/redisinsight/ui/src/components/query/Query/styles.module.scss +++ /dev/null @@ -1,140 +0,0 @@ -.wrapper { - position: relative; - height: 100%; - - :global(.editorBounder) { - bottom: 6px; - left: 18px; - right: 46px; - } -} -.container { - display: flex; - padding: 8px 0 8px 16px; - width: 100%; - height: 100%; - word-break: break-word; - text-align: left; - letter-spacing: 0; - background-color: var(--rsInputWrapperColor); - color: var(--euiTextSubduedColor) !important; - border: 1px solid var(--euiColorLightShade); -} - -.disabled { - opacity: 0.8; -} - -.disabledActions { - pointer-events: none; - user-select: none; -} - -.containerPlaceholder { - display: flex; - padding: 8px 16px 8px 16px; - width: 100%; - height: 100%; - background-color: var(--rsInputWrapperColor); - color: var(--euiTextSubduedColor) !important; - border: 1px solid var(--euiColorLightShade); - > div { - border: 1px solid var(--euiColorLightShade); - background-color: var(--euiColorEmptyShade); - padding: 8px 20px; - width: 100%; - } -} - -.input { - height: 100%; - width: calc(100% - 44px); - border: 1px solid var(--euiColorLightShade); - background-color: var(--rsInputColor); -} - -#script { - font: normal normal bold 14px/17px Inconsolata !important; - color: var(--textColorShade); - caret-color: var(--euiColorFullShade); - min-width: 5px; - display: inline; -} - -.actions { - width: 44px; - position: relative; - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: center; -} - -.textBtn.textBtn { - border: none !important; - width: 24px; - height: 24px !important; - min-width: auto !important; - min-height: auto !important; - border-radius: 4px !important; - background: transparent !important; - box-shadow: none !important; - - :global(.euiButton__content) { - padding: 0 !important; - } - - &:hover, - &:focus { - border: 1px solid var(--buttonSecondaryHoverColor) !important; - } - - svg path { - fill: var(--euiTextSubduedColor) !important; - } - - &:hover svg path, - &:focus svg path { - fill: var(--wbHoverIconColor) !important; - } - - &.activeBtn { - background: var(--browserComponentActive) !important; - border: 1px solid var(--euiColorSecondary); - - svg path { - fill: var(--wbActiveIconColor) !important; - } - - &:hover, - &:focus { - border: 1px solid var(--buttonSecondaryHoverColor) !important; - } - } -} - -.submitButton { - color: var(--rsSubmitBtn) !important; - width: 44px !important; - height: 44px !important; - - &Loading { - position: absolute; - left: 0; - opacity: 0.5; - margin-top: -10px; - svg { - width: 17px !important; - height: 17px !important; - } - } - - svg { - width: 24px; - height: 24px; - } -} - -.tooltipText { - font-size: 12px !important; -} diff --git a/redisinsight/ui/src/constants/keyboardShortcuts.tsx b/redisinsight/ui/src/constants/keyboardShortcuts.tsx index bf98ebb298..188ad8d421 100644 --- a/redisinsight/ui/src/constants/keyboardShortcuts.tsx +++ b/redisinsight/ui/src/constants/keyboardShortcuts.tsx @@ -107,8 +107,8 @@ const MAC_SHORTCUTS = { }, workbench: { runQuery: { - label: 'Run', - description: 'Run Command', + label: 'Run commands', + description: 'Run Commands', keys: [(), 'Enter'], }, nextLine: { diff --git a/redisinsight/ui/src/pages/workbench/components/query-actions/QueryActions.spec.tsx b/redisinsight/ui/src/pages/workbench/components/query-actions/QueryActions.spec.tsx new file mode 100644 index 0000000000..a913298d14 --- /dev/null +++ b/redisinsight/ui/src/pages/workbench/components/query-actions/QueryActions.spec.tsx @@ -0,0 +1,37 @@ +import React from 'react' +import { mock } from 'ts-mockito' +import { render, screen, fireEvent } from 'uiSrc/utils/test-utils' + +import QueryActions, { Props } from './QueryActions' + +const mockedProps = mock() + +describe('QueryActions', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should call props on click buttons', () => { + const onChangeMode = jest.fn() + const onChangeGroupMode = jest.fn() + const onSubmit = jest.fn() + + render( + + ) + + fireEvent.click(screen.getByTestId('btn-change-mode')) + expect(onChangeMode).toBeCalled() + + fireEvent.click(screen.getByTestId('btn-change-group-mode')) + expect(onChangeGroupMode).toBeCalled() + + fireEvent.click(screen.getByTestId('btn-submit')) + expect(onSubmit).toBeCalled() + }) +}) diff --git a/redisinsight/ui/src/pages/workbench/components/query-actions/QueryActions.tsx b/redisinsight/ui/src/pages/workbench/components/query-actions/QueryActions.tsx new file mode 100644 index 0000000000..d8fc8b047a --- /dev/null +++ b/redisinsight/ui/src/pages/workbench/components/query-actions/QueryActions.tsx @@ -0,0 +1,127 @@ +import React, { useRef } from 'react' + +import cx from 'classnames' +import { EuiButton, EuiSpacer, EuiText, EuiToolTip } from '@elastic/eui' +import { ResultsMode, RunQueryMode } from 'uiSrc/slices/interfaces' +import { KEYBOARD_SHORTCUTS } from 'uiSrc/constants' +import { KeyboardShortcut } from 'uiSrc/components' +import { isGroupMode } from 'uiSrc/utils' + +import GroupModeIcon from 'uiSrc/assets/img/icons/group_mode.svg?react' +import RawModeIcon from 'uiSrc/assets/img/icons/raw_mode.svg?react' + +import Divider from 'uiSrc/components/divider/Divider' +import styles from './styles.module.scss' + +export interface Props { + onChangeMode: () => void + onChangeGroupMode: () => void + onSubmit: () => void + activeMode: RunQueryMode + resultsMode?: ResultsMode + isLoading?: boolean + isDisabled?: boolean +} + +const QueryActions = (props: Props) => { + const { + isLoading, + isDisabled, + activeMode, + resultsMode, + onChangeMode, + onChangeGroupMode, + onSubmit, + } = props + const runTooltipRef = useRef(null) + + const KeyBoardTooltipContent = KEYBOARD_SHORTCUTS?.workbench?.runQuery && ( + <> + + {KEYBOARD_SHORTCUTS.workbench.runQuery?.label}: + + + + + ) + + return ( +
+ + onChangeMode()} + iconType={RawModeIcon} + disabled={isLoading} + className={cx(styles.btn, styles.textBtn, { [styles.activeBtn]: activeMode === RunQueryMode.Raw })} + data-testid="btn-change-mode" + > + Raw mode + + + + Groups the command results into a single window. +
+ When grouped, the results can be visualized only in the text format. + + )} + data-testid="group-results-tooltip" + > + onChangeGroupMode()} + disabled={isLoading} + iconType={GroupModeIcon} + className={cx(styles.btn, styles.textBtn, { [styles.activeBtn]: isGroupMode(resultsMode) })} + data-testid="btn-change-group-mode" + > + Group results + +
+ + + <> + { + onSubmit() + setTimeout(() => runTooltipRef?.current?.hideToolTip?.(), 0) + }} + isLoading={isLoading} + disabled={isLoading} + iconType="playFilled" + className={cx(styles.btn, styles.submitButton)} + aria-label="submit" + data-testid="btn-submit" + > + Run + + + +
+ ) +} + +export default QueryActions diff --git a/redisinsight/ui/src/pages/workbench/components/query-actions/index.ts b/redisinsight/ui/src/pages/workbench/components/query-actions/index.ts new file mode 100644 index 0000000000..474915f557 --- /dev/null +++ b/redisinsight/ui/src/pages/workbench/components/query-actions/index.ts @@ -0,0 +1,3 @@ +import QueryActions from './QueryActions' + +export default QueryActions diff --git a/redisinsight/ui/src/pages/workbench/components/query-actions/styles.module.scss b/redisinsight/ui/src/pages/workbench/components/query-actions/styles.module.scss new file mode 100644 index 0000000000..4764d8d231 --- /dev/null +++ b/redisinsight/ui/src/pages/workbench/components/query-actions/styles.module.scss @@ -0,0 +1,82 @@ +.actions { + display: flex; + justify-content: space-between; + align-items: center; + + .btn { + height: 24px !important; + min-width: auto !important; + min-height: auto !important; + border-radius: 4px !important; + background: transparent !important; + box-shadow: none !important; + + color: var(--euiTextColor) !important; + border: 1px solid transparent !important; + + :global(.euiButton__content) { + padding: 0 6px; + } + + :global(.euiButton__text) { + color: var(--euiTextColor) !important; + font: normal normal 400 14px/17px Graphik, sans-serif !important + } + + &:focus, &:active { + outline: 0 !important; + } + + svg { + margin-top: 1px; + width: 14px; + height: 14px; + } + } + + .textBtn { + margin: 0 8px; + + svg path { + fill: var(--euiTextSubduedColor) !important; + } + + &.activeBtn { + background: var(--browserComponentActive) !important; + border: 1px solid var(--euiColorPrimary) !important; + } + } + + .submitButton { + margin-left: 8px; + + svg { + color: var(--rsSubmitBtn) !important; + } + + :global(.euiLoadingSpinner) { + width: 14px; + height: 14px; + color: var(--rsSubmitBtn) !important; + } + } + + .divider { + height: 20px; + margin-left: 8px; + } + + .tooltipText { + font-size: 12px !important; + } +} + +@include global.insights-open(1220px) { + .actions { + .btn { + :global(.euiButton__text) { + display: none; + } + } + } +} diff --git a/redisinsight/ui/src/pages/workbench/components/query-tutorials/QueryTutorials.spec.tsx b/redisinsight/ui/src/pages/workbench/components/query-tutorials/QueryTutorials.spec.tsx new file mode 100644 index 0000000000..ce7f39786b --- /dev/null +++ b/redisinsight/ui/src/pages/workbench/components/query-tutorials/QueryTutorials.spec.tsx @@ -0,0 +1,58 @@ +import React from 'react' +import reactRouterDom from 'react-router-dom' +import { fireEvent, render, screen } from 'uiSrc/utils/test-utils' + +import { findTutorialPath } from 'uiSrc/utils' + +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' + +import QueryTutorials from './QueryTutorials' + +jest.mock('uiSrc/utils', () => ({ + ...jest.requireActual('uiSrc/utils'), + findTutorialPath: jest.fn(), +})) + +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) + +describe('QueryTutorial', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should call proper history push after click on guide with tutorial', () => { + const pushMock = jest.fn() + reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock }); + (findTutorialPath as jest.Mock).mockImplementation(() => 'path') + + render() + + fireEvent.click(screen.getByTestId('wb-tutorials-link_sq-intro')) + + expect(pushMock).toHaveBeenCalledWith({ + search: 'path=tutorials/path' + }) + }) + + it('should call proper telemetry event after click on guide', () => { + const sendEventTelemetryMock = jest.fn(); + (sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock); + (findTutorialPath as jest.Mock).mockImplementation(() => 'path') + + render() + + fireEvent.click(screen.getByTestId('wb-tutorials-link_sq-intro')) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.EXPLORE_PANEL_TUTORIAL_OPENED, + eventData: { + path: 'path', + databaseId: 'instanceId', + source: 'advanced_workbench_editor', + } + }) + }) +}) diff --git a/redisinsight/ui/src/pages/workbench/components/query-tutorials/QueryTutorials.tsx b/redisinsight/ui/src/pages/workbench/components/query-tutorials/QueryTutorials.tsx new file mode 100644 index 0000000000..b009b6b3e9 --- /dev/null +++ b/redisinsight/ui/src/pages/workbench/components/query-tutorials/QueryTutorials.tsx @@ -0,0 +1,66 @@ +import React from 'react' + +import { EuiLink, EuiText } from '@elastic/eui' +import { useDispatch } from 'react-redux' +import { useHistory, useParams } from 'react-router-dom' +import { findTutorialPath } from 'uiSrc/utils' +import { openTutorialByPath } from 'uiSrc/slices/panels/sidePanels' +import { sendEventTelemetry, TELEMETRY_EMPTY_VALUE, TelemetryEvent } from 'uiSrc/telemetry' + +import styles from './styles.module.scss' + +const TUTORIALS = [ + { + id: 'sq-intro', + title: 'Intro to search' + }, + { + id: 'redis_use_cases_basic', + title: 'Basic use cases' + }, + { + id: 'vss-intro', + title: 'Intro to vector search' + }, +] + +const QueryTutorials = () => { + const dispatch = useDispatch() + const history = useHistory() + const { instanceId } = useParams<{ instanceId: string }>() + + const handleClickTutorial = (id: string) => { + const tutorialPath = findTutorialPath({ id }) + dispatch(openTutorialByPath(tutorialPath, history, true)) + + sendEventTelemetry({ + event: TelemetryEvent.EXPLORE_PANEL_TUTORIAL_OPENED, + eventData: { + path: tutorialPath, + databaseId: instanceId || TELEMETRY_EMPTY_VALUE, + source: 'advanced_workbench_editor', + } + }) + } + + return ( +
+ + Tutorials: + + {TUTORIALS.map(({ id, title }) => ( + handleClickTutorial(id)} + data-testid={`wb-tutorials-link_${id}`} + > + {title} + + ))} +
+ ) +} + +export default QueryTutorials diff --git a/redisinsight/ui/src/pages/workbench/components/query-tutorials/index.ts b/redisinsight/ui/src/pages/workbench/components/query-tutorials/index.ts new file mode 100644 index 0000000000..c2c46d3a39 --- /dev/null +++ b/redisinsight/ui/src/pages/workbench/components/query-tutorials/index.ts @@ -0,0 +1,3 @@ +import QueryTutorials from './QueryTutorials' + +export default QueryTutorials diff --git a/redisinsight/ui/src/pages/workbench/components/query-tutorials/styles.module.scss b/redisinsight/ui/src/pages/workbench/components/query-tutorials/styles.module.scss new file mode 100644 index 0000000000..0e1e56dc13 --- /dev/null +++ b/redisinsight/ui/src/pages/workbench/components/query-tutorials/styles.module.scss @@ -0,0 +1,48 @@ +.container { + display: flex; + align-items: center; + + .title { + margin-right: 8px; + } + + .tutorialLink { + padding: 4px 8px; + + background-color: var(--browserTableRowEven); + + border-radius: 4px; + border: 1px solid var(--separatorColor); + + color: var(--htmlColor) !important; + text-decoration: none !important; + font-size: 12px; + + &:not(:first-of-type) { + margin-left: 8px; + } + + &:global(.euiLink) { + &:hover, &:focus { + color: var(--htmlColor); + text-decoration: underline !important; + outline: none !important; + animation: none !important; + } + } + } +} + +@include global.insights-open(1280px) { + .title { + display: none + } +} + +@include global.insights-open(1024px) { + .tutorialLink:last-of-type { + display: none; + } +} + + diff --git a/redisinsight/ui/src/components/query/Query/Query.spec.tsx b/redisinsight/ui/src/pages/workbench/components/query/Query/Query.spec.tsx similarity index 100% rename from redisinsight/ui/src/components/query/Query/Query.spec.tsx rename to redisinsight/ui/src/pages/workbench/components/query/Query/Query.spec.tsx diff --git a/redisinsight/ui/src/components/query/Query/Query.tsx b/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx similarity index 82% rename from redisinsight/ui/src/components/query/Query/Query.tsx rename to redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx index b59b5ec0e4..42ab11c6a0 100644 --- a/redisinsight/ui/src/components/query/Query/Query.tsx +++ b/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx @@ -2,14 +2,12 @@ import React, { useContext, useEffect, useRef, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { compact, first } from 'lodash' import cx from 'classnames' -import { EuiButtonIcon, EuiButton, EuiIcon, EuiLoadingSpinner, EuiText, EuiToolTip } from '@elastic/eui' import MonacoEditor, { monaco as monacoEditor } from 'react-monaco-editor' import { useParams } from 'react-router-dom' import { Theme, MonacoLanguage, - KEYBOARD_SHORTCUTS, DSLNaming, } from 'uiSrc/constants' import { @@ -21,13 +19,11 @@ import { getMonacoAction, getRedisCompletionProvider, getRedisSignatureHelpProvider, - isGroupMode, isParamsLine, MonacoAction, Nullable, toModelDeltaDecoration } from 'uiSrc/utils' -import { KeyboardShortcut } from 'uiSrc/components' import { ThemeContext } from 'uiSrc/contexts/themeContext' import { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands' import { IEditorMount, ISnippetController } from 'uiSrc/pages/workbench/interfaces' @@ -36,9 +32,9 @@ import { RunQueryMode, ResultsMode } from 'uiSrc/slices/interfaces/workbench' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { stopProcessing, workbenchResultsSelector } from 'uiSrc/slices/workbench/wb-results' import DedicatedEditor from 'uiSrc/components/monaco-editor/components/dedicated-editor' -import RawModeIcon from 'uiSrc/assets/img/icons/raw_mode.svg?react' -import GroupModeIcon from 'uiSrc/assets/img/icons/group_mode.svg?react' +import QueryActions from '../../query-actions' +import QueryTutorials from '../../query-tutorials' import styles from './styles.module.scss' export interface Props { @@ -47,7 +43,6 @@ export interface Props { resultsMode?: ResultsMode setQueryEl: Function setQuery: (script: string) => void - setIsCodeBtnDisabled: (value: boolean) => void onSubmit: (query?: string) => void onKeyDown?: (e: React.KeyboardEvent, script: string) => void onQueryChangeMode: () => void @@ -73,7 +68,6 @@ const Query = (props: Props) => { onKeyDown = () => {}, onSubmit = () => {}, setQueryEl = () => {}, - setIsCodeBtnDisabled = () => {}, onQueryChangeMode = () => {}, onChangeGroupMode = () => {} } = props @@ -91,7 +85,6 @@ const Query = (props: Props) => { const { items: execHistoryItems, loading, processing } = useSelector(workbenchResultsSelector) const { theme } = useContext(ThemeContext) const monacoObjects = useRef>(null) - const runTooltipRef = useRef(null) const { instanceId = '' } = useParams<{ instanceId: string }>() @@ -143,7 +136,6 @@ const Query = (props: Props) => { }, [query]) useEffect(() => { - setIsCodeBtnDisabled(isDedicatedEditorOpen) isDedicatedEditorOpenRef.current = isDedicatedEditorOpen }, [isDedicatedEditorOpen]) @@ -475,80 +467,19 @@ const Query = (props: Props) => { editorDidMount={editorDidMount} />
-
- - onQueryChangeMode()} - disabled={isLoading} - className={cx(styles.textBtn, { [styles.activeBtn]: activeMode === RunQueryMode.Raw })} - data-testid="btn-change-mode" - > - - - - - {`${KEYBOARD_SHORTCUTS.workbench.runQuery?.label}:\u00A0\u00A0`} - -
- ) - } - data-testid="run-query-tooltip" - > - <> - {isLoading && ( - - )} - { - handleSubmit() - setTimeout(() => runTooltipRef?.current?.hideToolTip?.(), 0) - }} - disabled={isLoading} - iconType="playFilled" - className={cx(styles.submitButton, { [styles.submitButtonLoading]: isLoading })} - aria-label="submit" - data-testid="btn-submit" - /> - - - - <> - onChangeGroupMode()} - disabled={isLoading} - className={cx(styles.textBtn, { [styles.activeBtn]: isGroupMode(resultsMode) })} - data-testid="btn-change-group-mode" - > - - - - +
+ +
+ {isDedicatedEditorOpen && ( div { + border: 1px solid var(--euiColorLightShade); + background-color: var(--euiColorEmptyShade); + padding: 8px 20px; + width: 100%; + } +} + +.input { + overflow: hidden; + flex-grow: 1; + width: 100%; + border: 1px solid var(--euiColorLightShade); + background-color: var(--rsInputColor); +} + +.queryFooter { + display: flex; + align-items: center; + justify-content: space-between; + + margin-top: 8px; + flex-shrink: 0; +} + +#script { + font: normal normal bold 14px/17px Inconsolata !important; + color: var(--textColorShade); + caret-color: var(--euiColorFullShade); + min-width: 5px; + display: inline; +} + diff --git a/redisinsight/ui/src/components/query/QueryWrapper.spec.tsx b/redisinsight/ui/src/pages/workbench/components/query/QueryWrapper.spec.tsx similarity index 100% rename from redisinsight/ui/src/components/query/QueryWrapper.spec.tsx rename to redisinsight/ui/src/pages/workbench/components/query/QueryWrapper.spec.tsx diff --git a/redisinsight/ui/src/components/query/QueryWrapper.tsx b/redisinsight/ui/src/pages/workbench/components/query/QueryWrapper.tsx similarity index 92% rename from redisinsight/ui/src/components/query/QueryWrapper.tsx rename to redisinsight/ui/src/pages/workbench/components/query/QueryWrapper.tsx index cab14f46bf..56a7d9e974 100644 --- a/redisinsight/ui/src/components/query/QueryWrapper.tsx +++ b/redisinsight/ui/src/pages/workbench/components/query/QueryWrapper.tsx @@ -13,7 +13,6 @@ export interface Props { resultsMode?: ResultsMode setQuery: (script: string) => void setQueryEl: Function - setIsCodeBtnDisabled: (value: boolean) => void onKeyDown?: (e: React.KeyboardEvent, script: string) => void onSubmit: (value?: string) => void onQueryChangeMode: () => void @@ -27,7 +26,6 @@ const QueryWrapper = (props: Props) => { resultsMode, setQuery, setQueryEl, - setIsCodeBtnDisabled, onKeyDown, onSubmit, onQueryChangeMode, @@ -53,7 +51,6 @@ const QueryWrapper = (props: Props) => { resultsMode={resultsMode} setQuery={setQuery} setQueryEl={setQueryEl} - setIsCodeBtnDisabled={setIsCodeBtnDisabled} onKeyDown={onKeyDown} onSubmit={onSubmit} onQueryChangeMode={onQueryChangeMode} diff --git a/redisinsight/ui/src/components/query/index.ts b/redisinsight/ui/src/pages/workbench/components/query/index.ts similarity index 100% rename from redisinsight/ui/src/components/query/index.ts rename to redisinsight/ui/src/pages/workbench/components/query/index.ts diff --git a/redisinsight/ui/src/pages/workbench/components/wb-results/WBResults/WBResults.tsx b/redisinsight/ui/src/pages/workbench/components/wb-results/WBResults/WBResults.tsx index 25edfe3dc4..00d4063f0c 100644 --- a/redisinsight/ui/src/pages/workbench/components/wb-results/WBResults/WBResults.tsx +++ b/redisinsight/ui/src/pages/workbench/components/wb-results/WBResults/WBResults.tsx @@ -11,7 +11,8 @@ import { Nullable } from 'uiSrc/utils' import QueryCard from 'uiSrc/components/query-card' import { CommandExecutionUI } from 'uiSrc/slices/interfaces' import { RunQueryMode, ResultsMode } from 'uiSrc/slices/interfaces/workbench' -import WbNoResultsMessage from 'uiSrc/pages/workbench/components/wb-no-results-message' + +import WbNoResultsMessage from '../../wb-no-results-message' import styles from './styles.module.scss' diff --git a/redisinsight/ui/src/pages/workbench/components/wb-view/WBView/WBView.tsx b/redisinsight/ui/src/pages/workbench/components/wb-view/WBView/WBView.tsx index d3f8a74239..1731efb148 100644 --- a/redisinsight/ui/src/pages/workbench/components/wb-view/WBView/WBView.tsx +++ b/redisinsight/ui/src/pages/workbench/components/wb-view/WBView/WBView.tsx @@ -1,10 +1,9 @@ -import React, { Ref, useCallback, useEffect, useRef, useState } from 'react' +import React, { Ref, useCallback, useEffect, useRef } from 'react' import { useDispatch, useSelector } from 'react-redux' import cx from 'classnames' import { isEmpty } from 'lodash' import { useParams } from 'react-router-dom' import { EuiResizableContainer } from '@elastic/eui' -import { monaco as monacoEditor } from 'react-monaco-editor' import { Maybe, @@ -12,7 +11,6 @@ import { getParsedParamsInQuery, getCommandsFromQuery } from 'uiSrc/utils' -import QueryWrapper from 'uiSrc/components/query' import { setWorkbenchVerticalPanelSizes, appContextWorkbench, @@ -25,7 +23,10 @@ import { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands' import { userSettingsConfigSelector } from 'uiSrc/slices/user/user-settings' import { PIPELINE_COUNT_DEFAULT } from 'uiSrc/constants/api' import { CodeButtonParams } from 'uiSrc/constants' + +import QueryWrapper from '../../query' import WBResultsWrapper from '../../wb-results' + import styles from './styles.module.scss' const verticalPanelIds = { @@ -41,7 +42,6 @@ export interface Props { isResultsLoaded: boolean setScript: (script: string) => void setScriptEl: Function - scriptEl: Nullable scrollDivRef: Ref activeMode: RunQueryMode resultsMode: ResultsMode @@ -71,7 +71,6 @@ const WBView = (props: Props) => { processing, setScript, setScriptEl, - scriptEl, activeMode, resultsMode, isResultsLoaded, @@ -94,8 +93,6 @@ const WBView = (props: Props) => { const { commandsArray: REDIS_COMMANDS_ARRAY } = useSelector(appRedisCommandsSelector) const { batchSize = PIPELINE_COUNT_DEFAULT } = useSelector(userSettingsConfigSelector) ?? {} - const [isCodeBtnDisabled, setIsCodeBtnDisabled] = useState(false) - const verticalSizesRef = useRef(vertical) const dispatch = useDispatch() @@ -178,7 +175,7 @@ const WBView = (props: Props) => { scrollable={false} className={styles.queryPanel} initialSize={vertical[verticalPanelIds.firstPanelId] ?? 20} - style={{ minHeight: '140px', zIndex: '8' }} + style={{ minHeight: '240px', zIndex: '8' }} > { resultsMode={resultsMode} setQuery={setScript} setQueryEl={setScriptEl} - setIsCodeBtnDisabled={setIsCodeBtnDisabled} onSubmit={handleSubmit} onQueryChangeMode={onQueryChangeMode} onChangeGroupMode={onChangeGroupMode} @@ -206,7 +202,7 @@ const WBView = (props: Props) => { initialSize={vertical[verticalPanelIds.secondPanelId] ?? 80} className={cx(styles.queryResults, styles.queryResultsPanel)} // Fix scroll on low height - 140px (queryPanel) - style={{ maxHeight: 'calc(100% - 140px)' }} + style={{ maxHeight: 'calc(100% - 240px)' }} > { script={script} setScript={setScript} setScriptEl={setScriptEl} - scriptEl={scriptEl} scrollDivRef={scrollDivRef} activeMode={activeRunQueryMode} onSubmit={sourceValueSubmit} From 64959bc4ee2b2e18cc42829c7a7dfafc7749031e Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Thu, 27 Jun 2024 15:35:02 +0200 Subject: [PATCH 003/112] #RI-5682 - remove fragment --- .../components/query-actions/QueryActions.tsx | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/redisinsight/ui/src/pages/workbench/components/query-actions/QueryActions.tsx b/redisinsight/ui/src/pages/workbench/components/query-actions/QueryActions.tsx index d8fc8b047a..2650352ccd 100644 --- a/redisinsight/ui/src/pages/workbench/components/query-actions/QueryActions.tsx +++ b/redisinsight/ui/src/pages/workbench/components/query-actions/QueryActions.tsx @@ -103,22 +103,20 @@ const QueryActions = (props: Props) => { content={isLoading ? 'Please wait while the commands are being executed…' : KeyBoardTooltipContent} data-testid="run-query-tooltip" > - <> - { - onSubmit() - setTimeout(() => runTooltipRef?.current?.hideToolTip?.(), 0) - }} - isLoading={isLoading} - disabled={isLoading} - iconType="playFilled" - className={cx(styles.btn, styles.submitButton)} - aria-label="submit" - data-testid="btn-submit" - > - Run - - + { + onSubmit() + setTimeout(() => runTooltipRef?.current?.hideToolTip?.(), 0) + }} + isLoading={isLoading} + disabled={isLoading} + iconType="playFilled" + className={cx(styles.btn, styles.submitButton)} + aria-label="submit" + data-testid="btn-submit" + > + Run + ) From 8186468e75080de2b5d8d238bb1ec16b32a9522a Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Thu, 27 Jun 2024 15:50:15 +0200 Subject: [PATCH 004/112] create structure for new Panel --- .../components/browser-tabs/BrowserTabs.tsx | 2 +- tests/e2e/helpers/constants.ts | 6 ++ .../pageObjects/components/insights-panel.ts | 2 +- .../components/keys-interaction-panel.ts | 56 +++++++++++++++++++ .../e2e/pageObjects/search-and-query-page.ts | 4 ++ 5 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 tests/e2e/pageObjects/components/keys-interaction-panel.ts create mode 100644 tests/e2e/pageObjects/search-and-query-page.ts diff --git a/redisinsight/ui/src/pages/keys/components/browser-tabs/BrowserTabs.tsx b/redisinsight/ui/src/pages/keys/components/browser-tabs/BrowserTabs.tsx index 6e34e2e159..4799a72bf5 100644 --- a/redisinsight/ui/src/pages/keys/components/browser-tabs/BrowserTabs.tsx +++ b/redisinsight/ui/src/pages/keys/components/browser-tabs/BrowserTabs.tsx @@ -43,7 +43,7 @@ const BrowserTabs = (props: Props) => { } return ( - + {tabs.map(({ id, title, page, onboard, isBeta }) => renderOnboardingTourWithChild( ( { + return this.activeTab.textContent; + } + + /** + * Click on Panel tab + * @param type of the tab + */ + async setActiveTab(type: KeysInteractionTabs): Promise { + const activeTabName = await this.getActiveTabName(); + + let tabSelector; + let pageClass; + + switch (type) { + case KeysInteractionTabs.BrowserAndFilter: + tabSelector = this.browserTab; + pageClass = BrowserPage; + break; + case KeysInteractionTabs.Workbench: + tabSelector = this.workbenchTab; + pageClass = WorkbenchPage; + break; + case KeysInteractionTabs.SearchAndQuery: + tabSelector = this.searchTab; + pageClass = SearchAndQueryPage; + break; + default: + throw new Error(`Unknown tab type: ${type}`); + } + + if (type !== activeTabName) { + await t.click(tabSelector); + } + + return new pageClass(); + } +} diff --git a/tests/e2e/pageObjects/search-and-query-page.ts b/tests/e2e/pageObjects/search-and-query-page.ts new file mode 100644 index 0000000000..289e8fb1ec --- /dev/null +++ b/tests/e2e/pageObjects/search-and-query-page.ts @@ -0,0 +1,4 @@ +import { InstancePage } from './instance-page'; + +export class SearchAndQueryPage extends InstancePage { +} From 91ed43be409527cbf14f019f5349b86020e2681b Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Fri, 28 Jun 2024 08:05:43 +0200 Subject: [PATCH 005/112] #RI-5682 - move tutorials to enum --- .../components/expert-chat/ExpertChat.tsx | 4 ++-- redisinsight/ui/src/constants/browser.ts | 2 -- redisinsight/ui/src/constants/index.ts | 1 + redisinsight/ui/src/constants/tutorials.ts | 11 +++++++++++ .../browser/components/no-keys-found/NoKeysFound.tsx | 6 ++++-- .../capability-promotion/CapabilityPromotion.tsx | 3 ++- .../components/query-tutorials/QueryTutorials.tsx | 7 ++++--- 7 files changed, 24 insertions(+), 10 deletions(-) create mode 100644 redisinsight/ui/src/constants/tutorials.ts diff --git a/redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/expert-chat/ExpertChat.tsx b/redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/expert-chat/ExpertChat.tsx index ae3f60d3b8..0dd2cf4048 100644 --- a/redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/expert-chat/ExpertChat.tsx +++ b/redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/expert-chat/ExpertChat.tsx @@ -19,7 +19,7 @@ import { oauthCloudUserSelector } from 'uiSrc/slices/oauth/cloud' import { fetchRedisearchListAction } from 'uiSrc/slices/browser/redisearch' import TelescopeImg from 'uiSrc/assets/img/telescope-dark.svg?react' import { openTutorialByPath } from 'uiSrc/slices/panels/sidePanels' -import { SAMPLE_DATA_TUTORIAL } from 'uiSrc/constants' +import { TutorialsIds } from 'uiSrc/constants' import NoIndexesInitialMessage from './components/no-indexes-initial-message' import ExpertChatHeader from './components/expert-chat-header' @@ -157,7 +157,7 @@ const ExpertChat = () => { }, []) const handleClickTutorial = () => { - const tutorialPath = findTutorialPath({ id: SAMPLE_DATA_TUTORIAL }) + const tutorialPath = findTutorialPath({ id: TutorialsIds.RedisUseCases }) dispatch(openTutorialByPath(tutorialPath, history, true)) sendEventTelemetry({ diff --git a/redisinsight/ui/src/constants/browser.ts b/redisinsight/ui/src/constants/browser.ts index 49d2912cdb..453f28dd97 100644 --- a/redisinsight/ui/src/constants/browser.ts +++ b/redisinsight/ui/src/constants/browser.ts @@ -1,7 +1,5 @@ import { KeyValueFormat, SortOrder } from './keys' -export const SAMPLE_DATA_TUTORIAL = 'redis_use_cases' - export const DEFAULT_DELIMITER = ':' export const DEFAULT_TREE_SORTING = SortOrder.ASC export const DEFAULT_SHOW_HIDDEN_RECOMMENDATIONS = false diff --git a/redisinsight/ui/src/constants/index.ts b/redisinsight/ui/src/constants/index.ts index 0e40368cc8..a0050a56d9 100644 --- a/redisinsight/ui/src/constants/index.ts +++ b/redisinsight/ui/src/constants/index.ts @@ -31,4 +31,5 @@ export * from './customErrorCodes' export * from './securityField' export * from './redisearch' export * from './browser/keyDetailsHeader' +export * from './tutorials' export { ApiEndpoints, BrowserStorageItem, ApiStatusCode, apiErrors } diff --git a/redisinsight/ui/src/constants/tutorials.ts b/redisinsight/ui/src/constants/tutorials.ts new file mode 100644 index 0000000000..45c856417e --- /dev/null +++ b/redisinsight/ui/src/constants/tutorials.ts @@ -0,0 +1,11 @@ +enum TutorialsIds { + IntroToSearch = 'sq-intro', + IntroToJSON = 'ds-json-intro', + BasicRedisUseCases = 'redis_use_cases_basic', + RedisUseCases = 'redis_use_cases', + IntroVectorSearch = 'vss-intro', +} + +export { + TutorialsIds +} diff --git a/redisinsight/ui/src/pages/browser/components/no-keys-found/NoKeysFound.tsx b/redisinsight/ui/src/pages/browser/components/no-keys-found/NoKeysFound.tsx index d783dadacf..6ef178f384 100644 --- a/redisinsight/ui/src/pages/browser/components/no-keys-found/NoKeysFound.tsx +++ b/redisinsight/ui/src/pages/browser/components/no-keys-found/NoKeysFound.tsx @@ -15,8 +15,10 @@ import { SidePanels } from 'uiSrc/slices/interfaces/insights' import { KeyViewType, SearchMode } from 'uiSrc/slices/interfaces/keys' import { changeKeyViewType, fetchKeys, keysSelector } from 'uiSrc/slices/browser/keys' import { SCAN_TREE_COUNT_DEFAULT } from 'uiSrc/constants/api' -import { SAMPLE_DATA_TUTORIAL } from 'uiSrc/constants' +import { TutorialsIds } from 'uiSrc/constants' + import LoadSampleData from '../load-sample-data' + import styles from './styles.module.scss' export interface Props { @@ -33,7 +35,7 @@ const NoKeysFound = (props: Props) => { const onSuccessLoadData = () => { if (openedPanel !== SidePanels.AiAssistant) { - const tutorialPath = findTutorialPath({ id: SAMPLE_DATA_TUTORIAL }) + const tutorialPath = findTutorialPath({ id: TutorialsIds.RedisUseCases }) dispatch(openTutorialByPath(tutorialPath, history, true)) } diff --git a/redisinsight/ui/src/pages/home/components/capability-promotion/CapabilityPromotion.tsx b/redisinsight/ui/src/pages/home/components/capability-promotion/CapabilityPromotion.tsx index 2ca214e307..ba9fc8120c 100644 --- a/redisinsight/ui/src/pages/home/components/capability-promotion/CapabilityPromotion.tsx +++ b/redisinsight/ui/src/pages/home/components/capability-promotion/CapabilityPromotion.tsx @@ -15,6 +15,7 @@ import { guideLinksSelector } from 'uiSrc/slices/content/guide-links' import GUIDE_ICONS from 'uiSrc/components/explore-guides/icons' import { findTutorialPath } from 'uiSrc/utils' import { InsightsPanelTabs, SidePanels } from 'uiSrc/slices/interfaces/insights' +import { TutorialsIds } from 'uiSrc/constants' import styles from './styles.module.scss' export interface Props { @@ -23,7 +24,7 @@ export interface Props { capabilityIds?: string[] } -const displayedCapabilityIds = ['sq-intro', 'ds-json-intro'] +const displayedCapabilityIds = [TutorialsIds.IntroToSearch, TutorialsIds.IntroToJSON] const CapabilityPromotion = (props: Props) => { const { mode = 'wide', wrapperClassName, capabilityIds = displayedCapabilityIds } = props diff --git a/redisinsight/ui/src/pages/workbench/components/query-tutorials/QueryTutorials.tsx b/redisinsight/ui/src/pages/workbench/components/query-tutorials/QueryTutorials.tsx index b009b6b3e9..83e765da76 100644 --- a/redisinsight/ui/src/pages/workbench/components/query-tutorials/QueryTutorials.tsx +++ b/redisinsight/ui/src/pages/workbench/components/query-tutorials/QueryTutorials.tsx @@ -4,6 +4,7 @@ import { EuiLink, EuiText } from '@elastic/eui' import { useDispatch } from 'react-redux' import { useHistory, useParams } from 'react-router-dom' import { findTutorialPath } from 'uiSrc/utils' +import { TutorialsIds } from 'uiSrc/constants' import { openTutorialByPath } from 'uiSrc/slices/panels/sidePanels' import { sendEventTelemetry, TELEMETRY_EMPTY_VALUE, TelemetryEvent } from 'uiSrc/telemetry' @@ -11,15 +12,15 @@ import styles from './styles.module.scss' const TUTORIALS = [ { - id: 'sq-intro', + id: TutorialsIds.IntroToSearch, title: 'Intro to search' }, { - id: 'redis_use_cases_basic', + id: TutorialsIds.BasicRedisUseCases, title: 'Basic use cases' }, { - id: 'vss-intro', + id: TutorialsIds.IntroVectorSearch, title: 'Intro to vector search' }, ] From 401be0e22c3538c526c0d12cc789106691f32b7b Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Fri, 28 Jun 2024 10:50:32 +0200 Subject: [PATCH 006/112] remove RDI --- .circleci/config.yml | 8 ++++---- tests/e2e/docker.web.docker-compose.yml | 2 +- tests/e2e/rte.docker-compose.yml | 18 +++++++++--------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 813c1d7a8d..c749db5e03 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -391,10 +391,10 @@ jobs: - attach_workspace: at: . - run: sudo apt-get install net-tools - - run: - name: Clone mocked RDI server - command: | - git clone https://$GH_RDI_MOCKED_SERVER_KEY@github.com/RedisInsight/RDI_server_mocked.git tests/e2e/rte/rdi + #- run: + # name: Clone mocked RDI server + # command: | + # git clone https://$GH_RDI_MOCKED_SERVER_KEY@github.com/RedisInsight/RDI_server_mocked.git tests/e2e/rte/rdi - run: name: .AppImage tests command: | diff --git a/tests/e2e/docker.web.docker-compose.yml b/tests/e2e/docker.web.docker-compose.yml index b406942cb8..d9dcaaee61 100644 --- a/tests/e2e/docker.web.docker-compose.yml +++ b/tests/e2e/docker.web.docker-compose.yml @@ -13,7 +13,7 @@ services: - rihomedir:/root/.redis-insight - tmp:/tmp - ./remote:/root/remote - - ./rdi:/root/rdi + #- ./rdi:/root/rdi env_file: - ./.env entrypoint: [ diff --git a/tests/e2e/rte.docker-compose.yml b/tests/e2e/rte.docker-compose.yml index a86fa54484..4aedbfda13 100644 --- a/tests/e2e/rte.docker-compose.yml +++ b/tests/e2e/rte.docker-compose.yml @@ -12,15 +12,15 @@ services: ports: - 5551:5551 # RDI mocked - rdi: - logging: *logging - build: - context: rte/rdi - dockerfile: Dockerfile - volumes: - - ./rdi:/data - ports: - - 4000:4000 + #rdi: + # logging: *logging + # build: + # context: rte/rdi + # dockerfile: Dockerfile + #volumes: + # - ./rdi:/data + # ports: + # - 4000:4000 # ssh ssh: logging: *logging From c56140b2fa83974b32192cb7fd31d5abb7ba215f Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Fri, 28 Jun 2024 11:06:16 +0200 Subject: [PATCH 007/112] fix for rdi --- .circleci/config.yml | 8 ++++---- tests/e2e/local.web.docker-compose.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c749db5e03..9cd941dd61 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -487,10 +487,10 @@ jobs: name: Load built docker image from workspace command: | docker image load -i /tmp/release/docker/docker-linux-alpine.amd64.tar - - run: - name: Clone mocked RDI server - command: | - git clone https://$GH_RDI_MOCKED_SERVER_KEY@github.com/RedisInsight/RDI_server_mocked.git tests/e2e/rte/rdi + # - run: + # name: Clone mocked RDI server + # command: | + # git clone https://$GH_RDI_MOCKED_SERVER_KEY@github.com/RedisInsight/RDI_server_mocked.git tests/e2e/rte/rdi - run: name: Run tests command: | diff --git a/tests/e2e/local.web.docker-compose.yml b/tests/e2e/local.web.docker-compose.yml index b093f1e149..69db32bc4a 100644 --- a/tests/e2e/local.web.docker-compose.yml +++ b/tests/e2e/local.web.docker-compose.yml @@ -13,7 +13,7 @@ services: - rihomedir:/root/.redis-insight - tmp:/tmp - ./remote:/root/remote - - ./rdi:/root/rdi + # - ./rdi:/root/rdi env_file: - ./.env entrypoint: [ From 58c6252aa4b1e761fc9f330543a81c9f31a93d23 Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Sat, 29 Jun 2024 13:22:30 +0200 Subject: [PATCH 008/112] fix tests part #1 --- tests/e2e/pageObjects/browser-page.ts | 2 ++ .../enablement-area-autoupdate.e2e.ts | 8 +++++--- .../critical-path/monitor/monitor.e2e.ts | 4 ++-- .../critical-path/workbench/index-schema.e2e.ts | 8 +++++--- .../workbench/json-workbench.e2e.ts | 8 +++++--- .../workbench/redis-stack-commands.e2e.ts | 8 +++++--- .../workbench/workbench-re-cluster.e2e.ts | 8 +++++--- .../critical-path/browser/bulk-delete.e2e.ts | 6 +++--- .../browser/search-capabilities.e2e.ts | 7 +++---- .../database-overview/database-index.e2e.ts | 4 ++-- .../database-overview/database-overview.e2e.ts | 5 +++-- .../memory-efficiency/memory-efficiency.e2e.ts | 2 +- .../web/critical-path/monitor/monitor.e2e.ts | 7 +++---- .../pub-sub/subscribe-unsubscribe.e2e.ts | 8 +++++--- .../critical-path/workbench/autocomplete.e2e.ts | 8 +++++--- .../workbench/command-results.e2e.ts | 8 +++++--- .../web/critical-path/workbench/context.e2e.ts | 12 +++++++----- .../web/critical-path/workbench/cypher.e2e.ts | 8 +++++--- .../workbench/default-scripts-area.e2e.ts | 9 ++++++--- .../workbench/scripting-area.e2e.ts | 8 +++++--- .../tests/web/regression/browser/context.e2e.ts | 9 ++++----- .../browser/keys-all-databases.e2e.ts | 6 +++--- .../web/regression/browser/onboarding.e2e.ts | 6 +++--- .../regression/browser/resize-columns.e2e.ts | 6 +++--- .../web/regression/browser/survey-link.e2e.ts | 4 ++-- .../web/regression/cli/cli-promote-workbench.ts | 5 +++-- .../database-overview/database-info.e2e.ts | 4 ++-- .../database-overview/database-overview.e2e.ts | 4 ++-- .../tests/web/regression/database/github.e2e.ts | 9 +++++---- .../regression/insights/import-tutorials.e2e.ts | 17 ++++++++++++----- .../insights/live-recommendations.e2e.ts | 6 ++++-- .../insights/open-insights-panel.e2e.ts | 4 ++-- .../tests/web/regression/monitor/monitor.e2e.ts | 4 ++-- .../regression/workbench/autocomplete.e2e.ts | 8 ++++---- .../workbench/autoexecute-button.e2e.ts | 8 ++++---- .../regression/workbench/command-results.e2e.ts | 9 +++++---- .../web/regression/workbench/context.e2e.ts | 8 ++++---- .../web/regression/workbench/cypher.e2e.ts | 8 ++++---- .../workbench/default-scripts-area.e2e.ts | 6 +++--- .../regression/workbench/editor-cleanup.e2e.ts | 10 ++++++---- .../workbench/empty-command-history.e2e.ts | 8 ++++---- .../web/regression/workbench/group-mode.e2e.ts | 8 ++++---- .../workbench/history-of-results.e2e.ts | 8 ++++---- .../web/regression/workbench/raw-mode.e2e.ts | 13 +++++++------ .../workbench/redis-stack-commands.e2e.ts | 8 ++++---- .../redisearch-module-not-available.e2e.ts | 8 ++++---- .../regression/workbench/scripting-area.e2e.ts | 13 ++++++++----- .../workbench/workbench-all-db-types.e2e.ts | 8 +++++--- .../workbench/workbench-non-auto-guides.e2e.ts | 9 +++++---- .../workbench/workbench-pipeline.e2e.ts | 17 +++++++++++------ .../web/smoke/workbench/json-workbench.e2e.ts | 8 ++++---- .../web/smoke/workbench/scripting-area.e2e.ts | 8 ++++---- 52 files changed, 223 insertions(+), 172 deletions(-) diff --git a/tests/e2e/pageObjects/browser-page.ts b/tests/e2e/pageObjects/browser-page.ts index 2dab013eb3..051db56250 100644 --- a/tests/e2e/pageObjects/browser-page.ts +++ b/tests/e2e/pageObjects/browser-page.ts @@ -2,10 +2,12 @@ import { t, Selector } from 'testcafe'; import { Common } from '../helpers/common'; import { InstancePage } from './instance-page'; import { BulkActions, TreeView } from './components/browser'; +import { KeysInteractionPanel } from './components/keys-interaction-panel'; export class BrowserPage extends InstancePage { BulkActions = new BulkActions(); TreeView = new TreeView(); + KeysInteractionPanel = new KeysInteractionPanel(); //CSS Selectors cssSelectorGrid = '[aria-label="grid"]'; diff --git a/tests/e2e/tests/electron/critical-path/files-auto-update/enablement-area-autoupdate.e2e.ts b/tests/e2e/tests/electron/critical-path/files-auto-update/enablement-area-autoupdate.e2e.ts index c12367a191..a24e178b01 100644 --- a/tests/e2e/tests/electron/critical-path/files-auto-update/enablement-area-autoupdate.e2e.ts +++ b/tests/e2e/tests/electron/critical-path/files-auto-update/enablement-area-autoupdate.e2e.ts @@ -3,15 +3,16 @@ import * as fs from 'fs'; import * as editJsonFile from 'edit-json-file'; import { DatabaseHelper } from '../../../../helpers/database'; -import { MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; +import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig, workingDirectory } from '../../../../helpers/conf'; -import { ExploreTabs, rte } from '../../../../helpers/constants'; +import { ExploreTabs, KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); +const browserPage = new BrowserPage(); if (fs.existsSync(workingDirectory)) { @@ -46,7 +47,8 @@ if (fs.existsSync(workingDirectory)) { const tutorialsTimestampFileNew = editJsonFile(tutorialsTimestampPath); // Open Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await t.click(myRedisDatabasePage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); // Check Enablement area and validate that removed file is existed in Guides await workbenchPage.InsightsPanel.togglePanel(true); diff --git a/tests/e2e/tests/electron/critical-path/monitor/monitor.e2e.ts b/tests/e2e/tests/electron/critical-path/monitor/monitor.e2e.ts index b79adef488..26613c2564 100644 --- a/tests/e2e/tests/electron/critical-path/monitor/monitor.e2e.ts +++ b/tests/e2e/tests/electron/critical-path/monitor/monitor.e2e.ts @@ -5,7 +5,7 @@ import { BrowserPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; import { APIKeyRequests } from '../../../../helpers/api/api-keys'; @@ -65,7 +65,7 @@ test('Verify that user can see the list of all commands from all clients ran for await browserPage.addHashKey(keyName); await browserPage.Profiler.checkCommandInMonitorResults(browser_command); //Open Workbench page to create new client - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); //Send command in Workbench await workbenchPage.sendCommandInWorkbench(workbench_command); //Check that command from Workbench is displayed in monitor diff --git a/tests/e2e/tests/electron/critical-path/workbench/index-schema.e2e.ts b/tests/e2e/tests/electron/critical-path/workbench/index-schema.e2e.ts index a9dc8ae6bd..98e1f7f811 100644 --- a/tests/e2e/tests/electron/critical-path/workbench/index-schema.e2e.ts +++ b/tests/e2e/tests/electron/critical-path/workbench/index-schema.e2e.ts @@ -1,6 +1,6 @@ -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; -import { MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; +import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneRedisearch } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; @@ -9,6 +9,7 @@ const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); +const browserPage = new BrowserPage(); let indexName = Common.generateWord(5); @@ -18,7 +19,8 @@ fixture `Index Schema at Workbench` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await t.click(myRedisDatabasePage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench);; }) .afterEach(async t => { // Drop index, documents and database diff --git a/tests/e2e/tests/electron/critical-path/workbench/json-workbench.e2e.ts b/tests/e2e/tests/electron/critical-path/workbench/json-workbench.e2e.ts index cfac929e85..f7d44417fe 100644 --- a/tests/e2e/tests/electron/critical-path/workbench/json-workbench.e2e.ts +++ b/tests/e2e/tests/electron/critical-path/workbench/json-workbench.e2e.ts @@ -1,6 +1,6 @@ -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; -import { MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; +import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneRedisearch } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; @@ -9,6 +9,7 @@ const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); +const browserPage = new BrowserPage(); let indexName = Common.generateWord(5); @@ -18,7 +19,8 @@ fixture `JSON verifications at Workbench` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await t.click(myRedisDatabasePage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); }) .afterEach(async t => { // Drop index, documents and database diff --git a/tests/e2e/tests/electron/regression/workbench/redis-stack-commands.e2e.ts b/tests/e2e/tests/electron/regression/workbench/redis-stack-commands.e2e.ts index 3215ef5ce7..71ab459442 100644 --- a/tests/e2e/tests/electron/regression/workbench/redis-stack-commands.e2e.ts +++ b/tests/e2e/tests/electron/regression/workbench/redis-stack-commands.e2e.ts @@ -1,14 +1,15 @@ import { t } from 'testcafe'; import { DatabaseHelper } from '../../../../helpers/database'; -import { WorkbenchPage, MyRedisDatabasePage } from '../../../../pageObjects'; +import { WorkbenchPage, MyRedisDatabasePage, BrowserPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; -import { ExploreTabs, rte } from '../../../../helpers/constants'; +import { ExploreTabs, KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); +const browserPage = new BrowserPage(); const keyNameGraph = 'bikes_graph'; @@ -17,7 +18,8 @@ fixture `Redis Stack command in Workbench` .page(commonUrl) .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await t.click(myRedisDatabasePage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); }) .afterEach(async() => { // Drop key and database diff --git a/tests/e2e/tests/electron/regression/workbench/workbench-re-cluster.e2e.ts b/tests/e2e/tests/electron/regression/workbench/workbench-re-cluster.e2e.ts index afb39c0ce2..b6f207a0db 100644 --- a/tests/e2e/tests/electron/regression/workbench/workbench-re-cluster.e2e.ts +++ b/tests/e2e/tests/electron/regression/workbench/workbench-re-cluster.e2e.ts @@ -1,12 +1,13 @@ import { t } from 'testcafe'; -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; -import { MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; +import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, redisEnterpriseClusterConfig } from '../../../../helpers/conf'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); const databaseHelper = new DatabaseHelper(); +const browserPage = new BrowserPage(); const commandForSend1 = 'info'; const commandForSend2 = 'FT._LIST'; @@ -17,7 +18,8 @@ const verifyCommandsInWorkbench = async(): Promise => { 'FT.SEARCH idx *' ]; - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await t.click(myRedisDatabasePage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); // Send commands await workbenchPage.sendCommandInWorkbench(commandForSend1); await workbenchPage.sendCommandInWorkbench(commandForSend2); diff --git a/tests/e2e/tests/web/critical-path/browser/bulk-delete.e2e.ts b/tests/e2e/tests/web/critical-path/browser/bulk-delete.e2e.ts index a6c48ced1b..af77a10ed5 100644 --- a/tests/e2e/tests/web/critical-path/browser/bulk-delete.e2e.ts +++ b/tests/e2e/tests/web/critical-path/browser/bulk-delete.e2e.ts @@ -1,4 +1,4 @@ -import { KeyTypesTexts, rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, KeyTypesTexts, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneRedisearch } from '../../../../helpers/conf'; @@ -99,9 +99,9 @@ test await t.click(browserPage.bulkActionsButton); await browserPage.BulkActions.startBulkDelete(); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); // Go to Browser Page - await t.click(myRedisDatabasePage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.BrowserAndFilter); await t.expect(browserPage.BulkActions.bulkStatusInProgress.exists).ok('Progress value not displayed', { timeout: 5000 }); }); test diff --git a/tests/e2e/tests/web/critical-path/browser/search-capabilities.e2e.ts b/tests/e2e/tests/web/critical-path/browser/search-capabilities.e2e.ts index 1728346db0..3aa3195286 100644 --- a/tests/e2e/tests/web/critical-path/browser/search-capabilities.e2e.ts +++ b/tests/e2e/tests/web/critical-path/browser/search-capabilities.e2e.ts @@ -7,7 +7,7 @@ import { ossStandaloneConfig, ossStandaloneV5Config } from '../../../../helpers/conf'; -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; import { verifyKeysDisplayingInTheList } from '../../../../helpers/keys'; @@ -242,9 +242,8 @@ test await t.click(browserPage.getKeySelectorByName(keyName)); // Verify that Redisearch context (inputs, key selected, scroll, key details) saved after switching between pages - await t - .click(myRedisDatabasePage.NavigationPanel.workbenchButton) - .click(myRedisDatabasePage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.BrowserAndFilter); await verifyContext(); // Verify that Redisearch context saved when switching between browser/tree view diff --git a/tests/e2e/tests/web/critical-path/database-overview/database-index.e2e.ts b/tests/e2e/tests/web/critical-path/database-overview/database-index.e2e.ts index 419e1199f9..c87bbcb487 100644 --- a/tests/e2e/tests/web/critical-path/database-overview/database-index.e2e.ts +++ b/tests/e2e/tests/web/critical-path/database-overview/database-index.e2e.ts @@ -1,5 +1,5 @@ import { DatabaseHelper } from '../../../../helpers/database'; -import { KeyTypesTexts, rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, KeyTypesTexts, rte } from '../../../../helpers/constants'; import { Common } from '../../../../helpers/common'; import { MyRedisDatabasePage, @@ -105,7 +105,7 @@ test('Switching between indexed databases', async t => { await verifySearchFilterValue('Hall School'); // Open Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); await workbenchPage.sendCommandInWorkbench(command); // Verify that user can see the database index before the command name executed in Workbench await workbenchPage.checkWorkbenchCommandResult(`[db1] ${command}`, '8'); diff --git a/tests/e2e/tests/web/critical-path/database-overview/database-overview.e2e.ts b/tests/e2e/tests/web/critical-path/database-overview/database-overview.e2e.ts index 5e25f6d550..b24400f5e0 100644 --- a/tests/e2e/tests/web/critical-path/database-overview/database-overview.e2e.ts +++ b/tests/e2e/tests/web/critical-path/database-overview/database-overview.e2e.ts @@ -1,6 +1,6 @@ import { Chance } from 'chance'; import { DatabaseHelper } from '../../../../helpers/database'; -import { ExploreTabs, rte } from '../../../../helpers/constants'; +import { ExploreTabs, KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { Common } from '../../../../helpers/common'; import { MyRedisDatabasePage, @@ -145,7 +145,8 @@ test }) .after(async t => { //Delete database and index - await t.click(browserPage.NavigationPanel.workbenchButton); + await t.click(myRedisDatabasePage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); await workbenchPage.sendCommandInWorkbench('FT.DROPINDEX idx:schools DD'); await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig); })('Verify that user can see additional information in Overview: Connected Clients, Commands/Sec, CPU (%) using Standalone DB connection type', async t => { diff --git a/tests/e2e/tests/web/critical-path/memory-efficiency/memory-efficiency.e2e.ts b/tests/e2e/tests/web/critical-path/memory-efficiency/memory-efficiency.e2e.ts index 26b988f70d..0f3079beb6 100644 --- a/tests/e2e/tests/web/critical-path/memory-efficiency/memory-efficiency.e2e.ts +++ b/tests/e2e/tests/web/critical-path/memory-efficiency/memory-efficiency.e2e.ts @@ -278,7 +278,7 @@ test } // Verify that specific report is saved as context await t.click(memoryEfficiencyPage.reportItem.nth(3)); - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await t.click(myRedisDatabasePage.NavigationPanel.browserButton); await t.click(myRedisDatabasePage.NavigationPanel.analysisPageButton); await t.expect(memoryEfficiencyPage.donutTotalKeys.sibling(1).textContent).eql(numberOfKeys[2], 'Context is not saved'); // Verify that user can see top keys table saved as context diff --git a/tests/e2e/tests/web/critical-path/monitor/monitor.e2e.ts b/tests/e2e/tests/web/critical-path/monitor/monitor.e2e.ts index b79adef488..c9c1435170 100644 --- a/tests/e2e/tests/web/critical-path/monitor/monitor.e2e.ts +++ b/tests/e2e/tests/web/critical-path/monitor/monitor.e2e.ts @@ -5,12 +5,11 @@ import { BrowserPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; import { APIKeyRequests } from '../../../../helpers/api/api-keys'; -const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); const browserPage = new BrowserPage(); const databaseHelper = new DatabaseHelper(); @@ -47,7 +46,7 @@ test('Verify that user can work with Monitor', async t => { await browserPage.Cli.getSuccessCommandResultFromCli(`${command} ${keyName} ${keyValue}`); await browserPage.Profiler.checkCommandInMonitorResults(command, [keyName, keyValue]); }); -test('Verify that user can see the list of all commands from all clients ran for this Redis database in the list of results in Monitor', async t => { +test.only('Verify that user can see the list of all commands from all clients ran for this Redis database in the list of results in Monitor', async t => { //Define commands in different clients const cli_command = 'command'; const workbench_command = 'hello'; @@ -65,7 +64,7 @@ test('Verify that user can see the list of all commands from all clients ran for await browserPage.addHashKey(keyName); await browserPage.Profiler.checkCommandInMonitorResults(browser_command); //Open Workbench page to create new client - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); //Send command in Workbench await workbenchPage.sendCommandInWorkbench(workbench_command); //Check that command from Workbench is displayed in monitor diff --git a/tests/e2e/tests/web/critical-path/pub-sub/subscribe-unsubscribe.e2e.ts b/tests/e2e/tests/web/critical-path/pub-sub/subscribe-unsubscribe.e2e.ts index f913c1b60f..74b3bdc5a2 100644 --- a/tests/e2e/tests/web/critical-path/pub-sub/subscribe-unsubscribe.e2e.ts +++ b/tests/e2e/tests/web/critical-path/pub-sub/subscribe-unsubscribe.e2e.ts @@ -1,7 +1,7 @@ import { DatabaseHelper } from '../../../../helpers/database'; -import { MyRedisDatabasePage, PubSubPage, WorkbenchPage } from '../../../../pageObjects'; +import { BrowserPage, MyRedisDatabasePage, PubSubPage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig, ossStandaloneV5Config } from '../../../../helpers/conf'; -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { verifyMessageDisplayingInPubSub } from '../../../../helpers/pub-sub'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; @@ -10,6 +10,7 @@ const pubSubPage = new PubSubPage(); const workbenchPage = new WorkbenchPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); +const browserPage = new BrowserPage(); fixture `Subscribe/Unsubscribe from a channel` .meta({ rte: rte.standalone, type: 'critical_path' }) @@ -120,7 +121,8 @@ test('Verify that user can see a internal link to pubsub window under word “Pu await t.expect(pubSubPage.pubSubPageContainer.exists).ok('Pubsub page is opened'); // Verify that user can see a custom message when he tries to run SUBSCRIBE command in Workbench: “Use Pub/Sub tool to subscribe to channels.” - await t.click(pubSubPage.NavigationPanel.workbenchButton); + await t.click(pubSubPage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); await workbenchPage.sendCommandInWorkbench(commandSecond); await t.expect(await workbenchPage.queryResult.textContent).eql('Use Pub/Sub tool to subscribe to channels.', 'Message is not displayed', { timeout: 10000 }); diff --git a/tests/e2e/tests/web/critical-path/workbench/autocomplete.e2e.ts b/tests/e2e/tests/web/critical-path/workbench/autocomplete.e2e.ts index ca1076997c..6bcb31367c 100644 --- a/tests/e2e/tests/web/critical-path/workbench/autocomplete.e2e.ts +++ b/tests/e2e/tests/web/critical-path/workbench/autocomplete.e2e.ts @@ -1,6 +1,6 @@ -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; -import { MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; +import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; @@ -8,6 +8,7 @@ const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); +const browserPage = new BrowserPage(); fixture `Autocomplete for entered commands` .meta({ type: 'critical_path', rte: rte.standalone }) @@ -15,7 +16,8 @@ fixture `Autocomplete for entered commands` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await t.click(myRedisDatabasePage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); }) .afterEach(async() => { // Delete database diff --git a/tests/e2e/tests/web/critical-path/workbench/command-results.e2e.ts b/tests/e2e/tests/web/critical-path/workbench/command-results.e2e.ts index ac4e43621b..bb954b91b1 100644 --- a/tests/e2e/tests/web/critical-path/workbench/command-results.e2e.ts +++ b/tests/e2e/tests/web/critical-path/workbench/command-results.e2e.ts @@ -1,6 +1,6 @@ -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; -import { MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; +import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; @@ -9,6 +9,7 @@ const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); +const browserPage = new BrowserPage(); const commandForSend1 = 'info'; const commandForSend2 = 'FT._LIST'; @@ -20,7 +21,8 @@ fixture `Command results at Workbench` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await t.click(myRedisDatabasePage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); }) .afterEach(async t => { await t.switchToMainWindow(); diff --git a/tests/e2e/tests/web/critical-path/workbench/context.e2e.ts b/tests/e2e/tests/web/critical-path/workbench/context.e2e.ts index ba1892f705..10038dd65c 100644 --- a/tests/e2e/tests/web/critical-path/workbench/context.e2e.ts +++ b/tests/e2e/tests/web/critical-path/workbench/context.e2e.ts @@ -1,6 +1,6 @@ -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; -import { MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; +import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; @@ -9,6 +9,7 @@ const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); +const browserPage = new BrowserPage(); const speed = 0.4; let indexName = Common.generateWord(5); @@ -19,7 +20,8 @@ fixture `Workbench Context` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await t.click(myRedisDatabasePage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); }) .afterEach(async() => { // Drop index, documents and database @@ -31,8 +33,8 @@ test('Verify that user can see saved input in Editor when navigates away to any const command = `FT.CREATE ${indexName} ON HASH PREFIX 1 product: SCHEMA name TEXT`; // Enter the command in the Workbench editor and navigate to Browser await t.typeText(workbenchPage.queryInput, command, { replace: true, speed: speed }); - await t.click(myRedisDatabasePage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.BrowserAndFilter); // Return back to Workbench and check input in editor - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); await t.expect((await workbenchPage.queryInputScriptArea.textContent).replace(/\s/g, ' ')).eql(command, 'Input in Editor is saved'); }); diff --git a/tests/e2e/tests/web/critical-path/workbench/cypher.e2e.ts b/tests/e2e/tests/web/critical-path/workbench/cypher.e2e.ts index 6e9a3ce63f..1dc48e7762 100644 --- a/tests/e2e/tests/web/critical-path/workbench/cypher.e2e.ts +++ b/tests/e2e/tests/web/critical-path/workbench/cypher.e2e.ts @@ -1,6 +1,6 @@ -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; -import { MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; +import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; @@ -8,6 +8,7 @@ const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); +const browserPage = new BrowserPage(); fixture `Cypher syntax at Workbench` .meta({ type: 'critical_path', rte: rte.standalone }) @@ -15,7 +16,8 @@ fixture `Cypher syntax at Workbench` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await t.click(myRedisDatabasePage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); }) .afterEach(async() => { // Drop database diff --git a/tests/e2e/tests/web/critical-path/workbench/default-scripts-area.e2e.ts b/tests/e2e/tests/web/critical-path/workbench/default-scripts-area.e2e.ts index 1791ce82c4..7cd1f5777e 100644 --- a/tests/e2e/tests/web/critical-path/workbench/default-scripts-area.e2e.ts +++ b/tests/e2e/tests/web/critical-path/workbench/default-scripts-area.e2e.ts @@ -1,7 +1,7 @@ import { Chance } from 'chance'; import { DatabaseHelper } from '../../../../helpers/database'; -import { MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; -import { ExploreTabs, rte } from '../../../../helpers/constants'; +import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; +import { ExploreTabs, KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { commonUrl, ossStandaloneRedisearch } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Telemetry } from '../../../../helpers/telemetry'; @@ -10,6 +10,8 @@ const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); +const browserPage = new BrowserPage(); + const chance = new Chance(); const telemetry = new Telemetry(); @@ -29,7 +31,8 @@ fixture `Default scripts area at Workbench` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await t.click(myRedisDatabasePage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); }) .afterEach(async t => { // Drop index, documents and database diff --git a/tests/e2e/tests/web/critical-path/workbench/scripting-area.e2e.ts b/tests/e2e/tests/web/critical-path/workbench/scripting-area.e2e.ts index 4fc4f744c6..1374c4a9f3 100644 --- a/tests/e2e/tests/web/critical-path/workbench/scripting-area.e2e.ts +++ b/tests/e2e/tests/web/critical-path/workbench/scripting-area.e2e.ts @@ -1,6 +1,6 @@ -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; -import { MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; +import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; @@ -9,6 +9,7 @@ const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); +const browserPage = new BrowserPage(); let indexName = Common.generateWord(5); let keyName = Common.generateWord(5); @@ -19,7 +20,8 @@ fixture `Scripting area at Workbench` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); //Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await t.click(myRedisDatabasePage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); }) .afterEach(async t => { await t.switchToMainWindow(); diff --git a/tests/e2e/tests/web/regression/browser/context.e2e.ts b/tests/e2e/tests/web/regression/browser/context.e2e.ts index c54087b6a4..13e11ab855 100644 --- a/tests/e2e/tests/web/regression/browser/context.e2e.ts +++ b/tests/e2e/tests/web/regression/browser/context.e2e.ts @@ -3,7 +3,7 @@ import { MyRedisDatabasePage, BrowserPage } from '../../../../pageObjects'; -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; @@ -41,7 +41,6 @@ test('Verify that if user has saved context on Browser page and go to Settings p await t.click(myRedisDatabasePage.NavigationPanel.settingsButton); // Verify that Browser and Workbench icons are displayed await t.expect(myRedisDatabasePage.NavigationPanel.browserButton.visible).ok('Browser icon is not displayed'); - await t.expect(myRedisDatabasePage.NavigationPanel.workbenchButton.visible).ok('Workbench icon is not displayed'); // Open Browser page and verify context await t.click(myRedisDatabasePage.NavigationPanel.browserButton); await verifySearchFilterValue(keyName); @@ -54,13 +53,13 @@ test('Verify that when user reload the window with saved context(on any page), c // Create context modificaions and navigate to Workbench await browserPage.addStringKey(keyName); await browserPage.openKeyDetails(keyName); - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); // Open Browser page and verify context - await t.click(myRedisDatabasePage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.BrowserAndFilter); await verifySearchFilterValue(keyName); await t.expect(browserPage.keyNameFormDetails.withExactText(keyName).exists).ok('The key details is not selected'); // Navigate to Workbench and reload the window - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await t.click(myRedisDatabasePage.NavigationPanel.pubSubButton); await myRedisDatabasePage.reloadPage(); // Return back to Browser and check context is not saved await t.click(myRedisDatabasePage.NavigationPanel.browserButton); diff --git a/tests/e2e/tests/web/regression/browser/keys-all-databases.e2e.ts b/tests/e2e/tests/web/regression/browser/keys-all-databases.e2e.ts index a9b292375e..c0767b9456 100644 --- a/tests/e2e/tests/web/regression/browser/keys-all-databases.e2e.ts +++ b/tests/e2e/tests/web/regression/browser/keys-all-databases.e2e.ts @@ -1,5 +1,5 @@ import { Selector, t } from 'testcafe'; -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage } from '../../../../pageObjects'; import { @@ -108,9 +108,9 @@ test await browserActions.verifyAllRenderedKeysHasText(); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); // Go to Browser Page - await t.click(myRedisDatabasePage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.BrowserAndFilter); // Verify that keys info in row not empty after switching between pages await browserActions.verifyAllRenderedKeysHasText(); }); diff --git a/tests/e2e/tests/web/regression/browser/onboarding.e2e.ts b/tests/e2e/tests/web/regression/browser/onboarding.e2e.ts index e136ff7e65..e9f114f800 100644 --- a/tests/e2e/tests/web/regression/browser/onboarding.e2e.ts +++ b/tests/e2e/tests/web/regression/browser/onboarding.e2e.ts @@ -2,7 +2,7 @@ import { DatabaseHelper } from '../../../../helpers/database'; import { commonUrl, ossStandaloneConfigEmpty } from '../../../../helpers/conf'; -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { Common } from '../../../../helpers/common'; import { MemoryEfficiencyPage, @@ -132,7 +132,7 @@ test('Verify onboard new user skip tour', async(t) => { await onboardingCardsDialog.clickNextStep(); // verify tree view step is visible await onboardingCardsDialog.verifyStepVisible('Tree view'); - await t.click(browserPage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); await t.click(myRedisDatabasePage.NavigationPanel.helpCenterButton); await t.expect(myRedisDatabasePage.NavigationPanel.HelpCenter.helpCenterPanel.visible).ok('help center panel is not opened'); await t.click(onboardingCardsDialog.resetOnboardingBtn); @@ -158,7 +158,7 @@ test.requestHooks(logger)('Verify that the final onboarding step is closed when // Verify last step of onboarding process is visible await onboardingCardsDialog.verifyStepVisible('Great job!'); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); // Verify that “ONBOARDING_TOUR_FINISHED” event is sent when user opens another page (or close the app) await telemetry.verifyEventHasProperties(telemetryEvent, expectedProperties, logger); diff --git a/tests/e2e/tests/web/regression/browser/resize-columns.e2e.ts b/tests/e2e/tests/web/regression/browser/resize-columns.e2e.ts index 5f86ecff1c..446c4b45a6 100644 --- a/tests/e2e/tests/web/regression/browser/resize-columns.e2e.ts +++ b/tests/e2e/tests/web/regression/browser/resize-columns.e2e.ts @@ -3,7 +3,7 @@ import { MyRedisDatabasePage, BrowserPage } from '../../../../pageObjects'; -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; @@ -80,8 +80,8 @@ test('Resize of columns in Hash, List, Zset Key details', async t => { } // Verify that resize saved when switching between pages - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); - await t.click(myRedisDatabasePage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.BrowserAndFilter); await browserPage.openKeyDetails(keys[0].name); await t.expect(field.clientWidth).within(keys[0].fieldWidthEnd - 5, keys[0].fieldWidthEnd + 5, 'Resize context not saved for key when switching between pages'); diff --git a/tests/e2e/tests/web/regression/browser/survey-link.e2e.ts b/tests/e2e/tests/web/regression/browser/survey-link.e2e.ts index 23176285e8..84471aae6e 100644 --- a/tests/e2e/tests/web/regression/browser/survey-link.e2e.ts +++ b/tests/e2e/tests/web/regression/browser/survey-link.e2e.ts @@ -1,5 +1,5 @@ import { DatabaseHelper } from '../../../../helpers/database'; -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { BrowserPage, MyRedisDatabasePage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; @@ -32,7 +32,7 @@ test('Verify that user can use survey link', async t => { // await Common.checkURL(externalPageLink); // await goBackHistory(); // Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); await t.expect(browserPage.userSurveyLink.visible).ok('Survey Link is not displayed'); // Slow Log page await t.click(myRedisDatabasePage.NavigationPanel.analysisPageButton); diff --git a/tests/e2e/tests/web/regression/cli/cli-promote-workbench.ts b/tests/e2e/tests/web/regression/cli/cli-promote-workbench.ts index 17a48dc1a7..30be6c7e77 100644 --- a/tests/e2e/tests/web/regression/cli/cli-promote-workbench.ts +++ b/tests/e2e/tests/web/regression/cli/cli-promote-workbench.ts @@ -1,7 +1,7 @@ import { DatabaseHelper } from '../../../../helpers/database'; import { WorkbenchPage, MyRedisDatabasePage, BrowserPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; const browserPage = new BrowserPage(); @@ -22,7 +22,8 @@ fixture `Promote workbench in CLI` }); test('Verify that user can see saved workbench context after redirection from CLI to workbench', async t => { // Open Workbench - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await t.click(myRedisDatabasePage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); const command = 'INFO'; await t.typeText(workbenchPage.queryInput, command, { replace: true, speed: 1, paste: true }); await t.click(myRedisDatabasePage.NavigationPanel.browserButton); diff --git a/tests/e2e/tests/web/regression/database-overview/database-info.e2e.ts b/tests/e2e/tests/web/regression/database-overview/database-info.e2e.ts index 68aa9f9764..522babd78a 100644 --- a/tests/e2e/tests/web/regression/database-overview/database-info.e2e.ts +++ b/tests/e2e/tests/web/regression/database-overview/database-info.e2e.ts @@ -4,7 +4,7 @@ import { WorkbenchPage, BrowserPage } from '../../../../pageObjects'; -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; @@ -41,6 +41,6 @@ test('Verify that user can see DB name, endpoint, connection type, Redis version // Verify that user can see an (i) icon next to the database name on Browser and Workbench pages await t.expect(browserPage.OverviewPanel.databaseInfoIcon.visible).ok('User can not see (i) icon on Browser page', { timeout: 10000 }); // Move to the Workbench page and check icon - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); await t.expect(workbenchPage.OverviewPanel.overviewTotalMemory.visible).ok('User can not see (i) icon on Workbench page', { timeout: 10000 }); }); diff --git a/tests/e2e/tests/web/regression/database-overview/database-overview.e2e.ts b/tests/e2e/tests/web/regression/database-overview/database-overview.e2e.ts index 8741192414..a09cc88a66 100644 --- a/tests/e2e/tests/web/regression/database-overview/database-overview.e2e.ts +++ b/tests/e2e/tests/web/regression/database-overview/database-overview.e2e.ts @@ -4,7 +4,7 @@ import { WorkbenchPage, BrowserPage } from '../../../../pageObjects'; -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { Common } from '../../../../helpers/common'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; @@ -35,7 +35,7 @@ test('Verify that user can connect to DB and see breadcrumbs at the top of the a // Verify that user can see breadcrumbs in Browser and Workbench views await t.expect(browserPage.breadcrumbsContainer.visible).ok('User can not see breadcrumbs in Browser page', { timeout: 10000 }); - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); await t.expect(browserPage.breadcrumbsContainer.visible).ok('User can not see breadcrumbs in Workbench page', { timeout: 10000 }); // Verify that user can see total memory and total number of keys updated in DB header in Workbench page diff --git a/tests/e2e/tests/web/regression/database/github.e2e.ts b/tests/e2e/tests/web/regression/database/github.e2e.ts index 2a05263539..d1e5c008d2 100644 --- a/tests/e2e/tests/web/regression/database/github.e2e.ts +++ b/tests/e2e/tests/web/regression/database/github.e2e.ts @@ -1,13 +1,13 @@ -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; -import { MyRedisDatabasePage } from '../../../../pageObjects'; +import { BrowserPage, MyRedisDatabasePage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; -import { Common } from '../../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); +const browserPage = new BrowserPage(); fixture `Github functionality` .meta({ type: 'regression', rte: rte.standalone }) @@ -29,7 +29,8 @@ test('Verify that user can work with Github link in the application', async t => await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName); await t.expect(myRedisDatabasePage.NavigationPanel.githubButton.visible).ok('Github button not found'); // Verify that user can see the icon for GitHub reference at the bottom of the left side bar on the Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await t.click(myRedisDatabasePage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); await t.expect(myRedisDatabasePage.NavigationPanel.githubButton.visible).ok('Github button'); // Verify that when user clicks on Github icon he redirects to the URL: https://github.com/RedisInsight/RedisInsight await t.click(myRedisDatabasePage.NavigationPanel.githubButton); diff --git a/tests/e2e/tests/web/regression/insights/import-tutorials.e2e.ts b/tests/e2e/tests/web/regression/insights/import-tutorials.e2e.ts index 644d73ec8a..b85fc6b332 100644 --- a/tests/e2e/tests/web/regression/insights/import-tutorials.e2e.ts +++ b/tests/e2e/tests/web/regression/insights/import-tutorials.e2e.ts @@ -2,7 +2,7 @@ import * as path from 'path'; import * as fs from 'fs'; import { join as joinPath } from 'path'; import { t } from 'testcafe'; -import { ExploreTabs, rte } from '../../../../helpers/constants'; +import { ExploreTabs, KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, MemoryEfficiencyPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig, ossStandaloneRedisearch, fileDownloadPath } from '../../../../helpers/conf'; @@ -41,7 +41,8 @@ fixture `Upload custom tutorials` .page(commonUrl) .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await t.click(myRedisDatabasePage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); }) .afterEach(async() => { await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); @@ -53,7 +54,9 @@ https://redislabs.atlassian.net/browse/RI-4302, https://redislabs.atlassian.net/ test.skip .before(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await t.click(myRedisDatabasePage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + tutorialName = `${zipFolderName}${Common.generateWord(5)}`; zipFilePath = path.join('..', 'test-data', 'upload-tutorials', `${tutorialName}.zip`); // Create zip file for uploading @@ -168,7 +171,9 @@ test test.skip .before(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await t.click(myRedisDatabasePage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + tutorialName = `${zipFolderName}${Common.generateWord(5)}`; zipFilePath = path.join('..', 'test-data', 'upload-tutorials', `${tutorialName}.zip`); // Create zip file for uploading @@ -180,7 +185,9 @@ test.skip await Common.deleteFileFromFolder(zipFilePath); await deleteAllKeysFromDB(ossStandaloneRedisearch.host, ossStandaloneRedisearch.port); // Clear and delete database - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await t.click(browserPage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await workbenchPage.InsightsPanel.togglePanel(true); const tutorials = await workbenchPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials); await tutorials.deleteTutorialByName(tutorialName); diff --git a/tests/e2e/tests/web/regression/insights/live-recommendations.e2e.ts b/tests/e2e/tests/web/regression/insights/live-recommendations.e2e.ts index bc14e55c69..50ff1d1fc7 100644 --- a/tests/e2e/tests/web/regression/insights/live-recommendations.e2e.ts +++ b/tests/e2e/tests/web/regression/insights/live-recommendations.e2e.ts @@ -1,6 +1,6 @@ import * as path from 'path'; import { BrowserPage, MemoryEfficiencyPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; -import { ExploreTabs, RecommendationIds, rte } from '../../../../helpers/constants'; +import { ExploreTabs, KeysInteractionTabs, RecommendationIds, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { commonUrl, ossStandaloneConfig, ossStandaloneV5Config } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; @@ -247,7 +247,9 @@ test('Verify that if user clicks on the Analyze button and link, the pop up with //Verify that user is navigated to DB Analysis page via Analyze button and new report is generated await t.click(memoryEfficiencyPage.selectedReport); await t.expect(memoryEfficiencyPage.reportItem.visible).ok('Database analysis page not opened'); - await t.click(memoryEfficiencyPage.NavigationPanel.workbenchButton); + await t.click(memoryEfficiencyPage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await workbenchPage.InsightsPanel.togglePanel(true); tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tips); await t.click(tab.analyzeDatabaseLink); diff --git a/tests/e2e/tests/web/regression/insights/open-insights-panel.e2e.ts b/tests/e2e/tests/web/regression/insights/open-insights-panel.e2e.ts index 4fd89e9874..8cdd8f2257 100644 --- a/tests/e2e/tests/web/regression/insights/open-insights-panel.e2e.ts +++ b/tests/e2e/tests/web/regression/insights/open-insights-panel.e2e.ts @@ -1,5 +1,5 @@ import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; -import { Compatibility, ExploreTabs, rte } from '../../../../helpers/constants'; +import { Compatibility, ExploreTabs, KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { commonUrl, @@ -47,7 +47,7 @@ test await t.click(browserPage.NavigationPanel.myRedisDBButton); await myRedisDatabasePage.clickOnDBByName(ossStandaloneV5Config.databaseName); - await t.click(browserPage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); await workbenchPage.sendCommandInWorkbench('TS.'); await t.click(browserPage.NavigationPanel.myRedisDBButton); diff --git a/tests/e2e/tests/web/regression/monitor/monitor.e2e.ts b/tests/e2e/tests/web/regression/monitor/monitor.e2e.ts index f5b5940704..4a86f49853 100644 --- a/tests/e2e/tests/web/regression/monitor/monitor.e2e.ts +++ b/tests/e2e/tests/web/regression/monitor/monitor.e2e.ts @@ -12,7 +12,7 @@ import { ossStandaloneConfig, ossStandaloneNoPermissionsConfig } from '../../../../helpers/conf'; -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { WorkbenchActions } from '../../../../common-actions/workbench-actions'; @@ -148,7 +148,7 @@ test.skip await t.expect(browserPage.Profiler.monitorNoPermissionsMessage.innerText).eql('The Profiler cannot be started. This user has no permissions to run the \'monitor\' command', 'No Permissions message not found'); // Verify that if user doesn't have permissions to run monitor, run monitor button is not available await t.expect(browserPage.Profiler.runMonitorToggle.withAttribute('disabled').exists).ok('No permissions run icon not found'); - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); await workbenchPage.sendCommandInWorkbench(command); // Verify that user have the following error when there is no permission to run the CLIENT LIST: "NOPERM this user has no permissions to run the 'CLIENT LIST' command or its subcommand" await workbenchActions.verifyClientListErrorMessage(); diff --git a/tests/e2e/tests/web/regression/workbench/autocomplete.e2e.ts b/tests/e2e/tests/web/regression/workbench/autocomplete.e2e.ts index 20cd197b92..db496c2bc3 100644 --- a/tests/e2e/tests/web/regression/workbench/autocomplete.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/autocomplete.e2e.ts @@ -1,13 +1,13 @@ -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; -import { MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; +import { BrowserPage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; -const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); +const browserPage = new BrowserPage(); fixture `Autocomplete for entered commands` .meta({ type: 'regression', rte: rte.standalone }) @@ -15,7 +15,7 @@ fixture `Autocomplete for entered commands` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); }) .afterEach(async() => { // Delete database diff --git a/tests/e2e/tests/web/regression/workbench/autoexecute-button.e2e.ts b/tests/e2e/tests/web/regression/workbench/autoexecute-button.e2e.ts index 5eb4caf0f2..9bc30b02ed 100644 --- a/tests/e2e/tests/web/regression/workbench/autoexecute-button.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/autoexecute-button.e2e.ts @@ -1,20 +1,20 @@ import { DatabaseHelper } from '../../../../helpers/database'; -import { WorkbenchPage, MyRedisDatabasePage } from '../../../../pageObjects'; -import { ExploreTabs, rte } from '../../../../helpers/constants'; +import { WorkbenchPage, BrowserPage } from '../../../../pageObjects'; +import { ExploreTabs, KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; -const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); +const browserPage = new BrowserPage(); fixture `Workbench Auto-Execute button` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); }) .afterEach(async() => { // Clear and delete database diff --git a/tests/e2e/tests/web/regression/workbench/command-results.e2e.ts b/tests/e2e/tests/web/regression/workbench/command-results.e2e.ts index 77cc8c791e..40b725c1e4 100644 --- a/tests/e2e/tests/web/regression/workbench/command-results.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/command-results.e2e.ts @@ -1,7 +1,7 @@ import { DatabaseHelper } from '../../../../helpers/database'; -import { WorkbenchPage, MyRedisDatabasePage } from '../../../../pageObjects'; +import { WorkbenchPage, MyRedisDatabasePage, BrowserPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneRedisearch } from '../../../../helpers/conf'; -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; import { WorkbenchActions } from '../../../../common-actions/workbench-actions'; @@ -11,6 +11,7 @@ const workbenchPage = new WorkbenchPage(); const workBenchActions = new WorkbenchActions(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); +const browserPage = new BrowserPage(); const indexName = Common.generateWord(5); const commandsForIndex = [ @@ -25,7 +26,7 @@ fixture `Command results at Workbench` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); // Add index and data - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); await workbenchPage.sendCommandsArrayInWorkbench(commandsForIndex); }) .afterEach(async t => { @@ -114,7 +115,7 @@ test('Big output in workbench is visible in virtualized table', async t => { test .before(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); }) .after(async t => { await t.switchToMainWindow(); diff --git a/tests/e2e/tests/web/regression/workbench/context.e2e.ts b/tests/e2e/tests/web/regression/workbench/context.e2e.ts index a51762ecd8..3755ab21d5 100644 --- a/tests/e2e/tests/web/regression/workbench/context.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/context.e2e.ts @@ -1,4 +1,4 @@ -import { ExploreTabs, rte } from '../../../../helpers/constants'; +import { ExploreTabs, KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; @@ -18,7 +18,7 @@ fixture `Workbench Context` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); }) .afterEach(async() => { // Delete database @@ -29,7 +29,7 @@ test('Verify that user can see saved CLI state when navigates away to any other await t.click(workbenchPage.Cli.cliExpandButton); await t.click(myRedisDatabasePage.NavigationPanel.browserButton); // Return back to Workbench and check CLI - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); await t.expect(workbenchPage.Cli.cliCollapseButton.exists).ok('CLI is not expanded'); }); // Update after resolving https://redislabs.atlassian.net/browse/RI-3299 @@ -55,7 +55,7 @@ test('Verify that user can see all the information removed when reloads the page await t.click(workbenchPage.Cli.cliExpandButton); await t.click(myRedisDatabasePage.NavigationPanel.browserButton); // Open Workbench page and verify context - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); await t.expect(workbenchPage.Cli.cliCollapseButton.exists).ok('CLI is not expanded'); await t.expect(workbenchPage.queryInputScriptArea.textContent).eql(command, 'Input in Editor is not saved'); // Reload the window and chek context diff --git a/tests/e2e/tests/web/regression/workbench/cypher.e2e.ts b/tests/e2e/tests/web/regression/workbench/cypher.e2e.ts index 044b546c91..13575f7249 100644 --- a/tests/e2e/tests/web/regression/workbench/cypher.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/cypher.e2e.ts @@ -1,13 +1,13 @@ -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; -import { MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; +import { BrowserPage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; -const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); +const browserPage = new BrowserPage(); const command = 'GRAPH.QUERY graph'; @@ -17,7 +17,7 @@ fixture `Cypher syntax at Workbench` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); }) .afterEach(async() => { // Drop database diff --git a/tests/e2e/tests/web/regression/workbench/default-scripts-area.e2e.ts b/tests/e2e/tests/web/regression/workbench/default-scripts-area.e2e.ts index db3898c8a7..fb468a9904 100644 --- a/tests/e2e/tests/web/regression/workbench/default-scripts-area.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/default-scripts-area.e2e.ts @@ -1,4 +1,4 @@ -import { ExploreTabs, rte } from '../../../../helpers/constants'; +import { ExploreTabs, KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; @@ -16,7 +16,7 @@ fixture `Default scripts area at Workbench` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); }) .afterEach(async() => { // Delete database @@ -70,7 +70,7 @@ test('Verify that user can see saved article in Enablement area when he leaves W // Go to Browser page await t.click(myRedisDatabasePage.NavigationPanel.browserButton); // Go back to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); // Verify that the same article is opened in Enablement area selector = tutorials.getRunSelector('Create a hash'); await t.expect(selector.visible).ok('The end of the page is not visible'); diff --git a/tests/e2e/tests/web/regression/workbench/editor-cleanup.e2e.ts b/tests/e2e/tests/web/regression/workbench/editor-cleanup.e2e.ts index 2052959b77..244a827ed5 100644 --- a/tests/e2e/tests/web/regression/workbench/editor-cleanup.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/editor-cleanup.e2e.ts @@ -1,6 +1,6 @@ import { DatabaseHelper } from '../../../../helpers/database'; -import { WorkbenchPage, MyRedisDatabasePage, SettingsPage } from '../../../../pageObjects'; -import { rte } from '../../../../helpers/constants'; +import { WorkbenchPage, MyRedisDatabasePage, SettingsPage, BrowserPage } from '../../../../pageObjects'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; @@ -9,6 +9,7 @@ const workbenchPage = new WorkbenchPage(); const settingsPage = new SettingsPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); +const browserPage = new BrowserPage(); const commandToSend = 'info server'; const databasesForAdding = [ @@ -35,7 +36,8 @@ test('Disabled Editor Cleanup toggle behavior', async t => { // Verify that user can see text "Clear the Editor after running commands" for Editor Cleanup In Settings await t.expect(settingsPage.switchEditorCleanupOption.sibling(0).withExactText('Clear the Editor after running commands').visible).ok('Cleanup text is not correct'); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await t.click(settingsPage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); // Send commands await workbenchPage.sendCommandInWorkbench(commandToSend); await workbenchPage.sendCommandInWorkbench(commandToSend); @@ -44,7 +46,7 @@ test('Disabled Editor Cleanup toggle behavior', async t => { }); test('Enabled Editor Cleanup toggle behavior', async t => { // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); // Send commands await workbenchPage.sendCommandInWorkbench(commandToSend); await workbenchPage.sendCommandInWorkbench(commandToSend); diff --git a/tests/e2e/tests/web/regression/workbench/empty-command-history.e2e.ts b/tests/e2e/tests/web/regression/workbench/empty-command-history.e2e.ts index fce78be20c..5071c5ae1d 100644 --- a/tests/e2e/tests/web/regression/workbench/empty-command-history.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/empty-command-history.e2e.ts @@ -1,11 +1,11 @@ import { Selector } from 'testcafe'; -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; -import { MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; +import { BrowserPage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; -const myRedisDatabasePage = new MyRedisDatabasePage(); +const browserPage = new BrowserPage(); const workbenchPage = new WorkbenchPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); @@ -16,7 +16,7 @@ fixture `Empty command history in Workbench` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); }) .afterEach(async() => { // Delete database diff --git a/tests/e2e/tests/web/regression/workbench/group-mode.e2e.ts b/tests/e2e/tests/web/regression/workbench/group-mode.e2e.ts index a115c0a95a..64c0cd0bb7 100644 --- a/tests/e2e/tests/web/regression/workbench/group-mode.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/group-mode.e2e.ts @@ -1,11 +1,11 @@ import { Selector } from 'testcafe'; -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; -import { MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; +import { BrowserPage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneBigConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; -const myRedisDatabasePage = new MyRedisDatabasePage(); +const browserPage = new BrowserPage(); const workbenchPage = new WorkbenchPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); @@ -23,7 +23,7 @@ fixture `Workbench Group Mode` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); }) .afterEach(async() => { // Delete database diff --git a/tests/e2e/tests/web/regression/workbench/history-of-results.e2e.ts b/tests/e2e/tests/web/regression/workbench/history-of-results.e2e.ts index 79b3e04501..fe0a0d024a 100644 --- a/tests/e2e/tests/web/regression/workbench/history-of-results.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/history-of-results.e2e.ts @@ -1,12 +1,12 @@ import { getRandomParagraph } from '../../../../helpers/keys'; import { DatabaseHelper } from '../../../../helpers/database'; -import { MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; -import { rte } from '../../../../helpers/constants'; +import { BrowserPage, WorkbenchPage } from '../../../../pageObjects'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; -const myRedisDatabasePage = new MyRedisDatabasePage(); +const browserPage = new BrowserPage(); const workbenchPage = new WorkbenchPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); @@ -21,7 +21,7 @@ fixture `History of results at Workbench` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); }) .afterEach(async() => { // Clear and delete database diff --git a/tests/e2e/tests/web/regression/workbench/raw-mode.e2e.ts b/tests/e2e/tests/web/regression/workbench/raw-mode.e2e.ts index 0b87cc4a19..94b08c001e 100644 --- a/tests/e2e/tests/web/regression/workbench/raw-mode.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/raw-mode.e2e.ts @@ -1,6 +1,6 @@ import { DatabaseHelper } from '../../../../helpers/database'; -import { WorkbenchPage, MyRedisDatabasePage } from '../../../../pageObjects'; -import { rte } from '../../../../helpers/constants'; +import { WorkbenchPage, MyRedisDatabasePage, BrowserPage } from '../../../../pageObjects'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { commonUrl, ossStandaloneConfig, ossStandaloneRedisearch } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; @@ -8,6 +8,7 @@ import { APIKeyRequests } from '../../../../helpers/api/api-keys'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); +const browserPage = new BrowserPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); const apiKeyRequests = new APIKeyRequests(); @@ -31,7 +32,7 @@ fixture `Workbench Raw mode` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); }) .afterEach(async t => { // Clear and delete database @@ -64,7 +65,7 @@ test await myRedisDatabasePage.reloadPage(); await myRedisDatabasePage.clickOnDBByName(databasesForAdding[0].databaseName); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); }) .after(async() => { // Clear and delete database @@ -81,7 +82,7 @@ test await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton); await myRedisDatabasePage.clickOnDBByName(databasesForAdding[1].databaseName); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); // Verify that user can see saved Raw mode state after re-connection to another DB await workbenchPage.sendCommandInWorkbench(commandsForSend[1]); await workbenchPage.checkWorkbenchCommandResult(commandsForSend[1], `"${unicodeValue}"`); @@ -93,7 +94,7 @@ test .before(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); }) .after(async t => { // Drop index, documents and database diff --git a/tests/e2e/tests/web/regression/workbench/redis-stack-commands.e2e.ts b/tests/e2e/tests/web/regression/workbench/redis-stack-commands.e2e.ts index 218411f615..2e6f062018 100644 --- a/tests/e2e/tests/web/regression/workbench/redis-stack-commands.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/redis-stack-commands.e2e.ts @@ -1,11 +1,11 @@ import { t } from 'testcafe'; import { DatabaseHelper } from '../../../../helpers/database'; -import { WorkbenchPage, MyRedisDatabasePage } from '../../../../pageObjects'; +import { WorkbenchPage, BrowserPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; -import { ExploreTabs, rte } from '../../../../helpers/constants'; +import { ExploreTabs, KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; -const myRedisDatabasePage = new MyRedisDatabasePage(); +const browserPage = new BrowserPage(); const workbenchPage = new WorkbenchPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); @@ -17,7 +17,7 @@ fixture `Redis Stack command in Workbench` .page(commonUrl) .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); }) .afterEach(async() => { // Drop key and database diff --git a/tests/e2e/tests/web/regression/workbench/redisearch-module-not-available.e2e.ts b/tests/e2e/tests/web/regression/workbench/redisearch-module-not-available.e2e.ts index 729387935c..ffa713a5d1 100644 --- a/tests/e2e/tests/web/regression/workbench/redisearch-module-not-available.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/redisearch-module-not-available.e2e.ts @@ -1,11 +1,11 @@ import { ClientFunction } from 'testcafe'; -import { ExploreTabs, rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; -import { MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; +import { BrowserPage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneV5Config } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; -const myRedisDatabasePage = new MyRedisDatabasePage(); +const browserPage = new BrowserPage(); const workbenchPage = new WorkbenchPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); @@ -18,7 +18,7 @@ fixture `Redisearch module not available` .page(commonUrl) .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneV5Config); - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); }) .afterEach(async() => { // Delete database diff --git a/tests/e2e/tests/web/regression/workbench/scripting-area.e2e.ts b/tests/e2e/tests/web/regression/workbench/scripting-area.e2e.ts index 35f701edc5..4904b186e1 100644 --- a/tests/e2e/tests/web/regression/workbench/scripting-area.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/scripting-area.e2e.ts @@ -1,6 +1,6 @@ -import { ExploreTabs, rte } from '../../../../helpers/constants'; +import { ExploreTabs, KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; -import { MyRedisDatabasePage, WorkbenchPage, SettingsPage } from '../../../../pageObjects'; +import { MyRedisDatabasePage, WorkbenchPage, SettingsPage, BrowserPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; @@ -10,6 +10,7 @@ const workbenchPage = new WorkbenchPage(); const settingsPage = new SettingsPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); +const browserPage = new BrowserPage(); const indexName = Common.generateWord(5); let keyName = Common.generateWord(5); @@ -20,7 +21,7 @@ fixture `Scripting area at Workbench` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); }) .afterEach(async() => { // Clear and delete database @@ -41,7 +42,8 @@ test('Verify that user can run multiple commands written in multiple lines in Wo await t.click(settingsPage.accordionWorkbenchSettings); await settingsPage.changeCommandsInPipeline('1'); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await t.click(settingsPage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); // Send commands in multiple lines await workbenchPage.sendCommandInWorkbench(commandsForSend.join('\n'), 0.5); // Check the result @@ -68,7 +70,8 @@ test await t.click(settingsPage.accordionWorkbenchSettings); await settingsPage.changeCommandsInPipeline('1'); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await t.click(settingsPage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); // Send commands in multiple lines with double slashes (//) wrapped in double quotes await workbenchPage.sendCommandInWorkbench(commandsForSend.join('\n"//"'), 0.5); // Check that all commands are executed diff --git a/tests/e2e/tests/web/regression/workbench/workbench-all-db-types.e2e.ts b/tests/e2e/tests/web/regression/workbench/workbench-all-db-types.e2e.ts index 633deadb0d..59de3d7644 100644 --- a/tests/e2e/tests/web/regression/workbench/workbench-all-db-types.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/workbench-all-db-types.e2e.ts @@ -1,12 +1,13 @@ import { t } from 'testcafe'; -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; -import { MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; +import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; import { cloudDatabaseConfig, commonUrl, ossClusterConfig, ossSentinelConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); +const browserPage = new BrowserPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); @@ -19,7 +20,8 @@ const verifyCommandsInWorkbench = async(): Promise => { 'FT.SEARCH idx *' ]; - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await t.click(myRedisDatabasePage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); // Send commands await workbenchPage.sendCommandInWorkbench(commandForSend1); await workbenchPage.sendCommandInWorkbench(commandForSend2); diff --git a/tests/e2e/tests/web/regression/workbench/workbench-non-auto-guides.e2e.ts b/tests/e2e/tests/web/regression/workbench/workbench-non-auto-guides.e2e.ts index 447c61115f..b920f2b265 100644 --- a/tests/e2e/tests/web/regression/workbench/workbench-non-auto-guides.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/workbench-non-auto-guides.e2e.ts @@ -1,6 +1,6 @@ import { DatabaseHelper } from '../../../../helpers/database'; -import { WorkbenchPage, MyRedisDatabasePage } from '../../../../pageObjects'; -import { rte } from '../../../../helpers/constants'; +import { WorkbenchPage, MyRedisDatabasePage, BrowserPage } from '../../../../pageObjects'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; @@ -8,6 +8,7 @@ import { APIKeyRequests } from '../../../../helpers/api/api-keys'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); +const browserPage = new BrowserPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); const apiKeyRequests = new APIKeyRequests(); @@ -37,7 +38,7 @@ fixture `Workbench modes to non-auto guides` .page(commonUrl) .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); }) .afterEach(async() => { // Delete database @@ -46,7 +47,7 @@ fixture `Workbench modes to non-auto guides` test .before(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); await workbenchPage.sendCommandInWorkbench(`set ${keyName} "${keyValue}"`); }) .after(async t => { diff --git a/tests/e2e/tests/web/regression/workbench/workbench-pipeline.e2e.ts b/tests/e2e/tests/web/regression/workbench/workbench-pipeline.e2e.ts index a78097f21e..fc97f27f8a 100644 --- a/tests/e2e/tests/web/regression/workbench/workbench-pipeline.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/workbench-pipeline.e2e.ts @@ -1,13 +1,14 @@ // import { ClientFunction } from 'testcafe'; -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; -import { MyRedisDatabasePage, SettingsPage, WorkbenchPage } from '../../../../pageObjects'; +import { BrowserPage, MyRedisDatabasePage, SettingsPage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneBigConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); +const browserPage = new BrowserPage(); const settingsPage = new SettingsPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); @@ -49,7 +50,8 @@ test('Verify that user can see the text in settings for pipeline with link', asy test.skip('Verify that only chosen in pipeline number of commands is loading at the same time in Workbench', async t => { await settingsPage.changeCommandsInPipeline(pipelineValues[1]); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await t.click(settingsPage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); await workbenchPage.sendCommandInWorkbench(commandForSend, 0.01); // Verify that only selected pipeline number of commands are loaded at the same time await t.expect(workbenchPage.loadedCommand.count).eql(Number(pipelineValues[1]), 'The number of sending commands is incorrect'); @@ -57,7 +59,8 @@ test.skip('Verify that only chosen in pipeline number of commands is loading at test.skip('Verify that user can see spinner over Run button and grey preloader for each command', async t => { await settingsPage.changeCommandsInPipeline(pipelineValues[3]); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await t.click(settingsPage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); await workbenchPage.sendCommandInWorkbench(commandForSend, 0.01); // Verify that user can`t start new commands from the Workbench while command(s) is executing await t.expect(workbenchPage.submitCommandButton.withAttribute('disabled').exists).ok('Run button is not disabled', { timeout: 5000 }); @@ -70,7 +73,8 @@ test('Verify that user can interact with the Editor while command(s) in progress await settingsPage.changeCommandsInPipeline(pipelineValues[2]); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await t.click(settingsPage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); await workbenchPage.sendCommandInWorkbench(commandForSend); await t.typeText(workbenchPage.queryInput, commandForSend, { replace: true, paste: true }); await t.pressKey('enter'); @@ -89,7 +93,8 @@ test('Verify that command results are added to history in order most recent - on await settingsPage.changeCommandsInPipeline(pipelineValues[2]); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await t.click(settingsPage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); await workbenchPage.sendCommandInWorkbench(multipleCommands.join('\n')); // Check that the results for all commands are displayed in workbench history in reverse order (most recent - on top) for (let i = 0; i < multipleCommands.length; i++) { diff --git a/tests/e2e/tests/web/smoke/workbench/json-workbench.e2e.ts b/tests/e2e/tests/web/smoke/workbench/json-workbench.e2e.ts index 7f8ebdd65a..735d3226a2 100644 --- a/tests/e2e/tests/web/smoke/workbench/json-workbench.e2e.ts +++ b/tests/e2e/tests/web/smoke/workbench/json-workbench.e2e.ts @@ -1,11 +1,11 @@ -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; -import { MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; +import { BrowserPage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneRedisearch } from '../../../../helpers/conf'; import { Common } from '../../../../helpers/common'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; -const myRedisDatabasePage = new MyRedisDatabasePage(); +const browserPage = new BrowserPage(); const workbenchPage = new WorkbenchPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); @@ -18,7 +18,7 @@ fixture `JSON verifications at Workbench` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); }) .afterEach(async t => { // Clear and delete database diff --git a/tests/e2e/tests/web/smoke/workbench/scripting-area.e2e.ts b/tests/e2e/tests/web/smoke/workbench/scripting-area.e2e.ts index 64ce6479d0..0a8eae3019 100644 --- a/tests/e2e/tests/web/smoke/workbench/scripting-area.e2e.ts +++ b/tests/e2e/tests/web/smoke/workbench/scripting-area.e2e.ts @@ -1,10 +1,10 @@ import { DatabaseHelper } from '../../../../helpers/database'; -import { MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; +import { BrowserPage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; -const myRedisDatabasePage = new MyRedisDatabasePage(); +const browserPage = new BrowserPage(); const workbenchPage = new WorkbenchPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); @@ -15,7 +15,7 @@ fixture `Scripting area at Workbench` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.workbenchButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); }) .afterEach(async() => { // Delete database From fbd5f305e46b536f3919586f27060c9806861772 Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Sat, 29 Jun 2024 14:13:14 +0200 Subject: [PATCH 009/112] fix tests #2 --- tests/e2e/tests/web/critical-path/monitor/monitor.e2e.ts | 2 +- tests/e2e/tests/web/regression/browser/onboarding.e2e.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/e2e/tests/web/critical-path/monitor/monitor.e2e.ts b/tests/e2e/tests/web/critical-path/monitor/monitor.e2e.ts index c9c1435170..24895ef253 100644 --- a/tests/e2e/tests/web/critical-path/monitor/monitor.e2e.ts +++ b/tests/e2e/tests/web/critical-path/monitor/monitor.e2e.ts @@ -46,7 +46,7 @@ test('Verify that user can work with Monitor', async t => { await browserPage.Cli.getSuccessCommandResultFromCli(`${command} ${keyName} ${keyValue}`); await browserPage.Profiler.checkCommandInMonitorResults(command, [keyName, keyValue]); }); -test.only('Verify that user can see the list of all commands from all clients ran for this Redis database in the list of results in Monitor', async t => { +test('Verify that user can see the list of all commands from all clients ran for this Redis database in the list of results in Monitor', async t => { //Define commands in different clients const cli_command = 'command'; const workbench_command = 'hello'; diff --git a/tests/e2e/tests/web/regression/browser/onboarding.e2e.ts b/tests/e2e/tests/web/regression/browser/onboarding.e2e.ts index e9f114f800..5ccf214dae 100644 --- a/tests/e2e/tests/web/regression/browser/onboarding.e2e.ts +++ b/tests/e2e/tests/web/regression/browser/onboarding.e2e.ts @@ -150,7 +150,7 @@ test('Verify onboard new user skip tour', async(t) => { await t.expect(browserPage.patternModeBtn.visible).ok('Browser page is not opened'); }); // https://redislabs.atlassian.net/browse/RI-4305 -test.requestHooks(logger)('Verify that the final onboarding step is closed when user opens another page', async(t) => { +test.only.requestHooks(logger)('Verify that the final onboarding step is closed when user opens another page', async(t) => { await t.click(myRedisDatabasePage.NavigationPanel.helpCenterButton); await t.click(onboardingCardsDialog.resetOnboardingBtn); await onboardingCardsDialog.startOnboarding(); @@ -158,6 +158,7 @@ test.requestHooks(logger)('Verify that the final onboarding step is closed when // Verify last step of onboarding process is visible await onboardingCardsDialog.verifyStepVisible('Great job!'); // Go to Workbench page + await t.click(myRedisDatabasePage.NavigationPanel.browserButton); await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); // Verify that “ONBOARDING_TOUR_FINISHED” event is sent when user opens another page (or close the app) From 114da79a2f620a555aa564fa3a4727abf142d947 Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Sun, 30 Jun 2024 12:42:44 +0200 Subject: [PATCH 010/112] remove only --- tests/e2e/tests/web/regression/browser/onboarding.e2e.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/tests/web/regression/browser/onboarding.e2e.ts b/tests/e2e/tests/web/regression/browser/onboarding.e2e.ts index 5ccf214dae..80d3961e34 100644 --- a/tests/e2e/tests/web/regression/browser/onboarding.e2e.ts +++ b/tests/e2e/tests/web/regression/browser/onboarding.e2e.ts @@ -150,7 +150,7 @@ test('Verify onboard new user skip tour', async(t) => { await t.expect(browserPage.patternModeBtn.visible).ok('Browser page is not opened'); }); // https://redislabs.atlassian.net/browse/RI-4305 -test.only.requestHooks(logger)('Verify that the final onboarding step is closed when user opens another page', async(t) => { +test.requestHooks(logger)('Verify that the final onboarding step is closed when user opens another page', async(t) => { await t.click(myRedisDatabasePage.NavigationPanel.helpCenterButton); await t.click(onboardingCardsDialog.resetOnboardingBtn); await onboardingCardsDialog.startOnboarding(); From 7e67e9f44609813d77cf993384c5ffc5cd61cb2a Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Mon, 1 Jul 2024 10:52:51 +0200 Subject: [PATCH 011/112] #RI-5682 - fix tests --- .../workbench/components/wb-view/WBViewWrapper.spec.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.spec.tsx b/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.spec.tsx index c0d9cf9787..6703bb781c 100644 --- a/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.spec.tsx +++ b/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.spec.tsx @@ -10,8 +10,8 @@ import { screen, act, } from 'uiSrc/utils/test-utils' -import QueryWrapper from 'uiSrc/components/query' -import { Props as QueryProps } from 'uiSrc/components/query/QueryWrapper' +import QueryWrapper from 'uiSrc/pages/workbench/components/query' +import { Props as QueryProps } from 'uiSrc/pages/workbench/components/query/QueryWrapper' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { clearWbResults, @@ -30,7 +30,7 @@ beforeEach(() => { store.clearActions() }) -jest.mock('uiSrc/components/query', () => ({ +jest.mock('uiSrc/pages/workbench/components/query', () => ({ __esModule: true, namedExport: jest.fn(), default: jest.fn(), From 6a576fb85f33ee3bfeeea8dbe1636fcff29f74ee Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Mon, 1 Jul 2024 16:06:43 +0200 Subject: [PATCH 012/112] #RI-5903 - fix suggestions for workbench editor #RI-5898 - fix nav meni redirect #RI-5899 - fix go to workbench button from recommendation --- .../ui/src/components/navigation-menu/NavigationMenu.tsx | 2 +- .../components/recommendation/Recommendation.tsx | 9 +++++++-- .../workbench/components/query/Query/styles.module.scss | 4 ++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx b/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx index f77a84689f..b40e4924ba 100644 --- a/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx +++ b/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx @@ -117,7 +117,7 @@ const NavigationMenu = () => { pageName: PageNames.browser, isActivePage: isBrowserPath(activePage), ariaLabel: 'Browser page button', - onClick: () => handleGoPage(Pages.browser(connectedInstanceId)), + onClick: () => handleGoPage(Pages.keys(connectedInstanceId)), dataTestId: 'browser-page-btn', connectedInstanceId, getClassName() { diff --git a/redisinsight/ui/src/components/side-panels/panels/live-time-recommendations/components/recommendation/Recommendation.tsx b/redisinsight/ui/src/components/side-panels/panels/live-time-recommendations/components/recommendation/Recommendation.tsx index 5f46da2728..5cacae6882 100644 --- a/redisinsight/ui/src/components/side-panels/panels/live-time-recommendations/components/recommendation/Recommendation.tsx +++ b/redisinsight/ui/src/components/side-panels/panels/live-time-recommendations/components/recommendation/Recommendation.tsx @@ -17,7 +17,7 @@ import { isUndefined } from 'lodash' import cx from 'classnames' import { Nullable, Maybe, findTutorialPath } from 'uiSrc/utils' -import { Theme } from 'uiSrc/constants' +import { Pages, Theme } from 'uiSrc/constants' import { RecommendationVoting, RecommendationCopyComponent, RecommendationBody } from 'uiSrc/components' import { Vote } from 'uiSrc/constants/recommendations' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' @@ -77,7 +77,12 @@ const Recommendation = ({ } }) - const tutorialPath = findTutorialPath({ id: tutorialId ?? '' }) + if (!tutorialId) { + history.push(Pages.workbench(instanceId)) + return + } + + const tutorialPath = findTutorialPath({ id: tutorialId }) dispatch(openTutorialByPath(tutorialPath ?? '', history)) } diff --git a/redisinsight/ui/src/pages/workbench/components/query/Query/styles.module.scss b/redisinsight/ui/src/pages/workbench/components/query/Query/styles.module.scss index 2213a8f856..18b46e1931 100644 --- a/redisinsight/ui/src/pages/workbench/components/query/Query/styles.module.scss +++ b/redisinsight/ui/src/pages/workbench/components/query/Query/styles.module.scss @@ -12,7 +12,6 @@ .container { display: flex; flex-direction: column; - overflow: hidden; padding: 8px 16px; width: 100%; height: 100%; @@ -50,7 +49,8 @@ } .input { - overflow: hidden; + // cannot use overflow since suggestions are absolute + max-height: calc(100% - 32px); flex-grow: 1; width: 100%; border: 1px solid var(--euiColorLightShade); From 8a8c3f2e2adf95404fda7deabe73ab2d20f948ec Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Tue, 2 Jul 2024 09:59:20 +0200 Subject: [PATCH 013/112] add new workbench tab test --- .../components/keys-interaction-panel.ts | 3 +++ tests/e2e/pageObjects/workbench-page.ts | 1 + .../web/critical-path/rdi/navigation.e2e.ts | 2 +- .../regression/workbench/workbench-tab.e2e.ts | 27 +++++++++++++++++++ 4 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 tests/e2e/tests/web/regression/workbench/workbench-tab.e2e.ts diff --git a/tests/e2e/pageObjects/components/keys-interaction-panel.ts b/tests/e2e/pageObjects/components/keys-interaction-panel.ts index 6404d5a54c..6f2ef1276c 100644 --- a/tests/e2e/pageObjects/components/keys-interaction-panel.ts +++ b/tests/e2e/pageObjects/components/keys-interaction-panel.ts @@ -24,6 +24,9 @@ export class KeysInteractionPanel { * Click on Panel tab * @param type of the tab */ + async setActiveTab(type: KeysInteractionTabs.BrowserAndFilter): Promise + async setActiveTab(type: KeysInteractionTabs.SearchAndQuery): Promise + async setActiveTab(type: KeysInteractionTabs.Workbench): Promise async setActiveTab(type: KeysInteractionTabs): Promise { const activeTabName = await this.getActiveTabName(); diff --git a/tests/e2e/pageObjects/workbench-page.ts b/tests/e2e/pageObjects/workbench-page.ts index 2d75d37a6a..b4b7d0e3ad 100644 --- a/tests/e2e/pageObjects/workbench-page.ts +++ b/tests/e2e/pageObjects/workbench-page.ts @@ -47,6 +47,7 @@ export class WorkbenchPage extends InstancePage { copyCommand = Selector('[data-testid=copy-command]'); clearResultsBtn = Selector('[data-testid=clear-history-btn]'); exploreRedisBtn = Selector('[data-testid=no-results-explore-btn]'); + basicUseCaseTutorialsButton = Selector('[data-testid=wb-tutorials-link_redis_use_cases_basic]'); //ICONS noCommandHistoryIcon = Selector('[data-testid=wb_no-results__icon]'); parametersAnchor = Selector('[data-testid=parameters-anchor]'); diff --git a/tests/e2e/tests/web/critical-path/rdi/navigation.e2e.ts b/tests/e2e/tests/web/critical-path/rdi/navigation.e2e.ts index 9df133405a..a08d7799da 100644 --- a/tests/e2e/tests/web/critical-path/rdi/navigation.e2e.ts +++ b/tests/e2e/tests/web/critical-path/rdi/navigation.e2e.ts @@ -85,7 +85,7 @@ test('Verify that Insight and Sign in buttons are displayed ', async() => { await t.expect(rdiInstancePage.RdiHeader.InsightsPanel.getInsightsPanel().exists).ok('Insight panel is not exist'); await rdiInstancePage.RdiHeader.InsightsPanel.togglePanel(true); const tab = await rdiInstancePage.RdiHeader.InsightsPanel.getActiveTabName(); - await t.expect(tab).eql('Explore'); + await t.expect(tab).eql('Tutorials'); await t.expect(rdiInstancePage.RdiHeader.cloudSignInButton.exists).ok('sight in button is not exist'); }); diff --git a/tests/e2e/tests/web/regression/workbench/workbench-tab.e2e.ts b/tests/e2e/tests/web/regression/workbench/workbench-tab.e2e.ts new file mode 100644 index 0000000000..02f71ece03 --- /dev/null +++ b/tests/e2e/tests/web/regression/workbench/workbench-tab.e2e.ts @@ -0,0 +1,27 @@ +import { ExploreTabs, KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { DatabaseHelper } from '../../../../helpers/database'; +import { BrowserPage } from '../../../../pageObjects'; +import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; +import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; + +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); +const browserPage = new BrowserPage(); + +fixture `Autocomplete for entered commands` + .meta({ type: 'regression', rte: rte.standalone }) + .page(commonUrl) + .beforeEach(async t => { + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); + }) + .afterEach(async() => { + // Delete database + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); + }); +test.only('Verify that tutorials can be opened from Workbench"', async t => { + const workbench = await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(workbench.basicUseCaseTutorialsButton); + await t.expect(workbench.InsightsPanel.sidePanel.exists).ok('Insight panel is not opened'); + const tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials); + await t.expect(tab.preselectArea.textContent).contains('BASIC REDIS USE CASES', 'the tutorial page is incorrect'); +}); From da577da1e6ba8cd405fc2623af2326456a08f86b Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Wed, 3 Jul 2024 10:57:26 +0200 Subject: [PATCH 014/112] fix per comments --- tests/e2e/tests/web/regression/workbench/workbench-tab.e2e.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/tests/web/regression/workbench/workbench-tab.e2e.ts b/tests/e2e/tests/web/regression/workbench/workbench-tab.e2e.ts index 02f71ece03..57c1898186 100644 --- a/tests/e2e/tests/web/regression/workbench/workbench-tab.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/workbench-tab.e2e.ts @@ -18,7 +18,7 @@ fixture `Autocomplete for entered commands` // Delete database await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); -test.only('Verify that tutorials can be opened from Workbench"', async t => { +test('Verify that tutorials can be opened from Workbench', async t => { const workbench = await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); await t.click(workbench.basicUseCaseTutorialsButton); await t.expect(workbench.InsightsPanel.sidePanel.exists).ok('Insight panel is not opened'); From 0f25ab6c1fbde92d4abdf12b2ef39d37268cca03 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Fri, 26 Jul 2024 11:10:25 +0200 Subject: [PATCH 015/112] #RI-5683 - update search and query page --- redisinsight/ui/src/components/query/index.ts | 9 ++ .../query-actions/QueryActions.spec.tsx | 0 .../query}/query-actions/QueryActions.tsx | 86 ++++++------- .../query}/query-actions/index.ts | 0 .../query}/query-actions/styles.module.scss | 0 .../{ => query}/query-card/QueryCard.spec.tsx | 0 .../{ => query}/query-card/QueryCard.tsx | 0 .../QueryCardCliDefaultResult.spec.tsx | 0 .../QueryCardCliDefaultResult.tsx | 0 .../QueryCardCliDefaultResult/index.ts | 0 .../styles.module.scss | 0 .../QueryCardCliGroupResult.spec.tsx | 0 .../QueryCardCliGroupResult.tsx | 0 .../QueryCardCliGroupResult/index.ts | 0 .../QueryCardCliPlugin.spec.tsx | 0 .../QueryCardCliPlugin/QueryCardCliPlugin.tsx | 0 .../query-card/QueryCardCliPlugin/index.ts | 0 .../QueryCardCliPlugin/styles.module.scss | 0 .../QueryCardCliResultWrapper.spec.tsx | 0 .../QueryCardCliResultWrapper.tsx | 0 .../QueryCardCliResultWrapper/index.ts | 0 .../styles.module.scss | 0 .../QueryCardCommonResult.spec.tsx | 0 .../QueryCardCommonResult.tsx | 0 .../CommonErrorResponse.tsx | 0 .../components/CommonErrorResponse/index.ts | 0 .../query-card/QueryCardCommonResult/index.ts | 0 .../QueryCardCommonResult/styles.module.scss | 0 .../QueryCardHeader/QueryCardHeader.spec.tsx | 0 .../QueryCardHeader/QueryCardHeader.tsx | 0 .../query-card/QueryCardHeader/index.ts | 0 .../QueryCardHeader/styles.module.scss | 0 .../QueryCardTooltip.spec.tsx | 0 .../QueryCardTooltip/QueryCardTooltip.tsx | 0 .../query-card/QueryCardTooltip/index.ts | 0 .../QueryCardTooltip/styles.module.scss | 0 .../{ => query}/query-card/index.ts | 0 .../{ => query}/query-card/styles.module.scss | 0 .../query-tutorials/QueryTutorials.spec.tsx | 28 ++++- .../query}/query-tutorials/QueryTutorials.tsx | 30 ++--- .../query}/query-tutorials/index.ts | 0 .../query}/query-tutorials/styles.module.scss | 0 .../ui/src/constants/monaco/monaco.ts | 19 +++ redisinsight/ui/src/constants/storage.ts | 1 + redisinsight/ui/src/constants/tutorials.ts | 2 + .../ui/src/pages/search/SearchPage.tsx | 116 +++++++++++++++++- .../ui/src/pages/search/components/index.ts | 7 ++ .../search/components/query/Query.spec.tsx | 10 ++ .../pages/search/components/query/Query.tsx | 105 ++++++++++++++++ .../search/components/query/constants.ts | 16 +++ .../pages/search/components/query/index.ts | 3 + .../components/query/styles.module.scss | 76 ++++++++++++ .../results-history/ResultsHistory.spec.tsx | 10 ++ .../results-history/ResultsHistory.tsx | 87 +++++++++++++ .../components/results-history/index.ts | 3 + .../results-history/styles.module.scss | 33 +++++ .../ui/src/pages/search/styles.module.scss | 32 +++++ .../components/query/Query/Query.tsx | 39 ++---- .../components/query/Query/constants.ts | 24 ++++ .../wb-results/WBResults/WBResults.tsx | 2 +- redisinsight/ui/src/slices/app/context.ts | 16 +++ redisinsight/ui/src/slices/interfaces/app.ts | 8 ++ .../ui/src/slices/interfaces/index.ts | 3 +- .../src/slices/interfaces/searchAndQuery.ts | 11 ++ .../ui/src/slices/search/searchAndQuery.ts | 34 +++++ redisinsight/ui/src/slices/store.ts | 4 + redisinsight/ui/src/telemetry/events.ts | 2 + redisinsight/ui/src/telemetry/pageViews.ts | 1 + redisinsight/ui/src/utils/test-utils.tsx | 4 + 69 files changed, 720 insertions(+), 101 deletions(-) create mode 100644 redisinsight/ui/src/components/query/index.ts rename redisinsight/ui/src/{pages/workbench/components => components/query}/query-actions/QueryActions.spec.tsx (100%) rename redisinsight/ui/src/{pages/workbench/components => components/query}/query-actions/QueryActions.tsx (62%) rename redisinsight/ui/src/{pages/workbench/components => components/query}/query-actions/index.ts (100%) rename redisinsight/ui/src/{pages/workbench/components => components/query}/query-actions/styles.module.scss (100%) rename redisinsight/ui/src/components/{ => query}/query-card/QueryCard.spec.tsx (100%) rename redisinsight/ui/src/components/{ => query}/query-card/QueryCard.tsx (100%) rename redisinsight/ui/src/components/{ => query}/query-card/QueryCardCliDefaultResult/QueryCardCliDefaultResult.spec.tsx (100%) rename redisinsight/ui/src/components/{ => query}/query-card/QueryCardCliDefaultResult/QueryCardCliDefaultResult.tsx (100%) rename redisinsight/ui/src/components/{ => query}/query-card/QueryCardCliDefaultResult/index.ts (100%) rename redisinsight/ui/src/components/{ => query}/query-card/QueryCardCliDefaultResult/styles.module.scss (100%) rename redisinsight/ui/src/components/{ => query}/query-card/QueryCardCliGroupResult/QueryCardCliGroupResult.spec.tsx (100%) rename redisinsight/ui/src/components/{ => query}/query-card/QueryCardCliGroupResult/QueryCardCliGroupResult.tsx (100%) rename redisinsight/ui/src/components/{ => query}/query-card/QueryCardCliGroupResult/index.ts (100%) rename redisinsight/ui/src/components/{ => query}/query-card/QueryCardCliPlugin/QueryCardCliPlugin.spec.tsx (100%) rename redisinsight/ui/src/components/{ => query}/query-card/QueryCardCliPlugin/QueryCardCliPlugin.tsx (100%) rename redisinsight/ui/src/components/{ => query}/query-card/QueryCardCliPlugin/index.ts (100%) rename redisinsight/ui/src/components/{ => query}/query-card/QueryCardCliPlugin/styles.module.scss (100%) rename redisinsight/ui/src/components/{ => query}/query-card/QueryCardCliResultWrapper/QueryCardCliResultWrapper.spec.tsx (100%) rename redisinsight/ui/src/components/{ => query}/query-card/QueryCardCliResultWrapper/QueryCardCliResultWrapper.tsx (100%) rename redisinsight/ui/src/components/{ => query}/query-card/QueryCardCliResultWrapper/index.ts (100%) rename redisinsight/ui/src/components/{ => query}/query-card/QueryCardCliResultWrapper/styles.module.scss (100%) rename redisinsight/ui/src/components/{ => query}/query-card/QueryCardCommonResult/QueryCardCommonResult.spec.tsx (100%) rename redisinsight/ui/src/components/{ => query}/query-card/QueryCardCommonResult/QueryCardCommonResult.tsx (100%) rename redisinsight/ui/src/components/{ => query}/query-card/QueryCardCommonResult/components/CommonErrorResponse/CommonErrorResponse.tsx (100%) rename redisinsight/ui/src/components/{ => query}/query-card/QueryCardCommonResult/components/CommonErrorResponse/index.ts (100%) rename redisinsight/ui/src/components/{ => query}/query-card/QueryCardCommonResult/index.ts (100%) rename redisinsight/ui/src/components/{ => query}/query-card/QueryCardCommonResult/styles.module.scss (100%) rename redisinsight/ui/src/components/{ => query}/query-card/QueryCardHeader/QueryCardHeader.spec.tsx (100%) rename redisinsight/ui/src/components/{ => query}/query-card/QueryCardHeader/QueryCardHeader.tsx (100%) rename redisinsight/ui/src/components/{ => query}/query-card/QueryCardHeader/index.ts (100%) rename redisinsight/ui/src/components/{ => query}/query-card/QueryCardHeader/styles.module.scss (100%) rename redisinsight/ui/src/components/{ => query}/query-card/QueryCardTooltip/QueryCardTooltip.spec.tsx (100%) rename redisinsight/ui/src/components/{ => query}/query-card/QueryCardTooltip/QueryCardTooltip.tsx (100%) rename redisinsight/ui/src/components/{ => query}/query-card/QueryCardTooltip/index.ts (100%) rename redisinsight/ui/src/components/{ => query}/query-card/QueryCardTooltip/styles.module.scss (100%) rename redisinsight/ui/src/components/{ => query}/query-card/index.ts (100%) rename redisinsight/ui/src/components/{ => query}/query-card/styles.module.scss (100%) rename redisinsight/ui/src/{pages/workbench/components => components/query}/query-tutorials/QueryTutorials.spec.tsx (66%) rename redisinsight/ui/src/{pages/workbench/components => components/query}/query-tutorials/QueryTutorials.tsx (73%) rename redisinsight/ui/src/{pages/workbench/components => components/query}/query-tutorials/index.ts (100%) rename redisinsight/ui/src/{pages/workbench/components => components/query}/query-tutorials/styles.module.scss (100%) create mode 100644 redisinsight/ui/src/pages/search/components/index.ts create mode 100644 redisinsight/ui/src/pages/search/components/query/Query.spec.tsx create mode 100644 redisinsight/ui/src/pages/search/components/query/Query.tsx create mode 100644 redisinsight/ui/src/pages/search/components/query/constants.ts create mode 100644 redisinsight/ui/src/pages/search/components/query/index.ts create mode 100644 redisinsight/ui/src/pages/search/components/query/styles.module.scss create mode 100644 redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.spec.tsx create mode 100644 redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.tsx create mode 100644 redisinsight/ui/src/pages/search/components/results-history/index.ts create mode 100644 redisinsight/ui/src/pages/search/components/results-history/styles.module.scss create mode 100644 redisinsight/ui/src/pages/search/styles.module.scss create mode 100644 redisinsight/ui/src/pages/workbench/components/query/Query/constants.ts create mode 100644 redisinsight/ui/src/slices/interfaces/searchAndQuery.ts create mode 100644 redisinsight/ui/src/slices/search/searchAndQuery.ts diff --git a/redisinsight/ui/src/components/query/index.ts b/redisinsight/ui/src/components/query/index.ts new file mode 100644 index 0000000000..00c92dffa3 --- /dev/null +++ b/redisinsight/ui/src/components/query/index.ts @@ -0,0 +1,9 @@ +import QueryCard from './query-card' +import QueryActions from './query-actions' +import QueryTutorials from './query-tutorials' + +export { + QueryCard, + QueryActions, + QueryTutorials +} diff --git a/redisinsight/ui/src/pages/workbench/components/query-actions/QueryActions.spec.tsx b/redisinsight/ui/src/components/query/query-actions/QueryActions.spec.tsx similarity index 100% rename from redisinsight/ui/src/pages/workbench/components/query-actions/QueryActions.spec.tsx rename to redisinsight/ui/src/components/query/query-actions/QueryActions.spec.tsx diff --git a/redisinsight/ui/src/pages/workbench/components/query-actions/QueryActions.tsx b/redisinsight/ui/src/components/query/query-actions/QueryActions.tsx similarity index 62% rename from redisinsight/ui/src/pages/workbench/components/query-actions/QueryActions.tsx rename to redisinsight/ui/src/components/query/query-actions/QueryActions.tsx index 2650352ccd..b4764713a3 100644 --- a/redisinsight/ui/src/pages/workbench/components/query-actions/QueryActions.tsx +++ b/redisinsight/ui/src/components/query/query-actions/QueryActions.tsx @@ -14,8 +14,8 @@ import Divider from 'uiSrc/components/divider/Divider' import styles from './styles.module.scss' export interface Props { - onChangeMode: () => void - onChangeGroupMode: () => void + onChangeMode?: () => void + onChangeGroupMode?: () => void onSubmit: () => void activeMode: RunQueryMode resultsMode?: ResultsMode @@ -54,48 +54,52 @@ const QueryActions = (props: Props) => { return (
- - onChangeMode()} - iconType={RawModeIcon} - disabled={isLoading} - className={cx(styles.btn, styles.textBtn, { [styles.activeBtn]: activeMode === RunQueryMode.Raw })} - data-testid="btn-change-mode" + {onChangeMode && ( + - Raw mode - - - - Groups the command results into a single window. -
- When grouped, the results can be visualized only in the text format. - + onChangeMode()} + iconType={RawModeIcon} + disabled={isLoading} + className={cx(styles.btn, styles.textBtn, { [styles.activeBtn]: activeMode === RunQueryMode.Raw })} + data-testid="btn-change-mode" + > + Raw mode + +
+ )} + {onChangeGroupMode && ( + + Groups the command results into a single window. +
+ When grouped, the results can be visualized only in the text format. + )} - data-testid="group-results-tooltip" - > - onChangeGroupMode()} - disabled={isLoading} - iconType={GroupModeIcon} - className={cx(styles.btn, styles.textBtn, { [styles.activeBtn]: isGroupMode(resultsMode) })} - data-testid="btn-change-group-mode" + data-testid="group-results-tooltip" > - Group results - -
+ onChangeGroupMode()} + disabled={isLoading} + iconType={GroupModeIcon} + className={cx(styles.btn, styles.textBtn, { [styles.activeBtn]: isGroupMode(resultsMode) })} + data-testid="btn-change-group-mode" + > + Group results + + + )} ({ ...jest.requireActual('uiSrc/utils'), findTutorialPath: jest.fn(), @@ -20,7 +36,7 @@ jest.mock('uiSrc/telemetry', () => ({ describe('QueryTutorial', () => { it('should render', () => { - expect(render()).toBeTruthy() + expect(render()).toBeTruthy() }) it('should call proper history push after click on guide with tutorial', () => { @@ -28,9 +44,9 @@ describe('QueryTutorial', () => { reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock }); (findTutorialPath as jest.Mock).mockImplementation(() => 'path') - render() + render() - fireEvent.click(screen.getByTestId('wb-tutorials-link_sq-intro')) + fireEvent.click(screen.getByTestId('query-tutorials-link_sq-intro')) expect(pushMock).toHaveBeenCalledWith({ search: 'path=tutorials/path' @@ -42,16 +58,16 @@ describe('QueryTutorial', () => { (sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock); (findTutorialPath as jest.Mock).mockImplementation(() => 'path') - render() + render() - fireEvent.click(screen.getByTestId('wb-tutorials-link_sq-intro')) + fireEvent.click(screen.getByTestId('query-tutorials-link_sq-intro')) expect(sendEventTelemetry).toBeCalledWith({ event: TelemetryEvent.EXPLORE_PANEL_TUTORIAL_OPENED, eventData: { path: 'path', databaseId: 'instanceId', - source: 'advanced_workbench_editor', + source: 'source', } }) }) diff --git a/redisinsight/ui/src/pages/workbench/components/query-tutorials/QueryTutorials.tsx b/redisinsight/ui/src/components/query/query-tutorials/QueryTutorials.tsx similarity index 73% rename from redisinsight/ui/src/pages/workbench/components/query-tutorials/QueryTutorials.tsx rename to redisinsight/ui/src/components/query/query-tutorials/QueryTutorials.tsx index 83e765da76..0fed95df93 100644 --- a/redisinsight/ui/src/pages/workbench/components/query-tutorials/QueryTutorials.tsx +++ b/redisinsight/ui/src/components/query/query-tutorials/QueryTutorials.tsx @@ -4,28 +4,20 @@ import { EuiLink, EuiText } from '@elastic/eui' import { useDispatch } from 'react-redux' import { useHistory, useParams } from 'react-router-dom' import { findTutorialPath } from 'uiSrc/utils' -import { TutorialsIds } from 'uiSrc/constants' import { openTutorialByPath } from 'uiSrc/slices/panels/sidePanels' import { sendEventTelemetry, TELEMETRY_EMPTY_VALUE, TelemetryEvent } from 'uiSrc/telemetry' import styles from './styles.module.scss' -const TUTORIALS = [ - { - id: TutorialsIds.IntroToSearch, - title: 'Intro to search' - }, - { - id: TutorialsIds.BasicRedisUseCases, - title: 'Basic use cases' - }, - { - id: TutorialsIds.IntroVectorSearch, - title: 'Intro to vector search' - }, -] +export interface Props { + tutorials: Array<{ + id: string + title: string + }> + source: string +} -const QueryTutorials = () => { +const QueryTutorials = ({ tutorials, source }: Props) => { const dispatch = useDispatch() const history = useHistory() const { instanceId } = useParams<{ instanceId: string }>() @@ -39,7 +31,7 @@ const QueryTutorials = () => { eventData: { path: tutorialPath, databaseId: instanceId || TELEMETRY_EMPTY_VALUE, - source: 'advanced_workbench_editor', + source, } }) } @@ -49,13 +41,13 @@ const QueryTutorials = () => { Tutorials: - {TUTORIALS.map(({ id, title }) => ( + {tutorials.map(({ id, title }) => ( handleClickTutorial(id)} - data-testid={`wb-tutorials-link_${id}`} + data-testid={`query-tutorials-link_${id}`} > {title} diff --git a/redisinsight/ui/src/pages/workbench/components/query-tutorials/index.ts b/redisinsight/ui/src/components/query/query-tutorials/index.ts similarity index 100% rename from redisinsight/ui/src/pages/workbench/components/query-tutorials/index.ts rename to redisinsight/ui/src/components/query/query-tutorials/index.ts diff --git a/redisinsight/ui/src/pages/workbench/components/query-tutorials/styles.module.scss b/redisinsight/ui/src/components/query/query-tutorials/styles.module.scss similarity index 100% rename from redisinsight/ui/src/pages/workbench/components/query-tutorials/styles.module.scss rename to redisinsight/ui/src/components/query/query-tutorials/styles.module.scss diff --git a/redisinsight/ui/src/constants/monaco/monaco.ts b/redisinsight/ui/src/constants/monaco/monaco.ts index a1ba8b1300..fad4f5528c 100644 --- a/redisinsight/ui/src/constants/monaco/monaco.ts +++ b/redisinsight/ui/src/constants/monaco/monaco.ts @@ -41,6 +41,25 @@ export enum MonacoLanguage { Text = 'text', } +export const defaultMonacoOptions: monacoEditor.editor.IStandaloneEditorConstructionOptions = { + tabCompletion: 'on', + wordWrap: 'on', + padding: { top: 10 }, + automaticLayout: true, + formatOnPaste: false, + glyphMargin: true, + stickyScroll: { + enabled: true, + defaultModel: 'indentationModel' + }, + suggest: { + preview: true, + showStatusBar: true, + showIcons: false, + }, + lineNumbersMinChars: 4 +} + export const DEFAULT_MONACO_YAML_URI = 'http://example.com/schema-name.json' export const DEFAULT_MONACO_FILE_MATCH = '*' diff --git a/redisinsight/ui/src/constants/storage.ts b/redisinsight/ui/src/constants/storage.ts index bd92ec4bf4..f0eb778d49 100644 --- a/redisinsight/ui/src/constants/storage.ts +++ b/redisinsight/ui/src/constants/storage.ts @@ -19,6 +19,7 @@ enum BrowserStorageItem { bulkActionDeleteId = 'bulkActionDeleteId', dbConfig = 'dbConfig_', RunQueryMode = 'RunQueryMode', + SQRunQueryMode = 'SQRunQueryMode', wbCleanUp = 'wbCleanUp', viewFormat = 'viewFormat', wbGroupMode = 'wbGroupMode', diff --git a/redisinsight/ui/src/constants/tutorials.ts b/redisinsight/ui/src/constants/tutorials.ts index 45c856417e..261e38c3f2 100644 --- a/redisinsight/ui/src/constants/tutorials.ts +++ b/redisinsight/ui/src/constants/tutorials.ts @@ -4,6 +4,8 @@ enum TutorialsIds { BasicRedisUseCases = 'redis_use_cases_basic', RedisUseCases = 'redis_use_cases', IntroVectorSearch = 'vss-intro', + ExactMatch = 'sq-exact-match', + FullTextSearch = 'sq-full-text', } export { diff --git a/redisinsight/ui/src/pages/search/SearchPage.tsx b/redisinsight/ui/src/pages/search/SearchPage.tsx index aef47b2781..c48219b554 100644 --- a/redisinsight/ui/src/pages/search/SearchPage.tsx +++ b/redisinsight/ui/src/pages/search/SearchPage.tsx @@ -1,13 +1,119 @@ -import React from 'react' +import React, { useCallback, useEffect, useRef, useState } from 'react' +import cx from 'classnames' +import { EuiResizableContainer } from '@elastic/eui' +import { useDispatch, useSelector } from 'react-redux' -export interface Props { +import { useParams } from 'react-router-dom' +import { + appContextSearchAndQuery, + setSQVerticalPanelSizes, +} from 'uiSrc/slices/app/context' +import { Query, ResultsHistory } from 'uiSrc/pages/search/components' +import { sendWbQueryAction } from 'uiSrc/slices/workbench/wb-results' +import { formatLongName, getDbIndex, Nullable, setTitle } from 'uiSrc/utils' + +import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' +import { sendPageViewTelemetry, TelemetryPageView } from 'uiSrc/telemetry' +import { CodeButtonParams } from 'uiSrc/constants' +import styles from './styles.module.scss' + +const verticalPanelIds = { + firstPanelId: 'scriptingArea', + secondPanelId: 'resultsArea' } -const SearchPage = (props: Props) => { - const {} = props +const SearchPage = () => { + const { name: connectedInstanceName, db } = useSelector(connectedInstanceSelector) + const { panelSizes: { vertical } } = useSelector(appContextSearchAndQuery) + const [isPageViewSent, setIsPageViewSent] = useState(false) + + const { instanceId } = useParams<{ instanceId: string }>() + const verticalSizesRef = useRef(vertical) + + const dispatch = useDispatch() + + setTitle(`${formatLongName(connectedInstanceName, 33, 0, '...')} ${getDbIndex(db)} - Search and Query`) + + useEffect(() => () => { + dispatch(setSQVerticalPanelSizes(verticalSizesRef.current)) + }, []) + + useEffect(() => { + if (connectedInstanceName && !isPageViewSent) { + sendPageView(instanceId) + } + }, [connectedInstanceName, isPageViewSent]) + + const onVerticalPanelWidthChange = useCallback((newSizes: any) => { + verticalSizesRef.current = newSizes + }, []) + + const sendPageView = (instanceId: string) => { + sendPageViewTelemetry({ + name: TelemetryPageView.SEARCH_AND_QUERY_PAGE, + databaseId: instanceId + }) + setIsPageViewSent(true) + } + + const handleSubmit = ( + commandInit: string, + commandId?: Nullable, + executeParams: CodeButtonParams = {} + ) => { + dispatch(sendWbQueryAction( + commandInit, + commandId, + { + ...executeParams, + results: 'single', + } + )) + } + return ( -
+
+
+
+ + {(EuiResizablePanel, EuiResizableButton) => ( + <> + + + + + + + + + + + )} + +
+
+
) } diff --git a/redisinsight/ui/src/pages/search/components/index.ts b/redisinsight/ui/src/pages/search/components/index.ts new file mode 100644 index 0000000000..704de0e116 --- /dev/null +++ b/redisinsight/ui/src/pages/search/components/index.ts @@ -0,0 +1,7 @@ +import Query from './query' +import ResultsHistory from './results-history' + +export { + Query, + ResultsHistory, +} diff --git a/redisinsight/ui/src/pages/search/components/query/Query.spec.tsx b/redisinsight/ui/src/pages/search/components/query/Query.spec.tsx new file mode 100644 index 0000000000..e87a00d627 --- /dev/null +++ b/redisinsight/ui/src/pages/search/components/query/Query.spec.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import { render, screen } from 'uiSrc/utils/test-utils' + +import Query from './Query' + +describe('Query', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/search/components/query/Query.tsx b/redisinsight/ui/src/pages/search/components/query/Query.tsx new file mode 100644 index 0000000000..f01c7ffe92 --- /dev/null +++ b/redisinsight/ui/src/pages/search/components/query/Query.tsx @@ -0,0 +1,105 @@ +import React, { useContext, useEffect, useRef, useState } from 'react' +import MonacoEditor from 'react-monaco-editor' +import { useDispatch, useSelector } from 'react-redux' +import { useParams } from 'react-router-dom' +import { QueryActions, QueryTutorials } from 'uiSrc/components/query' + +import { RunQueryMode } from 'uiSrc/slices/interfaces' +import { CodeButtonParams, defaultMonacoOptions, Theme } from 'uiSrc/constants' +import { ThemeContext } from 'uiSrc/contexts/themeContext' + +import { Nullable } from 'uiSrc/utils' +import { changeSQActiveRunQueryMode, searchAndQuerySelector } from 'uiSrc/slices/search/searchAndQuery' +import { appContextSearchAndQuery, setSQVerticalScript } from 'uiSrc/slices/app/context' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { TUTORIALS } from './constants' + +import styles from './styles.module.scss' + +export interface Props { + onSubmit: ( + commandInit: string, + commandId?: Nullable, + executeParams?: CodeButtonParams + ) => void +} + +const options = { ...defaultMonacoOptions } + +const Query = (props: Props) => { + const { onSubmit } = props + + const { script: scriptContext } = useSelector(appContextSearchAndQuery) + const { activeRunQueryMode } = useSelector(searchAndQuerySelector) + const [value, setValue] = useState(scriptContext) + + const { theme } = useContext(ThemeContext) + const input = useRef(null) + const scriptRef = useRef('') + + const { instanceId } = useParams<{ instanceId: string }>() + + const dispatch = useDispatch() + + useEffect(() => () => { + dispatch(setSQVerticalScript(scriptRef.current)) + }, []) + + useEffect(() => { + scriptRef.current = value + }, [value]) + + const handleChangeQueryRunMode = () => { + dispatch(changeSQActiveRunQueryMode( + activeRunQueryMode === RunQueryMode.ASCII + ? RunQueryMode.Raw + : RunQueryMode.ASCII + )) + } + + const handleSubmit = () => { + onSubmit(value, undefined, { mode: activeRunQueryMode }) + sendEventTelemetry({ + event: TelemetryEvent.SEARCH_COMMAND_SUBMITTED, + eventData: { + databaseId: instanceId, + mode: activeRunQueryMode, + // TODO sanitize user query + command: value + } + }) + } + + return ( +
+
{}} + role="textbox" + tabIndex={0} + data-testid="main-input-container-area" + > +
+ setValue(val)} + language="RediSearch" + theme={theme === Theme.Dark ? 'dark' : 'light'} + options={options} + /> +
+
+ + +
+
+
+ ) +} + +export default Query diff --git a/redisinsight/ui/src/pages/search/components/query/constants.ts b/redisinsight/ui/src/pages/search/components/query/constants.ts new file mode 100644 index 0000000000..322307978f --- /dev/null +++ b/redisinsight/ui/src/pages/search/components/query/constants.ts @@ -0,0 +1,16 @@ +import { TutorialsIds } from 'uiSrc/constants' + +export const TUTORIALS = [ + { + id: TutorialsIds.ExactMatch, + title: 'Exact match' + }, + { + id: TutorialsIds.FullTextSearch, + title: 'Full-text search' + }, + { + id: TutorialsIds.IntroVectorSearch, + title: 'Intro to vector search' + }, +] diff --git a/redisinsight/ui/src/pages/search/components/query/index.ts b/redisinsight/ui/src/pages/search/components/query/index.ts new file mode 100644 index 0000000000..611583bbb6 --- /dev/null +++ b/redisinsight/ui/src/pages/search/components/query/index.ts @@ -0,0 +1,3 @@ +import Query from './Query' + +export default Query diff --git a/redisinsight/ui/src/pages/search/components/query/styles.module.scss b/redisinsight/ui/src/pages/search/components/query/styles.module.scss new file mode 100644 index 0000000000..18b46e1931 --- /dev/null +++ b/redisinsight/ui/src/pages/search/components/query/styles.module.scss @@ -0,0 +1,76 @@ +.wrapper { + position: relative; + height: 100%; + + :global(.editorBounder) { + bottom: 6px; + left: 18px; + right: 46px; + } +} + +.container { + display: flex; + flex-direction: column; + padding: 8px 16px; + width: 100%; + height: 100%; + word-break: break-word; + text-align: left; + letter-spacing: 0; + background-color: var(--rsInputWrapperColor); + color: var(--euiTextSubduedColor) !important; + border: 1px solid var(--euiColorLightShade); +} + +.disabled { + opacity: 0.8; +} + +.disabledActions { + pointer-events: none; + user-select: none; +} + +.containerPlaceholder { + display: flex; + padding: 8px 16px 8px 16px; + width: 100%; + height: 100%; + background-color: var(--rsInputWrapperColor); + color: var(--euiTextSubduedColor) !important; + border: 1px solid var(--euiColorLightShade); + > div { + border: 1px solid var(--euiColorLightShade); + background-color: var(--euiColorEmptyShade); + padding: 8px 20px; + width: 100%; + } +} + +.input { + // cannot use overflow since suggestions are absolute + max-height: calc(100% - 32px); + flex-grow: 1; + width: 100%; + border: 1px solid var(--euiColorLightShade); + background-color: var(--rsInputColor); +} + +.queryFooter { + display: flex; + align-items: center; + justify-content: space-between; + + margin-top: 8px; + flex-shrink: 0; +} + +#script { + font: normal normal bold 14px/17px Inconsolata !important; + color: var(--textColorShade); + caret-color: var(--euiColorFullShade); + min-width: 5px; + display: inline; +} + diff --git a/redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.spec.tsx b/redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.spec.tsx new file mode 100644 index 0000000000..21d2666793 --- /dev/null +++ b/redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.spec.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import { render, screen } from 'uiSrc/utils/test-utils' + +import ResultsHistory from './ResultsHistory' + +describe('ResultsHistory', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.tsx b/redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.tsx new file mode 100644 index 0000000000..b15b21dace --- /dev/null +++ b/redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.tsx @@ -0,0 +1,87 @@ +import React, { useEffect } from 'react' +import cx from 'classnames' + +import { useDispatch, useSelector } from 'react-redux' +import { useParams } from 'react-router-dom' + +import { Nullable } from 'uiSrc/utils' +import { QueryCard } from 'uiSrc/components/query' +import { fetchWBCommandAction, fetchWBHistoryAction, workbenchResultsSelector } from 'uiSrc/slices/workbench/wb-results' +import { searchAndQuerySelector } from 'uiSrc/slices/search/searchAndQuery' + +import styles from './styles.module.scss' + +export interface Props { + onSubmit: ( + commandInit: string, + commandId?: Nullable, + ) => void +} + +const ResultsHistory = (props: Props) => { + const { onSubmit } = props + const { + items, + clearing, + } = useSelector(workbenchResultsSelector) + const { activeRunQueryMode } = useSelector(searchAndQuerySelector) + + const dispatch = useDispatch() + const { instanceId } = useParams<{ instanceId: string }>() + + useEffect(() => { + dispatch(fetchWBHistoryAction(instanceId)) + }, []) + + const handleQueryOpen = (commandId: string = '') => { + dispatch(fetchWBCommandAction(commandId)) + } + + return ( +
+
+
+ {items?.length ? items.map(( + { + command = '', + isOpen = false, + result = undefined, + summary = undefined, + id = '', + loading, + createdAt, + mode, + emptyCommand, + isNotStored, + executionTime, + db, + } + ) => ( + {}} + onQueryOpen={() => handleQueryOpen(id)} + onQueryReRun={() => onSubmit(command, id)} + onQueryDelete={() => {}} + /> + )) : null} +
+
+ ) +} + +export default ResultsHistory diff --git a/redisinsight/ui/src/pages/search/components/results-history/index.ts b/redisinsight/ui/src/pages/search/components/results-history/index.ts new file mode 100644 index 0000000000..a38f637f57 --- /dev/null +++ b/redisinsight/ui/src/pages/search/components/results-history/index.ts @@ -0,0 +1,3 @@ +import ResultsHistory from './ResultsHistory' + +export default ResultsHistory diff --git a/redisinsight/ui/src/pages/search/components/results-history/styles.module.scss b/redisinsight/ui/src/pages/search/components/results-history/styles.module.scss new file mode 100644 index 0000000000..7ea2ec299b --- /dev/null +++ b/redisinsight/ui/src/pages/search/components/results-history/styles.module.scss @@ -0,0 +1,33 @@ +.wrapper { + flex: 1; + height: 100%; + width: 100%; + background-color: var(--euiColorEmptyShade); + border: 1px solid var(--euiColorLightShade); + + display: flex; + flex-direction: column; + + position: relative; +} + +.container { + @include eui.scrollBar; + color: var(--euiTextSubduedColor) !important; + + flex: 1; + width: 100%; + overflow: auto; +} + +.header { + height: 42px; + display: flex; + align-items: center; + justify-content: flex-end; + padding: 0 12px; + + flex-shrink: 0; + border-bottom: 1px solid var(--tableDarkestBorderColor); +} + diff --git a/redisinsight/ui/src/pages/search/styles.module.scss b/redisinsight/ui/src/pages/search/styles.module.scss new file mode 100644 index 0000000000..e371f27293 --- /dev/null +++ b/redisinsight/ui/src/pages/search/styles.module.scss @@ -0,0 +1,32 @@ +.container { + min-height: 100%; + height: 1px !important; + display: flex; + flex-direction: column; +} + +.main { + display: flex; + flex: 1; + padding: 0 16px 0; + height: 100%; + width: 100%; +} + +.content { + display: flex; + flex-grow: 1; + width: 100%; +} + +.resizeButton { + z-index: 1 !important; +} + +.queryPanel { + padding-bottom: 8px; +} + +.queryResultsPanel { + padding-top: 8px; +} diff --git a/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx b/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx index 42ab11c6a0..f415fa85ca 100644 --- a/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx +++ b/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx @@ -32,9 +32,17 @@ import { RunQueryMode, ResultsMode } from 'uiSrc/slices/interfaces/workbench' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { stopProcessing, workbenchResultsSelector } from 'uiSrc/slices/workbench/wb-results' import DedicatedEditor from 'uiSrc/components/monaco-editor/components/dedicated-editor' +import { QueryActions, QueryTutorials } from 'uiSrc/components/query' + +import { + aroundQuotesRegExp, + argInQuotesRegExp, + SYNTAX_CONTEXT_ID, + SYNTAX_WIDGET_ID, + options, + TUTORIALS +} from './constants' -import QueryActions from '../../query-actions' -import QueryTutorials from '../../query-tutorials' import styles from './styles.module.scss' export interface Props { @@ -49,12 +57,6 @@ export interface Props { onChangeGroupMode: () => void } -const SYNTAX_CONTEXT_ID = 'syntaxWidgetContext' -const SYNTAX_WIDGET_ID = 'syntax.content.widget' - -const argInQuotesRegExp = /^['"](.|[\r\n])*['"]$/ -const aroundQuotesRegExp = /(^["']|["']$)/g - let execHistoryPos: number = 0 let execHistory: CommandExecutionUI[] = [] let decorationCollection: Nullable = null @@ -426,25 +428,6 @@ const Query = (props: Props) => { ).dispose } - const options: monacoEditor.editor.IStandaloneEditorConstructionOptions = { - tabCompletion: 'on', - wordWrap: 'on', - padding: { top: 10 }, - automaticLayout: true, - formatOnPaste: false, - glyphMargin: true, - stickyScroll: { - enabled: true, - defaultModel: 'indentationModel' - }, - suggest: { - preview: true, - showStatusBar: true, - showIcons: false, - }, - lineNumbersMinChars: 4 - } - const isLoading = loading || processing return ( @@ -468,7 +451,7 @@ const Query = (props: Props) => { />
- + { state.workbench.panelSizes.vertical = payload }, + setSQVerticalPanelSizes: (state, { payload }: { payload: any }) => { + state.searchAndQuery.panelSizes.vertical = payload + }, + setSQVerticalScript: (state, { payload }: { payload: any }) => { + state.searchAndQuery.script = payload + }, setLastPageContext: (state, { payload }: { payload: string }) => { state.lastBrowserPage = payload }, @@ -248,6 +260,8 @@ export const { resetBrowserTree, setWorkbenchScript, setWorkbenchVerticalPanelSizes, + setSQVerticalPanelSizes, + setSQVerticalScript, setLastPageContext, setPubSubFieldsContext, setBrowserBulkActionOpen, @@ -276,6 +290,8 @@ export const appContextBrowserKeyDetails = (state: RootState) => state.app.context.browser.keyDetailsSizes export const appContextWorkbench = (state: RootState) => state.app.context.workbench +export const appContextSearchAndQuery = (state: RootState) => + state.app.context.searchAndQuery export const appContextSelectedKey = (state: RootState) => state.app.context.browser.keyList.selectedKey export const appContextPubSub = (state: RootState) => diff --git a/redisinsight/ui/src/slices/interfaces/app.ts b/redisinsight/ui/src/slices/interfaces/app.ts index 00cd49aa91..ba7602c322 100644 --- a/redisinsight/ui/src/slices/interfaces/app.ts +++ b/redisinsight/ui/src/slices/interfaces/app.ts @@ -106,6 +106,14 @@ export interface StateAppContext { } } } + searchAndQuery: { + script: string + panelSizes: { + vertical: { + [key: string]: number + } + } + } pubsub: { channel: string message: string diff --git a/redisinsight/ui/src/slices/interfaces/index.ts b/redisinsight/ui/src/slices/interfaces/index.ts index e3294a071d..052fd92082 100644 --- a/redisinsight/ui/src/slices/interfaces/index.ts +++ b/redisinsight/ui/src/slices/interfaces/index.ts @@ -2,9 +2,10 @@ export * from './instances' export * from './hash' export * from './app' export * from './workbench' +export * from './redisearch' export * from './monitor' export * from './api' export * from './bulkActions' -export * from './redisearch' +export * from './searchAndQuery' export * from './cloud' export * from './rdi' diff --git a/redisinsight/ui/src/slices/interfaces/searchAndQuery.ts b/redisinsight/ui/src/slices/interfaces/searchAndQuery.ts new file mode 100644 index 0000000000..dd75a9be03 --- /dev/null +++ b/redisinsight/ui/src/slices/interfaces/searchAndQuery.ts @@ -0,0 +1,11 @@ +import { CommandExecutionUI, RunQueryMode } from 'uiSrc/slices/interfaces/workbench' + +export interface StateSearchAndQuery { + isLoaded: boolean + loading: boolean + processing: boolean + clearing: boolean + error: string + items: CommandExecutionUI[] + activeRunQueryMode: RunQueryMode +} diff --git a/redisinsight/ui/src/slices/search/searchAndQuery.ts b/redisinsight/ui/src/slices/search/searchAndQuery.ts new file mode 100644 index 0000000000..76b3c147fd --- /dev/null +++ b/redisinsight/ui/src/slices/search/searchAndQuery.ts @@ -0,0 +1,34 @@ +import { createSlice } from '@reduxjs/toolkit' +import { RunQueryMode, StateSearchAndQuery } from 'uiSrc/slices/interfaces' +import { localStorageService } from 'uiSrc/services' +import { BrowserStorageItem } from 'uiSrc/constants' +import { RootState } from 'uiSrc/slices/store' + +export const initialState: StateSearchAndQuery = { + isLoaded: false, + loading: false, + processing: false, + clearing: false, + error: '', + items: [], + activeRunQueryMode: localStorageService?.get(BrowserStorageItem.SQRunQueryMode) ?? RunQueryMode.ASCII, +} + +const searchAndQuerySlice = createSlice({ + name: 'searchAndQuery', + initialState, + reducers: { + changeSQActiveRunQueryMode: (state, { payload }) => { + state.activeRunQueryMode = payload + localStorageService.set(BrowserStorageItem.SQRunQueryMode, payload) + }, + } +}) + +export const searchAndQuerySelector = (state: RootState) => state.search.query + +export default searchAndQuerySlice.reducer + +export const { + changeSQActiveRunQueryMode, +} = searchAndQuerySlice.actions diff --git a/redisinsight/ui/src/slices/store.ts b/redisinsight/ui/src/slices/store.ts index c7a3bbf4e4..89ede143e9 100644 --- a/redisinsight/ui/src/slices/store.ts +++ b/redisinsight/ui/src/slices/store.ts @@ -32,6 +32,7 @@ import appOauthReducer from './oauth/cloud' import workbenchResultsReducer from './workbench/wb-results' import workbenchTutorialsReducer from './workbench/wb-tutorials' import workbenchCustomTutorialsReducer from './workbench/wb-custom-tutorials' +import searchAndQueryReducer from './search/searchAndQuery' import contentCreateRedisButtonReducer from './content/create-redis-buttons' import contentGuideLinksReducer from './content/guide-links' import pubSubReducer from './pubsub/pubsub' @@ -95,6 +96,9 @@ export const rootReducer = combineReducers({ tutorials: workbenchTutorialsReducer, customTutorials: workbenchCustomTutorialsReducer, }), + search: combineReducers({ + query: searchAndQueryReducer, + }), content: combineReducers({ createRedisButtons: contentCreateRedisButtonReducer, guideLinks: contentGuideLinksReducer, diff --git a/redisinsight/ui/src/telemetry/events.ts b/redisinsight/ui/src/telemetry/events.ts index 31a3c90858..680cb5e3c9 100644 --- a/redisinsight/ui/src/telemetry/events.ts +++ b/redisinsight/ui/src/telemetry/events.ts @@ -124,6 +124,8 @@ export enum TelemetryEvent { WORKBENCH_CLEAR_RESULT_CLICKED = 'WORKBENCH_CLEAR_RESULT_CLICKED', WORKBENCH_CLEAR_ALL_RESULTS_CLICKED = 'WORKBENCH_CLEAR_ALL_RESULTS_CLICKED', + SEARCH_COMMAND_SUBMITTED = 'SEARCH_COMMAND_SUBMITTED', + PROFILER_OPENED = 'PROFILER_OPENED', PROFILER_STARTED = 'PROFILER_STARTED', PROFILER_STOPPED = 'PROFILER_STOPPED', diff --git a/redisinsight/ui/src/telemetry/pageViews.ts b/redisinsight/ui/src/telemetry/pageViews.ts index 9562baebc5..1d4960beb0 100644 --- a/redisinsight/ui/src/telemetry/pageViews.ts +++ b/redisinsight/ui/src/telemetry/pageViews.ts @@ -4,6 +4,7 @@ export enum TelemetryPageView { SETTINGS_PAGE = 'Settings', BROWSER_PAGE = 'Browser', WORKBENCH_PAGE = 'Workbench', + SEARCH_AND_QUERY_PAGE = 'Search and Query', SLOWLOG_PAGE = 'Slow Log', CLUSTER_DETAILS_PAGE = 'Overview', PUBSUB_PAGE = 'Pub/Sub', diff --git a/redisinsight/ui/src/utils/test-utils.tsx b/redisinsight/ui/src/utils/test-utils.tsx index ae0d35d545..f6464e8005 100644 --- a/redisinsight/ui/src/utils/test-utils.tsx +++ b/redisinsight/ui/src/utils/test-utils.tsx @@ -38,6 +38,7 @@ import { initialState as initialStateUserSettings } from 'uiSrc/slices/user/user import { initialState as initialStateWBResults } from 'uiSrc/slices/workbench/wb-results' import { initialState as initialStateWBETutorials } from 'uiSrc/slices/workbench/wb-tutorials' import { initialState as initialStateWBECustomTutorials } from 'uiSrc/slices/workbench/wb-custom-tutorials' +import { initialState as initialStateSearchAndQuery } from 'uiSrc/slices/search/searchAndQuery' import { initialState as initialStateCreateRedisButtons } from 'uiSrc/slices/content/create-redis-buttons' import { initialState as initialStateGuideLinks } from 'uiSrc/slices/content/guide-links' import { initialState as initialStateSlowLog } from 'uiSrc/slices/analytics/slowlog' @@ -110,6 +111,9 @@ const initialStateDefault: RootState = { tutorials: cloneDeep(initialStateWBETutorials), customTutorials: cloneDeep(initialStateWBECustomTutorials), }, + search: { + query: cloneDeep(initialStateSearchAndQuery), + }, content: { createRedisButtons: cloneDeep(initialStateCreateRedisButtons), guideLinks: cloneDeep(initialStateGuideLinks), From ba71e159544e5c449d56f3827784a569f4d4bfe8 Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Mon, 29 Jul 2024 14:49:42 +0200 Subject: [PATCH 016/112] create a base class for the pages --- .../e2e/pageObjects/base-run-commands-page.ts | 78 +++++++++++++++++++ .../e2e/pageObjects/search-and-query-page.ts | 5 +- tests/e2e/pageObjects/workbench-page.ts | 69 +--------------- .../search-and-query/search-and-query-tab.ts | 27 +++++++ .../regression/workbench/workbench-tab.e2e.ts | 2 +- 5 files changed, 112 insertions(+), 69 deletions(-) create mode 100644 tests/e2e/pageObjects/base-run-commands-page.ts create mode 100644 tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.ts diff --git a/tests/e2e/pageObjects/base-run-commands-page.ts b/tests/e2e/pageObjects/base-run-commands-page.ts new file mode 100644 index 0000000000..becdb303b2 --- /dev/null +++ b/tests/e2e/pageObjects/base-run-commands-page.ts @@ -0,0 +1,78 @@ +import { Selector, t } from 'testcafe'; +import { InstancePage } from './instance-page'; + +export class BaseRunCommandsPage extends InstancePage { + + submitCommandButton = Selector('[data-testid=btn-submit]'); + queryInput = Selector('[data-testid=query-input-container]'); + + // History containers + queryCardCommand = Selector('[data-testid=query-card-command]'); + fullScreenButton = Selector('[data-testid=toggle-full-screen]'); + rawModeBtn = Selector('[data-testid="btn-change-mode"]'); + queryCardContainer = Selector('[data-testid^=query-card-container]'); + reRunCommandButton = Selector('[data-testid=re-run-command]'); + copyBtn = Selector('[data-testid^=copy-btn-]'); + copyCommand = Selector('[data-testid=copy-command]'); + + runButtonToolTip = Selector('[data-testid=run-query-tooltip]'); + loadedCommand = Selector('[class=euiLoadingContent__singleLine]'); + runButtonSpinner = Selector('[data-testid=loading-spinner]'); + commandExecutionDateAndTime = Selector('[data-testid=command-execution-date-time]'); + executionCommandTime = Selector('[data-testid=command-execution-time-value]'); + executionCommandIcon = Selector('[data-testid=command-execution-time-icon]'); + executedCommandTitle = Selector('[data-testid=query-card-tooltip-anchor]', { timeout: 500 }); + queryResult = Selector('[data-testid=query-common-result]'); + + cssQueryCardCommand = '[data-testid=query-card-command]'; + cssQueryTextResult = '[data-testid=query-cli-result]'; + cssReRunCommandButton = '[data-testid=re-run-command]'; + cssDeleteCommandButton = '[data-testid=delete-command]'; + + getTutorialLinkLocator = (tutorialName: string): Selector => + Selector(`[@data-testid="data-testid=query-tutorials-link_${tutorialName}"]`); + + /** + * Get card container by command + * @param command The command + */ + async getCardContainerByCommand(command: string): Promise { + return this.queryCardCommand.withExactText(command).parent('[data-testid^="query-card-container-"]'); + } + + /** + * Send a command in Workbench + * @param command The command + * @param speed The speed in seconds. Default is 1 + * @param paste + */ + 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); + } + + /** + * Check the last command and result in workbench + * @param command The command to check + * @param result The result to check + * @param childNum Indicator which command result need to check + */ + async checkWorkbenchCommandResult(command: string, result: string, childNum = 0): Promise { + // Compare the command with executed command + const actualCommand = await this.queryCardContainer.nth(childNum).find(this.cssQueryCardCommand).textContent; + await t.expect(actualCommand).contains(command, 'Actual command is not equal to executed'); + // Compare the command result with executed command + const actualCommandResult = await this.queryCardContainer.nth(childNum).find(this.cssQueryTextResult).textContent; + await t.expect(actualCommandResult).contains(result, 'Actual command result is not equal to executed'); + } + + /** + * Get selector with tutorial name + * @param tutorialName name of the uploaded tutorial + */ + getAccordionButtonWithName(tutorialName: string): Selector { + return Selector(`[data-testid=accordion-button-${tutorialName}]`); + } +} diff --git a/tests/e2e/pageObjects/search-and-query-page.ts b/tests/e2e/pageObjects/search-and-query-page.ts index 289e8fb1ec..2b6ba80d21 100644 --- a/tests/e2e/pageObjects/search-and-query-page.ts +++ b/tests/e2e/pageObjects/search-and-query-page.ts @@ -1,4 +1,5 @@ -import { InstancePage } from './instance-page'; +import { BaseRunCommandsPage } from './base-run-commands-page'; + +export class SearchAndQueryPage extends BaseRunCommandsPage { -export class SearchAndQueryPage extends InstancePage { } diff --git a/tests/e2e/pageObjects/workbench-page.ts b/tests/e2e/pageObjects/workbench-page.ts index b4b7d0e3ad..196cf20339 100644 --- a/tests/e2e/pageObjects/workbench-page.ts +++ b/tests/e2e/pageObjects/workbench-page.ts @@ -1,17 +1,14 @@ import { Selector, t } from 'testcafe'; -import { InstancePage } from './instance-page'; +import { BaseRunCommandsPage } from './base-run-commands-page'; -export class WorkbenchPage extends InstancePage { +export class WorkbenchPage extends BaseRunCommandsPage { //CSS selectors cssSelectorPaginationButtonPrevious = '[data-test-subj=pagination-button-previous]'; cssSelectorPaginationButtonNext = '[data-test-subj=pagination-button-next]'; - cssReRunCommandButton = '[data-testid=re-run-command]'; - cssDeleteCommandButton = '[data-testid=delete-command]'; cssTableViewTypeOption = '[data-testid=view-type-selected-Plugin-redisearch__redisearch]'; cssClientListViewTypeOption = '[data-testid=view-type-selected-Plugin-client-list__clients-list]'; cssJsonViewTypeOption = '[data-testid=view-type-selected-Plugin-client-list__json-view]'; cssMonacoCommandPaletteLine = '[aria-label="Command Palette"]'; - cssQueryTextResult = '[data-testid=query-cli-result]'; cssWorkbenchCommandInHistory = '[data-testid=wb-command]'; cssQueryTableResult = '[data-testid^=query-table-result-]'; queryGraphContainer = '[data-testid=query-graph-container]'; @@ -25,7 +22,6 @@ export class WorkbenchPage extends InstancePage { //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.). //------------------------------------------------------------------------------------------- //BUTTON - submitCommandButton = Selector('[data-testid=btn-submit]'); resizeButtonForScriptingAndResults = Selector('[data-test-subj=resize-btn-scripting-area-and-results]'); collapsePreselectAreaButton = Selector('[data-testid=collapse-enablement-area]'); expandPreselectAreaButton = Selector('[data-testid=expand-enablement-area]'); @@ -36,18 +32,12 @@ export class WorkbenchPage extends InstancePage { preselectCreateHashIndex = Selector('[data-testid="preselect-Create a hash index"]'); preselectGroupBy = Selector('[data-testid*=preselect-Group]'); preselectButtons = Selector('[data-testid^=preselect-]'); - copyBtn = Selector('[data-testid^=copy-btn-]'); - reRunCommandButton = Selector('[data-testid=re-run-command]'); preselectManual = Selector('[data-testid=preselect-Manual]'); - fullScreenButton = Selector('[data-testid=toggle-full-screen]'); queryCardNoModuleButton = Selector('[data-testid=query-card-no-module-button] a'); - rawModeBtn = Selector('[data-testid="btn-change-mode"]'); closeEnablementPage = Selector('[data-testid=enablement-area__page-close]'); groupMode = Selector('[data-testid=btn-change-group-mode]'); - copyCommand = Selector('[data-testid=copy-command]'); clearResultsBtn = Selector('[data-testid=clear-history-btn]'); exploreRedisBtn = Selector('[data-testid=no-results-explore-btn]'); - basicUseCaseTutorialsButton = Selector('[data-testid=wb-tutorials-link_redis_use_cases_basic]'); //ICONS noCommandHistoryIcon = Selector('[data-testid=wb_no-results__icon]'); parametersAnchor = Selector('[data-testid=parameters-anchor]'); @@ -56,15 +46,13 @@ export class WorkbenchPage extends InstancePage { silentModeIcon = Selector('[data-testid=silent-mode-tooltip]'); //LINKS //TEXT INPUTS (also referred to as 'Text fields') - queryInput = Selector('[data-testid=query-input-container]'); + iframe = Selector('[data-testid=pluginIframe]'); //TEXT ELEMENTS queryPluginResult = Selector('[data-testid=query-plugin-result]'); responseInfo = Selector('[class="responseInfo"]'); parsedRedisReply = Selector('[class="parsedRedisReply"]'); scriptsLines = Selector('[data-testid=query-input-container] .view-lines'); - queryCardContainer = Selector('[data-testid^=query-card-container]'); - queryCardCommand = Selector('[data-testid=query-card-command]'); queryTableResult = Selector('[data-testid^=query-table-result-]'); queryJsonResult = Selector('[data-testid=json-view]'); mainEditorArea = Selector('[data-testid=main-input-container-area]'); @@ -78,14 +66,6 @@ export class WorkbenchPage extends InstancePage { commandExecutionResult = Selector('[data-testid=welcome-page-title]'); commandExecutionResultFailed = Selector('[data-testid=cli-output-response-fail]'); chartViewTypeOptionSelected = Selector('[data-testid=view-type-selected-Plugin-redistimeseries__redistimeseries-chart]'); - runButtonToolTip = Selector('[data-testid=run-query-tooltip]'); - loadedCommand = Selector('[class=euiLoadingContent__singleLine]'); - runButtonSpinner = Selector('[data-testid=loading-spinner]'); - commandExecutionDateAndTime = Selector('[data-testid=command-execution-date-time]'); - executionCommandTime = Selector('[data-testid=command-execution-time-value]'); - executionCommandIcon = Selector('[data-testid=command-execution-time-icon]'); - executedCommandTitle = Selector('[data-testid=query-card-tooltip-anchor]', { timeout: 500 }); - queryResult = Selector('[data-testid=query-common-result]'); //OPTIONS selectViewType = Selector('[data-testid=select-view-type]'); textViewTypeOption = Selector('[data-test-subj^=view-type-option-Text]'); @@ -95,13 +75,6 @@ export class WorkbenchPage extends InstancePage { typeSelectedClientsList = Selector('[data-testid=view-type-selected-Plugin-client-list__clients-list]'); viewTypeOptionClientList = Selector('[data-test-subj=view-type-option-Plugin-client-list__clients-list]'); viewTypeOptionsText = Selector('[data-test-subj=view-type-option-Text-default__Text]'); - /** - * Get card container by command - * @param command The command - */ - async getCardContainerByCommand(command: string): Promise { - return this.queryCardCommand.withExactText(command).parent('[data-testid^="query-card-container-"]'); - } // Select Text view option in Workbench results async selectViewTypeText(): Promise { @@ -131,19 +104,6 @@ export class WorkbenchPage extends InstancePage { .click(this.graphViewTypeOption); } - /** - * Send a command in Workbench - * @param command The command - * @param speed The speed in seconds. Default is 1 - * @param paste - */ - 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); - } - /** * Send multiple commands in Workbench * @param commands The commands @@ -167,29 +127,6 @@ export class WorkbenchPage extends InstancePage { } } - /** - * Check the last command and result in workbench - * @param command The command to check - * @param result The result to check - * @param childNum Indicator which command result need to check - */ - async checkWorkbenchCommandResult(command: string, result: string, childNum = 0): Promise { - // Compare the command with executed command - const actualCommand = await this.queryCardContainer.nth(childNum).find(this.cssQueryCardCommand).textContent; - await t.expect(actualCommand).contains(command, 'Actual command is not equal to executed'); - // Compare the command result with executed command - const actualCommandResult = await this.queryCardContainer.nth(childNum).find(this.cssQueryTextResult).textContent; - await t.expect(actualCommandResult).contains(result, 'Actual command result is not equal to executed'); - } - - /** - * Get selector with tutorial name - * @param tutorialName name of the uploaded tutorial - */ - getAccordionButtonWithName(tutorialName: string): Selector { - return Selector(`[data-testid=accordion-button-${tutorialName}]`); - } - /** * Get internal tutorial link with .md name * @param internalLink name of the .md file diff --git a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.ts b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.ts new file mode 100644 index 0000000000..9c5c4ec175 --- /dev/null +++ b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.ts @@ -0,0 +1,27 @@ +import { DatabaseHelper } from '../../../../helpers'; +import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; +import { BrowserPage } from '../../../../pageObjects'; +import { ExploreTabs, KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; + +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); +const browserPage = new BrowserPage(); + +fixture `Autocomplete for entered commands` + .meta({ type: 'regression', rte: rte.standalone }) + .page(commonUrl) + .beforeEach(async t => { + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); + }) + .afterEach(async() => { + // Delete database + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); + }); +test('Verify that tutorials can be opened from Workbench', async t => { + const search = await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); + await t.click(search.getTutorialLinkLocator('sq-exact-match')); + await t.expect(search.InsightsPanel.sidePanel.exists).ok('Insight panel is not opened'); + const tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials); + await t.expect(tab.preselectArea.textContent).contains('EXACT MATCH', 'the tutorial page is incorrect'); +}); diff --git a/tests/e2e/tests/web/regression/workbench/workbench-tab.e2e.ts b/tests/e2e/tests/web/regression/workbench/workbench-tab.e2e.ts index 57c1898186..1091652ead 100644 --- a/tests/e2e/tests/web/regression/workbench/workbench-tab.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/workbench-tab.e2e.ts @@ -20,7 +20,7 @@ fixture `Autocomplete for entered commands` }); test('Verify that tutorials can be opened from Workbench', async t => { const workbench = await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); - await t.click(workbench.basicUseCaseTutorialsButton); + await t.click(workbench.getTutorialLinkLocator('redis_use_cases_basic')); await t.expect(workbench.InsightsPanel.sidePanel.exists).ok('Insight panel is not opened'); const tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials); await t.expect(tab.preselectArea.textContent).contains('BASIC REDIS USE CASES', 'the tutorial page is incorrect'); From d594f280308a1d7f031f6f1c6f978b115dd226e9 Mon Sep 17 00:00:00 2001 From: kchepikava Date: Thu, 25 Jul 2024 19:02:41 +0200 Subject: [PATCH 017/112] RI-5888 added get info api on be --- redisinsight/api/src/__mocks__/redisearch.ts | 216 +++++++++ .../browser/redisearch/dto/index.info.dto.ts | 412 ++++++++++++++++++ .../modules/browser/redisearch/dto/index.ts | 1 + .../redisearch/redisearch.controller.ts | 13 + .../redisearch/redisearch.service.spec.ts | 30 ++ .../browser/redisearch/redisearch.service.ts | 35 ++ .../modules/browser/utils/redisIndexInfo.ts | 56 +++ .../POST-databases-id-redisearch-info.test.ts | 125 ++++++ 8 files changed, 888 insertions(+) create mode 100644 redisinsight/api/src/__mocks__/redisearch.ts create mode 100644 redisinsight/api/src/modules/browser/redisearch/dto/index.info.dto.ts create mode 100644 redisinsight/api/src/modules/browser/utils/redisIndexInfo.ts create mode 100644 redisinsight/api/test/api/redisearch/POST-databases-id-redisearch-info.test.ts diff --git a/redisinsight/api/src/__mocks__/redisearch.ts b/redisinsight/api/src/__mocks__/redisearch.ts new file mode 100644 index 0000000000..a23dc8c735 --- /dev/null +++ b/redisinsight/api/src/__mocks__/redisearch.ts @@ -0,0 +1,216 @@ +import { IndexInfoDto } from 'src/modules/browser/redisearch/dto'; + +export const mockIndexInfoRaw = [ + 'index_name', + 'idx:movie', + 'index_options', + [], + 'index_definition', + ['key_type', 'HASH', 'prefixes', ['movie:'], 'default_score', '1'], + 'attributes', + [ + ['identifier', 'title', 'attribute', 'title', 'type', 'TEXT', 'WEIGHT', '1', 'SORTABLE'], + ['identifier', 'release_year', 'attribute', 'release_year', 'type', 'NUMERIC', 'SORTABLE', 'UNF'], + ['identifier', 'rating', 'attribute', 'rating', 'type', 'NUMERIC', 'SORTABLE', 'UNF'], + ['identifier', 'genre', 'attribute', 'genre', 'type', 'TAG', 'SEPARATOR', ',', 'SORTABLE'], + ], + 'num_docs', + '2', + 'max_doc_id', + '2', + 'num_terms', + '13', + 'num_records', + '19', + 'inverted_sz_mb', + '0.0016384124755859375', + 'vector_index_sz_mb', + '0', + 'total_inverted_index_blocks', + '17', + 'offset_vectors_sz_mb', + '1.239776611328125e-5', + 'doc_table_size_mb', + '1.468658447265625e-4', + 'sortable_values_size_mb', + '2.498626708984375e-4', + 'key_table_size_mb', + '8.296966552734375e-5', + 'tag_overhead_sz_mb', + '5.53131103515625e-5', + 'text_overhead_sz_mb', + '4.3392181396484375e-4', + 'total_index_memory_sz_mb', + '0.0026903152465820313', + 'geoshapes_sz_mb', '0', + 'records_per_doc_avg', + '9.5', + 'bytes_per_record_avg', + '90.42105102539063', + 'offsets_per_term_avg', + '0.6842105388641357', + 'offset_bits_per_record_avg', + '8', 'hash_indexing_failures', + '0', 'total_indexing_time', '0.890999972820282', 'indexing', '0', + 'percent_indexed', '1', 'number_of_uses', 17, 'cleaning', 0, 'gc_stats', + ['bytes_collected', '0', 'total_ms_run', '0', 'total_cycles', '0', + 'average_cycle_time_ms', 'nan', 'last_run_time_ms', '0', + 'gc_numeric_trees_missed', '0', 'gc_blocks_denied', '0', + ], + 'cursor_stats', + ['global_idle', 0, 'global_total', 0, 'index_capacity', 128, 'index_total', 0], + 'dialect_stats', + ['dialect_1', 1, 'dialect_2', 0, 'dialect_3', 0, 'dialect_4', 0], + 'Index Errors', + ['indexing failures', 0, 'last indexing error', 'N/A', 'last indexing error key', 'N/A'], + 'field statistics', + [ + ['identifier', 'title', 'attribute', 'title', 'Index Errors', + ['indexing failures', 0, 'last indexing error', 'N/A', 'last indexing error key', 'N/A'], + ], + ['identifier', 'release_year', 'attribute', 'release_year', 'Index Errors', + ['indexing failures', 0, 'last indexing error', 'N/A', 'last indexing error key', 'N/A'], + ], + ['identifier', 'rating', 'attribute', 'rating', 'Index Errors', + ['indexing failures', 0, 'last indexing error', 'N/A', 'last indexing error key', 'N/A'], + ], + ['identifier', 'genre', 'attribute', 'genre', 'Index Errors', + ['indexing failures', 0, 'last indexing error', 'N/A', 'last indexing error key', 'N/A'], + ]]]; + +export const mockIndexInfoDto: IndexInfoDto = { + index_name: 'idx:movie', + index_options: {}, + index_definition: { key_type: 'HASH', prefixes: ['movie:'], default_score: '1' }, + attributes: [ + { + identifier: 'title', + attribute: 'title', + type: 'TEXT', + WEIGHT: '1', + SORTABLE: true, + NOINDEX: undefined, + CASESENSITIVE: undefined, + UNF: undefined, + NOSTEM: undefined, + }, + { + identifier: 'release_year', + attribute: 'release_year', + type: 'NUMERIC', + SORTABLE: true, + NOINDEX: undefined, + CASESENSITIVE: undefined, + UNF: true, + NOSTEM: undefined, + }, + { + identifier: 'rating', + attribute: 'rating', + type: 'NUMERIC', + SORTABLE: true, + NOINDEX: undefined, + CASESENSITIVE: undefined, + UNF: true, + NOSTEM: undefined, + }, + { + identifier: 'genre', + attribute: 'genre', + type: 'TAG', + SEPARATOR: ',', + SORTABLE: true, + NOINDEX: undefined, + CASESENSITIVE: undefined, + UNF: undefined, + NOSTEM: undefined, + }, + ], + num_docs: '2', + max_doc_id: '2', + num_terms: '13', + num_records: '19', + inverted_sz_mb: '0.0016384124755859375', + vector_index_sz_mb: '0', + total_inverted_index_blocks: '17', + offset_vectors_sz_mb: '1.239776611328125e-5', + doc_table_size_mb: '1.468658447265625e-4', + sortable_values_size_mb: '2.498626708984375e-4', + tag_overhead_sz_mb: '5.53131103515625e-5', + text_overhead_sz_mb: '4.3392181396484375e-4', + total_index_memory_sz_mb: '0.0026903152465820313', + + key_table_size_mb: '8.296966552734375e-5', + geoshapes_sz_mb: '0', + records_per_doc_avg: '9.5', + bytes_per_record_avg: '90.42105102539063', + offsets_per_term_avg: '0.6842105388641357', + offset_bits_per_record_avg: '8', + hash_indexing_failures: '0', + total_indexing_time: '0.890999972820282', + indexing: '0', + percent_indexed: '1', + number_of_uses: 17, + cleaning: 0, + gc_stats: { + bytes_collected: '0', + total_ms_run: '0', + total_cycles: '0', + average_cycle_time_ms: 'nan', + last_run_time_ms: '0', + gc_numeric_trees_missed: '0', + gc_blocks_denied: '0', + }, + cursor_stats: { + global_idle: 0, + global_total: 0, + index_capacity: 128, + index_total: 0, + }, + dialect_stats: { + dialect_1: 1, dialect_2: 0, dialect_3: 0, dialect_4: 0, + }, + 'Index Errors': { + 'indexing failures': 0, + 'last indexing error': 'N/A', + 'last indexing error key': 'N/A', + }, + 'field statistics': [ + { + identifier: 'title', + attribute: 'title', + 'Index Errors': { + 'indexing failures': 0, + 'last indexing error': 'N/A', + 'last indexing error key': 'N/A', + }, + }, + { + identifier: 'release_year', + attribute: 'release_year', + 'Index Errors': { + 'indexing failures': 0, + 'last indexing error': 'N/A', + 'last indexing error key': 'N/A', + }, + }, + { + identifier: 'rating', + attribute: 'rating', + 'Index Errors': { + 'indexing failures': 0, + 'last indexing error': 'N/A', + 'last indexing error key': 'N/A', + }, + }, + { + identifier: 'genre', + attribute: 'genre', + 'Index Errors': { + 'indexing failures': 0, + 'last indexing error': 'N/A', + 'last indexing error key': 'N/A', + }, + }, + ], +}; diff --git a/redisinsight/api/src/modules/browser/redisearch/dto/index.info.dto.ts b/redisinsight/api/src/modules/browser/redisearch/dto/index.info.dto.ts new file mode 100644 index 0000000000..2700275fec --- /dev/null +++ b/redisinsight/api/src/modules/browser/redisearch/dto/index.info.dto.ts @@ -0,0 +1,412 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsDefined, +} from 'class-validator'; +import { IsRedisString, RedisStringType } from 'src/common/decorators'; +import { RedisString } from 'src/common/constants'; +import { Expose } from 'class-transformer'; + +export class IndexInfoRequestBodyDto { + @ApiProperty({ + description: 'Index name', + type: String, + }) + @IsDefined() + @RedisStringType() + @IsRedisString() + index: RedisString; +} + +export class IndexOptionsDto { + @ApiProperty({ + description: 'is a filter expression with the full RediSearch aggregation expression language.', + type: String, + }) + @Expose() + filter?: string; + + @ApiProperty({ + description: 'if set, indicates the default language for documents in the index. Default is English.', + type: String, + }) + @Expose() + default_lang?: string; +} + +export class IndexDefinitionDto { + @ApiProperty({ + description: 'key_type, hash or JSON', + type: String, + }) + @Expose() + key_type: string; + + @ApiProperty({ + description: 'Index prefixes given during create', + type: String, + isArray: true, + }) + @Expose() + prefixes: Array; + + @ApiProperty({ + description: 'Index default_score', + type: String, + }) + @Expose() + default_score: string; +} + +export class IndexAttibuteDto { + @ApiProperty({ + description: 'Field identifier', + type: String, + }) + @Expose() + identifier: string; + + @ApiProperty({ + description: 'Field attribute', + type: String, + }) + @Expose() + attribute: string; + + @ApiProperty({ + description: 'Field type', + type: String, + }) + @Expose() + type: string; + + @ApiProperty({ + description: 'Field weight', + type: String, + }) + @Expose() + WEIGHT?: string; + + @ApiProperty({ + description: 'Field can be sorted', + type: Boolean, + }) + @Expose() + SORTABLE?: boolean; + + @ApiProperty({ + description: 'Attributes can have the NOINDEX option, which means they will not be indexed. ', + type: Boolean, + }) + @Expose() + NOINDEX?: boolean; + + @ApiProperty({ + description: 'Attribute is case sensitive', + type: Boolean, + }) + @Expose() + CASESENSITIVE?: boolean; + + @ApiProperty({ + description: `By default, for hashes (not with JSON) SORTABLE applies a normalization to the indexed value + (characters set to lowercase, removal of diacritics).`, + type: Boolean, + }) + @Expose() + UNF?: boolean; + + @ApiProperty({ + description: `Text attributes can have the NOSTEM argument that disables stemming when indexing its values. + This may be ideal for things like proper names.`, + type: Boolean, + }) + @Expose() + NOSTEM?: boolean; + + @ApiProperty({ + description: `Indicates how the text contained in the attribute is to be split into individual tags. + The default is ,. The value must be a single character.`, + type: String, + }) + @Expose() + SEPARATOR?: string; +} + +export class FieldStatisticsDto { + @ApiProperty({ + description: 'Field identifier', + type: String, + }) + @Expose() + identifier: string; + + @ApiProperty({ + description: 'Field attribute', + type: String, + }) + @Expose() + attribute: string; + + @ApiProperty({ + description: 'Field errors', + type: Object, + }) + @Expose() + [ 'Index Errors']: object; +} + +// The list of return fields from redis: https://redis.io/docs/latest/commands/ft.info/ + +export class IndexInfoDto { + // General + @ApiProperty({ + description: 'The index name that was defined when index was created', + type: String, + }) + @Expose() + @IsDefined() + index_name: string; + + @ApiProperty({ + description: 'The index options selected during FT.CREATE such as FILTER {filter}, LANGUAGE {default_lang}, etc.', + type: IndexOptionsDto, + }) + @Expose() + index_options: IndexOptionsDto; + + @ApiProperty({ + description: 'Includes key_type, hash or JSON; prefixes, if any; and default_score.', + type: IndexDefinitionDto, + }) + @Expose() + index_definition: IndexDefinitionDto; + + @ApiProperty({ + description: 'The index schema field names, types, and attributes.', + type: IndexAttibuteDto, + isArray: true, + }) + @Expose() + attributes: IndexAttibuteDto[]; + + @ApiProperty({ + description: 'The number of documents.', + type: String, + }) + @Expose() + num_docs: string; + + @ApiProperty({ + description: 'The maximum document ID.', + type: String, + }) + @Expose() + max_doc_id?: string; + + @ApiProperty({ + description: 'The number of distinct terms.', + type: String, + }) + @Expose() + num_terms?: string; + + @ApiProperty({ + description: 'The total number of records.', + type: String, + }) + @Expose() + num_records?: string; + + // Various size statistics + @ApiProperty({ + description: `The memory used by the inverted index, which is the core data structure + used for searching in RediSearch. The size is given in megabytes.`, + type: String, + }) + @Expose() + inverted_sz_mb?: string; + + @ApiProperty({ + description: `The memory used by the vector index, + which stores any vectors associated with each document.`, + type: String, + }) + @Expose() + vector_index_sz_mb?: string; + + @ApiProperty({ + description: 'The total number of blocks in the inverted index.', + type: String, + }) + @Expose() + total_inverted_index_blocks?: string; + + @ApiProperty({ + description: `The memory used by the offset vectors, + which store positional information for terms in documents.`, + type: String, + }) + @Expose() + offset_vectors_sz_mb?: string; + + @ApiProperty({ + description: `The memory used by the document table, + which contains metadata about each document in the index.`, + type: String, + }) + @Expose() + doc_table_size_mb?: string; + + @ApiProperty({ + description: `The memory used by sortable values, + which are values associated with documents and used for sorting purposes.`, + type: String, + }) + @Expose() + sortable_values_size_mb?: string; + + @ApiProperty({ + description: 'Tag overhead memory usage in mb', + type: String, + }) + @Expose() + tag_overhead_sz_mb?: string; + + @ApiProperty({ + description: 'Text overhead memory usage in mb', + type: String, + }) + @Expose() + text_overhead_sz_mb?: string; + + @ApiProperty({ + description: 'Total index memory size in mb', + type: String, + }) + @Expose() + total_index_memory_sz_mb?: string; + + @ApiProperty({ + description: `The memory used by the key table, + which stores the mapping between document IDs and Redis keys`, + type: String, + }) + @Expose() + key_table_size_mb?: string; + + @ApiProperty({ + description: 'The memory used by GEO-related fields.', + type: String, + }) + @Expose() + geoshapes_sz_mb?: string; + + @ApiProperty({ + description: 'The average number of records (including deletions) per document.', + type: String, + }) + @Expose() + records_per_doc_avg?: string; + + @ApiProperty({ + description: 'The average size of each record in bytes.', + type: String, + }) + @Expose() + bytes_per_record_avg?: string; + + @ApiProperty({ + description: 'The average number of offsets (position information) per term.', + type: String, + }) + @Expose() + offsets_per_term_avg?: string; + + @ApiProperty({ + description: 'The average number of bits used for offsets per record.', + type: String, + }) + @Expose() + offset_bits_per_record_avg?: string; + + // Indexing-related statistics + @ApiProperty({ + description: 'The number of failures encountered during indexing.', + type: String, + }) + @Expose() + hash_indexing_failures?: string; + + @ApiProperty({ + description: 'The total time taken for indexing in seconds.', + type: String, + }) + @Expose() + total_indexing_time?: string; + + @ApiProperty({ + description: 'Indicates whether the index is currently being generated.', + type: String, + }) + @Expose() + indexing?: string; + + @ApiProperty({ + description: 'The percentage of the index that has been successfully generated.', + type: String, + }) + @Expose() + percent_indexed?: string; + + @ApiProperty({ + description: 'The number of times the index has been used.', + type: Number, + }) + @Expose() + number_of_uses?: number; + + @ApiProperty({ + description: 'The index deletion flag. A value of 1 indicates index deletion is in progress.', + type: Number, + }) + @Expose() + cleaning?: number; + + // Other + @ApiProperty({ + description: 'Garbage collection statistics', + type: Object, + }) + @Expose() + gc_stats?: object; + + @ApiProperty({ + description: 'Cursor statistics', + type: Object, + }) + @Expose() + cursor_stats?: object; + + @ApiProperty({ + description: 'Dialect statistics: the number of times the index was searched using each DIALECT, 1 - 4.', + type: Object, + }) + @Expose() + dialect_stats?: object; + + @ApiProperty({ + description: `Index error statistics, including indexing failures, last indexing error, + and last indexing error key.`, + type: Object, + }) + @Expose() + ['Index Errors']?: object; + + @ApiProperty({ + description: 'Dialect statistics: the number of times the index was searched using each DIALECT, 1 - 4.', + type: FieldStatisticsDto, + isArray: true, + }) + @Expose() + ['field statistics']?: Array; +} diff --git a/redisinsight/api/src/modules/browser/redisearch/dto/index.ts b/redisinsight/api/src/modules/browser/redisearch/dto/index.ts index 2390227ecd..56e0b39d48 100644 --- a/redisinsight/api/src/modules/browser/redisearch/dto/index.ts +++ b/redisinsight/api/src/modules/browser/redisearch/dto/index.ts @@ -1,3 +1,4 @@ export * from './create.redisearch-index.dto'; export * from './search.redisearch.dto'; export * from './list.redisearch-indexes.response'; +export * from './index.info.dto'; diff --git a/redisinsight/api/src/modules/browser/redisearch/redisearch.controller.ts b/redisinsight/api/src/modules/browser/redisearch/redisearch.controller.ts index 7c3a9cdb55..7956f7445c 100644 --- a/redisinsight/api/src/modules/browser/redisearch/redisearch.controller.ts +++ b/redisinsight/api/src/modules/browser/redisearch/redisearch.controller.ts @@ -19,6 +19,8 @@ import { BrowserClientMetadata } from 'src/modules/browser/decorators/browser-cl import { ApiQueryRedisStringEncoding } from 'src/common/decorators'; import { CreateRedisearchIndexDto, + IndexInfoDto, + IndexInfoRequestBodyDto, ListRedisearchIndexesResponse, SearchRedisearchDto, } from 'src/modules/browser/redisearch/dto'; @@ -72,4 +74,15 @@ export class RedisearchController extends BrowserBaseController { ): Promise { return await this.service.search(clientMetadata, dto); } + + @Post('info') + @HttpCode(200) + @ApiOperation({ description: 'Get index info' }) + @ApiOkResponse({ type: IndexInfoDto }) + async info( + @BrowserClientMetadata() clientMetadata: ClientMetadata, + @Body() dto: IndexInfoRequestBodyDto, + ): Promise { + return await this.service.getInfo(clientMetadata, dto); + } } diff --git a/redisinsight/api/src/modules/browser/redisearch/redisearch.service.spec.ts b/redisinsight/api/src/modules/browser/redisearch/redisearch.service.spec.ts index 0a0910e660..5ce2d03d16 100644 --- a/redisinsight/api/src/modules/browser/redisearch/redisearch.service.spec.ts +++ b/redisinsight/api/src/modules/browser/redisearch/redisearch.service.spec.ts @@ -2,6 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ConflictException, ForbiddenException, + InternalServerErrorException, } from '@nestjs/common'; import { when } from 'jest-when'; import { @@ -20,6 +21,7 @@ import { } from 'src/modules/browser/redisearch/dto'; import { BrowserHistoryService } from 'src/modules/browser/browser-history/browser-history.service'; import { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory'; +import { mockIndexInfoDto, mockIndexInfoRaw } from 'src/__mocks__/redisearch'; const keyName1 = Buffer.from('keyName1'); const keyName2 = Buffer.from('keyName2'); @@ -368,4 +370,32 @@ describe('RedisearchService', () => { expect(browserHistory.create).toHaveBeenCalled(); }); }); + + describe('getInfo', () => { + it('should get indexInfo', async () => { + const mockIndexName = 'idx:movie'; + when(standaloneClient.sendCommand) + .calledWith(['FT.INFO', mockIndexName], { replyEncoding: 'utf8' }) + .mockResolvedValue(mockIndexInfoRaw); + + const res = await service.getInfo(mockBrowserClientMetadata, { index: mockIndexName }); + expect(standaloneClient.sendCommand).toHaveBeenCalledWith([ + 'FT.INFO', + mockIndexName, + ], { replyEncoding: 'utf8' }); + expect(res).toEqual(mockIndexInfoDto); + }); + + it('should throw error if index name was not provided', async () => { + await expect(service.getInfo(mockBrowserClientMetadata, { index: '' })).rejects.toThrow('Index was not provided'); + }); + + it('should throw error if client was not created', async () => { + const error = new Error('Client was not created'); + databaseClientFactory.getOrCreateClient = jest.fn().mockRejectedValue(error); + + await expect(service.getInfo(mockBrowserClientMetadata, + { index: 'indexName' })).rejects.toThrow(InternalServerErrorException); + }); + }); }); diff --git a/redisinsight/api/src/modules/browser/redisearch/redisearch.service.ts b/redisinsight/api/src/modules/browser/redisearch/redisearch.service.ts index 325b41b555..39f2d4a856 100644 --- a/redisinsight/api/src/modules/browser/redisearch/redisearch.service.ts +++ b/redisinsight/api/src/modules/browser/redisearch/redisearch.service.ts @@ -11,6 +11,8 @@ import { catchAclError } from 'src/utils'; import { ClientMetadata } from 'src/common/models'; import { CreateRedisearchIndexDto, + IndexInfoDto, + IndexInfoRequestBodyDto, ListRedisearchIndexesResponse, SearchRedisearchDto, } from 'src/modules/browser/redisearch/dto'; @@ -28,6 +30,7 @@ import { RedisClientConnectionType, RedisClientNodeRole, } from 'src/modules/redis/client'; +import { convertIndexInfoReply } from '../utils/redisIndexInfo'; @Injectable() export class RedisearchService { @@ -138,6 +141,38 @@ export class RedisearchService { } } + /** + * Gets the info of a given index + * @param clientMetadata + * @param dto + */ + public async getInfo( + clientMetadata: ClientMetadata, + dto: IndexInfoRequestBodyDto, + ): Promise { + this.logger.log('Getting index info'); + + try { + const { index } = dto; + + if (!index) { + throw new Error('Index was not provided'); + } + + const client: RedisClient = await this.databaseClientFactory.getOrCreateClient(clientMetadata); + + const infoReply = await client.sendCommand( + ['FT.INFO', index], + { replyEncoding: 'utf8' }, + ) as string[][]; + + return plainToClass(IndexInfoDto, convertIndexInfoReply(infoReply)); + } catch (e) { + this.logger.error('Failed to get index info', e); + throw catchAclError(e); + } + } + /** * Search for key names using RediSearch module * Response is the same as for keys "scan" to have the same behaviour in the browser diff --git a/redisinsight/api/src/modules/browser/utils/redisIndexInfo.ts b/redisinsight/api/src/modules/browser/utils/redisIndexInfo.ts new file mode 100644 index 0000000000..edf1440d5b --- /dev/null +++ b/redisinsight/api/src/modules/browser/utils/redisIndexInfo.ts @@ -0,0 +1,56 @@ +import { chunk, isArray } from 'lodash'; + +type ArrayReplyEntry = string | string[]; +const errorField = 'Index Errors'; + +const infoFieldsToConvert = [ + 'index_options', + 'index_definition', + 'gc_stats', + 'cursor_stats', + 'dialect_stats', + errorField, +]; + +export const convertArrayReplyToObject = ( + input: ArrayReplyEntry[], +): { [key: string]: any } => { + const obj = {}; + + chunk(input, 2).forEach(([key, value]) => { + obj[key as string] = value; + }); + + return obj; +}; + +export const convertIndexInfoAttributeReply = (input: string[]): object => { + const attribute = convertArrayReplyToObject(input); + + if (isArray(input)) { + attribute['SORTABLE'] = input.includes('SORTABLE') || undefined; + attribute['NOINDEX'] = input.includes('NOINDEX') || undefined; + attribute['CASESENSITIVE'] = input.includes('CASESENSITIVE') || undefined; + attribute['UNF'] = input.includes('UNF') || undefined; + attribute['NOSTEM'] = input.includes('NOSTEM') || undefined; + } + + return attribute; +}; + +export const convertIndexInfoReply = (input: ArrayReplyEntry[]): object => { + const infoReply = convertArrayReplyToObject(input); + infoFieldsToConvert.forEach((field) => { + infoReply[field] = convertArrayReplyToObject(infoReply[field]); + }); + + infoReply['attributes'] = infoReply['attributes']?.map?.(convertIndexInfoAttributeReply); + infoReply['field statistics'] = infoReply['field statistics']?.map?.((sField) => { + const convertedField = convertArrayReplyToObject(sField); + if (convertedField[errorField] && Array.isArray(convertedField[errorField])) { + convertedField[errorField] = convertArrayReplyToObject(convertedField[errorField]); + } + return convertedField; + }); + return infoReply; +}; diff --git a/redisinsight/api/test/api/redisearch/POST-databases-id-redisearch-info.test.ts b/redisinsight/api/test/api/redisearch/POST-databases-id-redisearch-info.test.ts new file mode 100644 index 0000000000..a70d92e566 --- /dev/null +++ b/redisinsight/api/test/api/redisearch/POST-databases-id-redisearch-info.test.ts @@ -0,0 +1,125 @@ +import { + expect, + describe, + before, + Joi, + deps, + requirements, + generateInvalidDataTestCases, + validateInvalidDataTestCase, + getMainCheckFn, +} from '../deps'; + +const { + server, request, constants, rte, localDb, +} = deps; + +// endpoint to test +const endpoint = (instanceId = constants.TEST_INSTANCE_ID) => request(server) + .post(`/${constants.API.DATABASES}/${instanceId}/redisearch/info`); + +// input data schema +const dataSchema = Joi.object({ + index: Joi.string().required(), +}).strict(); + +const validInputData = { + index: constants.TEST_SEARCH_HASH_INDEX_1, +}; + +const responseSchema = Joi.object({ + index_name: Joi.string().required(), + index_options: Joi.object({}), + index_definition: Joi.object({ + key_type: Joi.string(), + prefixes: Joi.array(), + default_score: Joi.string(), + }), + attributes: Joi.array().items({ + identifier: Joi.string(), + attribute: Joi.string(), + type: Joi.string(), + WEIGHT: Joi.string(), + SORTABLE: Joi.string(), + NOINDEX: Joi.string(), + CASESENSITIVE: Joi.string(), + UNF: Joi.string(), + NOSTEM: Joi.string(), + SEPARATOR: Joi.string(), + }), + num_docs: Joi.string(), + max_doc_id: Joi.string(), + num_terms: Joi.string(), + num_records: Joi.string(), + inverted_sz_mb: Joi.string(), + vector_index_sz_mb: Joi.string(), + total_inverted_index_blocks: Joi.string(), + offset_vectors_sz_mb: Joi.string(), + doc_table_size_mb: Joi.string(), + sortable_values_size_mb: Joi.string(), + tag_overhead_sz_mb: Joi.string(), + text_overhead_sz_mb: Joi.string(), + total_index_memory_sz_mb: Joi.string(), + key_table_size_mb: Joi.string(), + geoshapes_sz_mb: Joi.string(), + records_per_doc_avg: Joi.string(), + bytes_per_record_avg: Joi.string(), + offsets_per_term_avg: Joi.string(), + offset_bits_per_record_avg: Joi.string(), + hash_indexing_failures: Joi.string(), + total_indexing_time: Joi.string(), + indexing: Joi.string(), + percent_indexed: Joi.string(), + number_of_uses: Joi.number(), + cleaning: Joi.number(), + gc_stats: Joi.object(), + cursor_stats: Joi.object(), + dialect_stats: Joi.object(), + 'Index Errors': Joi.object(), + 'field statistics': Joi.array().items({ + identifier: Joi.string(), + attribute: Joi.string(), + 'Index Errors': Joi.object(), + }), +}).required().strict(); +const mainCheckFn = getMainCheckFn(endpoint); + +describe('POST /databases/:id/redisearch/info', () => { + requirements('!rte.bigData', 'rte.modules.search'); + before(async () => { + await rte.data.generateRedisearchIndexes(true); + await localDb.createTestDbInstance(rte, {}, { id: constants.TEST_INSTANCE_ID_2 }); + }); + + describe('Validation', () => { + generateInvalidDataTestCases(dataSchema, validInputData).forEach( + validateInvalidDataTestCase(endpoint, dataSchema), + ); + }); + + describe('Common', () => { + [ + { + name: 'Should get info index', + data: validInputData, + responseSchema, + checkFn: async ({ body }) => { + expect(body.index_name).to.eq(constants.TEST_SEARCH_HASH_INDEX_1); + expect(body.index_definition?.key_type).to.eq('HASH'); + }, + }, + { + name: 'Should throw error if non-existent index provided', + data: { + index: 'Invalid index', + }, + statusCode: 500, + responseBody: { + message: 'Unknown index name', + error: 'Internal Server Error', + statusCode: 500, + }, + }, + ].forEach(mainCheckFn); + }); +}); From c61e3f48f3b4e7a8671fb260664409d2b6fac602 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Mon, 5 Aug 2024 11:48:13 +0200 Subject: [PATCH 018/112] #RI-5957 - integrate autocomplete to search and query --- .../ui/src/pages/search/SearchPage.tsx | 5 +- .../ui/src/pages/search/components/index.ts | 4 +- .../QueryWrapper.spec.tsx} | 4 +- .../components/query-wrapper/QueryWrapper.tsx | 111 ++++++ .../{query => query-wrapper}/constants.ts | 0 .../search/components/query-wrapper/index.ts | 3 + .../query-wrapper/styles.module.scss | 76 ++++ .../pages/search/components/query/Query.tsx | 346 +++++++++++++---- .../components/query/styles.module.scss | 76 ---- .../pages/search/components/query/types.ts | 22 ++ .../pages/search/components/query/utils.ts | 354 ++++++++++++++++++ 11 files changed, 839 insertions(+), 162 deletions(-) rename redisinsight/ui/src/pages/search/components/{query/Query.spec.tsx => query-wrapper/QueryWrapper.spec.tsx} (56%) create mode 100644 redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.tsx rename redisinsight/ui/src/pages/search/components/{query => query-wrapper}/constants.ts (100%) create mode 100644 redisinsight/ui/src/pages/search/components/query-wrapper/index.ts create mode 100644 redisinsight/ui/src/pages/search/components/query-wrapper/styles.module.scss create mode 100644 redisinsight/ui/src/pages/search/components/query/types.ts create mode 100644 redisinsight/ui/src/pages/search/components/query/utils.ts diff --git a/redisinsight/ui/src/pages/search/SearchPage.tsx b/redisinsight/ui/src/pages/search/SearchPage.tsx index c48219b554..bf6116641c 100644 --- a/redisinsight/ui/src/pages/search/SearchPage.tsx +++ b/redisinsight/ui/src/pages/search/SearchPage.tsx @@ -8,7 +8,7 @@ import { appContextSearchAndQuery, setSQVerticalPanelSizes, } from 'uiSrc/slices/app/context' -import { Query, ResultsHistory } from 'uiSrc/pages/search/components' +import { QueryWrapper, ResultsHistory } from 'uiSrc/pages/search/components' import { sendWbQueryAction } from 'uiSrc/slices/workbench/wb-results' import { formatLongName, getDbIndex, Nullable, setTitle } from 'uiSrc/utils' @@ -16,6 +16,7 @@ import { formatLongName, getDbIndex, Nullable, setTitle } from 'uiSrc/utils' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { sendPageViewTelemetry, TelemetryPageView } from 'uiSrc/telemetry' import { CodeButtonParams } from 'uiSrc/constants' + import styles from './styles.module.scss' const verticalPanelIds = { @@ -88,7 +89,7 @@ const SearchPage = () => { initialSize={vertical[verticalPanelIds.firstPanelId] ?? 20} style={{ minHeight: '240px', zIndex: '8' }} > - + { it('should render', () => { - expect(render()).toBeTruthy() + expect(render()).toBeTruthy() }) }) diff --git a/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.tsx b/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.tsx new file mode 100644 index 0000000000..9850d79545 --- /dev/null +++ b/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.tsx @@ -0,0 +1,111 @@ +import React, { useEffect, useRef, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { useParams } from 'react-router-dom' +import { QueryActions, QueryTutorials } from 'uiSrc/components/query' + +import { RunQueryMode } from 'uiSrc/slices/interfaces' +import { CodeButtonParams } from 'uiSrc/constants' + +import { Nullable } from 'uiSrc/utils' +import { changeSQActiveRunQueryMode, searchAndQuerySelector } from 'uiSrc/slices/search/searchAndQuery' +import { appContextSearchAndQuery, setSQVerticalScript } from 'uiSrc/slices/app/context' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { fetchRedisearchListAction, redisearchListSelector } from 'uiSrc/slices/browser/redisearch' +import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' +import { TUTORIALS } from './constants' + +import Query from '../query' + +import styles from './styles.module.scss' + +export interface Props { + onSubmit: ( + commandInit: string, + commandId?: Nullable, + executeParams?: CodeButtonParams + ) => void +} + +const QueryWrapper = (props: Props) => { + const { onSubmit } = props + + const { id: connectedIndstanceId } = useSelector(connectedInstanceSelector) + const { script: scriptContext } = useSelector(appContextSearchAndQuery) + const { activeRunQueryMode } = useSelector(searchAndQuerySelector) + const { data: indexes = [] } = useSelector(redisearchListSelector) + const [value, setValue] = useState(scriptContext) + + const input = useRef(null) + const scriptRef = useRef('') + + const { instanceId } = useParams<{ instanceId: string }>() + + const dispatch = useDispatch() + + useEffect(() => () => { + dispatch(setSQVerticalScript(scriptRef.current)) + }, []) + + useEffect(() => { + if (!connectedIndstanceId) return + + // fetch indexes + dispatch(fetchRedisearchListAction()) + }, [connectedIndstanceId]) + + useEffect(() => { + scriptRef.current = value + }, [value]) + + const handleChangeQueryRunMode = () => { + dispatch(changeSQActiveRunQueryMode( + activeRunQueryMode === RunQueryMode.ASCII + ? RunQueryMode.Raw + : RunQueryMode.ASCII + )) + } + + const handleSubmit = () => { + onSubmit(value.split('\n').join(' '), undefined, { mode: activeRunQueryMode }) + sendEventTelemetry({ + event: TelemetryEvent.SEARCH_COMMAND_SUBMITTED, + eventData: { + databaseId: instanceId, + mode: activeRunQueryMode, + // TODO sanitize user query + command: value + } + }) + } + + const handleChange = (val: string) => { + setValue(val) + } + + return ( +
+
{}} + role="textbox" + tabIndex={0} + data-testid="main-input-container-area" + > +
+ +
+
+ + +
+
+
+ ) +} + +export default React.memo(QueryWrapper) diff --git a/redisinsight/ui/src/pages/search/components/query/constants.ts b/redisinsight/ui/src/pages/search/components/query-wrapper/constants.ts similarity index 100% rename from redisinsight/ui/src/pages/search/components/query/constants.ts rename to redisinsight/ui/src/pages/search/components/query-wrapper/constants.ts diff --git a/redisinsight/ui/src/pages/search/components/query-wrapper/index.ts b/redisinsight/ui/src/pages/search/components/query-wrapper/index.ts new file mode 100644 index 0000000000..cf4185d43b --- /dev/null +++ b/redisinsight/ui/src/pages/search/components/query-wrapper/index.ts @@ -0,0 +1,3 @@ +import QueryWrapper from './QueryWrapper' + +export default QueryWrapper diff --git a/redisinsight/ui/src/pages/search/components/query-wrapper/styles.module.scss b/redisinsight/ui/src/pages/search/components/query-wrapper/styles.module.scss new file mode 100644 index 0000000000..18b46e1931 --- /dev/null +++ b/redisinsight/ui/src/pages/search/components/query-wrapper/styles.module.scss @@ -0,0 +1,76 @@ +.wrapper { + position: relative; + height: 100%; + + :global(.editorBounder) { + bottom: 6px; + left: 18px; + right: 46px; + } +} + +.container { + display: flex; + flex-direction: column; + padding: 8px 16px; + width: 100%; + height: 100%; + word-break: break-word; + text-align: left; + letter-spacing: 0; + background-color: var(--rsInputWrapperColor); + color: var(--euiTextSubduedColor) !important; + border: 1px solid var(--euiColorLightShade); +} + +.disabled { + opacity: 0.8; +} + +.disabledActions { + pointer-events: none; + user-select: none; +} + +.containerPlaceholder { + display: flex; + padding: 8px 16px 8px 16px; + width: 100%; + height: 100%; + background-color: var(--rsInputWrapperColor); + color: var(--euiTextSubduedColor) !important; + border: 1px solid var(--euiColorLightShade); + > div { + border: 1px solid var(--euiColorLightShade); + background-color: var(--euiColorEmptyShade); + padding: 8px 20px; + width: 100%; + } +} + +.input { + // cannot use overflow since suggestions are absolute + max-height: calc(100% - 32px); + flex-grow: 1; + width: 100%; + border: 1px solid var(--euiColorLightShade); + background-color: var(--rsInputColor); +} + +.queryFooter { + display: flex; + align-items: center; + justify-content: space-between; + + margin-top: 8px; + flex-shrink: 0; +} + +#script { + font: normal normal bold 14px/17px Inconsolata !important; + color: var(--textColorShade); + caret-color: var(--euiColorFullShade); + min-width: 5px; + display: inline; +} + diff --git a/redisinsight/ui/src/pages/search/components/query/Query.tsx b/redisinsight/ui/src/pages/search/components/query/Query.tsx index f01c7ffe92..ef9cc0768a 100644 --- a/redisinsight/ui/src/pages/search/components/query/Query.tsx +++ b/redisinsight/ui/src/pages/search/components/query/Query.tsx @@ -1,104 +1,290 @@ -import React, { useContext, useEffect, useRef, useState } from 'react' -import MonacoEditor from 'react-monaco-editor' -import { useDispatch, useSelector } from 'react-redux' -import { useParams } from 'react-router-dom' -import { QueryActions, QueryTutorials } from 'uiSrc/components/query' - -import { RunQueryMode } from 'uiSrc/slices/interfaces' -import { CodeButtonParams, defaultMonacoOptions, Theme } from 'uiSrc/constants' +import React, { useContext, useEffect, useRef } from 'react' +import MonacoEditor, { monaco } from 'react-monaco-editor' +import * as monacoEditor from 'monaco-editor' +import { useSelector } from 'react-redux' +import { merge } from 'lodash' +import { defaultMonacoOptions, Theme } from 'uiSrc/constants' import { ThemeContext } from 'uiSrc/contexts/themeContext' - -import { Nullable } from 'uiSrc/utils' -import { changeSQActiveRunQueryMode, searchAndQuerySelector } from 'uiSrc/slices/search/searchAndQuery' -import { appContextSearchAndQuery, setSQVerticalScript } from 'uiSrc/slices/app/context' -import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import { TUTORIALS } from './constants' - -import styles from './styles.module.scss' +import { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands' +import { bufferToString, formatLongName, getCommandMarkdown, Nullable } from 'uiSrc/utils' +import { IEditorMount } from 'uiSrc/pages/workbench/interfaces' +import { + buildSuggestion, + findCurrentArgument, + generateDetail, + getRange, + splitQueryByArgs +} from 'uiSrc/pages/search/components/query/utils' +import { SearchCommand, TokenType } from 'uiSrc/pages/search/components/query/types' +import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' export interface Props { - onSubmit: ( - commandInit: string, - commandId?: Nullable, - executeParams?: CodeButtonParams - ) => void + value: string + onChange: (val: string) => void + indexes: RedisResponseBuffer[] } -const options = { ...defaultMonacoOptions } +const options = merge(defaultMonacoOptions, + { + suggest: { + showWords: false, + showIcons: true + } + }) + +const SUPPORTED_COMMANDS_LIST = ['FT.SEARCH', 'FT.AGGREGATE'] const Query = (props: Props) => { - const { onSubmit } = props + const { value, onChange, indexes } = props + const { spec: REDIS_COMMANDS_SPEC } = useSelector(appRedisCommandsSelector) - const { script: scriptContext } = useSelector(appContextSearchAndQuery) - const { activeRunQueryMode } = useSelector(searchAndQuerySelector) - const [value, setValue] = useState(scriptContext) + const SUPPORTED_COMMANDS = SUPPORTED_COMMANDS_LIST.map((name) => ({ + ...REDIS_COMMANDS_SPEC[name], + name + })) as unknown as SearchCommand[] - const { theme } = useContext(ThemeContext) - const input = useRef(null) - const scriptRef = useRef('') + const monacoObjects = useRef>(null) + const disposeCompletionItemProvider = useRef(() => {}) + const disposeSignatureHelpProvider = useRef(() => {}) + const suggestionsRef = useRef([]) + const helpWidgetRef = useRef({ + isOpen: false, + parent: null, + currentArg: null + }) + const indexesRef = useRef([]) - const { instanceId } = useParams<{ instanceId: string }>() + const { theme } = useContext(ThemeContext) - const dispatch = useDispatch() + useEffect(() => { + monaco.languages.register({ id: 'RediSearch' }) - useEffect(() => () => { - dispatch(setSQVerticalScript(scriptRef.current)) + return () => { + disposeCompletionItemProvider.current?.() + disposeSignatureHelpProvider.current?.() + } }, []) useEffect(() => { - scriptRef.current = value - }, [value]) - - const handleChangeQueryRunMode = () => { - dispatch(changeSQActiveRunQueryMode( - activeRunQueryMode === RunQueryMode.ASCII - ? RunQueryMode.Raw - : RunQueryMode.ASCII - )) - } + indexesRef.current = indexes + }, [indexes]) + + const editorDidMount = ( + editor: monacoEditor.editor.IStandaloneCodeEditor, + monaco: typeof monacoEditor + ) => { + monacoObjects.current = { editor, monaco } + + suggestionsRef.current = getSuggestions(editor) + triggerSuggestions() + + disposeSignatureHelpProvider.current?.() + disposeSignatureHelpProvider.current = monaco.languages.registerSignatureHelpProvider( + 'RediSearch', + { + provideSignatureHelp: (): any => { + if (!helpWidgetRef.current?.isOpen) return null + + const { currentArg, parent } = helpWidgetRef.current + const label = generateDetail(parent) + const arg = currentArg?.type === TokenType.Block ? currentArg.arguments[0]?.name : (currentArg?.name || currentArg?.type || '') + + return { + dispose: () => {}, + value: { + activeParameter: 0, + activeSignature: 0, + signatures: [{ + label: label || '', + parameters: [{ label: arg }] + }] + } + } + } + } + ).dispose - const handleSubmit = () => { - onSubmit(value, undefined, { mode: activeRunQueryMode }) - sendEventTelemetry({ - event: TelemetryEvent.SEARCH_COMMAND_SUBMITTED, - eventData: { - databaseId: instanceId, - mode: activeRunQueryMode, - // TODO sanitize user query - command: value + disposeCompletionItemProvider.current?.() + disposeCompletionItemProvider.current = monaco.languages.registerCompletionItemProvider( + 'RediSearch', + { + provideCompletionItems: (): monacoEditor.languages.CompletionList => ({ suggestions: suggestionsRef.current }) } + ).dispose + + editor.onDidChangeCursorPosition(() => { + suggestionsRef.current = [] + + if (!editor.getSelection()?.isEmpty()) { + setTimeout(() => editor?.trigger('', 'hideSuggestWidget', null), 0) + return + } + + suggestionsRef.current = getSuggestions(editor) + + if (suggestionsRef.current?.length) { + triggerSuggestions() + helpWidgetRef.current.isOpen = false + return + } + + setTimeout(() => editor?.trigger('', 'hideSuggestWidget', null), 0) + editor?.trigger('', 'editor.action.triggerParameterHints', '') }) } + const getIndexesSuggestions = (range: monaco.IRange) => indexesRef.current.map((index) => { + const value = formatLongName(bufferToString(index)) + + return { + label: value, + kind: monacoEditor.languages.CompletionItemKind.Snippet, + insertText: `"${value}" "$1" `, + insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, + range, + } + }) + + const getFieldsSuggestions = (range: monaco.IRange, spaceAfter = false) => ['field1', 'field2'].map((field) => ({ + label: field, + kind: monacoEditor.languages.CompletionItemKind.Reference, + insertText: `${field}${spaceAfter ? ' ' : ''}`, + insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, + range, + })) + + const triggerSuggestions = () => { + const { monaco, editor } = monacoObjects.current || {} + if (!monaco) return + + setTimeout(() => editor?.trigger('', 'editor.action.triggerSuggest', { auto: false })) + } + + const getSuggestions = ( + editor: monacoEditor.editor.IStandaloneCodeEditor + ): monacoEditor.languages.CompletionItem[] => { + const position = editor.getPosition() + const model = editor.getModel() + + if (!position || !model) return [] + + const value = editor.getValue() + const offset = model.getOffsetAt(position) + const word = model.getWordUntilPosition(position) + const range = getRange(position, word) + + const { args, isCursorInArg, prevCursorChar, nextCursorChar } = splitQueryByArgs(value, offset) + const [beforeOffsetArgs] = args + const [firstArg, ...prevArgs] = beforeOffsetArgs + + const commandName = firstArg?.toUpperCase() + const command = REDIS_COMMANDS_SPEC[commandName] as unknown as SearchCommand + + if (!command && position.lineNumber === 1 && word.startColumn === 1) { + return SUPPORTED_COMMANDS.map((command) => buildSuggestion( + command, + range, + { + detail: generateDetail(command), + documentation: { + value: getCommandMarkdown(command as any) + }, + } + )) + } + + if (!command) return [] + + // cover query + if (command?.arguments?.[prevArgs.length]?.name === 'query') { + if (prevCursorChar === '@') { + return getFieldsSuggestions(range) + } + + return [] + } + + if (isCursorInArg || nextCursorChar?.trim()) return [] + + // just suggest indexes - in future get from BE + if (prevArgs.length === 0 && command?.arguments?.[0]?.name === 'index') { + return getIndexesSuggestions(range) + } + + if (prevArgs.length < 2) return [] + const foundArg = findCurrentArgument(command?.arguments || [], prevArgs) + + console.log('foundArg', foundArg) + helpWidgetRef.current = { + isOpen: !!foundArg?.stopArg, + parent: foundArg?.parent, + currentArg: foundArg?.stopArg + } + + // here we suggest arguments of argument + if (foundArg && !foundArg.isComplete) { + if (foundArg.stopArg?.name === 'field') { + return getFieldsSuggestions(range, true) + } + + if (foundArg.isBlocked) return [] + if (foundArg.append?.length) { + return foundArg.append.map((arg: any) => buildSuggestion( + arg, + range, + { + kind: monacoEditor.languages.CompletionItemKind.Property, + detail: generateDetail(foundArg?.parent) + } + )) + } + + return [] + } + + // the main list of arguments + optional from argument + // TODO: remove arguments which already used if they are not multiple + if (!foundArg || foundArg.isComplete) { + // here we can add append arguments + const appendCommands = foundArg?.append ?? [] + + return [ + ...appendCommands.map((arg: any) => buildSuggestion( + arg, + range, + { + sortText: 'a', + kind: monacoEditor.languages.CompletionItemKind.Property, + detail: generateDetail(foundArg?.parent) + } + )), + ...(command.arguments || []) + .filter((arg) => arg.optional) + .filter((arg) => arg.multiple || !args.flat().includes(arg.token || arg.arguments?.[0]?.token || '')) + .map((arg: any) => buildSuggestion( + arg, + range, + { + sortText: 'b', + kind: monacoEditor.languages.CompletionItemKind.Reference, + detail: generateDetail(arg) + } + )) + ] + } + + return [] + } + return ( -
-
{}} - role="textbox" - tabIndex={0} - data-testid="main-input-container-area" - > -
- setValue(val)} - language="RediSearch" - theme={theme === Theme.Dark ? 'dark' : 'light'} - options={options} - /> -
-
- - -
-
-
+ ) } diff --git a/redisinsight/ui/src/pages/search/components/query/styles.module.scss b/redisinsight/ui/src/pages/search/components/query/styles.module.scss index 18b46e1931..e69de29bb2 100644 --- a/redisinsight/ui/src/pages/search/components/query/styles.module.scss +++ b/redisinsight/ui/src/pages/search/components/query/styles.module.scss @@ -1,76 +0,0 @@ -.wrapper { - position: relative; - height: 100%; - - :global(.editorBounder) { - bottom: 6px; - left: 18px; - right: 46px; - } -} - -.container { - display: flex; - flex-direction: column; - padding: 8px 16px; - width: 100%; - height: 100%; - word-break: break-word; - text-align: left; - letter-spacing: 0; - background-color: var(--rsInputWrapperColor); - color: var(--euiTextSubduedColor) !important; - border: 1px solid var(--euiColorLightShade); -} - -.disabled { - opacity: 0.8; -} - -.disabledActions { - pointer-events: none; - user-select: none; -} - -.containerPlaceholder { - display: flex; - padding: 8px 16px 8px 16px; - width: 100%; - height: 100%; - background-color: var(--rsInputWrapperColor); - color: var(--euiTextSubduedColor) !important; - border: 1px solid var(--euiColorLightShade); - > div { - border: 1px solid var(--euiColorLightShade); - background-color: var(--euiColorEmptyShade); - padding: 8px 20px; - width: 100%; - } -} - -.input { - // cannot use overflow since suggestions are absolute - max-height: calc(100% - 32px); - flex-grow: 1; - width: 100%; - border: 1px solid var(--euiColorLightShade); - background-color: var(--rsInputColor); -} - -.queryFooter { - display: flex; - align-items: center; - justify-content: space-between; - - margin-top: 8px; - flex-shrink: 0; -} - -#script { - font: normal normal bold 14px/17px Inconsolata !important; - color: var(--textColorShade); - caret-color: var(--euiColorFullShade); - min-width: 5px; - display: inline; -} - diff --git a/redisinsight/ui/src/pages/search/components/query/types.ts b/redisinsight/ui/src/pages/search/components/query/types.ts new file mode 100644 index 0000000000..d1552818de --- /dev/null +++ b/redisinsight/ui/src/pages/search/components/query/types.ts @@ -0,0 +1,22 @@ +export enum TokenType { + PureToken = 'pure-token', + Block = 'block', + OneOf = 'oneof' +} + +export enum ArgName { + NArgs = 'nargs' +} + +export interface SearchCommand { + name?: string + type?: TokenType + token?: string + optional?: boolean + multiple?: boolean + arguments?: SearchCommand[] +} + +export interface SearchCommandTree extends SearchCommand { + parent?: SearchCommandTree +} diff --git a/redisinsight/ui/src/pages/search/components/query/utils.ts b/redisinsight/ui/src/pages/search/components/query/utils.ts new file mode 100644 index 0000000000..26e0436f72 --- /dev/null +++ b/redisinsight/ui/src/pages/search/components/query/utils.ts @@ -0,0 +1,354 @@ +/* eslint-disable no-continue */ + +import * as monacoEditor from 'monaco-editor' +import { monaco } from 'react-monaco-editor' +import { isString, toNumber } from 'lodash' +import { generateArgsNames, Maybe, Nullable } from 'uiSrc/utils' +import { CommandProvider } from 'uiSrc/constants' +import { ArgName, SearchCommand, SearchCommandTree, TokenType } from './types' + +export const buildSuggestion = (arg: SearchCommand, range: monaco.IRange, options: any = {}) => ({ + label: isString(arg) ? arg : arg.token || arg.arguments?.[0].token || arg.name || '', + insertText: `${arg.token || arg.arguments?.[0].token || arg.name?.toUpperCase() || ''} `, + insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, + range, + kind: options?.kind || monacoEditor.languages.CompletionItemKind.Function, + ...options, +}) + +export const splitQueryByArgs = (query: string, position: number = 0) => { + const args: [string[], string[]] = [[], []] + let arg = '' + let inQuotes = false + let escapeNextChar = false + let quoteChar = '' + const isCursorInArg = false + let lastArg = '' + + const pushToProperTuple = (isAfterOffset: boolean, arg: string) => { + lastArg = arg + isAfterOffset ? args[1].push(arg) : args[0].push(arg) + } + + const updateLastArgument = (isAfterOffset: boolean, arg: string) => { + const argsBySide = args[isAfterOffset ? 1 : 0] + argsBySide[argsBySide.length - 1] = `${argsBySide[argsBySide.length - 1]} ${arg}` + } + + for (let i = 0; i < query.length; i++) { + const char = query[i] + const isAfterOffset = i >= position + (inQuotes ? -1 : 0) + + if (escapeNextChar) { + arg += char + escapeNextChar = !quoteChar + } else if (char === '\\') { + escapeNextChar = true + } else if (inQuotes) { + if (char === quoteChar) { + inQuotes = false + const argWithChat = arg + char + + if (isCompositeArgument(argWithChat, lastArg)) { + updateLastArgument(isAfterOffset, argWithChat) + } else { + pushToProperTuple(isAfterOffset, argWithChat) + } + + arg = '' + } else { + arg += char + } + } else if (char === '"' || char === "'") { + inQuotes = true + quoteChar = char + arg += char + } else if (char === ' ' || char === '\n') { + if (arg.length > 0) { + if (isCompositeArgument(arg, lastArg)) { + updateLastArgument(isAfterOffset, arg) + } else { + pushToProperTuple(isAfterOffset, arg) + } + + arg = '' + } + } else { + arg += char + } + } + + if (arg.length > 0) { + pushToProperTuple(true, arg) + } + + return { args, isCursorInArg, prevCursorChar: query[position - 1], nextCursorChar: query[position] } +} + +export const getRange = (position: monaco.Position, word: monaco.editor.IWordAtPosition): monaco.IRange => ({ + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + endColumn: word.endColumn, + startColumn: word.startColumn, +}) + +export const findCurrentArgument = ( + args: SearchCommand[], + prev: string[], + parent?: SearchCommandTree +): Nullable<{ + isComplete: boolean + stopArg: Maybe, + isBlocked: boolean, + append: Maybe[], + parent: Maybe +}> => { + for (let i = prev.length - 1; i >= 0; i--) { + const arg = prev[i] + const currentArg = findArgByToken(args, arg) + const currentWithParent: SearchCommandTree = { ...currentArg, parent } + + if (currentArg?.arguments && currentArg?.type === TokenType.Block) { + return findCurrentArgument(currentArg.arguments, prev.slice(i), currentWithParent) + } + + const tokenIndex = args.findIndex((cArg) => + (cArg.type === TokenType.OneOf + ? cArg.arguments?.some((oneOfArg) => oneOfArg.token?.toLowerCase() === arg.toLowerCase()) + : cArg.token?.toLowerCase() === arg.toLowerCase())) + const token = args[tokenIndex] + + if (token) { + const pastArgs = prev.slice(i) + const commandArgs = parent ? args.slice(tokenIndex, args.length) : [token] + + // getArgByRest - here we preparing the list of arguments which can be inserted, + // this is the main function which creates the list of arguments + return { + ...getArgumentSuggestions(pastArgs, commandArgs, parent), + parent: parent || token + } + } + } + + return null +} + +const findStopArgumentInQuery = ( + queryArgs: string[], + restCommandArgs: Maybe = [], +): { + restArguments: SearchCommand[] + stopArgIndex: number + isBlocked: boolean +} => { + let currentCommandArgIndex = 0 + let isBlockedOnCommand = false + let multipleIndexStart = 0 + + const moveToNextCommandArg = () => currentCommandArgIndex++ + const blockCommand = () => { isBlockedOnCommand = true } + const unBlockCommand = () => { isBlockedOnCommand = false } + + const skipArg = () => { + moveToNextCommandArg() + unBlockCommand() + } + + for (let i = 0; i < queryArgs.length; i++) { + const arg = queryArgs[i] + const currentCommandArg = restCommandArgs[currentCommandArgIndex] + + if (currentCommandArg?.type === TokenType.PureToken) { + skipArg() + continue + } + + // if we are on token - that requires one more argument + if (currentCommandArg?.token === arg.toUpperCase()) { + blockCommand() + continue + } + + if (currentCommandArg?.type === TokenType.Block) { + // if block is multiple - we duplicate nArgs inner arguments + let blockArguments = currentCommandArg.arguments + + if (currentCommandArg?.multiple) { + const nArgs = toNumber(queryArgs[i - 1]) || 0 + blockArguments = Array(nArgs).fill(currentCommandArg.arguments).flat() + } + + const blockSuggestion = findStopArgumentInQuery(queryArgs.slice(i), blockArguments) + const stopArg = blockSuggestion.restArguments?.[blockSuggestion.stopArgIndex] + if (blockSuggestion.isBlocked || stopArg) return blockSuggestion + + i += queryArgs.slice(i).length - 1 + skipArg() + continue + } + + if (currentCommandArg?.name === ArgName.NArgs) { + const numberOfArgs = toNumber(arg) + + if (numberOfArgs === 0) { + moveToNextCommandArg() + skipArg() + continue + } + + moveToNextCommandArg() + blockCommand() + continue + } + + if (currentCommandArg?.type === TokenType.OneOf && currentCommandArg?.optional) { + // if oneof is optional then we can switch to another argument + if (!currentCommandArg?.arguments?.some(({ token }) => token === arg)) { + moveToNextCommandArg() + } + + skipArg() + continue + } + + if (currentCommandArg?.multiple) { + const numberOfArgs = toNumber(queryArgs[currentCommandArgIndex]) || 0 + + if (!multipleIndexStart) multipleIndexStart = currentCommandArgIndex + if (i - multipleIndexStart >= numberOfArgs) { + skipArg() + continue + } + + blockCommand() + continue + } + + moveToNextCommandArg() + + const nextCommand = restCommandArgs[currentCommandArgIndex + 1] + isBlockedOnCommand = (!!nextCommand && !nextCommand.optional) + } + + return { + restArguments: restCommandArgs, + stopArgIndex: currentCommandArgIndex, + isBlocked: isBlockedOnCommand + } +} + +export const getArgumentSuggestions = ( + pastStringArgs: string[], + pastCommandArgs: SearchCommand[], + current?: SearchCommandTree +): { + isComplete: boolean + stopArg: Maybe, + isBlocked: boolean, + append: Maybe[], +} => { + const { + restArguments, + stopArgIndex, + isBlocked: isWasBlocked + } = findStopArgumentInQuery(pastStringArgs, pastCommandArgs) + + // TODO refactor to early return related to where we stopped + + const stopArgument = restArguments[stopArgIndex] + const restNotFilledArgs = restArguments.slice(stopArgIndex) + const isBlocked = isWasBlocked || (stopArgument && !(stopArgument.token || stopArgument?.arguments?.length)) + const restParentOptionalSuggestions = !stopArgument || stopArgument?.optional + ? getRestParentArguments(current?.parent, current?.name, current?.multiple) + .filter((arg) => arg.optional) + .filter((arg) => arg.name !== current?.name) + : [] + + const restOptionalSuggestions = fillArgsByType([...restNotFilledArgs, ...restParentOptionalSuggestions]) + const isOneOfArgument = stopArgument?.type === TokenType.OneOf + || (stopArgument?.type === TokenType.PureToken && current?.parent?.type === TokenType.OneOf) + const isArgSuggestions = stopArgument && !stopArgument.optional && (stopArgument?.token || isOneOfArgument) + + const suggestions = isArgSuggestions + // only 1 suggestion since next arg is required + ? [isOneOfArgument ? stopArgument.arguments : stopArgument].flat() + : !isBlocked + ? restOptionalSuggestions + : [] + + const requiredArgsLength = restNotFilledArgs.filter((arg) => !arg.optional).length + + return { + isComplete: requiredArgsLength === 0 && !isBlocked, + stopArg: stopArgument, + isBlocked: isBlocked && !isOneOfArgument, + append: suggestions, + } +} + +export const getRestParentArguments = ( + parent?: SearchCommandTree, + currentArgName?: string, + isIncludeOwn: boolean = true, + prevArgs: SearchCommand[] = [] +): SearchCommand[] => { + if (!currentArgName) return [] + + const currentArgIndex = parent?.arguments?.findIndex((arg) => arg?.name === currentArgName) + if (!currentArgIndex) return prevArgs + + const currentRestArgs = parent?.arguments?.slice(currentArgIndex + (isIncludeOwn ? 0 : 1)) || [] + + if (parent?.parent) return getRestParentArguments(parent.parent, parent.name, true, currentRestArgs) + + return [...currentRestArgs, ...prevArgs] +} + +export const fillArgsByType = (args: SearchCommand[]) => { + const result = [] + + for (let i = 0; i < args.length; i++) { + const currentArg = args[i] + + if (currentArg.type === TokenType.OneOf) { + result.push(...(currentArg?.arguments || [])) + } + + if (currentArg.type === TokenType.Block) { + result.push(currentArg.arguments?.[0]) + } + + if (currentArg.token) { + result.push(currentArg) + } + } + + return result +} + +export const findArgByToken = (list: SearchCommand[], arg: string): Maybe => + list.find((cArg) => + (cArg.type === TokenType.OneOf + ? cArg.arguments?.some((oneOfArg) => oneOfArg?.token?.toLowerCase() === arg?.toLowerCase()) + : cArg.arguments?.[0]?.token?.toLowerCase() === arg.toLowerCase())) + +export const isCompositeArgument = (arg: string, prevArg?: string) => arg === '*' && prevArg === 'LOAD' + +export const generateDetail = (command: Maybe) => { + if (!command) return '' + + if (command.arguments) { + return generateArgsNames(CommandProvider.Main, command.arguments).join(' ') + } + + if (command.token) { + if (command.type === TokenType.PureToken) { + return command.token + } + + return `${command.token} ${command.name}` + } + + return '' +} From 00f546ad1df850a9b2a1ddcd211cdae08715b51f Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Mon, 5 Aug 2024 12:40:16 +0200 Subject: [PATCH 019/112] #RI-5957 - add monaco lang id --- redisinsight/ui/src/constants/monaco/monaco.ts | 1 + redisinsight/ui/src/pages/search/components/query/Query.tsx | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/redisinsight/ui/src/constants/monaco/monaco.ts b/redisinsight/ui/src/constants/monaco/monaco.ts index fad4f5528c..a8eea653d4 100644 --- a/redisinsight/ui/src/constants/monaco/monaco.ts +++ b/redisinsight/ui/src/constants/monaco/monaco.ts @@ -39,6 +39,7 @@ export enum MonacoLanguage { JMESPath = 'jmespathLanguage', SQLiteFunctions = 'sqliteFunctions', Text = 'text', + RediSearch = 'redisearch', } export const defaultMonacoOptions: monacoEditor.editor.IStandaloneEditorConstructionOptions = { diff --git a/redisinsight/ui/src/pages/search/components/query/Query.tsx b/redisinsight/ui/src/pages/search/components/query/Query.tsx index ef9cc0768a..0b000a3452 100644 --- a/redisinsight/ui/src/pages/search/components/query/Query.tsx +++ b/redisinsight/ui/src/pages/search/components/query/Query.tsx @@ -3,7 +3,7 @@ import MonacoEditor, { monaco } from 'react-monaco-editor' import * as monacoEditor from 'monaco-editor' import { useSelector } from 'react-redux' import { merge } from 'lodash' -import { defaultMonacoOptions, Theme } from 'uiSrc/constants' +import { defaultMonacoOptions, MonacoLanguage, Theme } from 'uiSrc/constants' import { ThemeContext } from 'uiSrc/contexts/themeContext' import { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands' import { bufferToString, formatLongName, getCommandMarkdown, Nullable } from 'uiSrc/utils' @@ -57,7 +57,7 @@ const Query = (props: Props) => { const { theme } = useContext(ThemeContext) useEffect(() => { - monaco.languages.register({ id: 'RediSearch' }) + monaco.languages.register({ id: MonacoLanguage.RediSearch }) return () => { disposeCompletionItemProvider.current?.() @@ -280,7 +280,7 @@ const Query = (props: Props) => { Date: Mon, 5 Aug 2024 13:46:09 +0200 Subject: [PATCH 020/112] #RI-5957 - refactor, fix tests --- .../pages/search/components/query/Query.tsx | 197 ++++++------------ .../search/components/query/constants.ts | 12 ++ .../search/{components/query => }/types.ts | 0 .../ui/src/pages/search/utils/index.ts | 2 + .../ui/src/pages/search/utils/monaco.ts | 74 +++++++ .../query/utils.ts => utils/query.ts} | 26 +-- 6 files changed, 155 insertions(+), 156 deletions(-) create mode 100644 redisinsight/ui/src/pages/search/components/query/constants.ts rename redisinsight/ui/src/pages/search/{components/query => }/types.ts (100%) create mode 100644 redisinsight/ui/src/pages/search/utils/index.ts create mode 100644 redisinsight/ui/src/pages/search/utils/monaco.ts rename redisinsight/ui/src/pages/search/{components/query/utils.ts => utils/query.ts} (90%) diff --git a/redisinsight/ui/src/pages/search/components/query/Query.tsx b/redisinsight/ui/src/pages/search/components/query/Query.tsx index 0b000a3452..dcd2d39352 100644 --- a/redisinsight/ui/src/pages/search/components/query/Query.tsx +++ b/redisinsight/ui/src/pages/search/components/query/Query.tsx @@ -1,22 +1,23 @@ import React, { useContext, useEffect, useRef } from 'react' -import MonacoEditor, { monaco } from 'react-monaco-editor' -import * as monacoEditor from 'monaco-editor' +import MonacoEditor, { monaco as monacoEditor } from 'react-monaco-editor' import { useSelector } from 'react-redux' -import { merge } from 'lodash' -import { defaultMonacoOptions, MonacoLanguage, Theme } from 'uiSrc/constants' + +import { MonacoLanguage, Theme } from 'uiSrc/constants' import { ThemeContext } from 'uiSrc/contexts/themeContext' import { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands' -import { bufferToString, formatLongName, getCommandMarkdown, Nullable } from 'uiSrc/utils' +import { getCommandMarkdown, Nullable } from 'uiSrc/utils' import { IEditorMount } from 'uiSrc/pages/workbench/interfaces' import { buildSuggestion, findCurrentArgument, - generateDetail, + generateDetail, getFieldsSuggestions, getIndexesSuggestions, getRange, + getRediSearchSignutureProvider, splitQueryByArgs -} from 'uiSrc/pages/search/components/query/utils' -import { SearchCommand, TokenType } from 'uiSrc/pages/search/components/query/types' +} from 'uiSrc/pages/search/utils' +import { SearchCommand } from 'uiSrc/pages/search/types' import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' +import { SUPPORTED_COMMANDS_LIST, options } from './constants' export interface Props { value: string @@ -24,16 +25,6 @@ export interface Props { indexes: RedisResponseBuffer[] } -const options = merge(defaultMonacoOptions, - { - suggest: { - showWords: false, - showIcons: true - } - }) - -const SUPPORTED_COMMANDS_LIST = ['FT.SEARCH', 'FT.AGGREGATE'] - const Query = (props: Props) => { const { value, onChange, indexes } = props const { spec: REDIS_COMMANDS_SPEC } = useSelector(appRedisCommandsSelector) @@ -56,13 +47,9 @@ const Query = (props: Props) => { const { theme } = useContext(ThemeContext) - useEffect(() => { - monaco.languages.register({ id: MonacoLanguage.RediSearch }) - - return () => { - disposeCompletionItemProvider.current?.() - disposeSignatureHelpProvider.current?.() - } + useEffect(() => () => { + disposeCompletionItemProvider.current?.() + disposeSignatureHelpProvider.current?.() }, []) useEffect(() => { @@ -73,85 +60,47 @@ const Query = (props: Props) => { editor: monacoEditor.editor.IStandaloneCodeEditor, monaco: typeof monacoEditor ) => { + monaco.languages.register({ id: MonacoLanguage.RediSearch }) monacoObjects.current = { editor, monaco } suggestionsRef.current = getSuggestions(editor) triggerSuggestions() disposeSignatureHelpProvider.current?.() - disposeSignatureHelpProvider.current = monaco.languages.registerSignatureHelpProvider( - 'RediSearch', - { - provideSignatureHelp: (): any => { - if (!helpWidgetRef.current?.isOpen) return null - - const { currentArg, parent } = helpWidgetRef.current - const label = generateDetail(parent) - const arg = currentArg?.type === TokenType.Block ? currentArg.arguments[0]?.name : (currentArg?.name || currentArg?.type || '') - - return { - dispose: () => {}, - value: { - activeParameter: 0, - activeSignature: 0, - signatures: [{ - label: label || '', - parameters: [{ label: arg }] - }] - } - } - } - } - ).dispose + disposeSignatureHelpProvider.current = monaco.languages.registerSignatureHelpProvider(MonacoLanguage.RediSearch, { + provideSignatureHelp: (): any => getRediSearchSignutureProvider(helpWidgetRef?.current) + }).dispose disposeCompletionItemProvider.current?.() - disposeCompletionItemProvider.current = monaco.languages.registerCompletionItemProvider( - 'RediSearch', - { - provideCompletionItems: (): monacoEditor.languages.CompletionList => ({ suggestions: suggestionsRef.current }) - } - ).dispose - - editor.onDidChangeCursorPosition(() => { - suggestionsRef.current = [] + disposeCompletionItemProvider.current = monaco.languages.registerCompletionItemProvider(MonacoLanguage.RediSearch, { + provideCompletionItems: (): monacoEditor.languages.CompletionList => ({ suggestions: suggestionsRef.current }) + }).dispose - if (!editor.getSelection()?.isEmpty()) { - setTimeout(() => editor?.trigger('', 'hideSuggestWidget', null), 0) - return - } + editor.onDidChangeCursorPosition(handleCursorChange) + } - suggestionsRef.current = getSuggestions(editor) + const handleCursorChange = () => { + const { editor } = monacoObjects.current || {} + suggestionsRef.current = [] - if (suggestionsRef.current?.length) { - triggerSuggestions() - helpWidgetRef.current.isOpen = false - return - } + if (!editor) return + if (!editor.getSelection()?.isEmpty()) { setTimeout(() => editor?.trigger('', 'hideSuggestWidget', null), 0) - editor?.trigger('', 'editor.action.triggerParameterHints', '') - }) - } + return + } - const getIndexesSuggestions = (range: monaco.IRange) => indexesRef.current.map((index) => { - const value = formatLongName(bufferToString(index)) + suggestionsRef.current = getSuggestions(editor) - return { - label: value, - kind: monacoEditor.languages.CompletionItemKind.Snippet, - insertText: `"${value}" "$1" `, - insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, - range, + if (suggestionsRef.current?.length) { + triggerSuggestions() + helpWidgetRef.current.isOpen = false + return } - }) - const getFieldsSuggestions = (range: monaco.IRange, spaceAfter = false) => ['field1', 'field2'].map((field) => ({ - label: field, - kind: monacoEditor.languages.CompletionItemKind.Reference, - insertText: `${field}${spaceAfter ? ' ' : ''}`, - insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, - range, - })) + setTimeout(() => editor?.trigger('', 'hideSuggestWidget', null), 0) + editor?.trigger('', 'editor.action.triggerParameterHints', '') + } const triggerSuggestions = () => { const { monaco, editor } = monacoObjects.current || {} @@ -181,16 +130,12 @@ const Query = (props: Props) => { const command = REDIS_COMMANDS_SPEC[commandName] as unknown as SearchCommand if (!command && position.lineNumber === 1 && word.startColumn === 1) { - return SUPPORTED_COMMANDS.map((command) => buildSuggestion( - command, - range, - { - detail: generateDetail(command), - documentation: { - value: getCommandMarkdown(command as any) - }, - } - )) + return SUPPORTED_COMMANDS.map((command) => buildSuggestion(command, range, { + detail: generateDetail(command), + documentation: { + value: getCommandMarkdown(command as any) + }, + })) } if (!command) return [] @@ -198,7 +143,7 @@ const Query = (props: Props) => { // cover query if (command?.arguments?.[prevArgs.length]?.name === 'query') { if (prevCursorChar === '@') { - return getFieldsSuggestions(range) + return getFieldsSuggestions(['field1', 'field2'], range) } return [] @@ -206,70 +151,54 @@ const Query = (props: Props) => { if (isCursorInArg || nextCursorChar?.trim()) return [] - // just suggest indexes - in future get from BE - if (prevArgs.length === 0 && command?.arguments?.[0]?.name === 'index') { - return getIndexesSuggestions(range) + // cover index field + if (command?.arguments?.[prevArgs.length]?.name === 'index') { + return getIndexesSuggestions(indexesRef.current, range) } if (prevArgs.length < 2) return [] const foundArg = findCurrentArgument(command?.arguments || [], prevArgs) - console.log('foundArg', foundArg) helpWidgetRef.current = { isOpen: !!foundArg?.stopArg, parent: foundArg?.parent, currentArg: foundArg?.stopArg } - // here we suggest arguments of argument + // suggest arguments of argument if (foundArg && !foundArg.isComplete) { if (foundArg.stopArg?.name === 'field') { - return getFieldsSuggestions(range, true) + return getFieldsSuggestions(['field1', 'field2'], range, true) } if (foundArg.isBlocked) return [] if (foundArg.append?.length) { - return foundArg.append.map((arg: any) => buildSuggestion( - arg, - range, - { - kind: monacoEditor.languages.CompletionItemKind.Property, - detail: generateDetail(foundArg?.parent) - } - )) + return foundArg.append.map((arg: any) => buildSuggestion(arg, range, { + kind: monacoEditor.languages.CompletionItemKind.Property, + detail: generateDetail(foundArg?.parent) + })) } return [] } - // the main list of arguments + optional from argument - // TODO: remove arguments which already used if they are not multiple if (!foundArg || foundArg.isComplete) { - // here we can add append arguments const appendCommands = foundArg?.append ?? [] return [ - ...appendCommands.map((arg: any) => buildSuggestion( - arg, - range, - { - sortText: 'a', - kind: monacoEditor.languages.CompletionItemKind.Property, - detail: generateDetail(foundArg?.parent) - } - )), - ...(command.arguments || []) + ...appendCommands.map((arg: any) => buildSuggestion(arg, range, { + sortText: 'a', + kind: monacoEditor.languages.CompletionItemKind.Property, + detail: generateDetail(foundArg?.parent) + })), + ...(command?.arguments || []) .filter((arg) => arg.optional) .filter((arg) => arg.multiple || !args.flat().includes(arg.token || arg.arguments?.[0]?.token || '')) - .map((arg: any) => buildSuggestion( - arg, - range, - { - sortText: 'b', - kind: monacoEditor.languages.CompletionItemKind.Reference, - detail: generateDetail(arg) - } - )) + .map((arg: any) => buildSuggestion(arg, range, { + sortText: 'b', + kind: monacoEditor.languages.CompletionItemKind.Reference, + detail: generateDetail(arg) + })) ] } diff --git a/redisinsight/ui/src/pages/search/components/query/constants.ts b/redisinsight/ui/src/pages/search/components/query/constants.ts new file mode 100644 index 0000000000..120ea9f891 --- /dev/null +++ b/redisinsight/ui/src/pages/search/components/query/constants.ts @@ -0,0 +1,12 @@ +import { merge } from 'lodash' +import { defaultMonacoOptions } from 'uiSrc/constants' + +export const options = merge(defaultMonacoOptions, + { + suggest: { + showWords: false, + showIcons: true + } + }) + +export const SUPPORTED_COMMANDS_LIST = ['FT.SEARCH', 'FT.AGGREGATE'] diff --git a/redisinsight/ui/src/pages/search/components/query/types.ts b/redisinsight/ui/src/pages/search/types.ts similarity index 100% rename from redisinsight/ui/src/pages/search/components/query/types.ts rename to redisinsight/ui/src/pages/search/types.ts diff --git a/redisinsight/ui/src/pages/search/utils/index.ts b/redisinsight/ui/src/pages/search/utils/index.ts new file mode 100644 index 0000000000..c820377353 --- /dev/null +++ b/redisinsight/ui/src/pages/search/utils/index.ts @@ -0,0 +1,2 @@ +export * from './query' +export * from './monaco' diff --git a/redisinsight/ui/src/pages/search/utils/monaco.ts b/redisinsight/ui/src/pages/search/utils/monaco.ts new file mode 100644 index 0000000000..cfdf1bb645 --- /dev/null +++ b/redisinsight/ui/src/pages/search/utils/monaco.ts @@ -0,0 +1,74 @@ +import { monaco } from 'react-monaco-editor' +import * as monacoEditor from 'monaco-editor' +import { isString } from 'lodash' +import { generateDetail } from 'uiSrc/pages/search/utils/query' +import { SearchCommand, TokenType } from 'uiSrc/pages/search/types' +import { bufferToString, formatLongName, Maybe } from 'uiSrc/utils' +import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' + +export const getRange = (position: monaco.Position, word: monaco.editor.IWordAtPosition): monaco.IRange => ({ + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + endColumn: word.endColumn, + startColumn: word.startColumn, +}) + +export const buildSuggestion = (arg: SearchCommand, range: monaco.IRange, options: any = {}) => ({ + label: isString(arg) ? arg : arg.token || arg.arguments?.[0].token || arg.name || '', + insertText: `${arg.token || arg.arguments?.[0].token || arg.name?.toUpperCase() || ''} `, + insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, + range, + kind: options?.kind || monacoEditor.languages.CompletionItemKind.Function, + ...options, +}) + +export const getIndexesSuggestions = (indexes: RedisResponseBuffer[], range: monaco.IRange) => + indexes.map((index) => { + const value = formatLongName(bufferToString(index)) + + console.log(value) + + return { + label: value || ' ', + kind: monacoEditor.languages.CompletionItemKind.Snippet, + insertText: `"${value}" "$1" `, + insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, + range, + } + }) + +export const getFieldsSuggestions = (fields: string[], range: monaco.IRange, spaceAfter = false) => + fields.map((field) => ({ + label: field, + kind: monacoEditor.languages.CompletionItemKind.Reference, + insertText: `${field}${spaceAfter ? ' ' : ''}`, + insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, + range, + })) + +export const getRediSearchSignutureProvider = (options: Maybe<{ + isOpen: boolean + currentArg: SearchCommand + parent: Maybe +}>) => { + const { isOpen, currentArg, parent } = options || {} + + if (!isOpen) return null + + const label = generateDetail(parent) + const arg = currentArg?.type === TokenType.Block + ? currentArg?.arguments?.[0]?.name + : (currentArg?.name || currentArg?.type || '') + + return { + dispose: () => {}, + value: { + activeParameter: 0, + activeSignature: 0, + signatures: [{ + label: label || '', + parameters: [{ label: arg }] + }] + } + } +} diff --git a/redisinsight/ui/src/pages/search/components/query/utils.ts b/redisinsight/ui/src/pages/search/utils/query.ts similarity index 90% rename from redisinsight/ui/src/pages/search/components/query/utils.ts rename to redisinsight/ui/src/pages/search/utils/query.ts index 26e0436f72..58286b67b8 100644 --- a/redisinsight/ui/src/pages/search/components/query/utils.ts +++ b/redisinsight/ui/src/pages/search/utils/query.ts @@ -1,20 +1,9 @@ /* eslint-disable no-continue */ -import * as monacoEditor from 'monaco-editor' -import { monaco } from 'react-monaco-editor' -import { isString, toNumber } from 'lodash' +import { toNumber } from 'lodash' import { generateArgsNames, Maybe, Nullable } from 'uiSrc/utils' import { CommandProvider } from 'uiSrc/constants' -import { ArgName, SearchCommand, SearchCommandTree, TokenType } from './types' - -export const buildSuggestion = (arg: SearchCommand, range: monaco.IRange, options: any = {}) => ({ - label: isString(arg) ? arg : arg.token || arg.arguments?.[0].token || arg.name || '', - insertText: `${arg.token || arg.arguments?.[0].token || arg.name?.toUpperCase() || ''} `, - insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, - range, - kind: options?.kind || monacoEditor.languages.CompletionItemKind.Function, - ...options, -}) +import { ArgName, SearchCommand, SearchCommandTree, TokenType } from '../types' export const splitQueryByArgs = (query: string, position: number = 0) => { const args: [string[], string[]] = [[], []] @@ -85,13 +74,6 @@ export const splitQueryByArgs = (query: string, position: number = 0) => { return { args, isCursorInArg, prevCursorChar: query[position - 1], nextCursorChar: query[position] } } -export const getRange = (position: monaco.Position, word: monaco.editor.IWordAtPosition): monaco.IRange => ({ - startLineNumber: position.lineNumber, - endLineNumber: position.lineNumber, - endColumn: word.endColumn, - startColumn: word.startColumn, -}) - export const findCurrentArgument = ( args: SearchCommand[], prev: string[], @@ -114,7 +96,7 @@ export const findCurrentArgument = ( const tokenIndex = args.findIndex((cArg) => (cArg.type === TokenType.OneOf - ? cArg.arguments?.some((oneOfArg) => oneOfArg.token?.toLowerCase() === arg.toLowerCase()) + ? cArg.arguments?.some((oneOfArg: SearchCommand) => oneOfArg.token?.toLowerCase() === arg.toLowerCase()) : cArg.token?.toLowerCase() === arg.toLowerCase())) const token = args[tokenIndex] @@ -330,7 +312,7 @@ export const fillArgsByType = (args: SearchCommand[]) => { export const findArgByToken = (list: SearchCommand[], arg: string): Maybe => list.find((cArg) => (cArg.type === TokenType.OneOf - ? cArg.arguments?.some((oneOfArg) => oneOfArg?.token?.toLowerCase() === arg?.toLowerCase()) + ? cArg.arguments?.some((oneOfArg: SearchCommand) => oneOfArg?.token?.toLowerCase() === arg?.toLowerCase()) : cArg.arguments?.[0]?.token?.toLowerCase() === arg.toLowerCase())) export const isCompositeArgument = (arg: string, prevArg?: string) => arg === '*' && prevArg === 'LOAD' From 0c9a7976dd6f35a5835484a9871fc1ad17791298 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Mon, 5 Aug 2024 13:46:31 +0200 Subject: [PATCH 021/112] #RI-5957 - remove console.log --- redisinsight/ui/src/pages/search/utils/monaco.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/redisinsight/ui/src/pages/search/utils/monaco.ts b/redisinsight/ui/src/pages/search/utils/monaco.ts index cfdf1bb645..924ea9ba1d 100644 --- a/redisinsight/ui/src/pages/search/utils/monaco.ts +++ b/redisinsight/ui/src/pages/search/utils/monaco.ts @@ -26,8 +26,6 @@ export const getIndexesSuggestions = (indexes: RedisResponseBuffer[], range: mon indexes.map((index) => { const value = formatLongName(bufferToString(index)) - console.log(value) - return { label: value || ' ', kind: monacoEditor.languages.CompletionItemKind.Snippet, From 827da349a8931c3ad9268c56aa788f4b4b8ecae6 Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Tue, 6 Aug 2024 10:34:36 +0200 Subject: [PATCH 022/112] fix per comments --- tests/e2e/pageObjects/base-run-commands-page.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/tests/e2e/pageObjects/base-run-commands-page.ts b/tests/e2e/pageObjects/base-run-commands-page.ts index becdb303b2..cf7949b393 100644 --- a/tests/e2e/pageObjects/base-run-commands-page.ts +++ b/tests/e2e/pageObjects/base-run-commands-page.ts @@ -25,6 +25,7 @@ export class BaseRunCommandsPage extends InstancePage { queryResult = Selector('[data-testid=query-common-result]'); cssQueryCardCommand = '[data-testid=query-card-command]'; + cssQueryCardContainer = '[data-testid^="query-card-container-"]'; cssQueryTextResult = '[data-testid=query-cli-result]'; cssReRunCommandButton = '[data-testid=re-run-command]'; cssDeleteCommandButton = '[data-testid=delete-command]'; @@ -37,7 +38,7 @@ export class BaseRunCommandsPage extends InstancePage { * @param command The command */ async getCardContainerByCommand(command: string): Promise { - return this.queryCardCommand.withExactText(command).parent('[data-testid^="query-card-container-"]'); + return this.queryCardCommand.withExactText(command).parent(this.cssQueryCardContainer); } /** @@ -67,12 +68,4 @@ export class BaseRunCommandsPage extends InstancePage { const actualCommandResult = await this.queryCardContainer.nth(childNum).find(this.cssQueryTextResult).textContent; await t.expect(actualCommandResult).contains(result, 'Actual command result is not equal to executed'); } - - /** - * Get selector with tutorial name - * @param tutorialName name of the uploaded tutorial - */ - getAccordionButtonWithName(tutorialName: string): Selector { - return Selector(`[data-testid=accordion-button-${tutorialName}]`); - } } From a03732bd3fd982a2828f7cd37cc62410091c5dde Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Wed, 7 Aug 2024 18:07:28 +0200 Subject: [PATCH 023/112] #RI-5888 - add fields and indexes suggestions --- redisinsight/ui/src/constants/api.ts | 1 + .../pages/search/components/query/Query.tsx | 145 ++-- .../search/components/query/constants.ts | 6 + .../pages/search/components/query/utils.ts | 88 +++ redisinsight/ui/src/pages/search/types.ts | 10 + .../ui/src/pages/search/utils/monaco.ts | 38 +- .../ui/src/pages/search/utils/query.ts | 31 +- .../ui/src/pages/search/utils/tests/mocks.ts | 676 ++++++++++++++++++ .../pages/search/utils/tests/query.spec.ts | 351 +++++++++ .../ui/src/slices/browser/redisearch.ts | 27 + .../slices/tests/browser/redisearch.spec.ts | 37 +- 11 files changed, 1296 insertions(+), 114 deletions(-) create mode 100644 redisinsight/ui/src/pages/search/components/query/utils.ts create mode 100644 redisinsight/ui/src/pages/search/utils/tests/mocks.ts create mode 100644 redisinsight/ui/src/pages/search/utils/tests/query.spec.ts diff --git a/redisinsight/ui/src/constants/api.ts b/redisinsight/ui/src/constants/api.ts index a9d3453639..23ff703f82 100644 --- a/redisinsight/ui/src/constants/api.ts +++ b/redisinsight/ui/src/constants/api.ts @@ -113,6 +113,7 @@ enum ApiEndpoints { REDISEARCH = 'redisearch', REDISEARCH_SEARCH = 'redisearch/search', + REDISEARCH_INFO = 'redisearch/info', HISTORY = 'history', FEATURES = 'features', diff --git a/redisinsight/ui/src/pages/search/components/query/Query.tsx b/redisinsight/ui/src/pages/search/components/query/Query.tsx index dcd2d39352..e5be89b8ab 100644 --- a/redisinsight/ui/src/pages/search/components/query/Query.tsx +++ b/redisinsight/ui/src/pages/search/components/query/Query.tsx @@ -1,23 +1,28 @@ -import React, { useContext, useEffect, useRef } from 'react' +import React, { useContext, useEffect, useRef, useState } from 'react' import MonacoEditor, { monaco as monacoEditor } from 'react-monaco-editor' -import { useSelector } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { MonacoLanguage, Theme } from 'uiSrc/constants' import { ThemeContext } from 'uiSrc/contexts/themeContext' import { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands' -import { getCommandMarkdown, Nullable } from 'uiSrc/utils' +import { Nullable } from 'uiSrc/utils' import { IEditorMount } from 'uiSrc/pages/workbench/interfaces' import { - buildSuggestion, findCurrentArgument, - generateDetail, getFieldsSuggestions, getIndexesSuggestions, getRange, - getRediSearchSignutureProvider, + getRediSearchSignutureProvider, setCursorPositionAtTheEnd, splitQueryByArgs } from 'uiSrc/pages/search/utils' import { SearchCommand } from 'uiSrc/pages/search/types' import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' -import { SUPPORTED_COMMANDS_LIST, options } from './constants' +import { fetchRedisearchInfoAction } from 'uiSrc/slices/browser/redisearch' +import { SUPPORTED_COMMANDS_LIST, options, DefinedArgumentName } from './constants' +import { + getFieldsSuggestions, + getIndexesSuggestions, + asSuggestionsRef, + getMandatoryArgumentSuggestions, getOptionalSuggestions, getCommandsSuggestions +} from './utils' export interface Props { value: string @@ -29,6 +34,8 @@ const Query = (props: Props) => { const { value, onChange, indexes } = props const { spec: REDIS_COMMANDS_SPEC } = useSelector(appRedisCommandsSelector) + const [selectedIndex, setSelectedIndex] = useState('') + const SUPPORTED_COMMANDS = SUPPORTED_COMMANDS_LIST.map((name) => ({ ...REDIS_COMMANDS_SPEC[name], name @@ -37,15 +44,20 @@ const Query = (props: Props) => { const monacoObjects = useRef>(null) const disposeCompletionItemProvider = useRef(() => {}) const disposeSignatureHelpProvider = useRef(() => {}) - const suggestionsRef = useRef([]) + const suggestionsRef = useRef<{ + forceHide: boolean + data: monacoEditor.languages.CompletionItem[] + }>({ forceHide: false, data: [] }) const helpWidgetRef = useRef({ isOpen: false, parent: null, currentArg: null }) const indexesRef = useRef([]) + const attributesRef = useRef([]) const { theme } = useContext(ThemeContext) + const dispatch = useDispatch() useEffect(() => () => { disposeCompletionItemProvider.current?.() @@ -56,6 +68,18 @@ const Query = (props: Props) => { indexesRef.current = indexes }, [indexes]) + useEffect(() => { + if (!selectedIndex) return + + const index = selectedIndex.replace(/^(['"])(.*)\1$/, '$2') + + dispatch(fetchRedisearchInfoAction(index, + (data) => { + const { attributes } = data as any + attributesRef.current = attributes + })) + }, [selectedIndex]) + const editorDidMount = ( editor: monacoEditor.editor.IStandaloneCodeEditor, monaco: typeof monacoEditor @@ -63,8 +87,16 @@ const Query = (props: Props) => { monaco.languages.register({ id: MonacoLanguage.RediSearch }) monacoObjects.current = { editor, monaco } - suggestionsRef.current = getSuggestions(editor) - triggerSuggestions() + if (value) { + setCursorPositionAtTheEnd(editor) + } else { + const position = editor.getPosition() + + if (position?.column === 1 && position?.lineNumber === 1) { + suggestionsRef.current = getSuggestions(editor) + triggerSuggestions() + } + } disposeSignatureHelpProvider.current?.() disposeSignatureHelpProvider.current = monaco.languages.registerSignatureHelpProvider(MonacoLanguage.RediSearch, { @@ -73,7 +105,8 @@ const Query = (props: Props) => { disposeCompletionItemProvider.current?.() disposeCompletionItemProvider.current = monaco.languages.registerCompletionItemProvider(MonacoLanguage.RediSearch, { - provideCompletionItems: (): monacoEditor.languages.CompletionList => ({ suggestions: suggestionsRef.current }) + provideCompletionItems: (): monacoEditor.languages.CompletionList => + ({ suggestions: suggestionsRef.current.data }) }).dispose editor.onDidChangeCursorPosition(handleCursorChange) @@ -81,25 +114,25 @@ const Query = (props: Props) => { const handleCursorChange = () => { const { editor } = monacoObjects.current || {} - suggestionsRef.current = [] + suggestionsRef.current.data = [] if (!editor) return - if (!editor.getSelection()?.isEmpty()) { - setTimeout(() => editor?.trigger('', 'hideSuggestWidget', null), 0) + editor?.trigger('', 'hideSuggestWidget', null) return } suggestionsRef.current = getSuggestions(editor) - - if (suggestionsRef.current?.length) { + if (suggestionsRef.current.data.length) { triggerSuggestions() helpWidgetRef.current.isOpen = false return } - setTimeout(() => editor?.trigger('', 'hideSuggestWidget', null), 0) - editor?.trigger('', 'editor.action.triggerParameterHints', '') + if (suggestionsRef.current.forceHide) { + setTimeout(() => editor?.trigger('', 'hideSuggestWidget', null), 0) + editor?.trigger('', 'editor.action.triggerParameterHints', '') + } } const triggerSuggestions = () => { @@ -111,11 +144,14 @@ const Query = (props: Props) => { const getSuggestions = ( editor: monacoEditor.editor.IStandaloneCodeEditor - ): monacoEditor.languages.CompletionItem[] => { + ): { + forceHide: boolean + data: monacoEditor.languages.CompletionItem[] + } => { const position = editor.getPosition() const model = editor.getModel() - if (!position || !model) return [] + if (!position || !model) return asSuggestionsRef([]) const value = editor.getValue() const offset = model.getOffsetAt(position) @@ -123,6 +159,7 @@ const Query = (props: Props) => { const range = getRange(position, word) const { args, isCursorInArg, prevCursorChar, nextCursorChar } = splitQueryByArgs(value, offset) + const allArgs = args.flat() const [beforeOffsetArgs] = args const [firstArg, ...prevArgs] = beforeOffsetArgs @@ -130,33 +167,32 @@ const Query = (props: Props) => { const command = REDIS_COMMANDS_SPEC[commandName] as unknown as SearchCommand if (!command && position.lineNumber === 1 && word.startColumn === 1) { - return SUPPORTED_COMMANDS.map((command) => buildSuggestion(command, range, { - detail: generateDetail(command), - documentation: { - value: getCommandMarkdown(command as any) - }, - })) + return getCommandsSuggestions(SUPPORTED_COMMANDS, range) } - if (!command) return [] + if (!command) return asSuggestionsRef([]) + + setSelectedIndex(allArgs[1] || '') // cover query - if (command?.arguments?.[prevArgs.length]?.name === 'query') { + if (command?.arguments?.[prevArgs.length]?.name === DefinedArgumentName.query) { if (prevCursorChar === '@') { - return getFieldsSuggestions(['field1', 'field2'], range) + return asSuggestionsRef(getFieldsSuggestions(attributesRef.current, range), false) } - return [] + return asSuggestionsRef([]) } - if (isCursorInArg || nextCursorChar?.trim()) return [] + if (isCursorInArg || nextCursorChar?.trim()) return asSuggestionsRef([]) // cover index field - if (command?.arguments?.[prevArgs.length]?.name === 'index') { - return getIndexesSuggestions(indexesRef.current, range) + if (command?.arguments?.[prevArgs.length]?.name === DefinedArgumentName.index) { + if (prevCursorChar?.trim()) return asSuggestionsRef([], false) + return asSuggestionsRef(getIndexesSuggestions(indexesRef.current, range)) } - if (prevArgs.length < 2) return [] + if (prevArgs.length < 2) return asSuggestionsRef([]) + const foundArg = findCurrentArgument(command?.arguments || [], prevArgs) helpWidgetRef.current = { @@ -165,44 +201,9 @@ const Query = (props: Props) => { currentArg: foundArg?.stopArg } - // suggest arguments of argument - if (foundArg && !foundArg.isComplete) { - if (foundArg.stopArg?.name === 'field') { - return getFieldsSuggestions(['field1', 'field2'], range, true) - } - - if (foundArg.isBlocked) return [] - if (foundArg.append?.length) { - return foundArg.append.map((arg: any) => buildSuggestion(arg, range, { - kind: monacoEditor.languages.CompletionItemKind.Property, - detail: generateDetail(foundArg?.parent) - })) - } - - return [] - } - - if (!foundArg || foundArg.isComplete) { - const appendCommands = foundArg?.append ?? [] - - return [ - ...appendCommands.map((arg: any) => buildSuggestion(arg, range, { - sortText: 'a', - kind: monacoEditor.languages.CompletionItemKind.Property, - detail: generateDetail(foundArg?.parent) - })), - ...(command?.arguments || []) - .filter((arg) => arg.optional) - .filter((arg) => arg.multiple || !args.flat().includes(arg.token || arg.arguments?.[0]?.token || '')) - .map((arg: any) => buildSuggestion(arg, range, { - sortText: 'b', - kind: monacoEditor.languages.CompletionItemKind.Reference, - detail: generateDetail(arg) - })) - ] - } - - return [] + if (foundArg && !foundArg.isComplete) return getMandatoryArgumentSuggestions(foundArg, attributesRef.current, range) + if (!foundArg || foundArg.isComplete) return getOptionalSuggestions(command, foundArg, allArgs, range) + return asSuggestionsRef([]) } return ( diff --git a/redisinsight/ui/src/pages/search/components/query/constants.ts b/redisinsight/ui/src/pages/search/components/query/constants.ts index 120ea9f891..15556977d6 100644 --- a/redisinsight/ui/src/pages/search/components/query/constants.ts +++ b/redisinsight/ui/src/pages/search/components/query/constants.ts @@ -10,3 +10,9 @@ export const options = merge(defaultMonacoOptions, }) export const SUPPORTED_COMMANDS_LIST = ['FT.SEARCH', 'FT.AGGREGATE'] + +export enum DefinedArgumentName { + index = 'index', + query = 'query', + field = 'field', +} diff --git a/redisinsight/ui/src/pages/search/components/query/utils.ts b/redisinsight/ui/src/pages/search/components/query/utils.ts new file mode 100644 index 0000000000..24537cdb32 --- /dev/null +++ b/redisinsight/ui/src/pages/search/components/query/utils.ts @@ -0,0 +1,88 @@ +import { monaco } from 'react-monaco-editor' +import * as monacoEditor from 'monaco-editor' +import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' +import { bufferToString, formatLongName, getCommandMarkdown, Nullable } from 'uiSrc/utils' +import { buildSuggestion, generateDetail } from 'uiSrc/pages/search/utils' +import { FoundCommandArgument, SearchCommand } from 'uiSrc/pages/search/types' +import { DefinedArgumentName } from 'uiSrc/pages/search/components/query/constants' + +export const asSuggestionsRef = (suggestions: monacoEditor.languages.CompletionItem[], forceHide = true) => ({ + data: suggestions, + forceHide +}) + +export const getIndexesSuggestions = (indexes: RedisResponseBuffer[], range: monaco.IRange) => + indexes.map((index) => { + const value = formatLongName(bufferToString(index)) + + return { + label: value || ' ', + kind: monacoEditor.languages.CompletionItemKind.Snippet, + insertText: `"${value}" "$1" `, + insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, + range, + } + }) + +export const getFieldsSuggestions = (fields: any[], range: monaco.IRange, spaceAfter = false) => + fields.map(({ attribute }) => ({ + label: attribute, + kind: monacoEditor.languages.CompletionItemKind.Reference, + insertText: `${attribute}${spaceAfter ? ' ' : ''}`, + insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, + range, + })) + +export const getCommandsSuggestions = (commands: SearchCommand[], range: monaco.IRange) => asSuggestionsRef( + commands.map((command) => buildSuggestion(command, range, { + detail: generateDetail(command), + documentation: { + value: getCommandMarkdown(command as any) + }, + })) +) + +export const getMandatoryArgumentSuggestions = ( + foundArg: FoundCommandArgument, + fields: any[], + range: monaco.IRange +) => { + if (foundArg.stopArg?.name === DefinedArgumentName.field) { + return asSuggestionsRef(getFieldsSuggestions(fields, range, true)) + } + + if (foundArg.isBlocked) return asSuggestionsRef([]) + if (foundArg.append?.length) { + return asSuggestionsRef(foundArg.append.map((arg: any) => buildSuggestion(arg, range, { + kind: monacoEditor.languages.CompletionItemKind.Property, + detail: generateDetail(foundArg?.parent) + }))) + } + + return asSuggestionsRef([]) +} + +export const getOptionalSuggestions = ( + command: SearchCommand, + foundArg: Nullable, + allArgs: string[], + range: monaco.IRange +) => { + const appendCommands = foundArg?.append ?? [] + + return asSuggestionsRef([ + ...appendCommands.map((arg) => buildSuggestion(arg, range, { + sortText: 'a', + kind: monacoEditor.languages.CompletionItemKind.Property, + detail: generateDetail(foundArg?.parent) + })), + ...(command?.arguments || []) + .filter((arg) => arg.optional) + .filter((arg) => arg.multiple || !allArgs.includes(arg.token || arg.arguments?.[0]?.token || '')) + .map((arg) => buildSuggestion(arg, range, { + sortText: 'b', + kind: monacoEditor.languages.CompletionItemKind.Reference, + detail: generateDetail(arg) + })) + ]) +} diff --git a/redisinsight/ui/src/pages/search/types.ts b/redisinsight/ui/src/pages/search/types.ts index d1552818de..d4973f1c0b 100644 --- a/redisinsight/ui/src/pages/search/types.ts +++ b/redisinsight/ui/src/pages/search/types.ts @@ -1,3 +1,5 @@ +import { Maybe } from 'uiSrc/utils' + export enum TokenType { PureToken = 'pure-token', Block = 'block', @@ -20,3 +22,11 @@ export interface SearchCommand { export interface SearchCommandTree extends SearchCommand { parent?: SearchCommandTree } + +export interface FoundCommandArgument { + isComplete: boolean + stopArg: Maybe + isBlocked: boolean + append: Maybe + parent: Maybe +} diff --git a/redisinsight/ui/src/pages/search/utils/monaco.ts b/redisinsight/ui/src/pages/search/utils/monaco.ts index 924ea9ba1d..41712e9568 100644 --- a/redisinsight/ui/src/pages/search/utils/monaco.ts +++ b/redisinsight/ui/src/pages/search/utils/monaco.ts @@ -3,8 +3,20 @@ import * as monacoEditor from 'monaco-editor' import { isString } from 'lodash' import { generateDetail } from 'uiSrc/pages/search/utils/query' import { SearchCommand, TokenType } from 'uiSrc/pages/search/types' -import { bufferToString, formatLongName, Maybe } from 'uiSrc/utils' -import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' +import { Maybe } from 'uiSrc/utils' + +export const setCursorPositionAtTheEnd = (editor: monacoEditor.editor.IStandaloneCodeEditor) => { + if (!editor) return + + const rows = editor.getValue().split('\n') + + editor.setPosition({ + column: rows[rows.length - 1].trimEnd().length + 1, + lineNumber: rows.length + }) + + editor.focus() +} export const getRange = (position: monaco.Position, word: monaco.editor.IWordAtPosition): monaco.IRange => ({ startLineNumber: position.lineNumber, @@ -22,28 +34,6 @@ export const buildSuggestion = (arg: SearchCommand, range: monaco.IRange, option ...options, }) -export const getIndexesSuggestions = (indexes: RedisResponseBuffer[], range: monaco.IRange) => - indexes.map((index) => { - const value = formatLongName(bufferToString(index)) - - return { - label: value || ' ', - kind: monacoEditor.languages.CompletionItemKind.Snippet, - insertText: `"${value}" "$1" `, - insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, - range, - } - }) - -export const getFieldsSuggestions = (fields: string[], range: monaco.IRange, spaceAfter = false) => - fields.map((field) => ({ - label: field, - kind: monacoEditor.languages.CompletionItemKind.Reference, - insertText: `${field}${spaceAfter ? ' ' : ''}`, - insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, - range, - })) - export const getRediSearchSignutureProvider = (options: Maybe<{ isOpen: boolean currentArg: SearchCommand diff --git a/redisinsight/ui/src/pages/search/utils/query.ts b/redisinsight/ui/src/pages/search/utils/query.ts index 58286b67b8..0a5e269068 100644 --- a/redisinsight/ui/src/pages/search/utils/query.ts +++ b/redisinsight/ui/src/pages/search/utils/query.ts @@ -3,7 +3,7 @@ import { toNumber } from 'lodash' import { generateArgsNames, Maybe, Nullable } from 'uiSrc/utils' import { CommandProvider } from 'uiSrc/constants' -import { ArgName, SearchCommand, SearchCommandTree, TokenType } from '../types' +import { ArgName, FoundCommandArgument, SearchCommand, SearchCommandTree, TokenType } from '../types' export const splitQueryByArgs = (query: string, position: number = 0) => { const args: [string[], string[]] = [[], []] @@ -78,13 +78,7 @@ export const findCurrentArgument = ( args: SearchCommand[], prev: string[], parent?: SearchCommandTree -): Nullable<{ - isComplete: boolean - stopArg: Maybe, - isBlocked: boolean, - append: Maybe[], - parent: Maybe -}> => { +): Nullable => { for (let i = prev.length - 1; i >= 0; i--) { const arg = prev[i] const currentArg = findArgByToken(args, arg) @@ -228,7 +222,7 @@ export const getArgumentSuggestions = ( isComplete: boolean stopArg: Maybe, isBlocked: boolean, - append: Maybe[], + append: SearchCommand[], } => { const { restArguments, @@ -240,14 +234,19 @@ export const getArgumentSuggestions = ( const stopArgument = restArguments[stopArgIndex] const restNotFilledArgs = restArguments.slice(stopArgIndex) - const isBlocked = isWasBlocked || (stopArgument && !(stopArgument.token || stopArgument?.arguments?.length)) + const isStopArgumentCannotSuggest = Boolean(stopArgument && !(stopArgument.token || stopArgument?.arguments?.length)) + const isBlocked = isWasBlocked || isStopArgumentCannotSuggest + const restParentOptionalSuggestions = !stopArgument || stopArgument?.optional ? getRestParentArguments(current?.parent, current?.name, current?.multiple) - .filter((arg) => arg.optional) - .filter((arg) => arg.name !== current?.name) + .filter((arg) => + arg.optional && arg.name !== stopArgument?.name + && (current?.multiple || arg.name !== current?.name)) : [] - const restOptionalSuggestions = fillArgsByType([...restNotFilledArgs, ...restParentOptionalSuggestions]) + const restOptionalSuggestions = isBlocked + ? [] + : fillArgsByType([...restNotFilledArgs, ...restParentOptionalSuggestions]) const isOneOfArgument = stopArgument?.type === TokenType.OneOf || (stopArgument?.type === TokenType.PureToken && current?.parent?.type === TokenType.OneOf) const isArgSuggestions = stopArgument && !stopArgument.optional && (stopArgument?.token || isOneOfArgument) @@ -255,16 +254,14 @@ export const getArgumentSuggestions = ( const suggestions = isArgSuggestions // only 1 suggestion since next arg is required ? [isOneOfArgument ? stopArgument.arguments : stopArgument].flat() - : !isBlocked - ? restOptionalSuggestions - : [] + : restOptionalSuggestions const requiredArgsLength = restNotFilledArgs.filter((arg) => !arg.optional).length return { isComplete: requiredArgsLength === 0 && !isBlocked, stopArg: stopArgument, - isBlocked: isBlocked && !isOneOfArgument, + isBlocked, append: suggestions, } } diff --git a/redisinsight/ui/src/pages/search/utils/tests/mocks.ts b/redisinsight/ui/src/pages/search/utils/tests/mocks.ts new file mode 100644 index 0000000000..8125afa84c --- /dev/null +++ b/redisinsight/ui/src/pages/search/utils/tests/mocks.ts @@ -0,0 +1,676 @@ +export const MOCKED_SUPPORTED_COMMANDS = { + 'FT.SEARCH': { + summary: 'Searches the index with a textual query, returning either documents or just ids', + complexity: 'O(N)', + history: [ + [ + '2.0.0', + 'Deprecated `WITHPAYLOADS` and `PAYLOAD` arguments' + ] + ], + arguments: [ + { + name: 'index', + type: 'string' + }, + { + name: 'query', + type: 'string' + }, + { + name: 'nocontent', + type: 'pure-token', + token: 'NOCONTENT', + optional: true + }, + { + name: 'verbatim', + type: 'pure-token', + token: 'VERBATIM', + optional: true + }, + { + name: 'nostopwords', + type: 'pure-token', + token: 'NOSTOPWORDS', + optional: true + }, + { + name: 'withscores', + type: 'pure-token', + token: 'WITHSCORES', + optional: true + }, + { + name: 'withpayloads', + type: 'pure-token', + token: 'WITHPAYLOADS', + optional: true + }, + { + name: 'withsortkeys', + type: 'pure-token', + token: 'WITHSORTKEYS', + optional: true + }, + { + name: 'filter', + type: 'block', + optional: true, + multiple: true, + arguments: [ + { + name: 'numeric_field', + type: 'string', + token: 'FILTER' + }, + { + name: 'min', + type: 'double' + }, + { + name: 'max', + type: 'double' + } + ] + }, + { + name: 'geo_filter', + type: 'block', + optional: true, + multiple: true, + arguments: [ + { + name: 'geo_field', + type: 'string', + token: 'GEOFILTER' + }, + { + name: 'lon', + type: 'double' + }, + { + name: 'lat', + type: 'double' + }, + { + name: 'radius', + type: 'double' + }, + { + name: 'radius_type', + type: 'oneof', + arguments: [ + { + name: 'm', + type: 'pure-token', + token: 'm' + }, + { + name: 'km', + type: 'pure-token', + token: 'km' + }, + { + name: 'mi', + type: 'pure-token', + token: 'mi' + }, + { + name: 'ft', + type: 'pure-token', + token: 'ft' + } + ] + } + ] + }, + { + name: 'in_keys', + type: 'block', + optional: true, + arguments: [ + { + name: 'count', + type: 'string', + token: 'INKEYS' + }, + { + name: 'key', + type: 'string', + multiple: true + } + ] + }, + { + name: 'in_fields', + type: 'block', + optional: true, + arguments: [ + { + name: 'count', + type: 'string', + token: 'INFIELDS' + }, + { + name: 'field', + type: 'string', + multiple: true + } + ] + }, + { + name: 'return', + type: 'block', + optional: true, + arguments: [ + { + name: 'count', + type: 'string', + token: 'RETURN' + }, + { + name: 'identifiers', + type: 'block', + multiple: true, + arguments: [ + { + name: 'identifier', + type: 'string' + }, + { + name: 'property', + type: 'string', + token: 'AS', + optional: true + } + ] + } + ] + }, + { + name: 'summarize', + type: 'block', + optional: true, + arguments: [ + { + name: 'summarize', + type: 'pure-token', + token: 'SUMMARIZE' + }, + { + name: 'fields', + type: 'block', + optional: true, + arguments: [ + { + name: 'count', + type: 'string', + token: 'FIELDS' + }, + { + name: 'field', + type: 'string', + multiple: true + } + ] + }, + { + name: 'num', + type: 'integer', + token: 'FRAGS', + optional: true + }, + { + name: 'fragsize', + type: 'integer', + token: 'LEN', + optional: true + }, + { + name: 'separator', + type: 'string', + token: 'SEPARATOR', + optional: true + } + ] + }, + { + name: 'highlight', + type: 'block', + optional: true, + arguments: [ + { + name: 'highlight', + type: 'pure-token', + token: 'HIGHLIGHT' + }, + { + name: 'fields', + type: 'block', + optional: true, + arguments: [ + { + name: 'count', + type: 'string', + token: 'FIELDS' + }, + { + name: 'field', + type: 'string', + multiple: true + } + ] + }, + { + name: 'tags', + type: 'block', + optional: true, + arguments: [ + { + name: 'tags', + type: 'pure-token', + token: 'TAGS' + }, + { + name: 'open', + type: 'string' + }, + { + name: 'close', + type: 'string' + } + ] + } + ] + }, + { + name: 'slop', + type: 'integer', + optional: true, + token: 'SLOP' + }, + { + name: 'timeout', + type: 'integer', + optional: true, + token: 'TIMEOUT' + }, + { + name: 'inorder', + type: 'pure-token', + token: 'INORDER', + optional: true + }, + { + name: 'language', + type: 'string', + optional: true, + token: 'LANGUAGE' + }, + { + name: 'expander', + type: 'string', + optional: true, + token: 'EXPANDER' + }, + { + name: 'scorer', + type: 'string', + optional: true, + token: 'SCORER' + }, + { + name: 'explainscore', + type: 'pure-token', + token: 'EXPLAINSCORE', + optional: true + }, + { + name: 'payload', + type: 'string', + optional: true, + token: 'PAYLOAD' + }, + { + name: 'sortby', + type: 'block', + optional: true, + arguments: [ + { + name: 'sortby', + type: 'string', + token: 'SORTBY' + }, + { + name: 'order', + type: 'oneof', + optional: true, + arguments: [ + { + name: 'asc', + type: 'pure-token', + token: 'ASC' + }, + { + name: 'desc', + type: 'pure-token', + token: 'DESC' + } + ] + } + ] + }, + { + name: 'limit', + type: 'block', + optional: true, + arguments: [ + { + name: 'limit', + type: 'pure-token', + token: 'LIMIT' + }, + { + name: 'offset', + type: 'integer' + }, + { + name: 'num', + type: 'integer' + } + ] + }, + { + name: 'params', + type: 'block', + optional: true, + arguments: [ + { + name: 'params', + type: 'pure-token', + token: 'PARAMS' + }, + { + name: 'nargs', + type: 'integer' + }, + { + name: 'values', + type: 'block', + multiple: true, + arguments: [ + { + name: 'name', + type: 'string' + }, + { + name: 'value', + type: 'string' + } + ] + } + ] + }, + { + name: 'dialect', + type: 'integer', + optional: true, + token: 'DIALECT', + since: '2.4.3' + } + ], + since: '1.0.0', + group: 'search' + }, + + 'FT.AGGREGATE': { + summary: 'Run a search query on an index and perform aggregate transformations on the results', + complexity: 'O(1)', + arguments: [ + { + name: 'index', + type: 'string' + }, + { + name: 'query', + type: 'string' + }, + { + name: 'verbatim', + type: 'pure-token', + token: 'VERBATIM', + optional: true + }, + { + name: 'load', + type: 'block', + optional: true, + arguments: [ + { + name: 'count', + type: 'string', + token: 'LOAD' + }, + { + name: 'field', + type: 'string', + multiple: true + } + ] + }, + { + name: 'timeout', + type: 'integer', + optional: true, + token: 'TIMEOUT' + }, + { + name: 'loadall', + type: 'pure-token', + token: 'LOAD *', + optional: true + }, + { + name: 'groupby', + type: 'block', + optional: true, + multiple: true, + arguments: [ + { + name: 'nargs', + type: 'integer', + token: 'GROUPBY' + }, + { + name: 'property', + type: 'string', + multiple: true + }, + { + name: 'reduce', + type: 'block', + optional: true, + multiple: true, + arguments: [ + { + name: 'function', + type: 'string', + token: 'REDUCE' + }, + { + name: 'nargs', + type: 'integer' + }, + { + name: 'arg', + type: 'string', + multiple: true + }, + { + name: 'name', + type: 'string', + token: 'AS', + optional: true + } + ] + } + ] + }, + { + name: 'sortby', + type: 'block', + optional: true, + arguments: [ + { + name: 'nargs', + type: 'integer', + token: 'SORTBY' + }, + { + name: 'fields', + type: 'block', + optional: true, + multiple: true, + arguments: [ + { + name: 'property', + type: 'string' + }, + { + name: 'order', + type: 'oneof', + arguments: [ + { + name: 'asc', + type: 'pure-token', + token: 'ASC' + }, + { + name: 'desc', + type: 'pure-token', + token: 'DESC' + } + ] + } + ] + }, + { + name: 'num', + type: 'integer', + token: 'MAX', + optional: true + } + ] + }, + { + name: 'apply', + type: 'block', + optional: true, + multiple: true, + arguments: [ + { + name: 'expression', + type: 'string', + token: 'APPLY' + }, + { + name: 'name', + type: 'string', + token: 'AS' + } + ] + }, + { + name: 'limit', + type: 'block', + optional: true, + arguments: [ + { + name: 'limit', + type: 'pure-token', + token: 'LIMIT' + }, + { + name: 'offset', + type: 'integer' + }, + { + name: 'num', + type: 'integer' + } + ] + }, + { + name: 'filter', + type: 'string', + optional: true, + token: 'FILTER' + }, + { + name: 'cursor', + type: 'block', + optional: true, + arguments: [ + { + name: 'withcursor', + type: 'pure-token', + token: 'WITHCURSOR' + }, + { + name: 'read_size', + type: 'integer', + optional: true, + token: 'COUNT' + }, + { + name: 'idle_time', + type: 'integer', + optional: true, + token: 'MAXIDLE' + } + ] + }, + { + name: 'params', + type: 'block', + optional: true, + arguments: [ + { + name: 'params', + type: 'pure-token', + token: 'PARAMS' + }, + { + name: 'nargs', + type: 'integer' + }, + { + name: 'values', + type: 'block', + multiple: true, + arguments: [ + { + name: 'name', + type: 'string' + }, + { + name: 'value', + type: 'string' + } + ] + } + ] + }, + { + name: 'dialect', + type: 'integer', + optional: true, + token: 'DIALECT', + since: '2.4.3' + } + ], + since: '1.1.0', + group: 'search' + } +} diff --git a/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts b/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts new file mode 100644 index 0000000000..93bd7ea54e --- /dev/null +++ b/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts @@ -0,0 +1,351 @@ +import { findCurrentArgument } from 'uiSrc/pages/search/utils' +import { SearchCommand } from 'uiSrc/pages/search/types' +import { MOCKED_SUPPORTED_COMMANDS } from './mocks' + +const ftSearchCommand = MOCKED_SUPPORTED_COMMANDS['FT.SEARCH'] +const ftAggregateCommand = MOCKED_SUPPORTED_COMMANDS['FT.AGGREGATE'] + +const ftAggreageTests = [ + { args: [''], result: null }, + { args: ['', ''], result: null }, + { + args: ['index', '"query"', 'APPLY'], + result: { + stopArg: { name: 'expression', token: 'APPLY', type: 'string' }, + append: [{ name: 'expression', token: 'APPLY', type: 'string' }], + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'APPLY', 'expression'], + result: { + stopArg: { name: 'name', token: 'AS', type: 'string' }, + append: expect.any(Array), + isBlocked: false, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'APPLY', 'expression', 'AS'], + result: { + stopArg: { name: 'name', token: 'AS', type: 'string' }, + append: expect.any(Array), + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'APPLY', 'expression', 'AS', 'name'], + result: { + stopArg: undefined, + append: expect.any(Array), + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['""', '""', 'GROUPBY', '2', 'p1', 'p2', 'REDUCE', 'f'], + result: { + stopArg: { name: 'nargs', type: 'integer' }, + append: [], + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['""', '""', 'GROUPBY', '2', 'p1', 'p2', 'REDUCE', 'f', '0'], + result: { + stopArg: { + name: 'name', + type: 'string', + token: 'AS', + optional: true + }, + append: [ + { + name: 'name', + type: 'string', + token: 'AS', + optional: true + }, + { + name: 'function', + type: 'string', + token: 'REDUCE' + } + ], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'SORTBY'], + result: { + stopArg: { name: 'nargs', token: 'SORTBY', type: 'integer' }, + append: expect.any(Array), + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'SORTBY', '1', 'p1'], + result: { + stopArg: { + name: 'order', + type: 'oneof', + arguments: [ + { + name: 'asc', + type: 'pure-token', + token: 'ASC' + }, + { + name: 'desc', + type: 'pure-token', + token: 'DESC' + } + ] + }, + append: [ + { + name: 'asc', + type: 'pure-token', + token: 'ASC' + }, + { + name: 'desc', + type: 'pure-token', + token: 'DESC' + } + ], + isBlocked: false, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'SORTBY', '1', 'p1', 'ASC'], + result: { + stopArg: { + name: 'num', + type: 'integer', + token: 'MAX', + optional: true + }, + append: [ + { + name: 'num', + type: 'integer', + token: 'MAX', + optional: true + } + ], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'SORTBY', '0'], + result: { + stopArg: { + name: 'num', + type: 'integer', + token: 'MAX', + optional: true + }, + append: [ + { + name: 'num', + type: 'integer', + token: 'MAX', + optional: true + } + ], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'SORTBY', '1', 'p1', 'ASC', 'MAX'], + result: { + stopArg: { + name: 'num', + type: 'integer', + token: 'MAX', + optional: true + }, + append: [], + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + } + }, +] + +const ftSearchTests = [ + { args: [''], result: null }, + { args: ['', ''], result: null }, + { + args: ['', '', 'SUMMARIZE'], + result: { + stopArg: { + name: 'fields', + type: 'block', + optional: true, + arguments: [ + { + name: 'count', + type: 'string', + token: 'FIELDS' + }, + { + name: 'field', + type: 'string', + multiple: true + } + ] + }, + append: [ + { + name: 'count', + type: 'string', + token: 'FIELDS' + }, + { + name: 'num', + type: 'integer', + token: 'FRAGS', + optional: true + }, + { + name: 'fragsize', + type: 'integer', + token: 'LEN', + optional: true + }, + { + name: 'separator', + type: 'string', + token: 'SEPARATOR', + optional: true + } + ], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'SUMMARIZE', 'FIELDS'], + result: { + stopArg: { + name: 'count', + type: 'string', + token: 'FIELDS' + }, + append: [ + { + name: 'count', + type: 'string', + token: 'FIELDS' + } + ], + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'SUMMARIZE', 'FIELDS', '1'], + result: { + stopArg: { + name: 'field', + type: 'string', + multiple: true + }, + append: [], + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'SUMMARIZE', 'FIELDS', '1', 'f', 'FRAGS'], + result: { + stopArg: { + name: 'num', + type: 'integer', + token: 'FRAGS', + optional: true + }, + append: [], + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'SUMMARIZE', 'FIELDS', '1', 'f', 'FRAGS', '10'], + result: { + stopArg: { + name: 'fragsize', + type: 'integer', + token: 'LEN', + optional: true + }, + append: [ + { + name: 'fragsize', + type: 'integer', + token: 'LEN', + optional: true + }, + { + name: 'separator', + type: 'string', + token: 'SEPARATOR', + optional: true + } + ], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, +] + +describe('findCurrentArgument', () => { + describe('FT.AGGREGATE', () => { + ftAggreageTests.forEach(({ args, result: testResult }) => { + it(`should return proper suggestions for ${args.join(' ')}`, () => { + const result = findCurrentArgument( + ftAggregateCommand.arguments as SearchCommand[], + args + ) + expect(result).toEqual(testResult) + }) + }) + }) + + describe('FT.SEARCH', () => { + ftSearchTests.forEach(({ args, result: testResult }) => { + it(`should return proper suggestions for ${args.join(' ')}`, () => { + const result = findCurrentArgument( + ftSearchCommand.arguments as SearchCommand[], + args + ) + expect(result).toEqual(testResult) + }) + }) + }) +}) diff --git a/redisinsight/ui/src/slices/browser/redisearch.ts b/redisinsight/ui/src/slices/browser/redisearch.ts index 911497e8c6..10a3cb46be 100644 --- a/redisinsight/ui/src/slices/browser/redisearch.ts +++ b/redisinsight/ui/src/slices/browser/redisearch.ts @@ -532,3 +532,30 @@ export function deleteRedisearchHistoryAction( } } } + +export function fetchRedisearchInfoAction( + index: string, + onSuccess?: (value: RedisResponseBuffer[]) => void, + onFailed?: () => void, +) { + return async (_: AppDispatch, stateInit: () => RootState) => { + try { + const state = stateInit() + const { data, status } = await apiService.post( + getUrl( + state.connections.instances.connectedInstance?.id, + ApiEndpoints.REDISEARCH_INFO + ), + { + index + } + ) + + if (isStatusSuccessful(status)) { + onSuccess?.(data) + } + } catch (_err) { + onFailed?.() + } + } +} diff --git a/redisinsight/ui/src/slices/tests/browser/redisearch.spec.ts b/redisinsight/ui/src/slices/tests/browser/redisearch.spec.ts index 8df3a44642..295f812c9a 100644 --- a/redisinsight/ui/src/slices/tests/browser/redisearch.spec.ts +++ b/redisinsight/ui/src/slices/tests/browser/redisearch.spec.ts @@ -47,7 +47,7 @@ import reducer, { deleteRediSearchHistorySuccess, deleteRediSearchHistoryFailure, fetchRedisearchHistoryAction, - deleteRedisearchHistoryAction, + deleteRedisearchHistoryAction, fetchRedisearchInfoAction, } from '../../browser/redisearch' let store: typeof mockedStore @@ -1276,5 +1276,40 @@ describe('redisearch slice', () => { expect(store.getActions()).toEqual(expectedActions) }) }) + + describe('fetchRedisearchInfoAction', () => { + it('success fetch info', async () => { + // Arrange + const responsePayload = { status: 200 } + const onSuccess = jest.fn() + + apiService.post = jest.fn().mockResolvedValue(responsePayload) + + // Act + await store.dispatch(fetchRedisearchInfoAction('index', onSuccess)) + + expect(onSuccess).toBeCalled() + }) + + it('failed to delete history', async () => { + // Arrange + const onFailed = jest.fn() + const errorMessage = 'some error' + const responsePayload = { + response: { + status: 500, + data: { message: errorMessage }, + }, + } + + apiService.post = jest.fn().mockRejectedValue(responsePayload) + + // Act + await store.dispatch(fetchRedisearchInfoAction('index', undefined, onFailed)) + + // Assert + expect(onFailed).toBeCalled() + }) + }) }) }) From 4b0719a1537991870020fee37d2e8cf0daaf617a Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Fri, 9 Aug 2024 16:34:28 +0200 Subject: [PATCH 024/112] e2e/feature/RI-5957_integrate-autocomplete-in-search-and-query --- tests/e2e/package.json | 2 +- .../e2e/pageObjects/base-run-commands-page.ts | 2 + .../pageObjects/components/monaco-editor.ts | 1 + tests/e2e/pageObjects/workbench-page.ts | 1 - ...kbench.ts => cli-promote-workbench.e2e.ts} | 0 ...ster-7.ts => pub-sub-oss-cluster-7.e2e.ts} | 0 .../search-and-query-tab.e2e.ts | 176 ++++++++++++++++++ .../search-and-query/search-and-query-tab.ts | 27 --- tests/e2e/web.runner.ts | 4 +- 9 files changed, 182 insertions(+), 31 deletions(-) rename tests/e2e/tests/web/regression/cli/{cli-promote-workbench.ts => cli-promote-workbench.e2e.ts} (100%) rename tests/e2e/tests/web/regression/pub-sub/{pub-sub-oss-cluster-7.ts => pub-sub-oss-cluster-7.e2e.ts} (100%) create mode 100644 tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts delete mode 100644 tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.ts diff --git a/tests/e2e/package.json b/tests/e2e/package.json index 2f9490ab86..30f8986af4 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -13,7 +13,7 @@ "build:ui": "yarn --cwd ../../ build:ui", "redis:last": "docker run --name redis-last-version -p 7777:6379 -d redislabs/redismod", "start:app": "cross-env yarn start:api", - "test:chrome": "testcafe --compiler-options typescript.configPath=tsconfig.testcafe.json --cache --allow-insecure-localhost --disable-multiple-windows --concurrency 1 chrome tests/ -r html:./report/report.html,spec -e -s takeOnFails=true,path=report/screenshots/,pathPattern=${OS}_${BROWSER}/${DATE}_${TIME}/${FIXTURE}_${TEST}_${FILE_INDEX}.png", + "test:chrome": "testcafe --compiler-options typescript.configPath=tsconfig.testcafe.json --cache --disable-multiple-windows --disable-search-engine-choice-screen --concurrency 1 chrome tests/ -r html:./report/report.html,spec -e -s takeOnFails=true,path=report/screenshots/,pathPattern=${OS}_${BROWSER}/${DATE}_${TIME}/${FIXTURE}_${TEST}_${FILE_INDEX}.png", "test:chrome:ci": "ts-node ./web.runner.ts", "test": "yarn test:chrome", "lint": "eslint . --ext .ts,.js,.tsx,.jsx", diff --git a/tests/e2e/pageObjects/base-run-commands-page.ts b/tests/e2e/pageObjects/base-run-commands-page.ts index cf7949b393..885933dc38 100644 --- a/tests/e2e/pageObjects/base-run-commands-page.ts +++ b/tests/e2e/pageObjects/base-run-commands-page.ts @@ -5,6 +5,7 @@ export class BaseRunCommandsPage extends InstancePage { submitCommandButton = Selector('[data-testid=btn-submit]'); queryInput = Selector('[data-testid=query-input-container]'); + queryInputForText = Selector('[data-testid=query-input-container] .view-lines'); // History containers queryCardCommand = Selector('[data-testid=query-card-command]'); @@ -23,6 +24,7 @@ export class BaseRunCommandsPage extends InstancePage { executionCommandIcon = Selector('[data-testid=command-execution-time-icon]'); executedCommandTitle = Selector('[data-testid=query-card-tooltip-anchor]', { timeout: 500 }); queryResult = Selector('[data-testid=query-common-result]'); + queryInputScriptArea = Selector('[data-testid=query-input-container] .view-line'); cssQueryCardCommand = '[data-testid=query-card-command]'; cssQueryCardContainer = '[data-testid^="query-card-container-"]'; diff --git a/tests/e2e/pageObjects/components/monaco-editor.ts b/tests/e2e/pageObjects/components/monaco-editor.ts index 642920213f..007ebaf859 100644 --- a/tests/e2e/pageObjects/components/monaco-editor.ts +++ b/tests/e2e/pageObjects/components/monaco-editor.ts @@ -10,6 +10,7 @@ export class MonacoEditor { monacoHintWithArguments = Selector('[widgetid="editor.widget.parameterHintsWidget"]'); monacoCommandIndicator = Selector('div.monaco-glyph-run-command'); monacoWidget = Selector('[data-testid=monaco-widget]'); + monacoSuggestWidget = Selector('.suggest-widget'); nonRedisEditorResizeBottom = Selector('.t_resize-bottom'); nonRedisEditorResizeTop = Selector('.t_resize-top'); diff --git a/tests/e2e/pageObjects/workbench-page.ts b/tests/e2e/pageObjects/workbench-page.ts index 196cf20339..83e83e7640 100644 --- a/tests/e2e/pageObjects/workbench-page.ts +++ b/tests/e2e/pageObjects/workbench-page.ts @@ -58,7 +58,6 @@ export class WorkbenchPage extends BaseRunCommandsPage { mainEditorArea = Selector('[data-testid=main-input-container-area]'); queryTextResult = Selector(this.cssQueryTextResult); queryColumns = Selector('[data-testid*=query-column-]'); - queryInputScriptArea = Selector('[data-testid=query-input-container] .view-line'); noCommandHistorySection = Selector('[data-testid=wb_no-results]'); noCommandHistoryTitle = Selector('[data-testid=wb_no-results__title]'); noCommandHistoryText = Selector('[data-testid=wb_no-results__summary]'); diff --git a/tests/e2e/tests/web/regression/cli/cli-promote-workbench.ts b/tests/e2e/tests/web/regression/cli/cli-promote-workbench.e2e.ts similarity index 100% rename from tests/e2e/tests/web/regression/cli/cli-promote-workbench.ts rename to tests/e2e/tests/web/regression/cli/cli-promote-workbench.e2e.ts diff --git a/tests/e2e/tests/web/regression/pub-sub/pub-sub-oss-cluster-7.ts b/tests/e2e/tests/web/regression/pub-sub/pub-sub-oss-cluster-7.e2e.ts similarity index 100% rename from tests/e2e/tests/web/regression/pub-sub/pub-sub-oss-cluster-7.ts rename to tests/e2e/tests/web/regression/pub-sub/pub-sub-oss-cluster-7.e2e.ts diff --git a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts new file mode 100644 index 0000000000..a719287a6d --- /dev/null +++ b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts @@ -0,0 +1,176 @@ +import { Common, DatabaseHelper } from '../../../../helpers'; +import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; +import { BrowserPage } from '../../../../pageObjects'; +import { ExploreTabs, KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; +import { SearchAndQueryPage } from '../../../../pageObjects/search-and-query-page'; +import { APIKeyRequests } from '../../../../helpers/api/api-keys'; + +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); +const browserPage = new BrowserPage(); +const searchAndQueryPage = new SearchAndQueryPage(); +const apiKeyRequests = new APIKeyRequests(); + +const keyName = Common.generateWord(10); +let keyNames: string[]; +let indexName1: string; +let indexName2: string; + +fixture `Autocomplete for entered commands in search and query` + .meta({ type: 'regression', rte: rte.standalone }) + .page(commonUrl) + .beforeEach(async() => { + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); + indexName1 = `idx1:${keyName}`; + indexName2 = `idx2:${keyName}`; + keyNames = [`${keyName}:1`, `${keyName}:2`, `${keyName}:3`]; + const commands = [ + `HSET ${keyNames[0]} "name" "Hall School" "description" " Spanning 10 states" "class" "independent" "type" "traditional" "address_city" "London" "address_street" "Manor Street" "students" 342 "location" "51.445417, -0.258352"`, + `HSET ${keyNames[1]} "name" "Garden School" "description" "Garden School is a new outdoor" "class" "state" "type" "forest; montessori;" "address_city" "London" "address_street" "Gordon Street" "students" 1452 "location" "51.402926, -0.321523"`, + `HSET ${keyNames[2]} "name" "Gillford School" "description" "Gillford School is a centre" "class" "private" "type" "democratic; waldorf" "address_city" "Goudhurst" "address_street" "Goudhurst" "students" 721 "location" "51.112685, 0.451076"`, + `FT.CREATE ${indexName1} ON HASH PREFIX 1 "${keyName}:" SCHEMA name TEXT NOSTEM description TEXT class TAG type TAG SEPARATOR ";" address_city AS city TAG address_street AS address TEXT NOSTEM students NUMERIC SORTABLE location GEO`, + `FT.CREATE ${indexName2} ON HASH PREFIX 1 "${keyName}:" SCHEMA name TEXT NOSTEM description TEXT class TAG type TAG SEPARATOR ";" address_city AS city TAG address_street AS address TEXT NOSTEM students NUMERIC SORTABLE location GEO` + ]; + + // Create 3 keys and index + await browserPage.Cli.sendCommandsInCli(commands); + }) + .afterEach(async() => { + // Clear and delete database + await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName); + await browserPage.Cli.sendCommandsInCli([`DEL ${keyNames.join(' ')}`, `FT.DROPINDEX ${indexName1}`]); + await browserPage.Cli.sendCommandsInCli([`DEL ${keyNames.join(' ')}`, `FT.DROPINDEX ${indexName2}`]); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); + }); +test('Verify that tutorials can be opened from Workbench', async t => { + const search = await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); + await t.click(search.getTutorialLinkLocator('sq-exact-match')); + await t.expect(search.InsightsPanel.sidePanel.exists).ok('Insight panel is not opened'); + const tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials); + await t.expect(tab.preselectArea.textContent).contains('EXACT MATCH', 'the tutorial page is incorrect'); +}); +test('Verify full commands suggestions with index and query for FT.AGGREGATE', async t => { + const groupByArgInfo = 'GROUPBY nargs property [property ...] [REDUCE function nargs arg [arg ...] [AS name] [REDUCE function nargs arg [arg ...] [AS name] ...]]'; + const indexFields = [ + 'address', + 'city', + 'class', + 'description', + 'location', + 'name', + 'students', + 'type' + ]; + const commandDetails = [ + 'index query [VERBATIM] [LOAD count field [field ...]]', + 'Run a search query on an index and perform aggregate transformations on the results', + 'Arguments:', + 'required index', + 'required query', + 'optional [verbatim]' + ]; + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); + + // Verify basic commands suggestions FT.SEARCH and FT.AGGREGATE + await t.typeText(searchAndQueryPage.queryInput, 'FT', { replace: true }); + // Verify that the list with FT.SEARCH and FT.AGGREGATE auto-suggestions is displayed + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.count).eql(2, 'FT.SEARCH and FT.AGGREGATE auto-suggestions are not displayed'); + // Verify that user can use show more to see command fully in 2nd tooltip + await t.pressKey('ctrl+space'); + await t.expect(searchAndQueryPage.MonacoEditor.monacoCommandDetails.exists).ok('The "read more" about the command is not opened'); + for(const detail of commandDetails) { + await t.expect(searchAndQueryPage.MonacoEditor.monacoCommandDetails.textContent).contains(detail, `The ${detail} command detail is not displayed`); + } + // Verify that user can close show more tooltip by 'x' or 'Show less' + await t.pressKey('ctrl+space'); + await t.expect(searchAndQueryPage.MonacoEditor.monacoCommandDetails.exists).notOk('The "read more" about the command is not closed'); + // Select command and check result + await t.pressKey('enter'); + let script = await searchAndQueryPage.queryInputScriptArea.textContent; + await t.expect(script.replace(/\s/g, ' ')).contains('FT.AGGREGATE ', 'Result of sent command exists'); + + // Verify that user can see the list of all the indexes in database when put a space after only FT.SEARCH and FT.AGGREGATE commands + await t.expect(script.replace(/\s/g, ' ')).contains(`"${indexName1}" "" `, 'Index not suggested into input'); + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText(indexName1).exists).ok('Index not auto-suggested'); + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText(indexName2).exists).ok('All indexes not auto-suggested'); + + await t.pressKey('tab'); + await t.typeText(searchAndQueryPage.queryInput, '@', { replace: false }); + script = await searchAndQueryPage.queryInputScriptArea.textContent; + // Verify that user can see the list of fields from the index selected when type in “@” + await t.expect(script.replace(/\s/g, ' ')).contains('address', 'Index not suggested into input'); + for(const field of indexFields) { + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText(field).exists).ok(`${field} Index field not auto-suggested`); + } + // Verify that user can use autosuggestions by typing fields from index after "@" + await t.typeText(searchAndQueryPage.queryInput, 'c', { replace: false }); + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('city').exists).ok('Index field not auto-suggested after starting typing'); + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.count).eql(1, 'Wrong index fields suggested after typing first letter'); + + // Verify contextual suggestions after typing letters for commands + await t.pressKey('tab'); + await t.pressKey('right'); + await t.pressKey('space'); + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('APPLY').exists).ok('FT.AGGREGATE arguments not suggested'); + await t.typeText(searchAndQueryPage.queryInput, 'g', { replace: false }); + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.nth(0).textContent).contains('GROUPBY', 'Argument not suggested after typing first letters'); + + await t.pressKey('tab'); + // Verify that user can see widget about entered argument + await t.expect(searchAndQueryPage.MonacoEditor.monacoHintWithArguments.withText(groupByArgInfo).exists).ok('Widget with info about entered argument not displayed'); + + await t.typeText(searchAndQueryPage.queryInput, '1 "London"', { replace: false }); + await t.pressKey('space'); + // Verify correct order of suggested arguments like LOAD, GROUPBY, SORTBY + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.nth(0).textContent).contains('REDUCE', 'Incorrect order of suggested arguments'); + await t.pressKey('tab'); + await t.typeText(searchAndQueryPage.queryInput, 'SUM 1 @students', { replace: false }); + await t.pressKey('space'); + + // Verify expression and function suggestions like AS for APPLY/GROUPBY + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.nth(0).textContent).contains('AS', 'Incorrect order of suggested arguments'); + await t.pressKey('tab'); + await t.typeText(searchAndQueryPage.queryInput, 'stud', { replace: false }); + + await t.pressKey('space'); + // Verify multiple argument option suggestions + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.nth(0).textContent).contains('REDUCE', 'Incorrect order of suggested arguments'); + // Verify complex command sequences like nargs and properties are suggested accurately for GROUPBY + const expectedText = `FT.AGGREGATE "${indexName1}" "@city" GROUPBY 1 "London" REDUCE SUM 1 @students AS stud REDUCE`.trim().replace(/\s+/g, ' '); + await t.expect((await searchAndQueryPage.queryInputForText.innerText).trim().replace(/\s+/g, ' ')).contains(expectedText, 'Incorrect order of entered arguments'); +}); +test('Verify full commands suggestions with index and query for FT.SEARCH', async t => { + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); + await t.typeText(searchAndQueryPage.queryInput, 'FT', { replace: true }); + // Select command and check result + await t.pressKey('down'); + await t.pressKey('enter'); + const script = await searchAndQueryPage.queryInputScriptArea.textContent; + await t.expect(script.replace(/\s/g, ' ')).contains('FT.SEARCH ', 'Result of sent command exists'); + + await t.pressKey('tab'); + await t.typeText(searchAndQueryPage.queryInput, '@c', { replace: false }); + // Select '@city' field + await t.pressKey('down'); + await t.pressKey('tab'); + await t.pressKey('right'); + await t.pressKey('space'); + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('DIALECT').exists).ok('FT.SEARCH arguments not suggested'); + await t.typeText(searchAndQueryPage.queryInput, 'n', { replace: false }); + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.nth(0).textContent).contains('NOCONTENT', 'Argument not suggested after typing first letters'); + await t.pressKey('tab'); + // Verify that FT.SEARCH and FT.AGGREGATE non-multiple arguments are suggested only once + await t.pressKey('space'); + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withText('NOCONTENT').exists).notOk('Non-multiple arguments are suggested not only once'); + + // Verify that suggestions correct to closest valid commands or options for invalid typing like WRONGCOMMAND + await t.typeText(searchAndQueryPage.queryInput, 'WRONGCOMMAND', { replace: false }); + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('WITHSORTKEYS').exists).ok('Closest suggestions not displayed'); + + await t.pressKey('space'); + await t.pressKey('backspace'); + await t.pressKey('backspace'); + // Verify that 'No suggestions' tooltip is displayed when returning to invalid typing like WRONGCOMMAND + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestWidget.textContent).contains('No suggestions.', 'Index not auto-suggested'); +}); \ No newline at end of file diff --git a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.ts b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.ts deleted file mode 100644 index 9c5c4ec175..0000000000 --- a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DatabaseHelper } from '../../../../helpers'; -import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; -import { BrowserPage } from '../../../../pageObjects'; -import { ExploreTabs, KeysInteractionTabs, rte } from '../../../../helpers/constants'; -import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; - -const databaseHelper = new DatabaseHelper(); -const databaseAPIRequests = new DatabaseAPIRequests(); -const browserPage = new BrowserPage(); - -fixture `Autocomplete for entered commands` - .meta({ type: 'regression', rte: rte.standalone }) - .page(commonUrl) - .beforeEach(async t => { - await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); - }) - .afterEach(async() => { - // Delete database - await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); - }); -test('Verify that tutorials can be opened from Workbench', async t => { - const search = await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); - await t.click(search.getTutorialLinkLocator('sq-exact-match')); - await t.expect(search.InsightsPanel.sidePanel.exists).ok('Insight panel is not opened'); - const tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials); - await t.expect(tab.preselectArea.textContent).contains('EXACT MATCH', 'the tutorial page is incorrect'); -}); diff --git a/tests/e2e/web.runner.ts b/tests/e2e/web.runner.ts index e77ccc1030..8d0c83e7a9 100644 --- a/tests/e2e/web.runner.ts +++ b/tests/e2e/web.runner.ts @@ -11,7 +11,7 @@ import testcafe from 'testcafe'; experimentalDecorators: true } }) .src((process.env.TEST_FILES || 'tests/web/**/*.e2e.ts').split('\n')) - .browsers(['chromium:headless --cache --allow-insecure-localhost --ignore-certificate-errors']) + .browsers(['chromium:headless --cache --allow-insecure-localhost --disable-search-engine-choice-screen --ignore-certificate-errors']) .screenshots({ path: 'report/screenshots/', takeOnFails: true, @@ -38,7 +38,7 @@ import testcafe from 'testcafe'; selectorTimeout: 5000, assertionTimeout: 5000, speed: 1, - quarantineMode: { successThreshold: 1, attemptLimit: 3 }, + // quarantineMode: { successThreshold: 1, attemptLimit: 3 }, pageRequestTimeout: 8000, disableMultipleWindows: true }); From 3ffb4dda3a57b02af0bfb5cf66672ef5ee5f7514 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Fri, 9 Aug 2024 16:38:22 +0200 Subject: [PATCH 025/112] uncomment row for web runner --- tests/e2e/web.runner.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/web.runner.ts b/tests/e2e/web.runner.ts index 8d0c83e7a9..cb090db3ae 100644 --- a/tests/e2e/web.runner.ts +++ b/tests/e2e/web.runner.ts @@ -38,7 +38,7 @@ import testcafe from 'testcafe'; selectorTimeout: 5000, assertionTimeout: 5000, speed: 1, - // quarantineMode: { successThreshold: 1, attemptLimit: 3 }, + quarantineMode: { successThreshold: 1, attemptLimit: 3 }, pageRequestTimeout: 8000, disableMultipleWindows: true }); From a58ec5988d59dbefa1a91d6b2ee3f31f95611e52 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Mon, 12 Aug 2024 17:13:16 +0200 Subject: [PATCH 026/112] fix by pr comments and some updates --- tests/e2e/desktop.runner.ci.ts | 53 +++++++++++++++++++ tests/e2e/desktop.runner.ts | 1 - tests/e2e/package.json | 8 +-- .../e2e/pageObjects/base-run-commands-page.ts | 2 +- .../workbench/index-schema.e2e.ts | 2 +- .../regression/browser/key-messages.e2e.ts | 3 +- .../web/regression/browser/onboarding.e2e.ts | 2 + .../web/regression/browser/ttl-format.e2e.ts | 6 +-- .../search-and-query-tab.e2e.ts | 38 +++++++------ tests/e2e/web.runner.ci.ts | 53 +++++++++++++++++++ tests/e2e/web.runner.ts | 3 +- 11 files changed, 140 insertions(+), 31 deletions(-) create mode 100644 tests/e2e/desktop.runner.ci.ts create mode 100644 tests/e2e/web.runner.ci.ts diff --git a/tests/e2e/desktop.runner.ci.ts b/tests/e2e/desktop.runner.ci.ts new file mode 100644 index 0000000000..dfb4bcf3f2 --- /dev/null +++ b/tests/e2e/desktop.runner.ci.ts @@ -0,0 +1,53 @@ +import testcafe from 'testcafe'; + +(async(): Promise => { + await testcafe('localhost') + .then(t => { + return t + .createRunner() + .compilerOptions({ + 'typescript': { + configPath: 'tsconfig.testcafe.json', + experimentalDecorators: true + } }) + .src((process.env.TEST_FILES || 'tests/electron/**/*.e2e.ts').split('\n')) + .browsers(['electron']) + .screenshots({ + path: './report/screenshots/', + takeOnFails: true, + pathPattern: '${USERAGENT}/${DATE}_${TIME}/${FIXTURE}_${TEST}_${FILE_INDEX}.png' + }) + .reporter([ + 'spec', + { + name: 'xunit', + output: './results/results.xml' + }, + { + name: 'json', + output: './results/e2e.results.json' + }, + { + name: 'html', + output: './report/report.html' + } + ]) + .run({ + skipJsErrors: true, + browserInitTimeout: 60000, + selectorTimeout: 5000, + assertionTimeout: 5000, + speed: 1, + quarantineMode: { successThreshold: 1, attemptLimit: 3 }, + pageRequestTimeout: 8000, + disableMultipleWindows: true + }); + }) + .then((failedCount) => { + process.exit(failedCount); + }) + .catch((e) => { + console.error(e); + process.exit(1); + }); +})(); diff --git a/tests/e2e/desktop.runner.ts b/tests/e2e/desktop.runner.ts index dfb4bcf3f2..7afe4f0daf 100644 --- a/tests/e2e/desktop.runner.ts +++ b/tests/e2e/desktop.runner.ts @@ -38,7 +38,6 @@ import testcafe from 'testcafe'; selectorTimeout: 5000, assertionTimeout: 5000, speed: 1, - quarantineMode: { successThreshold: 1, attemptLimit: 3 }, pageRequestTimeout: 8000, disableMultipleWindows: true }); diff --git a/tests/e2e/package.json b/tests/e2e/package.json index 30f8986af4..8b6db4b9dc 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -13,13 +13,13 @@ "build:ui": "yarn --cwd ../../ build:ui", "redis:last": "docker run --name redis-last-version -p 7777:6379 -d redislabs/redismod", "start:app": "cross-env yarn start:api", - "test:chrome": "testcafe --compiler-options typescript.configPath=tsconfig.testcafe.json --cache --disable-multiple-windows --disable-search-engine-choice-screen --concurrency 1 chrome tests/ -r html:./report/report.html,spec -e -s takeOnFails=true,path=report/screenshots/,pathPattern=${OS}_${BROWSER}/${DATE}_${TIME}/${FIXTURE}_${TEST}_${FILE_INDEX}.png", - "test:chrome:ci": "ts-node ./web.runner.ts", + "test:chrome": "ts-node ./web.runner.ts", + "test:chrome:ci": "ts-node ./web.runner.ci.ts", "test": "yarn test:chrome", "lint": "eslint . --ext .ts,.js,.tsx,.jsx", - "test:desktop:ci": "ts-node ./desktop.runner.ts", + "test:desktop:ci": "ts-node ./desktop.runner.ci.ts", "test:desktop:ci:win": "ts-node ./desktop.runner.win.ts", - "test:desktop": "testcafe electron tests/ --compiler-options typescript.configPath=tsconfig.testcafe.json --browser-init-timeout 180000 -e -r html:./report/desktop-report.html,spec -s takeOnFails=true,path=report/screenshots/,pathPattern=${OS}_${BROWSER}/${DATE}_${TIME}/${FIXTURE}_${TEST}_${FILE_INDEX}.png" + "test:desktop": "ts-node ./desktop.runner.ts" }, "keywords": [], "author": "", diff --git a/tests/e2e/pageObjects/base-run-commands-page.ts b/tests/e2e/pageObjects/base-run-commands-page.ts index 885933dc38..b9c965ecfa 100644 --- a/tests/e2e/pageObjects/base-run-commands-page.ts +++ b/tests/e2e/pageObjects/base-run-commands-page.ts @@ -33,7 +33,7 @@ export class BaseRunCommandsPage extends InstancePage { cssDeleteCommandButton = '[data-testid=delete-command]'; getTutorialLinkLocator = (tutorialName: string): Selector => - Selector(`[@data-testid="data-testid=query-tutorials-link_${tutorialName}"]`); + Selector(`[data-testid=query-tutorials-link_${tutorialName}]`); /** * Get card container by command diff --git a/tests/e2e/tests/electron/critical-path/workbench/index-schema.e2e.ts b/tests/e2e/tests/electron/critical-path/workbench/index-schema.e2e.ts index 98e1f7f811..9f55e7f9f3 100644 --- a/tests/e2e/tests/electron/critical-path/workbench/index-schema.e2e.ts +++ b/tests/e2e/tests/electron/critical-path/workbench/index-schema.e2e.ts @@ -20,7 +20,7 @@ fixture `Index Schema at Workbench` await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); // Go to Workbench page await t.click(myRedisDatabasePage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench);; + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); }) .afterEach(async t => { // Drop index, documents and database diff --git a/tests/e2e/tests/web/regression/browser/key-messages.e2e.ts b/tests/e2e/tests/web/regression/browser/key-messages.e2e.ts index f000bc28af..867b8c6b86 100644 --- a/tests/e2e/tests/web/regression/browser/key-messages.e2e.ts +++ b/tests/e2e/tests/web/regression/browser/key-messages.e2e.ts @@ -1,4 +1,4 @@ -import { rte } from '../../../../helpers/constants'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; @@ -62,6 +62,7 @@ test('Verify that user can see link to Workbench under word “Workbench” in t // Add key and verify Workbench link await browserPage.Cli.sendCommandInCli(commands[i]); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.BrowserAndFilter); await browserPage.searchByKeyName(keyName); await t.click(browserPage.keyNameInTheList); await t.click(browserPage.internalLinkToWorkbench); diff --git a/tests/e2e/tests/web/regression/browser/onboarding.e2e.ts b/tests/e2e/tests/web/regression/browser/onboarding.e2e.ts index 80d3961e34..a38078a197 100644 --- a/tests/e2e/tests/web/regression/browser/onboarding.e2e.ts +++ b/tests/e2e/tests/web/regression/browser/onboarding.e2e.ts @@ -137,6 +137,7 @@ test('Verify onboard new user skip tour', async(t) => { await t.expect(myRedisDatabasePage.NavigationPanel.HelpCenter.helpCenterPanel.visible).ok('help center panel is not opened'); await t.click(onboardingCardsDialog.resetOnboardingBtn); await t.click(myRedisDatabasePage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.BrowserAndFilter); // Verify that when user reset onboarding, user can see the onboarding triggered when user open the Browser page. await t.expect(onboardingCardsDialog.showMeAroundButton.visible).ok('onboarding starting is not visible'); // click skip tour @@ -171,6 +172,7 @@ test.requestHooks(logger)('Verify that the final onboarding step is closed when await t.expect(onboardingCardsDialog.stepTitle.exists).notOk('Onboarding tooltip still visible'); // Go to Browser Page await t.click(myRedisDatabasePage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.BrowserAndFilter); // Verify onboarding completed successfully await onboardingCardsDialog.completeOnboarding(); await t.expect(browserPage.patternModeBtn.visible).ok('Browser page is not opened'); diff --git a/tests/e2e/tests/web/regression/browser/ttl-format.e2e.ts b/tests/e2e/tests/web/regression/browser/ttl-format.e2e.ts index ba39510384..8e91c20f0e 100644 --- a/tests/e2e/tests/web/regression/browser/ttl-format.e2e.ts +++ b/tests/e2e/tests/web/regression/browser/ttl-format.e2e.ts @@ -1,6 +1,6 @@ import { Selector } from 'testcafe'; import { DatabaseHelper } from '../../../../helpers/database'; -import { keyTypes } from '../../../../helpers/keys'; +import { deleteKeysViaCli, keyTypes } from '../../../../helpers/keys'; import { rte, COMMANDS_TO_CREATE_KEY, keyLength } from '../../../../helpers/constants'; import { BrowserPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; @@ -31,9 +31,7 @@ fixture `TTL values in Keys Table` }) .afterEach(async() => { // Clear and delete database - for (let i = 0; i < keysData.length; i++) { - await browserPage.deleteKey(); - } + await deleteKeysViaCli(keysData); await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can see TTL in the list of keys rounded down to the nearest unit', async t => { diff --git a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts index a719287a6d..ff3a339217 100644 --- a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts +++ b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts @@ -50,18 +50,7 @@ test('Verify that tutorials can be opened from Workbench', async t => { const tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials); await t.expect(tab.preselectArea.textContent).contains('EXACT MATCH', 'the tutorial page is incorrect'); }); -test('Verify full commands suggestions with index and query for FT.AGGREGATE', async t => { - const groupByArgInfo = 'GROUPBY nargs property [property ...] [REDUCE function nargs arg [arg ...] [AS name] [REDUCE function nargs arg [arg ...] [AS name] ...]]'; - const indexFields = [ - 'address', - 'city', - 'class', - 'description', - 'location', - 'name', - 'students', - 'type' - ]; +test('Verify that user can use show more to see command fully in 2nd tooltip', async t => { const commandDetails = [ 'index query [VERBATIM] [LOAD count field [field ...]]', 'Run a search query on an index and perform aggregate transformations on the results', @@ -71,11 +60,7 @@ test('Verify full commands suggestions with index and query for FT.AGGREGATE', a 'optional [verbatim]' ]; await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); - - // Verify basic commands suggestions FT.SEARCH and FT.AGGREGATE await t.typeText(searchAndQueryPage.queryInput, 'FT', { replace: true }); - // Verify that the list with FT.SEARCH and FT.AGGREGATE auto-suggestions is displayed - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.count).eql(2, 'FT.SEARCH and FT.AGGREGATE auto-suggestions are not displayed'); // Verify that user can use show more to see command fully in 2nd tooltip await t.pressKey('ctrl+space'); await t.expect(searchAndQueryPage.MonacoEditor.monacoCommandDetails.exists).ok('The "read more" about the command is not opened'); @@ -85,6 +70,25 @@ test('Verify full commands suggestions with index and query for FT.AGGREGATE', a // Verify that user can close show more tooltip by 'x' or 'Show less' await t.pressKey('ctrl+space'); await t.expect(searchAndQueryPage.MonacoEditor.monacoCommandDetails.exists).notOk('The "read more" about the command is not closed'); +}); +test('Verify full commands suggestions with index and query for FT.AGGREGATE', async t => { + const groupByArgInfo = 'GROUPBY nargs property [property ...] [REDUCE function nargs arg [arg ...] [AS name] [REDUCE function nargs arg [arg ...] [AS name] ...]]'; + const indexFields = [ + 'address', + 'city', + 'class', + 'description', + 'location', + 'name', + 'students', + 'type' + ]; + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); + + // Verify basic commands suggestions FT.SEARCH and FT.AGGREGATE + await t.typeText(searchAndQueryPage.queryInput, 'FT', { replace: true }); + // Verify that the list with FT.SEARCH and FT.AGGREGATE auto-suggestions is displayed + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.count).eql(2, 'FT.SEARCH and FT.AGGREGATE auto-suggestions are not displayed'); // Select command and check result await t.pressKey('enter'); let script = await searchAndQueryPage.queryInputScriptArea.textContent; @@ -173,4 +177,4 @@ test('Verify full commands suggestions with index and query for FT.SEARCH', asyn await t.pressKey('backspace'); // Verify that 'No suggestions' tooltip is displayed when returning to invalid typing like WRONGCOMMAND await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestWidget.textContent).contains('No suggestions.', 'Index not auto-suggested'); -}); \ No newline at end of file +}); diff --git a/tests/e2e/web.runner.ci.ts b/tests/e2e/web.runner.ci.ts new file mode 100644 index 0000000000..cb090db3ae --- /dev/null +++ b/tests/e2e/web.runner.ci.ts @@ -0,0 +1,53 @@ +import testcafe from 'testcafe'; + +(async(): Promise => { + await testcafe() + .then(t => { + return t + .createRunner() + .compilerOptions({ + 'typescript': { + configPath: 'tsconfig.testcafe.json', + experimentalDecorators: true + } }) + .src((process.env.TEST_FILES || 'tests/web/**/*.e2e.ts').split('\n')) + .browsers(['chromium:headless --cache --allow-insecure-localhost --disable-search-engine-choice-screen --ignore-certificate-errors']) + .screenshots({ + path: 'report/screenshots/', + takeOnFails: true, + pathPattern: '${OS}_${BROWSER}/${DATE}_${TIME}/${FIXTURE}_${TEST}_${FILE_INDEX}.png' + }) + .reporter([ + 'spec', + { + name: 'xunit', + output: './results/results.xml' + }, + { + name: 'json', + output: './results/e2e.results.json' + }, + { + name: 'html', + output: './report/report.html' + } + ]) + .run({ + skipJsErrors: true, + browserInitTimeout: 60000, + selectorTimeout: 5000, + assertionTimeout: 5000, + speed: 1, + quarantineMode: { successThreshold: 1, attemptLimit: 3 }, + pageRequestTimeout: 8000, + disableMultipleWindows: true + }); + }) + .then((failedCount) => { + process.exit(failedCount); + }) + .catch((e) => { + console.error(e); + process.exit(1); + }); +})(); diff --git a/tests/e2e/web.runner.ts b/tests/e2e/web.runner.ts index cb090db3ae..a30406f2ce 100644 --- a/tests/e2e/web.runner.ts +++ b/tests/e2e/web.runner.ts @@ -11,7 +11,7 @@ import testcafe from 'testcafe'; experimentalDecorators: true } }) .src((process.env.TEST_FILES || 'tests/web/**/*.e2e.ts').split('\n')) - .browsers(['chromium:headless --cache --allow-insecure-localhost --disable-search-engine-choice-screen --ignore-certificate-errors']) + .browsers(['chrome --cache --allow-insecure-localhost --disable-search-engine-choice-screen --ignore-certificate-errors']) .screenshots({ path: 'report/screenshots/', takeOnFails: true, @@ -38,7 +38,6 @@ import testcafe from 'testcafe'; selectorTimeout: 5000, assertionTimeout: 5000, speed: 1, - quarantineMode: { successThreshold: 1, attemptLimit: 3 }, pageRequestTimeout: 8000, disableMultipleWindows: true }); From be0974047c1b1f5a50c4295db1ae92d446fbfeb5 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Mon, 12 Aug 2024 20:46:37 +0200 Subject: [PATCH 027/112] fix for failed test --- .../web/critical-path/database-overview/database-index.e2e.ts | 2 +- .../tests/web/regression/insights/live-recommendations.e2e.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/e2e/tests/web/critical-path/database-overview/database-index.e2e.ts b/tests/e2e/tests/web/critical-path/database-overview/database-index.e2e.ts index c87bbcb487..59a7c860e3 100644 --- a/tests/e2e/tests/web/critical-path/database-overview/database-index.e2e.ts +++ b/tests/e2e/tests/web/critical-path/database-overview/database-index.e2e.ts @@ -111,7 +111,7 @@ test('Switching between indexed databases', async t => { await workbenchPage.checkWorkbenchCommandResult(`[db1] ${command}`, '8'); // Open Browser page - await t.click(myRedisDatabasePage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); // Clear filter await t.click(browserPage.clearFilterButton); // Verify that data changed for indexed db on Workbench page (on Search capability page) diff --git a/tests/e2e/tests/web/regression/insights/live-recommendations.e2e.ts b/tests/e2e/tests/web/regression/insights/live-recommendations.e2e.ts index 0a2b177e2b..30f2d00cfe 100644 --- a/tests/e2e/tests/web/regression/insights/live-recommendations.e2e.ts +++ b/tests/e2e/tests/web/regression/insights/live-recommendations.e2e.ts @@ -275,7 +275,6 @@ test await t.click(tab.analyzeDatabaseLink); await t.click(tab.analyzeTooltipButton); await t.click(memoryEfficiencyPage.recommendationsTab); - await memoryEfficiencyPage.getRecommendationButtonByName(RecommendationIds.searchJson); keyNameFromRecommendation = await tab.getRecommendationByName(RecommendationIds.searchJson) .find(tab.cssKeyName) .innerText; From 92db7cf11f7183b4e1f8cde4427030e8e8411e6d Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Tue, 13 Aug 2024 15:08:34 +0200 Subject: [PATCH 028/112] #RI-5982 - add tokenizer for search and query fix bugs --- .../pages/search/components/query/Query.tsx | 79 +++++++-- .../search/components/query/constants.ts | 2 +- .../pages/search/components/query/utils.ts | 10 +- .../monacoRedisMonarchTokensProvider.ts | 8 +- .../ui/src/utils/monaco/monacoThemes.ts | 43 +++++ .../monaco/monarchTokens/redisearchTokens.ts | 152 ++++++++++++++++++ 6 files changed, 270 insertions(+), 24 deletions(-) create mode 100644 redisinsight/ui/src/utils/monaco/monacoThemes.ts create mode 100644 redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts diff --git a/redisinsight/ui/src/pages/search/components/query/Query.tsx b/redisinsight/ui/src/pages/search/components/query/Query.tsx index e5be89b8ab..aace67a0f1 100644 --- a/redisinsight/ui/src/pages/search/components/query/Query.tsx +++ b/redisinsight/ui/src/pages/search/components/query/Query.tsx @@ -10,18 +10,24 @@ import { IEditorMount } from 'uiSrc/pages/workbench/interfaces' import { findCurrentArgument, getRange, - getRediSearchSignutureProvider, setCursorPositionAtTheEnd, + getRediSearchSignutureProvider, + setCursorPositionAtTheEnd, splitQueryByArgs } from 'uiSrc/pages/search/utils' -import { SearchCommand } from 'uiSrc/pages/search/types' +import { SearchCommand, TokenType } from 'uiSrc/pages/search/types' import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' import { fetchRedisearchInfoAction } from 'uiSrc/slices/browser/redisearch' +import { getRediSearchMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokens' +import { installRedisearchTheme, RedisearchMonacoTheme } from 'uiSrc/utils/monaco/monacoThemes' +import { useDebouncedEffect } from 'uiSrc/services' import { SUPPORTED_COMMANDS_LIST, options, DefinedArgumentName } from './constants' import { getFieldsSuggestions, getIndexesSuggestions, asSuggestionsRef, - getMandatoryArgumentSuggestions, getOptionalSuggestions, getCommandsSuggestions + getMandatoryArgumentSuggestions, + getOptionalSuggestions, + getCommandsSuggestions } from './utils' export interface Props { @@ -34,6 +40,7 @@ const Query = (props: Props) => { const { value, onChange, indexes } = props const { spec: REDIS_COMMANDS_SPEC } = useSelector(appRedisCommandsSelector) + const [selectedCommand, setSelectedCommand] = useState('') const [selectedIndex, setSelectedIndex] = useState('') const SUPPORTED_COMMANDS = SUPPORTED_COMMANDS_LIST.map((name) => ({ @@ -69,16 +76,23 @@ const Query = (props: Props) => { }, [indexes]) useEffect(() => { + monacoEditor.languages.setMonarchTokensProvider( + MonacoLanguage.RediSearch, + getRediSearchMonarchTokensProvider(SUPPORTED_COMMANDS, selectedCommand) + ) + }, [selectedCommand]) + + useDebouncedEffect(() => { if (!selectedIndex) return + // TODO: add check is selectedIndex is complete (", "\", "dwadawd) - do not request const index = selectedIndex.replace(/^(['"])(.*)\1$/, '$2') - dispatch(fetchRedisearchInfoAction(index, (data) => { const { attributes } = data as any attributesRef.current = attributes })) - }, [selectedIndex]) + }, 200, [selectedIndex]) const editorDidMount = ( editor: monacoEditor.editor.IStandaloneCodeEditor, @@ -93,11 +107,17 @@ const Query = (props: Props) => { const position = editor.getPosition() if (position?.column === 1 && position?.lineNumber === 1) { + editor.focus() suggestionsRef.current = getSuggestions(editor) triggerSuggestions() } } + monaco.languages.setMonarchTokensProvider( + MonacoLanguage.RediSearch, + getRediSearchMonarchTokensProvider(SUPPORTED_COMMANDS) + ) + disposeSignatureHelpProvider.current?.() disposeSignatureHelpProvider.current = monaco.languages.registerSignatureHelpProvider(MonacoLanguage.RediSearch, { provideSignatureHelp: (): any => getRediSearchSignutureProvider(helpWidgetRef?.current) @@ -109,6 +129,7 @@ const Query = (props: Props) => { ({ suggestions: suggestionsRef.current.data }) }).dispose + installRedisearchTheme() editor.onDidChangeCursorPosition(handleCursorChange) } @@ -123,15 +144,21 @@ const Query = (props: Props) => { } suggestionsRef.current = getSuggestions(editor) + if (suggestionsRef.current.data.length) { - triggerSuggestions() helpWidgetRef.current.isOpen = false + triggerSuggestions() return } + editor?.trigger('', 'editor.action.triggerParameterHints', '') + + const suggestController = editor.getContribution('editor.contrib.suggestController') + const suggestModel = suggestController?.model + helpWidgetRef.current.isOpen = suggestModel?.state === 0 && helpWidgetRef.current.isOpen + if (suggestionsRef.current.forceHide) { setTimeout(() => editor?.trigger('', 'hideSuggestWidget', null), 0) - editor?.trigger('', 'editor.action.triggerParameterHints', '') } } @@ -160,35 +187,53 @@ const Query = (props: Props) => { const { args, isCursorInArg, prevCursorChar, nextCursorChar } = splitQueryByArgs(value, offset) const allArgs = args.flat() - const [beforeOffsetArgs] = args + const [beforeOffsetArgs, [currentOffsetArg]] = args const [firstArg, ...prevArgs] = beforeOffsetArgs - const commandName = firstArg?.toUpperCase() + const commandName = (firstArg || currentOffsetArg)?.toUpperCase() const command = REDIS_COMMANDS_SPEC[commandName] as unknown as SearchCommand - if (!command && position.lineNumber === 1 && word.startColumn === 1) { + const isCommandSuppurted = SUPPORTED_COMMANDS + .some(({ name }) => commandName === name) + if (command && !isCommandSuppurted) return asSuggestionsRef([]) + + if (!command && position.lineNumber === 1 && position.column === 1) { return getCommandsSuggestions(SUPPORTED_COMMANDS, range) } - if (!command) return asSuggestionsRef([]) + if (!command) { + helpWidgetRef.current.isOpen = false + return asSuggestionsRef([], false) + } + + helpWidgetRef.current = { + isOpen: beforeOffsetArgs.length > 0, + parent: { command, arguments: [{ token: commandName, type: TokenType.PureToken }, ...command.arguments!] }, + currentArg: command?.arguments?.[prevArgs.length] + } setSelectedIndex(allArgs[1] || '') + setSelectedCommand(commandName) // cover query if (command?.arguments?.[prevArgs.length]?.name === DefinedArgumentName.query) { if (prevCursorChar === '@') { + helpWidgetRef.current.isOpen = false return asSuggestionsRef(getFieldsSuggestions(attributesRef.current, range), false) } - return asSuggestionsRef([]) + return asSuggestionsRef([], false) } if (isCursorInArg || nextCursorChar?.trim()) return asSuggestionsRef([]) // cover index field if (command?.arguments?.[prevArgs.length]?.name === DefinedArgumentName.index) { - if (prevCursorChar?.trim()) return asSuggestionsRef([], false) - return asSuggestionsRef(getIndexesSuggestions(indexesRef.current, range)) + if (currentOffsetArg) return asSuggestionsRef([], false) + if (indexesRef.current.length) { + return asSuggestionsRef(getIndexesSuggestions(indexesRef.current, range)) + } + return asSuggestionsRef([]) } if (prevArgs.length < 2) return asSuggestionsRef([]) @@ -202,7 +247,9 @@ const Query = (props: Props) => { } if (foundArg && !foundArg.isComplete) return getMandatoryArgumentSuggestions(foundArg, attributesRef.current, range) - if (!foundArg || foundArg.isComplete) return getOptionalSuggestions(command, foundArg, allArgs, range) + if (!foundArg || foundArg.isComplete) { + return getOptionalSuggestions(command, foundArg, allArgs, range, currentOffsetArg) + } return asSuggestionsRef([]) } @@ -211,7 +258,7 @@ const Query = (props: Props) => { value={value} onChange={onChange} language={MonacoLanguage.RediSearch} - theme={theme === Theme.Dark ? 'dark' : 'light'} + theme={theme === Theme.Dark ? RedisearchMonacoTheme.dark : RedisearchMonacoTheme.light} options={options} editorDidMount={editorDidMount} /> diff --git a/redisinsight/ui/src/pages/search/components/query/constants.ts b/redisinsight/ui/src/pages/search/components/query/constants.ts index 15556977d6..252d2a8de3 100644 --- a/redisinsight/ui/src/pages/search/components/query/constants.ts +++ b/redisinsight/ui/src/pages/search/components/query/constants.ts @@ -5,7 +5,7 @@ export const options = merge(defaultMonacoOptions, { suggest: { showWords: false, - showIcons: true + showIcons: true, } }) diff --git a/redisinsight/ui/src/pages/search/components/query/utils.ts b/redisinsight/ui/src/pages/search/components/query/utils.ts index 24537cdb32..d9c034f8a5 100644 --- a/redisinsight/ui/src/pages/search/components/query/utils.ts +++ b/redisinsight/ui/src/pages/search/components/query/utils.ts @@ -21,6 +21,7 @@ export const getIndexesSuggestions = (indexes: RedisResponseBuffer[], range: mon insertText: `"${value}" "$1" `, insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, range, + detail: value || ' ', } }) @@ -36,10 +37,11 @@ export const getFieldsSuggestions = (fields: any[], range: monaco.IRange, spaceA export const getCommandsSuggestions = (commands: SearchCommand[], range: monaco.IRange) => asSuggestionsRef( commands.map((command) => buildSuggestion(command, range, { detail: generateDetail(command), + insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, documentation: { value: getCommandMarkdown(command as any) }, - })) + })), false ) export const getMandatoryArgumentSuggestions = ( @@ -66,7 +68,8 @@ export const getOptionalSuggestions = ( command: SearchCommand, foundArg: Nullable, allArgs: string[], - range: monaco.IRange + range: monaco.IRange, + currentArg?: string ) => { const appendCommands = foundArg?.append ?? [] @@ -78,7 +81,8 @@ export const getOptionalSuggestions = ( })), ...(command?.arguments || []) .filter((arg) => arg.optional) - .filter((arg) => arg.multiple || !allArgs.includes(arg.token || arg.arguments?.[0]?.token || '')) + .filter((arg) => + arg.multiple || !(currentArg !== arg.token && allArgs.includes(arg.token || arg.arguments?.[0]?.token || ''))) .map((arg) => buildSuggestion(arg, range, { sortText: 'b', kind: monacoEditor.languages.CompletionItemKind.Reference, diff --git a/redisinsight/ui/src/utils/monaco/monacoRedisMonarchTokensProvider.ts b/redisinsight/ui/src/utils/monaco/monacoRedisMonarchTokensProvider.ts index a4a1ce63bd..9f41113289 100644 --- a/redisinsight/ui/src/utils/monaco/monacoRedisMonarchTokensProvider.ts +++ b/redisinsight/ui/src/utils/monaco/monacoRedisMonarchTokensProvider.ts @@ -79,14 +79,14 @@ export const getRedisMonarchTokensProvider = (commands: string[]): monacoEditor. [/"/, { token: STRING_DOUBLE, next: '@stringDouble' }], ], string: [ - [/[^']+/, 'string'], - [/''/, 'string'], + [/\\./, 'string'], [/'/, { token: 'string', next: '@pop' }], + [/[^\\']+/, 'string'], ], stringDouble: [ - [/[^"]+/, STRING_DOUBLE], - [/""/, STRING_DOUBLE], + [/\\./, STRING_DOUBLE], [/"/, { token: STRING_DOUBLE, next: '@pop' }], + [/[^\\"]+/, STRING_DOUBLE], ], scopes: [ // NOT SUPPORTED diff --git a/redisinsight/ui/src/utils/monaco/monacoThemes.ts b/redisinsight/ui/src/utils/monaco/monacoThemes.ts new file mode 100644 index 0000000000..0e7f41f56f --- /dev/null +++ b/redisinsight/ui/src/utils/monaco/monacoThemes.ts @@ -0,0 +1,43 @@ +import { monaco as monacoEditor } from 'react-monaco-editor' + +export enum RedisearchMonacoTheme { + dark = 'redisearchDarkTheme', + light = 'redisearchLightTheme' +} + +export const installRedisearchTheme = () => { + monacoEditor.editor.defineTheme(RedisearchMonacoTheme.dark, { + base: 'vs-dark', + inherit: true, + rules: [ + { token: 'keyword', foreground: '#569cd6', fontStyle: 'bold' }, + // { token: 'argument.token', foreground: '#6db9a2' }, + { token: 'argument.block.0', foreground: '#66ccaf' }, + { token: 'argument.block.1', foreground: '#459d7f' }, + { token: 'argument.block.2', foreground: '#3c816a' }, + { token: 'argument.block.3', foreground: '#28644f' }, + { token: 'loadAll', foreground: '#6db9a2' }, + { token: 'index', foreground: '#ce51cc' }, + { token: 'query', foreground: '#5183ce' }, + { token: 'field', foreground: '#c43265' }, + ], + colors: {} + }) + + monacoEditor.editor.defineTheme(RedisearchMonacoTheme.light, { + base: 'vs', + inherit: true, + rules: [ + { token: 'keyword', foreground: '#569cd6', fontStyle: 'bold' }, + { token: 'argument.block.0', foreground: '#66ccaf' }, + { token: 'argument.block.1', foreground: '#459d7f' }, + { token: 'argument.block.2', foreground: '#3c816a' }, + { token: 'argument.block.3', foreground: '#28644f' }, + { token: 'loadAll', foreground: '#6db9a2' }, + { token: 'index', foreground: '#ce51cc' }, + { token: 'field', foreground: '#5183ce' }, + { token: 'field', foreground: '#c43265' }, + ], + colors: {} + }) +} diff --git a/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts b/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts new file mode 100644 index 0000000000..2eb71dde4f --- /dev/null +++ b/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts @@ -0,0 +1,152 @@ +import { monaco as monacoEditor } from 'react-monaco-editor' +import { SearchCommand, TokenType } from 'uiSrc/pages/search/types' + +const STRING_DOUBLE = 'string.double' + +const generateKeywords = (commands: SearchCommand[]) => commands.map(({ name }) => name) +const generateTokens = (command?: SearchCommand) => { + if (!command) return [] + const levels: Array> = [] + + function processArguments(args: SearchCommand[], level = 0) { + // Ensure the current level exists in the levels array + if (!levels[level]) { + levels[level] = [] + } + + args.forEach((arg) => { + if (arg.token) levels[level].push(arg.token) + + if (arg.type === TokenType.Block && arg.arguments) { + const blockToken = arg.arguments[0].token + const nextArgs = arg.arguments + if (blockToken) { + levels[level].push(blockToken) + } + processArguments(blockToken ? nextArgs.slice(1, nextArgs.length) : nextArgs, level + 1) + } + + if (arg.type === TokenType.OneOf && arg.arguments) { + arg.arguments.forEach((choice) => { + if (choice.token) levels[level].push(choice.token) + }) + } + }) + } + + if (command.arguments) { + processArguments(command.arguments, 0) + } + + return levels +} + +export const getRediSearchMonarchTokensProvider = ( + commands: SearchCommand[], + command?: string +): monacoEditor.languages.IMonarchLanguage => { + const currentCommand = commands.find(({ name }) => name === command) + + const keywords = generateKeywords(commands) + const argTokens = generateTokens(currentCommand) + + return ( + { + defaultToken: '', + tokenPostfix: '.redisearch', + ignoreCase: true, + brackets: [ + { open: '[', close: ']', token: 'delimiter.square' }, + { open: '(', close: ')', token: 'delimiter.parenthesis' }, + ], + keywords, + tokenizer: { + root: [ + { include: '@whitespace' }, + { include: '@numbers' }, + { include: '@strings' }, + { include: '@keyword' }, + [/LOAD\s+\*/, 'loadAll'], + { include: '@argument.block' }, + [/[;,.]/, 'delimiter'], + [/[()]/, '@brackets'], + [ + /[\w@#$]+/, + { + cases: { + '@keywords': 'keyword', + '@default': 'identifier', + }, + }, + ], + [/[<>=!%&+\-*/|~^]/, 'operator'], + ], + keyword: [ + [`(${keywords.join('|')})\\b`, { token: 'keyword', next: '@index' }] + ], + 'argument.block': argTokens.map((tokens, lvl) => [`(${tokens.join('|')})\\b`, `argument.block.${lvl}`]), + index: [ + [/"([^"\\]|\\.)*"/, { token: 'index', next: '@query' }], + [/'([^'\\]|\\.)*'/, { token: 'index', next: '@query' }], + [/[a-zA-Z_]\w*/, { token: 'index', next: '@query' }], + { include: 'root' } // Fallback to the root state if nothing matches + ], + query: [ + [/"/, { token: 'query', next: '@queryInsideDouble' }], + [/'/, { token: 'query', next: '@queryInsideSingle' }], + [/[a-zA-Z_]\w*/, { token: 'query', next: '@root' }], + { include: 'root' } // Fallback to the root state if nothing matches + ], + queryInsideDouble: [ + [/@/, { token: 'field', next: '@fieldInDouble' }], + [/\\"/, { token: 'query', next: 'queryInsideDouble' }], + [/"/, { token: 'query', next: '@root' }], + [/./, { token: 'query', next: '@queryInsideDouble' }], + { include: '@query' } // Fallback to the root state if nothing matches + ], + queryInsideSingle: [ + [/@/, { token: 'field', next: '@fieldInSingle' }], + [/\\'/, { token: 'query', next: 'queryInsideSingle' }], + [/'/, { token: 'query', next: '@root' }], + [/./, { token: 'query', next: '@queryInsideSingle' }], + { include: '@query' } // Fallback to the root state if nothing matches + ], + fieldInDouble: [ + [/\w+/, { token: 'field', next: '@queryInsideDouble' }], + [/\s+/, { token: '@rematch', next: '@queryInsideDouble' }], + [/"/, { token: 'query', next: '@root' }], + { include: '@query' } // Fallback to the root state if nothing matches + ], + fieldInSingle: [ + [/\w+/, { token: 'field', next: '@queryInsideSingle' }], + [/\s+/, { token: '@rematch', next: '@queryInsideSingle' }], + [/'/, { token: 'query', next: '@root' }], + { include: '@query' } // Fallback to the root state if nothing matches + ], + whitespace: [ + [/\s+/, 'white'], + [/\/\/.*$/, 'comment'], + ], + numbers: [ + [/0[xX][0-9a-fA-F]*/, 'number'], + [/[$][+-]*\d*(\.\d*)?/, 'number'], + [/((\d+(\.\d*)?)|(\.\d+))([eE][-+]?\d+)?/, 'number'], + ], + strings: [ + [/'/, { token: 'string', next: '@string' }], + [/"/, { token: STRING_DOUBLE, next: '@stringDouble' }], + ], + string: [ + [/\\./, 'string'], + [/'/, { token: 'string', next: '@pop' }], + [/[^\\']+/, 'string'], + ], + stringDouble: [ + [/\\./, STRING_DOUBLE], + [/"/, { token: STRING_DOUBLE, next: '@pop' }], + [/[^\\"]+/, STRING_DOUBLE], + ], + }, + } + ) +} From 6d0bf34b43df5568cfe7e3fb6b9e4f12c4ab81ac Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Thu, 15 Aug 2024 14:38:07 +0200 Subject: [PATCH 029/112] #RI-5988 - fix bugs, refactoring --- .../pages/search/components/query/Query.tsx | 60 +++++++------ .../pages/search/components/query/utils.ts | 36 ++++++-- .../ui/src/pages/search/utils/query.ts | 86 +++++++++---------- .../pages/search/utils/tests/query.spec.ts | 10 +-- 4 files changed, 105 insertions(+), 87 deletions(-) diff --git a/redisinsight/ui/src/pages/search/components/query/Query.tsx b/redisinsight/ui/src/pages/search/components/query/Query.tsx index aace67a0f1..a72ff71486 100644 --- a/redisinsight/ui/src/pages/search/components/query/Query.tsx +++ b/redisinsight/ui/src/pages/search/components/query/Query.tsx @@ -8,13 +8,14 @@ import { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands' import { Nullable } from 'uiSrc/utils' import { IEditorMount } from 'uiSrc/pages/workbench/interfaces' import { + addOwnTokenToArgs, findCurrentArgument, getRange, getRediSearchSignutureProvider, setCursorPositionAtTheEnd, splitQueryByArgs } from 'uiSrc/pages/search/utils' -import { SearchCommand, TokenType } from 'uiSrc/pages/search/types' +import { SearchCommand } from 'uiSrc/pages/search/types' import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' import { fetchRedisearchInfoAction } from 'uiSrc/slices/browser/redisearch' import { getRediSearchMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokens' @@ -27,7 +28,7 @@ import { asSuggestionsRef, getMandatoryArgumentSuggestions, getOptionalSuggestions, - getCommandsSuggestions + getCommandsSuggestions, isIndexComplete } from './utils' export interface Props { @@ -83,8 +84,8 @@ const Query = (props: Props) => { }, [selectedCommand]) useDebouncedEffect(() => { - if (!selectedIndex) return - // TODO: add check is selectedIndex is complete (", "\", "dwadawd) - do not request + attributesRef.current = [] + if (!isIndexComplete(selectedIndex)) return const index = selectedIndex.replace(/^(['"])(.*)\1$/, '$2') dispatch(fetchRedisearchInfoAction(index, @@ -133,6 +134,16 @@ const Query = (props: Props) => { editor.onDidChangeCursorPosition(handleCursorChange) } + const isSuggestionsOpened = () => { + const { editor } = monacoObjects.current || {} + + if (!editor) return false + const suggestController = editor.getContribution('editor.contrib.suggestController') + const suggestModel = suggestController?.model + + return suggestModel?.state === 1 + } + const handleCursorChange = () => { const { editor } = monacoObjects.current || {} suggestionsRef.current.data = [] @@ -151,14 +162,12 @@ const Query = (props: Props) => { return } - editor?.trigger('', 'editor.action.triggerParameterHints', '') - - const suggestController = editor.getContribution('editor.contrib.suggestController') - const suggestModel = suggestController?.model - helpWidgetRef.current.isOpen = suggestModel?.state === 0 && helpWidgetRef.current.isOpen + editor.trigger('', 'editor.action.triggerParameterHints', '') if (suggestionsRef.current.forceHide) { setTimeout(() => editor?.trigger('', 'hideSuggestWidget', null), 0) + } else { + helpWidgetRef.current.isOpen = !isSuggestionsOpened() && helpWidgetRef.current.isOpen } } @@ -169,6 +178,12 @@ const Query = (props: Props) => { setTimeout(() => editor?.trigger('', 'editor.action.triggerSuggest', { auto: false })) } + const updateHelpWidget = (isOpen: boolean, parent?: SearchCommand, currentArg?: SearchCommand) => { + helpWidgetRef.current.isOpen = isOpen + helpWidgetRef.current.parent = parent + helpWidgetRef.current.currentArg = currentArg + } + const getSuggestions = ( editor: monacoEditor.editor.IStandaloneCodeEditor ): { @@ -193,10 +208,8 @@ const Query = (props: Props) => { const commandName = (firstArg || currentOffsetArg)?.toUpperCase() const command = REDIS_COMMANDS_SPEC[commandName] as unknown as SearchCommand - const isCommandSuppurted = SUPPORTED_COMMANDS - .some(({ name }) => commandName === name) + const isCommandSuppurted = SUPPORTED_COMMANDS.some(({ name }) => commandName === name) if (command && !isCommandSuppurted) return asSuggestionsRef([]) - if (!command && position.lineNumber === 1 && position.column === 1) { return getCommandsSuggestions(SUPPORTED_COMMANDS, range) } @@ -206,17 +219,13 @@ const Query = (props: Props) => { return asSuggestionsRef([], false) } - helpWidgetRef.current = { - isOpen: beforeOffsetArgs.length > 0, - parent: { command, arguments: [{ token: commandName, type: TokenType.PureToken }, ...command.arguments!] }, - currentArg: command?.arguments?.[prevArgs.length] - } - setSelectedIndex(allArgs[1] || '') setSelectedCommand(commandName) // cover query if (command?.arguments?.[prevArgs.length]?.name === DefinedArgumentName.query) { + updateHelpWidget(true, addOwnTokenToArgs(commandName, command), command?.arguments?.[prevArgs.length]) + if (prevCursorChar === '@') { helpWidgetRef.current.isOpen = false return asSuggestionsRef(getFieldsSuggestions(attributesRef.current, range), false) @@ -227,24 +236,19 @@ const Query = (props: Props) => { if (isCursorInArg || nextCursorChar?.trim()) return asSuggestionsRef([]) - // cover index field + // cover index if (command?.arguments?.[prevArgs.length]?.name === DefinedArgumentName.index) { + updateHelpWidget(true, addOwnTokenToArgs(commandName, command), command?.arguments?.[prevArgs.length]) + if (currentOffsetArg) return asSuggestionsRef([], false) - if (indexesRef.current.length) { - return asSuggestionsRef(getIndexesSuggestions(indexesRef.current, range)) - } + if (indexesRef.current.length) return asSuggestionsRef(getIndexesSuggestions(indexesRef.current, range)) return asSuggestionsRef([]) } if (prevArgs.length < 2) return asSuggestionsRef([]) const foundArg = findCurrentArgument(command?.arguments || [], prevArgs) - - helpWidgetRef.current = { - isOpen: !!foundArg?.stopArg, - parent: foundArg?.parent, - currentArg: foundArg?.stopArg - } + updateHelpWidget(!!foundArg?.stopArg, foundArg?.parent, foundArg?.stopArg) if (foundArg && !foundArg.isComplete) return getMandatoryArgumentSuggestions(foundArg, attributesRef.current, range) if (!foundArg || foundArg.isComplete) { diff --git a/redisinsight/ui/src/pages/search/components/query/utils.ts b/redisinsight/ui/src/pages/search/components/query/utils.ts index d9c034f8a5..1df71cd5a2 100644 --- a/redisinsight/ui/src/pages/search/components/query/utils.ts +++ b/redisinsight/ui/src/pages/search/components/query/utils.ts @@ -26,13 +26,17 @@ export const getIndexesSuggestions = (indexes: RedisResponseBuffer[], range: mon }) export const getFieldsSuggestions = (fields: any[], range: monaco.IRange, spaceAfter = false) => - fields.map(({ attribute }) => ({ - label: attribute, - kind: monacoEditor.languages.CompletionItemKind.Reference, - insertText: `${attribute}${spaceAfter ? ' ' : ''}`, - insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, - range, - })) + fields.map(({ attribute }) => { + const insertText = attribute.trim() ? attribute : `"${attribute}"` + return { + label: attribute || ' ', + kind: monacoEditor.languages.CompletionItemKind.Reference, + insertText: `${insertText}${spaceAfter ? ' ' : ''}`, + insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, + range, + detail: attribute || ' ', + } + }) export const getCommandsSuggestions = (commands: SearchCommand[], range: monaco.IRange) => asSuggestionsRef( commands.map((command) => buildSuggestion(command, range, { @@ -50,6 +54,7 @@ export const getMandatoryArgumentSuggestions = ( range: monaco.IRange ) => { if (foundArg.stopArg?.name === DefinedArgumentName.field) { + if (!fields.length) asSuggestionsRef([]) return asSuggestionsRef(getFieldsSuggestions(fields, range, true)) } @@ -90,3 +95,20 @@ export const getOptionalSuggestions = ( })) ]) } + +export const isIndexComplete = (index: string) => { + if (!index) return false + if (index.startsWith('"')) { + if (index.length < 2) return false + if (!index.endsWith('"')) return false + return !index.endsWith('\\', -1) + } + + if (index.startsWith("'")) { + if (index.length < 2) return false + if (!index.endsWith("'")) return false + return !index.endsWith('\\', -1) + } + + return true +} diff --git a/redisinsight/ui/src/pages/search/utils/query.ts b/redisinsight/ui/src/pages/search/utils/query.ts index 0a5e269068..4a04e391bb 100644 --- a/redisinsight/ui/src/pages/search/utils/query.ts +++ b/redisinsight/ui/src/pages/search/utils/query.ts @@ -230,39 +230,45 @@ export const getArgumentSuggestions = ( isBlocked: isWasBlocked } = findStopArgumentInQuery(pastStringArgs, pastCommandArgs) - // TODO refactor to early return related to where we stopped - const stopArgument = restArguments[stopArgIndex] const restNotFilledArgs = restArguments.slice(stopArgIndex) - const isStopArgumentCannotSuggest = Boolean(stopArgument && !(stopArgument.token || stopArgument?.arguments?.length)) - const isBlocked = isWasBlocked || isStopArgumentCannotSuggest - - const restParentOptionalSuggestions = !stopArgument || stopArgument?.optional - ? getRestParentArguments(current?.parent, current?.name, current?.multiple) - .filter((arg) => - arg.optional && arg.name !== stopArgument?.name - && (current?.multiple || arg.name !== current?.name)) - : [] - const restOptionalSuggestions = isBlocked - ? [] - : fillArgsByType([...restNotFilledArgs, ...restParentOptionalSuggestions]) const isOneOfArgument = stopArgument?.type === TokenType.OneOf || (stopArgument?.type === TokenType.PureToken && current?.parent?.type === TokenType.OneOf) - const isArgSuggestions = stopArgument && !stopArgument.optional && (stopArgument?.token || isOneOfArgument) - const suggestions = isArgSuggestions - // only 1 suggestion since next arg is required - ? [isOneOfArgument ? stopArgument.arguments : stopArgument].flat() - : restOptionalSuggestions + if (isWasBlocked) { + return { + isComplete: false, + stopArg: stopArgument, + isBlocked: !isOneOfArgument, + append: isOneOfArgument ? stopArgument.arguments! : [], + } + } + + if (stopArgument && !stopArgument.optional) { + const isCanAppend = stopArgument?.token || isOneOfArgument + const append = isCanAppend ? [isOneOfArgument ? stopArgument.arguments! : stopArgument].flat() : [] + return { + isComplete: false, + stopArg: stopArgument, + isBlocked: !isCanAppend, + append, + } + } + + const restParentOptionalSuggestions = getRestParentArguments(current?.parent, current?.name, current?.multiple) + .filter((arg) => arg.optional && arg.name !== stopArgument?.name + && (current?.multiple || arg.name !== current?.name)) + + const restOptionalSuggestions = fillArgsByType([...restNotFilledArgs, ...restParentOptionalSuggestions]) const requiredArgsLength = restNotFilledArgs.filter((arg) => !arg.optional).length return { - isComplete: requiredArgsLength === 0 && !isBlocked, + isComplete: requiredArgsLength === 0, stopArg: stopArgument, - isBlocked, - append: suggestions, + isBlocked: false, + append: restOptionalSuggestions, } } @@ -284,23 +290,15 @@ export const getRestParentArguments = ( return [...currentRestArgs, ...prevArgs] } -export const fillArgsByType = (args: SearchCommand[]) => { - const result = [] +export const fillArgsByType = (args: SearchCommand[]): SearchCommand[] => { + const result: SearchCommand[] = [] for (let i = 0; i < args.length; i++) { const currentArg = args[i] - if (currentArg.type === TokenType.OneOf) { - result.push(...(currentArg?.arguments || [])) - } - - if (currentArg.type === TokenType.Block) { - result.push(currentArg.arguments?.[0]) - } - - if (currentArg.token) { - result.push(currentArg) - } + if (currentArg.type === TokenType.OneOf) result.push(...(currentArg?.arguments || [])) + if (currentArg.type === TokenType.Block) result.push(currentArg.arguments?.[0] as SearchCommand) + if (currentArg.token) result.push(currentArg) } return result @@ -316,18 +314,18 @@ export const isCompositeArgument = (arg: string, prevArg?: string) => arg === '* export const generateDetail = (command: Maybe) => { if (!command) return '' - - if (command.arguments) { - return generateArgsNames(CommandProvider.Main, command.arguments).join(' ') - } - + if (command.arguments) return generateArgsNames(CommandProvider.Main, command.arguments).join(' ') if (command.token) { - if (command.type === TokenType.PureToken) { - return command.token - } - + if (command.type === TokenType.PureToken) return command.token return `${command.token} ${command.name}` } return '' } + +export const addOwnTokenToArgs = (token: string, command: SearchCommand) => { + if (command.arguments) { + return ({ ...command, arguments: [{ token, type: TokenType.PureToken }, ...command.arguments] }) + } + return command +} diff --git a/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts b/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts index 93bd7ea54e..f550210ed4 100644 --- a/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts +++ b/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts @@ -12,7 +12,7 @@ const ftAggreageTests = [ args: ['index', '"query"', 'APPLY'], result: { stopArg: { name: 'expression', token: 'APPLY', type: 'string' }, - append: [{ name: 'expression', token: 'APPLY', type: 'string' }], + append: [], isBlocked: true, isComplete: false, parent: expect.any(Object) @@ -253,13 +253,7 @@ const ftSearchTests = [ type: 'string', token: 'FIELDS' }, - append: [ - { - name: 'count', - type: 'string', - token: 'FIELDS' - } - ], + append: [], isBlocked: true, isComplete: false, parent: expect.any(Object) From 82a505f1747ea471cd1b7ab1896afaba00fdf001 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Thu, 15 Aug 2024 14:40:46 +0200 Subject: [PATCH 030/112] #RI-5988 - fix tests --- redisinsight/__mocks__/monacoMock.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/redisinsight/__mocks__/monacoMock.js b/redisinsight/__mocks__/monacoMock.js index d26e492ffe..68045663d8 100644 --- a/redisinsight/__mocks__/monacoMock.js +++ b/redisinsight/__mocks__/monacoMock.js @@ -70,7 +70,8 @@ export const languages = { }, CompletionItemInsertTextRule: { InsertAsSnippet: 4 - } + }, + ...monacoEditor.languages } export const monaco = { From c4daceb8f831961f574ecabfbe0638ee06f44bc6 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Fri, 16 Aug 2024 10:13:22 +0200 Subject: [PATCH 031/112] #RI-6008, #RI-6009 - fix bugs --- .../pages/search/components/query/Query.tsx | 18 ++++++++++--- .../search/components/query/constants.ts | 1 + .../ui/src/pages/search/utils/query.ts | 26 +++++++++++++++---- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/redisinsight/ui/src/pages/search/components/query/Query.tsx b/redisinsight/ui/src/pages/search/components/query/Query.tsx index a72ff71486..6fc1fdd94b 100644 --- a/redisinsight/ui/src/pages/search/components/query/Query.tsx +++ b/redisinsight/ui/src/pages/search/components/query/Query.tsx @@ -63,6 +63,7 @@ const Query = (props: Props) => { }) const indexesRef = useRef([]) const attributesRef = useRef([]) + const isEscapedSuggestions = useRef(false) const { theme } = useContext(ThemeContext) const dispatch = useDispatch() @@ -102,6 +103,8 @@ const Query = (props: Props) => { monaco.languages.register({ id: MonacoLanguage.RediSearch }) monacoObjects.current = { editor, monaco } + suggestionsRef.current = getSuggestions(editor) + if (value) { setCursorPositionAtTheEnd(editor) } else { @@ -109,7 +112,6 @@ const Query = (props: Props) => { if (position?.column === 1 && position?.lineNumber === 1) { editor.focus() - suggestionsRef.current = getSuggestions(editor) triggerSuggestions() } } @@ -131,7 +133,13 @@ const Query = (props: Props) => { }).dispose installRedisearchTheme() + editor.onDidChangeCursorPosition(handleCursorChange) + editor.onKeyDown((e: monacoEditor.IKeyboardEvent) => { + if (e.keyCode === monacoEditor.KeyCode.Escape && isSuggestionsOpened()) { + isEscapedSuggestions.current = true + } + }) } const isSuggestionsOpened = () => { @@ -200,7 +208,7 @@ const Query = (props: Props) => { const word = model.getWordUntilPosition(position) const range = getRange(position, word) - const { args, isCursorInArg, prevCursorChar, nextCursorChar } = splitQueryByArgs(value, offset) + const { args, isCursorInQuotes, prevCursorChar, nextCursorChar } = splitQueryByArgs(value, offset) const allArgs = args.flat() const [beforeOffsetArgs, [currentOffsetArg]] = args const [firstArg, ...prevArgs] = beforeOffsetArgs @@ -234,18 +242,20 @@ const Query = (props: Props) => { return asSuggestionsRef([], false) } - if (isCursorInArg || nextCursorChar?.trim()) return asSuggestionsRef([]) - // cover index if (command?.arguments?.[prevArgs.length]?.name === DefinedArgumentName.index) { updateHelpWidget(true, addOwnTokenToArgs(commandName, command), command?.arguments?.[prevArgs.length]) + // we do not suggest indexes if there is something next if (currentOffsetArg) return asSuggestionsRef([], false) if (indexesRef.current.length) return asSuggestionsRef(getIndexesSuggestions(indexesRef.current, range)) return asSuggestionsRef([]) } + if (isCursorInQuotes || nextCursorChar?.trim()) return asSuggestionsRef([]) if (prevArgs.length < 2) return asSuggestionsRef([]) + if ((prevCursorChar?.trim() || isCursorInQuotes) && isEscapedSuggestions.current) return asSuggestionsRef([]) + isEscapedSuggestions.current = false const foundArg = findCurrentArgument(command?.arguments || [], prevArgs) updateHelpWidget(!!foundArg?.stopArg, foundArg?.parent, foundArg?.stopArg) diff --git a/redisinsight/ui/src/pages/search/components/query/constants.ts b/redisinsight/ui/src/pages/search/components/query/constants.ts index 252d2a8de3..437b0ae82c 100644 --- a/redisinsight/ui/src/pages/search/components/query/constants.ts +++ b/redisinsight/ui/src/pages/search/components/query/constants.ts @@ -6,6 +6,7 @@ export const options = merge(defaultMonacoOptions, suggest: { showWords: false, showIcons: true, + insertMode: 'replace', } }) diff --git a/redisinsight/ui/src/pages/search/utils/query.ts b/redisinsight/ui/src/pages/search/utils/query.ts index 4a04e391bb..703d651b31 100644 --- a/redisinsight/ui/src/pages/search/utils/query.ts +++ b/redisinsight/ui/src/pages/search/utils/query.ts @@ -1,6 +1,6 @@ /* eslint-disable no-continue */ -import { toNumber } from 'lodash' +import { toNumber, uniqBy } from 'lodash' import { generateArgsNames, Maybe, Nullable } from 'uiSrc/utils' import { CommandProvider } from 'uiSrc/constants' import { ArgName, FoundCommandArgument, SearchCommand, SearchCommandTree, TokenType } from '../types' @@ -11,7 +11,7 @@ export const splitQueryByArgs = (query: string, position: number = 0) => { let inQuotes = false let escapeNextChar = false let quoteChar = '' - const isCursorInArg = false + let isCursorInQuotes = false let lastArg = '' const pushToProperTuple = (isAfterOffset: boolean, arg: string) => { @@ -27,6 +27,7 @@ export const splitQueryByArgs = (query: string, position: number = 0) => { for (let i = 0; i < query.length; i++) { const char = query[i] const isAfterOffset = i >= position + (inQuotes ? -1 : 0) + if (i === position - 1) isCursorInQuotes = inQuotes if (escapeNextChar) { arg += char @@ -71,7 +72,7 @@ export const splitQueryByArgs = (query: string, position: number = 0) => { pushToProperTuple(true, arg) } - return { args, isCursorInArg, prevCursorChar: query[position - 1], nextCursorChar: query[position] } + return { args, isCursorInQuotes, prevCursorChar: query[position - 1], nextCursorChar: query[position] } } export const findCurrentArgument = ( @@ -140,6 +141,17 @@ const findStopArgumentInQuery = ( continue } + if ( + !isBlockedOnCommand + && currentCommandArg?.token + && currentCommandArg.optional + && currentCommandArg.token !== arg.toUpperCase() + ) { + moveToNextCommandArg() + skipArg() + continue + } + // if we are on token - that requires one more argument if (currentCommandArg?.token === arg.toUpperCase()) { blockCommand() @@ -204,7 +216,8 @@ const findStopArgumentInQuery = ( moveToNextCommandArg() const nextCommand = restCommandArgs[currentCommandArgIndex + 1] - isBlockedOnCommand = (!!nextCommand && !nextCommand.optional) + const currentCommand = restCommandArgs[currentCommandArgIndex] + isBlockedOnCommand = [currentCommand, nextCommand].every((arg) => arg && !arg.optional) } return { @@ -261,7 +274,10 @@ export const getArgumentSuggestions = ( .filter((arg) => arg.optional && arg.name !== stopArgument?.name && (current?.multiple || arg.name !== current?.name)) - const restOptionalSuggestions = fillArgsByType([...restNotFilledArgs, ...restParentOptionalSuggestions]) + const restOptionalSuggestions = uniqBy( + fillArgsByType([...restNotFilledArgs, ...restParentOptionalSuggestions]), + 'token' + ) const requiredArgsLength = restNotFilledArgs.filter((arg) => !arg.optional).length return { From f0c0cb653944e5ec8d0fb71aa8cc8b0a5af75764 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Mon, 19 Aug 2024 16:15:45 +0200 Subject: [PATCH 032/112] #RI-5993 - add tests --- redisinsight/__mocks__/monacoMock.js | 11 +- .../ui/src/pages/search/SearchPage.spec.tsx | 68 ++++- .../ui/src/pages/search/SearchPage.tsx | 4 +- .../query-wrapper/QueryWrapper.spec.tsx | 70 +++++- .../components/query-wrapper/QueryWrapper.tsx | 27 +- .../search/components/query/Query.spec.tsx | 10 + .../pages/search/components/query/Query.tsx | 28 +-- .../pages/search/utils/tests/monaco.spec.ts | 60 +++++ .../pages/search/utils/tests/query.spec.ts | 235 +++++++++++++++++- .../tests/search/searchAndQuery.spec.ts | 58 +++++ 10 files changed, 542 insertions(+), 29 deletions(-) create mode 100644 redisinsight/ui/src/pages/search/components/query/Query.spec.tsx create mode 100644 redisinsight/ui/src/pages/search/utils/tests/monaco.spec.ts create mode 100644 redisinsight/ui/src/slices/tests/search/searchAndQuery.spec.ts diff --git a/redisinsight/__mocks__/monacoMock.js b/redisinsight/__mocks__/monacoMock.js index 68045663d8..63b6eae71b 100644 --- a/redisinsight/__mocks__/monacoMock.js +++ b/redisinsight/__mocks__/monacoMock.js @@ -15,10 +15,14 @@ const editor = { executeEdits: jest.fn(), updateOptions: jest.fn(), setSelection: jest.fn(), + setPosition: jest.fn(), createDecorationsCollection: jest.fn(), getValue: jest.fn().mockReturnValue(''), - getModel: jest.fn().mockReturnValue({}), - getPosition: jest.fn(), + getModel: jest.fn().mockReturnValue({ + getOffsetAt: jest.fn().mockReturnValue(0), + getWordUntilPosition: jest.fn().mockReturnValue(''), + }), + getPosition: jest.fn().mockReturnValue({}), trigger: jest.fn(), } @@ -78,9 +82,10 @@ export const monaco = { languages, Selection: jest.fn().mockImplementation(() => ({})), editor: { + ...editor, colorize: jest.fn().mockImplementation((data) => Promise.resolve(data)), defineTheme: jest.fn(), - setTheme: jest.fn() + setTheme: jest.fn(), }, Range: monacoEditor.Range } diff --git a/redisinsight/ui/src/pages/search/SearchPage.spec.tsx b/redisinsight/ui/src/pages/search/SearchPage.spec.tsx index f48f5729ff..50a53c0b27 100644 --- a/redisinsight/ui/src/pages/search/SearchPage.spec.tsx +++ b/redisinsight/ui/src/pages/search/SearchPage.spec.tsx @@ -1,10 +1,76 @@ import React from 'react' -import { render, screen } from 'uiSrc/utils/test-utils' +import { cloneDeep } from 'lodash' +import { cleanup, fireEvent, mockedStore, render, screen } from 'uiSrc/utils/test-utils' +import { loadWBHistory, sendWBCommand } from 'uiSrc/slices/workbench/wb-results' +import { setDbIndexState } from 'uiSrc/slices/app/context' +import { sendPageViewTelemetry, TelemetryPageView } from 'uiSrc/telemetry' import SearchPage from './SearchPage' +jest.mock('uiSrc/slices/app/context', () => ({ + ...jest.requireActual('uiSrc/slices/app/context'), + appContextSearchAndQuery: jest.fn().mockReturnValue({ + script: 'value', + panelSizes: { vertical: 100 } + }) +})) + +jest.mock('uiSrc/slices/instances/instances', () => ({ + ...jest.requireActual('uiSrc/slices/instances/instances'), + connectedInstanceSelector: jest.fn().mockReturnValue({ + name: 'db_name', + }), +})) + +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), + sendPageViewTelemetry: jest.fn() +})) + +let store: typeof mockedStore +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +/** + * SearchPage tests + * + * @group component + */ describe('SearchPage', () => { it('should render', () => { expect(render()).toBeTruthy() }) + + it('should send page event', () => { + const sendPageViewTelemetryMock = jest.fn(); + (sendPageViewTelemetry as jest.Mock).mockImplementation(() => sendPageViewTelemetryMock) + + render() + + expect(sendPageViewTelemetry).toBeCalledWith({ + name: TelemetryPageView.SEARCH_AND_QUERY_PAGE, + eventData: { + databaseId: 'instanceId' + } + }) + }) + + it('should call proper actions on submit', () => { + render() + + fireEvent.click(screen.getByTestId('btn-submit')) + + expect(store.getActions()).toEqual([ + loadWBHistory(), + sendWBCommand({ + commandId: expect.any(String), + commands: ['value'] + }), + setDbIndexState(true) + ]) + }) }) diff --git a/redisinsight/ui/src/pages/search/SearchPage.tsx b/redisinsight/ui/src/pages/search/SearchPage.tsx index bf6116641c..f4e2562541 100644 --- a/redisinsight/ui/src/pages/search/SearchPage.tsx +++ b/redisinsight/ui/src/pages/search/SearchPage.tsx @@ -53,7 +53,9 @@ const SearchPage = () => { const sendPageView = (instanceId: string) => { sendPageViewTelemetry({ name: TelemetryPageView.SEARCH_AND_QUERY_PAGE, - databaseId: instanceId + eventData: { + databaseId: instanceId + } }) setIsPageViewSent(true) } diff --git a/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.spec.tsx b/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.spec.tsx index a0635f0dde..7c3eb91e4c 100644 --- a/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.spec.tsx +++ b/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.spec.tsx @@ -1,10 +1,78 @@ import React from 'react' -import { render, screen } from 'uiSrc/utils/test-utils' +import { cloneDeep } from 'lodash' +import { cleanup, mockedStore, render, fireEvent, screen } from 'uiSrc/utils/test-utils' +import { loadList } from 'uiSrc/slices/browser/redisearch' +import { changeSQActiveRunQueryMode } from 'uiSrc/slices/search/searchAndQuery' +import { RunQueryMode } from 'uiSrc/slices/interfaces' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import QueryWrapper from './QueryWrapper' +jest.mock('uiSrc/slices/instances/instances', () => ({ + ...jest.requireActual('uiSrc/slices/instances/instances'), + connectedInstanceSelector: jest.fn().mockReturnValue({ + id: '123', + connectionType: 'STANDALONE', + db: 0, + }), +})) + +jest.mock('uiSrc/slices/app/context', () => ({ + ...jest.requireActual('uiSrc/slices/app/context'), + appContextSearchAndQuery: jest.fn().mockReturnValue({ + script: 'value' + }) +})) + +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) + +let store: typeof mockedStore +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + describe('Query', () => { it('should render', () => { expect(render()).toBeTruthy() }) + + it('should fetch list of indexes', () => { + render() + + expect(store.getActions()).toEqual([loadList()]) + }) + + it('should call proper actions after change mode', () => { + render() + + fireEvent.click(screen.getByTestId('btn-change-mode')) + + expect(store.getActions()).toEqual([loadList(), changeSQActiveRunQueryMode(RunQueryMode.Raw)]) + }) + + it('should call proper actions after submit', () => { + const sendEventTelemetryMock = jest.fn(); + (sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock) + + const onSubmit = jest.fn() + render() + + fireEvent.click(screen.getByTestId('btn-submit')) + + expect(onSubmit).toBeCalledWith('value', undefined, { mode: RunQueryMode.ASCII }) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.SEARCH_COMMAND_SUBMITTED, + eventData: { + databaseId: 'instanceId', + mode: RunQueryMode.ASCII, + command: 'value' + } + }) + }) }) diff --git a/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.tsx b/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.tsx index 9850d79545..a52d5e7768 100644 --- a/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.tsx +++ b/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.tsx @@ -6,12 +6,15 @@ import { QueryActions, QueryTutorials } from 'uiSrc/components/query' import { RunQueryMode } from 'uiSrc/slices/interfaces' import { CodeButtonParams } from 'uiSrc/constants' -import { Nullable } from 'uiSrc/utils' +import { getCommandsFromQuery, Nullable } from 'uiSrc/utils' import { changeSQActiveRunQueryMode, searchAndQuerySelector } from 'uiSrc/slices/search/searchAndQuery' import { appContextSearchAndQuery, setSQVerticalScript } from 'uiSrc/slices/app/context' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { fetchRedisearchListAction, redisearchListSelector } from 'uiSrc/slices/browser/redisearch' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' +import { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands' +import { SUPPORTED_COMMANDS_LIST } from 'uiSrc/pages/search/components/query/constants' +import { SearchCommand } from 'uiSrc/pages/search/types' import { TUTORIALS } from './constants' import Query from '../query' @@ -33,11 +36,18 @@ const QueryWrapper = (props: Props) => { const { script: scriptContext } = useSelector(appContextSearchAndQuery) const { activeRunQueryMode } = useSelector(searchAndQuerySelector) const { data: indexes = [] } = useSelector(redisearchListSelector) + const { spec: REDIS_COMMANDS_SPEC, commandsArray } = useSelector(appRedisCommandsSelector) + const [value, setValue] = useState(scriptContext) const input = useRef(null) const scriptRef = useRef('') + const SUPPORTED_COMMANDS = SUPPORTED_COMMANDS_LIST.map((name) => ({ + ...REDIS_COMMANDS_SPEC[name], + name + })) as unknown as SearchCommand[] + const { instanceId } = useParams<{ instanceId: string }>() const dispatch = useDispatch() @@ -66,14 +76,17 @@ const QueryWrapper = (props: Props) => { } const handleSubmit = () => { - onSubmit(value.split('\n').join(' '), undefined, { mode: activeRunQueryMode }) + const val = value.split('\n').join(' ') + if (!val) return + + onSubmit(val, undefined, { mode: activeRunQueryMode }) sendEventTelemetry({ event: TelemetryEvent.SEARCH_COMMAND_SUBMITTED, eventData: { databaseId: instanceId, mode: activeRunQueryMode, // TODO sanitize user query - command: value + command: getCommandsFromQuery(value, commandsArray) || '' } }) } @@ -92,7 +105,13 @@ const QueryWrapper = (props: Props) => { data-testid="main-input-container-area" >
- +
diff --git a/redisinsight/ui/src/pages/search/components/query/Query.spec.tsx b/redisinsight/ui/src/pages/search/components/query/Query.spec.tsx new file mode 100644 index 0000000000..1f57ca4c4a --- /dev/null +++ b/redisinsight/ui/src/pages/search/components/query/Query.spec.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import { render } from 'uiSrc/utils/test-utils' + +import Query from './Query' + +describe('Query', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/search/components/query/Query.tsx b/redisinsight/ui/src/pages/search/components/query/Query.tsx index 6fc1fdd94b..81b309baaf 100644 --- a/redisinsight/ui/src/pages/search/components/query/Query.tsx +++ b/redisinsight/ui/src/pages/search/components/query/Query.tsx @@ -1,10 +1,9 @@ import React, { useContext, useEffect, useRef, useState } from 'react' import MonacoEditor, { monaco as monacoEditor } from 'react-monaco-editor' -import { useDispatch, useSelector } from 'react-redux' +import { useDispatch } from 'react-redux' -import { MonacoLanguage, Theme } from 'uiSrc/constants' +import { ICommands, MonacoLanguage, Theme } from 'uiSrc/constants' import { ThemeContext } from 'uiSrc/contexts/themeContext' -import { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands' import { Nullable } from 'uiSrc/utils' import { IEditorMount } from 'uiSrc/pages/workbench/interfaces' import { @@ -21,7 +20,7 @@ import { fetchRedisearchInfoAction } from 'uiSrc/slices/browser/redisearch' import { getRediSearchMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokens' import { installRedisearchTheme, RedisearchMonacoTheme } from 'uiSrc/utils/monaco/monacoThemes' import { useDebouncedEffect } from 'uiSrc/services' -import { SUPPORTED_COMMANDS_LIST, options, DefinedArgumentName } from './constants' +import { options, DefinedArgumentName } from './constants' import { getFieldsSuggestions, getIndexesSuggestions, @@ -35,20 +34,16 @@ export interface Props { value: string onChange: (val: string) => void indexes: RedisResponseBuffer[] + supportedCommands?: SearchCommand[] + commandsSpec?: ICommands } const Query = (props: Props) => { - const { value, onChange, indexes } = props - const { spec: REDIS_COMMANDS_SPEC } = useSelector(appRedisCommandsSelector) + const { value, onChange, indexes, supportedCommands = [], commandsSpec } = props const [selectedCommand, setSelectedCommand] = useState('') const [selectedIndex, setSelectedIndex] = useState('') - const SUPPORTED_COMMANDS = SUPPORTED_COMMANDS_LIST.map((name) => ({ - ...REDIS_COMMANDS_SPEC[name], - name - })) as unknown as SearchCommand[] - const monacoObjects = useRef>(null) const disposeCompletionItemProvider = useRef(() => {}) const disposeSignatureHelpProvider = useRef(() => {}) @@ -80,7 +75,7 @@ const Query = (props: Props) => { useEffect(() => { monacoEditor.languages.setMonarchTokensProvider( MonacoLanguage.RediSearch, - getRediSearchMonarchTokensProvider(SUPPORTED_COMMANDS, selectedCommand) + getRediSearchMonarchTokensProvider(supportedCommands, selectedCommand) ) }, [selectedCommand]) @@ -118,7 +113,7 @@ const Query = (props: Props) => { monaco.languages.setMonarchTokensProvider( MonacoLanguage.RediSearch, - getRediSearchMonarchTokensProvider(SUPPORTED_COMMANDS) + getRediSearchMonarchTokensProvider(supportedCommands) ) disposeSignatureHelpProvider.current?.() @@ -209,17 +204,18 @@ const Query = (props: Props) => { const range = getRange(position, word) const { args, isCursorInQuotes, prevCursorChar, nextCursorChar } = splitQueryByArgs(value, offset) + const allArgs = args.flat() const [beforeOffsetArgs, [currentOffsetArg]] = args const [firstArg, ...prevArgs] = beforeOffsetArgs const commandName = (firstArg || currentOffsetArg)?.toUpperCase() - const command = REDIS_COMMANDS_SPEC[commandName] as unknown as SearchCommand + const command = commandsSpec?.[commandName] as unknown as SearchCommand - const isCommandSuppurted = SUPPORTED_COMMANDS.some(({ name }) => commandName === name) + const isCommandSuppurted = supportedCommands.some(({ name }) => commandName === name) if (command && !isCommandSuppurted) return asSuggestionsRef([]) if (!command && position.lineNumber === 1 && position.column === 1) { - return getCommandsSuggestions(SUPPORTED_COMMANDS, range) + return getCommandsSuggestions(supportedCommands, range) } if (!command) { diff --git a/redisinsight/ui/src/pages/search/utils/tests/monaco.spec.ts b/redisinsight/ui/src/pages/search/utils/tests/monaco.spec.ts new file mode 100644 index 0000000000..70dc43d5ad --- /dev/null +++ b/redisinsight/ui/src/pages/search/utils/tests/monaco.spec.ts @@ -0,0 +1,60 @@ +import { getRediSearchSignutureProvider } from 'uiSrc/pages/search/utils' +import { MOCKED_SUPPORTED_COMMANDS } from 'uiSrc/pages/search/utils/tests/mocks' +import { SearchCommand } from 'uiSrc/pages/search/types' + +const ftAggregateCommand = MOCKED_SUPPORTED_COMMANDS['FT.AGGREGATE'] + +const getRediSearchSignutureProviderTests = [ + { + input: { + isOpen: false, + currentArg: {}, + parent: {} + }, + result: null + }, + { + input: { + isOpen: true, + currentArg: ftAggregateCommand.arguments.find(({ name }) => name === 'groupby') as SearchCommand, + parent: null + }, + result: { + dispose: expect.any(Function), + value: { + activeParameter: 0, + activeSignature: 0, + signatures: [{ + label: '', + parameters: [{ label: 'nargs' }] + }] + } + } + }, + { + input: { + isOpen: true, + currentArg: { name: 'expression' }, + parent: ftAggregateCommand.arguments.find(({ name }) => name === 'apply') as SearchCommand + }, + result: { + dispose: expect.any(Function), + value: { + activeParameter: 0, + activeSignature: 0, + signatures: [{ + label: 'APPLY expression AS name', + parameters: [{ label: 'expression' }] + }] + } + } + } +] + +describe('getRediSearchSignutureProvider', () => { + it.each(getRediSearchSignutureProviderTests)('should properly return result', ({ input, result }) => { + const testResult = getRediSearchSignutureProvider(input) + + expect(result).toEqual(testResult) + }) +}) diff --git a/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts b/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts index f550210ed4..584a587222 100644 --- a/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts +++ b/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts @@ -1,5 +1,6 @@ -import { findCurrentArgument } from 'uiSrc/pages/search/utils' +import { addOwnTokenToArgs, findCurrentArgument, generateDetail, splitQueryByArgs } from 'uiSrc/pages/search/utils' import { SearchCommand } from 'uiSrc/pages/search/types' +import { Maybe } from 'uiSrc/utils' import { MOCKED_SUPPORTED_COMMANDS } from './mocks' const ftSearchCommand = MOCKED_SUPPORTED_COMMANDS['FT.SEARCH'] @@ -190,6 +191,36 @@ const ftAggreageTests = [ parent: expect.any(Object) } }, + { + args: ['index', '"query"', 'LOAD', '4'], + result: { + stopArg: { multiple: true, name: 'field', type: 'string' }, + append: [], + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'LOAD', '4', '1', '2', '3'], + result: { + stopArg: { multiple: true, name: 'field', type: 'string' }, + append: [], + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'LOAD', '4', '1', '2', '3', '4'], + result: { + stopArg: undefined, + append: [], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, ] const ftSearchTests = [ @@ -316,6 +347,112 @@ const ftSearchTests = [ parent: expect.any(Object) } }, + { + args: ['', '', 'RETURN', '2', 'iden'], + result: { + stopArg: { + name: 'property', + type: 'string', + token: 'AS', + optional: true + }, + append: [ + { + name: 'property', + type: 'string', + token: 'AS', + optional: true + } + ], + isBlocked: false, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'RETURN', '2', 'iden', 'iden'], + result: { + stopArg: { + name: 'property', + type: 'string', + token: 'AS', + optional: true + }, + append: [ + { + name: 'property', + type: 'string', + token: 'AS', + optional: true + } + ], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'RETURN', '2', 'iden', 'iden', 'AS'], + result: { + stopArg: { + name: 'property', + type: 'string', + token: 'AS', + optional: true + }, + append: [], + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'SORTBY', 'f'], + result: { + stopArg: { + name: 'order', + type: 'oneof', + optional: true, + arguments: [ + { + name: 'asc', + type: 'pure-token', + token: 'ASC' + }, + { + name: 'desc', + type: 'pure-token', + token: 'DESC' + } + ] + }, + append: [ + { + name: 'asc', + type: 'pure-token', + token: 'ASC' + }, + { + name: 'desc', + type: 'pure-token', + token: 'DESC' + } + ], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'SORTBY', 'f', 'DESC'], + result: { + stopArg: undefined, + append: [], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, ] describe('findCurrentArgument', () => { @@ -326,7 +463,7 @@ describe('findCurrentArgument', () => { ftAggregateCommand.arguments as SearchCommand[], args ) - expect(result).toEqual(testResult) + expect(testResult).toEqual(result) }) }) }) @@ -338,8 +475,100 @@ describe('findCurrentArgument', () => { ftSearchCommand.arguments as SearchCommand[], args ) - expect(result).toEqual(testResult) + expect(testResult).toEqual(result) }) }) }) }) + +const splitQueryByArgsTests: Array<{ + input: [string, number?] + result: any +}> = [ + { + input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS'], + result: { + args: [[], ['FT.SEARCH', '"idx:bicycle"', '""', 'WITHSORTKEYS']], + isCursorInQuotes: false, + nextCursorChar: 'F', + prevCursorChar: undefined + } + }, + { + input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS', 17], + result: { + args: [['FT.SEARCH'], ['"idx:bicycle"', '""', 'WITHSORTKEYS']], + isCursorInQuotes: true, + nextCursorChar: 'c', + prevCursorChar: 'i' + } + }, + { + input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS', 39], + result: { + args: [['FT.SEARCH', '"idx:bicycle"', '""'], ['WITHSORTKEYS']], + isCursorInQuotes: false, + nextCursorChar: undefined, + prevCursorChar: 'S' + } + }, + { + input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS ', 40], + result: { + args: [['FT.SEARCH', '"idx:bicycle"', '""', 'WITHSORTKEYS'], []], + isCursorInQuotes: false, + nextCursorChar: undefined, + prevCursorChar: ' ' + } + }, + { + input: ['FT.SEARCH "idx:bicycle \\" \\"" "" WITHSORTKEYS ', 46], + result: { + args: [['FT.SEARCH', '"idx:bicycle " ""', '""', 'WITHSORTKEYS'], []], + isCursorInQuotes: false, + nextCursorChar: undefined, + prevCursorChar: ' ' + } + } +] + +describe('splitQueryByArgs', () => { + it.each(splitQueryByArgsTests)('should return for %input proper result', ({ input, result }) => { + const testResult = splitQueryByArgs(...input) + expect(testResult).toEqual(result) + }) +}) + +const generateDetailTests: Array<{ input: Maybe, result: any }> = [ + { + input: ftSearchCommand.arguments.find(({ name }) => name === 'nocontent') as SearchCommand, + result: 'NOCONTENT' + }, + { + input: ftSearchCommand.arguments.find(({ name }) => name === 'filter') as SearchCommand, + result: 'FILTER numeric_field min max' + }, + { + input: ftSearchCommand.arguments.find(({ name }) => name === 'geo_filter') as SearchCommand, + result: 'GEOFILTER geo_field lon lat radius m | km | mi | ft' + }, + { + input: ftAggregateCommand.arguments.find(({ name }) => name === 'groupby') as SearchCommand, + result: 'GROUPBY nargs property [property ...] [REDUCE function nargs arg [arg ...] [AS name] [REDUCE function nargs arg [arg ...] [AS name] ...]]' + }, +] + +describe('generateDetail', () => { + it.each(generateDetailTests)('should return for %input proper result', ({ input, result }) => { + const testResult = generateDetail(input) + expect(testResult).toEqual(result) + }) +}) + +describe('addOwnTokenToArgs', () => { + it('should add FT.SEARCH to args', () => { + const result = addOwnTokenToArgs('FT.SEARCH', { arguments: [] }) + + expect({ arguments: [{ token: 'FT.SEARCH', type: 'pure-token' }] }).toEqual(result) + }) +}) diff --git a/redisinsight/ui/src/slices/tests/search/searchAndQuery.spec.ts b/redisinsight/ui/src/slices/tests/search/searchAndQuery.spec.ts new file mode 100644 index 0000000000..29af826a88 --- /dev/null +++ b/redisinsight/ui/src/slices/tests/search/searchAndQuery.spec.ts @@ -0,0 +1,58 @@ +import { cloneDeep } from 'lodash' + +import { + cleanup, + initialStateDefault, + mockedStore, +} from 'uiSrc/utils/test-utils' + +import { RunQueryMode } from 'uiSrc/slices/interfaces' +import reducer, { + initialState, + searchAndQuerySelector, + changeSQActiveRunQueryMode +} from '../../search/searchAndQuery' + +jest.mock('uiSrc/services', () => ({ + ...jest.requireActual('uiSrc/services'), +})) + +let store: typeof mockedStore +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +describe('slices', () => { + describe('reducer, actions and selectors', () => { + it('should return the initial state on first run', () => { + // Arrange + const nextState = initialState + + // Act + const result = reducer(undefined, {} as any) + + // Assert + expect(result).toEqual(nextState) + }) + }) + + describe('changeSQActiveRunQueryMode', () => { + it('should properly set mode', () => { + const state = { + ...initialState, + activeRunQueryMode: RunQueryMode.Raw + } + + const nextState = reducer(initialState, changeSQActiveRunQueryMode(RunQueryMode.Raw)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + search: { query: nextState }, + }) + + expect(searchAndQuerySelector(rootState)).toEqual(state) + }) + }) +}) From 93c0e5ce3c235659021c1ec13b76a26173a23946 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Thu, 22 Aug 2024 11:03:01 +0200 Subject: [PATCH 033/112] #RI-5684 - add ft.profile and ft.explain to supported commands --- .../pages/search/components/query/Query.tsx | 75 ++++++++----- .../search/components/query/constants.ts | 2 +- .../search/components/query/utils.spec.ts | 39 +++++++ .../pages/search/components/query/utils.ts | 103 +++++++++++++++--- .../search/{utils/tests => mocks}/mocks.ts | 0 redisinsight/ui/src/pages/search/types.ts | 19 +++- .../ui/src/pages/search/utils/query.ts | 25 +++++ .../pages/search/utils/tests/query.spec.ts | 23 +++- .../monaco/monarchTokens/redisearchTokens.ts | 15 ++- 9 files changed, 249 insertions(+), 52 deletions(-) create mode 100644 redisinsight/ui/src/pages/search/components/query/utils.spec.ts rename redisinsight/ui/src/pages/search/{utils/tests => mocks}/mocks.ts (100%) diff --git a/redisinsight/ui/src/pages/search/components/query/Query.tsx b/redisinsight/ui/src/pages/search/components/query/Query.tsx index 81b309baaf..9db6aabfb3 100644 --- a/redisinsight/ui/src/pages/search/components/query/Query.tsx +++ b/redisinsight/ui/src/pages/search/components/query/Query.tsx @@ -8,13 +8,13 @@ import { Nullable } from 'uiSrc/utils' import { IEditorMount } from 'uiSrc/pages/workbench/interfaces' import { addOwnTokenToArgs, - findCurrentArgument, + findCurrentArgInQuery, findCurrentArgument, getRange, getRediSearchSignutureProvider, setCursorPositionAtTheEnd, splitQueryByArgs } from 'uiSrc/pages/search/utils' -import { SearchCommand } from 'uiSrc/pages/search/types' +import { CommandContext, CursorContext, SearchCommand } from 'uiSrc/pages/search/types' import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' import { fetchRedisearchInfoAction } from 'uiSrc/slices/browser/redisearch' import { getRediSearchMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokens' @@ -25,9 +25,9 @@ import { getFieldsSuggestions, getIndexesSuggestions, asSuggestionsRef, - getMandatoryArgumentSuggestions, - getOptionalSuggestions, - getCommandsSuggestions, isIndexComplete + getCommandsSuggestions, + isIndexComplete, + getGeneralSuggestions } from './utils' export interface Props { @@ -226,43 +226,64 @@ const Query = (props: Props) => { setSelectedIndex(allArgs[1] || '') setSelectedCommand(commandName) - // cover query - if (command?.arguments?.[prevArgs.length]?.name === DefinedArgumentName.query) { - updateHelpWidget(true, addOwnTokenToArgs(commandName, command), command?.arguments?.[prevArgs.length]) - - if (prevCursorChar === '@') { - helpWidgetRef.current.isOpen = false - return asSuggestionsRef(getFieldsSuggestions(attributesRef.current, range), false) - } + const currentCommandArg = findCurrentArgInQuery(prevArgs, command) - return asSuggestionsRef([], false) + // cover index + if (currentCommandArg?.name === DefinedArgumentName.index) { + updateHelpWidget(true, addOwnTokenToArgs(commandName, command), currentCommandArg) + return getIndexSuggestions(command, prevArgs.length, currentOffsetArg, range) } - // cover index - if (command?.arguments?.[prevArgs.length]?.name === DefinedArgumentName.index) { - updateHelpWidget(true, addOwnTokenToArgs(commandName, command), command?.arguments?.[prevArgs.length]) + // cover query + if (currentCommandArg?.name === DefinedArgumentName.query) { + updateHelpWidget(true, addOwnTokenToArgs(commandName, command), currentCommandArg) - // we do not suggest indexes if there is something next - if (currentOffsetArg) return asSuggestionsRef([], false) - if (indexesRef.current.length) return asSuggestionsRef(getIndexesSuggestions(indexesRef.current, range)) - return asSuggestionsRef([]) + return getQuerySuggestions(prevCursorChar, range) } if (isCursorInQuotes || nextCursorChar?.trim()) return asSuggestionsRef([]) - if (prevArgs.length < 2) return asSuggestionsRef([]) if ((prevCursorChar?.trim() || isCursorInQuotes) && isEscapedSuggestions.current) return asSuggestionsRef([]) isEscapedSuggestions.current = false - const foundArg = findCurrentArgument(command?.arguments || [], prevArgs) - updateHelpWidget(!!foundArg?.stopArg, foundArg?.parent, foundArg?.stopArg) + const cursorContext: CursorContext = { isCursorInQuotes, prevCursorChar, nextCursorChar, currentOffsetArg, range } + const commandContext: CommandContext = { commandName, command, prevArgs, allArgs, currentCommandArg } + + const { suggestions, forceHide, helpWidgetData } = getGeneralSuggestions( + commandContext, + cursorContext, + attributesRef.current + ) + + if (helpWidgetData) updateHelpWidget(helpWidgetData.isOpen, helpWidgetData.parent, helpWidgetData.currentArg) + return asSuggestionsRef(suggestions, forceHide) + } - if (foundArg && !foundArg.isComplete) return getMandatoryArgumentSuggestions(foundArg, attributesRef.current, range) - if (!foundArg || foundArg.isComplete) { - return getOptionalSuggestions(command, foundArg, allArgs, range, currentOffsetArg) + const getIndexSuggestions = ( + command: SearchCommand, + prevArgsLength: number, + currentOffsetArg: Nullable, + range: monacoEditor.IRange + ) => { + if (currentOffsetArg) return asSuggestionsRef([], false) + if (indexesRef.current.length) { + const isNextArgQuery = command?.arguments?.[prevArgsLength + 1]?.name === DefinedArgumentName.query + return asSuggestionsRef(getIndexesSuggestions(indexesRef.current, range, isNextArgQuery)) } return asSuggestionsRef([]) } + const getQuerySuggestions = ( + prevCursorChar: string, + range: monacoEditor.IRange + ) => { + if (prevCursorChar === '@') { + helpWidgetRef.current.isOpen = false + return asSuggestionsRef(getFieldsSuggestions(attributesRef.current, range), false) + } + + return asSuggestionsRef([], false) + } + return ( ({ + currentOffsetArg: undefined, + isCursorInQuotes: false, + nextCursorChar: undefined, + prevCursorChar: ' ', + range: {} +}) + +const getGeneralSuggestionsTests = [ + { + input: { + commandContext: { + allArgs: ['FT.AGGREGATE', '""', '""'], + command: ftAggregate, + commandName: 'FT.AGGREGATE', + currentCommandArg: null, + prevArgs: ['""', '""'] + }, + cursorContext: getCursorContext() + }, + result: { + helpWidgetData: expect.any(Object), + suggestions: expect.any(Array) + } + } +] + +describe('getGeneralSuggestions', () => { + it.each(getGeneralSuggestionsTests)('dawd', ({ input, result }) => { + const testResult = getGeneralSuggestions(input.commandContext, input.cursorContext, []) + + expect(testResult).toEqual(result) + }) +}) diff --git a/redisinsight/ui/src/pages/search/components/query/utils.ts b/redisinsight/ui/src/pages/search/components/query/utils.ts index 1df71cd5a2..ba133ec57f 100644 --- a/redisinsight/ui/src/pages/search/components/query/utils.ts +++ b/redisinsight/ui/src/pages/search/components/query/utils.ts @@ -2,8 +2,8 @@ import { monaco } from 'react-monaco-editor' import * as monacoEditor from 'monaco-editor' import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' import { bufferToString, formatLongName, getCommandMarkdown, Nullable } from 'uiSrc/utils' -import { buildSuggestion, generateDetail } from 'uiSrc/pages/search/utils' -import { FoundCommandArgument, SearchCommand } from 'uiSrc/pages/search/types' +import { addOwnTokenToArgs, buildSuggestion, findCurrentArgument, generateDetail } from 'uiSrc/pages/search/utils' +import { CommandContext, CursorContext, FoundCommandArgument, SearchCommand } from 'uiSrc/pages/search/types' import { DefinedArgumentName } from 'uiSrc/pages/search/components/query/constants' export const asSuggestionsRef = (suggestions: monacoEditor.languages.CompletionItem[], forceHide = true) => ({ @@ -11,14 +11,15 @@ export const asSuggestionsRef = (suggestions: monacoEditor.languages.CompletionI forceHide }) -export const getIndexesSuggestions = (indexes: RedisResponseBuffer[], range: monaco.IRange) => +export const getIndexesSuggestions = (indexes: RedisResponseBuffer[], range: monaco.IRange, nextQoutes = true) => indexes.map((index) => { const value = formatLongName(bufferToString(index)) + const insertQueryQuotes = nextQoutes ? ' "$1"' : '' return { label: value || ' ', kind: monacoEditor.languages.CompletionItemKind.Snippet, - insertText: `"${value}" "$1" `, + insertText: `"${value}"${insertQueryQuotes} `, insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, range, detail: value || ' ', @@ -52,25 +53,25 @@ export const getMandatoryArgumentSuggestions = ( foundArg: FoundCommandArgument, fields: any[], range: monaco.IRange -) => { +): monacoEditor.languages.CompletionItem[] => { if (foundArg.stopArg?.name === DefinedArgumentName.field) { - if (!fields.length) asSuggestionsRef([]) - return asSuggestionsRef(getFieldsSuggestions(fields, range, true)) + if (!fields.length) return [] + return getFieldsSuggestions(fields, range, true) } - if (foundArg.isBlocked) return asSuggestionsRef([]) + if (foundArg.isBlocked) return [] if (foundArg.append?.length) { - return asSuggestionsRef(foundArg.append.map((arg: any) => buildSuggestion(arg, range, { + return foundArg.append.map((arg: any) => buildSuggestion(arg, range, { kind: monacoEditor.languages.CompletionItemKind.Property, detail: generateDetail(foundArg?.parent) - }))) + })) } - return asSuggestionsRef([]) + return [] } -export const getOptionalSuggestions = ( - command: SearchCommand, +export const getCommandSuggestions = ( + firstLevelArgs: SearchCommand[], foundArg: Nullable, allArgs: string[], range: monaco.IRange, @@ -78,14 +79,13 @@ export const getOptionalSuggestions = ( ) => { const appendCommands = foundArg?.append ?? [] - return asSuggestionsRef([ + return [ ...appendCommands.map((arg) => buildSuggestion(arg, range, { sortText: 'a', kind: monacoEditor.languages.CompletionItemKind.Property, detail: generateDetail(foundArg?.parent) })), - ...(command?.arguments || []) - .filter((arg) => arg.optional) + ...firstLevelArgs .filter((arg) => arg.multiple || !(currentArg !== arg.token && allArgs.includes(arg.token || arg.arguments?.[0]?.token || ''))) .map((arg) => buildSuggestion(arg, range, { @@ -93,7 +93,76 @@ export const getOptionalSuggestions = ( kind: monacoEditor.languages.CompletionItemKind.Reference, detail: generateDetail(arg) })) - ]) + ] +} + +export const getGeneralSuggestions = ( + commandContext: CommandContext, + cursorContext: CursorContext, + fields: any[], +): { + suggestions: monacoEditor.languages.CompletionItem[], + forceHide?: boolean + helpWidgetData?: any +} => { + const { command, prevArgs } = commandContext + const { range } = cursorContext + const foundArg = findCurrentArgument(command?.arguments || [], prevArgs) + + if (foundArg && !foundArg.isComplete) { + return { + suggestions: getMandatoryArgumentSuggestions(foundArg, fields, range), + helpWidgetData: { + isOpen: !!foundArg?.stopArg, + parent: foundArg?.parent, + currentArg: foundArg?.stopArg + } + } + } + + return getNextSuggestions(commandContext, cursorContext, foundArg) +} + +export const getNextSuggestions = ( + { command, currentCommandArg, prevArgs, allArgs }: CommandContext, + { currentOffsetArg, range }: CursorContext, + foundArg: Nullable +) => { + if (!command) return { suggestions: [] } + if (foundArg && !foundArg.isComplete) return { suggestions: [], helpWidgetData: { isOpen: false } } + + const parentArgIndex = command.arguments + ?.findIndex(({ name }) => name === foundArg?.parent?.name) || -1 + const currentArgIndex = parentArgIndex > -1 ? parentArgIndex : prevArgs.length - 1 + const nextMandatoryIndex = command.arguments + ?.findIndex(({ optional }, i) => !optional && i > currentArgIndex) || -1 + + const nextOptionalArgs = ( + nextMandatoryIndex > -1 + ? command.arguments?.slice(currentArgIndex + 1, nextMandatoryIndex) + : command.arguments?.filter(({ optional }) => optional) + ) || [] + const nextMandatoryArg = command.arguments?.[nextMandatoryIndex] + + if (nextMandatoryArg?.token) { + nextOptionalArgs.unshift(nextMandatoryArg) + } + + if (nextMandatoryArg && !nextMandatoryArg.token) { + return { + helpWidgetData: { + isOpen: !!currentCommandArg, + parent: addOwnTokenToArgs(command.name!, command), + currentArg: nextMandatoryArg + }, + suggestions: [] + } + } + + return { + suggestions: getCommandSuggestions(nextOptionalArgs, foundArg, allArgs, range, currentOffsetArg), + helpWidgetData: { isOpen: false } + } } export const isIndexComplete = (index: string) => { diff --git a/redisinsight/ui/src/pages/search/utils/tests/mocks.ts b/redisinsight/ui/src/pages/search/mocks/mocks.ts similarity index 100% rename from redisinsight/ui/src/pages/search/utils/tests/mocks.ts rename to redisinsight/ui/src/pages/search/mocks/mocks.ts diff --git a/redisinsight/ui/src/pages/search/types.ts b/redisinsight/ui/src/pages/search/types.ts index d4973f1c0b..d4eca49253 100644 --- a/redisinsight/ui/src/pages/search/types.ts +++ b/redisinsight/ui/src/pages/search/types.ts @@ -1,4 +1,5 @@ -import { Maybe } from 'uiSrc/utils' +import { monaco as monacoEditor } from 'react-monaco-editor' +import { Maybe, Nullable } from 'uiSrc/utils' export enum TokenType { PureToken = 'pure-token', @@ -30,3 +31,19 @@ export interface FoundCommandArgument { append: Maybe parent: Maybe } + +export interface CommandContext { + commandName: string + command: Maybe + prevArgs: string[] + allArgs: string[] + currentCommandArg: Nullable +} + +export interface CursorContext { + isCursorInQuotes: boolean + prevCursorChar: string + nextCursorChar: string + currentOffsetArg: any + range: monacoEditor.IRange +} diff --git a/redisinsight/ui/src/pages/search/utils/query.ts b/redisinsight/ui/src/pages/search/utils/query.ts index 703d651b31..17a8ad5ef2 100644 --- a/redisinsight/ui/src/pages/search/utils/query.ts +++ b/redisinsight/ui/src/pages/search/utils/query.ts @@ -108,6 +108,13 @@ export const findCurrentArgument = ( } } + if (args[prev.length] && !args[prev.length].optional) { + return { + ...getArgumentSuggestions([], args.slice(prev.length)), + parent: undefined + } + } + return null } @@ -345,3 +352,21 @@ export const addOwnTokenToArgs = (token: string, command: SearchCommand) => { } return command } + +export const findCurrentArgInQuery = (args: string[], command: SearchCommand) => { + if (!command.arguments || !command.arguments.length) return null + + let argIndex = 0 + args.forEach((arg) => { + for (let i = argIndex; i < command.arguments!.length; i++) { + if (command.arguments![i]?.optional && command.arguments![i]?.token?.toUpperCase() !== arg?.toUpperCase()) { + continue + } + + argIndex = i + 1 + break + } + }) + + return command.arguments[argIndex] +} diff --git a/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts b/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts index 584a587222..bff290b4da 100644 --- a/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts +++ b/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts @@ -1,13 +1,22 @@ import { addOwnTokenToArgs, findCurrentArgument, generateDetail, splitQueryByArgs } from 'uiSrc/pages/search/utils' import { SearchCommand } from 'uiSrc/pages/search/types' import { Maybe } from 'uiSrc/utils' -import { MOCKED_SUPPORTED_COMMANDS } from './mocks' +import { MOCKED_SUPPORTED_COMMANDS } from '../../mocks/mocks' const ftSearchCommand = MOCKED_SUPPORTED_COMMANDS['FT.SEARCH'] const ftAggregateCommand = MOCKED_SUPPORTED_COMMANDS['FT.AGGREGATE'] const ftAggreageTests = [ - { args: [''], result: null }, + { + args: [''], + result: { + append: [], + isBlocked: true, + isComplete: false, + parent: undefined, + stopArg: { name: 'query', type: 'string' } + } + }, { args: ['', ''], result: null }, { args: ['index', '"query"', 'APPLY'], @@ -224,7 +233,15 @@ const ftAggreageTests = [ ] const ftSearchTests = [ - { args: [''], result: null }, + { + args: [''], + result: { + append: [], + isBlocked: true, + isComplete: false, + parent: undefined, + stopArg: { name: 'query', type: 'string' } } + }, { args: ['', ''], result: null }, { args: ['', '', 'SUMMARIZE'], diff --git a/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts b/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts index 2eb71dde4f..9d91952d2d 100644 --- a/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts +++ b/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts @@ -1,5 +1,6 @@ import { monaco as monacoEditor } from 'react-monaco-editor' import { SearchCommand, TokenType } from 'uiSrc/pages/search/types' +import { DefinedArgumentName } from 'uiSrc/pages/search/components/query/constants' const STRING_DOUBLE = 'string.double' @@ -41,6 +42,13 @@ const generateTokens = (command?: SearchCommand) => { return levels } +const isQueryAfterIndex = (command?: SearchCommand) => { + if (!command) return false + + const index = command.arguments?.findIndex(({ name }) => name === DefinedArgumentName.index) || -2 + return command.arguments?.[index + 1]?.name === DefinedArgumentName.query +} + export const getRediSearchMonarchTokensProvider = ( commands: SearchCommand[], command?: string @@ -49,6 +57,7 @@ export const getRediSearchMonarchTokensProvider = ( const keywords = generateKeywords(commands) const argTokens = generateTokens(currentCommand) + const isHighlightQuery = isQueryAfterIndex(currentCommand) return ( { @@ -86,9 +95,9 @@ export const getRediSearchMonarchTokensProvider = ( ], 'argument.block': argTokens.map((tokens, lvl) => [`(${tokens.join('|')})\\b`, `argument.block.${lvl}`]), index: [ - [/"([^"\\]|\\.)*"/, { token: 'index', next: '@query' }], - [/'([^'\\]|\\.)*'/, { token: 'index', next: '@query' }], - [/[a-zA-Z_]\w*/, { token: 'index', next: '@query' }], + [/"([^"\\]|\\.)*"/, { token: 'index', next: isHighlightQuery ? '@query' : '@root' }], + [/'([^'\\]|\\.)*'/, { token: 'index', next: isHighlightQuery ? '@query' : '@root' }], + [/[a-zA-Z_]\w*/, { token: 'index', next: isHighlightQuery ? '@query' : '@root' }], { include: 'root' } // Fallback to the root state if nothing matches ], query: [ From a70b25f1592d7452dd96aa0a878f3f1b5088ef5a Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Thu, 22 Aug 2024 11:54:25 +0200 Subject: [PATCH 034/112] #RI-5684 - start refactoring --- .../pages/search/components/query/Query.tsx | 100 +++++++++++++----- .../pages/search/components/query/utils.ts | 5 +- 2 files changed, 74 insertions(+), 31 deletions(-) diff --git a/redisinsight/ui/src/pages/search/components/query/Query.tsx b/redisinsight/ui/src/pages/search/components/query/Query.tsx index 9db6aabfb3..063511a729 100644 --- a/redisinsight/ui/src/pages/search/components/query/Query.tsx +++ b/redisinsight/ui/src/pages/search/components/query/Query.tsx @@ -14,7 +14,7 @@ import { setCursorPositionAtTheEnd, splitQueryByArgs } from 'uiSrc/pages/search/utils' -import { CommandContext, CursorContext, SearchCommand } from 'uiSrc/pages/search/types' +import { CommandContext, CursorContext, SearchCommand, TokenType } from 'uiSrc/pages/search/types' import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' import { fetchRedisearchInfoAction } from 'uiSrc/slices/browser/redisearch' import { getRediSearchMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokens' @@ -27,7 +27,7 @@ import { asSuggestionsRef, getCommandsSuggestions, isIndexComplete, - getGeneralSuggestions + getGeneralSuggestions, } from './utils' export interface Props { @@ -60,6 +60,12 @@ const Query = (props: Props) => { const attributesRef = useRef([]) const isEscapedSuggestions = useRef(false) + const COMMANDS_LIST = supportedCommands.map((command) => ({ + ...addOwnTokenToArgs(command.name!, command), + token: command.name!, + type: TokenType.Block + })) + const { theme } = useContext(ThemeContext) const dispatch = useDispatch() @@ -227,35 +233,73 @@ const Query = (props: Props) => { setSelectedCommand(commandName) const currentCommandArg = findCurrentArgInQuery(prevArgs, command) + const foundArg = findCurrentArgument(COMMANDS_LIST, beforeOffsetArgs) - // cover index - if (currentCommandArg?.name === DefinedArgumentName.index) { - updateHelpWidget(true, addOwnTokenToArgs(commandName, command), currentCommandArg) - return getIndexSuggestions(command, prevArgs.length, currentOffsetArg, range) - } - - // cover query - if (currentCommandArg?.name === DefinedArgumentName.query) { - updateHelpWidget(true, addOwnTokenToArgs(commandName, command), currentCommandArg) + console.log(foundArg) - return getQuerySuggestions(prevCursorChar, range) + switch (foundArg?.stopArg?.name) { + case DefinedArgumentName.index: { + updateHelpWidget(true, command, foundArg?.stopArg) + return getIndexSuggestions(command, prevArgs.length, currentOffsetArg, range) + } + case DefinedArgumentName.query: { + updateHelpWidget(true, command, foundArg?.stopArg) + return getQuerySuggestions(prevCursorChar, range) + } + default: { + console.log(foundArg) + const cursorContext: CursorContext = { isCursorInQuotes, prevCursorChar, nextCursorChar, currentOffsetArg, range } + const commandContext: CommandContext = { + commands: COMMANDS_LIST, + commandName, + command, + prevArgs: beforeOffsetArgs, + allArgs, + currentCommandArg: foundArg?.stopArg + } + + const { suggestions, forceHide, helpWidgetData } = getGeneralSuggestions( + foundArg, + commandContext, + cursorContext, + attributesRef.current + ) + + console.log(suggestions) + console.log(helpWidgetData) + + if (helpWidgetData) updateHelpWidget(helpWidgetData.isOpen, helpWidgetData.parent, helpWidgetData.currentArg) + return asSuggestionsRef(suggestions, forceHide) + } } - - if (isCursorInQuotes || nextCursorChar?.trim()) return asSuggestionsRef([]) - if ((prevCursorChar?.trim() || isCursorInQuotes) && isEscapedSuggestions.current) return asSuggestionsRef([]) - isEscapedSuggestions.current = false - - const cursorContext: CursorContext = { isCursorInQuotes, prevCursorChar, nextCursorChar, currentOffsetArg, range } - const commandContext: CommandContext = { commandName, command, prevArgs, allArgs, currentCommandArg } - - const { suggestions, forceHide, helpWidgetData } = getGeneralSuggestions( - commandContext, - cursorContext, - attributesRef.current - ) - - if (helpWidgetData) updateHelpWidget(helpWidgetData.isOpen, helpWidgetData.parent, helpWidgetData.currentArg) - return asSuggestionsRef(suggestions, forceHide) + // // cover index + // if (currentCommandArg?.name === DefinedArgumentName.index) { + // updateHelpWidget(true, addOwnTokenToArgs(commandName, command), currentCommandArg) + // return getIndexSuggestions(command, prevArgs.length, currentOffsetArg, range) + // } + // + // // cover query + // if (currentCommandArg?.name === DefinedArgumentName.query) { + // updateHelpWidget(true, addOwnTokenToArgs(commandName, command), currentCommandArg) + // + // return getQuerySuggestions(prevCursorChar, range) + // } + // + // if (isCursorInQuotes || nextCursorChar?.trim()) return asSuggestionsRef([]) + // if ((prevCursorChar?.trim() || isCursorInQuotes) && isEscapedSuggestions.current) return asSuggestionsRef([]) + // isEscapedSuggestions.current = false + // + // const cursorContext: CursorContext = { isCursorInQuotes, prevCursorChar, nextCursorChar, currentOffsetArg, range } + // const commandContext: CommandContext = { commandName, command, prevArgs, allArgs, currentCommandArg } + // + // const { suggestions, forceHide, helpWidgetData } = getGeneralSuggestions( + // commandContext, + // cursorContext, + // attributesRef.current + // ) + // + // if (helpWidgetData) updateHelpWidget(helpWidgetData.isOpen, helpWidgetData.parent, helpWidgetData.currentArg) + // return asSuggestionsRef(suggestions, forceHide) } const getIndexSuggestions = ( diff --git a/redisinsight/ui/src/pages/search/components/query/utils.ts b/redisinsight/ui/src/pages/search/components/query/utils.ts index ba133ec57f..65a1380efe 100644 --- a/redisinsight/ui/src/pages/search/components/query/utils.ts +++ b/redisinsight/ui/src/pages/search/components/query/utils.ts @@ -97,6 +97,7 @@ export const getCommandSuggestions = ( } export const getGeneralSuggestions = ( + foundArg: any, commandContext: CommandContext, cursorContext: CursorContext, fields: any[], @@ -105,9 +106,7 @@ export const getGeneralSuggestions = ( forceHide?: boolean helpWidgetData?: any } => { - const { command, prevArgs } = commandContext const { range } = cursorContext - const foundArg = findCurrentArgument(command?.arguments || [], prevArgs) if (foundArg && !foundArg.isComplete) { return { @@ -152,7 +151,7 @@ export const getNextSuggestions = ( return { helpWidgetData: { isOpen: !!currentCommandArg, - parent: addOwnTokenToArgs(command.name!, command), + parent: foundArg?.stopArg, currentArg: nextMandatoryArg }, suggestions: [] From 3d8d242582bad7d99ee258d289fcaca24b092259 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Fri, 23 Aug 2024 09:03:49 +0200 Subject: [PATCH 035/112] #RI-5684 - udd tests --- .../pages/search/components/query/Query.tsx | 3 +- .../search/components/query/utils.spec.ts | 97 ++++++++++++++++++- .../pages/search/components/query/utils.ts | 23 ++--- .../ui/src/pages/search/mocks/mocks.ts | 45 +++++++++ .../pages/search/utils/tests/monaco.spec.ts | 2 +- 5 files changed, 153 insertions(+), 17 deletions(-) diff --git a/redisinsight/ui/src/pages/search/components/query/Query.tsx b/redisinsight/ui/src/pages/search/components/query/Query.tsx index 9db6aabfb3..e3b605b7a1 100644 --- a/redisinsight/ui/src/pages/search/components/query/Query.tsx +++ b/redisinsight/ui/src/pages/search/components/query/Query.tsx @@ -8,7 +8,7 @@ import { Nullable } from 'uiSrc/utils' import { IEditorMount } from 'uiSrc/pages/workbench/interfaces' import { addOwnTokenToArgs, - findCurrentArgInQuery, findCurrentArgument, + findCurrentArgInQuery, getRange, getRediSearchSignutureProvider, setCursorPositionAtTheEnd, @@ -223,6 +223,7 @@ const Query = (props: Props) => { return asSuggestionsRef([], false) } + // TODO: change to more generic logic setSelectedIndex(allArgs[1] || '') setSelectedCommand(commandName) diff --git a/redisinsight/ui/src/pages/search/components/query/utils.spec.ts b/redisinsight/ui/src/pages/search/components/query/utils.spec.ts index 2b917ec384..bb71924265 100644 --- a/redisinsight/ui/src/pages/search/components/query/utils.spec.ts +++ b/redisinsight/ui/src/pages/search/components/query/utils.spec.ts @@ -1,7 +1,10 @@ -import { getGeneralSuggestions } from 'uiSrc/pages/search/components/query/utils' +import { getGeneralSuggestions, isIndexComplete } from 'uiSrc/pages/search/components/query/utils' import { MOCKED_SUPPORTED_COMMANDS } from 'uiSrc/pages/search/mocks/mocks' +import { buildSuggestion } from 'uiSrc/pages/search/utils' +import { SearchCommand } from 'uiSrc/pages/search/types' -const ftAggregate = MOCKED_SUPPORTED_COMMANDS['FT.SEARCH'] +const ftAggregate = MOCKED_SUPPORTED_COMMANDS['FT.AGGREGATE'] +const ftProfile = MOCKED_SUPPORTED_COMMANDS['FT.PROFILE'] const getCursorContext = () => ({ currentOffsetArg: undefined, @@ -25,15 +28,101 @@ const getGeneralSuggestionsTests = [ }, result: { helpWidgetData: expect.any(Object), - suggestions: expect.any(Array) + suggestions: ftAggregate.arguments + .slice(2) + .map((arg) => ({ + ...buildSuggestion(arg as SearchCommand, {} as any), + sortText: expect.any(String), + kind: undefined, + detail: expect.any(String) + })) + } + }, + { + input: { + commandContext: { + allArgs: ['FT.AGGREGATE', '""', '""', 'APPLY', 'expression'], + command: ftAggregate, + commandName: 'FT.AGGREGATE', + currentCommandArg: null, + prevArgs: ['""', '""', 'APPLY', 'expression'] + }, + cursorContext: getCursorContext() + }, + result: { + helpWidgetData: expect.any(Object), + suggestions: [ + { + label: 'AS', + insertText: 'AS ', + insertTextRules: 4, + range: expect.any(Object), + kind: undefined, + detail: 'APPLY expression AS name', + } + ] + } + }, + { + input: { + commandContext: { + allArgs: ['FT.PROFILE', '""'], + command: ftProfile, + commandName: 'FT.PROFILE', + currentCommandArg: null, + prevArgs: ['""'] + }, + cursorContext: getCursorContext() + }, + result: { + helpWidgetData: expect.any(Object), + suggestions: [ + { + label: 'SEARCH', + insertText: 'SEARCH ', + insertTextRules: 4, + range: expect.any(Object), + kind: undefined, + detail: '', + }, + { + label: 'AGGREGATE', + insertText: 'AGGREGATE ', + insertTextRules: 4, + range: expect.any(Object), + kind: undefined, + detail: '', + } + ] } } ] describe('getGeneralSuggestions', () => { - it.each(getGeneralSuggestionsTests)('dawd', ({ input, result }) => { + it.each(getGeneralSuggestionsTests)('should properly return suggestions', ({ input, result }) => { const testResult = getGeneralSuggestions(input.commandContext, input.cursorContext, []) expect(testResult).toEqual(result) }) }) + +const isIndexCompleteTests: Array<[string, boolean]> = [ + ['', false], + ['"', false], + ['\"\\"', false], + ['""', true], + ["'", false], + ["''", true], + ["'index\\'", false], + ["'index'", true], + ['"index \\\\"', true], + ['index', true], +] + +describe('isIndexComplete', () => { + it.each(isIndexCompleteTests)('should properly return value for %s', (index, result) => { + const testResult = isIndexComplete(index) + + expect(testResult).toEqual(result) + }) +}) diff --git a/redisinsight/ui/src/pages/search/components/query/utils.ts b/redisinsight/ui/src/pages/search/components/query/utils.ts index ba133ec57f..a22e1b1d6c 100644 --- a/redisinsight/ui/src/pages/search/components/query/utils.ts +++ b/redisinsight/ui/src/pages/search/components/query/utils.ts @@ -166,18 +166,19 @@ export const getNextSuggestions = ( } export const isIndexComplete = (index: string) => { - if (!index) return false - if (index.startsWith('"')) { - if (index.length < 2) return false - if (!index.endsWith('"')) return false - return !index.endsWith('\\', -1) - } + if (index.length === 0) return false + + const firstChar = index[0] + const lastChar = index[index.length - 1] + + if (firstChar !== '"' && firstChar !== "'") return true + if (index.length === 1 && (firstChar === '"' || firstChar === "'")) return false + if (firstChar !== lastChar) return false - if (index.startsWith("'")) { - if (index.length < 2) return false - if (!index.endsWith("'")) return false - return !index.endsWith('\\', -1) + let escape = false + for (let i = 1; i < index.length - 1; i++) { + escape = index[i] === '\\' && !escape } - return true + return !escape } diff --git a/redisinsight/ui/src/pages/search/mocks/mocks.ts b/redisinsight/ui/src/pages/search/mocks/mocks.ts index 8125afa84c..01a67f5c19 100644 --- a/redisinsight/ui/src/pages/search/mocks/mocks.ts +++ b/redisinsight/ui/src/pages/search/mocks/mocks.ts @@ -672,5 +672,50 @@ export const MOCKED_SUPPORTED_COMMANDS = { ], since: '1.1.0', group: 'search' + }, + + 'FT.PROFILE': { + summary: 'Performs a `FT.SEARCH` or `FT.AGGREGATE` command and collects performance information', + complexity: 'O(N)', + arguments: [ + { + name: 'index', + type: 'string' + }, + { + name: 'querytype', + type: 'oneof', + arguments: [ + { + name: 'search', + type: 'pure-token', + token: 'SEARCH' + }, + { + name: 'aggregate', + type: 'pure-token', + token: 'AGGREGATE' + } + ] + }, + { + name: 'limited', + type: 'pure-token', + token: 'LIMITED', + optional: true + }, + { + name: 'queryword', + type: 'pure-token', + token: 'QUERY' + }, + { + name: 'query', + type: 'string' + } + ], + since: '2.2.0', + group: 'search', + provider: 'redisearch' } } diff --git a/redisinsight/ui/src/pages/search/utils/tests/monaco.spec.ts b/redisinsight/ui/src/pages/search/utils/tests/monaco.spec.ts index 70dc43d5ad..48b9ff5c3e 100644 --- a/redisinsight/ui/src/pages/search/utils/tests/monaco.spec.ts +++ b/redisinsight/ui/src/pages/search/utils/tests/monaco.spec.ts @@ -1,5 +1,5 @@ import { getRediSearchSignutureProvider } from 'uiSrc/pages/search/utils' -import { MOCKED_SUPPORTED_COMMANDS } from 'uiSrc/pages/search/utils/tests/mocks' +import { MOCKED_SUPPORTED_COMMANDS } from 'uiSrc/pages/search/mocks/mocks' import { SearchCommand } from 'uiSrc/pages/search/types' const ftAggregateCommand = MOCKED_SUPPORTED_COMMANDS['FT.AGGREGATE'] From 283c3a459da3ff7de93ab5f550dcb18852d7af25 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Sat, 24 Aug 2024 14:53:15 +0200 Subject: [PATCH 036/112] refactoring --- .../pages/search/components/query/Query.tsx | 55 ++------- .../pages/search/components/query/utils.ts | 90 +++++--------- redisinsight/ui/src/pages/search/types.ts | 21 +--- .../ui/src/pages/search/utils/query.ts | 116 ++++++++++-------- 4 files changed, 105 insertions(+), 177 deletions(-) diff --git a/redisinsight/ui/src/pages/search/components/query/Query.tsx b/redisinsight/ui/src/pages/search/components/query/Query.tsx index 7f742e5df7..21cbfa6f2e 100644 --- a/redisinsight/ui/src/pages/search/components/query/Query.tsx +++ b/redisinsight/ui/src/pages/search/components/query/Query.tsx @@ -8,13 +8,13 @@ import { Nullable } from 'uiSrc/utils' import { IEditorMount } from 'uiSrc/pages/workbench/interfaces' import { addOwnTokenToArgs, - findCurrentArgInQuery, + findCurrentArgument, getRange, getRediSearchSignutureProvider, setCursorPositionAtTheEnd, splitQueryByArgs } from 'uiSrc/pages/search/utils' -import { CommandContext, CursorContext, SearchCommand, TokenType } from 'uiSrc/pages/search/types' +import { SearchCommand, TokenType } from 'uiSrc/pages/search/types' import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' import { fetchRedisearchInfoAction } from 'uiSrc/slices/browser/redisearch' import { getRediSearchMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokens' @@ -233,11 +233,8 @@ const Query = (props: Props) => { setSelectedIndex(allArgs[1] || '') setSelectedCommand(commandName) - const currentCommandArg = findCurrentArgInQuery(prevArgs, command) const foundArg = findCurrentArgument(COMMANDS_LIST, beforeOffsetArgs) - console.log(foundArg) - switch (foundArg?.stopArg?.name) { case DefinedArgumentName.index: { updateHelpWidget(true, command, foundArg?.stopArg) @@ -248,59 +245,21 @@ const Query = (props: Props) => { return getQuerySuggestions(prevCursorChar, range) } default: { - console.log(foundArg) - const cursorContext: CursorContext = { isCursorInQuotes, prevCursorChar, nextCursorChar, currentOffsetArg, range } - const commandContext: CommandContext = { - commands: COMMANDS_LIST, - commandName, - command, - prevArgs: beforeOffsetArgs, - allArgs, - currentCommandArg: foundArg?.stopArg - } + if (isCursorInQuotes || nextCursorChar?.trim()) return asSuggestionsRef([]) + if ((prevCursorChar?.trim() || isCursorInQuotes) && isEscapedSuggestions.current) return asSuggestionsRef([]) + isEscapedSuggestions.current = false const { suggestions, forceHide, helpWidgetData } = getGeneralSuggestions( foundArg, - commandContext, - cursorContext, + allArgs, + range, attributesRef.current ) - console.log(suggestions) - console.log(helpWidgetData) - if (helpWidgetData) updateHelpWidget(helpWidgetData.isOpen, helpWidgetData.parent, helpWidgetData.currentArg) return asSuggestionsRef(suggestions, forceHide) } } - // // cover index - // if (currentCommandArg?.name === DefinedArgumentName.index) { - // updateHelpWidget(true, addOwnTokenToArgs(commandName, command), currentCommandArg) - // return getIndexSuggestions(command, prevArgs.length, currentOffsetArg, range) - // } - // - // // cover query - // if (currentCommandArg?.name === DefinedArgumentName.query) { - // updateHelpWidget(true, addOwnTokenToArgs(commandName, command), currentCommandArg) - // - // return getQuerySuggestions(prevCursorChar, range) - // } - // - // if (isCursorInQuotes || nextCursorChar?.trim()) return asSuggestionsRef([]) - // if ((prevCursorChar?.trim() || isCursorInQuotes) && isEscapedSuggestions.current) return asSuggestionsRef([]) - // isEscapedSuggestions.current = false - // - // const cursorContext: CursorContext = { isCursorInQuotes, prevCursorChar, nextCursorChar, currentOffsetArg, range } - // const commandContext: CommandContext = { commandName, command, prevArgs, allArgs, currentCommandArg } - // - // const { suggestions, forceHide, helpWidgetData } = getGeneralSuggestions( - // commandContext, - // cursorContext, - // attributesRef.current - // ) - // - // if (helpWidgetData) updateHelpWidget(helpWidgetData.isOpen, helpWidgetData.parent, helpWidgetData.currentArg) - // return asSuggestionsRef(suggestions, forceHide) } const getIndexSuggestions = ( diff --git a/redisinsight/ui/src/pages/search/components/query/utils.ts b/redisinsight/ui/src/pages/search/components/query/utils.ts index 8dae0dea29..10f1527fb2 100644 --- a/redisinsight/ui/src/pages/search/components/query/utils.ts +++ b/redisinsight/ui/src/pages/search/components/query/utils.ts @@ -2,8 +2,12 @@ import { monaco } from 'react-monaco-editor' import * as monacoEditor from 'monaco-editor' import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' import { bufferToString, formatLongName, getCommandMarkdown, Nullable } from 'uiSrc/utils' -import { addOwnTokenToArgs, buildSuggestion, findCurrentArgument, generateDetail } from 'uiSrc/pages/search/utils' -import { CommandContext, CursorContext, FoundCommandArgument, SearchCommand } from 'uiSrc/pages/search/types' +import { + buildSuggestion, + generateDetail, + removeNotSuggestedArgs +} from 'uiSrc/pages/search/utils' +import { FoundCommandArgument, SearchCommand } from 'uiSrc/pages/search/types' import { DefinedArgumentName } from 'uiSrc/pages/search/components/query/constants' export const asSuggestionsRef = (suggestions: monacoEditor.languages.CompletionItem[], forceHide = true) => ({ @@ -61,7 +65,7 @@ export const getMandatoryArgumentSuggestions = ( if (foundArg.isBlocked) return [] if (foundArg.append?.length) { - return foundArg.append.map((arg: any) => buildSuggestion(arg, range, { + return foundArg.append.flat().map((arg: any) => buildSuggestion(arg, range, { kind: monacoEditor.languages.CompletionItemKind.Property, detail: generateDetail(foundArg?.parent) })) @@ -71,43 +75,44 @@ export const getMandatoryArgumentSuggestions = ( } export const getCommandSuggestions = ( - firstLevelArgs: SearchCommand[], foundArg: Nullable, allArgs: string[], range: monaco.IRange, - currentArg?: string ) => { const appendCommands = foundArg?.append ?? [] + const suggestions = [] - return [ - ...appendCommands.map((arg) => buildSuggestion(arg, range, { - sortText: 'a', - kind: monacoEditor.languages.CompletionItemKind.Property, - detail: generateDetail(foundArg?.parent) - })), - ...firstLevelArgs - .filter((arg) => - arg.multiple || !(currentArg !== arg.token && allArgs.includes(arg.token || arg.arguments?.[0]?.token || ''))) + for (let i = 0; i < appendCommands.length; i++) { + const isLastLevel = i === appendCommands.length - 1 + const filteredFileldArgs = isLastLevel + ? removeNotSuggestedArgs(allArgs, appendCommands[i]) + : appendCommands[i] + + const leveledSuggestions = filteredFileldArgs .map((arg) => buildSuggestion(arg, range, { - sortText: 'b', - kind: monacoEditor.languages.CompletionItemKind.Reference, - detail: generateDetail(arg) + sortText: `${i}`, + kind: isLastLevel + ? monacoEditor.languages.CompletionItemKind.Reference + : monacoEditor.languages.CompletionItemKind.Property, + detail: generateDetail(arg?.parent) })) - ] + + suggestions.push(leveledSuggestions) + } + + return suggestions.flat() } export const getGeneralSuggestions = ( - foundArg: any, - commandContext: CommandContext, - cursorContext: CursorContext, + foundArg: Nullable, + allArgs: string[], + range: monacoEditor.IRange, fields: any[], ): { suggestions: monacoEditor.languages.CompletionItem[], forceHide?: boolean helpWidgetData?: any } => { - const { range } = cursorContext - if (foundArg && !foundArg.isComplete) { return { suggestions: getMandatoryArgumentSuggestions(foundArg, fields, range), @@ -119,47 +124,18 @@ export const getGeneralSuggestions = ( } } - return getNextSuggestions(commandContext, cursorContext, foundArg) + return getNextSuggestions(foundArg, allArgs, range) } export const getNextSuggestions = ( - { command, currentCommandArg, prevArgs, allArgs }: CommandContext, - { currentOffsetArg, range }: CursorContext, - foundArg: Nullable + foundArg: Nullable, + allArgs: string[], + range: monacoEditor.IRange ) => { - if (!command) return { suggestions: [] } if (foundArg && !foundArg.isComplete) return { suggestions: [], helpWidgetData: { isOpen: false } } - const parentArgIndex = command.arguments - ?.findIndex(({ name }) => name === foundArg?.parent?.name) || -1 - const currentArgIndex = parentArgIndex > -1 ? parentArgIndex : prevArgs.length - 1 - const nextMandatoryIndex = command.arguments - ?.findIndex(({ optional }, i) => !optional && i > currentArgIndex) || -1 - - const nextOptionalArgs = ( - nextMandatoryIndex > -1 - ? command.arguments?.slice(currentArgIndex + 1, nextMandatoryIndex) - : command.arguments?.filter(({ optional }) => optional) - ) || [] - const nextMandatoryArg = command.arguments?.[nextMandatoryIndex] - - if (nextMandatoryArg?.token) { - nextOptionalArgs.unshift(nextMandatoryArg) - } - - if (nextMandatoryArg && !nextMandatoryArg.token) { - return { - helpWidgetData: { - isOpen: !!currentCommandArg, - parent: foundArg?.stopArg, - currentArg: nextMandatoryArg - }, - suggestions: [] - } - } - return { - suggestions: getCommandSuggestions(nextOptionalArgs, foundArg, allArgs, range, currentOffsetArg), + suggestions: getCommandSuggestions(foundArg, allArgs, range), helpWidgetData: { isOpen: false } } } diff --git a/redisinsight/ui/src/pages/search/types.ts b/redisinsight/ui/src/pages/search/types.ts index d4eca49253..a2d96a5a4d 100644 --- a/redisinsight/ui/src/pages/search/types.ts +++ b/redisinsight/ui/src/pages/search/types.ts @@ -1,5 +1,4 @@ -import { monaco as monacoEditor } from 'react-monaco-editor' -import { Maybe, Nullable } from 'uiSrc/utils' +import { Maybe } from 'uiSrc/utils' export enum TokenType { PureToken = 'pure-token', @@ -28,22 +27,6 @@ export interface FoundCommandArgument { isComplete: boolean stopArg: Maybe isBlocked: boolean - append: Maybe + append: Maybe> parent: Maybe } - -export interface CommandContext { - commandName: string - command: Maybe - prevArgs: string[] - allArgs: string[] - currentCommandArg: Nullable -} - -export interface CursorContext { - isCursorInQuotes: boolean - prevCursorChar: string - nextCursorChar: string - currentOffsetArg: any - range: monacoEditor.IRange -} diff --git a/redisinsight/ui/src/pages/search/utils/query.ts b/redisinsight/ui/src/pages/search/utils/query.ts index 17a8ad5ef2..89178a4fcf 100644 --- a/redisinsight/ui/src/pages/search/utils/query.ts +++ b/redisinsight/ui/src/pages/search/utils/query.ts @@ -1,6 +1,6 @@ /* eslint-disable no-continue */ -import { toNumber, uniqBy } from 'lodash' +import { toNumber } from 'lodash' import { generateArgsNames, Maybe, Nullable } from 'uiSrc/utils' import { CommandProvider } from 'uiSrc/constants' import { ArgName, FoundCommandArgument, SearchCommand, SearchCommandTree, TokenType } from '../types' @@ -27,7 +27,6 @@ export const splitQueryByArgs = (query: string, position: number = 0) => { for (let i = 0; i < query.length; i++) { const char = query[i] const isAfterOffset = i >= position + (inQuotes ? -1 : 0) - if (i === position - 1) isCursorInQuotes = inQuotes if (escapeNextChar) { arg += char @@ -66,6 +65,8 @@ export const splitQueryByArgs = (query: string, position: number = 0) => { } else { arg += char } + + if (i === position - 1) isCursorInQuotes = inQuotes } if (arg.length > 0) { @@ -108,13 +109,6 @@ export const findCurrentArgument = ( } } - if (args[prev.length] && !args[prev.length].optional) { - return { - ...getArgumentSuggestions([], args.slice(prev.length)), - parent: undefined - } - } - return null } @@ -242,7 +236,7 @@ export const getArgumentSuggestions = ( isComplete: boolean stopArg: Maybe, isBlocked: boolean, - append: SearchCommand[], + append: Array, } => { const { restArguments, @@ -261,13 +255,13 @@ export const getArgumentSuggestions = ( isComplete: false, stopArg: stopArgument, isBlocked: !isOneOfArgument, - append: isOneOfArgument ? stopArgument.arguments! : [], + append: isOneOfArgument ? [stopArgument.arguments!] : [], } } if (stopArgument && !stopArgument.optional) { const isCanAppend = stopArgument?.token || isOneOfArgument - const append = isCanAppend ? [isOneOfArgument ? stopArgument.arguments! : stopArgument].flat() : [] + const append = isCanAppend ? [[isOneOfArgument ? stopArgument.arguments! : stopArgument].flat()] : [] return { isComplete: false, @@ -277,49 +271,83 @@ export const getArgumentSuggestions = ( } } - const restParentOptionalSuggestions = getRestParentArguments(current?.parent, current?.name, current?.multiple) - .filter((arg) => arg.optional && arg.name !== stopArgument?.name - && (current?.multiple || arg.name !== current?.name)) - - const restOptionalSuggestions = uniqBy( - fillArgsByType([...restNotFilledArgs, ...restParentOptionalSuggestions]), - 'token' - ) + const beforeMandatoryOptionalArgs = getAllRestArguments(current, stopArgument, pastStringArgs) const requiredArgsLength = restNotFilledArgs.filter((arg) => !arg.optional).length return { isComplete: requiredArgsLength === 0, stopArg: stopArgument, isBlocked: false, - append: restOptionalSuggestions, + append: beforeMandatoryOptionalArgs, } } -export const getRestParentArguments = ( - parent?: SearchCommandTree, - currentArgName?: string, - isIncludeOwn: boolean = true, - prevArgs: SearchCommand[] = [] -): SearchCommand[] => { - if (!currentArgName) return [] +export const getRestArguments = ( + current: Maybe, + stopArgument: Nullable +): SearchCommandTree[] => { + if (!stopArgument) return [] + + const argumentIndexInArg = current?.arguments + ?.findIndex(({ name }) => name === stopArgument?.name) || -1 + const nextMandatoryIndex = current?.arguments + ?.findIndex(({ optional }, i) => !optional && i > argumentIndexInArg) || -1 - const currentArgIndex = parent?.arguments?.findIndex((arg) => arg?.name === currentArgName) - if (!currentArgIndex) return prevArgs + const beforeMandatoryOptionalArgs = ( + nextMandatoryIndex > -1 + ? current?.arguments?.slice(argumentIndexInArg, nextMandatoryIndex) + : current?.arguments?.filter(({ optional }) => optional) + ) || [] - const currentRestArgs = parent?.arguments?.slice(currentArgIndex + (isIncludeOwn ? 0 : 1)) || [] + const nextMandatoryArg = current?.arguments?.[nextMandatoryIndex] - if (parent?.parent) return getRestParentArguments(parent.parent, parent.name, true, currentRestArgs) + if (nextMandatoryArg?.token) { + beforeMandatoryOptionalArgs.unshift(nextMandatoryArg) + } - return [...currentRestArgs, ...prevArgs] + return fillArgsByType(beforeMandatoryOptionalArgs) + .map((arg) => ({ + ...arg, + parent: current + })) } -export const fillArgsByType = (args: SearchCommand[]): SearchCommand[] => { +export const getAllRestArguments = ( + current: Maybe, + stopArgument: Nullable, + prevStringArgs: string[] = [], +) => { + const appendArgs: Array = [] + const currentLvlNextArgs = removeNotSuggestedArgs( + prevStringArgs, + getRestArguments(current, stopArgument) + ) + + if (currentLvlNextArgs.length) { + appendArgs.push(currentLvlNextArgs) + } + + if (current?.parent) { + const parentArgs = getAllRestArguments(current.parent, current, []) + if (parentArgs?.length) { + appendArgs.push(...parentArgs) + } + } + + return appendArgs +} + +export const removeNotSuggestedArgs = (args: string[], commandArgs: SearchCommandTree[]) => + commandArgs.filter((arg) => arg.token + && (arg.multiple || !args.some((queryArg) => queryArg.toUpperCase() === arg.token?.toUpperCase()))) + +export const fillArgsByType = (args: SearchCommand[], expandBlock = true): SearchCommand[] => { const result: SearchCommand[] = [] for (let i = 0; i < args.length; i++) { const currentArg = args[i] - if (currentArg.type === TokenType.OneOf) result.push(...(currentArg?.arguments || [])) + if (expandBlock && currentArg.type === TokenType.OneOf) result.push(...(currentArg?.arguments || [])) if (currentArg.type === TokenType.Block) result.push(currentArg.arguments?.[0] as SearchCommand) if (currentArg.token) result.push(currentArg) } @@ -352,21 +380,3 @@ export const addOwnTokenToArgs = (token: string, command: SearchCommand) => { } return command } - -export const findCurrentArgInQuery = (args: string[], command: SearchCommand) => { - if (!command.arguments || !command.arguments.length) return null - - let argIndex = 0 - args.forEach((arg) => { - for (let i = argIndex; i < command.arguments!.length; i++) { - if (command.arguments![i]?.optional && command.arguments![i]?.token?.toUpperCase() !== arg?.toUpperCase()) { - continue - } - - argIndex = i + 1 - break - } - }) - - return command.arguments[argIndex] -} From 354b6148a27cee31b288f18d6bb799dec0f31467 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Mon, 26 Aug 2024 09:27:36 +0200 Subject: [PATCH 037/112] #RI-6061 - fix overflow --- redisinsight/ui/src/pages/search/styles.module.scss | 4 ++-- .../workbench/components/wb-view/WBView/styles.module.scss | 4 ++-- .../ui/src/templates/explore-panel/styles.module.scss | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/redisinsight/ui/src/pages/search/styles.module.scss b/redisinsight/ui/src/pages/search/styles.module.scss index e371f27293..359b2462eb 100644 --- a/redisinsight/ui/src/pages/search/styles.module.scss +++ b/redisinsight/ui/src/pages/search/styles.module.scss @@ -1,8 +1,8 @@ .container { - min-height: 100%; - height: 1px !important; + flex-grow: 1; display: flex; flex-direction: column; + max-height: calc(100% - 50px); } .main { diff --git a/redisinsight/ui/src/pages/workbench/components/wb-view/WBView/styles.module.scss b/redisinsight/ui/src/pages/workbench/components/wb-view/WBView/styles.module.scss index eff58ba867..cea9ac8893 100644 --- a/redisinsight/ui/src/pages/workbench/components/wb-view/WBView/styles.module.scss +++ b/redisinsight/ui/src/pages/workbench/components/wb-view/WBView/styles.module.scss @@ -1,8 +1,8 @@ .container { - min-height: 100%; - height: 1px !important; display: flex; + flex-grow: 1; flex-direction: column; + max-height: calc(100% - 50px); } .main { diff --git a/redisinsight/ui/src/templates/explore-panel/styles.module.scss b/redisinsight/ui/src/templates/explore-panel/styles.module.scss index 49cef4b420..9f5842eadc 100644 --- a/redisinsight/ui/src/templates/explore-panel/styles.module.scss +++ b/redisinsight/ui/src/templates/explore-panel/styles.module.scss @@ -4,7 +4,6 @@ height: 100%; width: 100%; position: relative; - overflow: hidden; } .mainPanel { From b24377cc3db1fff3f515a0b5329f5deb1fe22f29 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Mon, 26 Aug 2024 09:56:01 +0200 Subject: [PATCH 038/112] #RI-6062 - add initial commands json --- .../constants/supported_commands.json | 745 ++++++++++++++++++ 1 file changed, 745 insertions(+) create mode 100644 redisinsight/ui/src/pages/search/components/constants/supported_commands.json diff --git a/redisinsight/ui/src/pages/search/components/constants/supported_commands.json b/redisinsight/ui/src/pages/search/components/constants/supported_commands.json new file mode 100644 index 0000000000..4eb75226c2 --- /dev/null +++ b/redisinsight/ui/src/pages/search/components/constants/supported_commands.json @@ -0,0 +1,745 @@ +{ + "FT.AGGREGATE": { + "summary": "Run a search query on an index and perform aggregate transformations on the results", + "complexity": "O(1)", + "arguments": [ + { + "name": "index", + "type": "string" + }, + { + "name": "query", + "type": "string" + }, + { + "name": "verbatim", + "type": "pure-token", + "token": "VERBATIM", + "optional": true + }, + { + "name": "load", + "type": "block", + "optional": true, + "arguments": [ + { + "name": "count", + "type": "string", + "token": "LOAD" + }, + { + "name": "field", + "type": "string", + "multiple": true + } + ] + }, + { + "name": "timeout", + "type": "integer", + "optional": true, + "token": "TIMEOUT" + }, + { + "name": "loadall", + "type": "pure-token", + "token": "LOAD *", + "optional": true + }, + { + "name": "groupby", + "type": "block", + "optional": true, + "multiple": true, + "arguments": [ + { + "name": "nargs", + "type": "integer", + "token": "GROUPBY" + }, + { + "name": "property", + "type": "string", + "multiple": true + }, + { + "name": "reduce", + "type": "block", + "optional": true, + "multiple": true, + "arguments": [ + { + "name": "function", + "type": "string", + "token": "REDUCE" + }, + { + "name": "nargs", + "type": "integer" + }, + { + "name": "arg", + "type": "string", + "multiple": true + }, + { + "name": "name", + "type": "string", + "token": "AS", + "optional": true + } + ] + } + ] + }, + { + "name": "sortby", + "type": "block", + "optional": true, + "arguments": [ + { + "name": "nargs", + "type": "integer", + "token": "SORTBY" + }, + { + "name": "fields", + "type": "block", + "optional": true, + "multiple": true, + "arguments": [ + { + "name": "property", + "type": "string" + }, + { + "name": "order", + "type": "oneof", + "arguments": [ + { + "name": "asc", + "type": "pure-token", + "token": "ASC" + }, + { + "name": "desc", + "type": "pure-token", + "token": "DESC" + } + ] + } + ] + }, + { + "name": "num", + "type": "integer", + "token": "MAX", + "optional": true + } + ] + }, + { + "name": "apply", + "type": "block", + "optional": true, + "multiple": true, + "arguments": [ + { + "name": "expression", + "type": "string", + "token": "APPLY" + }, + { + "name": "name", + "type": "string", + "token": "AS" + } + ] + }, + { + "name": "limit", + "type": "block", + "optional": true, + "arguments": [ + { + "name": "limit", + "type": "pure-token", + "token": "LIMIT" + }, + { + "name": "offset", + "type": "integer" + }, + { + "name": "num", + "type": "integer" + } + ] + }, + { + "name": "filter", + "type": "string", + "optional": true, + "token": "FILTER" + }, + { + "name": "cursor", + "type": "block", + "optional": true, + "arguments": [ + { + "name": "withcursor", + "type": "pure-token", + "token": "WITHCURSOR" + }, + { + "name": "read_size", + "type": "integer", + "optional": true, + "token": "COUNT" + }, + { + "name": "idle_time", + "type": "integer", + "optional": true, + "token": "MAXIDLE" + } + ] + }, + { + "name": "params", + "type": "block", + "optional": true, + "arguments": [ + { + "name": "params", + "type": "pure-token", + "token": "PARAMS" + }, + { + "name": "nargs", + "type": "integer" + }, + { + "name": "values", + "type": "block", + "multiple": true, + "arguments": [ + { + "name": "name", + "type": "string" + }, + { + "name": "value", + "type": "string" + } + ] + } + ] + }, + { + "name": "dialect", + "type": "integer", + "optional": true, + "token": "DIALECT", + "since": "2.4.3" + } + ], + "since": "1.1.0", + "group": "search", + "provider": "redisearch" + }, + "FT.EXPLAIN": { + "summary": "Returns the execution plan for a complex query", + "complexity": "O(1)", + "arguments": [ + { + "name": "index", + "type": "string" + }, + { + "name": "query", + "type": "string" + }, + { + "name": "dialect", + "type": "integer", + "optional": true, + "token": "DIALECT", + "since": "2.4.3" + } + ], + "since": "1.0.0", + "group": "search", + "provider": "redisearch" + }, + "FT.PROFILE": { + "summary": "Performs a `FT.SEARCH` or `FT.AGGREGATE` command and collects performance information", + "complexity": "O(N)", + "arguments": [ + { + "name": "index", + "type": "string" + }, + { + "name": "querytype", + "type": "oneof", + "arguments": [ + { + "name": "search", + "type": "pure-token", + "token": "SEARCH" + }, + { + "name": "aggregate", + "type": "pure-token", + "token": "AGGREGATE" + } + ] + }, + { + "name": "limited", + "type": "pure-token", + "token": "LIMITED", + "optional": true + }, + { + "name": "queryword", + "type": "pure-token", + "token": "QUERY" + }, + { + "name": "query", + "type": "string" + } + ], + "since": "2.2.0", + "group": "search", + "provider": "redisearch" + }, + "FT.SEARCH": { + "summary": "Searches the index with a textual query, returning either documents or just ids", + "complexity": "O(N)", + "history": [ + [ + "2.0.0", + "Deprecated `WITHPAYLOADS` and `PAYLOAD` arguments" + ] + ], + "arguments": [ + { + "name": "index", + "type": "string" + }, + { + "name": "query", + "type": "string" + }, + { + "name": "nocontent", + "type": "pure-token", + "token": "NOCONTENT", + "optional": true + }, + { + "name": "verbatim", + "type": "pure-token", + "token": "VERBATIM", + "optional": true + }, + { + "name": "nostopwords", + "type": "pure-token", + "token": "NOSTOPWORDS", + "optional": true + }, + { + "name": "withscores", + "type": "pure-token", + "token": "WITHSCORES", + "optional": true + }, + { + "name": "withpayloads", + "type": "pure-token", + "token": "WITHPAYLOADS", + "optional": true + }, + { + "name": "withsortkeys", + "type": "pure-token", + "token": "WITHSORTKEYS", + "optional": true + }, + { + "name": "filter", + "type": "block", + "optional": true, + "multiple": true, + "arguments": [ + { + "name": "numeric_field", + "type": "string", + "token": "FILTER" + }, + { + "name": "min", + "type": "double" + }, + { + "name": "max", + "type": "double" + } + ] + }, + { + "name": "geo_filter", + "type": "block", + "optional": true, + "multiple": true, + "arguments": [ + { + "name": "geo_field", + "type": "string", + "token": "GEOFILTER" + }, + { + "name": "lon", + "type": "double" + }, + { + "name": "lat", + "type": "double" + }, + { + "name": "radius", + "type": "double" + }, + { + "name": "radius_type", + "type": "oneof", + "arguments": [ + { + "name": "m", + "type": "pure-token", + "token": "m" + }, + { + "name": "km", + "type": "pure-token", + "token": "km" + }, + { + "name": "mi", + "type": "pure-token", + "token": "mi" + }, + { + "name": "ft", + "type": "pure-token", + "token": "ft" + } + ] + } + ] + }, + { + "name": "in_keys", + "type": "block", + "optional": true, + "arguments": [ + { + "name": "count", + "type": "string", + "token": "INKEYS" + }, + { + "name": "key", + "type": "string", + "multiple": true + } + ] + }, + { + "name": "in_fields", + "type": "block", + "optional": true, + "arguments": [ + { + "name": "count", + "type": "string", + "token": "INFIELDS" + }, + { + "name": "field", + "type": "string", + "multiple": true + } + ] + }, + { + "name": "return", + "type": "block", + "optional": true, + "arguments": [ + { + "name": "count", + "type": "string", + "token": "RETURN" + }, + { + "name": "identifiers", + "type": "block", + "multiple": true, + "arguments": [ + { + "name": "identifier", + "type": "string" + }, + { + "name": "property", + "type": "string", + "token": "AS", + "optional": true + } + ] + } + ] + }, + { + "name": "summarize", + "type": "block", + "optional": true, + "arguments": [ + { + "name": "summarize", + "type": "pure-token", + "token": "SUMMARIZE" + }, + { + "name": "fields", + "type": "block", + "optional": true, + "arguments": [ + { + "name": "count", + "type": "string", + "token": "FIELDS" + }, + { + "name": "field", + "type": "string", + "multiple": true + } + ] + }, + { + "name": "num", + "type": "integer", + "token": "FRAGS", + "optional": true + }, + { + "name": "fragsize", + "type": "integer", + "token": "LEN", + "optional": true + }, + { + "name": "separator", + "type": "string", + "token": "SEPARATOR", + "optional": true + } + ] + }, + { + "name": "highlight", + "type": "block", + "optional": true, + "arguments": [ + { + "name": "highlight", + "type": "pure-token", + "token": "HIGHLIGHT" + }, + { + "name": "fields", + "type": "block", + "optional": true, + "arguments": [ + { + "name": "count", + "type": "string", + "token": "FIELDS" + }, + { + "name": "field", + "type": "string", + "multiple": true + } + ] + }, + { + "name": "tags", + "type": "block", + "optional": true, + "arguments": [ + { + "name": "tags", + "type": "pure-token", + "token": "TAGS" + }, + { + "name": "open", + "type": "string" + }, + { + "name": "close", + "type": "string" + } + ] + } + ] + }, + { + "name": "slop", + "type": "integer", + "optional": true, + "token": "SLOP" + }, + { + "name": "timeout", + "type": "integer", + "optional": true, + "token": "TIMEOUT" + }, + { + "name": "inorder", + "type": "pure-token", + "token": "INORDER", + "optional": true + }, + { + "name": "language", + "type": "string", + "optional": true, + "token": "LANGUAGE" + }, + { + "name": "expander", + "type": "string", + "optional": true, + "token": "EXPANDER" + }, + { + "name": "scorer", + "type": "string", + "optional": true, + "token": "SCORER" + }, + { + "name": "explainscore", + "type": "pure-token", + "token": "EXPLAINSCORE", + "optional": true + }, + { + "name": "payload", + "type": "string", + "optional": true, + "token": "PAYLOAD" + }, + { + "name": "sortby", + "type": "block", + "optional": true, + "arguments": [ + { + "name": "sortby", + "type": "string", + "token": "SORTBY" + }, + { + "name": "order", + "type": "oneof", + "optional": true, + "arguments": [ + { + "name": "asc", + "type": "pure-token", + "token": "ASC" + }, + { + "name": "desc", + "type": "pure-token", + "token": "DESC" + } + ] + } + ] + }, + { + "name": "limit", + "type": "block", + "optional": true, + "arguments": [ + { + "name": "limit", + "type": "pure-token", + "token": "LIMIT" + }, + { + "name": "offset", + "type": "integer" + }, + { + "name": "num", + "type": "integer" + } + ] + }, + { + "name": "params", + "type": "block", + "optional": true, + "arguments": [ + { + "name": "params", + "type": "pure-token", + "token": "PARAMS" + }, + { + "name": "nargs", + "type": "integer" + }, + { + "name": "values", + "type": "block", + "multiple": true, + "arguments": [ + { + "name": "name", + "type": "string" + }, + { + "name": "value", + "type": "string" + } + ] + } + ] + }, + { + "name": "dialect", + "type": "integer", + "optional": true, + "token": "DIALECT", + "since": "2.4.3" + } + ], + "since": "1.0.0", + "group": "search", + "provider": "redisearch" + } +} From f6a8d05cae9b24a85b520e1b0dd76eb4a28a3179 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Tue, 27 Aug 2024 13:38:58 +0200 Subject: [PATCH 039/112] changes from main --- .../navigation-menu/NavigationMenu.spec.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/redisinsight/ui/src/components/navigation-menu/NavigationMenu.spec.tsx b/redisinsight/ui/src/components/navigation-menu/NavigationMenu.spec.tsx index 2dfa3c3e57..bcdba4a63d 100644 --- a/redisinsight/ui/src/components/navigation-menu/NavigationMenu.spec.tsx +++ b/redisinsight/ui/src/components/navigation-menu/NavigationMenu.spec.tsx @@ -28,7 +28,16 @@ jest.mock('uiSrc/slices/instances/instances', () => ({ ...jest.requireActual('uiSrc/slices/instances/instances'), connectedInstanceSelector: jest.fn().mockReturnValue({ id: '' - }) + }), +})) + +jest.mock('uiSrc/slices/app/features', () => ({ + ...jest.requireActual('uiSrc/slices/app/features'), + appFeatureFlagsFeaturesSelector: jest.fn().mockReturnValue({ + appSettings: { + flag: true, + }, + }), })) describe('NavigationMenu', () => { From 865bb59d29c72b5282f0624169af1e007b02fcfe Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Fri, 30 Aug 2024 15:46:42 +0200 Subject: [PATCH 040/112] #RI-6026 - support apply functions, filter expressions, reducer functions --- .../constants/supported_commands.json | 419 +++++++++++++++++- .../components/query-wrapper/QueryWrapper.tsx | 4 +- .../pages/search/components/query/Query.tsx | 108 +++-- .../search/components/query/constants.ts | 3 + .../pages/search/components/query/utils.ts | 27 +- redisinsight/ui/src/pages/search/types.ts | 15 +- .../ui/src/pages/search/utils/monaco.ts | 19 +- .../ui/src/pages/search/utils/query.ts | 58 ++- .../ui/src/utils/monaco/monacoThemes.ts | 13 +- .../monaco/monarchTokens/redisearchTokens.ts | 93 +--- .../redisearchTokensTemplates.ts | 104 +++++ .../ui/src/utils/monaco/redisearch/utils.ts | 136 ++++++ 12 files changed, 854 insertions(+), 145 deletions(-) create mode 100644 redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokensTemplates.ts create mode 100644 redisinsight/ui/src/utils/monaco/redisearch/utils.ts diff --git a/redisinsight/ui/src/pages/search/components/constants/supported_commands.json b/redisinsight/ui/src/pages/search/components/constants/supported_commands.json index 4eb75226c2..fa08d11556 100644 --- a/redisinsight/ui/src/pages/search/components/constants/supported_commands.json +++ b/redisinsight/ui/src/pages/search/components/constants/supported_commands.json @@ -68,10 +68,76 @@ "optional": true, "multiple": true, "arguments": [ + { + "name": "reduce", + "token": "REDUCE", + "type": "pure-token" + }, { "name": "function", - "type": "string", - "token": "REDUCE" + "type": "oneof", + "arguments": [ + { + "name": "count", + "type": "pure-token", + "token": "COUNT" + }, + { + "name": "count_distinct", + "type": "pure-token", + "token": "COUNT_DISTINCT" + }, + { + "name": "count_distinctish", + "type": "pure-token", + "token": "COUNT_DISTINCTISH" + }, + { + "name": "sum", + "type": "pure-token", + "token": "SUM" + }, + { + "name": "min", + "type": "pure-token", + "token": "MIN" + }, + { + "name": "max", + "type": "pure-token", + "token": "MAX" + }, + { + "name": "avg", + "type": "pure-token", + "token": "AVG" + }, + { + "name": "stddev", + "type": "pure-token", + "token": "STDDEV" + }, + { + "name": "quantile", + "type": "pure-token", + "token": "QUANTILE" + }, + { + "name": "tolist", + "type": "pure-token", + "token": "TOLIST" + }, + { + "name": "first_value", + "type": "pure-token", + "token": "FIRST_VALUE" + }, + { + "name": "random_sample", + "type": "pure-token", + "token": "RANDOM_SAMPLE" + } + ] }, { "name": "nargs", @@ -147,7 +213,350 @@ { "name": "expression", "type": "string", - "token": "APPLY" + "expression": true, + "token": "APPLY", + "arguments": [ + { + "name": "exists", + "token": "exists", + "type": "function", + "summary": "Checks whether a field exists in a document.", + "arguments": [ + { + "token": "s" + } + ] + }, + { + "name": "log", + "token": "log", + "type": "function", + "summary": "Return the logarithm of a number, property or subexpression", + "arguments": [ + { + "token": "x" + } + ] + }, + { + "name": "abs", + "token": "abs", + "type": "function", + "summary": "Return the absolute number of a numeric expression", + "arguments": [ + { + "token": "x" + } + ] + }, + { + "name": "ceil", + "token": "ceil", + "type": "function", + "summary": "Round to the smallest value not less than x", + "arguments": [ + { + "token": "x" + } + ] + }, + { + "name": "floor", + "token": "floor", + "type": "function", + "summary": "Round to largest value not greater than x", + "arguments": [ + { + "token": "x" + } + ] + }, + { + "name": "log2", + "token": "log2", + "type": "function", + "summary": "Return the logarithm of x to base 2", + "arguments": [ + { + "token": "x" + } + ] + }, + { + "name": "exp", + "token": "exp", + "type": "function", + "summary": "Return the exponent of x, e.g., e^x", + "arguments": [ + { + "token": "x" + } + ] + }, + { + "name": "sqrt", + "token": "sqrt", + "type": "function", + "summary": "Return the square root of x", + "arguments": [ + { + "token": "x" + } + ] + }, + { + "name": "upper", + "token": "upper", + "type": "function", + "summary": "Return the uppercase conversion of s", + "arguments": [ + { + "token": "s" + } + ] + }, + { + "name": "lower", + "token": "lower", + "type": "function", + "summary": "Return the lowercase conversion of s", + "arguments": [ + { + "token": "s" + } + ] + }, + { + "name": "startswith", + "token": "startswith", + "type": "function", + "summary": "Return 1 if s2 is the prefix of s1, 0 otherwise.", + "arguments": [ + { + "token": "s1" + }, + { + "token": "s2" + } + ] + }, + { + "name": "contains", + "token": "contains", + "type": "function", + "summary": "Return the number of occurrences of s2 in s1, 0 otherwise. If s2 is an empty string, return length(s1) + 1.", + "arguments": [ + { + "token": "s1" + }, + { + "token": "s2" + } + ] + }, + { + "name": "strlen", + "token": "strlen", + "type": "function", + "summary": "Return the length of s", + "arguments": [ + { + "token": "s" + } + ] + }, + { + "name": "substr", + "token": "substr", + "type": "function", + "summary": "Return the substring of s, starting at offset and having count characters.If offset is negative, it represents the distance from the end of the string.If count is -1, it means \"the rest of the string starting at offset\".", + "arguments": [ + { + "token": "s" + }, + { + "token": "offset" + }, + { + "token": "count" + } + ] + }, + { + "name": "format", + "token": "format", + "type": "function", + "summary": "Use the arguments following fmt to format a string.Currently the only format argument supported is %s and it applies to all types of arguments.", + "arguments": [ + { + "token": "fmt" + } + ] + }, + { + "name": "matched_terms", + "token": "matched_terms", + "type": "function", + "summary": "Return the query terms that matched for each record (up to 100), as a list. If a limit is specified, Redis will return the first N matches found, based on query order.", + "arguments": [ + { + "token": "max_terms=100", + "optional": true + } + ] + }, + { + "name": "split", + "token": "split", + "type": "function", + "summary": "Split a string by any character in the string sep, and strip any characters in strip. If only s is specified, it is split by commas and spaces are stripped. The output is an array.", + "arguments": [ + { + "token": "s" + } + ] + }, + { + "name": "timefmt", + "token": "timefmt", + "type": "function", + "summary": "Return a formatted time string based on a numeric timestamp value x.", + "arguments": [ + { + "token": "x" + }, + { + "token": "fmt", + "optional": true + } + ] + }, + { + "name": "parsetime", + "token": "parsetime", + "type": "function", + "summary": "The opposite of timefmt() - parse a time format using a given format string", + "arguments": [ + { + "token": "timesharing" + }, + { + "token": "fmt", + "optional": true + } + ] + }, + { + "name": "day", + "token": "day", + "type": "function", + "summary": "Round a Unix timestamp to midnight (00:00) start of the current day.", + "arguments": [ + { + "token": "timestamp" + } + ] + }, + { + "name": "hour", + "token": "hour", + "type": "function", + "summary": "Round a Unix timestamp to the beginning of the current hour.", + "arguments": [ + { + "token": "timestamp" + } + ] + }, + { + "name": "minute", + "token": "minute", + "type": "function", + "summary": "Round a Unix timestamp to the beginning of the current minute.", + "arguments": [ + { + "token": "timestamp" + } + ] + }, + { + "name": "month", + "token": "month", + "type": "function", + "summary": "Round a unix timestamp to the beginning of the current month.", + "arguments": [ + { + "token": "timestamp" + } + ] + }, + { + "name": "dayofweek", + "token": "dayofweek", + "type": "function", + "summary": "Convert a Unix timestamp to the day number (Sunday = 0).", + "arguments": [ + { + "token": "timestamp" + } + ] + }, + { + "name": "dayofmonth", + "token": "dayofmonth", + "type": "function", + "summary": "Convert a Unix timestamp to the day of month number (1 .. 31).", + "arguments": [ + { + "token": "timestamp" + } + ] + }, + { + "name": "dayofyear", + "token": "dayofyear", + "type": "function", + "summary": "Convert a Unix timestamp to the day of year number (0 .. 365).", + "arguments": [ + { + "token": "timestamp" + } + ] + }, + { + "name": "year", + "token": "year", + "type": "function", + "summary": "Convert a Unix timestamp to the current year (e.g. 2018).", + "arguments": [ + { + "token": "timestamp" + } + ] + }, + { + "name": "monthofyear", + "token": "monthofyear", + "type": "function", + "summary": "Convert a Unix timestamp to the current month (0 .. 11).", + "arguments": [ + { + "token": "timestamp" + } + ] + }, + { + "name": "geodistance", + "token": "geodistance", + "type": "function", + "summary": "Return distance in meters.", + "arguments": [ + { + "token": "" + } + ] + } + ] }, { "name": "name", @@ -180,6 +589,7 @@ "name": "filter", "type": "string", "optional": true, + "expression": true, "token": "FILTER" }, { @@ -306,7 +716,8 @@ { "name": "queryword", "type": "pure-token", - "token": "QUERY" + "token": "QUERY", + "expression": true }, { "name": "query", diff --git a/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.tsx b/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.tsx index a52d5e7768..2d47aeaa91 100644 --- a/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.tsx +++ b/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.tsx @@ -17,6 +17,8 @@ import { SUPPORTED_COMMANDS_LIST } from 'uiSrc/pages/search/components/query/con import { SearchCommand } from 'uiSrc/pages/search/types' import { TUTORIALS } from './constants' +import REDIS_COMMANDS_SPEC from '../constants/supported_commands.json' + import Query from '../query' import styles from './styles.module.scss' @@ -36,7 +38,7 @@ const QueryWrapper = (props: Props) => { const { script: scriptContext } = useSelector(appContextSearchAndQuery) const { activeRunQueryMode } = useSelector(searchAndQuerySelector) const { data: indexes = [] } = useSelector(redisearchListSelector) - const { spec: REDIS_COMMANDS_SPEC, commandsArray } = useSelector(appRedisCommandsSelector) + const { commandsArray } = useSelector(appRedisCommandsSelector) const [value, setValue] = useState(scriptContext) diff --git a/redisinsight/ui/src/pages/search/components/query/Query.tsx b/redisinsight/ui/src/pages/search/components/query/Query.tsx index 21cbfa6f2e..dc8ce60263 100644 --- a/redisinsight/ui/src/pages/search/components/query/Query.tsx +++ b/redisinsight/ui/src/pages/search/components/query/Query.tsx @@ -14,13 +14,13 @@ import { setCursorPositionAtTheEnd, splitQueryByArgs } from 'uiSrc/pages/search/utils' -import { SearchCommand, TokenType } from 'uiSrc/pages/search/types' +import { CursorContext, FoundCommandArgument, SearchCommand, TokenType } from 'uiSrc/pages/search/types' import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' import { fetchRedisearchInfoAction } from 'uiSrc/slices/browser/redisearch' import { getRediSearchMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokens' import { installRedisearchTheme, RedisearchMonacoTheme } from 'uiSrc/utils/monaco/monacoThemes' import { useDebouncedEffect } from 'uiSrc/services' -import { options, DefinedArgumentName } from './constants' +import { options, DefinedArgumentName, FIELD_START_SYMBOL } from './constants' import { getFieldsSuggestions, getIndexesSuggestions, @@ -28,6 +28,7 @@ import { getCommandsSuggestions, isIndexComplete, getGeneralSuggestions, + getFunctionsSuggestions, } from './utils' export interface Props { @@ -48,6 +49,7 @@ const Query = (props: Props) => { const disposeCompletionItemProvider = useRef(() => {}) const disposeSignatureHelpProvider = useRef(() => {}) const suggestionsRef = useRef<{ + forceShow?: boolean forceHide: boolean data: monacoEditor.languages.CompletionItem[] }>({ forceHide: false, data: [] }) @@ -145,12 +147,9 @@ const Query = (props: Props) => { const isSuggestionsOpened = () => { const { editor } = monacoObjects.current || {} - if (!editor) return false const suggestController = editor.getContribution('editor.contrib.suggestController') - const suggestModel = suggestController?.model - - return suggestModel?.state === 1 + return suggestController?.model?.state === 1 } const handleCursorChange = () => { @@ -165,6 +164,11 @@ const Query = (props: Props) => { suggestionsRef.current = getSuggestions(editor) + if (!suggestionsRef.current.forceShow) { + editor.trigger('', 'editor.action.triggerParameterHints', '') + return + } + if (suggestionsRef.current.data.length) { helpWidgetRef.current.isOpen = false triggerSuggestions() @@ -209,7 +213,8 @@ const Query = (props: Props) => { const word = model.getWordUntilPosition(position) const range = getRange(position, word) - const { args, isCursorInQuotes, prevCursorChar, nextCursorChar } = splitQueryByArgs(value, offset) + const { args, cursor } = splitQueryByArgs(value, offset) + const { prevCursorChar } = cursor const allArgs = args.flat() const [beforeOffsetArgs, [currentOffsetArg]] = args @@ -233,41 +238,35 @@ const Query = (props: Props) => { setSelectedIndex(allArgs[1] || '') setSelectedCommand(commandName) + if (prevCursorChar === FIELD_START_SYMBOL) { + helpWidgetRef.current.isOpen = false + return asSuggestionsRef(getFieldsSuggestions(attributesRef.current, range), false) + } + + const cursorContext: CursorContext = { ...cursor, currentOffsetArg, offset } const foundArg = findCurrentArgument(COMMANDS_LIST, beforeOffsetArgs) switch (foundArg?.stopArg?.name) { case DefinedArgumentName.index: { - updateHelpWidget(true, command, foundArg?.stopArg) - return getIndexSuggestions(command, prevArgs.length, currentOffsetArg, range) + return handleIndexSuggestions(command, foundArg, prevArgs.length, currentOffsetArg, range) } case DefinedArgumentName.query: { - updateHelpWidget(true, command, foundArg?.stopArg) - return getQuerySuggestions(prevCursorChar, range) + return handleQuerySuggestions(command, foundArg) } default: { - if (isCursorInQuotes || nextCursorChar?.trim()) return asSuggestionsRef([]) - if ((prevCursorChar?.trim() || isCursorInQuotes) && isEscapedSuggestions.current) return asSuggestionsRef([]) - isEscapedSuggestions.current = false - - const { suggestions, forceHide, helpWidgetData } = getGeneralSuggestions( - foundArg, - allArgs, - range, - attributesRef.current - ) - - if (helpWidgetData) updateHelpWidget(helpWidgetData.isOpen, helpWidgetData.parent, helpWidgetData.currentArg) - return asSuggestionsRef(suggestions, forceHide) + return handleCommonSuggestions(value, foundArg, allArgs, cursorContext, range) } } } - const getIndexSuggestions = ( + const handleIndexSuggestions = ( command: SearchCommand, + foundArg: FoundCommandArgument, prevArgsLength: number, currentOffsetArg: Nullable, range: monacoEditor.IRange ) => { + updateHelpWidget(true, command, foundArg?.stopArg) if (currentOffsetArg) return asSuggestionsRef([], false) if (indexesRef.current.length) { const isNextArgQuery = command?.arguments?.[prevArgsLength + 1]?.name === DefinedArgumentName.query @@ -276,16 +275,61 @@ const Query = (props: Props) => { return asSuggestionsRef([]) } - const getQuerySuggestions = ( - prevCursorChar: string, + const handleQuerySuggestions = ( + command: SearchCommand, + foundArg: FoundCommandArgument, + ) => { + updateHelpWidget(true, command, foundArg?.stopArg) + return asSuggestionsRef([], false) + } + + const handleExpressionSuggestions = ( + value: string, + foundArg: FoundCommandArgument, + cursorContext: CursorContext, range: monacoEditor.IRange ) => { - if (prevCursorChar === '@') { - helpWidgetRef.current.isOpen = false - return asSuggestionsRef(getFieldsSuggestions(attributesRef.current, range), false) - } + updateHelpWidget(true, foundArg?.parent, foundArg?.stopArg) - return asSuggestionsRef([], false) + const { isCursorInQuotes, offset, argLeftOffset } = cursorContext + if (!isCursorInQuotes) return asSuggestionsRef([]) + + const stringBeforeCursor = value.substring(argLeftOffset, offset) || '' + const { args } = splitQueryByArgs( + stringBeforeCursor.replace(/^["']|["']$/g, ''), + offset - argLeftOffset + ) + const [, [currentArg]] = args + + const functions = foundArg?.stopArg?.arguments ?? [] + + const suggestions = getFunctionsSuggestions(functions, range) + const isStartsWithFunction = functions.some(({ token }) => token?.startsWith(currentArg)) + return asSuggestionsRef(suggestions, true, isStartsWithFunction) + } + + const handleCommonSuggestions = ( + value: string, + foundArg: Nullable, + allArgs: string[], + cursorContext: CursorContext, + range: monacoEditor.IRange + ) => { + if (foundArg?.stopArg?.expression) return handleExpressionSuggestions(value, foundArg, cursorContext, range) + + const { prevCursorChar, nextCursorChar, isCursorInQuotes } = cursorContext + if (isCursorInQuotes || nextCursorChar?.trim()) return asSuggestionsRef([]) + if ((prevCursorChar?.trim() || isCursorInQuotes) && isEscapedSuggestions.current) return asSuggestionsRef([]) + + const { suggestions, forceHide, helpWidgetData } = getGeneralSuggestions( + foundArg, + allArgs, + range, + attributesRef.current + ) + + if (helpWidgetData) updateHelpWidget(helpWidgetData.isOpen, helpWidgetData.parent, helpWidgetData.currentArg) + return asSuggestionsRef(suggestions, forceHide) } return ( diff --git a/redisinsight/ui/src/pages/search/components/query/constants.ts b/redisinsight/ui/src/pages/search/components/query/constants.ts index f06c41a66c..22dcf561d6 100644 --- a/redisinsight/ui/src/pages/search/components/query/constants.ts +++ b/redisinsight/ui/src/pages/search/components/query/constants.ts @@ -16,4 +16,7 @@ export enum DefinedArgumentName { index = 'index', query = 'query', field = 'field', + expression = 'expression' } + +export const FIELD_START_SYMBOL = '@' diff --git a/redisinsight/ui/src/pages/search/components/query/utils.ts b/redisinsight/ui/src/pages/search/components/query/utils.ts index 10f1527fb2..4e7964b2c2 100644 --- a/redisinsight/ui/src/pages/search/components/query/utils.ts +++ b/redisinsight/ui/src/pages/search/components/query/utils.ts @@ -1,7 +1,7 @@ import { monaco } from 'react-monaco-editor' import * as monacoEditor from 'monaco-editor' import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' -import { bufferToString, formatLongName, getCommandMarkdown, Nullable } from 'uiSrc/utils' +import { bufferToString, formatLongName, generateArgsForInsertText, getCommandMarkdown, Nullable } from 'uiSrc/utils' import { buildSuggestion, generateDetail, @@ -10,9 +10,14 @@ import { import { FoundCommandArgument, SearchCommand } from 'uiSrc/pages/search/types' import { DefinedArgumentName } from 'uiSrc/pages/search/components/query/constants' -export const asSuggestionsRef = (suggestions: monacoEditor.languages.CompletionItem[], forceHide = true) => ({ +export const asSuggestionsRef = ( + suggestions: monacoEditor.languages.CompletionItem[], + forceHide = true, + forceShow = true +) => ({ data: suggestions, - forceHide + forceHide, + forceShow }) export const getIndexesSuggestions = (indexes: RedisResponseBuffer[], range: monaco.IRange, nextQoutes = true) => @@ -43,6 +48,22 @@ export const getFieldsSuggestions = (fields: any[], range: monaco.IRange, spaceA } }) +const insertFunctionArguments = (args: SearchCommand[]) => + generateArgsForInsertText( + args.map(({ token, optional }) => (optional ? `[${token}]` : (token || ''))) as string[], + ', ' + ) + +export const getFunctionsSuggestions = (functions: SearchCommand[], range: monaco.IRange) => functions + .map(({ token, summary, arguments: args }) => ({ + label: token || '', + insertText: `${token}(${insertFunctionArguments(args || [])})`, + insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, + range, + kind: monacoEditor.languages.CompletionItemKind.Function, + detail: summary + })) + export const getCommandsSuggestions = (commands: SearchCommand[], range: monaco.IRange) => asSuggestionsRef( commands.map((command) => buildSuggestion(command, range, { detail: generateDetail(command), diff --git a/redisinsight/ui/src/pages/search/types.ts b/redisinsight/ui/src/pages/search/types.ts index a2d96a5a4d..f1d9287048 100644 --- a/redisinsight/ui/src/pages/search/types.ts +++ b/redisinsight/ui/src/pages/search/types.ts @@ -3,7 +3,8 @@ import { Maybe } from 'uiSrc/utils' export enum TokenType { PureToken = 'pure-token', Block = 'block', - OneOf = 'oneof' + OneOf = 'oneof', + String = 'string', } export enum ArgName { @@ -12,6 +13,8 @@ export enum ArgName { export interface SearchCommand { name?: string + summary?: string + expression?: boolean type?: TokenType token?: string optional?: boolean @@ -30,3 +33,13 @@ export interface FoundCommandArgument { append: Maybe> parent: Maybe } + +export interface CursorContext { + prevCursorChar: string + nextCursorChar: string + isCursorInQuotes: boolean + currentOffsetArg: string + offset: number + argLeftOffset: number + argRightOffset: number +} diff --git a/redisinsight/ui/src/pages/search/utils/monaco.ts b/redisinsight/ui/src/pages/search/utils/monaco.ts index 41712e9568..bfc1d6ea08 100644 --- a/redisinsight/ui/src/pages/search/utils/monaco.ts +++ b/redisinsight/ui/src/pages/search/utils/monaco.ts @@ -25,14 +25,17 @@ export const getRange = (position: monaco.Position, word: monaco.editor.IWordAtP startColumn: word.startColumn, }) -export const buildSuggestion = (arg: SearchCommand, range: monaco.IRange, options: any = {}) => ({ - label: isString(arg) ? arg : arg.token || arg.arguments?.[0].token || arg.name || '', - insertText: `${arg.token || arg.arguments?.[0].token || arg.name?.toUpperCase() || ''} `, - insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, - range, - kind: options?.kind || monacoEditor.languages.CompletionItemKind.Function, - ...options, -}) +export const buildSuggestion = (arg: SearchCommand, range: monaco.IRange, options: any = {}) => { + const extraQuotes = arg.expression ? '"$1"' : '' + return { + label: isString(arg) ? arg : arg.token || arg.arguments?.[0].token || arg.name || '', + insertText: `${arg.token || arg.arguments?.[0].token || arg.name?.toUpperCase() || ''} ${extraQuotes}`, + insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, + range, + kind: options?.kind || monacoEditor.languages.CompletionItemKind.Function, + ...options, + } +} export const getRediSearchSignutureProvider = (options: Maybe<{ isOpen: boolean diff --git a/redisinsight/ui/src/pages/search/utils/query.ts b/redisinsight/ui/src/pages/search/utils/query.ts index 89178a4fcf..a66bdb2596 100644 --- a/redisinsight/ui/src/pages/search/utils/query.ts +++ b/redisinsight/ui/src/pages/search/utils/query.ts @@ -13,6 +13,8 @@ export const splitQueryByArgs = (query: string, position: number = 0) => { let quoteChar = '' let isCursorInQuotes = false let lastArg = '' + let argLeftOffset = 0 + let argRightOffset = 0 const pushToProperTuple = (isAfterOffset: boolean, arg: string) => { lastArg = arg @@ -24,6 +26,11 @@ export const splitQueryByArgs = (query: string, position: number = 0) => { argsBySide[argsBySide.length - 1] = `${argsBySide[argsBySide.length - 1]} ${arg}` } + const updateArgOffsets = (left: number, right: number) => { + argLeftOffset = left + argRightOffset = right + } + for (let i = 0; i < query.length; i++) { const char = query[i] const isAfterOffset = i >= position + (inQuotes ? -1 : 0) @@ -38,6 +45,10 @@ export const splitQueryByArgs = (query: string, position: number = 0) => { inQuotes = false const argWithChat = arg + char + if (isAfterOffset && !argLeftOffset) { + updateArgOffsets(i - arg.length, i + 1) + } + if (isCompositeArgument(argWithChat, lastArg)) { updateLastArgument(isAfterOffset, argWithChat) } else { @@ -54,6 +65,10 @@ export const splitQueryByArgs = (query: string, position: number = 0) => { arg += char } else if (char === ' ' || char === '\n') { if (arg.length > 0) { + if (isAfterOffset && !argLeftOffset) { + updateArgOffsets(i - arg.length, i) + } + if (isCompositeArgument(arg, lastArg)) { updateLastArgument(isAfterOffset, arg) } else { @@ -70,10 +85,19 @@ export const splitQueryByArgs = (query: string, position: number = 0) => { } if (arg.length > 0) { + if (!argLeftOffset) updateArgOffsets(query.length - arg.length, query.length) pushToProperTuple(true, arg) } - return { args, isCursorInQuotes, prevCursorChar: query[position - 1], nextCursorChar: query[position] } + const cursor = { + isCursorInQuotes, + prevCursorChar: query[position - 1], + nextCursorChar: query[position], + argLeftOffset, + argRightOffset + } + + return { args, cursor } } export const findCurrentArgument = ( @@ -91,9 +115,7 @@ export const findCurrentArgument = ( } const tokenIndex = args.findIndex((cArg) => - (cArg.type === TokenType.OneOf - ? cArg.arguments?.some((oneOfArg: SearchCommand) => oneOfArg.token?.toLowerCase() === arg.toLowerCase()) - : cArg.token?.toLowerCase() === arg.toLowerCase())) + cArg.token?.toLowerCase() === arg.toLowerCase()) const token = args[tokenIndex] if (token) { @@ -123,6 +145,7 @@ const findStopArgumentInQuery = ( let currentCommandArgIndex = 0 let isBlockedOnCommand = false let multipleIndexStart = 0 + let multipleCountNumber = 0 const moveToNextCommandArg = () => currentCommandArgIndex++ const blockCommand = () => { isBlockedOnCommand = true } @@ -202,11 +225,14 @@ const findStopArgumentInQuery = ( } if (currentCommandArg?.multiple) { - const numberOfArgs = toNumber(queryArgs[currentCommandArgIndex]) || 0 + if (!multipleIndexStart) { + multipleCountNumber = toNumber(queryArgs[i - 1]) + multipleIndexStart = i - 1 + } - if (!multipleIndexStart) multipleIndexStart = currentCommandArgIndex - if (i - multipleIndexStart >= numberOfArgs) { + if (i - multipleIndexStart >= multipleCountNumber) { skipArg() + multipleIndexStart = 0 continue } @@ -271,7 +297,9 @@ export const getArgumentSuggestions = ( } } - const beforeMandatoryOptionalArgs = getAllRestArguments(current, stopArgument, pastStringArgs) + // if we finished argument - stopArgument will be undefined, then we get it as token + const lastArgument = stopArgument ?? restArguments[0] + const beforeMandatoryOptionalArgs = getAllRestArguments(current, lastArgument, pastStringArgs) const requiredArgsLength = restNotFilledArgs.filter((arg) => !arg.optional).length return { @@ -286,20 +314,20 @@ export const getRestArguments = ( current: Maybe, stopArgument: Nullable ): SearchCommandTree[] => { - if (!stopArgument) return [] - const argumentIndexInArg = current?.arguments - ?.findIndex(({ name }) => name === stopArgument?.name) || -1 - const nextMandatoryIndex = current?.arguments - ?.findIndex(({ optional }, i) => !optional && i > argumentIndexInArg) || -1 + ?.findIndex(({ name }) => name === stopArgument?.name) + const nextMandatoryIndex = argumentIndexInArg && argumentIndexInArg > -1 ? current?.arguments + ?.findIndex(({ optional }, i) => !optional && i > argumentIndexInArg) : -1 const beforeMandatoryOptionalArgs = ( - nextMandatoryIndex > -1 + nextMandatoryIndex && nextMandatoryIndex > -1 ? current?.arguments?.slice(argumentIndexInArg, nextMandatoryIndex) : current?.arguments?.filter(({ optional }) => optional) ) || [] - const nextMandatoryArg = current?.arguments?.[nextMandatoryIndex] + const nextMandatoryArg = nextMandatoryIndex && nextMandatoryIndex > -1 + ? current?.arguments?.[nextMandatoryIndex] + : undefined if (nextMandatoryArg?.token) { beforeMandatoryOptionalArgs.unshift(nextMandatoryArg) diff --git a/redisinsight/ui/src/utils/monaco/monacoThemes.ts b/redisinsight/ui/src/utils/monaco/monacoThemes.ts index 0e7f41f56f..3982d4e651 100644 --- a/redisinsight/ui/src/utils/monaco/monacoThemes.ts +++ b/redisinsight/ui/src/utils/monaco/monacoThemes.ts @@ -11,15 +11,20 @@ export const installRedisearchTheme = () => { inherit: true, rules: [ { token: 'keyword', foreground: '#569cd6', fontStyle: 'bold' }, - // { token: 'argument.token', foreground: '#6db9a2' }, { token: 'argument.block.0', foreground: '#66ccaf' }, { token: 'argument.block.1', foreground: '#459d7f' }, { token: 'argument.block.2', foreground: '#3c816a' }, { token: 'argument.block.3', foreground: '#28644f' }, + { token: 'argument.block.withToken.0', foreground: '#66ccaf' }, + { token: 'argument.block.withToken.1', foreground: '#459d7f' }, + { token: 'argument.block.withToken.2', foreground: '#3c816a' }, + { token: 'argument.block.withToken.3', foreground: '#28644f' }, { token: 'loadAll', foreground: '#6db9a2' }, { token: 'index', foreground: '#ce51cc' }, { token: 'query', foreground: '#5183ce' }, { token: 'field', foreground: '#c43265' }, + { token: 'query.operator', foreground: '#a4e7df' }, + { token: 'function', foreground: '#aa58d2' }, ], colors: {} }) @@ -33,10 +38,16 @@ export const installRedisearchTheme = () => { { token: 'argument.block.1', foreground: '#459d7f' }, { token: 'argument.block.2', foreground: '#3c816a' }, { token: 'argument.block.3', foreground: '#28644f' }, + { token: 'argument.block.withToken.0', foreground: '#66ccaf' }, + { token: 'argument.block.withToken.1', foreground: '#459d7f' }, + { token: 'argument.block.withToken.2', foreground: '#3c816a' }, + { token: 'argument.block.withToken.3', foreground: '#28644f' }, { token: 'loadAll', foreground: '#6db9a2' }, { token: 'index', foreground: '#ce51cc' }, { token: 'field', foreground: '#5183ce' }, { token: 'field', foreground: '#c43265' }, + { token: 'query.operator', foreground: '#a4e7df' }, + { token: 'function', foreground: '#aa58d2' }, ], colors: {} }) diff --git a/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts b/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts index 9d91952d2d..a4814e00c3 100644 --- a/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts +++ b/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts @@ -1,54 +1,16 @@ import { monaco as monacoEditor } from 'react-monaco-editor' -import { SearchCommand, TokenType } from 'uiSrc/pages/search/types' -import { DefinedArgumentName } from 'uiSrc/pages/search/components/query/constants' +import { SearchCommand } from 'uiSrc/pages/search/types' +import { + generateKeywords, + generateTokens, + generateTokensWithFunctions, + getBlockTokens, + isQueryAfterIndex +} from 'uiSrc/utils/monaco/redisearch/utils' +import { generateQuery } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokensTemplates' const STRING_DOUBLE = 'string.double' -const generateKeywords = (commands: SearchCommand[]) => commands.map(({ name }) => name) -const generateTokens = (command?: SearchCommand) => { - if (!command) return [] - const levels: Array> = [] - - function processArguments(args: SearchCommand[], level = 0) { - // Ensure the current level exists in the levels array - if (!levels[level]) { - levels[level] = [] - } - - args.forEach((arg) => { - if (arg.token) levels[level].push(arg.token) - - if (arg.type === TokenType.Block && arg.arguments) { - const blockToken = arg.arguments[0].token - const nextArgs = arg.arguments - if (blockToken) { - levels[level].push(blockToken) - } - processArguments(blockToken ? nextArgs.slice(1, nextArgs.length) : nextArgs, level + 1) - } - - if (arg.type === TokenType.OneOf && arg.arguments) { - arg.arguments.forEach((choice) => { - if (choice.token) levels[level].push(choice.token) - }) - } - }) - } - - if (command.arguments) { - processArguments(command.arguments, 0) - } - - return levels -} - -const isQueryAfterIndex = (command?: SearchCommand) => { - if (!command) return false - - const index = command.arguments?.findIndex(({ name }) => name === DefinedArgumentName.index) || -2 - return command.arguments?.[index + 1]?.name === DefinedArgumentName.query -} - export const getRediSearchMonarchTokensProvider = ( commands: SearchCommand[], command?: string @@ -77,6 +39,7 @@ export const getRediSearchMonarchTokensProvider = ( { include: '@keyword' }, [/LOAD\s+\*/, 'loadAll'], { include: '@argument.block' }, + { include: '@argument.block.withFunctions' }, [/[;,.]/, 'delimiter'], [/[()]/, '@brackets'], [ @@ -93,45 +56,15 @@ export const getRediSearchMonarchTokensProvider = ( keyword: [ [`(${keywords.join('|')})\\b`, { token: 'keyword', next: '@index' }] ], - 'argument.block': argTokens.map((tokens, lvl) => [`(${tokens.join('|')})\\b`, `argument.block.${lvl}`]), + 'argument.block': getBlockTokens(argTokens?.pureTokens), + ...generateTokensWithFunctions(argTokens?.tokensWithQueryAfter), index: [ [/"([^"\\]|\\.)*"/, { token: 'index', next: isHighlightQuery ? '@query' : '@root' }], [/'([^'\\]|\\.)*'/, { token: 'index', next: isHighlightQuery ? '@query' : '@root' }], [/[a-zA-Z_]\w*/, { token: 'index', next: isHighlightQuery ? '@query' : '@root' }], { include: 'root' } // Fallback to the root state if nothing matches ], - query: [ - [/"/, { token: 'query', next: '@queryInsideDouble' }], - [/'/, { token: 'query', next: '@queryInsideSingle' }], - [/[a-zA-Z_]\w*/, { token: 'query', next: '@root' }], - { include: 'root' } // Fallback to the root state if nothing matches - ], - queryInsideDouble: [ - [/@/, { token: 'field', next: '@fieldInDouble' }], - [/\\"/, { token: 'query', next: 'queryInsideDouble' }], - [/"/, { token: 'query', next: '@root' }], - [/./, { token: 'query', next: '@queryInsideDouble' }], - { include: '@query' } // Fallback to the root state if nothing matches - ], - queryInsideSingle: [ - [/@/, { token: 'field', next: '@fieldInSingle' }], - [/\\'/, { token: 'query', next: 'queryInsideSingle' }], - [/'/, { token: 'query', next: '@root' }], - [/./, { token: 'query', next: '@queryInsideSingle' }], - { include: '@query' } // Fallback to the root state if nothing matches - ], - fieldInDouble: [ - [/\w+/, { token: 'field', next: '@queryInsideDouble' }], - [/\s+/, { token: '@rematch', next: '@queryInsideDouble' }], - [/"/, { token: 'query', next: '@root' }], - { include: '@query' } // Fallback to the root state if nothing matches - ], - fieldInSingle: [ - [/\w+/, { token: 'field', next: '@queryInsideSingle' }], - [/\s+/, { token: '@rematch', next: '@queryInsideSingle' }], - [/'/, { token: 'query', next: '@root' }], - { include: '@query' } // Fallback to the root state if nothing matches - ], + ...generateQuery(), whitespace: [ [/\s+/, 'white'], [/\/\/.*$/, 'comment'], diff --git a/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokensTemplates.ts b/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokensTemplates.ts new file mode 100644 index 0000000000..9c2901029f --- /dev/null +++ b/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokensTemplates.ts @@ -0,0 +1,104 @@ +import { languages } from 'monaco-editor' +import { curryRight } from 'lodash' +import { SearchCommand } from 'uiSrc/pages/search/types' +import { Maybe } from 'uiSrc/utils' + +const appendToken = (token: string, name: Maybe) => (name ? `${token}.${name}` : token) +export const generateQuery = ( + argToken?: SearchCommand, + args?: SearchCommand[] +): { [name: string]: languages.IMonarchLanguageRule[] } => { + const curriedAppendToken = curryRight(appendToken) + const appendTokenName = curriedAppendToken(argToken?.token) + + const getFunctionsTokens = (tokenName: string): languages.IMonarchLanguageRule => (args?.length ? [ + `(${args?.map(({ token }) => token).join('|')})\\b`, { token: 'function', next: appendTokenName(tokenName) } + ] : [/_/, '']) + + return { + [appendTokenName('query')]: [ + [/"/, { token: appendTokenName('query'), next: appendTokenName('@query.inside.double') }], + [/'/, { token: appendTokenName('query'), next: appendTokenName('@query.inside.single') }], + [/[a-zA-Z_]\w*/, { token: appendTokenName('query'), next: '@root' }], + { include: 'root' } // Fallback to the root state if nothing matches + ], + [appendTokenName('query.inside.double')]: [ + [/@/, { token: 'field', next: appendTokenName('@field.inside.double') }], + [/\\"/, { token: 'query', next: appendTokenName('@query.inside.double') }], + [/==|!=|<=|>=|<|>/, { token: 'query.operator' }], + [/&&|\|\|/, { token: 'query.operator' }], + getFunctionsTokens('@function.inside.double'), + [/"/, { token: appendTokenName('query'), next: '@root' }], + [/./, { token: appendTokenName('query'), next: appendTokenName('@query.inside.double') }], + { include: '@query' } // Fallback to the root state if nothing matches + ], + [appendTokenName('query.inside.single')]: [ + [/@/, { token: 'field', next: appendTokenName('@field.inside.single') }], + [/\\'/, { token: appendTokenName('query'), next: appendTokenName('query.inside.single') }], + [/==|!=|<=|>=|<|>/, { token: 'query.operator' }], + [/&&|\|\|/, { token: 'query.operator' }], + getFunctionsTokens('@function.inside.single'), + [/'/, { token: appendTokenName('query'), next: '@root' }], + [/./, { token: appendTokenName('query'), next: appendTokenName('@query.inside.single') }], + { include: appendTokenName('@query') } // Fallback to the root state if nothing matches + ], + [appendTokenName('field.inside.double')]: [ + [/\w+/, { token: 'field', next: appendTokenName('@query.inside.double') }], + [/\s+/, { token: '@rematch', next: appendTokenName('@query.inside.double') }], + [/"/, { token: appendTokenName('query'), next: '@root' }], + { include: appendTokenName('@query') } // Fallback to the root state if nothing matches + ], + [appendTokenName('field.inside.single')]: [ + [/\w+/, { token: 'field', next: appendTokenName('@query.inside.single') }], + [/\s+/, { token: '@rematch', next: appendTokenName('@query.inside.single') }], + [/'/, { token: appendTokenName('query'), next: '@root' }], + + { include: appendTokenName('@query') } + ], + [appendTokenName('function.inside.double')]: [ + [/\s+/, 'white'], // Handle whitespace + [/\(/, { token: 'delimiter.parenthesis', next: appendTokenName('@function.args.double') }], + { include: appendTokenName('@query') } + ], + [appendTokenName('function.inside.double')]: [ + [/\s+/, 'white'], // Handle whitespace + [/\(/, { token: 'delimiter.parenthesis', next: appendTokenName('@function.args.double') }], + { include: appendTokenName('@query') } + ], + [appendTokenName('function.args.double')]: [ + [/\)/, { token: 'delimiter.parenthesis', next: appendTokenName('@query.inside.double') }], + [/,/, 'delimiter.comma'], // Match commas between arguments + getFunctionsTokens('@function.inside.double'), + [/[a-zA-Z_]\w*/, { token: 'parameter' }], // Highlight parameters + [/\s+/, 'white'], // Handle whitespace + [/@\w+/, { token: 'field' }], + + // // Handle strings with escaped quotes + [/\\"/, 'parameter'], // Match escaped double quote + [/\\'/, 'parameter'], // Match escaped single quote + [/'/, 'parameter'], // Match escaped single quote + + { include: appendTokenName('@query') } // Fallback to root state + ], + [appendTokenName('function.inside.single')]: [ + [/\s+/, 'white'], // Handle whitespace + [/\(/, { token: 'delimiter.parenthesis', next: appendTokenName('@function.args.single') }], + { include: appendTokenName('@query') } + ], + [appendTokenName('function.args.single')]: [ + [/\)/, { token: 'delimiter.parenthesis', next: appendTokenName('@query.inside.single') }], + [/,/, 'delimiter.comma'], // Match commas between arguments + getFunctionsTokens('@function.inside.single'), + [/[a-zA-Z_]\w*/, { token: 'parameter' }], // Highlight parameters + [/\s+/, 'white'], // Handle whitespace + [/@\w+/, { token: 'field' }], + + [/"/, 'parameter'], // Match escaped double quote + // // Handle strings with escaped quotes + [/\\"/, 'parameter'], // Match escaped double quote + [/\\'/, 'parameter'], // Match escaped single quote + + { include: appendTokenName('@query') } // Fallback to root state + ] + } +} diff --git a/redisinsight/ui/src/utils/monaco/redisearch/utils.ts b/redisinsight/ui/src/utils/monaco/redisearch/utils.ts new file mode 100644 index 0000000000..c65b4bb80a --- /dev/null +++ b/redisinsight/ui/src/utils/monaco/redisearch/utils.ts @@ -0,0 +1,136 @@ +import { isNumber, remove } from 'lodash' +import { languages } from 'monaco-editor' +import { SearchCommand, TokenType } from 'uiSrc/pages/search/types' +import { Maybe, Nullable } from 'uiSrc/utils' +import { DefinedArgumentName } from 'uiSrc/pages/search/components/query/constants' +import { generateQuery } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokensTemplates' + +export const generateKeywords = (commands: SearchCommand[]) => commands.map(({ name }) => name) +export const generateTokens = (command?: SearchCommand): Nullable<{ + pureTokens: Array> + tokensWithQueryAfter: Array> +}> => { + if (!command) return null + const pureTokens: Array> = [] + const tokensWithQueryAfter: Array> = [] + + function processArguments(args: SearchCommand[], level = 0) { + if (!pureTokens[level]) pureTokens[level] = [] + if (!tokensWithQueryAfter[level]) tokensWithQueryAfter[level] = [] + + args.forEach((arg) => { + if (arg.token) pureTokens[level].push(arg) + + if (arg.type === TokenType.Block && arg.arguments) { + const blockToken = arg.arguments[0] + const nextArgs = arg.arguments + const isArgHasOwnSyntax = arg.arguments[0].expression && !!arg.arguments[0].arguments?.length + + if (blockToken?.token) { + if (isArgHasOwnSyntax) { + tokensWithQueryAfter[level].push({ + token: blockToken, + arguments: arg.arguments[0].arguments as SearchCommand[] + }) + } else { + pureTokens[level].push(blockToken) + } + } + + processArguments(blockToken ? nextArgs.slice(1, nextArgs.length) : nextArgs, level + 1) + } + + if (arg.type === TokenType.OneOf && arg.arguments) { + arg.arguments.forEach((choice) => { + if (choice?.token) pureTokens[level].push(choice) + }) + } + }) + } + + if (command.arguments) { + processArguments(command.arguments, 0) + } + + return { pureTokens, tokensWithQueryAfter } +} + +export const isQueryAfterIndex = (command?: SearchCommand) => { + if (!command) return false + + const index = command.arguments?.findIndex(({ name }) => name === DefinedArgumentName.index) + return isNumber(index) && index > -1 ? command.arguments?.[index + 1]?.name === DefinedArgumentName.query : false +} + +export const appendTokenWithQuery = ( + args: Array<{ token: SearchCommand, arguments: SearchCommand[] }>, + level: number +): languages.IMonarchLanguageRule[] => + args.map(({ token }) => [`(${token.token})\\b`, { token: `argument.block.${level}`, next: `@query.${token.token}` }]) + +export const appendQueryWithNextFunctions = (tokens: Array<{ token: SearchCommand, arguments: SearchCommand[] }>): { + [name: string]: languages.IMonarchLanguageRule[] +} => { + let result: { [name: string]: languages.IMonarchLanguageRule[] } = {} + + tokens.forEach(({ token, arguments: args }) => { + result = { + ...result, + ...generateQuery(token, args) + } + }) + + return result +} + +export const generateTokensWithFunctions = ( + tokens?: Array> +): { + [name: string]: languages.IMonarchLanguageRule[] +} => { + if (!tokens) return { 'argument.block.withFunctions': [] } + + const actualTokens = tokens.filter((tokens) => tokens.length) + + return { + 'argument.block.withFunctions': [ + ...actualTokens + .map((tokens, lvl) => appendTokenWithQuery(tokens, lvl)) + .flat() + ], + ...appendQueryWithNextFunctions(actualTokens.flat()) + } +} + +export const getBlockTokens = ( + pureTokens: Maybe[]> +): languages.IMonarchLanguageRule[] => { + if (!pureTokens) return [] + + const getLeveledToken = ( + tokens: SearchCommand[], + lvl: number + ): languages.IMonarchLanguageRule[] => { + const result: languages.IMonarchLanguageRule[] = [] + const restTokens = [...tokens] + const tokensWithNextExpression = remove(restTokens, (({ expression }) => expression)) + + if (tokensWithNextExpression.length) { + result.push([ + `(${tokensWithNextExpression.map(({ token }) => token).join('|')})\\b`, + { + token: `argument.block.${lvl}`, + next: '@query' + }, + ]) + } + + if (restTokens.length) { + result.push([`(${restTokens.map(({ token }) => token).join('|')})\\b`, { token: `argument.block.${lvl}`, next: '@root' }]) + } + + return result + } + + return pureTokens.map((tokens, lvl) => getLeveledToken(tokens, lvl)).flat() +} From 063e093023bbf7d3f53f7bce4afba3a43cfa5ec0 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Tue, 3 Sep 2024 18:26:33 +0200 Subject: [PATCH 041/112] #RI-6026 - add tests, fix tests --- .../search/components/query/utils.spec.ts | 81 +++--- .../pages/search/components/query/utils.ts | 2 +- .../ui/src/pages/search/utils/query.ts | 21 +- .../pages/search/utils/tests/query.spec.ts | 246 +++++++++++------- .../cyber/monarchTokensProvider.spec.ts | 17 ++ 5 files changed, 225 insertions(+), 142 deletions(-) diff --git a/redisinsight/ui/src/pages/search/components/query/utils.spec.ts b/redisinsight/ui/src/pages/search/components/query/utils.spec.ts index bb71924265..ded3afcccd 100644 --- a/redisinsight/ui/src/pages/search/components/query/utils.spec.ts +++ b/redisinsight/ui/src/pages/search/components/query/utils.spec.ts @@ -1,35 +1,32 @@ import { getGeneralSuggestions, isIndexComplete } from 'uiSrc/pages/search/components/query/utils' import { MOCKED_SUPPORTED_COMMANDS } from 'uiSrc/pages/search/mocks/mocks' -import { buildSuggestion } from 'uiSrc/pages/search/utils' -import { SearchCommand } from 'uiSrc/pages/search/types' +import { addOwnTokenToArgs, buildSuggestion, findCurrentArgument } from 'uiSrc/pages/search/utils' +import { SearchCommand, TokenType } from 'uiSrc/pages/search/types' const ftAggregate = MOCKED_SUPPORTED_COMMANDS['FT.AGGREGATE'] -const ftProfile = MOCKED_SUPPORTED_COMMANDS['FT.PROFILE'] -const getCursorContext = () => ({ - currentOffsetArg: undefined, - isCursorInQuotes: false, - nextCursorChar: undefined, - prevCursorChar: ' ', - range: {} -}) +const commands = Object.keys(MOCKED_SUPPORTED_COMMANDS) + .map((key) => ({ + ...addOwnTokenToArgs(key, MOCKED_SUPPORTED_COMMANDS[key]), + token: key, + type: TokenType.Block + })) + +const ftAggregateAppend = ftAggregate.arguments.slice(2) + .map((arg) => ({ ...arg, parent: ftAggregate })) const getGeneralSuggestionsTests = [ { input: { - commandContext: { - allArgs: ['FT.AGGREGATE', '""', '""'], - command: ftAggregate, - commandName: 'FT.AGGREGATE', - currentCommandArg: null, - prevArgs: ['""', '""'] - }, - cursorContext: getCursorContext() + foundArg: findCurrentArgument( + commands, + ['FT.AGGREGATE', '""', '""'] + ), + allArgs: ['FT.AGGREGATE', '""', '""'] }, result: { helpWidgetData: expect.any(Object), - suggestions: ftAggregate.arguments - .slice(2) + suggestions: ftAggregateAppend .map((arg) => ({ ...buildSuggestion(arg as SearchCommand, {} as any), sortText: expect.any(String), @@ -40,14 +37,11 @@ const getGeneralSuggestionsTests = [ }, { input: { - commandContext: { - allArgs: ['FT.AGGREGATE', '""', '""', 'APPLY', 'expression'], - command: ftAggregate, - commandName: 'FT.AGGREGATE', - currentCommandArg: null, - prevArgs: ['""', '""', 'APPLY', 'expression'] - }, - cursorContext: getCursorContext() + foundArg: findCurrentArgument( + commands, + ['FT.AGGREGATE', '""', '""', 'APPLY', 'expression'] + ), + allArgs: ['FT.AGGREGATE', '""', '""', 'APPLY', 'expression'] }, result: { helpWidgetData: expect.any(Object), @@ -65,14 +59,11 @@ const getGeneralSuggestionsTests = [ }, { input: { - commandContext: { - allArgs: ['FT.PROFILE', '""'], - command: ftProfile, - commandName: 'FT.PROFILE', - currentCommandArg: null, - prevArgs: ['""'] - }, - cursorContext: getCursorContext() + foundArg: findCurrentArgument( + commands, + ['FT.PROFILE', '""'] + ), + allArgs: ['FT.PROFILE', '""'] }, result: { helpWidgetData: expect.any(Object), @@ -83,7 +74,7 @@ const getGeneralSuggestionsTests = [ insertTextRules: 4, range: expect.any(Object), kind: undefined, - detail: '', + detail: expect.any(String), }, { label: 'AGGREGATE', @@ -91,16 +82,26 @@ const getGeneralSuggestionsTests = [ insertTextRules: 4, range: expect.any(Object), kind: undefined, - detail: '', + detail: expect.any(String), } ] } - } + }, ] describe('getGeneralSuggestions', () => { it.each(getGeneralSuggestionsTests)('should properly return suggestions', ({ input, result }) => { - const testResult = getGeneralSuggestions(input.commandContext, input.cursorContext, []) + const testResult = getGeneralSuggestions( + input.foundArg as any, + input.allArgs, + {} as any, + [] + ) + + console.log(findCurrentArgument( + commands, + ['FT.AGGREGATE', '""', '""'] + )) expect(testResult).toEqual(result) }) diff --git a/redisinsight/ui/src/pages/search/components/query/utils.ts b/redisinsight/ui/src/pages/search/components/query/utils.ts index 4e7964b2c2..3c3def04ed 100644 --- a/redisinsight/ui/src/pages/search/components/query/utils.ts +++ b/redisinsight/ui/src/pages/search/components/query/utils.ts @@ -86,7 +86,7 @@ export const getMandatoryArgumentSuggestions = ( if (foundArg.isBlocked) return [] if (foundArg.append?.length) { - return foundArg.append.flat().map((arg: any) => buildSuggestion(arg, range, { + return foundArg.append[0].map((arg: any) => buildSuggestion(arg, range, { kind: monacoEditor.languages.CompletionItemKind.Property, detail: generateDetail(foundArg?.parent) })) diff --git a/redisinsight/ui/src/pages/search/utils/query.ts b/redisinsight/ui/src/pages/search/utils/query.ts index a66bdb2596..921c1f5674 100644 --- a/redisinsight/ui/src/pages/search/utils/query.ts +++ b/redisinsight/ui/src/pages/search/utils/query.ts @@ -125,7 +125,7 @@ export const findCurrentArgument = ( // getArgByRest - here we preparing the list of arguments which can be inserted, // this is the main function which creates the list of arguments return { - ...getArgumentSuggestions(pastArgs, commandArgs, parent), + ...getArgumentSuggestions({ tokenArgs: pastArgs, levelArgs: prev }, commandArgs, parent), parent: parent || token } } @@ -242,9 +242,7 @@ const findStopArgumentInQuery = ( moveToNextCommandArg() - const nextCommand = restCommandArgs[currentCommandArgIndex + 1] - const currentCommand = restCommandArgs[currentCommandArgIndex] - isBlockedOnCommand = [currentCommand, nextCommand].every((arg) => arg && !arg.optional) + isBlockedOnCommand = false } return { @@ -255,7 +253,10 @@ const findStopArgumentInQuery = ( } export const getArgumentSuggestions = ( - pastStringArgs: string[], + { tokenArgs, levelArgs }: { + tokenArgs: string[], + levelArgs: string[] + }, pastCommandArgs: SearchCommand[], current?: SearchCommandTree ): { @@ -268,7 +269,7 @@ export const getArgumentSuggestions = ( restArguments, stopArgIndex, isBlocked: isWasBlocked - } = findStopArgumentInQuery(pastStringArgs, pastCommandArgs) + } = findStopArgumentInQuery(tokenArgs, pastCommandArgs) const stopArgument = restArguments[stopArgIndex] const restNotFilledArgs = restArguments.slice(stopArgIndex) @@ -299,7 +300,7 @@ export const getArgumentSuggestions = ( // if we finished argument - stopArgument will be undefined, then we get it as token const lastArgument = stopArgument ?? restArguments[0] - const beforeMandatoryOptionalArgs = getAllRestArguments(current, lastArgument, pastStringArgs) + const beforeMandatoryOptionalArgs = getAllRestArguments(current, lastArgument, levelArgs, !stopArgument) const requiredArgsLength = restNotFilledArgs.filter((arg) => !arg.optional).length return { @@ -344,19 +345,21 @@ export const getAllRestArguments = ( current: Maybe, stopArgument: Nullable, prevStringArgs: string[] = [], + skipLevel = false ) => { const appendArgs: Array = [] + const currentLvlNextArgs = removeNotSuggestedArgs( prevStringArgs, getRestArguments(current, stopArgument) ) - if (currentLvlNextArgs.length) { + if (!skipLevel) { appendArgs.push(currentLvlNextArgs) } if (current?.parent) { - const parentArgs = getAllRestArguments(current.parent, current, []) + const parentArgs = getAllRestArguments(current.parent, current, skipLevel ? prevStringArgs : []) if (parentArgs?.length) { appendArgs.push(...parentArgs) } diff --git a/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts b/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts index bff290b4da..f356961a93 100644 --- a/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts +++ b/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts @@ -7,16 +7,7 @@ const ftSearchCommand = MOCKED_SUPPORTED_COMMANDS['FT.SEARCH'] const ftAggregateCommand = MOCKED_SUPPORTED_COMMANDS['FT.AGGREGATE'] const ftAggreageTests = [ - { - args: [''], - result: { - append: [], - isBlocked: true, - isComplete: false, - parent: undefined, - stopArg: { name: 'query', type: 'string' } - } - }, + { args: [''], result: null }, { args: ['', ''], result: null }, { args: ['index', '"query"', 'APPLY'], @@ -78,17 +69,75 @@ const ftAggreageTests = [ optional: true }, append: [ - { - name: 'name', - type: 'string', - token: 'AS', - optional: true - }, - { - name: 'function', - type: 'string', - token: 'REDUCE' - } + [ + { + name: 'name', + type: 'string', + token: 'AS', + optional: true, + parent: { + name: 'reduce', + type: 'block', + optional: true, + multiple: true, + arguments: [ + { + name: 'function', + token: 'REDUCE', + type: 'string' + }, + { + name: 'nargs', + type: 'integer' + }, + { + name: 'arg', + type: 'string', + multiple: true + }, + { + name: 'name', + type: 'string', + token: 'AS', + optional: true + } + ], + parent: expect.any(Object) + } + } + ], + [ + { + name: 'function', + token: 'REDUCE', + type: 'string', + parent: { + name: 'groupby', + type: 'block', + optional: true, + multiple: true, + arguments: [ + { + name: 'nargs', + type: 'integer', + token: 'GROUPBY' + }, + { + name: 'property', + type: 'string', + multiple: true + }, + { + name: 'reduce', + type: 'block', + optional: true, + multiple: true, + arguments: expect.any(Array) + } + ] + } + } + ] ], isBlocked: false, isComplete: true, @@ -124,7 +173,7 @@ const ftAggreageTests = [ } ] }, - append: [ + append: [[ { name: 'asc', type: 'pure-token', @@ -135,7 +184,7 @@ const ftAggreageTests = [ type: 'pure-token', token: 'DESC' } - ], + ]], isBlocked: false, isComplete: false, parent: expect.any(Object) @@ -151,12 +200,15 @@ const ftAggreageTests = [ optional: true }, append: [ - { - name: 'num', - type: 'integer', - token: 'MAX', - optional: true - } + [ + { + name: 'num', + type: 'integer', + token: 'MAX', + optional: true, + parent: expect.any(Object) + } + ] ], isBlocked: false, isComplete: true, @@ -173,12 +225,13 @@ const ftAggreageTests = [ optional: true }, append: [ - { + [{ name: 'num', type: 'integer', token: 'MAX', - optional: true - } + optional: true, + parent: expect.any(Object) + }] ], isBlocked: false, isComplete: true, @@ -233,15 +286,7 @@ const ftAggreageTests = [ ] const ftSearchTests = [ - { - args: [''], - result: { - append: [], - isBlocked: true, - isComplete: false, - parent: undefined, - stopArg: { name: 'query', type: 'string' } } - }, + { args: [''], result: null }, { args: ['', ''], result: null }, { args: ['', '', 'SUMMARIZE'], @@ -263,31 +308,35 @@ const ftSearchTests = [ } ] }, - append: [ + append: [[ { name: 'count', type: 'string', - token: 'FIELDS' + token: 'FIELDS', + parent: expect.any(Object) }, { name: 'num', type: 'integer', token: 'FRAGS', - optional: true + optional: true, + parent: expect.any(Object) }, { name: 'fragsize', type: 'integer', token: 'LEN', - optional: true + optional: true, + parent: expect.any(Object) }, { name: 'separator', type: 'string', token: 'SEPARATOR', - optional: true + optional: true, + parent: expect.any(Object) } - ], + ]], isBlocked: false, isComplete: true, parent: expect.any(Object) @@ -345,20 +394,22 @@ const ftSearchTests = [ token: 'LEN', optional: true }, - append: [ + append: [[ { name: 'fragsize', type: 'integer', token: 'LEN', - optional: true + optional: true, + parent: expect.any(Object) }, { name: 'separator', type: 'string', token: 'SEPARATOR', - optional: true + optional: true, + parent: expect.any(Object) } - ], + ]], isBlocked: false, isComplete: true, parent: expect.any(Object) @@ -373,14 +424,8 @@ const ftSearchTests = [ token: 'AS', optional: true }, - append: [ - { - name: 'property', - type: 'string', - token: 'AS', - optional: true - } - ], + // TODO: append may have AS token, since it is optional - we skip for now + append: [[]], isBlocked: false, isComplete: false, parent: expect.any(Object) @@ -395,14 +440,7 @@ const ftSearchTests = [ token: 'AS', optional: true }, - append: [ - { - name: 'property', - type: 'string', - token: 'AS', - optional: true - } - ], + append: [[]], isBlocked: false, isComplete: true, parent: expect.any(Object) @@ -444,16 +482,20 @@ const ftSearchTests = [ ] }, append: [ - { - name: 'asc', - type: 'pure-token', - token: 'ASC' - }, - { - name: 'desc', - type: 'pure-token', - token: 'DESC' - } + [ + { + name: 'asc', + type: 'pure-token', + token: 'ASC', + parent: expect.any(Object) + }, + { + name: 'desc', + type: 'pure-token', + token: 'DESC', + parent: expect.any(Object) + } + ] ], isBlocked: false, isComplete: true, @@ -506,45 +548,65 @@ const splitQueryByArgsTests: Array<{ input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS'], result: { args: [[], ['FT.SEARCH', '"idx:bicycle"', '""', 'WITHSORTKEYS']], - isCursorInQuotes: false, - nextCursorChar: 'F', - prevCursorChar: undefined + cursor: { + argLeftOffset: 10, + argRightOffset: 23, + isCursorInQuotes: false, + nextCursorChar: 'F', + prevCursorChar: undefined + } } }, { input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS', 17], result: { args: [['FT.SEARCH'], ['"idx:bicycle"', '""', 'WITHSORTKEYS']], - isCursorInQuotes: true, - nextCursorChar: 'c', - prevCursorChar: 'i' + cursor: { + argLeftOffset: 10, + argRightOffset: 23, + isCursorInQuotes: true, + nextCursorChar: 'c', + prevCursorChar: 'i' + } } }, { input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS', 39], result: { args: [['FT.SEARCH', '"idx:bicycle"', '""'], ['WITHSORTKEYS']], - isCursorInQuotes: false, - nextCursorChar: undefined, - prevCursorChar: 'S' + cursor: { + argLeftOffset: 27, + argRightOffset: 39, + isCursorInQuotes: false, + nextCursorChar: undefined, + prevCursorChar: 'S' + } } }, { input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS ', 40], result: { args: [['FT.SEARCH', '"idx:bicycle"', '""', 'WITHSORTKEYS'], []], - isCursorInQuotes: false, - nextCursorChar: undefined, - prevCursorChar: ' ' + cursor: { + argLeftOffset: 0, + argRightOffset: 0, + isCursorInQuotes: false, + nextCursorChar: undefined, + prevCursorChar: ' ' + } } }, { input: ['FT.SEARCH "idx:bicycle \\" \\"" "" WITHSORTKEYS ', 46], result: { args: [['FT.SEARCH', '"idx:bicycle " ""', '""', 'WITHSORTKEYS'], []], - isCursorInQuotes: false, - nextCursorChar: undefined, - prevCursorChar: ' ' + cursor: { + argLeftOffset: 0, + argRightOffset: 0, + isCursorInQuotes: false, + nextCursorChar: undefined, + prevCursorChar: ' ' + } } } ] diff --git a/redisinsight/ui/src/utils/tests/monaco/cyber/monarchTokensProvider.spec.ts b/redisinsight/ui/src/utils/tests/monaco/cyber/monarchTokensProvider.spec.ts index caaedee9a4..711e7bba49 100644 --- a/redisinsight/ui/src/utils/tests/monaco/cyber/monarchTokensProvider.spec.ts +++ b/redisinsight/ui/src/utils/tests/monaco/cyber/monarchTokensProvider.spec.ts @@ -1,6 +1,8 @@ import { getCypherMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/cypherTokens' import { getJmespathMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/jmespathTokens' import { getSqliteFunctionsMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/sqliteFunctionsTokens' +import { getRediSearchMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokens' +import { MOCKED_SUPPORTED_COMMANDS } from 'uiSrc/pages/search/mocks/mocks' describe('getCypherMonarchTokensProvider', () => { it('should be truthy', () => { @@ -19,3 +21,18 @@ describe('getSqliteFunctionsMonarchTokensProvider', () => { expect(getSqliteFunctionsMonarchTokensProvider([])).toBeTruthy() }) }) + +describe('getRediSearchMonarchTokensProvider', () => { + it('should be truthy', () => { + expect(getRediSearchMonarchTokensProvider([])).toBeTruthy() + }) + + it('should be truthy with command', () => { + const commands = Object.keys(MOCKED_SUPPORTED_COMMANDS) + .map((key) => ({ + ...MOCKED_SUPPORTED_COMMANDS[key], + name: key + })) + expect(getRediSearchMonarchTokensProvider(commands, 'FT.AGGREGATE')).toBeTruthy() + }) +}) From bd3402cb955566272a9211cb1ce0074c956f5025 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Tue, 3 Sep 2024 18:27:33 +0200 Subject: [PATCH 042/112] #RI-6026 - remove console.log --- .../ui/src/pages/search/components/query/utils.spec.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/redisinsight/ui/src/pages/search/components/query/utils.spec.ts b/redisinsight/ui/src/pages/search/components/query/utils.spec.ts index ded3afcccd..43b8d04254 100644 --- a/redisinsight/ui/src/pages/search/components/query/utils.spec.ts +++ b/redisinsight/ui/src/pages/search/components/query/utils.spec.ts @@ -98,11 +98,6 @@ describe('getGeneralSuggestions', () => { [] ) - console.log(findCurrentArgument( - commands, - ['FT.AGGREGATE', '""', '""'] - )) - expect(testResult).toEqual(result) }) }) From 187b6eacfcb606d8390c1606c608a1785d232bb7 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Wed, 4 Sep 2024 19:18:55 +0200 Subject: [PATCH 043/112] #RI-6027 - add completion types for query fields --- .../pages/search/components/query/Query.tsx | 13 +++-- .../search/components/query/utils.spec.ts | 51 ++++++++++++++++++- .../pages/search/components/query/utils.ts | 25 +++++++-- 3 files changed, 82 insertions(+), 7 deletions(-) diff --git a/redisinsight/ui/src/pages/search/components/query/Query.tsx b/redisinsight/ui/src/pages/search/components/query/Query.tsx index dc8ce60263..5e7daef53e 100644 --- a/redisinsight/ui/src/pages/search/components/query/Query.tsx +++ b/redisinsight/ui/src/pages/search/components/query/Query.tsx @@ -238,14 +238,21 @@ const Query = (props: Props) => { setSelectedIndex(allArgs[1] || '') setSelectedCommand(commandName) + const foundArg = findCurrentArgument(COMMANDS_LIST, beforeOffsetArgs) if (prevCursorChar === FIELD_START_SYMBOL) { helpWidgetRef.current.isOpen = false - return asSuggestionsRef(getFieldsSuggestions(attributesRef.current, range), false) + return asSuggestionsRef( + getFieldsSuggestions( + attributesRef.current, + range, + false, + foundArg?.stopArg?.name === DefinedArgumentName.query + ), + false + ) } const cursorContext: CursorContext = { ...cursor, currentOffsetArg, offset } - const foundArg = findCurrentArgument(COMMANDS_LIST, beforeOffsetArgs) - switch (foundArg?.stopArg?.name) { case DefinedArgumentName.index: { return handleIndexSuggestions(command, foundArg, prevArgs.length, currentOffsetArg, range) diff --git a/redisinsight/ui/src/pages/search/components/query/utils.spec.ts b/redisinsight/ui/src/pages/search/components/query/utils.spec.ts index 43b8d04254..9e08a40283 100644 --- a/redisinsight/ui/src/pages/search/components/query/utils.spec.ts +++ b/redisinsight/ui/src/pages/search/components/query/utils.spec.ts @@ -1,4 +1,9 @@ -import { getGeneralSuggestions, isIndexComplete } from 'uiSrc/pages/search/components/query/utils' +import { + addFieldAttribute, + getFieldsSuggestions, + getGeneralSuggestions, + isIndexComplete +} from 'uiSrc/pages/search/components/query/utils' import { MOCKED_SUPPORTED_COMMANDS } from 'uiSrc/pages/search/mocks/mocks' import { addOwnTokenToArgs, buildSuggestion, findCurrentArgument } from 'uiSrc/pages/search/utils' import { SearchCommand, TokenType } from 'uiSrc/pages/search/types' @@ -122,3 +127,47 @@ describe('isIndexComplete', () => { expect(testResult).toEqual(result) }) }) + +const mockedFields = [ + { identifier: 'name', attribute: 'name', type: 'TEXT', WEIGHT: '1', SORTABLE: true, NOSTEM: true }, + { identifier: 'description', attribute: 'description', type: 'TEXT', WEIGHT: '1' }, + { identifier: 'class', attribute: 'class', type: 'TAG', SEPARATOR: ',' }, + { identifier: 'type', attribute: 'type', type: 'TAG', SEPARATOR: ';' }, + { identifier: 'address_city', attribute: 'city', type: 'TAG', SEPARATOR: ',' }, + { identifier: 'address_street', attribute: 'address', type: 'TEXT', WEIGHT: '1', NOSTEM: true }, + { identifier: 'students', attribute: 'students', type: 'NUMERIC', SORTABLE: true }, + { identifier: 'location', attribute: 'location', type: 'GEO' } +] + +const getFieldsSuggestionsTests = [ + [ + [mockedFields, {}], + mockedFields.map((field) => ({ + detail: field.attribute, + insertText: field.attribute, + insertTextRules: 4, + kind: undefined, + label: field.attribute, + range: expect.any(Object), + })) + ], + [ + [mockedFields, {}, false, true], + mockedFields.map((field) => ({ + detail: field.attribute, + insertText: addFieldAttribute(field.attribute, field.type), + insertTextRules: 4, + kind: undefined, + label: field.attribute, + range: expect.any(Object), + })) + ], +] + +describe('getFieldsSuggestions', () => { + it.each(getFieldsSuggestionsTests)('should properly return value for %s', (input, result) => { + const testResult = getFieldsSuggestions(...input) + + expect(testResult).toEqual(result) + }) +}) diff --git a/redisinsight/ui/src/pages/search/components/query/utils.ts b/redisinsight/ui/src/pages/search/components/query/utils.ts index 3c3def04ed..16035f2641 100644 --- a/redisinsight/ui/src/pages/search/components/query/utils.ts +++ b/redisinsight/ui/src/pages/search/components/query/utils.ts @@ -35,9 +35,28 @@ export const getIndexesSuggestions = (indexes: RedisResponseBuffer[], range: mon } }) -export const getFieldsSuggestions = (fields: any[], range: monaco.IRange, spaceAfter = false) => - fields.map(({ attribute }) => { - const insertText = attribute.trim() ? attribute : `"${attribute}"` +export const addFieldAttribute = (attribute: string, type: string) => { + switch (type) { + case 'TAG': return `${attribute}:{\${1:tag}}` + case 'TEXT': return `${attribute}:(\${1:term})` + case 'NUMERIC': return `${attribute}:[\${1:range}]` + case 'GEO': return `${attribute}:[\${1:lon} \${2:lat} \${3:radius} \${4:unit}]` + case 'VECTOR': return `${attribute} \${1:vector}` + default: return attribute + } +} + +export const getFieldsSuggestions = ( + fields: any[], + range: monaco.IRange, + spaceAfter = false, + withType = false +) => + fields.map((field) => { + const { attribute, type } = field + const attibuteText = attribute.trim() ? attribute : `\\"${attribute}\\"` + const insertText = withType ? addFieldAttribute(attibuteText, type) : attibuteText + // const insertText = attibuteText return { label: attribute || ' ', kind: monacoEditor.languages.CompletionItemKind.Reference, From 083adc3d783377c647eb6df8b8d3258f69c6d785 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Wed, 4 Sep 2024 19:20:24 +0200 Subject: [PATCH 044/112] #RI-6027 - remove commented code --- redisinsight/ui/src/pages/search/components/query/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisinsight/ui/src/pages/search/components/query/utils.ts b/redisinsight/ui/src/pages/search/components/query/utils.ts index 16035f2641..6519932cbc 100644 --- a/redisinsight/ui/src/pages/search/components/query/utils.ts +++ b/redisinsight/ui/src/pages/search/components/query/utils.ts @@ -56,7 +56,7 @@ export const getFieldsSuggestions = ( const { attribute, type } = field const attibuteText = attribute.trim() ? attribute : `\\"${attribute}\\"` const insertText = withType ? addFieldAttribute(attibuteText, type) : attibuteText - // const insertText = attibuteText + return { label: attribute || ' ', kind: monacoEditor.languages.CompletionItemKind.Reference, From 3e9bf7bb07770c0b299ac9e0a813f81afb9d913b Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Fri, 6 Sep 2024 18:13:40 +0200 Subject: [PATCH 045/112] add profile and explain tests --- .../search-and-query-tab.e2e.ts | 92 ++++++++++++++++++- 1 file changed, 89 insertions(+), 3 deletions(-) diff --git a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts index ff3a339217..abdbdd2930 100644 --- a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts +++ b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts @@ -88,7 +88,9 @@ test('Verify full commands suggestions with index and query for FT.AGGREGATE', a // Verify basic commands suggestions FT.SEARCH and FT.AGGREGATE await t.typeText(searchAndQueryPage.queryInput, 'FT', { replace: true }); // Verify that the list with FT.SEARCH and FT.AGGREGATE auto-suggestions is displayed - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.count).eql(2, 'FT.SEARCH and FT.AGGREGATE auto-suggestions are not displayed'); + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withText('FT.SEARCH').exists).ok('FT.SEARCH auto-suggestions are not displayed'); + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withText('FT.AGGREGATE').exists).ok('FT.AGGREGATE auto-suggestions are not displayed'); + // Select command and check result await t.pressKey('enter'); let script = await searchAndQueryPage.queryInputScriptArea.textContent; @@ -146,9 +148,8 @@ test('Verify full commands suggestions with index and query for FT.AGGREGATE', a }); test('Verify full commands suggestions with index and query for FT.SEARCH', async t => { await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); - await t.typeText(searchAndQueryPage.queryInput, 'FT', { replace: true }); + await t.typeText(searchAndQueryPage.queryInput, 'FT.SE', { replace: true }); // Select command and check result - await t.pressKey('down'); await t.pressKey('enter'); const script = await searchAndQueryPage.queryInputScriptArea.textContent; await t.expect(script.replace(/\s/g, ' ')).contains('FT.SEARCH ', 'Result of sent command exists'); @@ -178,3 +179,88 @@ test('Verify full commands suggestions with index and query for FT.SEARCH', asyn // Verify that 'No suggestions' tooltip is displayed when returning to invalid typing like WRONGCOMMAND await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestWidget.textContent).contains('No suggestions.', 'Index not auto-suggested'); }); +test('Verify full commands suggestions with index and query for FT.PROFILE(SEARCH)', async t => { + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); + await t.typeText(searchAndQueryPage.queryInput, 'FT.PR', { replace: true }); + // Select command and check result + await t.pressKey('enter'); + const script = await searchAndQueryPage.queryInputScriptArea.textContent; + await t.expect(script.replace(/\s/g, ' ')).contains('FT.PROFILE ', 'Result of sent command exists'); + + await t.pressKey('tab'); + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('AGGREGATE').exists).ok('FT.PROFILE aggregate argument not suggested'); + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('SEARCH').exists).ok('FT.PROFILE search argument not suggested'); + + // Select SEARCH command + await t.typeText(searchAndQueryPage.queryInput, 'SEA', { replace: false }); + await t.pressKey('enter'); + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('LIMITED').exists).ok('FT.PROFILE SEARCH arguments not suggested'); + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('QUERY').exists).ok('FT.PROFILE SEARCH arguments not suggested'); + + // Select QUERY + await t.typeText(searchAndQueryPage.queryInput, 'QUE', { replace: false }); + await t.pressKey('enter'); + await t.typeText(searchAndQueryPage.queryInput, '@c', { replace: false }); + // Select '@city' field + await t.pressKey('down'); + await t.pressKey('tab'); + await t.pressKey('right'); + await t.pressKey('space'); + // Verify that there are no more suggestions + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.exists).notOk('Additional invalid commands suggested'); + const expectedText = `FT.PROFILE "${indexName1}" SEARCH QUERY "@city"`.trim().replace(/\s+/g, ' '); + // Verify command entered correctly + await t.expect((await searchAndQueryPage.queryInputForText.innerText).trim().replace(/\s+/g, ' ')).contains(expectedText, 'Incorrect order of entered arguments'); +}); +test('Verify full commands suggestions with index and query for FT.PROFILE(AGGREGATE)', async t => { + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); + await t.typeText(searchAndQueryPage.queryInput, 'FT.PR', { replace: true }); + // Select command and check result + await t.pressKey('enter'); + await t.pressKey('tab'); + // Select AGGREGATE command + await t.typeText(searchAndQueryPage.queryInput, 'AGG', { replace: false }); + await t.pressKey('enter'); + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('LIMITED').exists).ok('FT.PROFILE AGGREGATE arguments not suggested'); + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('QUERY').exists).ok('FT.PROFILE AGGREGATE arguments not suggested'); + + // Select QUERY + await t.typeText(searchAndQueryPage.queryInput, 'QUE', { replace: false }); + await t.pressKey('enter'); + await t.typeText(searchAndQueryPage.queryInput, '@c', { replace: false }); + // Select '@city' field + await t.pressKey('down'); + await t.pressKey('tab'); + await t.pressKey('right'); + await t.pressKey('space'); + // Verify that there are no more suggestions + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.exists).notOk('Additional invalid commands suggested'); + const expectedText = `FT.PROFILE "${indexName1}" AGGREGATE QUERY "@city"`.trim().replace(/\s+/g, ' '); + // Verify command entered correctly + await t.expect((await searchAndQueryPage.queryInputForText.innerText).trim().replace(/\s+/g, ' ')).contains(expectedText, 'Incorrect order of entered arguments'); +}); +test('Verify full commands suggestions with index and query for FT.EXPLAIN', async t => { + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); + await t.typeText(searchAndQueryPage.queryInput, 'FT.EX', { replace: true }); + // Select command and check result + await t.pressKey('enter'); + await t.pressKey('tab'); + + await t.typeText(searchAndQueryPage.queryInput, '@c', { replace: false }); + // Select '@city' field + await t.pressKey('down'); + await t.pressKey('tab'); + await t.pressKey('right'); + await t.pressKey('space'); + + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('DIALECT').exists).ok('FT.EXPLAIN arguments not suggested'); + // Add DIALECT + await t.pressKey('enter'); + await t.typeText(searchAndQueryPage.queryInput, 'dialectTest', { replace: false }); + // Verify that there are no more suggestions + await t.pressKey('space'); + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.exists).notOk('Additional invalid commands suggested'); + const expectedText = `FT.EXPLAIN "${indexName1}" "@city" DIALECT dialectTest`.trim().replace(/\s+/g, ' '); + // Verify command entered correctly + await t.expect((await searchAndQueryPage.queryInputForText.innerText).trim().replace(/\s+/g, ' ')).contains(expectedText, 'Incorrect order of entered arguments'); +}); From 8754910e35ffc88dc4eae86870d7e0da8f62e6c0 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Mon, 9 Sep 2024 09:40:01 +0200 Subject: [PATCH 046/112] fixes --- .../search-and-query-tab.e2e.ts | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts index abdbdd2930..b51b417049 100644 --- a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts +++ b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts @@ -72,7 +72,7 @@ test('Verify that user can use show more to see command fully in 2nd tooltip', a await t.expect(searchAndQueryPage.MonacoEditor.monacoCommandDetails.exists).notOk('The "read more" about the command is not closed'); }); test('Verify full commands suggestions with index and query for FT.AGGREGATE', async t => { - const groupByArgInfo = 'GROUPBY nargs property [property ...] [REDUCE function nargs arg [arg ...] [AS name] [REDUCE function nargs arg [arg ...] [AS name] ...]]'; + const groupByArgInfo = 'GROUPBY nargs property [property ...] [REDUCE '; const indexFields = [ 'address', 'city', @@ -102,6 +102,7 @@ test('Verify full commands suggestions with index and query for FT.AGGREGATE', a await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText(indexName2).exists).ok('All indexes not auto-suggested'); await t.pressKey('tab'); + await t.wait(200); await t.typeText(searchAndQueryPage.queryInput, '@', { replace: false }); script = await searchAndQueryPage.queryInputScriptArea.textContent; // Verify that user can see the list of fields from the index selected when type in “@” @@ -110,7 +111,7 @@ test('Verify full commands suggestions with index and query for FT.AGGREGATE', a await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText(field).exists).ok(`${field} Index field not auto-suggested`); } // Verify that user can use autosuggestions by typing fields from index after "@" - await t.typeText(searchAndQueryPage.queryInput, 'c', { replace: false }); + await t.typeText(searchAndQueryPage.queryInput, 'ci', { replace: false }); await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('city').exists).ok('Index field not auto-suggested after starting typing'); await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.count).eql(1, 'Wrong index fields suggested after typing first letter'); @@ -124,7 +125,7 @@ test('Verify full commands suggestions with index and query for FT.AGGREGATE', a await t.pressKey('tab'); // Verify that user can see widget about entered argument - await t.expect(searchAndQueryPage.MonacoEditor.monacoHintWithArguments.withText(groupByArgInfo).exists).ok('Widget with info about entered argument not displayed'); + await t.expect(searchAndQueryPage.MonacoEditor.monacoHintWithArguments.textContent).contains(groupByArgInfo, 'Widget with info about entered argument not displayed'); await t.typeText(searchAndQueryPage.queryInput, '1 "London"', { replace: false }); await t.pressKey('space'); @@ -140,6 +141,7 @@ test('Verify full commands suggestions with index and query for FT.AGGREGATE', a await t.typeText(searchAndQueryPage.queryInput, 'stud', { replace: false }); await t.pressKey('space'); + await t.debug(); // Verify multiple argument option suggestions await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.nth(0).textContent).contains('REDUCE', 'Incorrect order of suggested arguments'); // Verify complex command sequences like nargs and properties are suggested accurately for GROUPBY @@ -155,9 +157,11 @@ test('Verify full commands suggestions with index and query for FT.SEARCH', asyn await t.expect(script.replace(/\s/g, ' ')).contains('FT.SEARCH ', 'Result of sent command exists'); await t.pressKey('tab'); - await t.typeText(searchAndQueryPage.queryInput, '@c', { replace: false }); + await t.wait(200); + await t.typeText(searchAndQueryPage.queryInput, '@', { replace: false }); + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed'); + await t.typeText(searchAndQueryPage.queryInput, 'ci', { replace: false }); // Select '@city' field - await t.pressKey('down'); await t.pressKey('tab'); await t.pressKey('right'); await t.pressKey('space'); @@ -200,9 +204,10 @@ test('Verify full commands suggestions with index and query for FT.PROFILE(SEARC // Select QUERY await t.typeText(searchAndQueryPage.queryInput, 'QUE', { replace: false }); await t.pressKey('enter'); - await t.typeText(searchAndQueryPage.queryInput, '@c', { replace: false }); + await t.typeText(searchAndQueryPage.queryInput, '@', { replace: false }); + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed'); + await t.typeText(searchAndQueryPage.queryInput, 'ci', { replace: false }); // Select '@city' field - await t.pressKey('down'); await t.pressKey('tab'); await t.pressKey('right'); await t.pressKey('space'); @@ -227,9 +232,10 @@ test('Verify full commands suggestions with index and query for FT.PROFILE(AGGRE // Select QUERY await t.typeText(searchAndQueryPage.queryInput, 'QUE', { replace: false }); await t.pressKey('enter'); - await t.typeText(searchAndQueryPage.queryInput, '@c', { replace: false }); + await t.typeText(searchAndQueryPage.queryInput, '@', { replace: false }); + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed'); + await t.typeText(searchAndQueryPage.queryInput, 'ci', { replace: false }); // Select '@city' field - await t.pressKey('down'); await t.pressKey('tab'); await t.pressKey('right'); await t.pressKey('space'); @@ -245,10 +251,10 @@ test('Verify full commands suggestions with index and query for FT.EXPLAIN', asy // Select command and check result await t.pressKey('enter'); await t.pressKey('tab'); - - await t.typeText(searchAndQueryPage.queryInput, '@c', { replace: false }); + await t.typeText(searchAndQueryPage.queryInput, '@', { replace: false }); + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed'); + await t.typeText(searchAndQueryPage.queryInput, 'ci', { replace: false }); // Select '@city' field - await t.pressKey('down'); await t.pressKey('tab'); await t.pressKey('right'); await t.pressKey('space'); From e687014db99a151e3a4dccb7f4d78a3d74943af3 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Mon, 9 Sep 2024 10:27:07 +0200 Subject: [PATCH 047/112] add waiter because of slow work of suggestions --- .../regression/search-and-query/search-and-query-tab.e2e.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts index b51b417049..088674bcf4 100644 --- a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts +++ b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts @@ -204,6 +204,7 @@ test('Verify full commands suggestions with index and query for FT.PROFILE(SEARC // Select QUERY await t.typeText(searchAndQueryPage.queryInput, 'QUE', { replace: false }); await t.pressKey('enter'); + await t.wait(200); await t.typeText(searchAndQueryPage.queryInput, '@', { replace: false }); await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed'); await t.typeText(searchAndQueryPage.queryInput, 'ci', { replace: false }); @@ -232,6 +233,7 @@ test('Verify full commands suggestions with index and query for FT.PROFILE(AGGRE // Select QUERY await t.typeText(searchAndQueryPage.queryInput, 'QUE', { replace: false }); await t.pressKey('enter'); + await t.wait(200); await t.typeText(searchAndQueryPage.queryInput, '@', { replace: false }); await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed'); await t.typeText(searchAndQueryPage.queryInput, 'ci', { replace: false }); @@ -251,6 +253,7 @@ test('Verify full commands suggestions with index and query for FT.EXPLAIN', asy // Select command and check result await t.pressKey('enter'); await t.pressKey('tab'); + await t.wait(200); await t.typeText(searchAndQueryPage.queryInput, '@', { replace: false }); await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed'); await t.typeText(searchAndQueryPage.queryInput, 'ci', { replace: false }); From b782852e9501c0031f8369c041714aed9dfcc087 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Mon, 9 Sep 2024 11:59:56 +0200 Subject: [PATCH 048/112] add env variable for local --- tests/e2e/local.web.docker-compose.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/e2e/local.web.docker-compose.yml b/tests/e2e/local.web.docker-compose.yml index 69db32bc4a..788a88304e 100644 --- a/tests/e2e/local.web.docker-compose.yml +++ b/tests/e2e/local.web.docker-compose.yml @@ -43,9 +43,10 @@ services: env_file: - ./.env environment: - RI_ENCRYPTION_KEY: $E2E_RI_ENCRYPTION_KEY + RI_ENCRYPTION_KEY: $RI_ENCRYPTION_KEY RI_SERVER_TLS_CERT: $RI_SERVER_TLS_CERT RI_SERVER_TLS_KEY: $RI_SERVER_TLS_KEY + BUILD_TYPE: DOCKER_ON_PREMISE volumes: - ./rihomedir:/data - tmp:/tmp From ed29703fe491bf1afd5d58e5a8ee3c471e79d44e Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Mon, 9 Sep 2024 12:00:38 +0200 Subject: [PATCH 049/112] add env --- tests/e2e/.env | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e/.env b/tests/e2e/.env index 509b8704a1..9242dec68e 100644 --- a/tests/e2e/.env +++ b/tests/e2e/.env @@ -1,5 +1,6 @@ COMMON_URL=https://app:5540 API_URL=https://app:5540/api +BUILD_TYPE=DOCKER_ON_PREMISE OSS_SENTINEL_PASSWORD=password RI_NOTIFICATION_UPDATE_URL=https://s3.amazonaws.com/redisinsight.test/public/tests/e2e/notifications.json RI_NOTIFICATION_SYNC_INTERVAL=30000 From 6698ebef0251b6f7d10d6deaa5eb8602c8c962ac Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Mon, 9 Sep 2024 12:13:50 +0200 Subject: [PATCH 050/112] add method for selecting query --- .../e2e/pageObjects/search-and-query-page.ts | 15 ++++++++ .../search-and-query-tab.e2e.ts | 35 +++---------------- 2 files changed, 19 insertions(+), 31 deletions(-) diff --git a/tests/e2e/pageObjects/search-and-query-page.ts b/tests/e2e/pageObjects/search-and-query-page.ts index 2b6ba80d21..7943ca5131 100644 --- a/tests/e2e/pageObjects/search-and-query-page.ts +++ b/tests/e2e/pageObjects/search-and-query-page.ts @@ -1,5 +1,20 @@ +import { t } from 'testcafe'; import { BaseRunCommandsPage } from './base-run-commands-page'; export class SearchAndQueryPage extends BaseRunCommandsPage { + /** + * Select query using autosuggest + * @param query Value of query + */ + async selectQueryUsingAutosuggest(value: string): Promise { + await t.wait(200); + await t.typeText(this.queryInput, '@', { replace: false }); + await t.expect(this.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed'); + await t.typeText(this.queryInput, value, { replace: false }); + // Select query option into autosuggest and go out of quotes + await t.pressKey('tab'); + await t.pressKey('right'); + await t.pressKey('space'); + } } diff --git a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts index 088674bcf4..1b50e224be 100644 --- a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts +++ b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts @@ -157,14 +157,8 @@ test('Verify full commands suggestions with index and query for FT.SEARCH', asyn await t.expect(script.replace(/\s/g, ' ')).contains('FT.SEARCH ', 'Result of sent command exists'); await t.pressKey('tab'); - await t.wait(200); - await t.typeText(searchAndQueryPage.queryInput, '@', { replace: false }); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed'); - await t.typeText(searchAndQueryPage.queryInput, 'ci', { replace: false }); // Select '@city' field - await t.pressKey('tab'); - await t.pressKey('right'); - await t.pressKey('space'); + await searchAndQueryPage.selectQueryUsingAutosuggest('city'); await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('DIALECT').exists).ok('FT.SEARCH arguments not suggested'); await t.typeText(searchAndQueryPage.queryInput, 'n', { replace: false }); await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.nth(0).textContent).contains('NOCONTENT', 'Argument not suggested after typing first letters'); @@ -204,14 +198,7 @@ test('Verify full commands suggestions with index and query for FT.PROFILE(SEARC // Select QUERY await t.typeText(searchAndQueryPage.queryInput, 'QUE', { replace: false }); await t.pressKey('enter'); - await t.wait(200); - await t.typeText(searchAndQueryPage.queryInput, '@', { replace: false }); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed'); - await t.typeText(searchAndQueryPage.queryInput, 'ci', { replace: false }); - // Select '@city' field - await t.pressKey('tab'); - await t.pressKey('right'); - await t.pressKey('space'); + await searchAndQueryPage.selectQueryUsingAutosuggest('city'); // Verify that there are no more suggestions await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.exists).notOk('Additional invalid commands suggested'); const expectedText = `FT.PROFILE "${indexName1}" SEARCH QUERY "@city"`.trim().replace(/\s+/g, ' '); @@ -233,14 +220,7 @@ test('Verify full commands suggestions with index and query for FT.PROFILE(AGGRE // Select QUERY await t.typeText(searchAndQueryPage.queryInput, 'QUE', { replace: false }); await t.pressKey('enter'); - await t.wait(200); - await t.typeText(searchAndQueryPage.queryInput, '@', { replace: false }); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed'); - await t.typeText(searchAndQueryPage.queryInput, 'ci', { replace: false }); - // Select '@city' field - await t.pressKey('tab'); - await t.pressKey('right'); - await t.pressKey('space'); + await searchAndQueryPage.selectQueryUsingAutosuggest('city'); // Verify that there are no more suggestions await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.exists).notOk('Additional invalid commands suggested'); const expectedText = `FT.PROFILE "${indexName1}" AGGREGATE QUERY "@city"`.trim().replace(/\s+/g, ' '); @@ -253,14 +233,7 @@ test('Verify full commands suggestions with index and query for FT.EXPLAIN', asy // Select command and check result await t.pressKey('enter'); await t.pressKey('tab'); - await t.wait(200); - await t.typeText(searchAndQueryPage.queryInput, '@', { replace: false }); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed'); - await t.typeText(searchAndQueryPage.queryInput, 'ci', { replace: false }); - // Select '@city' field - await t.pressKey('tab'); - await t.pressKey('right'); - await t.pressKey('space'); + await searchAndQueryPage.selectQueryUsingAutosuggest('city'); await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('DIALECT').exists).ok('FT.EXPLAIN arguments not suggested'); // Add DIALECT From e9bf2ee1f6796d8a874f5a33df214bdb90c1bab3 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Mon, 9 Sep 2024 12:14:04 +0200 Subject: [PATCH 051/112] #RI-6091 - fix highlighting #RI-6093 - fix expression suggestions --- redisinsight/ui/src/pages/search/components/query/Query.tsx | 4 +++- .../ui/src/utils/monaco/monarchTokens/redisearchTokens.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/redisinsight/ui/src/pages/search/components/query/Query.tsx b/redisinsight/ui/src/pages/search/components/query/Query.tsx index dc8ce60263..c9ceeb1c83 100644 --- a/redisinsight/ui/src/pages/search/components/query/Query.tsx +++ b/redisinsight/ui/src/pages/search/components/query/Query.tsx @@ -315,7 +315,9 @@ const Query = (props: Props) => { cursorContext: CursorContext, range: monacoEditor.IRange ) => { - if (foundArg?.stopArg?.expression) return handleExpressionSuggestions(value, foundArg, cursorContext, range) + if (foundArg?.isBlocked && foundArg?.stopArg?.expression) { + return handleExpressionSuggestions(value, foundArg, cursorContext, range) + } const { prevCursorChar, nextCursorChar, isCursorInQuotes } = cursorContext if (isCursorInQuotes || nextCursorChar?.trim()) return asSuggestionsRef([]) diff --git a/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts b/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts index a4814e00c3..323a7be11d 100644 --- a/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts +++ b/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts @@ -61,7 +61,7 @@ export const getRediSearchMonarchTokensProvider = ( index: [ [/"([^"\\]|\\.)*"/, { token: 'index', next: isHighlightQuery ? '@query' : '@root' }], [/'([^'\\]|\\.)*'/, { token: 'index', next: isHighlightQuery ? '@query' : '@root' }], - [/[a-zA-Z_]\w*/, { token: 'index', next: isHighlightQuery ? '@query' : '@root' }], + [/[\w:]+/, { token: 'index', next: isHighlightQuery ? '@query' : '@root' }], { include: 'root' } // Fallback to the root state if nothing matches ], ...generateQuery(), From 7f7b2efa868318d00d7bd104c199a65b903980a3 Mon Sep 17 00:00:00 2001 From: ArtemHoruzhenko Date: Mon, 9 Sep 2024 13:43:20 +0300 Subject: [PATCH 052/112] init implementation --- .../dto/create-command-execution.dto.ts | 56 ++----------- .../dto/create-command-executions.dto.ts | 34 ++------ .../entities/command-execution.entity.ts | 45 +++------- .../models/command-execution-result.ts | 7 +- .../workbench/models/command-execution.ts | 64 ++++++++++++-- .../models/command-executions.filter.ts | 4 + .../src/modules/workbench/plugins.service.ts | 2 +- .../providers/workbench-commands.executor.ts | 3 +- .../command-execution.repository.ts | 51 +++++++++++- .../local-command-execution.repository.ts | 83 ++++++++----------- .../modules/workbench/workbench.controller.ts | 8 +- .../modules/workbench/workbench.service.ts | 18 ++-- 12 files changed, 195 insertions(+), 180 deletions(-) create mode 100644 redisinsight/api/src/modules/workbench/models/command-executions.filter.ts diff --git a/redisinsight/api/src/modules/workbench/dto/create-command-execution.dto.ts b/redisinsight/api/src/modules/workbench/dto/create-command-execution.dto.ts index 50e7388163..9664a64e97 100644 --- a/redisinsight/api/src/modules/workbench/dto/create-command-execution.dto.ts +++ b/redisinsight/api/src/modules/workbench/dto/create-command-execution.dto.ts @@ -1,51 +1,7 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { - IsEnum, IsNotEmpty, IsOptional, IsString, -} from 'class-validator'; +import { PickType } from '@nestjs/swagger'; +import { CommandExecution } from 'src/modules/workbench/models/command-execution'; -export enum RunQueryMode { - Raw = 'RAW', - ASCII = 'ASCII', -} - -export enum ResultsMode { - Default = 'DEFAULT', - GroupMode = 'GROUP_MODE', - Silent = 'SILENT', -} - -export class CreateCommandExecutionDto { - @ApiProperty({ - type: String, - description: 'Redis command', - }) - @IsString() - @IsNotEmpty() - command: string; - - @ApiPropertyOptional({ - description: 'Workbench mode', - default: RunQueryMode.ASCII, - enum: RunQueryMode, - }) - @IsOptional() - @IsEnum(RunQueryMode, { - message: `mode must be a valid enum value. Valid values: ${Object.values( - RunQueryMode, - )}.`, - }) - mode?: RunQueryMode = RunQueryMode.ASCII; - - @ApiPropertyOptional({ - description: 'Workbench group mode', - default: ResultsMode.Default, - enum: ResultsMode, - }) - @IsOptional() - @IsEnum(ResultsMode, { - message: `resultsMode must be a valid enum value. Valid values: ${Object.values( - ResultsMode, - )}.`, - }) - resultsMode?: ResultsMode; -} +export class CreateCommandExecutionDto extends PickType( + CommandExecution, + ['command', 'mode', 'resultsMode', 'type'] as const, +) {} diff --git a/redisinsight/api/src/modules/workbench/dto/create-command-executions.dto.ts b/redisinsight/api/src/modules/workbench/dto/create-command-executions.dto.ts index f8749e20cd..9ca21174ba 100644 --- a/redisinsight/api/src/modules/workbench/dto/create-command-executions.dto.ts +++ b/redisinsight/api/src/modules/workbench/dto/create-command-executions.dto.ts @@ -1,39 +1,23 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiProperty, PickType } from '@nestjs/swagger'; import { - IsEnum, IsArray, IsDefined, IsOptional, IsString, ArrayNotEmpty, + IsArray, IsDefined, IsString, ArrayNotEmpty, } from 'class-validator'; -import { RunQueryMode, ResultsMode } from './create-command-execution.dto'; +import { CommandExecution } from 'src/modules/workbench/models/command-execution'; +import { Expose } from 'class-transformer'; -export class CreateCommandExecutionsDto { +export class CreateCommandExecutionsDto extends PickType( + CommandExecution, + ['mode', 'resultsMode', 'type'] as const, +) { @ApiProperty({ isArray: true, type: String, description: 'Redis commands', }) + @Expose() @IsArray() @ArrayNotEmpty() @IsDefined() @IsString({ each: true }) commands: string[]; - - @ApiPropertyOptional({ - description: 'Workbench mode', - default: RunQueryMode.ASCII, - enum: RunQueryMode, - }) - @IsOptional() - @IsEnum(RunQueryMode, { - message: `mode must be a valid enum value. Valid values: ${Object.values( - RunQueryMode, - )}.`, - }) - mode?: RunQueryMode = RunQueryMode.ASCII; - - @IsOptional() - @IsEnum(ResultsMode, { - message: `resultsMode must be a valid enum value. Valid values: ${Object.values( - ResultsMode, - )}.`, - }) - resultsMode?: ResultsMode; } diff --git a/redisinsight/api/src/modules/workbench/entities/command-execution.entity.ts b/redisinsight/api/src/modules/workbench/entities/command-execution.entity.ts index ec060896a1..09e56ba76f 100644 --- a/redisinsight/api/src/modules/workbench/entities/command-execution.entity.ts +++ b/redisinsight/api/src/modules/workbench/entities/command-execution.entity.ts @@ -2,9 +2,10 @@ import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, JoinColumn, Index, } from 'typeorm'; import { DatabaseEntity } from 'src/modules/database/entities/database.entity'; -import { RunQueryMode, ResultsMode } from 'src/modules/workbench/dto/create-command-execution.dto'; -import { Expose, Transform } from 'class-transformer'; +import { Expose } from 'class-transformer'; import { IsInt, Min } from 'class-validator'; +import { CommandExecutionType, ResultsMode, RunQueryMode } from 'src/modules/workbench/models/command-execution'; +import { DataAsJsonString } from 'src/common/decorators'; @Entity('command_execution') export class CommandExecutionEntity { @@ -24,6 +25,7 @@ export class CommandExecutionEntity { }, ) @JoinColumn({ name: 'databaseId' }) + @Expose() database: DatabaseEntity; @Column({ nullable: false, type: 'text' }) @@ -35,48 +37,27 @@ export class CommandExecutionEntity { mode?: string = RunQueryMode.ASCII; @Column({ nullable: false, type: 'text' }) - @Transform((object) => JSON.stringify(object), { toClassOnly: true }) - @Transform((string) => { - try { - return JSON.parse(string); - } catch (e) { - return undefined; - } - }, { toPlainOnly: true }) + @DataAsJsonString() @Expose() result: string; @Column({ nullable: true }) @Expose() - role?: string = null; + role?: string; @Column({ nullable: true }) @Expose() resultsMode?: string = ResultsMode.Default; @Column({ nullable: true }) - @Transform((object) => JSON.stringify(object), { toClassOnly: true }) - @Transform((string) => { - try { - return JSON.parse(string); - } catch (e) { - return undefined; - } - }, { toPlainOnly: true }) + @DataAsJsonString() @Expose() summary?: string; @Column({ nullable: true }) - @Transform((object) => JSON.stringify(object), { toClassOnly: true }) - @Transform((string) => { - try { - return JSON.parse(string); - } catch (e) { - return undefined; - } - }, { toPlainOnly: true }) + @DataAsJsonString() @Expose() - nodeOptions?: string = null; + nodeOptions?: string; @Column({ nullable: true }) encryption: string; @@ -91,12 +72,12 @@ export class CommandExecutionEntity { @Min(0) db?: number; + @Column({ nullable: false, default: CommandExecutionType.Workbench }) + @Expose() + type?: string = CommandExecutionType.Workbench; + @CreateDateColumn() @Index() @Expose() createdAt: Date; - - constructor(entity: Partial) { - Object.assign(this, entity); - } } diff --git a/redisinsight/api/src/modules/workbench/models/command-execution-result.ts b/redisinsight/api/src/modules/workbench/models/command-execution-result.ts index 2ba5d706a8..51852ef731 100644 --- a/redisinsight/api/src/modules/workbench/models/command-execution-result.ts +++ b/redisinsight/api/src/modules/workbench/models/command-execution-result.ts @@ -1,5 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto'; +import { Expose } from 'class-transformer'; export class CommandExecutionResult { @ApiProperty({ @@ -7,15 +8,13 @@ export class CommandExecutionResult { default: CommandExecutionStatus.Success, enum: CommandExecutionStatus, }) + @Expose() status: CommandExecutionStatus; @ApiProperty({ type: String, description: 'Redis response', }) + @Expose() response: any; - - constructor(partial: Partial = {}) { - Object.assign(this, partial); - } } diff --git a/redisinsight/api/src/modules/workbench/models/command-execution.ts b/redisinsight/api/src/modules/workbench/models/command-execution.ts index d7c0da3041..37d9fafa09 100644 --- a/redisinsight/api/src/modules/workbench/models/command-execution.ts +++ b/redisinsight/api/src/modules/workbench/models/command-execution.ts @@ -1,10 +1,32 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { - IsDefined, IsInt, IsOptional, Min, + IsDefined, + IsEnum, + IsInt, + IsNotEmpty, + IsOptional, + IsString, + Min, } from 'class-validator'; import { CommandExecutionResult } from 'src/modules/workbench/models/command-execution-result'; -import { RunQueryMode, ResultsMode } from 'src/modules/workbench/dto/create-command-execution.dto'; -import { Expose } from 'class-transformer'; +import { Expose, Type } from 'class-transformer'; +import { Default } from 'src/common/decorators'; + +export enum RunQueryMode { + Raw = 'RAW', + ASCII = 'ASCII', +} + +export enum ResultsMode { + Default = 'DEFAULT', + GroupMode = 'GROUP_MODE', + Silent = 'SILENT', +} + +export enum CommandExecutionType { + Workbench = 'WORKBENCH', + Search = 'SEARCH', +} export class ResultsSummary { @ApiProperty({ @@ -45,9 +67,11 @@ export class CommandExecution { databaseId: string; @ApiProperty({ - description: 'Redis command executed', + description: 'Redis command', type: String, }) + @IsString() + @IsNotEmpty() @Expose() command: string; @@ -57,7 +81,14 @@ export class CommandExecution { enum: RunQueryMode, }) @Expose() - mode?: RunQueryMode = RunQueryMode.ASCII; + @IsOptional() + @IsEnum(RunQueryMode, { + message: `mode must be a valid enum value. Valid values: ${Object.values( + RunQueryMode, + )}.`, + }) + @Default(RunQueryMode.ASCII) + mode?: RunQueryMode; @ApiPropertyOptional({ description: 'Workbench result mode', @@ -65,7 +96,14 @@ export class CommandExecution { enum: ResultsMode, }) @Expose() - resultsMode?: ResultsMode = ResultsMode.Default; + @IsOptional() + @IsEnum(ResultsMode, { + message: `resultsMode must be a valid enum value. Valid values: ${Object.values( + ResultsMode, + )}.`, + }) + @Default(ResultsMode.Default) + resultsMode?: ResultsMode; @ApiPropertyOptional({ description: 'Workbench executions summary', @@ -79,6 +117,7 @@ export class CommandExecution { type: () => CommandExecutionResult, isArray: true, }) + @Type(() => CommandExecutionResult) @Expose() result: CommandExecutionResult[]; @@ -113,7 +152,14 @@ export class CommandExecution { @IsOptional() db?: number; - constructor(partial: Partial = {}) { - Object.assign(this, partial); - } + @ApiPropertyOptional({ + description: 'Command execution type. Used to distinguish between search and workbench', + default: CommandExecutionType.Workbench, + enum: CommandExecutionType, + }) + @Expose() + @IsOptional() + @IsEnum(CommandExecutionType) + @Default(CommandExecutionType.Workbench) + type?: CommandExecutionType; } diff --git a/redisinsight/api/src/modules/workbench/models/command-executions.filter.ts b/redisinsight/api/src/modules/workbench/models/command-executions.filter.ts new file mode 100644 index 0000000000..0cc3dccbe9 --- /dev/null +++ b/redisinsight/api/src/modules/workbench/models/command-executions.filter.ts @@ -0,0 +1,4 @@ +import { PickType } from '@nestjs/swagger'; +import { CommandExecution } from 'src/modules/workbench/models/command-execution'; + +export class CommandExecutionFilter extends PickType(CommandExecution, ['type'] as const) {} diff --git a/redisinsight/api/src/modules/workbench/plugins.service.ts b/redisinsight/api/src/modules/workbench/plugins.service.ts index dda9d1b02b..8e05865d92 100644 --- a/redisinsight/api/src/modules/workbench/plugins.service.ts +++ b/redisinsight/api/src/modules/workbench/plugins.service.ts @@ -52,7 +52,7 @@ export class PluginsService { return new PluginCommandExecution({ ...dto, databaseId: clientMetadata.databaseId, - result: [new CommandExecutionResult({ + result: [plainToClass(CommandExecutionResult, { response: error.message, status: CommandExecutionStatus.Fail, })], diff --git a/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.ts b/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.ts index 1228edbbf9..8f6a30684d 100644 --- a/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.ts +++ b/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.ts @@ -11,7 +11,7 @@ import { } from 'src/modules/cli/constants/errors'; import { unknownCommand } from 'src/constants'; import { CommandExecutionResult } from 'src/modules/workbench/models/command-execution-result'; -import { CreateCommandExecutionDto, RunQueryMode } from 'src/modules/workbench/dto/create-command-execution.dto'; +import { CreateCommandExecutionDto } from 'src/modules/workbench/dto/create-command-execution.dto'; import { FormatterManager, FormatterTypes, @@ -20,6 +20,7 @@ import { } from 'src/common/transformers'; import { RedisClient } from 'src/modules/redis/client'; import { getAnalyticsDataFromIndexInfo } from 'src/utils'; +import { RunQueryMode } from 'src/modules/workbench/models/command-execution'; import { WorkbenchAnalyticsService } from '../services/workbench-analytics/workbench-analytics.service'; @Injectable() diff --git a/redisinsight/api/src/modules/workbench/repositories/command-execution.repository.ts b/redisinsight/api/src/modules/workbench/repositories/command-execution.repository.ts index ab13569de4..0125933cab 100644 --- a/redisinsight/api/src/modules/workbench/repositories/command-execution.repository.ts +++ b/redisinsight/api/src/modules/workbench/repositories/command-execution.repository.ts @@ -1,14 +1,61 @@ import { CommandExecution } from 'src/modules/workbench/models/command-execution'; import { ShortCommandExecution } from 'src/modules/workbench/models/short-command-execution'; import { SessionMetadata } from 'src/common/models'; +import { CommandExecutionFilter } from 'src/modules/workbench/models/command-executions.filter'; export abstract class CommandExecutionRepository { + /** + * Create multiple entities + * + * @param sessionMetadata + * @param commandExecutions + */ abstract createMany( sessionMetadata: SessionMetadata, commandExecutions: Partial[], ): Promise; - abstract getList(sessionMetadata: SessionMetadata, databaseId: string): Promise; + + /** + * Fetch only needed fields to show in list to avoid huge decryption work + * + * @param sessionMetadata + * @param databaseId + * @param filter + */ + abstract getList( + sessionMetadata: SessionMetadata, + databaseId: string, + filter: CommandExecutionFilter, + ): Promise; + + /** + * Get single command execution entity, decrypt and convert to model + * + * @param sessionMetadata + * @param databaseId + * @param id + */ abstract getOne(sessionMetadata: SessionMetadata, databaseId: string, id: string): Promise; + + /** + * Delete single item + * + * @param sessionMetadata + * @param databaseId + * @param id + */ abstract delete(sessionMetadata: SessionMetadata, databaseId: string, id: string): Promise; - abstract deleteAll(sessionMetadata: SessionMetadata, databaseId: string): Promise; + + /** + * Delete all items + * + * @param sessionMetadata + * @param databaseId + * @param filter + */ + abstract deleteAll( + sessionMetadata: SessionMetadata, + databaseId: string, + filter: CommandExecutionFilter, + ): Promise; } diff --git a/redisinsight/api/src/modules/workbench/repositories/local-command-execution.repository.ts b/redisinsight/api/src/modules/workbench/repositories/local-command-execution.repository.ts index be0f4c6e05..499ab942bd 100644 --- a/redisinsight/api/src/modules/workbench/repositories/local-command-execution.repository.ts +++ b/redisinsight/api/src/modules/workbench/repositories/local-command-execution.repository.ts @@ -14,6 +14,7 @@ import { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto'; import { CommandExecutionRepository } from 'src/modules/workbench/repositories/command-execution.repository'; import config from 'src/utils/config'; import { SessionMetadata } from 'src/common/models'; +import { CommandExecutionFilter } from 'src/modules/workbench/models/command-executions.filter'; const WORKBENCH_CONFIG = config.get('workbench'); @@ -29,19 +30,20 @@ export class LocalCommandExecutionRepository extends CommandExecutionRepository private readonly encryptionService: EncryptionService, ) { super(); - this.modelEncryptor = new ModelEncryptor(encryptionService, ['command', 'result']); + this.modelEncryptor = new ModelEncryptor(this.encryptionService, ['command', 'result']); } /** - * Encrypt command executions and save entire entities + * @inheritDoc + * ___ + * Should encrypt command executions * Should always throw and error in case when unable to encrypt for some reason - * @param _ - * @param commandExecutions */ async createMany(_: SessionMetadata, commandExecutions: Partial[]): Promise { // todo: limit by 30 max to insert - let entities = await Promise.all(commandExecutions.map(async (commandExecution) => { + const response = await Promise.all(commandExecutions.map(async (commandExecution, idx) => { const entity = plainToClass(CommandExecutionEntity, commandExecution); + let isNotStored = false; // Do not store command execution result that exceeded limitation if (JSON.stringify(entity.result).length > WORKBENCH_CONFIG.maxResultSize) { @@ -52,31 +54,23 @@ export class LocalCommandExecutionRepository extends CommandExecutionRepository }, ]); // Hack, do not store isNotStored. Send once to show warning - entity['isNotStored'] = true; + isNotStored = true; } - return this.modelEncryptor.encryptEntity(entity); + return classToClass(CommandExecution, { + ...(await this.commandExecutionRepository.save(await this.modelEncryptor.encryptEntity(entity))), + command: commandExecutions[idx].command, // avoid decryption + mode: commandExecutions[idx].mode, + result: commandExecutions[idx].result, // avoid decryption + show original response when it was huge + summary: commandExecutions[idx].summary, + executionTime: commandExecutions[idx].executionTime, + isNotStored, + }); })); - entities = await this.commandExecutionRepository.save(entities); - - const response = await Promise.all( - entities.map((entity, idx) => classToClass( - CommandExecution, - { - ...entity, - command: commandExecutions[idx].command, - mode: commandExecutions[idx].mode, - result: commandExecutions[idx].result, - summary: commandExecutions[idx].summary, - executionTime: commandExecutions[idx].executionTime, - }, - )), - ); - // cleanup history and ignore error if any try { - await this.cleanupDatabaseHistory(entities[0].databaseId); + await this.cleanupDatabaseHistory(response[0].databaseId, { type: commandExecutions[0].type }); } catch (e) { this.logger.error('Error when trying to cleanup history after insert', e); } @@ -85,15 +79,17 @@ export class LocalCommandExecutionRepository extends CommandExecutionRepository } /** - * Fetch only needed fields to show in list to avoid huge decryption work - * @param _ - * @param databaseId + * @inheritDoc */ - async getList(_: SessionMetadata, databaseId: string): Promise { + async getList( + _: SessionMetadata, + databaseId: string, + queryFilter: CommandExecutionFilter, + ): Promise { this.logger.log('Getting command executions'); const entities = await this.commandExecutionRepository .createQueryBuilder('e') - .where({ databaseId }) + .where({ databaseId, type: queryFilter.type }) .select([ 'e.id', 'e.command', @@ -105,6 +101,7 @@ export class LocalCommandExecutionRepository extends CommandExecutionRepository 'e.resultsMode', 'e.executionTime', 'e.db', + 'e.type', ]) .orderBy('e.createdAt', 'DESC') .limit(WORKBENCH_CONFIG.maxItemsPerDb) @@ -127,11 +124,7 @@ export class LocalCommandExecutionRepository extends CommandExecutionRepository } /** - * Get single command execution entity, decrypt and convert to model - * - * @param _ - * @param databaseId - * @param id + * @inheritDoc */ async getOne(_: SessionMetadata, databaseId: string, id: string): Promise { this.logger.log('Getting command executions'); @@ -151,11 +144,7 @@ export class LocalCommandExecutionRepository extends CommandExecutionRepository } /** - * Delete single item - * - * @param _ - * @param databaseId - * @param id + * @inheritDoc */ async delete(_: SessionMetadata, databaseId: string, id: string): Promise { this.logger.log('Delete command execution'); @@ -166,28 +155,26 @@ export class LocalCommandExecutionRepository extends CommandExecutionRepository } /** - * Delete all items - * - * @param _ - * @param databaseId + * @inheritDoc */ - async deleteAll(_: SessionMetadata, databaseId: string): Promise { + async deleteAll(_: SessionMetadata, databaseId: string, queryFilter: CommandExecutionFilter): Promise { this.logger.log('Delete all command executions'); - await this.commandExecutionRepository.delete({ databaseId }); + await this.commandExecutionRepository.delete({ databaseId, type: queryFilter.type }); this.logger.log('Command executions deleted'); } /** - * Clean history for particular database to fit 30 items limitation + * Clean history for particular database to fit N items limitation * @param databaseId + * @param queryFilter */ - private async cleanupDatabaseHistory(databaseId: string): Promise { + private async cleanupDatabaseHistory(databaseId: string, queryFilter: CommandExecutionFilter): Promise { // todo: investigate why delete with sub-query doesn't works const idsToDelete = (await this.commandExecutionRepository .createQueryBuilder() - .where({ databaseId }) + .where({ databaseId, type: queryFilter.type }) .select('id') .orderBy('createdAt', 'DESC') .offset(WORKBENCH_CONFIG.maxItemsPerDb) diff --git a/redisinsight/api/src/modules/workbench/workbench.controller.ts b/redisinsight/api/src/modules/workbench/workbench.controller.ts index 1ba53775df..5803864756 100644 --- a/redisinsight/api/src/modules/workbench/workbench.controller.ts +++ b/redisinsight/api/src/modules/workbench/workbench.controller.ts @@ -6,6 +6,7 @@ import { Get, Param, Post, + Query, UseInterceptors, UsePipes, ValidationPipe, @@ -19,6 +20,7 @@ import { CreateCommandExecutionsDto } from 'src/modules/workbench/dto/create-com import { ShortCommandExecution } from 'src/modules/workbench/models/short-command-execution'; import { ClientMetadata } from 'src/common/models'; import { WorkbenchClientMetadata } from 'src/modules/workbench/decorators/workbench-client-metadata.decorator'; +import { CommandExecutionFilter } from 'src/modules/workbench/models/command-executions.filter'; @ApiTags('Workbench') @UsePipes(new ValidationPipe({ transform: true })) @@ -62,8 +64,9 @@ export class WorkbenchController { @ApiRedisParams() async listCommandExecutions( @WorkbenchClientMetadata() clientMetadata: ClientMetadata, + @Query() filter: CommandExecutionFilter, ): Promise { - return this.service.listCommandExecutions(clientMetadata); + return this.service.listCommandExecutions(clientMetadata, filter); } @ApiEndpoint({ @@ -107,7 +110,8 @@ export class WorkbenchController { @ApiRedisParams() async deleteCommandExecutions( @WorkbenchClientMetadata() clientMetadata: ClientMetadata, + @Body() filter: CommandExecutionFilter, ): Promise { - return this.service.deleteCommandExecutions(clientMetadata); + return this.service.deleteCommandExecutions(clientMetadata, filter); } } diff --git a/redisinsight/api/src/modules/workbench/workbench.service.ts b/redisinsight/api/src/modules/workbench/workbench.service.ts index 2a4a972995..30142a1c8d 100644 --- a/redisinsight/api/src/modules/workbench/workbench.service.ts +++ b/redisinsight/api/src/modules/workbench/workbench.service.ts @@ -2,8 +2,8 @@ import { Injectable } from '@nestjs/common'; import { omit } from 'lodash'; import { ClientMetadata } from 'src/common/models'; import { WorkbenchCommandsExecutor } from 'src/modules/workbench/providers/workbench-commands.executor'; -import { CommandExecution } from 'src/modules/workbench/models/command-execution'; -import { CreateCommandExecutionDto, ResultsMode } from 'src/modules/workbench/dto/create-command-execution.dto'; +import { CommandExecution, ResultsMode } from 'src/modules/workbench/models/command-execution'; +import { CreateCommandExecutionDto } from 'src/modules/workbench/dto/create-command-execution.dto'; import { CreateCommandExecutionsDto } from 'src/modules/workbench/dto/create-command-executions.dto'; import { getBlockingCommands, multilineCommandToOneLine } from 'src/utils/cli-helper'; import ERROR_MESSAGES from 'src/constants/error-messages'; @@ -12,6 +12,7 @@ import { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto'; import { CommandExecutionRepository } from 'src/modules/workbench/repositories/command-execution.repository'; import { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory'; import { RedisClient } from 'src/modules/redis/client'; +import { CommandExecutionFilter } from 'src/modules/workbench/models/command-executions.filter'; import { getUnsupportedCommands } from './utils/getUnsupportedCommands'; import { WorkbenchAnalyticsService } from './services/workbench-analytics/workbench-analytics.service'; @@ -160,9 +161,13 @@ export class WorkbenchService { * Get list command execution history per instance (last 30 items) * * @param clientMetadata + * @param filter */ - async listCommandExecutions(clientMetadata: ClientMetadata): Promise { - return this.commandExecutionRepository.getList(clientMetadata.sessionMetadata, clientMetadata.databaseId); + async listCommandExecutions( + clientMetadata: ClientMetadata, + filter: CommandExecutionFilter, + ): Promise { + return this.commandExecutionRepository.getList(clientMetadata.sessionMetadata, clientMetadata.databaseId, filter); } /** @@ -190,9 +195,10 @@ export class WorkbenchService { * Delete command executions by databaseId * * @param clientMetadata + * @param filter */ - async deleteCommandExecutions(clientMetadata: ClientMetadata): Promise { - await this.commandExecutionRepository.deleteAll(clientMetadata.sessionMetadata, clientMetadata.databaseId); + async deleteCommandExecutions(clientMetadata: ClientMetadata, filter: CommandExecutionFilter): Promise { + await this.commandExecutionRepository.deleteAll(clientMetadata.sessionMetadata, clientMetadata.databaseId, filter); } /** From 3ba5eb6dccd6f5cf5b4803cdc8e10ff67ae8a9cc Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Mon, 9 Sep 2024 16:00:10 +0200 Subject: [PATCH 053/112] #RI-6079 - update colors --- redisinsight/ui/src/constants/monaco/theme.ts | 36 ++++++++++++++++++ .../ui/src/utils/monaco/monacoThemes.ts | 37 ++----------------- 2 files changed, 39 insertions(+), 34 deletions(-) diff --git a/redisinsight/ui/src/constants/monaco/theme.ts b/redisinsight/ui/src/constants/monaco/theme.ts index bdfcbe0d29..0a48b48ded 100644 --- a/redisinsight/ui/src/constants/monaco/theme.ts +++ b/redisinsight/ui/src/constants/monaco/theme.ts @@ -26,3 +26,39 @@ export const lightTheme: monaco.editor.IStandaloneThemeData = { rules: lightThemeRules, colors: {} } + +export const redisearchDarKThemeRules = [ + { token: 'keyword', foreground: '#8094B1', fontStyle: 'bold' }, + { token: 'argument.block.0', foreground: '#BDE8D7' }, + { token: 'argument.block.1', foreground: '#8CD7B9' }, + { token: 'argument.block.2', foreground: '#5BC69B' }, + { token: 'argument.block.3', foreground: '#3A8365' }, + { token: 'argument.block.withToken.0', foreground: '#BDE8D7' }, + { token: 'argument.block.withToken.1', foreground: '#8CD7B9' }, + { token: 'argument.block.withToken.2', foreground: '#5BC69B' }, + { token: 'argument.block.withToken.3', foreground: '#3A8365' }, + { token: 'loadAll', foreground: '#BDE8D7' }, + { token: 'index', foreground: '#DE47BB' }, + { token: 'query', foreground: '#7B90E0' }, + { token: 'field', foreground: '#B02C30' }, + { token: 'query.operator', foreground: '#B9F0F3' }, + { token: 'function', foreground: '#9E7EE8' }, +] + +export const redisearchLightThemeRules = [ + { token: 'keyword', foreground: '#8094B1', fontStyle: 'bold' }, + { token: 'argument.block.0', foreground: '#BDE8D7' }, + { token: 'argument.block.1', foreground: '#8CD7B9' }, + { token: 'argument.block.2', foreground: '#5BC69B' }, + { token: 'argument.block.3', foreground: '#3A8365' }, + { token: 'argument.block.withToken.0', foreground: '#BDE8D7' }, + { token: 'argument.block.withToken.1', foreground: '#8CD7B9' }, + { token: 'argument.block.withToken.2', foreground: '#5BC69B' }, + { token: 'argument.block.withToken.3', foreground: '#3A8365' }, + { token: 'loadAll', foreground: '#BDE8D7' }, + { token: 'index', foreground: '#DE47BB' }, + { token: 'query', foreground: '#7B90E0' }, + { token: 'field', foreground: '#B02C30' }, + { token: 'query.operator', foreground: '#B9F0F3' }, + { token: 'function', foreground: '#9E7EE8' }, +] diff --git a/redisinsight/ui/src/utils/monaco/monacoThemes.ts b/redisinsight/ui/src/utils/monaco/monacoThemes.ts index 3982d4e651..c3fd5313a3 100644 --- a/redisinsight/ui/src/utils/monaco/monacoThemes.ts +++ b/redisinsight/ui/src/utils/monaco/monacoThemes.ts @@ -1,4 +1,5 @@ import { monaco as monacoEditor } from 'react-monaco-editor' +import { redisearchDarKThemeRules, redisearchLightThemeRules } from 'uiSrc/constants/monaco' export enum RedisearchMonacoTheme { dark = 'redisearchDarkTheme', @@ -9,46 +10,14 @@ export const installRedisearchTheme = () => { monacoEditor.editor.defineTheme(RedisearchMonacoTheme.dark, { base: 'vs-dark', inherit: true, - rules: [ - { token: 'keyword', foreground: '#569cd6', fontStyle: 'bold' }, - { token: 'argument.block.0', foreground: '#66ccaf' }, - { token: 'argument.block.1', foreground: '#459d7f' }, - { token: 'argument.block.2', foreground: '#3c816a' }, - { token: 'argument.block.3', foreground: '#28644f' }, - { token: 'argument.block.withToken.0', foreground: '#66ccaf' }, - { token: 'argument.block.withToken.1', foreground: '#459d7f' }, - { token: 'argument.block.withToken.2', foreground: '#3c816a' }, - { token: 'argument.block.withToken.3', foreground: '#28644f' }, - { token: 'loadAll', foreground: '#6db9a2' }, - { token: 'index', foreground: '#ce51cc' }, - { token: 'query', foreground: '#5183ce' }, - { token: 'field', foreground: '#c43265' }, - { token: 'query.operator', foreground: '#a4e7df' }, - { token: 'function', foreground: '#aa58d2' }, - ], + rules: redisearchDarKThemeRules, colors: {} }) monacoEditor.editor.defineTheme(RedisearchMonacoTheme.light, { base: 'vs', inherit: true, - rules: [ - { token: 'keyword', foreground: '#569cd6', fontStyle: 'bold' }, - { token: 'argument.block.0', foreground: '#66ccaf' }, - { token: 'argument.block.1', foreground: '#459d7f' }, - { token: 'argument.block.2', foreground: '#3c816a' }, - { token: 'argument.block.3', foreground: '#28644f' }, - { token: 'argument.block.withToken.0', foreground: '#66ccaf' }, - { token: 'argument.block.withToken.1', foreground: '#459d7f' }, - { token: 'argument.block.withToken.2', foreground: '#3c816a' }, - { token: 'argument.block.withToken.3', foreground: '#28644f' }, - { token: 'loadAll', foreground: '#6db9a2' }, - { token: 'index', foreground: '#ce51cc' }, - { token: 'field', foreground: '#5183ce' }, - { token: 'field', foreground: '#c43265' }, - { token: 'query.operator', foreground: '#a4e7df' }, - { token: 'function', foreground: '#aa58d2' }, - ], + rules: redisearchLightThemeRules, colors: {} }) } From 829d39ede830171ea506a451e56f1ffa04535558 Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Tue, 10 Sep 2024 11:31:02 +0200 Subject: [PATCH 054/112] test for APPLY, FILTER and REDUCE --- .../e2e/pageObjects/search-and-query-page.ts | 2 +- .../search-and-query-tab.e2e.ts | 93 ++++++++++++++++--- 2 files changed, 80 insertions(+), 15 deletions(-) diff --git a/tests/e2e/pageObjects/search-and-query-page.ts b/tests/e2e/pageObjects/search-and-query-page.ts index 7943ca5131..b4fa96b8e0 100644 --- a/tests/e2e/pageObjects/search-and-query-page.ts +++ b/tests/e2e/pageObjects/search-and-query-page.ts @@ -7,7 +7,7 @@ export class SearchAndQueryPage extends BaseRunCommandsPage { * Select query using autosuggest * @param query Value of query */ - async selectQueryUsingAutosuggest(value: string): Promise { + async selectFieldUsingAutosuggest(value: string): Promise { await t.wait(200); await t.typeText(this.queryInput, '@', { replace: false }); await t.expect(this.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed'); diff --git a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts index 1b50e224be..cc334fa495 100644 --- a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts +++ b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts @@ -35,6 +35,7 @@ fixture `Autocomplete for entered commands in search and query` // Create 3 keys and index await browserPage.Cli.sendCommandsInCli(commands); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); }) .afterEach(async() => { // Clear and delete database @@ -44,9 +45,9 @@ fixture `Autocomplete for entered commands in search and query` await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that tutorials can be opened from Workbench', async t => { - const search = await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); - await t.click(search.getTutorialLinkLocator('sq-exact-match')); - await t.expect(search.InsightsPanel.sidePanel.exists).ok('Insight panel is not opened'); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); + await t.click(searchAndQueryPage.getTutorialLinkLocator('sq-exact-match')); + await t.expect(searchAndQueryPage.InsightsPanel.sidePanel.exists).ok('Insight panel is not opened'); const tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials); await t.expect(tab.preselectArea.textContent).contains('EXACT MATCH', 'the tutorial page is incorrect'); }); @@ -59,7 +60,6 @@ test('Verify that user can use show more to see command fully in 2nd tooltip', a 'required query', 'optional [verbatim]' ]; - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); await t.typeText(searchAndQueryPage.queryInput, 'FT', { replace: true }); // Verify that user can use show more to see command fully in 2nd tooltip await t.pressKey('ctrl+space'); @@ -83,8 +83,6 @@ test('Verify full commands suggestions with index and query for FT.AGGREGATE', a 'students', 'type' ]; - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); - // Verify basic commands suggestions FT.SEARCH and FT.AGGREGATE await t.typeText(searchAndQueryPage.queryInput, 'FT', { replace: true }); // Verify that the list with FT.SEARCH and FT.AGGREGATE auto-suggestions is displayed @@ -149,7 +147,6 @@ test('Verify full commands suggestions with index and query for FT.AGGREGATE', a await t.expect((await searchAndQueryPage.queryInputForText.innerText).trim().replace(/\s+/g, ' ')).contains(expectedText, 'Incorrect order of entered arguments'); }); test('Verify full commands suggestions with index and query for FT.SEARCH', async t => { - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); await t.typeText(searchAndQueryPage.queryInput, 'FT.SE', { replace: true }); // Select command and check result await t.pressKey('enter'); @@ -158,7 +155,7 @@ test('Verify full commands suggestions with index and query for FT.SEARCH', asyn await t.pressKey('tab'); // Select '@city' field - await searchAndQueryPage.selectQueryUsingAutosuggest('city'); + await searchAndQueryPage.selectFieldUsingAutosuggest('city'); await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('DIALECT').exists).ok('FT.SEARCH arguments not suggested'); await t.typeText(searchAndQueryPage.queryInput, 'n', { replace: false }); await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.nth(0).textContent).contains('NOCONTENT', 'Argument not suggested after typing first letters'); @@ -178,7 +175,6 @@ test('Verify full commands suggestions with index and query for FT.SEARCH', asyn await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestWidget.textContent).contains('No suggestions.', 'Index not auto-suggested'); }); test('Verify full commands suggestions with index and query for FT.PROFILE(SEARCH)', async t => { - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); await t.typeText(searchAndQueryPage.queryInput, 'FT.PR', { replace: true }); // Select command and check result await t.pressKey('enter'); @@ -198,7 +194,7 @@ test('Verify full commands suggestions with index and query for FT.PROFILE(SEARC // Select QUERY await t.typeText(searchAndQueryPage.queryInput, 'QUE', { replace: false }); await t.pressKey('enter'); - await searchAndQueryPage.selectQueryUsingAutosuggest('city'); + await searchAndQueryPage.selectFieldUsingAutosuggest('city'); // Verify that there are no more suggestions await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.exists).notOk('Additional invalid commands suggested'); const expectedText = `FT.PROFILE "${indexName1}" SEARCH QUERY "@city"`.trim().replace(/\s+/g, ' '); @@ -206,7 +202,6 @@ test('Verify full commands suggestions with index and query for FT.PROFILE(SEARC await t.expect((await searchAndQueryPage.queryInputForText.innerText).trim().replace(/\s+/g, ' ')).contains(expectedText, 'Incorrect order of entered arguments'); }); test('Verify full commands suggestions with index and query for FT.PROFILE(AGGREGATE)', async t => { - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); await t.typeText(searchAndQueryPage.queryInput, 'FT.PR', { replace: true }); // Select command and check result await t.pressKey('enter'); @@ -220,7 +215,7 @@ test('Verify full commands suggestions with index and query for FT.PROFILE(AGGRE // Select QUERY await t.typeText(searchAndQueryPage.queryInput, 'QUE', { replace: false }); await t.pressKey('enter'); - await searchAndQueryPage.selectQueryUsingAutosuggest('city'); + await searchAndQueryPage.selectFieldUsingAutosuggest('city'); // Verify that there are no more suggestions await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.exists).notOk('Additional invalid commands suggested'); const expectedText = `FT.PROFILE "${indexName1}" AGGREGATE QUERY "@city"`.trim().replace(/\s+/g, ' '); @@ -228,12 +223,11 @@ test('Verify full commands suggestions with index and query for FT.PROFILE(AGGRE await t.expect((await searchAndQueryPage.queryInputForText.innerText).trim().replace(/\s+/g, ' ')).contains(expectedText, 'Incorrect order of entered arguments'); }); test('Verify full commands suggestions with index and query for FT.EXPLAIN', async t => { - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); await t.typeText(searchAndQueryPage.queryInput, 'FT.EX', { replace: true }); // Select command and check result await t.pressKey('enter'); await t.pressKey('tab'); - await searchAndQueryPage.selectQueryUsingAutosuggest('city'); + await searchAndQueryPage.selectFieldUsingAutosuggest('city'); await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('DIALECT').exists).ok('FT.EXPLAIN arguments not suggested'); // Add DIALECT @@ -246,3 +240,74 @@ test('Verify full commands suggestions with index and query for FT.EXPLAIN', asy // Verify command entered correctly await t.expect((await searchAndQueryPage.queryInputForText.innerText).trim().replace(/\s+/g, ' ')).contains(expectedText, 'Incorrect order of entered arguments'); }); +test('Verify commands suggestions for APPLY and FILTER', async t => { + await t.typeText(searchAndQueryPage.queryInput, 'FT.AGGREGATE ', { replace: true }); + await t.pressKey('enter'); + + await t.typeText(searchAndQueryPage.queryInput, '*'); + await t.pressKey('right'); + await t.pressKey('space'); + //Verify APPLY command + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('APPLY').exists).ok('Apply is not suggested'); + await t.pressKey('enter'); + + await t.typeText(searchAndQueryPage.queryInput, 'g'); + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.exists).ok('commands is not suggested'); + await t.pressKey('enter'); + await t.typeText(searchAndQueryPage.queryInput, '@', { replace: false }); + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed'); + await t.typeText(searchAndQueryPage.queryInput, 'location', { replace: false }); + await t.typeText(searchAndQueryPage.queryInput, ', \'40.7128,-74.0060\''); + for (let i = 0; i < 3; i++) { + await t.pressKey('right'); + } + await t.pressKey('space'); + await t.pressKey('tab'); + await t.typeText(searchAndQueryPage.queryInput, 'apply_key', { replace: false }); + + await t.pressKey('space'); + //Verify Filter command + await t.typeText(searchAndQueryPage.queryInput, 'F'); + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('FILTER').exists).ok('FILTER is not suggested'); + await t.pressKey('enter'); + await t.typeText(searchAndQueryPage.queryInput, 'apply_key < 5000', { replace: false }); + await t.pressKey('right'); + await t.pressKey('space'); + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('GROUPBY').exists).ok('query can not be prolong'); +}); + +test('Verify REDUCE commands', async t => { + await t.typeText(searchAndQueryPage.queryInput, `FT.AGGREGATE ${indexName1} "*" GROUPBY 1 @location`, { replace: true }); + await t.pressKey('space'); + // select Reduce + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('REDUCE').exists).ok('REDUCE is not suggested'); + await t.typeText(searchAndQueryPage.queryInput, 'R'); + await t.pressKey('enter'); + + // set value of reduce + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed'); + await t.typeText(searchAndQueryPage.queryInput, 'CO'); + await t.pressKey('enter'); + await t.typeText(searchAndQueryPage.queryInput, '0'); + + // verify that count of nargs is correct + await t.pressKey('space'); + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('AS').exists).ok('AS is not suggested'); + await t.pressKey('enter'); + await t.typeText(searchAndQueryPage.queryInput, 'item_count '); + + //add additional reduce + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('REDUCE').exists).ok('Apply is not suggested'); + await t.typeText(searchAndQueryPage.queryInput, 'R'); + await t.pressKey('enter'); + await t.typeText(searchAndQueryPage.queryInput, 'SUM'); + await t.pressKey('enter'); + await t.typeText(searchAndQueryPage.queryInput, '1 '); + + await t.typeText(searchAndQueryPage.queryInput, '@', { replace: false }); + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed'); + await t.typeText(searchAndQueryPage.queryInput, 'students ', { replace: false }); + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('AS').exists).ok('AS is not suggested'); + await t.pressKey('enter'); + await t.typeText(searchAndQueryPage.queryInput, 'total_students'); +}); From ce73e8274aacf16a98886d4ebb16c23476578201 Mon Sep 17 00:00:00 2001 From: ArtemHoruzhenko Date: Tue, 10 Sep 2024 14:27:21 +0300 Subject: [PATCH 055/112] fix tests + small rework --- redisinsight/api/src/__mocks__/common.ts | 3 +- redisinsight/api/src/__mocks__/index.ts | 1 + redisinsight/api/src/__mocks__/workbench.ts | 100 ++++++ .../workbench/models/command-execution.ts | 6 +- .../models/plugin-command-execution.ts | 7 +- .../modules/workbench/plugins.service.spec.ts | 43 +-- .../workbench-commands.executor.spec.ts | 2 +- .../providers/workbench-commands.executor.ts | 2 +- ...local-command-execution.repository.spec.ts | 288 ++++++++---------- .../local-command-execution.repository.ts | 2 +- .../workbench/workbench.service.spec.ts | 135 ++++---- ...id-workbench-command_executions-id.test.ts | 24 +- ...es-id-workbench-command_executions.test.ts | 91 ++++-- ...id-workbench-command_executions-id.test.ts | 3 +- ...es-id-workbench-command_executions.test.ts | 80 +++-- ...es-id-workbench-command_executions.test.ts | 25 +- 16 files changed, 437 insertions(+), 375 deletions(-) create mode 100644 redisinsight/api/src/__mocks__/workbench.ts diff --git a/redisinsight/api/src/__mocks__/common.ts b/redisinsight/api/src/__mocks__/common.ts index 2e3e86348a..874e8a0c79 100644 --- a/redisinsight/api/src/__mocks__/common.ts +++ b/redisinsight/api/src/__mocks__/common.ts @@ -8,6 +8,7 @@ export type MockType = { }; export const mockQueryBuilderWhere = jest.fn().mockReturnThis(); +export const mockQueryBuilderWhereInIds = jest.fn().mockReturnThis(); export const mockQueryBuilderSelect = jest.fn().mockReturnThis(); export const mockQueryBuilderGetOne = jest.fn(); export const mockQueryBuilderGetMany = jest.fn(); @@ -17,6 +18,7 @@ export const mockQueryBuilderExecute = jest.fn(); export const mockCreateQueryBuilder = jest.fn(() => ({ // where: jest.fn().mockReturnThis(), where: mockQueryBuilderWhere, + whereInIds: mockQueryBuilderWhereInIds, orWhere: mockQueryBuilderWhere, update: jest.fn().mockReturnThis(), select: mockQueryBuilderSelect, @@ -29,7 +31,6 @@ export const mockCreateQueryBuilder = jest.fn(() => ({ leftJoinAndSelect: jest.fn().mockReturnThis(), offset: jest.fn().mockReturnThis(), delete: jest.fn().mockReturnThis(), - whereInIds: jest.fn().mockReturnThis(), execute: mockQueryBuilderExecute, getCount: mockQueryBuilderGetCount, getRawMany: mockQueryBuilderGetManyRaw, diff --git a/redisinsight/api/src/__mocks__/index.ts b/redisinsight/api/src/__mocks__/index.ts index 8999864e19..19a2c1aad5 100644 --- a/redisinsight/api/src/__mocks__/index.ts +++ b/redisinsight/api/src/__mocks__/index.ts @@ -41,3 +41,4 @@ export * from './cloud-session'; export * from './database-info'; export * from './cloud-job'; export * from './rdi'; +export * from './workbench'; diff --git a/redisinsight/api/src/__mocks__/workbench.ts b/redisinsight/api/src/__mocks__/workbench.ts new file mode 100644 index 0000000000..c5b94301e5 --- /dev/null +++ b/redisinsight/api/src/__mocks__/workbench.ts @@ -0,0 +1,100 @@ +import { v4 as uuidv4 } from 'uuid'; +import { + CommandExecution, + CommandExecutionType, + ResultsMode, + RunQueryMode, +} from 'src/modules/workbench/models/command-execution'; +import { CommandExecutionEntity } from 'src/modules/workbench/entities/command-execution.entity'; +import { mockDatabase } from 'src/__mocks__/databases'; +import { CommandExecutionResult } from 'src/modules/workbench/models/command-execution-result'; +import { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto'; +import { CreateCommandExecutionDto } from 'src/modules/workbench/dto/create-command-execution.dto'; +import { ShortCommandExecution } from 'src/modules/workbench/models/short-command-execution'; +import { CommandExecutionFilter } from 'src/modules/workbench/models/command-executions.filter'; +import ERROR_MESSAGES from 'src/constants/error-messages'; +import { PluginCommandExecution } from 'src/modules/workbench/models/plugin-command-execution'; + +export const mockCommandExecutionUnsupportedCommandResult = Object.assign(new CommandExecutionResult(), { + response: ERROR_MESSAGES.PLUGIN_COMMAND_NOT_SUPPORTED('subscribe'.toUpperCase()), + status: CommandExecutionStatus.Fail, +}); + +export const mockCommandExecutionSuccessResult = Object.assign(new CommandExecutionResult(), { + status: CommandExecutionStatus.Success, + response: 'bar', +}); + +export const mockCommendExecutionHugeResultPlaceholder = Object.assign(new CommandExecutionResult(), { + status: CommandExecutionStatus.Success, + response: 'Results have been deleted since they exceed 1 MB. Re-run the command to see new results.', +}); + +export const mockCommendExecutionHugeResultPlaceholderEncrypted = 'huge_result_placeholder_encrypted'; + +export const mockCommandExecution = Object.assign(new CommandExecution(), { + id: uuidv4(), + databaseId: mockDatabase.id, + command: 'get foo', + mode: RunQueryMode.ASCII, + resultsMode: ResultsMode.Default, + type: CommandExecutionType.Workbench, + result: [mockCommandExecutionSuccessResult], + createdAt: new Date(), + db: 0, +}); + +export const mockCommandExecutionEntity = Object.assign(new CommandExecutionEntity(), { + ...mockCommandExecution, + command: 'encrypted_command', + result: `${JSON.stringify([mockCommandExecutionSuccessResult])}_encrypted`, + encryption: 'KEYTAR', +}); + +export const mockShortCommandExecution = Object.assign(new ShortCommandExecution(), { + id: mockCommandExecution.id, + databaseId: mockCommandExecution.id, + command: mockCommandExecution.command, + createdAt: mockCommandExecution.createdAt, + mode: mockCommandExecution.mode, + summary: mockCommandExecution.summary, + resultsMode: mockCommandExecution.resultsMode, + executionTime: mockCommandExecution.executionTime, + db: mockCommandExecution.db, + type: mockCommandExecution.type, +}); + +export const mockShortCommandExecutionEntity = Object.assign(new CommandExecutionEntity(), { + ...mockShortCommandExecution, + command: mockCommandExecutionEntity.command, + encryption: mockCommandExecutionEntity.encryption, +}); + +export const mockCreateCommandExecutionDto = Object.assign(new CreateCommandExecutionDto(), { + command: mockCommandExecution.command, + mode: mockCommandExecution.mode, + resultsMode: mockCommandExecution.resultsMode, + type: mockCommandExecution.type, +}); + +export const mockCommandExecutionFilter = Object.assign(new CommandExecutionFilter(), { + type: mockCommandExecution.type, +}); + +export const mockPluginCommandExecution = Object.assign(new PluginCommandExecution(), { + ...mockCreateCommandExecutionDto, + databaseId: mockDatabase.id, + result: [mockCommandExecutionSuccessResult], +}); + +export const mockWorkbenchCommandsExecutor = () => ({ + sendCommand: jest.fn().mockResolvedValue([mockCommandExecutionSuccessResult]), +}); + +export const mockCommandExecutionRepository = () => ({ + createMany: jest.fn().mockResolvedValue([mockCommandExecution]), + getList: jest.fn().mockResolvedValue([mockCommandExecution]), + getOne: jest.fn().mockResolvedValue(mockCommandExecution), + delete: jest.fn(), + deleteAll: jest.fn(), +}); diff --git a/redisinsight/api/src/modules/workbench/models/command-execution.ts b/redisinsight/api/src/modules/workbench/models/command-execution.ts index 37d9fafa09..f22b9beb3f 100644 --- a/redisinsight/api/src/modules/workbench/models/command-execution.ts +++ b/redisinsight/api/src/modules/workbench/models/command-execution.ts @@ -159,7 +159,11 @@ export class CommandExecution { }) @Expose() @IsOptional() - @IsEnum(CommandExecutionType) + @IsEnum(CommandExecutionType, { + message: `type must be a valid enum value. Valid values: ${Object.values( + CommandExecutionType, + )}.`, + }) @Default(CommandExecutionType.Workbench) type?: CommandExecutionType; } diff --git a/redisinsight/api/src/modules/workbench/models/plugin-command-execution.ts b/redisinsight/api/src/modules/workbench/models/plugin-command-execution.ts index 8dc38bb30f..94add4f14d 100644 --- a/redisinsight/api/src/modules/workbench/models/plugin-command-execution.ts +++ b/redisinsight/api/src/modules/workbench/models/plugin-command-execution.ts @@ -3,9 +3,4 @@ import { OmitType, PartialType } from '@nestjs/swagger'; export class PluginCommandExecution extends PartialType( OmitType(CommandExecution, ['createdAt', 'id'] as const), -) { - constructor(partial: Partial) { - super(); - Object.assign(this, partial); - } -} +) {} diff --git a/redisinsight/api/src/modules/workbench/plugins.service.spec.ts b/redisinsight/api/src/modules/workbench/plugins.service.spec.ts index ea1580cf60..4f75731fdf 100644 --- a/redisinsight/api/src/modules/workbench/plugins.service.spec.ts +++ b/redisinsight/api/src/modules/workbench/plugins.service.spec.ts @@ -1,20 +1,16 @@ import { Test, TestingModule } from '@nestjs/testing'; import { mockClientMetadata, - mockDatabase, + mockCommandExecutionUnsupportedCommandResult, + mockCreateCommandExecutionDto, mockDatabaseClientFactory, + mockPluginCommandExecution, mockWhitelistCommandsResponse, mockWorkbenchClientMetadata, + mockWorkbenchCommandsExecutor, } from 'src/__mocks__'; import { v4 as uuidv4 } from 'uuid'; import { WorkbenchCommandsExecutor } from 'src/modules/workbench/providers/workbench-commands.executor'; -import { - CreateCommandExecutionDto, - ResultsMode, - RunQueryMode, -} from 'src/modules/workbench/dto/create-command-execution.dto'; -import { CommandExecutionResult } from 'src/modules/workbench/models/command-execution-result'; -import { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto'; import { BadRequestException } from '@nestjs/common'; import ERROR_MESSAGES from 'src/constants/error-messages'; import { PluginsService } from 'src/modules/workbench/plugins.service'; @@ -24,27 +20,10 @@ import { PluginStateRepository } from 'src/modules/workbench/repositories/plugin import { PluginState } from 'src/modules/workbench/models/plugin-state'; import config from 'src/utils/config'; import { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory'; +import { RunQueryMode } from 'src/modules/workbench/models/command-execution'; const PLUGINS_CONFIG = config.get('plugins'); -const mockCreateCommandExecutionDto: CreateCommandExecutionDto = { - command: 'get foo', - mode: RunQueryMode.ASCII, - resultsMode: ResultsMode.Default, -}; - -const mockCommandExecutionResults: CommandExecutionResult[] = [ - new CommandExecutionResult({ - status: CommandExecutionStatus.Success, - response: 'OK', - }), -]; -const mockPluginCommandExecution = new PluginCommandExecution({ - ...mockCreateCommandExecutionDto, - databaseId: mockDatabase.id, - result: mockCommandExecutionResults, -}); - const mockVisualizationId = 'pluginName_visualizationName'; const mockCommandExecutionId = uuidv4(); const mockState = { @@ -80,9 +59,7 @@ describe('PluginsService', () => { PluginsService, { provide: WorkbenchCommandsExecutor, - useFactory: () => ({ - sendCommand: jest.fn(), - }), + useFactory: mockWorkbenchCommandsExecutor, }, { provide: PluginCommandsWhitelistProvider, @@ -107,7 +84,6 @@ describe('PluginsService', () => { describe('sendCommand', () => { it('should successfully execute command', async () => { - workbenchCommandsExecutor.sendCommand.mockResolvedValueOnce(mockCommandExecutionResults); pluginsCommandsWhitelistProvider.getWhitelistCommands.mockResolvedValueOnce(mockWhitelistCommandsResponse); const result = await service.sendCommand(mockWorkbenchClientMetadata, mockCreateCommandExecutionDto); @@ -128,10 +104,7 @@ describe('PluginsService', () => { expect(result).toEqual(new PluginCommandExecution({ ...dto, databaseId: mockWorkbenchClientMetadata.databaseId, - result: [new CommandExecutionResult({ - response: ERROR_MESSAGES.PLUGIN_COMMAND_NOT_SUPPORTED('subscribe'.toUpperCase()), - status: CommandExecutionStatus.Fail, - })], + result: [mockCommandExecutionUnsupportedCommandResult], })); expect(workbenchCommandsExecutor.sendCommand).not.toHaveBeenCalled(); }); @@ -140,7 +113,6 @@ describe('PluginsService', () => { workbenchCommandsExecutor.sendCommand.mockRejectedValueOnce(new BadRequestException('error')); const dto = { - ...mockCommandExecutionResults, command: 'get foo', mode: RunQueryMode.ASCII, }; @@ -155,7 +127,6 @@ describe('PluginsService', () => { }); describe('getWhitelistCommands', () => { it('should successfully return whitelisted commands', async () => { - workbenchCommandsExecutor.sendCommand.mockResolvedValueOnce(mockCommandExecutionResults); pluginsCommandsWhitelistProvider.getWhitelistCommands.mockResolvedValueOnce(mockWhitelistCommandsResponse); const result = await service.getWhitelistCommands(mockWorkbenchClientMetadata); diff --git a/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.spec.ts b/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.spec.ts index 741b61d50b..4db9dc9cd6 100644 --- a/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.spec.ts +++ b/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.spec.ts @@ -11,7 +11,6 @@ import { unknownCommand } from 'src/constants'; import { WorkbenchCommandsExecutor } from 'src/modules/workbench/providers/workbench-commands.executor'; import { CreateCommandExecutionDto, - RunQueryMode, } from 'src/modules/workbench/dto/create-command-execution.dto'; import { CommandExecutionResult } from 'src/modules/workbench/models/command-execution-result'; import { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto'; @@ -19,6 +18,7 @@ import { ServiceUnavailableException } from '@nestjs/common'; import { CommandNotSupportedError, CommandParsingError } from 'src/modules/cli/constants/errors'; import { FormatterManager, IFormatterStrategy, FormatterTypes } from 'src/common/transformers'; import { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory'; +import { RunQueryMode } from 'src/modules/workbench/models/command-execution'; import { WorkbenchAnalyticsService } from '../services/workbench-analytics/workbench-analytics.service'; const MOCK_ERROR_MESSAGE = 'Some error'; diff --git a/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.ts b/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.ts index 8f6a30684d..223da5efcf 100644 --- a/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.ts +++ b/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.ts @@ -45,7 +45,7 @@ export class WorkbenchCommandsExecutor { /** * Entrypoint for any CommandExecution - * Will determine type of a command (standalone, per node(s)) and format, and execute it + * Will determine type of command (standalone, per node(s)) and format, and execute it * Also sis a single place of analytics events invocation * @param client * @param dto diff --git a/redisinsight/api/src/modules/workbench/repositories/local-command-execution.repository.spec.ts b/redisinsight/api/src/modules/workbench/repositories/local-command-execution.repository.spec.ts index 10703459f9..3769d6bd0a 100644 --- a/redisinsight/api/src/modules/workbench/repositories/local-command-execution.repository.spec.ts +++ b/redisinsight/api/src/modules/workbench/repositories/local-command-execution.repository.spec.ts @@ -1,23 +1,19 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { v4 as uuidv4 } from 'uuid'; +import { when } from 'jest-when'; import { mockEncryptionService, - mockEncryptResult, - mockQueryBuilderGetMany, - mockQueryBuilderGetManyRaw, mockRepository, - mockDatabase, MockType, mockSessionMetadata, + mockCommandExecutionEntity, + mockCommandExecution, + mockCommendExecutionHugeResultPlaceholder, + mockCommendExecutionHugeResultPlaceholderEncrypted, + mockShortCommandExecutionEntity, + mockShortCommandExecution, + mockCommandExecutionFilter, } from 'src/__mocks__'; -import { omit } from 'lodash'; -import { - CreateCommandExecutionDto, - RunQueryMode, -} from 'src/modules/workbench/dto/create-command-execution.dto'; -import { CommandExecution } from 'src/modules/workbench/models/command-execution'; -import { CommandExecutionResult } from 'src/modules/workbench/models/command-execution-result'; -import { CommandExecutionStatus, ICliExecResultFromNode } from 'src/modules/cli/dto/cli.dto'; +import { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto'; import { NotFoundException } from '@nestjs/common'; import { EncryptionService } from 'src/modules/encryption/encryption.service'; import { Repository } from 'typeorm'; @@ -30,49 +26,14 @@ import { LocalCommandExecutionRepository } from 'src/modules/workbench/repositor const WORKBENCH_CONFIG = config.get('workbench'); -const mockNodeEndpoint = { - host: '127.0.0.1', - port: 6379, -}; - -const mockCliNodeResponse: ICliExecResultFromNode = { - ...mockNodeEndpoint, - response: 'OK', - status: CommandExecutionStatus.Success, -}; - -const mockCreateCommandExecutionDto: CreateCommandExecutionDto = { - command: 'set foo bar', - mode: RunQueryMode.ASCII, -}; - -const mockCommandExecutionEntity = new CommandExecutionEntity({ - id: uuidv4(), - databaseId: mockDatabase.id, - command: mockEncryptResult.data, - result: mockEncryptResult.data, - mode: mockCreateCommandExecutionDto.mode, - encryption: 'KEYTAR', - createdAt: new Date(), -}); - -const mockCommandExecutionResult: CommandExecutionResult = { - status: mockCliNodeResponse.status, - response: mockCliNodeResponse.response, -}; - -const mockCommandExecutionPartial: Partial = new CommandExecution({ - ...mockCreateCommandExecutionDto, - databaseId: mockDatabase.id, - result: [mockCommandExecutionResult], -}); - describe('LocalCommandExecutionRepository', () => { let service: LocalCommandExecutionRepository; let repository: MockType>; - let encryptionService; + let encryptionService: MockType; beforeEach(async () => { + jest.clearAllMocks(); + const module: TestingModule = await Test.createTestingModule({ providers: [ LocalCommandExecutionRepository, @@ -89,177 +50,178 @@ describe('LocalCommandExecutionRepository', () => { service = module.get(LocalCommandExecutionRepository); repository = module.get(getRepositoryToken(CommandExecutionEntity)); - encryptionService = module.get(EncryptionService); + encryptionService = module.get(EncryptionService); + + when(encryptionService.encrypt) + .calledWith(mockCommandExecution.command) + .mockResolvedValue({ + data: mockCommandExecutionEntity.command, + encryption: mockCommandExecutionEntity.encryption, + }) + .calledWith(JSON.stringify(mockCommandExecution.result)) + .mockResolvedValue({ + data: mockCommandExecutionEntity.result, + encryption: mockCommandExecutionEntity.encryption, + }) + .calledWith(JSON.stringify([mockCommendExecutionHugeResultPlaceholder])) + .mockResolvedValue({ + data: mockCommendExecutionHugeResultPlaceholderEncrypted, + encryption: mockCommandExecutionEntity.encryption, + }); + + when(encryptionService.decrypt) + .calledWith(mockCommandExecutionEntity.command, jasmine.anything()) + .mockResolvedValue(mockCommandExecution.command) + .calledWith(mockCommandExecutionEntity.result, jasmine.anything()) + .mockResolvedValue(JSON.stringify(mockCommandExecution.result)); + + repository.save.mockReturnValue(mockCommandExecutionEntity); + repository.findOneBy.mockReturnValue(mockCommandExecutionEntity); }); describe('create', () => { - it('should process new entity', async () => { - repository.save.mockReturnValueOnce([mockCommandExecutionEntity]); - encryptionService.encrypt.mockReturnValue(mockEncryptResult); + let cleanupSpy: jest.SpyInstance; - expect(await service.createMany(mockSessionMetadata, [mockCommandExecutionPartial])).toEqual([{ - ...mockCommandExecutionPartial, - id: mockCommandExecutionEntity.id, - createdAt: mockCommandExecutionEntity.createdAt, - }]); + beforeEach(() => { + cleanupSpy = jest.spyOn(service as any, 'cleanupDatabaseHistory'); }); - it('should return full result even if size limit exceeded', async () => { - repository.save.mockReturnValueOnce([mockCommandExecutionEntity]); - encryptionService.encrypt.mockReturnValue(mockEncryptResult); - - const executionResult = [{ - status: CommandExecutionStatus.Success, - response: `${Buffer.alloc(WORKBENCH_CONFIG.maxResultSize, 'a').toString()}`, - }]; + it('should process new entity', async () => { expect(await service.createMany(mockSessionMetadata, [{ - ...mockCommandExecutionPartial, - result: executionResult, - }])).toEqual([{ - ...mockCommandExecutionPartial, - id: mockCommandExecutionEntity.id, - createdAt: mockCommandExecutionEntity.createdAt, - result: executionResult, - }]); - - expect(encryptionService.encrypt).toHaveBeenLastCalledWith(JSON.stringify([{ - status: CommandExecutionStatus.Success, - response: 'Results have been deleted since they exceed 1 MB. Re-run the command to see new results.', - }])); + ...mockCommandExecution, + id: undefined, + createdAt: undefined, + }])).toEqual([mockCommandExecution]); + expect(repository.save).toHaveBeenCalledWith({ + ...mockCommandExecutionEntity, + id: undefined, + createdAt: undefined, + }); + expect(cleanupSpy).toBeCalledTimes(1); + expect(cleanupSpy).toHaveBeenCalledWith(mockCommandExecution.databaseId, { type: mockCommandExecution.type }); }); - it('should return with flag isNotStored="true" even if size limit exceeded', async () => { - repository.save.mockReturnValueOnce([{ ...mockCommandExecutionEntity, isNotStored: true }]); - encryptionService.encrypt.mockReturnValue(mockEncryptResult); - + it('should return full result even if size limit exceeded', async () => { const executionResult = [{ status: CommandExecutionStatus.Success, response: `${Buffer.alloc(WORKBENCH_CONFIG.maxResultSize, 'a').toString()}`, }]; expect(await service.createMany(mockSessionMetadata, [{ - ...mockCommandExecutionPartial, + ...mockCommandExecution, result: executionResult, }])).toEqual([{ - ...mockCommandExecutionPartial, - id: mockCommandExecutionEntity.id, - createdAt: mockCommandExecutionEntity.createdAt, + ...mockCommandExecution, result: executionResult, - isNotStored: true, + isNotStored: true, // double check that for such cases special flag returned }]); - - expect(encryptionService.encrypt).toHaveBeenLastCalledWith(JSON.stringify([ - new CommandExecutionResult({ - status: CommandExecutionStatus.Success, - response: 'Results have been deleted since they exceed 1 MB. Re-run the command to see new results.', - }), - ])); + expect(repository.save).toHaveBeenCalledWith({ + ...mockCommandExecutionEntity, + command: mockCommandExecutionEntity.command, + result: mockCommendExecutionHugeResultPlaceholderEncrypted, + }); }); }); describe('getList', () => { it('should return list (2) of command execution', async () => { - const entityResponse = new CommandExecutionEntity({ ...omit(mockCommandExecutionEntity, 'result') }); - mockQueryBuilderGetMany.mockReturnValueOnce([entityResponse, entityResponse]); - encryptionService.decrypt.mockReturnValueOnce(mockCreateCommandExecutionDto.command); - encryptionService.decrypt.mockReturnValueOnce(mockCreateCommandExecutionDto.command); - - const commandExecution = new CommandExecution({ - ...omit(mockCommandExecutionPartial, ['result']), - id: mockCommandExecutionEntity.id, - createdAt: mockCommandExecutionEntity.createdAt, - }); - - expect(await service.getList(mockSessionMetadata, mockCommandExecutionEntity.databaseId)).toEqual([ - commandExecution, - commandExecution, + repository.createQueryBuilder() + .getMany.mockReturnValueOnce([mockShortCommandExecutionEntity, mockShortCommandExecutionEntity]); + + expect(await service.getList( + mockSessionMetadata, + mockCommandExecutionEntity.databaseId, + mockCommandExecutionFilter, + )).toEqual([ + mockShortCommandExecution, + mockShortCommandExecution, ]); + expect(repository.createQueryBuilder().where).toHaveBeenCalledWith({ + databaseId: mockCommandExecution.databaseId, + type: mockCommandExecutionFilter.type, + }); }); it('should return list (1) of command execution without failed decrypted item', async () => { - const entityResponse = new CommandExecutionEntity({ ...omit(mockCommandExecutionEntity, 'result') }); - mockQueryBuilderGetMany.mockReturnValueOnce([entityResponse, entityResponse]); - encryptionService.decrypt.mockReturnValueOnce(mockCreateCommandExecutionDto.command); + repository.createQueryBuilder().getMany.mockResolvedValueOnce([ + mockShortCommandExecutionEntity, + { + ...mockShortCommandExecutionEntity, + command: 'something that can not be decrypted', + }, + ]); + encryptionService.decrypt.mockResolvedValueOnce(mockShortCommandExecution.command); encryptionService.decrypt.mockRejectedValueOnce(new KeytarDecryptionErrorException()); - const commandExecution = new CommandExecution({ - ...omit(mockCommandExecutionPartial, ['result']), - id: mockCommandExecutionEntity.id, - createdAt: mockCommandExecutionEntity.createdAt, - }); - - expect(await service.getList(mockSessionMetadata, mockCommandExecutionEntity.databaseId)).toEqual([ - commandExecution, + expect(await service.getList( + mockSessionMetadata, + mockCommandExecution.databaseId, + mockCommandExecutionFilter, + )).toEqual([ + mockShortCommandExecution, ]); }); }); describe('getOne', () => { it('should return decrypted and transformed command execution', async () => { - repository.findOneBy.mockResolvedValueOnce(mockCommandExecutionEntity); - encryptionService.decrypt.mockReturnValueOnce(mockCreateCommandExecutionDto.command); - encryptionService.decrypt.mockReturnValueOnce(JSON.stringify([mockCommandExecutionResult])); - - const commandExecution = new CommandExecution({ - ...mockCommandExecutionPartial, - id: mockCommandExecutionEntity.id, - createdAt: mockCommandExecutionEntity.createdAt, + expect(await service.getOne(mockSessionMetadata, mockCommandExecution.databaseId, mockCommandExecution.id)) + .toEqual(mockCommandExecution); + expect(repository.findOneBy).toHaveBeenCalledWith({ + id: mockCommandExecution.id, + databaseId: mockCommandExecution.databaseId, }); - - expect(await service.getOne(mockSessionMetadata, mockDatabase.id, mockCommandExecutionEntity.id)).toEqual( - commandExecution, - ); }); it('should return null fields in case of decryption errors', async () => { - repository.findOneBy.mockResolvedValueOnce(mockCommandExecutionEntity); - encryptionService.decrypt.mockReturnValueOnce(mockCreateCommandExecutionDto.command); + encryptionService.decrypt.mockReturnValueOnce(mockCommandExecution.command); encryptionService.decrypt.mockRejectedValueOnce(new KeytarDecryptionErrorException()); - const commandExecution = new CommandExecution({ - ...mockCommandExecutionPartial, - id: mockCommandExecutionEntity.id, - createdAt: mockCommandExecutionEntity.createdAt, - result: null, - }); - - expect(await service.getOne(mockSessionMetadata, mockDatabase.id, mockCommandExecutionEntity.id)).toEqual( - commandExecution, - ); + expect(await service.getOne(mockSessionMetadata, mockCommandExecution.databaseId, mockCommandExecution.id)) + .toEqual({ + ...mockCommandExecution, + result: null, + }); }); it('should return not found exception', async () => { repository.findOneBy.mockResolvedValueOnce(null); - try { - await service.getOne(mockSessionMetadata, mockDatabase.id, mockCommandExecutionEntity.id); - fail(); - } catch (e) { - expect(e).toBeInstanceOf(NotFoundException); - expect(e.message).toEqual(ERROR_MESSAGES.COMMAND_EXECUTION_NOT_FOUND); - } + await expect(service.getOne(mockSessionMetadata, mockCommandExecution.databaseId, mockCommandExecution.id)) + .rejects.toEqual(new NotFoundException(ERROR_MESSAGES.COMMAND_EXECUTION_NOT_FOUND)); }); }); describe('delete', () => { it('Should not return anything on delete', async () => { repository.delete.mockResolvedValueOnce(1); - expect(await service.delete(mockSessionMetadata, mockDatabase.id, mockCommandExecutionEntity.id)).toEqual( - undefined, - ); + expect(await service.delete(mockSessionMetadata, mockCommandExecution.databaseId, mockCommandExecution.id)) + .toEqual(undefined); + expect(repository.delete).toHaveBeenCalledWith({ + id: mockCommandExecution.id, + databaseId: mockCommandExecution.databaseId, + }); }); }); describe('deleteAll', () => { it('Should not return anything on delete', async () => { repository.delete.mockResolvedValueOnce(1); - expect(await service.deleteAll(mockSessionMetadata, mockDatabase.id)).toEqual( - undefined, - ); + expect(await service.deleteAll(mockSessionMetadata, mockCommandExecution.databaseId, mockCommandExecutionFilter)) + .toEqual(undefined); + expect(repository.delete).toHaveBeenCalledWith({ + databaseId: mockCommandExecution.databaseId, + type: mockCommandExecutionFilter.type, + }); }); }); describe('cleanupDatabaseHistory', () => { it('Should should not return anything on cleanup', async () => { - mockQueryBuilderGetManyRaw.mockReturnValueOnce([ + repository.createQueryBuilder().getRawMany.mockReturnValueOnce([ { id: mockCommandExecutionEntity.id }, { id: mockCommandExecutionEntity.id }, ]); - expect(await service['cleanupDatabaseHistory'](mockDatabase.id)).toEqual( - undefined, - ); + expect(await service['cleanupDatabaseHistory'](mockCommandExecution.databaseId, mockCommandExecutionFilter)) + .toEqual(undefined); + expect(repository.createQueryBuilder().where).toHaveBeenCalledWith({ + databaseId: mockCommandExecution.databaseId, + type: mockCommandExecutionFilter.type, + }); + expect(repository.createQueryBuilder().whereInIds) + .toHaveBeenCalledWith([mockCommandExecutionEntity.id, mockCommandExecutionEntity.id]); }); }); }); diff --git a/redisinsight/api/src/modules/workbench/repositories/local-command-execution.repository.ts b/redisinsight/api/src/modules/workbench/repositories/local-command-execution.repository.ts index 499ab942bd..5207b41cd3 100644 --- a/redisinsight/api/src/modules/workbench/repositories/local-command-execution.repository.ts +++ b/redisinsight/api/src/modules/workbench/repositories/local-command-execution.repository.ts @@ -43,7 +43,7 @@ export class LocalCommandExecutionRepository extends CommandExecutionRepository // todo: limit by 30 max to insert const response = await Promise.all(commandExecutions.map(async (commandExecution, idx) => { const entity = plainToClass(CommandExecutionEntity, commandExecution); - let isNotStored = false; + let isNotStored: undefined | boolean; // Do not store command execution result that exceeded limitation if (JSON.stringify(entity.result).length > WORKBENCH_CONFIG.maxResultSize) { diff --git a/redisinsight/api/src/modules/workbench/workbench.service.spec.ts b/redisinsight/api/src/modules/workbench/workbench.service.spec.ts index b4a056b520..aabd4d6611 100644 --- a/redisinsight/api/src/modules/workbench/workbench.service.spec.ts +++ b/redisinsight/api/src/modules/workbench/workbench.service.spec.ts @@ -1,22 +1,18 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { v4 as uuidv4 } from 'uuid'; import { when } from 'jest-when'; import { - mockDatabase, + mockCommandExecution, mockCommandExecutionFilter, mockCommandExecutionRepository, + mockCommandExecutionSuccessResult, + mockCreateCommandExecutionDto, mockDatabaseClientFactory, - mockStandaloneRedisClient, + mockStandaloneRedisClient, MockType, mockWorkbenchAnalyticsService, - mockWorkbenchClientMetadata, + mockWorkbenchClientMetadata, mockWorkbenchCommandsExecutor, } from 'src/__mocks__'; import { WorkbenchService } from 'src/modules/workbench/workbench.service'; import { WorkbenchCommandsExecutor } from 'src/modules/workbench/providers/workbench-commands.executor'; import { CommandExecutionRepository } from 'src/modules/workbench/repositories/command-execution.repository'; -import { - CreateCommandExecutionDto, - RunQueryMode, - ResultsMode, -} from 'src/modules/workbench/dto/create-command-execution.dto'; -import { CommandExecution } from 'src/modules/workbench/models/command-execution'; +import { ResultsMode, RunQueryMode } from 'src/modules/workbench/models/command-execution'; import { CommandExecutionResult } from 'src/modules/workbench/models/command-execution-result'; import { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto'; import { BadRequestException, InternalServerErrorException } from '@nestjs/common'; @@ -25,12 +21,6 @@ import { CreateCommandExecutionsDto } from 'src/modules/workbench/dto/create-com import { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory'; import { WorkbenchAnalyticsService } from './services/workbench-analytics/workbench-analytics.service'; -const mockCreateCommandExecutionDto: CreateCommandExecutionDto = { - command: 'set foo bar', - mode: RunQueryMode.ASCII, - resultsMode: ResultsMode.Default, -}; - const mockCommands = ['set 1 1', 'get 1']; const mockCreateCommandExecutionDtoWithGroupMode: CreateCommandExecutionsDto = { @@ -54,23 +44,8 @@ const mockCreateCommandExecutionsDto: CreateCommandExecutionsDto = { }; const mockCommandExecutionResults: CommandExecutionResult[] = [ - new CommandExecutionResult({ - status: CommandExecutionStatus.Success, - response: 'OK', - }), + mockCommandExecutionSuccessResult, ]; -const mockCommandExecutionToRun: CommandExecution = new CommandExecution({ - ...mockCreateCommandExecutionDto, - databaseId: mockDatabase.id, - db: 0, -}); - -const mockCommandExecution: CommandExecution = new CommandExecution({ - ...mockCommandExecutionToRun, - id: uuidv4(), - createdAt: new Date(), - result: mockCommandExecutionResults, -}); const mockSendCommandResultSuccess = { response: '1', status: 'success' }; const mockSendCommandResultFail = { response: 'error', status: 'fail' }; @@ -106,19 +81,10 @@ const mockCommandExecutionWithSilentMode = { }], }; -const mockCommandExecutionRepository = () => ({ - createMany: jest.fn(), - getList: jest.fn(), - getOne: jest.fn(), - delete: jest.fn(), - deleteAll: jest.fn(), -}); - describe('WorkbenchService', () => { - const client = mockStandaloneRedisClient; let service: WorkbenchService; - let workbenchCommandsExecutor; - let commandExecutionProvider; + let workbenchCommandsExecutor: MockType; + let commandExecutionRepository: MockType; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -130,9 +96,7 @@ describe('WorkbenchService', () => { }, { provide: WorkbenchCommandsExecutor, - useFactory: () => ({ - sendCommand: jest.fn(), - }), + useFactory: mockWorkbenchCommandsExecutor, }, { provide: CommandExecutionRepository, @@ -145,30 +109,38 @@ describe('WorkbenchService', () => { ], }).compile(); - service = module.get(WorkbenchService); - workbenchCommandsExecutor = module.get(WorkbenchCommandsExecutor); - commandExecutionProvider = module.get(CommandExecutionRepository); + service = module.get(WorkbenchService); + workbenchCommandsExecutor = module.get(WorkbenchCommandsExecutor); + commandExecutionRepository = module.get(CommandExecutionRepository); }); describe('createCommandExecution', () => { it('should successfully execute command and save it', async () => { - const result = await service.createCommandExecution(client, mockCreateCommandExecutionDto); - // can't predict execution time - expect(result).toMatchObject(mockCommandExecutionToRun); - expect(result.executionTime).toBeGreaterThan(0); + const result = await service.createCommandExecution(mockStandaloneRedisClient, mockCreateCommandExecutionDto); + expect(result).toEqual({ + ...mockCommandExecution, + executionTime: result.executionTime, + id: undefined, // result was not saved yet + createdAt: undefined, // result was not saved yet + }); }); it('should save db index', async () => { const db = 2; - client.getCurrentDbIndex = jest.fn().mockResolvedValueOnce(db); + mockStandaloneRedisClient.getCurrentDbIndex.mockResolvedValueOnce(db); const result = await service.createCommandExecution( - client, + mockStandaloneRedisClient, mockCreateCommandExecutionDto, ); - expect(result).toMatchObject({ ...mockCommandExecutionToRun, db }); - expect(result.db).toBe(db); + expect(result).toEqual({ + ...mockCommandExecution, + executionTime: result.executionTime, + id: undefined, // result was not saved yet + createdAt: undefined, // result was not saved yet + db, + }); }); it('should save result as unsupported command message', async () => { - client.getCurrentDbIndex = jest.fn().mockResolvedValueOnce(0); + mockStandaloneRedisClient.getCurrentDbIndex = jest.fn().mockResolvedValueOnce(0); workbenchCommandsExecutor.sendCommand.mockResolvedValueOnce(mockCommandExecutionResults); const dto = { @@ -177,7 +149,7 @@ describe('WorkbenchService', () => { mode: RunQueryMode.ASCII, }; - expect(await service.createCommandExecution(client, dto)).toEqual({ + expect(await service.createCommandExecution(mockStandaloneRedisClient, dto)).toEqual({ ...dto, db: 0, databaseId: mockWorkbenchClientMetadata.databaseId, @@ -199,7 +171,7 @@ describe('WorkbenchService', () => { }; try { - await service.createCommandExecution(client, dto); + await service.createCommandExecution(mockStandaloneRedisClient, dto); fail(); } catch (e) { expect(e).toBeInstanceOf(BadRequestException); @@ -211,19 +183,18 @@ describe('WorkbenchService', () => { workbenchCommandsExecutor.sendCommand.mockResolvedValueOnce( [mockCommandExecutionResults, mockCommandExecutionResults], ); - commandExecutionProvider.createMany.mockResolvedValueOnce([mockCommandExecution, mockCommandExecution]); + commandExecutionRepository.createMany.mockResolvedValueOnce([mockCommandExecution, mockCommandExecution]); const result = await service.createCommandExecutions(mockWorkbenchClientMetadata, mockCreateCommandExecutionsDto); expect(result).toEqual([mockCommandExecution, mockCommandExecution]); }); - it('should successfully execute commands and save in group mode view', async () => { when(workbenchCommandsExecutor.sendCommand) - .calledWith(client, expect.anything()) + .calledWith(mockStandaloneRedisClient, expect.anything()) .mockResolvedValue([mockSendCommandResultSuccess]); - commandExecutionProvider.createMany.mockResolvedValueOnce([mockCommandExecutionWithGroupMode]); + commandExecutionRepository.createMany.mockResolvedValueOnce([mockCommandExecutionWithGroupMode]); const result = await service.createCommandExecutions( mockWorkbenchClientMetadata, @@ -232,13 +203,12 @@ describe('WorkbenchService', () => { expect(result).toEqual([mockCommandExecutionWithGroupMode]); }); - it('should successfully execute commands and save in silent mode view', async () => { when(workbenchCommandsExecutor.sendCommand) - .calledWith(client, expect.anything()) + .calledWith(mockStandaloneRedisClient, expect.anything()) .mockResolvedValue([mockSendCommandResultSuccess]); - commandExecutionProvider.createMany.mockResolvedValueOnce([mockCommandExecutionWithSilentMode]); + commandExecutionRepository.createMany.mockResolvedValueOnce([mockCommandExecutionWithSilentMode]); const result = await service.createCommandExecutions( mockWorkbenchClientMetadata, @@ -250,20 +220,20 @@ describe('WorkbenchService', () => { it('should successfully execute commands with error and save summary', async () => { when(workbenchCommandsExecutor.sendCommand) - .calledWith(client, { + .calledWith(mockStandaloneRedisClient, { ...mockCreateCommandExecutionDtoWithGroupMode, command: mockCommands[0], }) .mockResolvedValue([mockSendCommandResultSuccess]); when(workbenchCommandsExecutor.sendCommand) - .calledWith(client, { + .calledWith(mockStandaloneRedisClient, { ...mockCreateCommandExecutionDtoWithGroupMode, command: mockCommands[1], }) .mockResolvedValue([mockSendCommandResultFail]); - commandExecutionProvider.createMany.mockResolvedValueOnce([mockCommandExecutionWithGroupMode]); + commandExecutionRepository.createMany.mockResolvedValueOnce([mockCommandExecutionWithGroupMode]); const result = await service.createCommandExecutions( mockWorkbenchClientMetadata, @@ -275,20 +245,20 @@ describe('WorkbenchService', () => { it('should successfully execute commands with error and save summary in silent mode view', async () => { when(workbenchCommandsExecutor.sendCommand) - .calledWith(client, { + .calledWith(mockStandaloneRedisClient, { ...mockCreateCommandExecutionDtoWithSilentMode, command: mockCommands[0], }) .mockResolvedValue([mockSendCommandResultSuccess]); when(workbenchCommandsExecutor.sendCommand) - .calledWith(client, { + .calledWith(mockStandaloneRedisClient, { ...mockCreateCommandExecutionDtoWithSilentMode, command: mockCommands[1], }) .mockResolvedValue([mockSendCommandResultFail]); - commandExecutionProvider.createMany.mockResolvedValueOnce([mockCommandExecutionWithSilentMode]); + commandExecutionRepository.createMany.mockResolvedValueOnce([mockCommandExecutionWithSilentMode]); const result = await service.createCommandExecutions( mockWorkbenchClientMetadata, @@ -310,7 +280,7 @@ describe('WorkbenchService', () => { }); it('should throw an error from command execution provider (create)', async () => { workbenchCommandsExecutor.sendCommand.mockResolvedValueOnce([mockCommandExecutionResults]); - commandExecutionProvider.createMany.mockRejectedValueOnce(new InternalServerErrorException('db error')); + commandExecutionRepository.createMany.mockRejectedValueOnce(new InternalServerErrorException('db error')); try { await service.createCommandExecutions(mockWorkbenchClientMetadata, mockCreateCommandExecutionsDto); @@ -322,17 +292,17 @@ describe('WorkbenchService', () => { }); describe('listCommandExecutions', () => { it('should return list of command executions', async () => { - commandExecutionProvider.getList.mockResolvedValueOnce([mockCommandExecution, mockCommandExecution]); + commandExecutionRepository.getList.mockResolvedValueOnce([mockCommandExecution, mockCommandExecution]); - const result = await service.listCommandExecutions(mockWorkbenchClientMetadata); + const result = await service.listCommandExecutions(mockWorkbenchClientMetadata, mockCommandExecutionFilter); expect(result).toEqual([mockCommandExecution, mockCommandExecution]); }); it('should throw an error from command execution provider (getList)', async () => { - commandExecutionProvider.getList.mockRejectedValueOnce(new InternalServerErrorException()); + commandExecutionRepository.getList.mockRejectedValueOnce(new InternalServerErrorException()); try { - await service.listCommandExecutions(mockWorkbenchClientMetadata); + await service.listCommandExecutions(mockWorkbenchClientMetadata, mockCommandExecutionFilter); fail(); } catch (e) { expect(e).toBeInstanceOf(InternalServerErrorException); @@ -341,14 +311,14 @@ describe('WorkbenchService', () => { }); describe('getCommandExecution', () => { it('should return full command executions', async () => { - commandExecutionProvider.getOne.mockResolvedValueOnce(mockCommandExecution); + commandExecutionRepository.getOne.mockResolvedValueOnce(mockCommandExecution); const result = await service.getCommandExecution(mockWorkbenchClientMetadata, mockCommandExecution.id); expect(result).toEqual(mockCommandExecution); }); it('should throw an error from command execution provider (getOne)', async () => { - commandExecutionProvider.getOne.mockRejectedValueOnce(new InternalServerErrorException()); + commandExecutionRepository.getOne.mockRejectedValueOnce(new InternalServerErrorException()); try { await service.getCommandExecution(mockWorkbenchClientMetadata, mockCommandExecution.id); @@ -360,7 +330,7 @@ describe('WorkbenchService', () => { }); describe('deleteCommandExecution', () => { it('should not return anything on delete', async () => { - commandExecutionProvider.delete.mockResolvedValueOnce('some response'); + commandExecutionRepository.delete.mockResolvedValueOnce('some response'); const result = await service.deleteCommandExecution( mockWorkbenchClientMetadata, @@ -372,10 +342,11 @@ describe('WorkbenchService', () => { }); describe('deleteCommandExecutions', () => { it('should not return anything on delete', async () => { - commandExecutionProvider.deleteAll.mockResolvedValueOnce('some response'); + commandExecutionRepository.deleteAll.mockResolvedValueOnce('some response'); const result = await service.deleteCommandExecutions( mockWorkbenchClientMetadata, + mockCommandExecutionFilter, ); expect(result).toEqual(undefined); diff --git a/redisinsight/api/test/api/workbench/DELETE-databases-id-workbench-command_executions-id.test.ts b/redisinsight/api/test/api/workbench/DELETE-databases-id-workbench-command_executions-id.test.ts index 1aac29ec4f..193fa3aa5b 100644 --- a/redisinsight/api/test/api/workbench/DELETE-databases-id-workbench-command_executions-id.test.ts +++ b/redisinsight/api/test/api/workbench/DELETE-databases-id-workbench-command_executions-id.test.ts @@ -1,10 +1,9 @@ import { expect, describe, - it, deps, - validateApiCall, -} from '../deps'; + getMainCheckFn, +} from '../deps' const { server, request, constants, rte, localDb } = deps; // endpoint to test @@ -14,24 +13,7 @@ const endpoint = ( ) => request(server).delete(`/${constants.API.DATABASES}/${instanceId}/workbench/command-executions/${id}`); -const mainCheckFn = async (testCase) => { - it(testCase.name, async () => { - // additional checks before test run - if (testCase.before) { - await testCase.before(); - } - - await validateApiCall({ - endpoint, - ...testCase, - }); - - // additional checks after test pass - if (testCase.after) { - await testCase.after(); - } - }); -}; +const mainCheckFn = getMainCheckFn(endpoint); describe('DELETE /databases/:instanceId/workbench/command-executions/:commandExecutionId', () => { describe('Common', () => { diff --git a/redisinsight/api/test/api/workbench/DELETE-databases-id-workbench-command_executions.test.ts b/redisinsight/api/test/api/workbench/DELETE-databases-id-workbench-command_executions.test.ts index 671a25b733..81a9fb05fa 100644 --- a/redisinsight/api/test/api/workbench/DELETE-databases-id-workbench-command_executions.test.ts +++ b/redisinsight/api/test/api/workbench/DELETE-databases-id-workbench-command_executions.test.ts @@ -1,11 +1,11 @@ import { expect, describe, - it, deps, - validateApiCall, -} from '../deps'; -const { server, request, constants, rte, localDb } = deps; + getMainCheckFn, + Joi, generateInvalidDataTestCases, validateInvalidDataTestCase, +} from '../deps' +const { server, request, constants, localDb } = deps; // endpoint to test const endpoint = ( @@ -13,26 +13,26 @@ const endpoint = ( ) => request(server).delete(`/${constants.API.DATABASES}/${instanceId}/workbench/command-executions`); -const mainCheckFn = async (testCase) => { - it(testCase.name, async () => { - // additional checks before test run - if (testCase.before) { - await testCase.before(); - } +// input data schema +const dataSchema = Joi.object({ + type: Joi.string().valid('WORKBENCH', 'SEARCH').allow(null), +}).messages({ + 'any.required': '{#label} should not be empty', +}).strict(); - await validateApiCall({ - endpoint, - ...testCase, - }); - - // additional checks after test pass - if (testCase.after) { - await testCase.after(); - } - }); +const validInputData = { + type: 'WORKBENCH', }; +const mainCheckFn = getMainCheckFn(endpoint); + describe('DELETE /databases/:instanceId/workbench/command-executions', () => { + describe('Validation', () => { + generateInvalidDataTestCases(dataSchema, validInputData).map( + validateInvalidDataTestCase(endpoint, dataSchema), + ); + }); + describe('Common', () => { [ { @@ -61,4 +61,55 @@ describe('DELETE /databases/:instanceId/workbench/command-executions', () => { }, ].map(mainCheckFn); }); + describe('Filter', () => { + beforeEach(async () => { + await localDb.generateNCommandExecutions({ + databaseId: constants.TEST_INSTANCE_ID, + type: 'WORKBENCH', + }, 20, true); + await localDb.generateNCommandExecutions({ + databaseId: constants.TEST_INSTANCE_ID, + type: 'SEARCH', + }, 10, false); + }); + + [ + { + name: 'Should return 404 not found when incorrect instance', + endpoint: () => endpoint( + constants.TEST_NOT_EXISTED_INSTANCE_ID, + ), + statusCode: 404, + responseBody: { + statusCode: 404, + message: 'Invalid database instance id.', + error: 'Not Found' + }, + }, + { + name: 'Should return remove only WORKBENCH items (by default)', + after: async () => { + expect(await (await localDb.getRepository(localDb.repositories.COMMAND_EXECUTION)).count({})).to.eq(10) + }, + }, + { + name: 'Should return remove only WORKBENCH items', + data: { + type: 'WORKBENCH', + }, + after: async () => { + expect(await (await localDb.getRepository(localDb.repositories.COMMAND_EXECUTION)).count({})).to.eq(10) + }, + }, + { + name: 'Should return remove only SEARCH items', + data: { + type: 'SEARCH', + }, + after: async () => { + expect(await (await localDb.getRepository(localDb.repositories.COMMAND_EXECUTION)).count({})).to.eq(20) + }, + }, + ].map(mainCheckFn); + }); }); diff --git a/redisinsight/api/test/api/workbench/GET-databases-id-workbench-command_executions-id.test.ts b/redisinsight/api/test/api/workbench/GET-databases-id-workbench-command_executions-id.test.ts index c50e1324b0..62878b5b99 100644 --- a/redisinsight/api/test/api/workbench/GET-databases-id-workbench-command_executions-id.test.ts +++ b/redisinsight/api/test/api/workbench/GET-databases-id-workbench-command_executions-id.test.ts @@ -6,7 +6,7 @@ import { deps, validateApiCall, } from '../deps'; -const { server, request, constants, rte, localDb } = deps; +const { server, request, constants, localDb } = deps; // endpoint to test const endpoint = ( @@ -29,6 +29,7 @@ const responseSchema = Joi.object().keys({ executionTime: Joi.number().required(), db: Joi.number().integer().allow(null), createdAt: Joi.date().required(), + type: Joi.string().valid('WORKBENCH', 'SEARCH').required(), }).required(); const mainCheckFn = async (testCase) => { diff --git a/redisinsight/api/test/api/workbench/GET-databases-id-workbench-command_executions.test.ts b/redisinsight/api/test/api/workbench/GET-databases-id-workbench-command_executions.test.ts index f705326087..32fc90b682 100644 --- a/redisinsight/api/test/api/workbench/GET-databases-id-workbench-command_executions.test.ts +++ b/redisinsight/api/test/api/workbench/GET-databases-id-workbench-command_executions.test.ts @@ -1,11 +1,11 @@ import { expect, describe, - it, + before, Joi, deps, - validateApiCall, -} from '../deps'; + getMainCheckFn, +} from '../deps' const { server, request, constants, localDb } = deps; // endpoint to test @@ -28,26 +28,10 @@ const responseSchema = Joi.array().items(Joi.object().keys({ }).allow(null), db: Joi.number().integer().allow(null), createdAt: Joi.date().required(), + type: Joi.string().valid('WORKBENCH', 'SEARCH').required(), })).required().max(30); -const mainCheckFn = async (testCase) => { - it(testCase.name, async () => { - // additional checks before test run - if (testCase.before) { - await testCase.before(); - } - - await validateApiCall({ - endpoint, - ...testCase, - }); - - // additional checks after test pass - if (testCase.after) { - await testCase.after(); - } - }); -}; +const mainCheckFn = getMainCheckFn(endpoint); describe('GET /databases/:instanceId/workbench/command-executions', () => { describe('Common', () => { @@ -110,4 +94,58 @@ describe('GET /databases/:instanceId/workbench/command-executions', () => { }, ].map(mainCheckFn); }); + describe('Filter', () => { + before(async () => { + await localDb.generateNCommandExecutions({ + databaseId: constants.TEST_INSTANCE_ID, + type: 'WORKBENCH', + }, 20, true); + await localDb.generateNCommandExecutions({ + databaseId: constants.TEST_INSTANCE_ID, + type: 'SEARCH', + }, 10, false); + }); + + [ + { + name: 'Should get only 20 items (workbench by default)', + responseSchema, + checkFn: async ({ body }) => { + expect(body.length).to.eql(20); + for (let i = 0; i < 20; i ++) { + expect(body[i].command).to.eql('set foo bar'); + expect(body[i].type).to.eql('WORKBENCH'); + } + }, + }, + { + name: 'Should get only 20 items filtered by type (WORKBENCH)', + query: { + type: 'WORKBENCH', + }, + responseSchema, + checkFn: async ({ body }) => { + expect(body.length).to.eql(20); + for (let i = 0; i < 20; i ++) { + expect(body[i].command).to.eql('set foo bar'); + expect(body[i].type).to.eql('WORKBENCH'); + } + }, + }, + { + name: 'Should get only 10 items filtered by type (SEARCH)', + responseSchema, + query: { + type: 'SEARCH', + }, + checkFn: async ({ body }) => { + expect(body.length).to.eql(10); + for (let i = 0; i < 10; i ++) { + expect(body[i].command).to.eql('set foo bar'); + expect(body[i].type).to.eql('SEARCH'); + } + }, + }, + ].map(mainCheckFn); + }); }); diff --git a/redisinsight/api/test/api/workbench/POST-databases-id-workbench-command_executions.test.ts b/redisinsight/api/test/api/workbench/POST-databases-id-workbench-command_executions.test.ts index a98a551fc5..5fd42e4bcc 100644 --- a/redisinsight/api/test/api/workbench/POST-databases-id-workbench-command_executions.test.ts +++ b/redisinsight/api/test/api/workbench/POST-databases-id-workbench-command_executions.test.ts @@ -9,8 +9,8 @@ import { generateInvalidDataTestCases, validateInvalidDataTestCase, validateApiCall, - requirements, -} from '../deps'; + requirements, getMainCheckFn, +} from '../deps' import { convertArrayReplyToObject } from 'src/modules/redis/utils'; const { server, request, constants, rte, localDb } = deps; @@ -25,6 +25,7 @@ const dataSchema = Joi.object({ }), mode: Joi.string().valid('RAW', 'ASCII').allow(null), resultsMode: Joi.string().valid('DEFAULT', 'GROUP_MODE').allow(null), + type: Joi.string().valid('WORKBENCH', 'SEARCH').allow(null), }).messages({ 'any.required': '{#label} should not be empty', }).strict(); @@ -54,26 +55,10 @@ const responseSchema = Joi.array().items(Joi.object().keys({ success: Joi.number(), fail: Joi.number(), }), + type: Joi.string().valid('WORKBENCH', 'SEARCH').required(), })).required(); -const mainCheckFn = async (testCase) => { - it(testCase.name, async () => { - // additional checks before test run - if (testCase.before) { - await testCase.before(); - } - - await validateApiCall({ - endpoint, - ...testCase, - }); - - // additional checks after test pass - if (testCase.after) { - await testCase.after(); - } - }); -}; +const mainCheckFn = getMainCheckFn(endpoint); describe('POST /databases/:instanceId/workbench/command-executions', () => { before(rte.data.truncate); From 25792fa5710d13dc4d7e681e7e765c279f998b6e Mon Sep 17 00:00:00 2001 From: ArtemHoruzhenko Date: Tue, 10 Sep 2024 18:55:35 +0300 Subject: [PATCH 056/112] fix tests --- .../src/modules/workbench/plugins.service.ts | 6 ++--- ...ases-id-plugins-command_executions.test.ts | 24 ++++--------------- ...es-id-workbench-command_executions.test.ts | 10 ++++---- 3 files changed, 12 insertions(+), 28 deletions(-) diff --git a/redisinsight/api/src/modules/workbench/plugins.service.ts b/redisinsight/api/src/modules/workbench/plugins.service.ts index 8e05865d92..8ca00e88ba 100644 --- a/redisinsight/api/src/modules/workbench/plugins.service.ts +++ b/redisinsight/api/src/modules/workbench/plugins.service.ts @@ -49,13 +49,13 @@ export class PluginsService { }); } catch (error) { if (error instanceof CommandNotSupportedError) { - return new PluginCommandExecution({ + return plainToClass(PluginCommandExecution, { ...dto, databaseId: clientMetadata.databaseId, - result: [plainToClass(CommandExecutionResult, { + result: [{ response: error.message, status: CommandExecutionStatus.Fail, - })], + }], }); } diff --git a/redisinsight/api/test/api/plugins/POST-databases-id-plugins-command_executions.test.ts b/redisinsight/api/test/api/plugins/POST-databases-id-plugins-command_executions.test.ts index 0526aae7b4..2a8e9a6e20 100644 --- a/redisinsight/api/test/api/plugins/POST-databases-id-plugins-command_executions.test.ts +++ b/redisinsight/api/test/api/plugins/POST-databases-id-plugins-command_executions.test.ts @@ -8,8 +8,8 @@ import { generateInvalidDataTestCases, validateInvalidDataTestCase, validateApiCall, - requirements, -} from '../deps'; + requirements, getMainCheckFn, +} from '../deps' const { server, request, constants, rte, localDb } = deps; // endpoint to test @@ -40,26 +40,10 @@ const responseSchema = Joi.object().keys({ })), mode: Joi.string().required(), resultsMode: Joi.string().required(), + type: Joi.string().valid('WORKBENCH', 'SEARCH').required(), }).required(); -const mainCheckFn = async (testCase) => { - it(testCase.name, async () => { - // additional checks before test run - if (testCase.before) { - await testCase.before(); - } - - await validateApiCall({ - endpoint, - ...testCase, - }); - - // additional checks after test pass - if (testCase.after) { - await testCase.after(); - } - }); -}; +const mainCheckFn = getMainCheckFn(endpoint); describe('POST /databases/:instanceId/plugins/command-executions', () => { before(rte.data.truncate); diff --git a/redisinsight/api/test/api/workbench/POST-databases-id-workbench-command_executions.test.ts b/redisinsight/api/test/api/workbench/POST-databases-id-workbench-command_executions.test.ts index 5fd42e4bcc..e337f46ca1 100644 --- a/redisinsight/api/test/api/workbench/POST-databases-id-workbench-command_executions.test.ts +++ b/redisinsight/api/test/api/workbench/POST-databases-id-workbench-command_executions.test.ts @@ -71,7 +71,7 @@ describe('POST /databases/:instanceId/workbench/command-executions', () => { describe('Common', () => { describe('String', () => { - const bigStringValue = Buffer.alloc(1023 * 1024, 'a').toString(); + const bigStringValue = Buffer.alloc(10, 'a').toString(); [ { @@ -103,8 +103,8 @@ describe('POST /databases/:instanceId/workbench/command-executions', () => { }); expect(entity.encryption).to.eql(constants.TEST_ENCRYPTION_STRATEGY); - expect(localDb.encryptData(body[0].command)).to.eql(entity.command); - expect(localDb.encryptData(JSON.stringify(body[0].result))).to.eql(entity.result); + expect(body[0].command).to.eql(localDb.decryptData(entity.command)); + expect(body[0].result).to.eql(JSON.parse(localDb.decryptData(entity.result))); }, before: async () => { expect(await rte.client.set(constants.TEST_STRING_KEY_1, bigStringValue)); @@ -215,8 +215,8 @@ describe('POST /databases/:instanceId/workbench/command-executions', () => { }); expect(entity.encryption).to.eql(constants.TEST_ENCRYPTION_STRATEGY); - expect(localDb.encryptData(body[0].command)).to.eql(entity.command); - expect(localDb.encryptData(JSON.stringify(body[0].result))).to.eql(entity.result); + expect(body[0].command).to.eql(localDb.decryptData(entity.command)); + expect(body[0].result).to.eql(JSON.parse(localDb.decryptData(entity.result))); } }, { From 054a7e70ae051e4d54a878b98922e2878926a64a Mon Sep 17 00:00:00 2001 From: ArtemHoruzhenko Date: Tue, 10 Sep 2024 19:03:57 +0300 Subject: [PATCH 057/112] fix tests --- .../api/src/modules/workbench/plugins.service.spec.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/redisinsight/api/src/modules/workbench/plugins.service.spec.ts b/redisinsight/api/src/modules/workbench/plugins.service.spec.ts index 4f75731fdf..89358ff46f 100644 --- a/redisinsight/api/src/modules/workbench/plugins.service.spec.ts +++ b/redisinsight/api/src/modules/workbench/plugins.service.spec.ts @@ -20,7 +20,7 @@ import { PluginStateRepository } from 'src/modules/workbench/repositories/plugin import { PluginState } from 'src/modules/workbench/models/plugin-state'; import config from 'src/utils/config'; import { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory'; -import { RunQueryMode } from 'src/modules/workbench/models/command-execution'; +import {CommandExecutionType, ResultsMode, RunQueryMode} from 'src/modules/workbench/models/command-execution' const PLUGINS_CONFIG = config.get('plugins'); @@ -101,11 +101,13 @@ describe('PluginsService', () => { const result = await service.sendCommand(mockWorkbenchClientMetadata, dto); - expect(result).toEqual(new PluginCommandExecution({ + expect(result).toEqual({ ...dto, databaseId: mockWorkbenchClientMetadata.databaseId, result: [mockCommandExecutionUnsupportedCommandResult], - })); + resultsMode: ResultsMode.Default, + type: CommandExecutionType.Workbench, + }); expect(workbenchCommandsExecutor.sendCommand).not.toHaveBeenCalled(); }); it('should throw an error when command execution failed', async () => { From efc04c7a4bb67427d10821d4333ed982a0a491df Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Wed, 11 Sep 2024 10:14:20 +0200 Subject: [PATCH 058/112] test for suggestions for fields --- .../search-and-query-tab.e2e.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts index cc334fa495..e2be296bf2 100644 --- a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts +++ b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts @@ -311,3 +311,28 @@ test('Verify REDUCE commands', async t => { await t.pressKey('enter'); await t.typeText(searchAndQueryPage.queryInput, 'total_students'); }); +test('Verify suggestions for fields', async t => { + await t.typeText(searchAndQueryPage.queryInput, 'FT.AGGREGATE ', { replace: true }); + await t.typeText(searchAndQueryPage.queryInput, 'idx1'); + await t.pressKey('enter'); + await t.wait(200); + + await t.typeText(searchAndQueryPage.queryInput, '@'); + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed'); + + //verify suggestions for geo + await t.typeText(searchAndQueryPage.queryInput, 'l'); + await t.pressKey('tab'); + await t.expect((await searchAndQueryPage.MonacoEditor.getTextFromMonaco()).trim()).eql(`FT.AGGREGATE "${indexName1}" "@location:[lon lat radius unit]"`); + + //verify for numeric + await t.typeText(searchAndQueryPage.queryInput, 'FT.AGGREGATE ', { replace: true }); + await t.typeText(searchAndQueryPage.queryInput, 'idx1'); + await t.pressKey('enter'); + await t.wait(200); + + await t.typeText(searchAndQueryPage.queryInput, '@'); + await t.typeText(searchAndQueryPage.queryInput, 's'); + await t.pressKey('tab'); + await t.expect((await searchAndQueryPage.MonacoEditor.getTextFromMonaco()).trim()).eql(`FT.AGGREGATE "${indexName1}" "@students:[range]"`); +}); From 77b0a6297431d630113c2d3a1b9d91811e148837 Mon Sep 17 00:00:00 2001 From: ArtemHoruzhenko Date: Wed, 11 Sep 2024 15:44:55 +0300 Subject: [PATCH 059/112] add migrations --- .../1726058563737-command-execution.ts | 24 +++++++++++++++++++ redisinsight/api/migration/index.ts | 2 ++ 2 files changed, 26 insertions(+) create mode 100644 redisinsight/api/migration/1726058563737-command-execution.ts diff --git a/redisinsight/api/migration/1726058563737-command-execution.ts b/redisinsight/api/migration/1726058563737-command-execution.ts new file mode 100644 index 0000000000..e8df57332b --- /dev/null +++ b/redisinsight/api/migration/1726058563737-command-execution.ts @@ -0,0 +1,24 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class CommandExecution1726058563737 implements MigrationInterface { + name = 'CommandExecution1726058563737' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_5cd90dd6def1fd7c521e53fb2c"`); + await queryRunner.query(`CREATE TABLE "temporary_command_execution" ("id" varchar PRIMARY KEY NOT NULL, "databaseId" varchar NOT NULL, "command" text NOT NULL, "result" text NOT NULL, "role" varchar, "nodeOptions" varchar, "encryption" varchar, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "mode" varchar, "resultsMode" varchar, "summary" varchar, "executionTime" integer, "db" integer, "type" varchar NOT NULL DEFAULT ('WORKBENCH'), CONSTRAINT "FK_ea8adfe9aceceb79212142206b8" FOREIGN KEY ("databaseId") REFERENCES "database_instance" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_command_execution"("id", "databaseId", "command", "result", "role", "nodeOptions", "encryption", "createdAt", "mode", "resultsMode", "summary", "executionTime", "db") SELECT "id", "databaseId", "command", "result", "role", "nodeOptions", "encryption", "createdAt", "mode", "resultsMode", "summary", "executionTime", "db" FROM "command_execution"`); + await queryRunner.query(`DROP TABLE "command_execution"`); + await queryRunner.query(`ALTER TABLE "temporary_command_execution" RENAME TO "command_execution"`); + await queryRunner.query(`CREATE INDEX "IDX_5cd90dd6def1fd7c521e53fb2c" ON "command_execution" ("createdAt") `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_5cd90dd6def1fd7c521e53fb2c"`); + await queryRunner.query(`ALTER TABLE "command_execution" RENAME TO "temporary_command_execution"`); + await queryRunner.query(`CREATE TABLE "command_execution" ("id" varchar PRIMARY KEY NOT NULL, "databaseId" varchar NOT NULL, "command" text NOT NULL, "result" text NOT NULL, "role" varchar, "nodeOptions" varchar, "encryption" varchar, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "mode" varchar, "resultsMode" varchar, "summary" varchar, "executionTime" integer, "db" integer, CONSTRAINT "FK_ea8adfe9aceceb79212142206b8" FOREIGN KEY ("databaseId") REFERENCES "database_instance" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "command_execution"("id", "databaseId", "command", "result", "role", "nodeOptions", "encryption", "createdAt", "mode", "resultsMode", "summary", "executionTime", "db") SELECT "id", "databaseId", "command", "result", "role", "nodeOptions", "encryption", "createdAt", "mode", "resultsMode", "summary", "executionTime", "db" FROM "temporary_command_execution"`); + await queryRunner.query(`DROP TABLE "temporary_command_execution"`); + await queryRunner.query(`CREATE INDEX "IDX_5cd90dd6def1fd7c521e53fb2c" ON "command_execution" ("createdAt") `); + } + +} diff --git a/redisinsight/api/migration/index.ts b/redisinsight/api/migration/index.ts index 621b74750d..60f4bda910 100644 --- a/redisinsight/api/migration/index.ts +++ b/redisinsight/api/migration/index.ts @@ -42,6 +42,7 @@ import { AiHistory1713515657364 } from './1713515657364-ai-history'; import { AiHistorySteps1714501203616 } from './1714501203616-ai-history-steps'; import { Rdi1716370509836 } from './1716370509836-rdi'; import { AiHistory1718260230164 } from './1718260230164-ai-history'; +import { CommandExecution1726058563737 } from './1726058563737-command-execution'; export default [ initialMigration1614164490968, @@ -88,4 +89,5 @@ export default [ AiHistorySteps1714501203616, Rdi1716370509836, AiHistory1718260230164, + CommandExecution1726058563737, ]; From 3928bf4ef5f55ca56c56564859f284187c1841b9 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Wed, 11 Sep 2024 18:57:18 +0200 Subject: [PATCH 060/112] #RI-5992 - update search and query history --- .../components/query/query-card/QueryCard.tsx | 20 +-- .../QueryCardCliPlugin/QueryCardCliPlugin.tsx | 14 +- .../QueryCardHeader/QueryCardHeader.tsx | 69 +++++--- .../components/code-block/CodeBlock.tsx | 10 +- .../enablement-area/EnablementAreaWrapper.tsx | 10 +- .../src/pages/instance/InstancePage.spec.tsx | 2 + .../ui/src/pages/instance/InstancePage.tsx | 5 + .../ui/src/pages/search/SearchPage.tsx | 16 +- .../components/query-wrapper/QueryWrapper.tsx | 10 +- .../results-history/ResultsHistory.spec.tsx | 149 +++++++++++++++++- .../results-history/ResultsHistory.tsx | 99 +++++++++++- .../results-history/styles.module.scss | 11 ++ .../ui/src/pages/workbench/WorkbenchPage.tsx | 8 +- .../components/wb-view/WBViewWrapper.tsx | 22 ++- redisinsight/ui/src/slices/app/context.ts | 4 +- redisinsight/ui/src/slices/app/plugins.ts | 23 ++- .../ui/src/slices/interfaces/workbench.ts | 5 + .../ui/src/slices/workbench/wb-results.ts | 25 ++- redisinsight/ui/src/telemetry/events.ts | 10 ++ 19 files changed, 427 insertions(+), 85 deletions(-) diff --git a/redisinsight/ui/src/components/query/query-card/QueryCard.tsx b/redisinsight/ui/src/components/query/query-card/QueryCard.tsx index d1890eaada..29b9192bbe 100644 --- a/redisinsight/ui/src/components/query/query-card/QueryCard.tsx +++ b/redisinsight/ui/src/components/query/query-card/QueryCard.tsx @@ -5,15 +5,9 @@ import { EuiLoadingContent, keys } from '@elastic/eui' import { useParams } from 'react-router-dom' import { isNull } from 'lodash' -import { WBQueryType, ProfileQueryType, DEFAULT_TEXT_VIEW_TYPE } from 'uiSrc/pages/workbench/constants' -import { RunQueryMode, ResultsMode, ResultsSummary } from 'uiSrc/slices/interfaces/workbench' -import { - getWBQueryType, - getVisualizationsByCommand, - Maybe, - isGroupResults, - isSilentModeWithoutError, -} from 'uiSrc/utils' +import { DEFAULT_TEXT_VIEW_TYPE, ProfileQueryType, WBQueryType } from 'uiSrc/pages/workbench/constants' +import { CommandExecutionType, ResultsMode, ResultsSummary, RunQueryMode } from 'uiSrc/slices/interfaces/workbench' +import { getVisualizationsByCommand, getWBQueryType, isGroupResults, isSilentModeWithoutError, Maybe, } from 'uiSrc/utils' import { appPluginsSelector } from 'uiSrc/slices/app/plugins' import { CommandExecutionResult, IPluginVisualization } from 'uiSrc/slices/interfaces' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' @@ -36,6 +30,7 @@ export interface Props { activeResultsMode?: ResultsMode resultsMode?: ResultsMode emptyCommand?: boolean + executionType?: CommandExecutionType summary?: ResultsSummary createdAt?: Date loading?: boolean @@ -73,6 +68,7 @@ const QueryCard = (props: Props) => { mode, activeResultsMode, resultsMode, + executionType = CommandExecutionType.Workbench, summary, isOpen, createdAt, @@ -117,7 +113,9 @@ const QueryCard = (props: Props) => { const toggleFullScreen = () => { setIsFullScreen((isFull) => { sendEventTelemetry({ - event: TelemetryEvent.WORKBENCH_RESULTS_IN_FULL_SCREEN, + event: executionType === CommandExecutionType.Search + ? TelemetryEvent.SEARCH_RESULTS_IN_FULL_SCREEN + : TelemetryEvent.WORKBENCH_RESULTS_IN_FULL_SCREEN, eventData: { databaseId: instanceId, state: isFull ? 'Close' : 'Open' @@ -184,6 +182,7 @@ const QueryCard = (props: Props) => { mode={mode} resultsMode={resultsMode} activeResultsMode={activeResultsMode} + executionType={executionType} emptyCommand={emptyCommand} summary={summary} summaryText={getSummaryText(summary, resultsMode)} @@ -224,6 +223,7 @@ const QueryCard = (props: Props) => { result={result} query={command} mode={mode} + executionType={executionType} setMessage={setMessage} commandId={id} /> diff --git a/redisinsight/ui/src/components/query/query-card/QueryCardCliPlugin/QueryCardCliPlugin.tsx b/redisinsight/ui/src/components/query/query-card/QueryCardCliPlugin/QueryCardCliPlugin.tsx index 0b07487bca..629b849408 100644 --- a/redisinsight/ui/src/components/query/query-card/QueryCardCliPlugin/QueryCardCliPlugin.tsx +++ b/redisinsight/ui/src/components/query/query-card/QueryCardCliPlugin/QueryCardCliPlugin.tsx @@ -7,7 +7,7 @@ import { pluginApi } from 'uiSrc/services/PluginAPI' import { ThemeContext } from 'uiSrc/contexts/themeContext' import { getBaseApiUrl, Nullable, formatToText, replaceEmptyValue } from 'uiSrc/utils' import { Theme } from 'uiSrc/constants' -import { CommandExecutionResult, IPluginVisualization, RunQueryMode } from 'uiSrc/slices/interfaces' +import { CommandExecutionResult, CommandExecutionType, IPluginVisualization, RunQueryMode } from 'uiSrc/slices/interfaces' import { PluginEvents } from 'uiSrc/plugins/pluginEvents' import { prepareIframeHtml } from 'uiSrc/plugins/pluginImport' import { @@ -28,6 +28,7 @@ export interface Props { setMessage: (text: string) => void commandId: string mode?: RunQueryMode + executionType?: CommandExecutionType } enum StylesNamePostfix { @@ -44,7 +45,15 @@ enum ActionTypes { const baseUrl = getBaseApiUrl() const QueryCardCliPlugin = (props: Props) => { - const { query, id, result, setMessage, commandId, mode = RunQueryMode.Raw } = props + const { + query, + id, + result, + setMessage, + commandId, + mode = RunQueryMode.Raw, + executionType + } = props const { visualizations = [], staticPath } = useSelector(appPluginsSelector) const { modules = [] } = useSelector(connectedInstanceSelector) const serverInfo = useSelector(appServerInfoSelector) @@ -84,6 +93,7 @@ const QueryCardCliPlugin = (props: Props) => { dispatch( sendPluginCommandAction({ command, + executionType, onSuccessAction: (response) => { sendMessageToPlugin({ ...commonOptions, diff --git a/redisinsight/ui/src/components/query/query-card/QueryCardHeader/QueryCardHeader.tsx b/redisinsight/ui/src/components/query/query-card/QueryCardHeader/QueryCardHeader.tsx index f2de4c4f30..fe9d8b4b6d 100644 --- a/redisinsight/ui/src/components/query/query-card/QueryCardHeader/QueryCardHeader.tsx +++ b/redisinsight/ui/src/components/query/query-card/QueryCardHeader/QueryCardHeader.tsx @@ -19,21 +19,27 @@ import { getCommandNameFromQuery, getVisualizationsByCommand, isGroupMode, - truncateText, - urlForAsset, - truncateMilliseconds, + isGroupResults, isRawMode, isSilentMode, isSilentModeWithoutError, - isGroupResults, + truncateMilliseconds, + truncateText, + urlForAsset, } from 'uiSrc/utils' import { numberWithSpaces } from 'uiSrc/utils/numbers' import { ThemeContext } from 'uiSrc/contexts/themeContext' import { appPluginsSelector } from 'uiSrc/slices/app/plugins' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import { getViewTypeOptions, WBQueryType, getProfileViewTypeOptions, ProfileQueryType, isCommandAllowedForProfile } from 'uiSrc/pages/workbench/constants' -import { IPluginVisualization } from 'uiSrc/slices/interfaces' -import { RunQueryMode, ResultsMode, ResultsSummary } from 'uiSrc/slices/interfaces/workbench' +import { + getProfileViewTypeOptions, + getViewTypeOptions, + isCommandAllowedForProfile, + ProfileQueryType, + WBQueryType +} from 'uiSrc/pages/workbench/constants' +import { CommandExecutionType, IPluginVisualization } from 'uiSrc/slices/interfaces' +import { ResultsMode, ResultsSummary, RunQueryMode } from 'uiSrc/slices/interfaces/workbench' import { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands' import { FormatedDate, FullScreen } from 'uiSrc/components' @@ -57,6 +63,7 @@ export interface Props { mode?: RunQueryMode resultsMode?: ResultsMode activeResultsMode?: ResultsMode + executionType?: CommandExecutionType summary?: ResultsSummary summaryText?: string queryType: WBQueryType @@ -102,6 +109,7 @@ const QueryCardHeader = (props: Props) => { createdAt, mode, resultsMode, + executionType, summary, activeResultsMode, summaryText, @@ -121,6 +129,7 @@ const QueryCardHeader = (props: Props) => { const { instanceId = '' } = useParams<{ instanceId: string }>() const { theme } = useContext(ThemeContext) + const isExecuteTypeSearch = executionType === CommandExecutionType.Search const eventStop = (event: React.MouseEvent) => { event.preventDefault() @@ -139,7 +148,12 @@ const QueryCardHeader = (props: Props) => { } const handleCopy = (event: React.MouseEvent, query: string) => { - sendEvent(TelemetryEvent.WORKBENCH_COMMAND_COPIED, query) + sendEvent( + isExecuteTypeSearch + ? TelemetryEvent.SEARCH_COMMAND_COPIED + : TelemetryEvent.WORKBENCH_COMMAND_COPIED, + query + ) eventStop(event) navigator.clipboard?.writeText?.(query) } @@ -154,24 +168,29 @@ const QueryCardHeader = (props: Props) => { const previousView = options.find(({ id }) => id === selectedValue) const type = currentView.value setSelectedValue(type as WBQueryType, initValue) - sendEvent( - TelemetryEvent.WORKBENCH_RESULT_VIEW_CHANGED, - query, - { - rawMode: isRawMode(activeMode), - group: isGroupMode(activeResultsMode), - previousView: previousView?.name, - isPreviousViewInternal: !!previousView?.internal, - currentView: currentView?.name, - isCurrentViewInternal: !!currentView?.internal, - } - ) + sendEvent(isExecuteTypeSearch + ? TelemetryEvent.SEARCH_RESULT_VIEW_CHANGED + : TelemetryEvent.WORKBENCH_RESULT_VIEW_CHANGED, + query, + { + rawMode: isRawMode(activeMode), + group: isGroupMode(activeResultsMode), + previousView: previousView?.name, + isPreviousViewInternal: !!previousView?.internal, + currentView: currentView?.name, + isCurrentViewInternal: !!currentView?.internal, + }) } const handleQueryDelete = (event: React.MouseEvent) => { eventStop(event) onQueryDelete() - sendEvent(TelemetryEvent.WORKBENCH_CLEAR_RESULT_CLICKED, query) + sendEvent( + isExecuteTypeSearch + ? TelemetryEvent.SEARCH_CLEAR_RESULT_CLICKED + : TelemetryEvent.WORKBENCH_CLEAR_RESULT_CLICKED, + query + ) } const handleQueryReRun = (event: React.MouseEvent) => { @@ -181,8 +200,14 @@ const QueryCardHeader = (props: Props) => { const handleToggleOpen = () => { if (!isFullScreen && !isSilentModeWithoutError(resultsMode, summary?.fail)) { + const collapsedEventName = isExecuteTypeSearch + ? TelemetryEvent.SEARCH_RESULTS_COLLAPSED + : TelemetryEvent.WORKBENCH_RESULTS_COLLAPSED + const expandedEventName = isExecuteTypeSearch + ? TelemetryEvent.SEARCH_RESULTS_EXPANDED + : TelemetryEvent.WORKBENCH_RESULTS_EXPANDED sendEvent( - isOpen ? TelemetryEvent.WORKBENCH_RESULTS_COLLAPSED : TelemetryEvent.WORKBENCH_RESULTS_EXPANDED, + isOpen ? collapsedEventName : expandedEventName, query ) } diff --git a/redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/markdown-message/components/code-block/CodeBlock.tsx b/redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/markdown-message/components/code-block/CodeBlock.tsx index 30b51199a4..582e9b94c8 100644 --- a/redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/markdown-message/components/code-block/CodeBlock.tsx +++ b/redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/markdown-message/components/code-block/CodeBlock.tsx @@ -4,6 +4,7 @@ import { CodeButtonParams } from 'uiSrc/constants' import { sendWbQueryAction } from 'uiSrc/slices/workbench/wb-results' import { CodeButtonBlock } from 'uiSrc/components/markdown' import { ButtonLang } from 'uiSrc/utils/formatters/markdown/remarkCode' +import { CommandExecutionType } from 'uiSrc/slices/interfaces' import { AdditionalRedisModule } from 'apiSrc/modules/database/models/additional.redis.module' export interface Props { @@ -20,7 +21,14 @@ const CodeBlock = (props: Props) => { const handleApply = (params?: CodeButtonParams, onFinish?: () => void) => { onRunCommand?.(children) - dispatch(sendWbQueryAction(children, null, params, { afterAll: onFinish }, onFinish)) + dispatch(sendWbQueryAction( + children, + null, + params, + CommandExecutionType.Workbench, + { afterAll: onFinish }, + onFinish + )) } return ( diff --git a/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementAreaWrapper.tsx b/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementAreaWrapper.tsx index 72acc86174..e94b0ee661 100644 --- a/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementAreaWrapper.tsx +++ b/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementAreaWrapper.tsx @@ -7,6 +7,7 @@ import { workbenchCustomTutorialsSelector } from 'uiSrc/slices/workbench/wb-cust import { sendEventTelemetry, TELEMETRY_EMPTY_VALUE, TelemetryEvent } from 'uiSrc/telemetry' import { CodeButtonParams } from 'uiSrc/constants' import { sendWbQueryAction } from 'uiSrc/slices/workbench/wb-results' +import { CommandExecutionType } from 'uiSrc/slices/interfaces' import { getTutorialSection } from './EnablementArea/utils' import EnablementArea from './EnablementArea' @@ -26,7 +27,14 @@ const EnablementAreaWrapper = () => { params?: CodeButtonParams, onFinish?: () => void ) => { - dispatch(sendWbQueryAction(script, null, params, { afterAll: onFinish }, onFinish)) + dispatch(sendWbQueryAction( + script, + null, + params, + CommandExecutionType.Workbench, + { afterAll: onFinish }, + onFinish + )) } const onOpenInternalPage = ({ path, manifestPath }: IInternalPage) => { diff --git a/redisinsight/ui/src/pages/instance/InstancePage.spec.tsx b/redisinsight/ui/src/pages/instance/InstancePage.spec.tsx index bf183f94a8..e8f2b7fb53 100644 --- a/redisinsight/ui/src/pages/instance/InstancePage.spec.tsx +++ b/redisinsight/ui/src/pages/instance/InstancePage.spec.tsx @@ -28,6 +28,7 @@ import { } from 'uiSrc/slices/instances/instances' import { resetConnectedInstance as resetRdiConnectedInstance } from 'uiSrc/slices/rdi/instances' import { clearExpertChatHistory } from 'uiSrc/slices/panels/aiAssistant' +import { getAllPlugins } from 'uiSrc/slices/app/plugins' import InstancePage, { Props } from './InstancePage' const INSTANCE_ID_MOCK = 'instanceId' @@ -119,6 +120,7 @@ describe('InstancePage', () => { ] const expectedActions = [ + getAllPlugins(), setDefaultInstance(), setConnectedInstance(), getDatabaseConfigInfo(), diff --git a/redisinsight/ui/src/pages/instance/InstancePage.tsx b/redisinsight/ui/src/pages/instance/InstancePage.tsx index 220d238588..7f71ad7b49 100644 --- a/redisinsight/ui/src/pages/instance/InstancePage.tsx +++ b/redisinsight/ui/src/pages/instance/InstancePage.tsx @@ -23,6 +23,7 @@ import { localStorageService } from 'uiSrc/services' import { InstancePageTemplate } from 'uiSrc/templates' import { getPageName } from 'uiSrc/utils/routing' import { resetConnectedInstance as resetRdiConnectedInstance } from 'uiSrc/slices/rdi/instances' +import { loadPluginsAction } from 'uiSrc/slices/app/plugins' import InstancePageRouter from './InstancePageRouter' export interface Props { @@ -41,6 +42,10 @@ const InstancePage = ({ routes = [] }: Props) => { const lastPageRef = useRef() + useEffect(() => { + dispatch(loadPluginsAction()) + }, []) + useEffect(() => { dispatch(fetchConnectedInstanceAction(connectionInstanceId, () => { !modulesData.length && dispatch(fetchInstancesAction()) diff --git a/redisinsight/ui/src/pages/search/SearchPage.tsx b/redisinsight/ui/src/pages/search/SearchPage.tsx index f4e2562541..10c267ec84 100644 --- a/redisinsight/ui/src/pages/search/SearchPage.tsx +++ b/redisinsight/ui/src/pages/search/SearchPage.tsx @@ -4,10 +4,7 @@ import { EuiResizableContainer } from '@elastic/eui' import { useDispatch, useSelector } from 'react-redux' import { useParams } from 'react-router-dom' -import { - appContextSearchAndQuery, - setSQVerticalPanelSizes, -} from 'uiSrc/slices/app/context' +import { appContextSearchAndQuery, setSQVerticalPanelSizes, } from 'uiSrc/slices/app/context' import { QueryWrapper, ResultsHistory } from 'uiSrc/pages/search/components' import { sendWbQueryAction } from 'uiSrc/slices/workbench/wb-results' @@ -17,6 +14,8 @@ import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { sendPageViewTelemetry, TelemetryPageView } from 'uiSrc/telemetry' import { CodeButtonParams } from 'uiSrc/constants' +import { CommandExecutionType } from 'uiSrc/slices/interfaces' +import { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands' import styles from './styles.module.scss' const verticalPanelIds = { @@ -26,6 +25,8 @@ const verticalPanelIds = { const SearchPage = () => { const { name: connectedInstanceName, db } = useSelector(connectedInstanceSelector) + const { commandsArray } = useSelector(appRedisCommandsSelector) + const { panelSizes: { vertical } } = useSelector(appContextSearchAndQuery) const [isPageViewSent, setIsPageViewSent] = useState(false) @@ -71,7 +72,8 @@ const SearchPage = () => { { ...executeParams, results: 'single', - } + }, + CommandExecutionType.Search )) } @@ -91,7 +93,7 @@ const SearchPage = () => { initialSize={vertical[verticalPanelIds.firstPanelId] ?? 20} style={{ minHeight: '240px', zIndex: '8' }} > - + { // Fix scroll on low height - 140px (queryPanel) style={{ maxHeight: 'calc(100% - 240px)' }} > - + )} diff --git a/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.tsx b/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.tsx index 2d47aeaa91..1db85cde05 100644 --- a/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.tsx +++ b/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.tsx @@ -8,11 +8,10 @@ import { CodeButtonParams } from 'uiSrc/constants' import { getCommandsFromQuery, Nullable } from 'uiSrc/utils' import { changeSQActiveRunQueryMode, searchAndQuerySelector } from 'uiSrc/slices/search/searchAndQuery' -import { appContextSearchAndQuery, setSQVerticalScript } from 'uiSrc/slices/app/context' +import { appContextSearchAndQuery, setSQScript } from 'uiSrc/slices/app/context' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { fetchRedisearchListAction, redisearchListSelector } from 'uiSrc/slices/browser/redisearch' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' -import { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands' import { SUPPORTED_COMMANDS_LIST } from 'uiSrc/pages/search/components/query/constants' import { SearchCommand } from 'uiSrc/pages/search/types' import { TUTORIALS } from './constants' @@ -24,6 +23,7 @@ import Query from '../query' import styles from './styles.module.scss' export interface Props { + commandsArray?: string[] onSubmit: ( commandInit: string, commandId?: Nullable, @@ -32,13 +32,12 @@ export interface Props { } const QueryWrapper = (props: Props) => { - const { onSubmit } = props + const { commandsArray = [], onSubmit } = props const { id: connectedIndstanceId } = useSelector(connectedInstanceSelector) const { script: scriptContext } = useSelector(appContextSearchAndQuery) const { activeRunQueryMode } = useSelector(searchAndQuerySelector) const { data: indexes = [] } = useSelector(redisearchListSelector) - const { commandsArray } = useSelector(appRedisCommandsSelector) const [value, setValue] = useState(scriptContext) @@ -55,7 +54,7 @@ const QueryWrapper = (props: Props) => { const dispatch = useDispatch() useEffect(() => () => { - dispatch(setSQVerticalScript(scriptRef.current)) + dispatch(setSQScript(scriptRef.current)) }, []) useEffect(() => { @@ -87,7 +86,6 @@ const QueryWrapper = (props: Props) => { eventData: { databaseId: instanceId, mode: activeRunQueryMode, - // TODO sanitize user query command: getCommandsFromQuery(value, commandsArray) || '' } }) diff --git a/redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.spec.tsx b/redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.spec.tsx index 21d2666793..a152317088 100644 --- a/redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.spec.tsx +++ b/redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.spec.tsx @@ -1,10 +1,157 @@ import React from 'react' -import { render, screen } from 'uiSrc/utils/test-utils' +import { cloneDeep } from 'lodash' +import { cleanup, fireEvent, mockedStore, render, screen } from 'uiSrc/utils/test-utils' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/analytics/clusterDetailsHandlers' +import { + clearWbResults, + loadWBHistory, + processWBCommand, + workbenchResultsSelector +} from 'uiSrc/slices/workbench/wb-results' import ResultsHistory from './ResultsHistory' +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) + +jest.mock('uiSrc/slices/workbench/wb-results', () => ({ + ...jest.requireActual('uiSrc/slices/workbench/wb-results'), + workbenchResultsSelector: jest.fn().mockReturnValue({ + loading: false, + items: [] + }) +})) + +let store: typeof mockedStore +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + describe('ResultsHistory', () => { it('should render', () => { expect(render()).toBeTruthy() }) + + it('should call proper actions on rerun', async () => { + const onSubmit = jest.fn() + const sendEventTelemetryMock = jest.fn(); + (sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock); + + (workbenchResultsSelector as jest.Mock).mockReturnValue({ + items: [ + { + mode: 'RAW', + resultsMode: 'DEFAULT', + id: '9dda0f6d-9265-4b15-b627-82d2eb867605', + databaseId: '18c37d1d-bc25-4e46-a20d-a1f9bf228946', + command: 'info', + summary: null, + createdAt: '2022-09-28T18:04:46.000Z', + emptyCommand: false + } + ] + }) + + render() + + fireEvent.click(screen.getByTestId('re-run-command')) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.SEARCH_COMMAND_RUN_AGAIN, + eventData: { + command: 'INFO', + databaseId: INSTANCE_ID_MOCK, + mode: 'RAW' + } + }); + (sendEventTelemetry as jest.Mock).mockRestore() + + expect(onSubmit).toBeCalledWith( + 'info', + '9dda0f6d-9265-4b15-b627-82d2eb867605', + { mode: 'RAW' } + ) + }) + + it('should call proper actions on delete', async () => { + const onSubmit = jest.fn() + const sendEventTelemetryMock = jest.fn(); + (sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock); + + (workbenchResultsSelector as jest.Mock).mockReturnValue({ + items: [ + { + mode: 'RAW', + resultsMode: 'DEFAULT', + id: '9dda0f6d-9265-4b15-b627-82d2eb867605', + databaseId: '18c37d1d-bc25-4e46-a20d-a1f9bf228946', + command: 'info', + summary: null, + createdAt: '2022-09-28T18:04:46.000Z', + emptyCommand: false + } + ] + }) + + render() + + fireEvent.click(screen.getByTestId('delete-command')) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.SEARCH_CLEAR_RESULT_CLICKED, + eventData: { + command: 'info', + databaseId: INSTANCE_ID_MOCK, + } + }); + (sendEventTelemetry as jest.Mock).mockRestore() + + expect(store.getActions()).toEqual([ + loadWBHistory(), + processWBCommand('9dda0f6d-9265-4b15-b627-82d2eb867605') + ]) + }) + + it('should call proper actions on clear all commands', async () => { + const onSubmit = jest.fn() + const sendEventTelemetryMock = jest.fn(); + (sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock); + + (workbenchResultsSelector as jest.Mock).mockReturnValue({ + items: [ + { + mode: 'RAW', + resultsMode: 'DEFAULT', + id: '9dda0f6d-9265-4b15-b627-82d2eb867605', + databaseId: '18c37d1d-bc25-4e46-a20d-a1f9bf228946', + command: 'info', + summary: null, + createdAt: '2022-09-28T18:04:46.000Z', + emptyCommand: false + } + ] + }) + + render() + + fireEvent.click(screen.getByTestId('clear-history-btn')) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.SEARCH_CLEAR_ALL_RESULTS_CLICKED, + eventData: { + databaseId: INSTANCE_ID_MOCK, + } + }); + (sendEventTelemetry as jest.Mock).mockRestore() + + expect(store.getActions()).toEqual([ + loadWBHistory(), + clearWbResults() + ]) + }) }) diff --git a/redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.tsx b/redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.tsx index b15b21dace..d03724bf7c 100644 --- a/redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.tsx +++ b/redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.tsx @@ -4,25 +4,41 @@ import cx from 'classnames' import { useDispatch, useSelector } from 'react-redux' import { useParams } from 'react-router-dom' -import { Nullable } from 'uiSrc/utils' +import { EuiButtonEmpty, EuiProgress } from '@elastic/eui' +import { getCommandsFromQuery, Nullable } from 'uiSrc/utils' import { QueryCard } from 'uiSrc/components/query' -import { fetchWBCommandAction, fetchWBHistoryAction, workbenchResultsSelector } from 'uiSrc/slices/workbench/wb-results' +import { + clearWbResultsAction, + deleteWBCommandAction, + fetchWBCommandAction, + fetchWBHistoryAction, + resetWBHistoryItems, + workbenchResultsSelector +} from 'uiSrc/slices/workbench/wb-results' import { searchAndQuerySelector } from 'uiSrc/slices/search/searchAndQuery' +import { CommandExecutionType, RunQueryMode } from 'uiSrc/slices/interfaces' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { ProfileQueryType } from 'uiSrc/pages/workbench/constants' +import { generateProfileQueryForCommand } from 'uiSrc/pages/workbench/utils' +import { CodeButtonParams } from 'uiSrc/constants' import styles from './styles.module.scss' export interface Props { + commandsArray?: string[] onSubmit: ( commandInit: string, commandId?: Nullable, + executeParams?: CodeButtonParams ) => void } const ResultsHistory = (props: Props) => { - const { onSubmit } = props + const { commandsArray = [], onSubmit } = props const { items, clearing, + isLoaded } = useSelector(workbenchResultsSelector) const { activeRunQueryMode } = useSelector(searchAndQuerySelector) @@ -30,15 +46,82 @@ const ResultsHistory = (props: Props) => { const { instanceId } = useParams<{ instanceId: string }>() useEffect(() => { - dispatch(fetchWBHistoryAction(instanceId)) + dispatch(fetchWBHistoryAction(instanceId, CommandExecutionType.Search)) + + return () => { + dispatch(resetWBHistoryItems()) + } }, []) const handleQueryOpen = (commandId: string = '') => { dispatch(fetchWBCommandAction(commandId)) } + const handleQueryDelete = (commandId: string) => { + dispatch(deleteWBCommandAction(commandId)) + } + + const handleAllQueriesDelete = () => { + dispatch(clearWbResultsAction(CommandExecutionType.Search)) + sendEventTelemetry({ + event: TelemetryEvent.SEARCH_CLEAR_ALL_RESULTS_CLICKED, + eventData: { + databaseId: instanceId, + } + }) + } + + const handleQueryReRun = ( + query: string, + commandId?: Nullable, + mode?: RunQueryMode + ) => { + sendEventTelemetry({ + event: TelemetryEvent.SEARCH_COMMAND_RUN_AGAIN, + eventData: { + databaseId: instanceId, + command: getCommandsFromQuery(query, commandsArray) || '', + mode + } + }) + onSubmit(query, commandId, { mode }) + } + + const handleQueryProfile = ( + profileType: ProfileQueryType, + commandExecution: { command: string, mode?: RunQueryMode } + ) => { + const { command, mode } = commandExecution + const profileQuery = generateProfileQueryForCommand(command, profileType) + if (profileQuery) { + onSubmit(command, null, { mode }) + } + } + return (
+ {!isLoaded && ( + + )} + {!!items?.length && ( +
+ + Clear Results + +
+ )}
{items?.length ? items.map(( @@ -72,11 +155,13 @@ const ResultsHistory = (props: Props) => { executionTime={executionTime} mode={mode} activeMode={activeRunQueryMode} + executionType={CommandExecutionType.Search} db={db} - onQueryProfile={() => {}} + onQueryProfile={(profileType) => + handleQueryProfile(profileType, { command, mode })} onQueryOpen={() => handleQueryOpen(id)} - onQueryReRun={() => onSubmit(command, id)} - onQueryDelete={() => {}} + onQueryReRun={() => handleQueryReRun(command, id, mode)} + onQueryDelete={() => handleQueryDelete(id)} /> )) : null}
diff --git a/redisinsight/ui/src/pages/search/components/results-history/styles.module.scss b/redisinsight/ui/src/pages/search/components/results-history/styles.module.scss index 7ea2ec299b..84827c7662 100644 --- a/redisinsight/ui/src/pages/search/components/results-history/styles.module.scss +++ b/redisinsight/ui/src/pages/search/components/results-history/styles.module.scss @@ -31,3 +31,14 @@ border-bottom: 1px solid var(--tableDarkestBorderColor); } +.clearAllBtn { + font-size: 14px !important; + + :global { + .euiIcon { + width: 14px !important; + height: 14px !important; + } + } +} + diff --git a/redisinsight/ui/src/pages/workbench/WorkbenchPage.tsx b/redisinsight/ui/src/pages/workbench/WorkbenchPage.tsx index 455dd9ca5f..2e92e9c76b 100644 --- a/redisinsight/ui/src/pages/workbench/WorkbenchPage.tsx +++ b/redisinsight/ui/src/pages/workbench/WorkbenchPage.tsx @@ -1,10 +1,9 @@ import React, { useEffect, useState } from 'react' -import { useDispatch, useSelector } from 'react-redux' +import { useSelector } from 'react-redux' import { useParams } from 'react-router-dom' import { formatLongName, getDbIndex, setTitle } from 'uiSrc/utils' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' -import { loadPluginsAction } from 'uiSrc/slices/app/plugins' import { sendPageViewTelemetry, TelemetryPageView } from 'uiSrc/telemetry' import WBViewWrapper from './components/wb-view' @@ -15,7 +14,6 @@ const WorkbenchPage = () => { const { instanceId } = useParams<{ instanceId: string }>() - const dispatch = useDispatch() setTitle(`${formatLongName(connectedInstanceName, 33, 0, '...')} ${getDbIndex(db)} - Workbench`) useEffect(() => { @@ -34,10 +32,6 @@ const WorkbenchPage = () => { setIsPageViewSent(true) } - useEffect(() => { - dispatch(loadPluginsAction()) - }, []) - return () } diff --git a/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.tsx b/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.tsx index e484b25f28..bb050d7f89 100644 --- a/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.tsx +++ b/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.tsx @@ -15,7 +15,7 @@ import { sendWbQueryAction, workbenchResultsSelector, } from 'uiSrc/slices/workbench/wb-results' -import { Instance, IPluginVisualization } from 'uiSrc/slices/interfaces' +import { CommandExecutionType, Instance, IPluginVisualization } from 'uiSrc/slices/interfaces' import { connectedInstanceSelector, initialState as instanceInitState } from 'uiSrc/slices/instances/instances' import { ResultsMode, RunQueryMode } from 'uiSrc/slices/interfaces/workbench' import { cliSettingsSelector, fetchBlockingCliCommandsAction } from 'uiSrc/slices/cli/cli-settings' @@ -161,13 +161,19 @@ const WBViewWrapper = () => { ) => { if (!commandInit?.length) return - dispatch(sendWbQueryAction(commandInit, commandId, executeParams, { - afterEach: () => { - const isNewCommand = !commandId - isNewCommand && scrollResults('start') - }, - afterAll: updateOnboardingOnSubmit - })) + dispatch(sendWbQueryAction( + commandInit, + commandId, + executeParams, + CommandExecutionType.Workbench, + { + afterEach: () => { + const isNewCommand = !commandId + isNewCommand && scrollResults('start') + }, + afterAll: updateOnboardingOnSubmit + } + )) } const scrollResults = (inline: ScrollLogicalPosition = 'start') => { diff --git a/redisinsight/ui/src/slices/app/context.ts b/redisinsight/ui/src/slices/app/context.ts index 07ca089c37..4fce0714a5 100644 --- a/redisinsight/ui/src/slices/app/context.ts +++ b/redisinsight/ui/src/slices/app/context.ts @@ -187,7 +187,7 @@ const appContextSlice = createSlice({ setSQVerticalPanelSizes: (state, { payload }: { payload: any }) => { state.searchAndQuery.panelSizes.vertical = payload }, - setSQVerticalScript: (state, { payload }: { payload: any }) => { + setSQScript: (state, { payload }: { payload: any }) => { state.searchAndQuery.script = payload }, setLastPageContext: (state, { payload }: { payload: string }) => { @@ -261,7 +261,7 @@ export const { setWorkbenchScript, setWorkbenchVerticalPanelSizes, setSQVerticalPanelSizes, - setSQVerticalScript, + setSQScript, setLastPageContext, setPubSubFieldsContext, setBrowserBulkActionOpen, diff --git a/redisinsight/ui/src/slices/app/plugins.ts b/redisinsight/ui/src/slices/app/plugins.ts index 77c248067c..1f30dea8a5 100644 --- a/redisinsight/ui/src/slices/app/plugins.ts +++ b/redisinsight/ui/src/slices/app/plugins.ts @@ -9,7 +9,7 @@ import { } from 'uiSrc/utils' import { apiService } from 'uiSrc/services' import { ApiEndpoints } from 'uiSrc/constants' -import { IPlugin, PluginsResponse, StateAppPlugins } from 'uiSrc/slices/interfaces' +import { CommandExecutionType, IPlugin, PluginsResponse, StateAppPlugins } from 'uiSrc/slices/interfaces' import { SendCommandResponse } from 'apiSrc/modules/cli/dto/cli.dto' import { PluginState } from 'apiSrc/modules/workbench/models/plugin-state' @@ -95,11 +95,19 @@ export function loadPluginsAction() { } // Asynchronous thunk action -export function sendPluginCommandAction({ command = '', onSuccessAction, onFailAction }: { - command: string - onSuccessAction?: (responseData: any) => void - onFailAction?: (error: any) => void -}) { +export function sendPluginCommandAction( + { + command = '', + executionType = CommandExecutionType.Workbench, + onSuccessAction, + onFailAction + }: { + command: string + executionType?: CommandExecutionType + onSuccessAction?: (responseData: any) => void + onFailAction?: (error: any) => void + } +) { return async (_dispatch: AppDispatch, stateInit: () => RootState) => { try { const state = stateInit() @@ -112,7 +120,8 @@ export function sendPluginCommandAction({ command = '', onSuccessAction, onFailA ApiEndpoints.COMMAND_EXECUTIONS ), { - command: multilineCommandToOneLine(command) + command: multilineCommandToOneLine(command), + type: executionType } ) diff --git a/redisinsight/ui/src/slices/interfaces/workbench.ts b/redisinsight/ui/src/slices/interfaces/workbench.ts index 0dafcdb0ec..94652c9768 100644 --- a/redisinsight/ui/src/slices/interfaces/workbench.ts +++ b/redisinsight/ui/src/slices/interfaces/workbench.ts @@ -69,6 +69,11 @@ export enum ResultsMode { GroupMode = 'GROUP_MODE', } +export enum CommandExecutionType { + Workbench = 'WORKBENCH', + Search = 'SEARCH', +} + export interface ResultsSummary { total: number success: number diff --git a/redisinsight/ui/src/slices/workbench/wb-results.ts b/redisinsight/ui/src/slices/workbench/wb-results.ts index 7c1c570ebd..3939291d3b 100644 --- a/redisinsight/ui/src/slices/workbench/wb-results.ts +++ b/redisinsight/ui/src/slices/workbench/wb-results.ts @@ -5,7 +5,7 @@ import { apiService, localStorageService } from 'uiSrc/services' import { ApiEndpoints, BrowserStorageItem, CodeButtonParams, EMPTY_COMMAND } from 'uiSrc/constants' import { addErrorNotification } from 'uiSrc/slices/app/notifications' import { CliOutputFormatterType } from 'uiSrc/constants/cliOutput' -import { RunQueryMode, ResultsMode } from 'uiSrc/slices/interfaces/workbench' +import { RunQueryMode, ResultsMode, CommandExecutionType } from 'uiSrc/slices/interfaces/workbench' import { getApiErrorMessage, getCommandsForExecution, getExecuteParams, @@ -242,7 +242,10 @@ export const workbenchResultsSelector = (state: RootState) => state.workbench.re export default workbenchResultsSlice.reducer // Asynchronous thunk actions -export function fetchWBHistoryAction(instanceId: string) { +export function fetchWBHistoryAction( + instanceId: string, + executionType = CommandExecutionType.Workbench, +) { return async (dispatch: AppDispatch) => { dispatch(loadWBHistory()) @@ -251,7 +254,8 @@ export function fetchWBHistoryAction(instanceId: string) { getUrl( instanceId, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS, - ) + ), + { params: { type: executionType } } ) if (isStatusSuccessful(status)) { @@ -273,6 +277,7 @@ export function sendWBCommandAction({ mode = RunQueryMode.ASCII, resultsMode = ResultsMode.Default, commandId = `${Date.now()}`, + executionType = CommandExecutionType.Workbench, onSuccessAction, onFailAction, }: { @@ -281,6 +286,7 @@ export function sendWBCommandAction({ commandId?: string mode: RunQueryMode resultsMode?: ResultsMode + executionType?: CommandExecutionType onSuccessAction?: (multiCommands: string[]) => void onFailAction?: () => void }) { @@ -304,7 +310,8 @@ export function sendWBCommandAction({ { commands, mode, - resultsMode + resultsMode, + type: executionType } ) @@ -335,6 +342,7 @@ export function sendWBCommandClusterAction({ mode = RunQueryMode.ASCII, resultsMode = ResultsMode.Default, commandId = `${Date.now()}`, + executionType = CommandExecutionType.Workbench, onSuccessAction, onFailAction, }: { @@ -344,6 +352,7 @@ export function sendWBCommandClusterAction({ multiCommands?: string[] mode?: RunQueryMode, resultsMode?: ResultsMode + executionType?: CommandExecutionType onSuccessAction?: (multiCommands: string[]) => void onFailAction?: () => void }) { @@ -367,6 +376,7 @@ export function sendWBCommandClusterAction({ commands, mode, resultsMode, + type: executionType, outputFormat: CliOutputFormatterType.Raw } ) @@ -462,6 +472,7 @@ export function deleteWBCommandAction( // Asynchronous thunk action export function clearWbResultsAction( + executionType = CommandExecutionType.Workbench, onSuccessAction?: () => void, onFailAction?: () => void, ) { @@ -477,6 +488,9 @@ export function clearWbResultsAction( id, ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS, ), + { + params: { type: executionType } + } ) if (isStatusSuccessful(status)) { @@ -498,6 +512,7 @@ export function sendWbQueryAction( queryInit: string, commandId?: Nullable, executeParams: CodeButtonParams = {}, + executionType?: CommandExecutionType, onSuccessAction?: { afterEach?: () => void, afterAll?: () => void @@ -533,6 +548,7 @@ export function sendWbQueryAction( commands, multiCommands, mode: activeRunQueryMode, + executionType, onSuccessAction: onSuccess, onFailAction: onFail })) @@ -555,6 +571,7 @@ export function sendWbQueryAction( mode: activeRunQueryMode, resultsMode, multiCommands, + executionType, onSuccessAction: onSuccess, onFailAction: onFail }) diff --git a/redisinsight/ui/src/telemetry/events.ts b/redisinsight/ui/src/telemetry/events.ts index 9b379e999b..b82ee884b3 100644 --- a/redisinsight/ui/src/telemetry/events.ts +++ b/redisinsight/ui/src/telemetry/events.ts @@ -127,6 +127,16 @@ export enum TelemetryEvent { WORKBENCH_CLEAR_ALL_RESULTS_CLICKED = 'WORKBENCH_CLEAR_ALL_RESULTS_CLICKED', SEARCH_COMMAND_SUBMITTED = 'SEARCH_COMMAND_SUBMITTED', + SEARCH_RESULT_VIEW_CHANGED = 'SEARCH_RESULT_VIEW_CHANGED', + SEARCH_COMMAND_COPIED = 'SEARCH_COMMAND_COPIED', + SEARCH_COMMAND_RUN_AGAIN = 'SEARCH_COMMAND_RUN_AGAIN', + SEARCH_RESULTS_IN_FULL_SCREEN = 'SEARCH_RESULTS_IN_FULL_SCREEN', + SEARCH_RESULTS_COLLAPSED = 'SEARCH_RESULTS_COLLAPSED', + SEARCH_RESULTS_EXPANDED = 'SEARCH_RESULTS_EXPANDED', + SEARCH_CLEAR_RESULT_CLICKED = 'SEARCH_CLEAR_RESULT_CLICKED', + SEARCH_CLEAR_ALL_RESULTS_CLICKED = 'SEARCH_CLEAR_ALL_RESULTS_CLICKED', + SEARCH_EXPAND_ALL_RESULTS = 'SEARCH_EXPAND_ALL_RESULTS', + SEARCH_FORMATTER_CHANGED = 'SEARCH_FORMATTER_CHANGED', PROFILER_OPENED = 'PROFILER_OPENED', PROFILER_STARTED = 'PROFILER_STARTED', From 6e1cad4ae23ced3133504992fe771be271e36f7c Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Thu, 12 Sep 2024 11:28:59 +0200 Subject: [PATCH 061/112] #RI-6079 - update colors #RI-6099 - highlight fields --- redisinsight/ui/src/constants/monaco/theme.ts | 18 +++++++++--------- .../monaco/monarchTokens/redisearchTokens.ts | 4 ++++ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/redisinsight/ui/src/constants/monaco/theme.ts b/redisinsight/ui/src/constants/monaco/theme.ts index 0a48b48ded..f7f69e1887 100644 --- a/redisinsight/ui/src/constants/monaco/theme.ts +++ b/redisinsight/ui/src/constants/monaco/theme.ts @@ -47,15 +47,15 @@ export const redisearchDarKThemeRules = [ export const redisearchLightThemeRules = [ { token: 'keyword', foreground: '#8094B1', fontStyle: 'bold' }, - { token: 'argument.block.0', foreground: '#BDE8D7' }, - { token: 'argument.block.1', foreground: '#8CD7B9' }, - { token: 'argument.block.2', foreground: '#5BC69B' }, - { token: 'argument.block.3', foreground: '#3A8365' }, - { token: 'argument.block.withToken.0', foreground: '#BDE8D7' }, - { token: 'argument.block.withToken.1', foreground: '#8CD7B9' }, - { token: 'argument.block.withToken.2', foreground: '#5BC69B' }, - { token: 'argument.block.withToken.3', foreground: '#3A8365' }, - { token: 'loadAll', foreground: '#BDE8D7' }, + { token: 'argument.block.0', foreground: '#8CD7B9' }, + { token: 'argument.block.1', foreground: '#5BC69B' }, + { token: 'argument.block.2', foreground: '#3A8365' }, + { token: 'argument.block.3', foreground: '#244F3E' }, + { token: 'argument.block.withToken.0', foreground: '#8CD7B9' }, + { token: 'argument.block.withToken.1', foreground: '#5BC69B' }, + { token: 'argument.block.withToken.2', foreground: '#3A8365' }, + { token: 'argument.block.withToken.3', foreground: '#244F3E' }, + { token: 'loadAll', foreground: '#8CD7B9' }, { token: 'index', foreground: '#DE47BB' }, { token: 'query', foreground: '#7B90E0' }, { token: 'field', foreground: '#B02C30' }, diff --git a/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts b/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts index 323a7be11d..187fe305fe 100644 --- a/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts +++ b/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts @@ -33,6 +33,7 @@ export const getRediSearchMonarchTokensProvider = ( keywords, tokenizer: { root: [ + { include: '@fields' }, { include: '@whitespace' }, { include: '@numbers' }, { include: '@strings' }, @@ -65,6 +66,9 @@ export const getRediSearchMonarchTokensProvider = ( { include: 'root' } // Fallback to the root state if nothing matches ], ...generateQuery(), + fields: [ + [/@\w+/, { token: 'field', }] + ], whitespace: [ [/\s+/, 'white'], [/\/\/.*$/, 'comment'], From 5a019a6f34c29ebe776af088f631ca8009f495a7 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Thu, 12 Sep 2024 14:13:48 +0200 Subject: [PATCH 062/112] #RI-6108 - fix clear results #RI-6109 - fix history #RI-6110 - fix profile/explain --- .../components/code-block/CodeBlock.spec.tsx | 4 ++- .../ui/src/pages/search/SearchPage.spec.tsx | 7 +++-- .../ui/src/pages/search/SearchPage.tsx | 9 ++++-- .../results-history/ResultsHistory.tsx | 2 +- .../ui/src/pages/workbench/WorkbenchPage.tsx | 9 +++++- .../ui/src/slices/interfaces/workbench.ts | 1 + .../slices/tests/workbench/wb-results.spec.ts | 31 +++++++++++++++++-- .../ui/src/slices/workbench/wb-results.ts | 27 ++++++++++++---- 8 files changed, 74 insertions(+), 16 deletions(-) diff --git a/redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/markdown-message/components/code-block/CodeBlock.spec.tsx b/redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/markdown-message/components/code-block/CodeBlock.spec.tsx index 7d1502fda6..98bbebd6a9 100644 --- a/redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/markdown-message/components/code-block/CodeBlock.spec.tsx +++ b/redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/markdown-message/components/code-block/CodeBlock.spec.tsx @@ -5,6 +5,7 @@ import { ButtonLang } from 'uiSrc/utils/formatters/markdown/remarkCode' import { sendWBCommand } from 'uiSrc/slices/workbench/wb-results' import { setDbIndexState } from 'uiSrc/slices/app/context' +import { CommandExecutionType } from 'uiSrc/slices/interfaces' import CodeBlock from './CodeBlock' let store: typeof mockedStore @@ -27,7 +28,8 @@ describe('CodeBlock', () => { expect(store.getActions()).toEqual([ sendWBCommand({ commandId: expect.any(String), - commands: ['info'] + commands: ['info'], + executionType: CommandExecutionType.Workbench }), setDbIndexState(true) ]) diff --git a/redisinsight/ui/src/pages/search/SearchPage.spec.tsx b/redisinsight/ui/src/pages/search/SearchPage.spec.tsx index 50a53c0b27..c160ed2a87 100644 --- a/redisinsight/ui/src/pages/search/SearchPage.spec.tsx +++ b/redisinsight/ui/src/pages/search/SearchPage.spec.tsx @@ -2,9 +2,10 @@ import React from 'react' import { cloneDeep } from 'lodash' import { cleanup, fireEvent, mockedStore, render, screen } from 'uiSrc/utils/test-utils' -import { loadWBHistory, sendWBCommand } from 'uiSrc/slices/workbench/wb-results' +import { loadWBHistory, sendWBCommand, setExecutionType } from 'uiSrc/slices/workbench/wb-results' import { setDbIndexState } from 'uiSrc/slices/app/context' import { sendPageViewTelemetry, TelemetryPageView } from 'uiSrc/telemetry' +import { CommandExecutionType } from 'uiSrc/slices/interfaces' import SearchPage from './SearchPage' jest.mock('uiSrc/slices/app/context', () => ({ @@ -66,9 +67,11 @@ describe('SearchPage', () => { expect(store.getActions()).toEqual([ loadWBHistory(), + setExecutionType(CommandExecutionType.Search), sendWBCommand({ commandId: expect.any(String), - commands: ['value'] + commands: ['value'], + executionType: CommandExecutionType.Search }), setDbIndexState(true) ]) diff --git a/redisinsight/ui/src/pages/search/SearchPage.tsx b/redisinsight/ui/src/pages/search/SearchPage.tsx index 10c267ec84..e69dd222fd 100644 --- a/redisinsight/ui/src/pages/search/SearchPage.tsx +++ b/redisinsight/ui/src/pages/search/SearchPage.tsx @@ -7,7 +7,7 @@ import { useParams } from 'react-router-dom' import { appContextSearchAndQuery, setSQVerticalPanelSizes, } from 'uiSrc/slices/app/context' import { QueryWrapper, ResultsHistory } from 'uiSrc/pages/search/components' -import { sendWbQueryAction } from 'uiSrc/slices/workbench/wb-results' +import { sendWbQueryAction, setExecutionType } from 'uiSrc/slices/workbench/wb-results' import { formatLongName, getDbIndex, Nullable, setTitle } from 'uiSrc/utils' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' @@ -37,8 +37,11 @@ const SearchPage = () => { setTitle(`${formatLongName(connectedInstanceName, 33, 0, '...')} ${getDbIndex(db)} - Search and Query`) - useEffect(() => () => { - dispatch(setSQVerticalPanelSizes(verticalSizesRef.current)) + useEffect(() => { + dispatch(setExecutionType(CommandExecutionType.Search)) + return () => { + dispatch(setSQVerticalPanelSizes(verticalSizesRef.current)) + } }, []) useEffect(() => { diff --git a/redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.tsx b/redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.tsx index d03724bf7c..9d05a11205 100644 --- a/redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.tsx +++ b/redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.tsx @@ -94,7 +94,7 @@ const ResultsHistory = (props: Props) => { const { command, mode } = commandExecution const profileQuery = generateProfileQueryForCommand(command, profileType) if (profileQuery) { - onSubmit(command, null, { mode }) + onSubmit(profileQuery, null, { mode }) } } diff --git a/redisinsight/ui/src/pages/workbench/WorkbenchPage.tsx b/redisinsight/ui/src/pages/workbench/WorkbenchPage.tsx index 2e92e9c76b..bdd0a14058 100644 --- a/redisinsight/ui/src/pages/workbench/WorkbenchPage.tsx +++ b/redisinsight/ui/src/pages/workbench/WorkbenchPage.tsx @@ -1,10 +1,12 @@ import React, { useEffect, useState } from 'react' -import { useSelector } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { useParams } from 'react-router-dom' import { formatLongName, getDbIndex, setTitle } from 'uiSrc/utils' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { sendPageViewTelemetry, TelemetryPageView } from 'uiSrc/telemetry' +import { CommandExecutionType } from 'uiSrc/slices/interfaces' +import { setExecutionType } from 'uiSrc/slices/workbench/wb-results' import WBViewWrapper from './components/wb-view' const WorkbenchPage = () => { @@ -13,9 +15,14 @@ const WorkbenchPage = () => { const { name: connectedInstanceName, db } = useSelector(connectedInstanceSelector) const { instanceId } = useParams<{ instanceId: string }>() + const dispatch = useDispatch() setTitle(`${formatLongName(connectedInstanceName, 33, 0, '...')} ${getDbIndex(db)} - Workbench`) + useEffect(() => { + dispatch(setExecutionType(CommandExecutionType.Workbench)) + }, []) + useEffect(() => { if (connectedInstanceName && !isPageViewSent) { sendPageView(instanceId) diff --git a/redisinsight/ui/src/slices/interfaces/workbench.ts b/redisinsight/ui/src/slices/interfaces/workbench.ts index 94652c9768..4bb7378e84 100644 --- a/redisinsight/ui/src/slices/interfaces/workbench.ts +++ b/redisinsight/ui/src/slices/interfaces/workbench.ts @@ -9,6 +9,7 @@ export interface StateWorkbenchSettings { } export interface StateWorkbenchResults { + type: CommandExecutionType isLoaded: boolean loading: boolean processing: boolean diff --git a/redisinsight/ui/src/slices/tests/workbench/wb-results.spec.ts b/redisinsight/ui/src/slices/tests/workbench/wb-results.spec.ts index c9d9669382..e5338ed994 100644 --- a/redisinsight/ui/src/slices/tests/workbench/wb-results.spec.ts +++ b/redisinsight/ui/src/slices/tests/workbench/wb-results.spec.ts @@ -12,7 +12,7 @@ import { apiService } from 'uiSrc/services' import { addErrorNotification } from 'uiSrc/slices/app/notifications' import { ClusterNodeRole, CommandExecutionStatus } from 'uiSrc/slices/interfaces/cli' import { EMPTY_COMMAND } from 'uiSrc/constants' -import { ResultsMode } from 'uiSrc/slices/interfaces' +import { CommandExecutionType, ResultsMode } from 'uiSrc/slices/interfaces' import { setDbIndexState } from 'uiSrc/slices/app/context' import { SendClusterCommandDto } from 'apiSrc/modules/cli/dto/cli.dto' import reducer, { @@ -67,7 +67,8 @@ describe('workbench results slice', () => { // Arrange const mockPayload = { commands: ['command', 'command2'], - commandId: '123' + commandId: '123', + executionType: CommandExecutionType.Workbench } const state = { ...initialState, @@ -95,6 +96,32 @@ describe('workbench results slice', () => { }) }) + describe('sendWBCommand with another type', () => { + it('should properly set state', () => { + // Arrange + const mockPayload = { + commands: ['command', 'command2'], + commandId: '123', + executionType: CommandExecutionType.Search + } + const state = { + ...initialState, + items: [] + } + + // Act + const nextState = reducer(initialState, sendWBCommand(mockPayload)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + workbench: { + results: nextState, + }, + }) + expect(workbenchResultsSelector(rootState)).toEqual(state) + }) + }) + describe('toggleOpenWBResult', () => { it('should properly set isOpen = true', () => { // Arrange diff --git a/redisinsight/ui/src/slices/workbench/wb-results.ts b/redisinsight/ui/src/slices/workbench/wb-results.ts index 3939291d3b..f3a9d177f8 100644 --- a/redisinsight/ui/src/slices/workbench/wb-results.ts +++ b/redisinsight/ui/src/slices/workbench/wb-results.ts @@ -1,4 +1,4 @@ -import { createSlice } from '@reduxjs/toolkit' +import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { AxiosError } from 'axios' import { chunk, reverse } from 'lodash' import { apiService, localStorageService } from 'uiSrc/services' @@ -29,6 +29,7 @@ import { } from '../interfaces' export const initialState: StateWorkbenchResults = { + type: CommandExecutionType.Workbench, isLoaded: false, loading: false, processing: false, @@ -46,6 +47,10 @@ const workbenchResultsSlice = createSlice({ reducers: { setWBResultsInitialState: () => initialState, + setExecutionType: (state, { payload }: PayloadAction) => { + state.type = payload + }, + // Fetch Workbench history loadWBHistory: (state) => { state.loading = true @@ -110,8 +115,15 @@ const workbenchResultsSlice = createSlice({ state.processing = false }, - sendWBCommand: (state, { payload: { commands, commandId } }: - { payload: { commands: string[], commandId: string } }) => { + sendWBCommand: ( + state, + { + payload: { commands, commandId, executionType } + }: + PayloadAction<{ commands: string[], commandId: string, executionType: CommandExecutionType }> + ) => { + if (executionType !== state.type) return + let newItems = [ ...commands.map((command, i) => ({ command, @@ -215,6 +227,7 @@ const workbenchResultsSlice = createSlice({ // Actions generated from the slice export const { setWBResultsInitialState, + setExecutionType, loadWBHistory, loadWBHistorySuccess, loadWBHistoryFailure, @@ -297,7 +310,8 @@ export function sendWBCommandAction({ dispatch(sendWBCommand({ commands: isGroupResults(resultsMode) ? [`${commands.length} - Command(s)`] : commands, - commandId + commandId, + executionType })) dispatch(setDbIndexState(true)) @@ -363,7 +377,8 @@ export function sendWBCommandClusterAction({ dispatch(sendWBCommand({ commands: isGroupResults(resultsMode) ? [`${commands.length} - Commands`] : commands, - commandId + commandId, + executionType })) const { data, status } = await apiService.post( @@ -489,7 +504,7 @@ export function clearWbResultsAction( ApiEndpoints.WORKBENCH_COMMAND_EXECUTIONS, ), { - params: { type: executionType } + data: { type: executionType } } ) From cb75d3a2ac7856809554cdf73896dc8839a30193 Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Fri, 13 Sep 2024 11:45:32 +0200 Subject: [PATCH 063/112] add tests for search history --- .../e2e/pageObjects/base-run-commands-page.ts | 32 ++++ tests/e2e/pageObjects/workbench-page.ts | 52 ++---- .../search-and-query/commands-history.e2e.ts | 164 ++++++++++++++++++ .../search-and-query/raw-mode.e2e.ts | 69 ++++++++ .../workbench/command-results.e2e.ts | 3 +- 5 files changed, 278 insertions(+), 42 deletions(-) create mode 100644 tests/e2e/tests/web/regression/search-and-query/commands-history.e2e.ts create mode 100644 tests/e2e/tests/web/regression/search-and-query/raw-mode.e2e.ts diff --git a/tests/e2e/pageObjects/base-run-commands-page.ts b/tests/e2e/pageObjects/base-run-commands-page.ts index b9c965ecfa..d40b647560 100644 --- a/tests/e2e/pageObjects/base-run-commands-page.ts +++ b/tests/e2e/pageObjects/base-run-commands-page.ts @@ -25,12 +25,30 @@ export class BaseRunCommandsPage extends InstancePage { executedCommandTitle = Selector('[data-testid=query-card-tooltip-anchor]', { timeout: 500 }); queryResult = Selector('[data-testid=query-common-result]'); queryInputScriptArea = Selector('[data-testid=query-input-container] .view-line'); + parametersAnchor = Selector('[data-testid=parameters-anchor]'); + + iframe = Selector('[data-testid=pluginIframe]'); + + //OPTIONS + selectViewType = Selector('[data-testid=select-view-type]'); + queryTableResult = Selector('[data-testid^=query-table-result-]'); + textViewTypeOption = Selector('[data-test-subj^=view-type-option-Text]'); + tableViewTypeOption = Selector('[data-test-subj^=view-type-option-Plugin]'); + + clearResultsBtn = Selector('[data-testid=clear-history-btn]'); + rawModeIcon = Selector('[data-testid=raw-mode-tooltip]'); cssQueryCardCommand = '[data-testid=query-card-command]'; cssQueryCardContainer = '[data-testid^="query-card-container-"]'; cssQueryTextResult = '[data-testid=query-cli-result]'; cssReRunCommandButton = '[data-testid=re-run-command]'; cssDeleteCommandButton = '[data-testid=delete-command]'; + cssJsonViewTypeOption = '[data-testid=view-type-selected-Plugin-client-list__json-view]'; + cssQueryTableResult = '[data-testid^=query-table-result-]'; + cssTableViewTypeOption = '[data-testid=view-type-selected-Plugin-redisearch__redisearch]'; + cssCommandExecutionDateTime = '[data-testid=command-execution-date-time]'; + + queryTextResult = Selector(this.cssQueryTextResult); getTutorialLinkLocator = (tutorialName: string): Selector => Selector(`[data-testid=query-tutorials-link_${tutorialName}]`); @@ -70,4 +88,18 @@ export class BaseRunCommandsPage extends InstancePage { const actualCommandResult = await this.queryCardContainer.nth(childNum).find(this.cssQueryTextResult).textContent; await t.expect(actualCommandResult).contains(result, 'Actual command result is not equal to executed'); } + + // Select Text view option in Workbench results + async selectViewTypeText(): Promise { + await t + .click(this.selectViewType) + .click(this.textViewTypeOption); + } + + // Select Table view option in Workbench results + async selectViewTypeTable(): Promise { + await t + .click(this.selectViewType) + .doubleClick(this.tableViewTypeOption); + } } diff --git a/tests/e2e/pageObjects/workbench-page.ts b/tests/e2e/pageObjects/workbench-page.ts index bf30d3938d..3d4987677d 100644 --- a/tests/e2e/pageObjects/workbench-page.ts +++ b/tests/e2e/pageObjects/workbench-page.ts @@ -5,16 +5,12 @@ export class WorkbenchPage extends BaseRunCommandsPage { //CSS selectors cssSelectorPaginationButtonPrevious = '[data-test-subj=pagination-button-previous]'; cssSelectorPaginationButtonNext = '[data-test-subj=pagination-button-next]'; - cssTableViewTypeOption = '[data-testid=view-type-selected-Plugin-redisearch__redisearch]'; - cssClientListViewTypeOption = '[data-testid=view-type-selected-Plugin-client-list__clients-list]'; - cssJsonViewTypeOption = '[data-testid=view-type-selected-Plugin-client-list__json-view]'; cssMonacoCommandPaletteLine = '[aria-label="Command Palette"]'; cssWorkbenchCommandInHistory = '[data-testid=wb-command]'; - cssQueryTableResult = '[data-testid^=query-table-result-]'; queryGraphContainer = '[data-testid=query-graph-container]'; cssQueryCardCommand = '[data-testid=query-card-command]'; - cssCommandExecutionDateTime = '[data-testid=command-execution-date-time]'; cssRowInVirtualizedTable = '[data-testid^=row-]'; + cssClientListViewTypeOption = '[data-testid=view-type-selected-Plugin-client-list__clients-list]'; //------------------------------------------------------------------------------------------- //DECLARATION OF SELECTORS //*Declare all elements/components of the relevant page. @@ -36,26 +32,17 @@ export class WorkbenchPage extends BaseRunCommandsPage { queryCardNoModuleButton = Selector('[data-testid=query-card-no-module-button] a'); closeEnablementPage = Selector('[data-testid=enablement-area__page-close]'); groupMode = Selector('[data-testid=btn-change-group-mode]'); - clearResultsBtn = Selector('[data-testid=clear-history-btn]'); + //ICONS noCommandHistoryIcon = Selector('[data-testid=wb_no-results__icon]'); - parametersAnchor = Selector('[data-testid=parameters-anchor]'); groupModeIcon = Selector('[data-testid=group-mode-tooltip]'); - rawModeIcon = Selector('[data-testid=raw-mode-tooltip]'); silentModeIcon = Selector('[data-testid=silent-mode-tooltip]'); - //LINKS - //TEXT INPUTS (also referred to as 'Text fields') - iframe = Selector('[data-testid=pluginIframe]'); //TEXT ELEMENTS queryPluginResult = Selector('[data-testid=query-plugin-result]'); responseInfo = Selector('[class="responseInfo"]'); parsedRedisReply = Selector('[class="parsedRedisReply"]'); - scriptsLines = Selector('[data-testid=query-input-container] .view-lines'); - queryTableResult = Selector('[data-testid^=query-table-result-]'); - queryJsonResult = Selector('[data-testid=json-view]'); mainEditorArea = Selector('[data-testid=main-input-container-area]'); - queryTextResult = Selector(this.cssQueryTextResult); queryColumns = Selector('[data-testid*=query-column-]'); noCommandHistorySection = Selector('[data-testid=wb_no-results]'); noCommandHistoryTitle = Selector('[data-testid=wb_no-results__title]'); @@ -64,37 +51,15 @@ export class WorkbenchPage extends BaseRunCommandsPage { commandExecutionResult = Selector('[data-testid=welcome-page-title]'); commandExecutionResultFailed = Selector('[data-testid=cli-output-response-fail]'); chartViewTypeOptionSelected = Selector('[data-testid=view-type-selected-Plugin-redistimeseries__redistimeseries-chart]'); - //OPTIONS - selectViewType = Selector('[data-testid=select-view-type]'); - textViewTypeOption = Selector('[data-test-subj^=view-type-option-Text]'); + scriptsLines = Selector('[data-testid=query-input-container] .view-lines'); + queryJsonResult = Selector('[data-testid=json-view]'); jsonStringViewTypeOption = Selector('[data-test-subj=view-type-option-Plugin-client-list__json-string-view]'); - tableViewTypeOption = Selector('[data-test-subj^=view-type-option-Plugin]'); + graphViewTypeOption = Selector('[data-test-subj^=view-type-option-Plugin-graph]'); typeSelectedClientsList = Selector('[data-testid=view-type-selected-Plugin-client-list__clients-list]'); viewTypeOptionClientList = Selector('[data-test-subj=view-type-option-Plugin-client-list__clients-list]'); viewTypeOptionsText = Selector('[data-test-subj=view-type-option-Text-default__Text]'); - // Select Text view option in Workbench results - async selectViewTypeText(): Promise { - await t - .click(this.selectViewType) - .click(this.textViewTypeOption); - } - - // Select Json view option in Workbench results - async selectViewTypeJson(): Promise { - await t - .click(this.selectViewType) - .click(this.jsonStringViewTypeOption); - } - - // Select Table view option in Workbench results - async selectViewTypeTable(): Promise { - await t - .click(this.selectViewType) - .doubleClick(this.tableViewTypeOption); - } - // Select view option in Workbench results async selectViewTypeGraph(): Promise { await t @@ -140,4 +105,11 @@ export class WorkbenchPage extends BaseRunCommandsPage { getInternalLinkWithoutManifest(internalLink: string): Selector { return Selector(`[data-testid="internal-link-${internalLink}"]`); } + + // Select Json view option in Workbench results + async selectViewTypeJson(): Promise { + await t + .click(this.selectViewType) + .click(this.jsonStringViewTypeOption); + } } diff --git a/tests/e2e/tests/web/regression/search-and-query/commands-history.e2e.ts b/tests/e2e/tests/web/regression/search-and-query/commands-history.e2e.ts new file mode 100644 index 0000000000..c0dc5acf22 --- /dev/null +++ b/tests/e2e/tests/web/regression/search-and-query/commands-history.e2e.ts @@ -0,0 +1,164 @@ +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { DatabaseHelper } from '../../../../helpers/database'; +import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; +import { SearchAndQueryPage } from '../../../../pageObjects/search-and-query-page'; +import { commonUrl, ossClusterConfig } from '../../../../helpers/conf'; +import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; +import { Common } from '../../../../helpers/common'; + +const myRedisDatabasePage = new MyRedisDatabasePage(); +const searchAndQueryPage = new SearchAndQueryPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); +const browserPage = new BrowserPage(); +const workbenchPage = new WorkbenchPage(); + +const commandForSend1 = 'FT.INFO'; +const commandForSend2 = 'FT._LIST'; +let indexName = Common.generateWord(5); + +const commandsForIndex = [ + `FT.CREATE ${indexName} ON HASH PREFIX 1 product: SCHEMA price NUMERIC SORTABLE`, + 'HMSET product:1 price 20', + 'HMSET product:2 price 100' +]; + +fixture `Command results at Search and Query` + .meta({ type: 'critical_path', rte: rte.standalone }) + .page(commonUrl) + .beforeEach(async t => { + await databaseHelper.acceptLicenseTermsAndAddOSSClusterDatabase(ossClusterConfig); + // Go to Workbench page + await t.click(myRedisDatabasePage.NavigationPanel.browserButton); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); + }) + .afterEach(async t => { + await t.switchToMainWindow(); + await workbenchPage.sendCommandInWorkbench(`FT.DROPINDEX ${indexName} DD`); + await databaseAPIRequests.deleteOSSClusterDatabaseApi(ossClusterConfig); + }); +test('Verify that user can see re-run icon near the already executed command and re-execute the command by clicking on the icon in Workbench page', async t => { + // Send commands + await searchAndQueryPage.sendCommandInWorkbench(commandForSend1); + await searchAndQueryPage.sendCommandInWorkbench(commandForSend2); + const containerOfCommand = await searchAndQueryPage.getCardContainerByCommand(commandForSend1); + const containerOfCommand2 = await searchAndQueryPage.getCardContainerByCommand(commandForSend2); + // Verify that re-run icon is displayed + await t.expect(await searchAndQueryPage.reRunCommandButton.visible).ok('Re-run icon is not displayed'); + // Re-run the last command in results + await t.click(containerOfCommand.find(searchAndQueryPage.cssReRunCommandButton)); + // Verify that command is re-executed + await t.expect(searchAndQueryPage.queryCardCommand.textContent).eql(commandForSend1, 'The command is not re-executed'); + + // Verify that user can see expanded result after command re-run at the top of results table in Workbench + await t.expect(await searchAndQueryPage.queryCardContainer.nth(0).find(searchAndQueryPage.cssQueryTextResult).visible) + .ok('Re-executed command is not expanded'); + await t.expect(searchAndQueryPage.queryCardCommand.nth(0).textContent).eql(commandForSend1, 'The re-executed command is not at the top of results table'); + + // Delete the command from results + await t.click(containerOfCommand2.find(searchAndQueryPage.cssDeleteCommandButton)); + // Verify that user can delete command with result from table with results in Workbench + await t.expect(searchAndQueryPage.queryCardCommand.withExactText(commandForSend2).exists).notOk(`Command ${commandForSend2} is not deleted from table with results`); +}); +test('Verify that user can see the results found in the table view by default for FT.INFO, FT.SEARCH and FT.AGGREGATE', async t => { + const commands = [ + 'FT.INFO', + 'FT.SEARCH', + 'FT.AGGREGATE' + ]; + // Send commands and check table view is default + for(const command of commands) { + await searchAndQueryPage.sendCommandInWorkbench(command); + await t.expect(searchAndQueryPage.queryCardContainer.nth(0).find(searchAndQueryPage.cssTableViewTypeOption).exists).ok(`The table view is not selected by default for command ${command}`); + } +}); +test + .after(async() => { + await searchAndQueryPage.sendCommandInWorkbench(`FT.DROPINDEX ${indexName} DD`); + })('Verify that user can switches between views and see results according to the view rules in Workbench in results', async t => { + indexName = Common.generateWord(5); + const commands = [ + 'hset doc:10 title "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud" url "redis.io" author "Test" rate "undefined" review "0" comment "Test comment"', + `FT.CREATE ${indexName} ON HASH PREFIX 1 doc: SCHEMA title TEXT WEIGHT 5.0 body TEXT url TEXT author TEXT rate TEXT review TEXT comment TEXT`, + `FT.SEARCH ${indexName} * limit 0 10000` + ]; + // Send commands and check table view is default for Search command + for (const command of commands) { + await searchAndQueryPage.sendCommandInWorkbench(command); + } + await t.expect(await searchAndQueryPage.queryCardContainer.nth(0).find(searchAndQueryPage.cssTableViewTypeOption).exists) + .ok('The table view is not selected by default for command FT.SEARCH'); + await t.switchToIframe(searchAndQueryPage.iframe); + await t.expect(await searchAndQueryPage.queryTableResult.visible).ok('The table result is not displayed for command FT.SEARCH'); + // Select Text view and check result + await t.switchToMainWindow(); + await searchAndQueryPage.selectViewTypeText(); + await t.expect(await searchAndQueryPage.queryCardContainer.nth(0).find(searchAndQueryPage.cssQueryTextResult).visible).ok('The result is not displayed in Text view'); + }); + +test('Verify that user can clear all results at once.', async t => { + await t.click(searchAndQueryPage.clearResultsBtn); + await t.expect(searchAndQueryPage.queryTextResult.exists).notOk('Clear all button does not remove commands'); +}); + +test('Verify that user can switches between Table and Text for FT.AGGREGATE and see results corresponding to their views', async t => { + + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await workbenchPage.sendCommandsArrayInWorkbench(commandsForIndex); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); + const aggregateCommand = `FT.Aggregate ${indexName} * GROUPBY 0 REDUCE MAX 1 @price AS max_price`; + + // Send FT.AGGREGATE and switch to Text view + await searchAndQueryPage.sendCommandInWorkbench(aggregateCommand); + await searchAndQueryPage.selectViewTypeText(); + await t.expect(searchAndQueryPage.queryCardContainer.nth(0).find(searchAndQueryPage.cssQueryTextResult).visible).ok('The text view is not switched for command FT.AGGREGATE'); + // Switch to Table view and check result + await searchAndQueryPage.selectViewTypeTable(); + await t.switchToIframe(searchAndQueryPage.iframe); + await t.expect(searchAndQueryPage.queryTableResult.exists).ok('The table view is not switched for command FT.AGGREGATE'); +}); + +test('Verify that user can switches between Table and Text for FT.SEARCH and see results corresponding to their views', async t => { + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await workbenchPage.sendCommandsArrayInWorkbench(commandsForIndex); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); + const searchCommand = `FT.SEARCH ${indexName} *`; + + // Send FT.SEARCH and switch to Text view + await searchAndQueryPage.sendCommandInWorkbench(searchCommand); + await searchAndQueryPage.selectViewTypeText(); + await t.expect(searchAndQueryPage.queryCardContainer.nth(0).find(searchAndQueryPage.cssQueryTextResult).visible).ok('The text view is not switched for command FT.SEARCH'); + // Switch to Table view and check result + await searchAndQueryPage.selectViewTypeTable(); + await t.switchToIframe(searchAndQueryPage.iframe); + await t.expect(searchAndQueryPage.queryTableResult.exists).ok('The table view is not switched for command FT.SEARCH'); +}); +test('Verify that user can switches between Table and Text for FT.INFO and see results corresponding to their views', async t => { + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await workbenchPage.sendCommandsArrayInWorkbench(commandsForIndex); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); + const infoCommand = `FT.INFO ${indexName}`; + + // Send FT.INFO and switch to Text view + await searchAndQueryPage.sendCommandInWorkbench(infoCommand); + await searchAndQueryPage.selectViewTypeText(); + await t.expect(searchAndQueryPage.queryCardContainer.nth(0).find(searchAndQueryPage.cssQueryTextResult).exists).ok('The text view is not switched for command FT.INFO'); + // Switch to Table view and check result + await searchAndQueryPage.selectViewTypeTable(); + await t.switchToIframe(searchAndQueryPage.iframe); + await t.expect(searchAndQueryPage.queryTableResult.exists).ok('The table view is not switched for command FT.INFO'); +}); +test + .meta({ rte: rte.standalone })('Verify that user can see original date and time of command execution in Workbench history after the page update', async t => { + const keyName = Common.generateWord(5); + const command = `set ${keyName} test`; + + // Send command and remember the time + await searchAndQueryPage.sendCommandInWorkbench(command); + const dateTime = await searchAndQueryPage.queryCardContainer.nth(0).find(searchAndQueryPage.cssCommandExecutionDateTime).textContent; + // Wait fo 1 minute, refresh page and check results + await t.wait(60000); + await searchAndQueryPage.reloadPage(); + await t.expect(searchAndQueryPage.queryCardContainer.nth(0).find(searchAndQueryPage.cssCommandExecutionDateTime).textContent).eql(dateTime, 'The original date and time of command execution is not saved after the page update'); + }); + diff --git a/tests/e2e/tests/web/regression/search-and-query/raw-mode.e2e.ts b/tests/e2e/tests/web/regression/search-and-query/raw-mode.e2e.ts new file mode 100644 index 0000000000..32d7ed7a05 --- /dev/null +++ b/tests/e2e/tests/web/regression/search-and-query/raw-mode.e2e.ts @@ -0,0 +1,69 @@ +import { DatabaseHelper } from '../../../../helpers/database'; +import { WorkbenchPage, MyRedisDatabasePage, BrowserPage } from '../../../../pageObjects'; +import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { commonUrl, ossStandaloneConfig, ossStandaloneRedisearch } from '../../../../helpers/conf'; +import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; +import { Common } from '../../../../helpers/common'; +import { SearchAndQueryPage } from '../../../../pageObjects/search-and-query-page'; + +const myRedisDatabasePage = new MyRedisDatabasePage(); +const workbenchPage = new WorkbenchPage(); +const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); +const searchAndQueryPage = new SearchAndQueryPage(); + +const indexName = Common.generateWord(5); +const unicodeValue = '山女馬 / 马目 abc 123'; + +const databasesForAdding = + { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port, databaseName: 'testDB2' }; + +fixture `Search and Query Raw mode` + .meta({ type: 'critical_path', rte: rte.standalone }) + .page(commonUrl); + +test + .before(async t => { + await databaseHelper.acceptLicenseTerms(); + await databaseAPIRequests.addNewStandaloneDatabaseApi( + databasesForAdding); + await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); + // Go to Workbench page + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + }) + .after(async t => { + // Drop index, documents and database + await t.switchToMainWindow(); + await workbenchPage.sendCommandInWorkbench(`FT.DROPINDEX ${indexName} DD`); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneRedisearch); + + })('Display Raw mode for plugins and save state', async t => { + const commandsForSend = [ + `FT.CREATE ${indexName} ON HASH PREFIX 1 product: SCHEMA name TEXT`, + `HMSET product:1 name "${unicodeValue}"` + ]; + const commandSearch = `FT.SEARCH ${indexName} "${unicodeValue}"`; + + await workbenchPage.sendCommandsArrayInWorkbench(commandsForSend); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); + // Send command in raw mode + await t.click(searchAndQueryPage.rawModeBtn); + await searchAndQueryPage.sendCommandInWorkbench(commandSearch); + // Check the FT.SEARCH result + await t.switchToIframe(workbenchPage.iframe); + let name = workbenchPage.queryTableResult.withText(unicodeValue); + await t.expect(name.exists).ok('The added key name field is not converted to Unicode'); + await t.switchToMainWindow(); + await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton); + await myRedisDatabasePage.clickOnDBByName(databasesForAdding.databaseName); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + + await workbenchPage.sendCommandsArrayInWorkbench(commandsForSend); + await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); + await searchAndQueryPage.sendCommandInWorkbench(commandSearch); + // Check the FT.SEARCH result + await t.switchToIframe(workbenchPage.iframe); + name = workbenchPage.queryTableResult.withText(unicodeValue); + await t.expect(name.exists).ok('The added key name field is not converted to Unicode'); + }); diff --git a/tests/e2e/tests/web/regression/workbench/command-results.e2e.ts b/tests/e2e/tests/web/regression/workbench/command-results.e2e.ts index 40b725c1e4..88b58e051e 100644 --- a/tests/e2e/tests/web/regression/workbench/command-results.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/command-results.e2e.ts @@ -1,12 +1,11 @@ import { DatabaseHelper } from '../../../../helpers/database'; -import { WorkbenchPage, MyRedisDatabasePage, BrowserPage } from '../../../../pageObjects'; +import { WorkbenchPage, BrowserPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneRedisearch } from '../../../../helpers/conf'; import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; import { WorkbenchActions } from '../../../../common-actions/workbench-actions'; -const myRedisDatabasePage = new MyRedisDatabasePage(); const workbenchPage = new WorkbenchPage(); const workBenchActions = new WorkbenchActions(); const databaseHelper = new DatabaseHelper(); From 55a8abba039feaca4a4754b2ca11cf8707c1caa9 Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Fri, 13 Sep 2024 11:49:04 +0200 Subject: [PATCH 064/112] remove unused meta --- .../search-and-query/commands-history.e2e.ts | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/tests/e2e/tests/web/regression/search-and-query/commands-history.e2e.ts b/tests/e2e/tests/web/regression/search-and-query/commands-history.e2e.ts index c0dc5acf22..f41f2daea5 100644 --- a/tests/e2e/tests/web/regression/search-and-query/commands-history.e2e.ts +++ b/tests/e2e/tests/web/regression/search-and-query/commands-history.e2e.ts @@ -148,17 +148,16 @@ test('Verify that user can switches between Table and Text for FT.INFO and see r await t.switchToIframe(searchAndQueryPage.iframe); await t.expect(searchAndQueryPage.queryTableResult.exists).ok('The table view is not switched for command FT.INFO'); }); -test - .meta({ rte: rte.standalone })('Verify that user can see original date and time of command execution in Workbench history after the page update', async t => { - const keyName = Common.generateWord(5); - const command = `set ${keyName} test`; +test('Verify that user can see original date and time of command execution in Workbench history after the page update', async t => { + const keyName = Common.generateWord(5); + const command = `set ${keyName} test`; - // Send command and remember the time - await searchAndQueryPage.sendCommandInWorkbench(command); - const dateTime = await searchAndQueryPage.queryCardContainer.nth(0).find(searchAndQueryPage.cssCommandExecutionDateTime).textContent; - // Wait fo 1 minute, refresh page and check results - await t.wait(60000); - await searchAndQueryPage.reloadPage(); - await t.expect(searchAndQueryPage.queryCardContainer.nth(0).find(searchAndQueryPage.cssCommandExecutionDateTime).textContent).eql(dateTime, 'The original date and time of command execution is not saved after the page update'); - }); + // Send command and remember the time + await searchAndQueryPage.sendCommandInWorkbench(command); + const dateTime = await searchAndQueryPage.queryCardContainer.nth(0).find(searchAndQueryPage.cssCommandExecutionDateTime).textContent; + // Wait fo 1 minute, refresh page and check results + await t.wait(60000); + await searchAndQueryPage.reloadPage(); + await t.expect(searchAndQueryPage.queryCardContainer.nth(0).find(searchAndQueryPage.cssCommandExecutionDateTime).textContent).eql(dateTime, 'The original date and time of command execution is not saved after the page update'); +}); From 2499a3df25b8342430f32ef804976f3a877fcc4f Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Fri, 13 Sep 2024 17:03:37 +0200 Subject: [PATCH 065/112] #RI-6086 - fix number of args next to count #RI-6088 - fix multiple arguments --- .../pages/search/components/query/Query.tsx | 4 +- .../ui/src/pages/search/utils/query.ts | 51 ++++++--- .../pages/search/utils/tests/query.spec.ts | 106 +++++++++++------- 3 files changed, 105 insertions(+), 56 deletions(-) diff --git a/redisinsight/ui/src/pages/search/components/query/Query.tsx b/redisinsight/ui/src/pages/search/components/query/Query.tsx index 96f613b1e3..28c0e1a582 100644 --- a/redisinsight/ui/src/pages/search/components/query/Query.tsx +++ b/redisinsight/ui/src/pages/search/components/query/Query.tsx @@ -18,7 +18,7 @@ import { CursorContext, FoundCommandArgument, SearchCommand, TokenType } from 'u import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' import { fetchRedisearchInfoAction } from 'uiSrc/slices/browser/redisearch' import { getRediSearchMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokens' -import { installRedisearchTheme, RedisearchMonacoTheme } from 'uiSrc/utils/monaco/monacoThemes' +import { installRedisearchTheme } from 'uiSrc/utils/monaco/monacoThemes' import { useDebouncedEffect } from 'uiSrc/services' import { options, DefinedArgumentName, FIELD_START_SYMBOL } from './constants' import { @@ -346,7 +346,7 @@ const Query = (props: Props) => { value={value} onChange={onChange} language={MonacoLanguage.RediSearch} - theme={theme === Theme.Dark ? RedisearchMonacoTheme.dark : RedisearchMonacoTheme.light} + theme={theme === Theme.Dark ? 'dark' : 'light'} options={options} editorDidMount={editorDidMount} /> diff --git a/redisinsight/ui/src/pages/search/utils/query.ts b/redisinsight/ui/src/pages/search/utils/query.ts index 921c1f5674..ab6a1ae2a6 100644 --- a/redisinsight/ui/src/pages/search/utils/query.ts +++ b/redisinsight/ui/src/pages/search/utils/query.ts @@ -1,6 +1,6 @@ /* eslint-disable no-continue */ -import { toNumber } from 'lodash' +import { isNumber, toNumber } from 'lodash' import { generateArgsNames, Maybe, Nullable } from 'uiSrc/utils' import { CommandProvider } from 'uiSrc/constants' import { ArgName, FoundCommandArgument, SearchCommand, SearchCommandTree, TokenType } from '../types' @@ -140,18 +140,24 @@ const findStopArgumentInQuery = ( ): { restArguments: SearchCommand[] stopArgIndex: number + argumentsIntered?: number isBlocked: boolean } => { let currentCommandArgIndex = 0 + let argumentsIntered = 0 let isBlockedOnCommand = false let multipleIndexStart = 0 let multipleCountNumber = 0 - const moveToNextCommandArg = () => currentCommandArgIndex++ + const moveToNextCommandArg = () => { + currentCommandArgIndex++ + argumentsIntered++ + } const blockCommand = () => { isBlockedOnCommand = true } const unBlockCommand = () => { isBlockedOnCommand = false } const skipArg = () => { + argumentsIntered -= 1 moveToNextCommandArg() unBlockCommand() } @@ -165,15 +171,16 @@ const findStopArgumentInQuery = ( continue } - if ( - !isBlockedOnCommand - && currentCommandArg?.token - && currentCommandArg.optional - && currentCommandArg.token !== arg.toUpperCase() - ) { - moveToNextCommandArg() - skipArg() - continue + if (!isBlockedOnCommand && currentCommandArg.optional) { + const isNotToken = currentCommandArg?.token && currentCommandArg.token !== arg.toUpperCase() + const isNotOneOfToken = currentCommandArg?.type === TokenType.OneOf + && currentCommandArg?.arguments?.every(({ token }) => token !== arg.toUpperCase()) + + if (isNotToken || isNotOneOfToken) { + moveToNextCommandArg() + skipArg() + continue + } } // if we are on token - that requires one more argument @@ -183,16 +190,23 @@ const findStopArgumentInQuery = ( } if (currentCommandArg?.type === TokenType.Block) { - // if block is multiple - we duplicate nArgs inner arguments let blockArguments = currentCommandArg.arguments - + const nArgs = toNumber(queryArgs[i - 1]) || 0 + // if block is multiple - we duplicate nArgs inner arguments if (currentCommandArg?.multiple) { - const nArgs = toNumber(queryArgs[i - 1]) || 0 blockArguments = Array(nArgs).fill(currentCommandArg.arguments).flat() } const blockSuggestion = findStopArgumentInQuery(queryArgs.slice(i), blockArguments) const stopArg = blockSuggestion.restArguments?.[blockSuggestion.stopArgIndex] + const { argumentsIntered } = blockSuggestion + + if (isNumber(argumentsIntered) && argumentsIntered >= nArgs) { + i += queryArgs.slice(i).length - 1 + skipArg() + continue + } + if (blockSuggestion.isBlocked || stopArg) return blockSuggestion i += queryArgs.slice(i).length - 1 @@ -248,6 +262,7 @@ const findStopArgumentInQuery = ( return { restArguments: restCommandArgs, stopArgIndex: currentCommandArgIndex, + argumentsIntered, isBlocked: isBlockedOnCommand } } @@ -379,7 +394,13 @@ export const fillArgsByType = (args: SearchCommand[], expandBlock = true): Searc const currentArg = args[i] if (expandBlock && currentArg.type === TokenType.OneOf) result.push(...(currentArg?.arguments || [])) - if (currentArg.type === TokenType.Block) result.push(currentArg.arguments?.[0] as SearchCommand) + if (currentArg.type === TokenType.Block) { + result.push({ + multiple: currentArg.multiple, + optional: currentArg.optional, + ...(currentArg?.arguments?.[0] as SearchCommand || {}), + }) + } if (currentArg.token) result.push(currentArg) } diff --git a/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts b/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts index f356961a93..8dc839f048 100644 --- a/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts +++ b/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts @@ -111,6 +111,8 @@ const ftAggreageTests = [ name: 'function', token: 'REDUCE', type: 'string', + multiple: true, + optional: true, parent: { name: 'groupby', type: 'block', @@ -144,6 +146,27 @@ const ftAggreageTests = [ parent: expect.any(Object) } }, + { + args: ['""', '""', 'GROUPBY', '2', 'p1', 'p2', 'REDUCE', 'f', '1', 'AS', 'name'], + result: { + stopArg: undefined, + append: [ + [ + { + name: 'function', + token: 'REDUCE', + type: 'string', + multiple: true, + optional: true, + parent: expect.any(Object) + } + ] + ], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, { args: ['index', '"query"', 'SORTBY'], result: { @@ -158,40 +181,29 @@ const ftAggreageTests = [ args: ['index', '"query"', 'SORTBY', '1', 'p1'], result: { stopArg: { - name: 'order', - type: 'oneof', - arguments: [ - { - name: 'asc', - type: 'pure-token', - token: 'ASC' - }, + name: 'num', + type: 'integer', + token: 'MAX', + optional: true + }, + append: [ + [ { - name: 'desc', - type: 'pure-token', - token: 'DESC' + name: 'num', + type: 'integer', + token: 'MAX', + optional: true, + parent: expect.any(Object) } ] - }, - append: [[ - { - name: 'asc', - type: 'pure-token', - token: 'ASC' - }, - { - name: 'desc', - type: 'pure-token', - token: 'DESC' - } - ]], + ], isBlocked: false, - isComplete: false, + isComplete: true, parent: expect.any(Object) } }, { - args: ['index', '"query"', 'SORTBY', '1', 'p1', 'ASC'], + args: ['index', '"query"', 'SORTBY', '2', 'p1', 'ASC'], result: { stopArg: { name: 'num', @@ -239,7 +251,7 @@ const ftAggreageTests = [ } }, { - args: ['index', '"query"', 'SORTBY', '1', 'p1', 'ASC', 'MAX'], + args: ['index', '"query"', 'SORTBY', '2', 'p1', 'ASC', 'MAX'], result: { stopArg: { name: 'num', @@ -313,7 +325,8 @@ const ftSearchTests = [ name: 'count', type: 'string', token: 'FIELDS', - parent: expect.any(Object) + parent: expect.any(Object), + optional: true }, { name: 'num', @@ -415,6 +428,17 @@ const ftSearchTests = [ parent: expect.any(Object) } }, + { + args: ['', '', 'RETURN', '1', 'iden'], + result: { + stopArg: undefined, + // TODO: append may have AS token, since it is optional - we skip for now + append: [], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, { args: ['', '', 'RETURN', '2', 'iden'], result: { @@ -424,7 +448,6 @@ const ftSearchTests = [ token: 'AS', optional: true }, - // TODO: append may have AS token, since it is optional - we skip for now append: [[]], isBlocked: false, isComplete: false, @@ -434,20 +457,15 @@ const ftSearchTests = [ { args: ['', '', 'RETURN', '2', 'iden', 'iden'], result: { - stopArg: { - name: 'property', - type: 'string', - token: 'AS', - optional: true - }, - append: [[]], + stopArg: undefined, + append: [], isBlocked: false, isComplete: true, parent: expect.any(Object) } }, { - args: ['', '', 'RETURN', '2', 'iden', 'iden', 'AS'], + args: ['', '', 'RETURN', '3', 'iden', 'iden'], result: { stopArg: { name: 'property', @@ -455,12 +473,22 @@ const ftSearchTests = [ token: 'AS', optional: true }, - append: [], - isBlocked: true, + append: [[]], + isBlocked: false, isComplete: false, parent: expect.any(Object) } }, + { + args: ['', '', 'RETURN', '3', 'iden', 'iden', 'AS', 'iden2'], + result: { + stopArg: undefined, + append: [], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, { args: ['', '', 'SORTBY', 'f'], result: { From 8ade5c89d026f5ca120a37aa23200fcd2bed4da4 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Sat, 14 Sep 2024 17:03:48 +0200 Subject: [PATCH 066/112] revert theme --- redisinsight/ui/src/pages/search/components/query/Query.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/redisinsight/ui/src/pages/search/components/query/Query.tsx b/redisinsight/ui/src/pages/search/components/query/Query.tsx index 28c0e1a582..96f613b1e3 100644 --- a/redisinsight/ui/src/pages/search/components/query/Query.tsx +++ b/redisinsight/ui/src/pages/search/components/query/Query.tsx @@ -18,7 +18,7 @@ import { CursorContext, FoundCommandArgument, SearchCommand, TokenType } from 'u import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' import { fetchRedisearchInfoAction } from 'uiSrc/slices/browser/redisearch' import { getRediSearchMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokens' -import { installRedisearchTheme } from 'uiSrc/utils/monaco/monacoThemes' +import { installRedisearchTheme, RedisearchMonacoTheme } from 'uiSrc/utils/monaco/monacoThemes' import { useDebouncedEffect } from 'uiSrc/services' import { options, DefinedArgumentName, FIELD_START_SYMBOL } from './constants' import { @@ -346,7 +346,7 @@ const Query = (props: Props) => { value={value} onChange={onChange} language={MonacoLanguage.RediSearch} - theme={theme === Theme.Dark ? 'dark' : 'light'} + theme={theme === Theme.Dark ? RedisearchMonacoTheme.dark : RedisearchMonacoTheme.light} options={options} editorDidMount={editorDidMount} /> From 7855447f26de4f67c709c79c2abe7850f420c69c Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Mon, 16 Sep 2024 13:53:52 +0200 Subject: [PATCH 067/112] #RI-6100 - merge colors --- redisinsight/ui/src/constants/monaco/theme.ts | 62 +++++++++++-------- .../pages/search/components/query/Query.tsx | 5 +- .../ui/src/utils/monaco/monacoThemes.ts | 23 ------- 3 files changed, 36 insertions(+), 54 deletions(-) delete mode 100644 redisinsight/ui/src/utils/monaco/monacoThemes.ts diff --git a/redisinsight/ui/src/constants/monaco/theme.ts b/redisinsight/ui/src/constants/monaco/theme.ts index f7f69e1887..db0e6521fa 100644 --- a/redisinsight/ui/src/constants/monaco/theme.ts +++ b/redisinsight/ui/src/constants/monaco/theme.ts @@ -1,32 +1,5 @@ import { monaco } from 'react-monaco-editor' -export const darkThemeRules = [ - { token: 'function', foreground: 'BFBC4E' } -] - -export const lightThemeRules = [ - { token: 'function', foreground: '795E26' } -] - -export enum MonacoThemes { - Dark = 'dark', - Light = 'light' -} - -export const darkTheme: monaco.editor.IStandaloneThemeData = { - base: 'vs-dark', - inherit: true, - rules: darkThemeRules, - colors: {} -} - -export const lightTheme: monaco.editor.IStandaloneThemeData = { - base: 'vs', - inherit: true, - rules: lightThemeRules, - colors: {} -} - export const redisearchDarKThemeRules = [ { token: 'keyword', foreground: '#8094B1', fontStyle: 'bold' }, { token: 'argument.block.0', foreground: '#BDE8D7' }, @@ -62,3 +35,38 @@ export const redisearchLightThemeRules = [ { token: 'query.operator', foreground: '#B9F0F3' }, { token: 'function', foreground: '#9E7EE8' }, ] + +export const darkThemeRules = [ + { token: 'function', foreground: 'BFBC4E' }, + ...redisearchDarKThemeRules.map((rule) => ({ + ...rule, + token: `${rule.token}.redisearch` + })) +] + +export const lightThemeRules = [ + { token: 'function', foreground: '795E26' }, + ...redisearchLightThemeRules.map((rule) => ({ + ...rule, + token: `${rule.token}.redisearch` + })) +] + +export enum MonacoThemes { + Dark = 'dark', + Light = 'light' +} + +export const darkTheme: monaco.editor.IStandaloneThemeData = { + base: 'vs-dark', + inherit: true, + rules: darkThemeRules, + colors: {} +} + +export const lightTheme: monaco.editor.IStandaloneThemeData = { + base: 'vs', + inherit: true, + rules: lightThemeRules, + colors: {} +} diff --git a/redisinsight/ui/src/pages/search/components/query/Query.tsx b/redisinsight/ui/src/pages/search/components/query/Query.tsx index 96f613b1e3..f08e55d688 100644 --- a/redisinsight/ui/src/pages/search/components/query/Query.tsx +++ b/redisinsight/ui/src/pages/search/components/query/Query.tsx @@ -18,7 +18,6 @@ import { CursorContext, FoundCommandArgument, SearchCommand, TokenType } from 'u import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' import { fetchRedisearchInfoAction } from 'uiSrc/slices/browser/redisearch' import { getRediSearchMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokens' -import { installRedisearchTheme, RedisearchMonacoTheme } from 'uiSrc/utils/monaco/monacoThemes' import { useDebouncedEffect } from 'uiSrc/services' import { options, DefinedArgumentName, FIELD_START_SYMBOL } from './constants' import { @@ -135,8 +134,6 @@ const Query = (props: Props) => { ({ suggestions: suggestionsRef.current.data }) }).dispose - installRedisearchTheme() - editor.onDidChangeCursorPosition(handleCursorChange) editor.onKeyDown((e: monacoEditor.IKeyboardEvent) => { if (e.keyCode === monacoEditor.KeyCode.Escape && isSuggestionsOpened()) { @@ -346,7 +343,7 @@ const Query = (props: Props) => { value={value} onChange={onChange} language={MonacoLanguage.RediSearch} - theme={theme === Theme.Dark ? RedisearchMonacoTheme.dark : RedisearchMonacoTheme.light} + theme={theme === Theme.Dark ? 'dark' : 'light'} options={options} editorDidMount={editorDidMount} /> diff --git a/redisinsight/ui/src/utils/monaco/monacoThemes.ts b/redisinsight/ui/src/utils/monaco/monacoThemes.ts deleted file mode 100644 index c3fd5313a3..0000000000 --- a/redisinsight/ui/src/utils/monaco/monacoThemes.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { monaco as monacoEditor } from 'react-monaco-editor' -import { redisearchDarKThemeRules, redisearchLightThemeRules } from 'uiSrc/constants/monaco' - -export enum RedisearchMonacoTheme { - dark = 'redisearchDarkTheme', - light = 'redisearchLightTheme' -} - -export const installRedisearchTheme = () => { - monacoEditor.editor.defineTheme(RedisearchMonacoTheme.dark, { - base: 'vs-dark', - inherit: true, - rules: redisearchDarKThemeRules, - colors: {} - }) - - monacoEditor.editor.defineTheme(RedisearchMonacoTheme.light, { - base: 'vs', - inherit: true, - rules: redisearchLightThemeRules, - colors: {} - }) -} From bb11155b1c4f38256952e65d1c85d6bd7dbd699e Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Mon, 16 Sep 2024 16:11:40 +0200 Subject: [PATCH 068/112] #RI-6111 - fix suggestions --- .../ui/src/pages/search/utils/query.ts | 5 +- .../pages/search/utils/tests/query.spec.ts | 96 ++++++++++++++++++- 2 files changed, 94 insertions(+), 7 deletions(-) diff --git a/redisinsight/ui/src/pages/search/utils/query.ts b/redisinsight/ui/src/pages/search/utils/query.ts index ab6a1ae2a6..2d3a030449 100644 --- a/redisinsight/ui/src/pages/search/utils/query.ts +++ b/redisinsight/ui/src/pages/search/utils/query.ts @@ -171,7 +171,7 @@ const findStopArgumentInQuery = ( continue } - if (!isBlockedOnCommand && currentCommandArg.optional) { + if (!isBlockedOnCommand && currentCommandArg?.optional) { const isNotToken = currentCommandArg?.token && currentCommandArg.token !== arg.toUpperCase() const isNotOneOfToken = currentCommandArg?.type === TokenType.OneOf && currentCommandArg?.arguments?.every(({ token }) => token !== arg.toUpperCase()) @@ -315,7 +315,8 @@ export const getArgumentSuggestions = ( // if we finished argument - stopArgument will be undefined, then we get it as token const lastArgument = stopArgument ?? restArguments[0] - const beforeMandatoryOptionalArgs = getAllRestArguments(current, lastArgument, levelArgs, !stopArgument) + const isBlockComplete = !stopArgument && current?.name === lastArgument?.name + const beforeMandatoryOptionalArgs = getAllRestArguments(current, lastArgument, levelArgs, isBlockComplete) const requiredArgsLength = restNotFilledArgs.filter((arg) => !arg.optional).length return { diff --git a/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts b/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts index 8dc839f048..48ea441b65 100644 --- a/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts +++ b/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts @@ -1,10 +1,20 @@ import { addOwnTokenToArgs, findCurrentArgument, generateDetail, splitQueryByArgs } from 'uiSrc/pages/search/utils' -import { SearchCommand } from 'uiSrc/pages/search/types' +import { SearchCommand, TokenType } from 'uiSrc/pages/search/types' import { Maybe } from 'uiSrc/utils' import { MOCKED_SUPPORTED_COMMANDS } from '../../mocks/mocks' const ftSearchCommand = MOCKED_SUPPORTED_COMMANDS['FT.SEARCH'] const ftAggregateCommand = MOCKED_SUPPORTED_COMMANDS['FT.AGGREGATE'] +const COMMANDS = [ + { + name: 'FT.SEARCH', + ...ftSearchCommand + }, + { + name: 'FT.AGGREGATE', + ...ftAggregateCommand + } +] const ftAggreageTests = [ { args: [''], result: null }, @@ -151,6 +161,7 @@ const ftAggreageTests = [ result: { stopArg: undefined, append: [ + [], [ { name: 'function', @@ -289,7 +300,7 @@ const ftAggreageTests = [ args: ['index', '"query"', 'LOAD', '4', '1', '2', '3', '4'], result: { stopArg: undefined, - append: [], + append: [[]], isBlocked: false, isComplete: true, parent: expect.any(Object) @@ -433,7 +444,9 @@ const ftSearchTests = [ result: { stopArg: undefined, // TODO: append may have AS token, since it is optional - we skip for now - append: [], + append: [ + [] + ], isBlocked: false, isComplete: true, parent: expect.any(Object) @@ -458,7 +471,9 @@ const ftSearchTests = [ args: ['', '', 'RETURN', '2', 'iden', 'iden'], result: { stopArg: undefined, - append: [], + append: [ + [] + ], isBlocked: false, isComplete: true, parent: expect.any(Object) @@ -483,7 +498,9 @@ const ftSearchTests = [ args: ['', '', 'RETURN', '3', 'iden', 'iden', 'AS', 'iden2'], result: { stopArg: undefined, - append: [], + append: [ + [] + ], isBlocked: false, isComplete: true, parent: expect.any(Object) @@ -540,9 +557,78 @@ const ftSearchTests = [ parent: expect.any(Object) } }, + { + args: ['', '', 'DIALECT', '1'], + result: { + stopArg: undefined, + append: [ + [] + ], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, +] + +// Common test cases - provides list of suggestions +const commonfindCurrentArgumentCases = [ + { + input: 'FT.SEARCH index "" DIALECT 1', + result: { + stopArg: undefined, + append: expect.any(Array), + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + }, + appendIncludes: ['WITHSCORES', 'VERBATIM', 'FILTER', 'SORTBY', 'RETURN'], + appendNotIncludes: ['DIALECT'] + }, + { + input: 'FT.AGGREGATE "idx:schools" "" GROUPBY 1 p REDUCE AVG 1 a1 AS name ', + result: { + stopArg: undefined, + append: expect.any(Array), + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + }, + appendIncludes: ['REDUCE', 'APPLY', 'SORTBY', 'GROUPBY'], + appendNotIncludes: ['AS'], + }, ] describe('findCurrentArgument', () => { + describe('with list of commands', () => { + commonfindCurrentArgumentCases.forEach(({ input, result, appendIncludes, appendNotIncludes }) => { + it(`should return proper suggestions for ${input}`, () => { + const { args } = splitQueryByArgs(input) + const COMMANDS_LIST = COMMANDS.map((command) => ({ + ...addOwnTokenToArgs(command.name!, command), + token: command.name!, + type: TokenType.Block + })) + + const testResult = findCurrentArgument( + COMMANDS_LIST, + args.flat() + ) + expect(testResult).toEqual(result) + expect( + testResult?.append?.flat()?.map((arg) => arg.token) + ).toEqual( + expect.arrayContaining(appendIncludes) + ) + expect( + testResult?.append?.flat()?.map((arg) => arg.token) + ).toEqual( + expect.not.arrayContaining(appendNotIncludes) + ) + }) + }) + }) + describe('FT.AGGREGATE', () => { ftAggreageTests.forEach(({ args, result: testResult }) => { it(`should return proper suggestions for ${args.join(' ')}`, () => { From c005b70cc11b059f73442e9988946ed25b6ef0a7 Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Wed, 18 Sep 2024 14:05:02 +0200 Subject: [PATCH 069/112] fix vector --- redisinsight/ui/src/pages/search/components/query/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisinsight/ui/src/pages/search/components/query/utils.ts b/redisinsight/ui/src/pages/search/components/query/utils.ts index 6519932cbc..c2503e6d0b 100644 --- a/redisinsight/ui/src/pages/search/components/query/utils.ts +++ b/redisinsight/ui/src/pages/search/components/query/utils.ts @@ -41,7 +41,7 @@ export const addFieldAttribute = (attribute: string, type: string) => { case 'TEXT': return `${attribute}:(\${1:term})` case 'NUMERIC': return `${attribute}:[\${1:range}]` case 'GEO': return `${attribute}:[\${1:lon} \${2:lat} \${3:radius} \${4:unit}]` - case 'VECTOR': return `${attribute} \${1:vector}` + case 'VECTOR': return `${attribute} \\$\${1:vector}` default: return attribute } } From 75a5208c0f397d25910e6af8a9b13baccdb9ae08 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Thu, 19 Sep 2024 10:26:12 +0200 Subject: [PATCH 070/112] #RI-6119 - move browser pages from tabs to menu --- .../ui/src/assets/img/sidebar/search.svg | 3 + .../src/assets/img/sidebar/search_active.svg | 3 + .../main-router/constants/defaultRoutes.ts | 27 +++--- .../main-router/constants/redisStackRoutes.ts | 46 ++++------ .../constants/sub-routes/browserRoutes.ts | 22 ----- .../main-router/constants/sub-routes/index.ts | 2 - .../navigation-menu/NavigationMenu.spec.tsx | 4 +- .../navigation-menu/NavigationMenu.tsx | 46 ++++++++-- redisinsight/ui/src/constants/pages.ts | 7 +- .../top-namespace/TopNamespace.spec.tsx | 2 +- .../database-alias/DatabaseAlias.tsx | 2 +- .../DatabasesListWrapper.tsx | 2 +- .../ui/src/pages/keys/KeysPage.spec.tsx | 85 ------------------- redisinsight/ui/src/pages/keys/KeysPage.tsx | 62 -------------- .../browser-tabs/BrowserTabs.spec.tsx | 40 --------- .../components/browser-tabs/BrowserTabs.tsx | 67 --------------- .../keys/components/browser-tabs/index.ts | 3 - .../browser-tabs/styles.module.scss | 44 ---------- redisinsight/ui/src/pages/keys/index.ts | 3 - .../ui/src/pages/search/styles.module.scss | 2 +- .../wb-view/WBView/styles.module.scss | 2 +- redisinsight/ui/src/utils/routing.ts | 7 +- .../ui/src/utils/tests/routing.spec.ts | 8 +- 23 files changed, 93 insertions(+), 396 deletions(-) create mode 100644 redisinsight/ui/src/assets/img/sidebar/search.svg create mode 100644 redisinsight/ui/src/assets/img/sidebar/search_active.svg delete mode 100644 redisinsight/ui/src/components/main-router/constants/sub-routes/browserRoutes.ts delete mode 100644 redisinsight/ui/src/pages/keys/KeysPage.spec.tsx delete mode 100644 redisinsight/ui/src/pages/keys/KeysPage.tsx delete mode 100644 redisinsight/ui/src/pages/keys/components/browser-tabs/BrowserTabs.spec.tsx delete mode 100644 redisinsight/ui/src/pages/keys/components/browser-tabs/BrowserTabs.tsx delete mode 100644 redisinsight/ui/src/pages/keys/components/browser-tabs/index.ts delete mode 100644 redisinsight/ui/src/pages/keys/components/browser-tabs/styles.module.scss delete mode 100644 redisinsight/ui/src/pages/keys/index.ts diff --git a/redisinsight/ui/src/assets/img/sidebar/search.svg b/redisinsight/ui/src/assets/img/sidebar/search.svg new file mode 100644 index 0000000000..5fe15b2504 --- /dev/null +++ b/redisinsight/ui/src/assets/img/sidebar/search.svg @@ -0,0 +1,3 @@ + + + diff --git a/redisinsight/ui/src/assets/img/sidebar/search_active.svg b/redisinsight/ui/src/assets/img/sidebar/search_active.svg new file mode 100644 index 0000000000..fd5f5821e4 --- /dev/null +++ b/redisinsight/ui/src/assets/img/sidebar/search_active.svg @@ -0,0 +1,3 @@ + + + diff --git a/redisinsight/ui/src/components/main-router/constants/defaultRoutes.ts b/redisinsight/ui/src/components/main-router/constants/defaultRoutes.ts index 3e42509952..600f89c5e5 100644 --- a/redisinsight/ui/src/components/main-router/constants/defaultRoutes.ts +++ b/redisinsight/ui/src/components/main-router/constants/defaultRoutes.ts @@ -1,5 +1,6 @@ import { IRoute, FeatureFlags, PageNames, Pages } from 'uiSrc/constants' import { + BrowserPage, HomePage, InstancePage, RedisCloudDatabasesPage, @@ -8,23 +9,33 @@ import { RedisCloudSubscriptionsPage, RedisClusterDatabasesPage, } from 'uiSrc/pages' -import KeysPage from 'uiSrc/pages/keys' +import WorkbenchPage from 'uiSrc/pages/workbench' import PubSubPage from 'uiSrc/pages/pub-sub' import AnalyticsPage from 'uiSrc/pages/analytics' import RdiPage from 'uiSrc/pages/rdi/home' import RdiInstancePage from 'uiSrc/pages/rdi/instance' import RdiStatisticsPage from 'uiSrc/pages/rdi/statistics' import PipelineManagementPage from 'uiSrc/pages/rdi/pipeline-management' - -import { ANALYTICS_ROUTES, RDI_PIPELINE_MANAGEMENT_ROUTES, BROWSER_ROUTES } from './sub-routes' +import SearchPage from 'uiSrc/pages/search' +import { ANALYTICS_ROUTES, RDI_PIPELINE_MANAGEMENT_ROUTES } from './sub-routes' import COMMON_ROUTES from './commonRoutes' const INSTANCE_ROUTES: IRoute[] = [ { - path: Pages.keys(':instanceId'), - component: KeysPage, - routes: BROWSER_ROUTES, + pageName: PageNames.browser, + path: Pages.browser(':instanceId'), + component: BrowserPage, + }, + { + pageName: PageNames.workbench, + path: Pages.workbench(':instanceId'), + component: WorkbenchPage, + }, + { + pageName: PageNames.search, + path: Pages.search(':instanceId'), + component: SearchPage, }, { pageName: PageNames.pubSub, @@ -36,10 +47,6 @@ const INSTANCE_ROUTES: IRoute[] = [ component: AnalyticsPage, routes: ANALYTICS_ROUTES, }, - { - path: '/:instanceId/workbench', - redirect: (params) => Pages.workbench(params?.instanceId || '') - } ] const RDI_INSTANCE_ROUTES: IRoute[] = [ diff --git a/redisinsight/ui/src/components/main-router/constants/redisStackRoutes.ts b/redisinsight/ui/src/components/main-router/constants/redisStackRoutes.ts index 043937c1ca..9697c50110 100644 --- a/redisinsight/ui/src/components/main-router/constants/redisStackRoutes.ts +++ b/redisinsight/ui/src/components/main-router/constants/redisStackRoutes.ts @@ -2,7 +2,6 @@ import { PageNames, Pages, IRoute } from 'uiSrc/constants' import { BrowserPage, InstancePage, } from 'uiSrc/pages' -import KeysPage from 'uiSrc/pages/keys' import WorkbenchPage from 'uiSrc/pages/workbench' import SlowLogPage from 'uiSrc/pages/slow-log' import PubSubPage from 'uiSrc/pages/pub-sub' @@ -13,27 +12,6 @@ import DatabaseAnalysisPage from 'uiSrc/pages/database-analysis' import SearchPage from 'uiSrc/pages/search' import COMMON_ROUTES from './commonRoutes' -const BROWSER_ROUTES: IRoute[] = [ - { - pageName: PageNames.browser, - protected: true, - path: Pages.browser(':instanceId'), - component: BrowserPage, - }, - { - pageName: PageNames.search, - protected: true, - path: Pages.search(':instanceId'), - component: SearchPage, - }, - { - pageName: PageNames.workbench, - protected: true, - path: Pages.workbench(':instanceId'), - component: WorkbenchPage, - }, -] - const ANALYTICS_ROUTES: IRoute[] = [ { pageName: PageNames.slowLog, @@ -57,9 +35,22 @@ const ANALYTICS_ROUTES: IRoute[] = [ const INSTANCE_ROUTES: IRoute[] = [ { - path: Pages.keys(':instanceId'), - component: KeysPage, - routes: BROWSER_ROUTES, + pageName: PageNames.browser, + protected: true, + path: Pages.browser(':instanceId'), + component: BrowserPage, + }, + { + pageName: PageNames.workbench, + protected: true, + path: Pages.workbench(':instanceId'), + component: WorkbenchPage, + }, + { + pageName: PageNames.search, + protected: true, + path: Pages.search(':instanceId'), + component: SearchPage, }, { pageName: PageNames.pubSub, @@ -73,11 +64,6 @@ const INSTANCE_ROUTES: IRoute[] = [ component: AnalyticsPage, routes: ANALYTICS_ROUTES, }, - // redirect to the new workbench path - { - path: ':instanceId/workbench', - redirect: (params) => Pages.workbench(params?.instanceId || '') - } ] const ROUTES: IRoute[] = [ diff --git a/redisinsight/ui/src/components/main-router/constants/sub-routes/browserRoutes.ts b/redisinsight/ui/src/components/main-router/constants/sub-routes/browserRoutes.ts deleted file mode 100644 index 16c739a1be..0000000000 --- a/redisinsight/ui/src/components/main-router/constants/sub-routes/browserRoutes.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { IRoute, PageNames, Pages } from 'uiSrc/constants' -import BrowserPage from 'uiSrc/pages/browser' -import SearchPage from 'uiSrc/pages/search' -import WorkbenchPage from 'uiSrc/pages/workbench' - -export const BROWSER_ROUTES: IRoute[] = [ - { - pageName: PageNames.browser, - path: Pages.browser(':instanceId'), - component: BrowserPage, - }, - { - pageName: PageNames.search, - path: Pages.search(':instanceId'), - component: SearchPage, - }, - { - pageName: PageNames.workbench, - path: Pages.workbench(':instanceId'), - component: WorkbenchPage, - } -] diff --git a/redisinsight/ui/src/components/main-router/constants/sub-routes/index.ts b/redisinsight/ui/src/components/main-router/constants/sub-routes/index.ts index 5770a6d4ed..68a3732539 100644 --- a/redisinsight/ui/src/components/main-router/constants/sub-routes/index.ts +++ b/redisinsight/ui/src/components/main-router/constants/sub-routes/index.ts @@ -1,9 +1,7 @@ import { ANALYTICS_ROUTES } from './analyticsRoutes' import { RDI_PIPELINE_MANAGEMENT_ROUTES } from './rdiPipelineManagementRoutes' -import { BROWSER_ROUTES } from './browserRoutes' export { ANALYTICS_ROUTES, RDI_PIPELINE_MANAGEMENT_ROUTES, - BROWSER_ROUTES } diff --git a/redisinsight/ui/src/components/navigation-menu/NavigationMenu.spec.tsx b/redisinsight/ui/src/components/navigation-menu/NavigationMenu.spec.tsx index bcdba4a63d..c8a07c32a4 100644 --- a/redisinsight/ui/src/components/navigation-menu/NavigationMenu.spec.tsx +++ b/redisinsight/ui/src/components/navigation-menu/NavigationMenu.spec.tsx @@ -127,9 +127,9 @@ describe('NavigationMenu', () => { })) render() - screen.debug(undefined, 100_000) - expect(screen.getByTestId('browser-page-btn')).toBeTruthy() + expect(screen.getByTestId('workbench-page-btn')).toBeTruthy() + expect(screen.getByTestId('search-page-btn')).toBeTruthy() }) it('should render public routes', () => { diff --git a/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx b/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx index 0e03a29009..ef97859d0d 100644 --- a/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx +++ b/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx @@ -13,7 +13,7 @@ import { EuiToolTip } from '@elastic/eui' import HighlightedFeature, { Props as HighlightedFeatureProps } from 'uiSrc/components/hightlighted-feature/HighlightedFeature' -import { ANALYTICS_ROUTES, BROWSER_ROUTES } from 'uiSrc/components/main-router/constants/sub-routes' +import { ANALYTICS_ROUTES } from 'uiSrc/components/main-router/constants/sub-routes' import { FeatureFlags, PageNames, Pages } from 'uiSrc/constants' import { EXTERNAL_LINKS } from 'uiSrc/constants/links' @@ -24,6 +24,10 @@ import SettingsSVG from 'uiSrc/assets/img/sidebar/settings.svg' import SettingsActiveSVG from 'uiSrc/assets/img/sidebar/settings_active.svg' import BrowserSVG from 'uiSrc/assets/img/sidebar/browser.svg' import BrowserActiveSVG from 'uiSrc/assets/img/sidebar/browser_active.svg' +import WorkbenchSVG from 'uiSrc/assets/img/sidebar/workbench.svg' +import WorkbenchActiveSVG from 'uiSrc/assets/img/sidebar/workbench_active.svg' +import SearchSVG from 'uiSrc/assets/img/sidebar/search.svg' +import SearchActiveSVG from 'uiSrc/assets/img/sidebar/search_active.svg' import SlowLogSVG from 'uiSrc/assets/img/sidebar/slowlog.svg' import SlowLogActiveSVG from 'uiSrc/assets/img/sidebar/slowlog_active.svg' import PubSubSVG from 'uiSrc/assets/img/sidebar/pubsub.svg' @@ -88,10 +92,6 @@ const NavigationMenu = () => { ({ path }) => (`/${last(path.split('/'))}` === activePage) ) - const isBrowserPath = (activePage: string) => !!BROWSER_ROUTES.find( - ({ path }) => (`/${last(path.split('/'))}` === activePage) - ) - const isPipelineManagementPath = () => location.pathname?.startsWith(Pages.rdiPipelineManagement(connectedRdiInstanceId)) @@ -111,9 +111,9 @@ const NavigationMenu = () => { { tooltipText: 'Browser', pageName: PageNames.browser, - isActivePage: isBrowserPath(activePage), + isActivePage: activePage === `/${PageNames.browser}`, ariaLabel: 'Browser page button', - onClick: () => handleGoPage(Pages.keys(connectedInstanceId)), + onClick: () => handleGoPage(Pages.browser(connectedInstanceId)), dataTestId: 'browser-page-btn', connectedInstanceId, getClassName() { @@ -122,6 +122,38 @@ const NavigationMenu = () => { getIconType() { return this.isActivePage ? BrowserSVG : BrowserActiveSVG }, + onboard: ONBOARDING_FEATURES.BROWSER_PAGE + }, + { + tooltipText: 'Search and Query', + pageName: PageNames.search, + ariaLabel: 'Search and Query page button', + onClick: () => handleGoPage(Pages.search(connectedInstanceId)), + dataTestId: 'search-page-btn', + connectedInstanceId, + isActivePage: activePage === `/${PageNames.search}`, + getClassName() { + return cx(styles.navigationButton, { [styles.active]: this.isActivePage }) + }, + getIconType() { + return this.isActivePage ? SearchSVG : SearchActiveSVG + }, + }, + { + tooltipText: 'Workbench', + pageName: PageNames.workbench, + ariaLabel: 'Workbench page button', + onClick: () => handleGoPage(Pages.workbench(connectedInstanceId)), + dataTestId: 'workbench-page-btn', + connectedInstanceId, + isActivePage: activePage === `/${PageNames.workbench}`, + getClassName() { + return cx(styles.navigationButton, { [styles.active]: this.isActivePage }) + }, + getIconType() { + return this.isActivePage ? WorkbenchSVG : WorkbenchActiveSVG + }, + onboard: ONBOARDING_FEATURES.WORKBENCH_PAGE }, { tooltipText: 'Analysis Tools', diff --git a/redisinsight/ui/src/constants/pages.ts b/redisinsight/ui/src/constants/pages.ts index 17b6d7a35c..b063b6cc68 100644 --- a/redisinsight/ui/src/constants/pages.ts +++ b/redisinsight/ui/src/constants/pages.ts @@ -47,10 +47,9 @@ export const Pages = { sentinel, sentinelDatabases: `${sentinel}/databases`, sentinelDatabasesResult: `${sentinel}/databases-result`, - keys: (instanceId: string) => `/${instanceId}/${PageNames.browser}`, - browser: (instanceId: string) => `/${instanceId}/${PageNames.browser}/${PageNames.browser}`, - search: (instanceId: string) => `/${instanceId}/${PageNames.browser}/${PageNames.search}`, - workbench: (instanceId: string) => `/${instanceId}/${PageNames.browser}/${PageNames.workbench}`, + browser: (instanceId: string) => `/${instanceId}/${PageNames.browser}`, + workbench: (instanceId: string) => `/${instanceId}/${PageNames.workbench}`, + search: (instanceId: string) => `/${instanceId}/${PageNames.search}`, pubSub: (instanceId: string) => `/${instanceId}/${PageNames.pubSub}`, analytics: (instanceId: string) => `/${instanceId}/${PageNames.analytics}`, slowLog: (instanceId: string) => `/${instanceId}/${PageNames.analytics}/${PageNames.slowLog}`, diff --git a/redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespace.spec.tsx b/redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespace.spec.tsx index 88d050d31e..9a8a0c1c14 100644 --- a/redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespace.spec.tsx +++ b/redisinsight/ui/src/pages/database-analysis/components/top-namespace/TopNamespace.spec.tsx @@ -166,6 +166,6 @@ describe('TopNamespace', () => { expect(store.getActions()).toEqual(expectedActions) expect(pushMock).toHaveBeenCalledTimes(1) - expect(pushMock).toHaveBeenCalledWith('/instanceId/browser/browser') + expect(pushMock).toHaveBeenCalledWith('/instanceId/browser') }) }) diff --git a/redisinsight/ui/src/pages/home/components/database-alias/DatabaseAlias.tsx b/redisinsight/ui/src/pages/home/components/database-alias/DatabaseAlias.tsx index c3510d03ee..5018d2061e 100644 --- a/redisinsight/ui/src/pages/home/components/database-alias/DatabaseAlias.tsx +++ b/redisinsight/ui/src/pages/home/components/database-alias/DatabaseAlias.tsx @@ -95,7 +95,7 @@ const DatabaseAlias = (props: Props) => { dispatch(setAppContextInitialState()) } dispatch(setConnectedInstanceId(id ?? '')) - history.push(Pages.keys(id ?? '')) + history.push(Pages.browser(id ?? '')) } const handleOpen = (event: any) => { diff --git a/redisinsight/ui/src/pages/home/components/database-list-component/DatabasesListWrapper.tsx b/redisinsight/ui/src/pages/home/components/database-list-component/DatabasesListWrapper.tsx index e413acf9f9..03898384bf 100644 --- a/redisinsight/ui/src/pages/home/components/database-list-component/DatabasesListWrapper.tsx +++ b/redisinsight/ui/src/pages/home/components/database-list-component/DatabasesListWrapper.tsx @@ -125,7 +125,7 @@ const DatabasesListWrapper = ({ width, onEditInstance, editedInstance, onDeleteI } dispatch(setConnectedInstanceId(id)) - history.push(Pages.keys(id)) + history.push(Pages.browser(id)) } const handleCheckConnectToInstance = ( event: React.MouseEvent | React.KeyboardEvent, diff --git a/redisinsight/ui/src/pages/keys/KeysPage.spec.tsx b/redisinsight/ui/src/pages/keys/KeysPage.spec.tsx deleted file mode 100644 index b3b4982efe..0000000000 --- a/redisinsight/ui/src/pages/keys/KeysPage.spec.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import React from 'react' -import reactRouterDom, { BrowserRouter } from 'react-router-dom' -import { cleanup } from '@testing-library/react' -import { cloneDeep } from 'lodash' -import { mockedStore, render } from 'uiSrc/utils/test-utils' - -import { Pages } from 'uiSrc/constants' -import { appContextSelector, setLastPageContext } from 'uiSrc/slices/app/context' -import KeysPage from './KeysPage' - -jest.mock('uiSrc/slices/app/context', () => ({ - ...jest.requireActual('uiSrc/slices/app/context'), - appContextSelector: jest.fn().mockReturnValue({ - lastBrowserPage: '', - }), -})) - -let store: typeof mockedStore -beforeEach(() => { - cleanup() - store = cloneDeep(mockedStore) - store.clearActions() -}) - -const mockedRoutes = [ - { - path: '/123/browser', - }, -] - -describe('KeysPage', () => { - it('should render', () => { - expect( - render( - - - - ) - ).toBeTruthy() - }) - - it('should redirect to the browser by default', () => { - const pushMock = jest.fn() - reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock }) - reactRouterDom.useLocation = jest.fn().mockReturnValue({ pathname: Pages.keys('instanceId') }) - - render( - - - - ) - - expect(pushMock).toBeCalledWith(Pages.browser('instanceId')) - }) - - it('should redirect to the prev page from context', () => { - (appContextSelector as jest.Mock).mockReturnValueOnce({ - lastBrowserPage: Pages.workbench('instanceId') - }) - const pushMock = jest.fn() - reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock }) - reactRouterDom.useLocation = jest.fn().mockReturnValue({ pathname: Pages.keys('instanceId') }) - - render( - - - - ) - - expect(pushMock).toBeCalledWith(Pages.workbench('instanceId')) - }) - - it('should save proper page on unmount', () => { - reactRouterDom.useLocation = jest.fn().mockReturnValue({ pathname: Pages.workbench('instanceId') }) - - const { unmount } = render( - - - - ) - - unmount() - expect(store.getActions()).toEqual([setLastPageContext('/instanceId/browser/workbench')]) - }) -}) diff --git a/redisinsight/ui/src/pages/keys/KeysPage.tsx b/redisinsight/ui/src/pages/keys/KeysPage.tsx deleted file mode 100644 index 9ac514c88d..0000000000 --- a/redisinsight/ui/src/pages/keys/KeysPage.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React, { useEffect, useRef } from 'react' -import { Switch, useHistory, useLocation, useParams } from 'react-router-dom' -import { useDispatch, useSelector } from 'react-redux' -import { IRoute, Pages } from 'uiSrc/constants' -import { appContextSelector, setLastPageContext } from 'uiSrc/slices/app/context' -import RouteWithSubRoutes from 'uiSrc/utils/routerWithSubRoutes' - -import BrowserTabs from './components/browser-tabs' - -export interface Props { - routes: IRoute[] -} - -const KeysPage = (props: Props) => { - const { routes } = props - - const { lastBrowserPage } = useSelector(appContextSelector) - const { instanceId } = useParams<{ instanceId: string }>() - const { pathname } = useLocation() - const pathnameRef = useRef('') - - const history = useHistory() - const dispatch = useDispatch() - - useEffect(() => () => { - dispatch(setLastPageContext(pathnameRef.current)) - }, []) - - useEffect(() => { - if (pathname === Pages.keys(instanceId)) { - // restore current inner page and ignore context (as we store context on unmount) - if (pathnameRef.current && pathnameRef.current !== lastBrowserPage) { - history.push(pathnameRef.current) - return - } - - // restore from context - if (lastBrowserPage) { - history.push(lastBrowserPage) - return - } - - history.push(Pages.browser(instanceId)) - } - - pathnameRef.current = pathname === Pages.keys(instanceId) ? '' : pathname - }, [pathname]) - - return ( - <> - - - {routes.map((route, i) => ( - // eslint-disable-next-line react/no-array-index-key - - ))} - - - ) -} - -export default KeysPage diff --git a/redisinsight/ui/src/pages/keys/components/browser-tabs/BrowserTabs.spec.tsx b/redisinsight/ui/src/pages/keys/components/browser-tabs/BrowserTabs.spec.tsx deleted file mode 100644 index a85228d2e4..0000000000 --- a/redisinsight/ui/src/pages/keys/components/browser-tabs/BrowserTabs.spec.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react' -import reactRouterDom from 'react-router-dom' -import { render, screen, fireEvent } from 'uiSrc/utils/test-utils' - -import BrowserTabs from './BrowserTabs' - -jest.mock('uiSrc/telemetry', () => ({ - ...jest.requireActual('uiSrc/telemetry'), - sendEventTelemetry: jest.fn(), -})) - -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useHistory: () => ({ - push: jest.fn, - }), -})) - -const MOCKED_INSTANCE_ID = 'instanceId' - -describe('BrowserTabs', () => { - it('should render', () => { - expect(render()).toBeTruthy() - }) - - it('should call proper history push after click on tabs', () => { - const pushMock = jest.fn() - reactRouterDom.useHistory = jest.fn().mockReturnValue({ push: pushMock }) - - render() - - fireEvent.click(screen.getByTestId('browser-tab-workbench')) - expect(pushMock).toBeCalledWith('/instanceId/browser/workbench') - - pushMock.mockRestore() - - fireEvent.click(screen.getByTestId('browser-tab-browser')) - expect(pushMock).toBeCalledWith('/instanceId/browser/browser') - }) -}) diff --git a/redisinsight/ui/src/pages/keys/components/browser-tabs/BrowserTabs.tsx b/redisinsight/ui/src/pages/keys/components/browser-tabs/BrowserTabs.tsx deleted file mode 100644 index 4799a72bf5..0000000000 --- a/redisinsight/ui/src/pages/keys/components/browser-tabs/BrowserTabs.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react' -import { EuiBadge, EuiTab, EuiTabs } from '@elastic/eui' -import { useHistory } from 'react-router-dom' -import { Pages } from 'uiSrc/constants/pages' - -import { renderOnboardingTourWithChild } from 'uiSrc/utils/onboarding' -import { ONBOARDING_FEATURES } from 'uiSrc/components/onboarding-features' -import styles from './styles.module.scss' - -export interface Props { - instanceId: string - pathname: string -} - -const BrowserTabs = (props: Props) => { - const { instanceId, pathname } = props - - const history = useHistory() - - const tabs = [ - { - id: 'browser', - title: 'Browse and filter', - page: Pages.browser(instanceId), - onboard: ONBOARDING_FEATURES.BROWSER_PAGE - }, - { - id: 'search', - title: 'Search and query', - page: Pages.search(instanceId), - isBeta: true - }, - { - id: 'workbench', - title: 'Workbench', - page: Pages.workbench(instanceId), - onboard: ONBOARDING_FEATURES.WORKBENCH_PAGE, - } - ] - - const onClickTab = (page: string) => { - history.push(page) - } - - return ( - - {tabs.map(({ id, title, page, onboard, isBeta }) => renderOnboardingTourWithChild( - ( - onClickTab(page)} - data-testid={`browser-tab-${id}`} - > - {title} - {isBeta && ((New!))} - - ), - { options: onboard, anchorPosition: 'downLeft' }, - pathname === page - ))} - - ) -} - -export default BrowserTabs diff --git a/redisinsight/ui/src/pages/keys/components/browser-tabs/index.ts b/redisinsight/ui/src/pages/keys/components/browser-tabs/index.ts deleted file mode 100644 index e1617ed596..0000000000 --- a/redisinsight/ui/src/pages/keys/components/browser-tabs/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import BrowserTabs from './BrowserTabs' - -export default BrowserTabs diff --git a/redisinsight/ui/src/pages/keys/components/browser-tabs/styles.module.scss b/redisinsight/ui/src/pages/keys/components/browser-tabs/styles.module.scss deleted file mode 100644 index 3072523b71..0000000000 --- a/redisinsight/ui/src/pages/keys/components/browser-tabs/styles.module.scss +++ /dev/null @@ -1,44 +0,0 @@ -.tabs { - margin: 0 16px 16px; - background: var(--euiColorEmptyShade); - - .tab { - padding: 8px 16px !important; - margin: 0 !important; - border-radius: 0 !important; - color: var(--euiTextSubduedColor) !important; - border-right: 1px solid var(--separatorColor); - - &:hover { - background: var(--tableLightBorderColor); - } - - &:global(.euiTab-isSelected) { - color: var(--euiTextColor) !important; - background: var(--insightsTriggerBgColor) !important; - } - - &:after { - display: none !important; - } - } - - .betaLabel { - margin-left: 12px; - font-size: 8px !important; - line-height: 12px !important; - background-color: var(--recommendationLiveBorderColor) !important; - border: 1px solid var(--triggerIconActiveColor) !important; - color: #FFF7EA !important; - border-radius: 2px !important; - padding: 0 3px !important; - margin-bottom: 2px; - - transition: transform 250ms ease-in-out; - pointer-events: none; - - :global(.euiBadge__content) { - min-height: 12px !important; - } - } -} diff --git a/redisinsight/ui/src/pages/keys/index.ts b/redisinsight/ui/src/pages/keys/index.ts deleted file mode 100644 index 06362669d9..0000000000 --- a/redisinsight/ui/src/pages/keys/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import KeysPage from './KeysPage' - -export default KeysPage diff --git a/redisinsight/ui/src/pages/search/styles.module.scss b/redisinsight/ui/src/pages/search/styles.module.scss index 359b2462eb..160dcb9b75 100644 --- a/redisinsight/ui/src/pages/search/styles.module.scss +++ b/redisinsight/ui/src/pages/search/styles.module.scss @@ -2,7 +2,7 @@ flex-grow: 1; display: flex; flex-direction: column; - max-height: calc(100% - 50px); + max-height: 100%; } .main { diff --git a/redisinsight/ui/src/pages/workbench/components/wb-view/WBView/styles.module.scss b/redisinsight/ui/src/pages/workbench/components/wb-view/WBView/styles.module.scss index cea9ac8893..5d5312b271 100644 --- a/redisinsight/ui/src/pages/workbench/components/wb-view/WBView/styles.module.scss +++ b/redisinsight/ui/src/pages/workbench/components/wb-view/WBView/styles.module.scss @@ -2,7 +2,7 @@ display: flex; flex-grow: 1; flex-direction: column; - max-height: calc(100% - 50px); + max-height: 100%; } .main { diff --git a/redisinsight/ui/src/utils/routing.ts b/redisinsight/ui/src/utils/routing.ts index 341b33e01a..78ea0d2ec2 100644 --- a/redisinsight/ui/src/utils/routing.ts +++ b/redisinsight/ui/src/utils/routing.ts @@ -1,4 +1,4 @@ -import { IRoute, Pages } from 'uiSrc/constants' +import { IRoute } from 'uiSrc/constants' import { Maybe, Nullable } from 'uiSrc/utils' import DEFAULT_ROUTES from 'uiSrc/components/main-router/constants/defaultRoutes' @@ -43,11 +43,6 @@ export const getRedirectionPage = ( page += '&insights=open' } - // old page - temp redirection - if (page === 'workbench' && databaseId) { - return `${Pages.keys(databaseId)}/${page}` - } - const foundRoute = findRouteByPathname(DEFAULT_ROUTES, pathname) if (!foundRoute) return undefined diff --git a/redisinsight/ui/src/utils/tests/routing.spec.ts b/redisinsight/ui/src/utils/tests/routing.spec.ts index ab019d61b5..adc3a1a3b3 100644 --- a/redisinsight/ui/src/utils/tests/routing.spec.ts +++ b/redisinsight/ui/src/utils/tests/routing.spec.ts @@ -14,10 +14,10 @@ Object.defineProperty(window, 'location', { const databaseId = '1' const getRedirectionPageTests = [ { input: ['settings'], expected: '/settings' }, - { input: ['workbench', databaseId], expected: '/1/browser/workbench' }, - { input: ['/workbench', databaseId], expected: '/1/browser/workbench' }, - { input: ['browser/workbench', databaseId], expected: '/1/browser/workbench' }, - { input: ['/browser/workbench', databaseId], expected: '/1/browser/workbench' }, + { input: ['workbench', databaseId], expected: '/1/workbench' }, + { input: ['/workbench', databaseId], expected: '/1/workbench' }, + { input: ['browser', databaseId], expected: '/1/browser' }, + { input: ['/browser', databaseId], expected: '/1/browser' }, { input: ['/analytics/slowlog', databaseId], expected: '/1/analytics/slowlog' }, { input: ['/analytics/slowlog'], expected: null }, { input: ['/analytics', databaseId], expected: '/1/analytics' }, From e345e8181e03b4c7e01543ae37a88d4ad3c63959 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Thu, 19 Sep 2024 10:46:19 +0200 Subject: [PATCH 071/112] #RI-6119 - add tests, fix test --- .../triggers/insights-trigger/InsightsTrigger.spec.tsx | 2 +- redisinsight/ui/src/utils/tests/routing.spec.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/redisinsight/ui/src/components/triggers/insights-trigger/InsightsTrigger.spec.tsx b/redisinsight/ui/src/components/triggers/insights-trigger/InsightsTrigger.spec.tsx index 2619b6a388..2c4dc23b9f 100644 --- a/redisinsight/ui/src/components/triggers/insights-trigger/InsightsTrigger.spec.tsx +++ b/redisinsight/ui/src/components/triggers/insights-trigger/InsightsTrigger.spec.tsx @@ -86,7 +86,7 @@ describe('InsightsTrigger', () => { databaseId: 'instanceId', provider: 'RE_CLOUD', source: 'overview', - page: '/browser/browser', + page: '/browser', tab: 'tips' }, }); diff --git a/redisinsight/ui/src/utils/tests/routing.spec.ts b/redisinsight/ui/src/utils/tests/routing.spec.ts index adc3a1a3b3..6f11ef1ae2 100644 --- a/redisinsight/ui/src/utils/tests/routing.spec.ts +++ b/redisinsight/ui/src/utils/tests/routing.spec.ts @@ -18,6 +18,8 @@ const getRedirectionPageTests = [ { input: ['/workbench', databaseId], expected: '/1/workbench' }, { input: ['browser', databaseId], expected: '/1/browser' }, { input: ['/browser', databaseId], expected: '/1/browser' }, + { input: ['search', databaseId], expected: '/1/search' }, + { input: ['/search', databaseId], expected: '/1/search' }, { input: ['/analytics/slowlog', databaseId], expected: '/1/analytics/slowlog' }, { input: ['/analytics/slowlog'], expected: null }, { input: ['/analytics', databaseId], expected: '/1/analytics' }, From 50746e3d01404f02d154a846500108b14a6601e1 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Mon, 23 Sep 2024 09:31:40 +0200 Subject: [PATCH 072/112] refactoring --- .../pages/search/components/query/Query.tsx | 148 ++++++++---------- .../search/components/query/constants.ts | 2 + .../pages/search/components/query/utils.ts | 27 +--- .../ui/src/pages/search/utils/query.ts | 4 +- .../pages/search/utils/tests/query.spec.ts | 12 +- 5 files changed, 81 insertions(+), 112 deletions(-) diff --git a/redisinsight/ui/src/pages/search/components/query/Query.tsx b/redisinsight/ui/src/pages/search/components/query/Query.tsx index f08e55d688..d4b1f0c92e 100644 --- a/redisinsight/ui/src/pages/search/components/query/Query.tsx +++ b/redisinsight/ui/src/pages/search/components/query/Query.tsx @@ -47,11 +47,7 @@ const Query = (props: Props) => { const monacoObjects = useRef>(null) const disposeCompletionItemProvider = useRef(() => {}) const disposeSignatureHelpProvider = useRef(() => {}) - const suggestionsRef = useRef<{ - forceShow?: boolean - forceHide: boolean - data: monacoEditor.languages.CompletionItem[] - }>({ forceHide: false, data: [] }) + const suggestionsRef = useRef([]) const helpWidgetRef = useRef({ isOpen: false, parent: null, @@ -92,9 +88,8 @@ const Query = (props: Props) => { const index = selectedIndex.replace(/^(['"])(.*)\1$/, '$2') dispatch(fetchRedisearchInfoAction(index, - (data) => { - const { attributes } = data as any - attributesRef.current = attributes + (data: any) => { + attributesRef.current = data?.attributes || [] })) }, 200, [selectedIndex]) @@ -105,19 +100,6 @@ const Query = (props: Props) => { monaco.languages.register({ id: MonacoLanguage.RediSearch }) monacoObjects.current = { editor, monaco } - suggestionsRef.current = getSuggestions(editor) - - if (value) { - setCursorPositionAtTheEnd(editor) - } else { - const position = editor.getPosition() - - if (position?.column === 1 && position?.lineNumber === 1) { - editor.focus() - triggerSuggestions() - } - } - monaco.languages.setMonarchTokensProvider( MonacoLanguage.RediSearch, getRediSearchMonarchTokensProvider(supportedCommands) @@ -131,7 +113,7 @@ const Query = (props: Props) => { disposeCompletionItemProvider.current?.() disposeCompletionItemProvider.current = monaco.languages.registerCompletionItemProvider(MonacoLanguage.RediSearch, { provideCompletionItems: (): monacoEditor.languages.CompletionList => - ({ suggestions: suggestionsRef.current.data }) + ({ suggestions: suggestionsRef.current }) }).dispose editor.onDidChangeCursorPosition(handleCursorChange) @@ -140,6 +122,18 @@ const Query = (props: Props) => { isEscapedSuggestions.current = true } }) + + suggestionsRef.current = getSuggestions(editor).data + if (value) { + setCursorPositionAtTheEnd(editor) + return + } + + const position = editor.getPosition() + if (position?.column === 1 && position?.lineNumber === 1) { + editor.focus() + triggerSuggestions() + } } const isSuggestionsOpened = () => { @@ -151,7 +145,7 @@ const Query = (props: Props) => { const handleCursorChange = () => { const { editor } = monacoObjects.current || {} - suggestionsRef.current.data = [] + suggestionsRef.current = [] if (!editor) return if (!editor.getSelection()?.isEmpty()) { @@ -159,14 +153,15 @@ const Query = (props: Props) => { return } - suggestionsRef.current = getSuggestions(editor) + const { data, forceHide, forceShow } = getSuggestions(editor) + suggestionsRef.current = data - if (!suggestionsRef.current.forceShow) { + if (!forceShow) { editor.trigger('', 'editor.action.triggerParameterHints', '') return } - if (suggestionsRef.current.data.length) { + if (data.length) { helpWidgetRef.current.isOpen = false triggerSuggestions() return @@ -174,7 +169,7 @@ const Query = (props: Props) => { editor.trigger('', 'editor.action.triggerParameterHints', '') - if (suggestionsRef.current.forceHide) { + if (forceHide) { setTimeout(() => editor?.trigger('', 'hideSuggestWidget', null), 0) } else { helpWidgetRef.current.isOpen = !isSuggestionsOpened() && helpWidgetRef.current.isOpen @@ -182,22 +177,20 @@ const Query = (props: Props) => { } const triggerSuggestions = () => { - const { monaco, editor } = monacoObjects.current || {} - if (!monaco) return - + const { editor } = monacoObjects.current || {} + isEscapedSuggestions.current = false setTimeout(() => editor?.trigger('', 'editor.action.triggerSuggest', { auto: false })) } const updateHelpWidget = (isOpen: boolean, parent?: SearchCommand, currentArg?: SearchCommand) => { - helpWidgetRef.current.isOpen = isOpen - helpWidgetRef.current.parent = parent - helpWidgetRef.current.currentArg = currentArg + helpWidgetRef.current = { isOpen, parent, currentArg } } const getSuggestions = ( editor: monacoEditor.editor.IStandaloneCodeEditor ): { forceHide: boolean + forceShow: boolean data: monacoEditor.languages.CompletionItem[] } => { const position = editor.getPosition() @@ -211,22 +204,23 @@ const Query = (props: Props) => { const range = getRange(position, word) const { args, cursor } = splitQueryByArgs(value, offset) - const { prevCursorChar } = cursor + const { argLeftOffset, prevCursorChar } = cursor + const argBeforeCursor = prevCursorChar ?? (value.substring(argLeftOffset, offset) || '') const allArgs = args.flat() const [beforeOffsetArgs, [currentOffsetArg]] = args - const [firstArg, ...prevArgs] = beforeOffsetArgs + const [firstArg] = beforeOffsetArgs const commandName = (firstArg || currentOffsetArg)?.toUpperCase() const command = commandsSpec?.[commandName] as unknown as SearchCommand + const isCommandSupported = supportedCommands.some(({ name }) => commandName === name) - const isCommandSuppurted = supportedCommands.some(({ name }) => commandName === name) - if (command && !isCommandSuppurted) return asSuggestionsRef([]) - if (!command && position.lineNumber === 1 && position.column === 1) { - return getCommandsSuggestions(supportedCommands, range) - } - + if (command && !isCommandSupported) return asSuggestionsRef([]) if (!command) { + if (position.lineNumber === 1 && position.column === 1) { + return asSuggestionsRef(getCommandsSuggestions(supportedCommands, range), false) + } + helpWidgetRef.current.isOpen = false return asSuggestionsRef([], false) } @@ -235,24 +229,14 @@ const Query = (props: Props) => { setSelectedIndex(allArgs[1] || '') setSelectedCommand(commandName) + const cursorContext: CursorContext = { ...cursor, currentOffsetArg, offset } const foundArg = findCurrentArgument(COMMANDS_LIST, beforeOffsetArgs) - if (prevCursorChar === FIELD_START_SYMBOL) { - helpWidgetRef.current.isOpen = false - return asSuggestionsRef( - getFieldsSuggestions( - attributesRef.current, - range, - false, - foundArg?.stopArg?.name === DefinedArgumentName.query - ), - false - ) - } - const cursorContext: CursorContext = { ...cursor, currentOffsetArg, offset } + if (argBeforeCursor.startsWith(FIELD_START_SYMBOL)) return handleFieldSuggestions(foundArg, range) + switch (foundArg?.stopArg?.name) { case DefinedArgumentName.index: { - return handleIndexSuggestions(command, foundArg, prevArgs.length, currentOffsetArg, range) + return handleIndexSuggestions(command, foundArg, beforeOffsetArgs.length, currentOffsetArg, range) } case DefinedArgumentName.query: { return handleQuerySuggestions(command, foundArg) @@ -263,26 +247,29 @@ const Query = (props: Props) => { } } + const handleFieldSuggestions = (foundArg: Nullable, range: monacoEditor.IRange) => { + const isInQuery = foundArg?.stopArg?.name === DefinedArgumentName.query + const fieldSuggestions = getFieldsSuggestions(attributesRef.current, range, true, isInQuery) + return asSuggestionsRef(fieldSuggestions, true) + } + const handleIndexSuggestions = ( command: SearchCommand, foundArg: FoundCommandArgument, - prevArgsLength: number, + lastArgIndex: number, currentOffsetArg: Nullable, range: monacoEditor.IRange ) => { updateHelpWidget(true, command, foundArg?.stopArg) - if (currentOffsetArg) return asSuggestionsRef([], false) - if (indexesRef.current.length) { - const isNextArgQuery = command?.arguments?.[prevArgsLength + 1]?.name === DefinedArgumentName.query - return asSuggestionsRef(getIndexesSuggestions(indexesRef.current, range, isNextArgQuery)) - } - return asSuggestionsRef([]) + + const isIndex = indexesRef.current.length > 0 + if (!isIndex || currentOffsetArg) return asSuggestionsRef([], !currentOffsetArg) + + const isNextArgQuery = command?.arguments?.[lastArgIndex]?.name === DefinedArgumentName.query + return asSuggestionsRef(getIndexesSuggestions(indexesRef.current, range, isNextArgQuery)) } - const handleQuerySuggestions = ( - command: SearchCommand, - foundArg: FoundCommandArgument, - ) => { + const handleQuerySuggestions = (command: SearchCommand, foundArg: FoundCommandArgument) => { updateHelpWidget(true, command, foundArg?.stopArg) return asSuggestionsRef([], false) } @@ -299,16 +286,14 @@ const Query = (props: Props) => { if (!isCursorInQuotes) return asSuggestionsRef([]) const stringBeforeCursor = value.substring(argLeftOffset, offset) || '' - const { args } = splitQueryByArgs( - stringBeforeCursor.replace(/^["']|["']$/g, ''), - offset - argLeftOffset - ) + const expression = stringBeforeCursor.replace(/^["']|["']$/g, '') + const { args } = splitQueryByArgs(expression, offset - argLeftOffset) const [, [currentArg]] = args const functions = foundArg?.stopArg?.arguments ?? [] - const suggestions = getFunctionsSuggestions(functions, range) const isStartsWithFunction = functions.some(({ token }) => token?.startsWith(currentArg)) + return asSuggestionsRef(suggestions, true, isStartsWithFunction) } @@ -319,20 +304,17 @@ const Query = (props: Props) => { cursorContext: CursorContext, range: monacoEditor.IRange ) => { - if (foundArg?.isBlocked && foundArg?.stopArg?.expression) { - return handleExpressionSuggestions(value, foundArg, cursorContext, range) - } + if (foundArg?.stopArg?.expression) return handleExpressionSuggestions(value, foundArg, cursorContext, range) const { prevCursorChar, nextCursorChar, isCursorInQuotes } = cursorContext - if (isCursorInQuotes || nextCursorChar?.trim()) return asSuggestionsRef([]) - if ((prevCursorChar?.trim() || isCursorInQuotes) && isEscapedSuggestions.current) return asSuggestionsRef([]) - - const { suggestions, forceHide, helpWidgetData } = getGeneralSuggestions( - foundArg, - allArgs, - range, - attributesRef.current - ) + const shouldHideSuggestions = isCursorInQuotes || nextCursorChar || (prevCursorChar && isEscapedSuggestions.current) + if (shouldHideSuggestions) return asSuggestionsRef([]) + + const { + suggestions, + forceHide, + helpWidgetData + } = getGeneralSuggestions(foundArg, allArgs, range, attributesRef.current) if (helpWidgetData) updateHelpWidget(helpWidgetData.isOpen, helpWidgetData.parent, helpWidgetData.currentArg) return asSuggestionsRef(suggestions, forceHide) diff --git a/redisinsight/ui/src/pages/search/components/query/constants.ts b/redisinsight/ui/src/pages/search/components/query/constants.ts index 22dcf561d6..589f6f1484 100644 --- a/redisinsight/ui/src/pages/search/components/query/constants.ts +++ b/redisinsight/ui/src/pages/search/components/query/constants.ts @@ -7,6 +7,8 @@ export const options = merge(defaultMonacoOptions, showWords: false, showIcons: true, insertMode: 'replace', + filterGraceful: false, + matchOnWordStartOnly: true } }) diff --git a/redisinsight/ui/src/pages/search/components/query/utils.ts b/redisinsight/ui/src/pages/search/components/query/utils.ts index c2503e6d0b..581b85c2d2 100644 --- a/redisinsight/ui/src/pages/search/components/query/utils.ts +++ b/redisinsight/ui/src/pages/search/components/query/utils.ts @@ -20,10 +20,10 @@ export const asSuggestionsRef = ( forceShow }) -export const getIndexesSuggestions = (indexes: RedisResponseBuffer[], range: monaco.IRange, nextQoutes = true) => +export const getIndexesSuggestions = (indexes: RedisResponseBuffer[], range: monaco.IRange, nextQuotes = true) => indexes.map((index) => { const value = formatLongName(bufferToString(index)) - const insertQueryQuotes = nextQoutes ? ' "$1"' : '' + const insertQueryQuotes = nextQuotes ? ' "$1"' : '' return { label: value || ' ', @@ -83,15 +83,14 @@ export const getFunctionsSuggestions = (functions: SearchCommand[], range: monac detail: summary })) -export const getCommandsSuggestions = (commands: SearchCommand[], range: monaco.IRange) => asSuggestionsRef( +export const getCommandsSuggestions = (commands: SearchCommand[], range: monaco.IRange) => commands.map((command) => buildSuggestion(command, range, { detail: generateDetail(command), insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, documentation: { value: getCommandMarkdown(command as any) }, - })), false -) + })) export const getMandatoryArgumentSuggestions = ( foundArg: FoundCommandArgument, @@ -147,7 +146,7 @@ export const getGeneralSuggestions = ( foundArg: Nullable, allArgs: string[], range: monacoEditor.IRange, - fields: any[], + fields: any[] ): { suggestions: monacoEditor.languages.CompletionItem[], forceHide?: boolean @@ -156,24 +155,10 @@ export const getGeneralSuggestions = ( if (foundArg && !foundArg.isComplete) { return { suggestions: getMandatoryArgumentSuggestions(foundArg, fields, range), - helpWidgetData: { - isOpen: !!foundArg?.stopArg, - parent: foundArg?.parent, - currentArg: foundArg?.stopArg - } + helpWidgetData: { isOpen: !!foundArg?.stopArg, parent: foundArg?.parent, currentArg: foundArg?.stopArg } } } - return getNextSuggestions(foundArg, allArgs, range) -} - -export const getNextSuggestions = ( - foundArg: Nullable, - allArgs: string[], - range: monacoEditor.IRange -) => { - if (foundArg && !foundArg.isComplete) return { suggestions: [], helpWidgetData: { isOpen: false } } - return { suggestions: getCommandSuggestions(foundArg, allArgs, range), helpWidgetData: { isOpen: false } diff --git a/redisinsight/ui/src/pages/search/utils/query.ts b/redisinsight/ui/src/pages/search/utils/query.ts index 2d3a030449..17fbffcb53 100644 --- a/redisinsight/ui/src/pages/search/utils/query.ts +++ b/redisinsight/ui/src/pages/search/utils/query.ts @@ -91,8 +91,8 @@ export const splitQueryByArgs = (query: string, position: number = 0) => { const cursor = { isCursorInQuotes, - prevCursorChar: query[position - 1], - nextCursorChar: query[position], + prevCursorChar: query[position - 1]?.trim() || '', + nextCursorChar: query[position]?.trim() || '', argLeftOffset, argRightOffset } diff --git a/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts b/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts index 48ea441b65..b6b4a49991 100644 --- a/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts +++ b/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts @@ -667,7 +667,7 @@ const splitQueryByArgsTests: Array<{ argRightOffset: 23, isCursorInQuotes: false, nextCursorChar: 'F', - prevCursorChar: undefined + prevCursorChar: '' } } }, @@ -692,7 +692,7 @@ const splitQueryByArgsTests: Array<{ argLeftOffset: 27, argRightOffset: 39, isCursorInQuotes: false, - nextCursorChar: undefined, + nextCursorChar: '', prevCursorChar: 'S' } } @@ -705,8 +705,8 @@ const splitQueryByArgsTests: Array<{ argLeftOffset: 0, argRightOffset: 0, isCursorInQuotes: false, - nextCursorChar: undefined, - prevCursorChar: ' ' + nextCursorChar: '', + prevCursorChar: '' } } }, @@ -718,8 +718,8 @@ const splitQueryByArgsTests: Array<{ argLeftOffset: 0, argRightOffset: 0, isCursorInQuotes: false, - nextCursorChar: undefined, - prevCursorChar: ' ' + nextCursorChar: '', + prevCursorChar: '' } } } From 05b1c4b9fd081bfb5679f0d23b1dd2bc35a92b1e Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Mon, 23 Sep 2024 10:53:28 +0200 Subject: [PATCH 073/112] fix some issues --- .../ui/src/pages/search/components/query/Query.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/redisinsight/ui/src/pages/search/components/query/Query.tsx b/redisinsight/ui/src/pages/search/components/query/Query.tsx index d4b1f0c92e..5dff9067c1 100644 --- a/redisinsight/ui/src/pages/search/components/query/Query.tsx +++ b/redisinsight/ui/src/pages/search/components/query/Query.tsx @@ -178,7 +178,6 @@ const Query = (props: Props) => { const triggerSuggestions = () => { const { editor } = monacoObjects.current || {} - isEscapedSuggestions.current = false setTimeout(() => editor?.trigger('', 'editor.action.triggerSuggest', { auto: false })) } @@ -204,9 +203,8 @@ const Query = (props: Props) => { const range = getRange(position, word) const { args, cursor } = splitQueryByArgs(value, offset) - const { argLeftOffset, prevCursorChar } = cursor + const { prevCursorChar } = cursor - const argBeforeCursor = prevCursorChar ?? (value.substring(argLeftOffset, offset) || '') const allArgs = args.flat() const [beforeOffsetArgs, [currentOffsetArg]] = args const [firstArg] = beforeOffsetArgs @@ -217,7 +215,7 @@ const Query = (props: Props) => { if (command && !isCommandSupported) return asSuggestionsRef([]) if (!command) { - if (position.lineNumber === 1 && position.column === 1) { + if ((position.lineNumber === 1 && position.column === 1) || beforeOffsetArgs.length === 0) { return asSuggestionsRef(getCommandsSuggestions(supportedCommands, range), false) } @@ -232,7 +230,7 @@ const Query = (props: Props) => { const cursorContext: CursorContext = { ...cursor, currentOffsetArg, offset } const foundArg = findCurrentArgument(COMMANDS_LIST, beforeOffsetArgs) - if (argBeforeCursor.startsWith(FIELD_START_SYMBOL)) return handleFieldSuggestions(foundArg, range) + if (prevCursorChar === FIELD_START_SYMBOL) return handleFieldSuggestions(foundArg, range) switch (foundArg?.stopArg?.name) { case DefinedArgumentName.index: { From 813f4d9f255ceab64ad8a7d84c986c1bd5627bde Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Mon, 23 Sep 2024 13:19:39 +0200 Subject: [PATCH 074/112] #RI-6085 - fix sending query --- .../components/query-wrapper/QueryWrapper.spec.tsx | 13 +++++++++++++ .../components/query-wrapper/QueryWrapper.tsx | 4 +++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.spec.tsx b/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.spec.tsx index 7c3eb91e4c..cb604dcca3 100644 --- a/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.spec.tsx +++ b/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.spec.tsx @@ -75,4 +75,17 @@ describe('Query', () => { } }) }) + + it('should call onSubmit with proper value', () => { + const sendEventTelemetryMock = jest.fn(); + (sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock) + + const onSubmit = jest.fn() + render() + + fireEvent.change(screen.getByTestId('monaco'), { target: { value: 'set\ra\rb\n\nc \nd' } }) + fireEvent.click(screen.getByTestId('btn-submit')) + + expect(onSubmit).toBeCalledWith('set a b c d', undefined, { mode: RunQueryMode.ASCII }) + }) }) diff --git a/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.tsx b/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.tsx index 1db85cde05..93d2c1e3ff 100644 --- a/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.tsx +++ b/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.tsx @@ -77,7 +77,9 @@ const QueryWrapper = (props: Props) => { } const handleSubmit = () => { - const val = value.split('\n').join(' ') + const val = value + .replace(/[\r\n?]{2}|\n\n/g, ' ') + .replace(/\n/g, ' ') if (!val) return onSubmit(val, undefined, { mode: activeRunQueryMode }) From bb373c6f86c92650cd1d3d1cfc873d6f9c670b1d Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Tue, 1 Oct 2024 11:56:50 +0200 Subject: [PATCH 075/112] #RI-6089 - add support ft commands --- .../ui/src/pages/search/SearchPage.tsx | 4 +- .../components/query-wrapper/QueryWrapper.tsx | 15 +- .../pages/search/components/query/Query.tsx | 38 +- .../search/components/query/constants.ts | 39 +- .../ui/src/pages/search/mocks/mocks.ts | 806 +++++++++++++++++- .../ui/src/pages/search/utils/query.ts | 72 +- .../pages/search/utils/tests/query.spec.ts | 619 +------------- .../search/utils/tests/test-cases/common.ts | 177 ++++ .../utils/tests/test-cases/ft-aggregate.ts | 291 +++++++ .../utils/tests/test-cases/ft-search.ts | 283 ++++++ .../search/utils/tests/test-cases/index.ts | 3 + .../monaco/monarchTokens/redisearchTokens.ts | 5 +- .../ui/src/utils/monaco/redisearch/utils.ts | 7 + 13 files changed, 1708 insertions(+), 651 deletions(-) create mode 100644 redisinsight/ui/src/pages/search/utils/tests/test-cases/common.ts create mode 100644 redisinsight/ui/src/pages/search/utils/tests/test-cases/ft-aggregate.ts create mode 100644 redisinsight/ui/src/pages/search/utils/tests/test-cases/ft-search.ts create mode 100644 redisinsight/ui/src/pages/search/utils/tests/test-cases/index.ts diff --git a/redisinsight/ui/src/pages/search/SearchPage.tsx b/redisinsight/ui/src/pages/search/SearchPage.tsx index e69dd222fd..2ff8291b8a 100644 --- a/redisinsight/ui/src/pages/search/SearchPage.tsx +++ b/redisinsight/ui/src/pages/search/SearchPage.tsx @@ -25,7 +25,7 @@ const verticalPanelIds = { const SearchPage = () => { const { name: connectedInstanceName, db } = useSelector(connectedInstanceSelector) - const { commandsArray } = useSelector(appRedisCommandsSelector) + const { commandsArray, spec } = useSelector(appRedisCommandsSelector) const { panelSizes: { vertical } } = useSelector(appContextSearchAndQuery) const [isPageViewSent, setIsPageViewSent] = useState(false) @@ -96,7 +96,7 @@ const SearchPage = () => { initialSize={vertical[verticalPanelIds.firstPanelId] ?? 20} style={{ minHeight: '240px', zIndex: '8' }} > - + , @@ -32,7 +33,7 @@ export interface Props { } const QueryWrapper = (props: Props) => { - const { commandsArray = [], onSubmit } = props + const { commandsArray = [], spec = {}, onSubmit } = props const { id: connectedIndstanceId } = useSelector(connectedInstanceSelector) const { script: scriptContext } = useSelector(appContextSearchAndQuery) @@ -44,10 +45,12 @@ const QueryWrapper = (props: Props) => { const input = useRef(null) const scriptRef = useRef('') - const SUPPORTED_COMMANDS = SUPPORTED_COMMANDS_LIST.map((name) => ({ - ...REDIS_COMMANDS_SPEC[name], - name - })) as unknown as SearchCommand[] + const getCommandByName = (name: string) => + (name in REDIS_COMMANDS_SPEC ? REDIS_COMMANDS_SPEC[name] : (spec[name] || {})) + + const SUPPORTED_COMMANDS = commandsArray + .filter((item) => item.startsWith('FT.')) + .map((name) => ({ ...getCommandByName(name), name })) as unknown as SearchCommand[] const { instanceId } = useParams<{ instanceId: string }>() diff --git a/redisinsight/ui/src/pages/search/components/query/Query.tsx b/redisinsight/ui/src/pages/search/components/query/Query.tsx index 5dff9067c1..0e73ae51bc 100644 --- a/redisinsight/ui/src/pages/search/components/query/Query.tsx +++ b/redisinsight/ui/src/pages/search/components/query/Query.tsx @@ -2,7 +2,8 @@ import React, { useContext, useEffect, useRef, useState } from 'react' import MonacoEditor, { monaco as monacoEditor } from 'react-monaco-editor' import { useDispatch } from 'react-redux' -import { ICommands, MonacoLanguage, Theme } from 'uiSrc/constants' +import { isNumber } from 'lodash' +import { MonacoLanguage, Theme } from 'uiSrc/constants' import { ThemeContext } from 'uiSrc/contexts/themeContext' import { Nullable } from 'uiSrc/utils' import { IEditorMount } from 'uiSrc/pages/workbench/interfaces' @@ -19,7 +20,7 @@ import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' import { fetchRedisearchInfoAction } from 'uiSrc/slices/browser/redisearch' import { getRediSearchMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokens' import { useDebouncedEffect } from 'uiSrc/services' -import { options, DefinedArgumentName, FIELD_START_SYMBOL } from './constants' +import { options, DefinedArgumentName, FIELD_START_SYMBOL, COMMANDS_TO_GET_INDEX_INFO } from './constants' import { getFieldsSuggestions, getIndexesSuggestions, @@ -35,11 +36,10 @@ export interface Props { onChange: (val: string) => void indexes: RedisResponseBuffer[] supportedCommands?: SearchCommand[] - commandsSpec?: ICommands } const Query = (props: Props) => { - const { value, onChange, indexes, supportedCommands = [], commandsSpec } = props + const { value, onChange, indexes, supportedCommands = [] } = props const [selectedCommand, setSelectedCommand] = useState('') const [selectedIndex, setSelectedIndex] = useState('') @@ -209,22 +209,21 @@ const Query = (props: Props) => { const [beforeOffsetArgs, [currentOffsetArg]] = args const [firstArg] = beforeOffsetArgs - const commandName = (firstArg || currentOffsetArg)?.toUpperCase() - const command = commandsSpec?.[commandName] as unknown as SearchCommand - const isCommandSupported = supportedCommands.some(({ name }) => commandName === name) + if ((position.lineNumber === 1 && position.column === 1) || beforeOffsetArgs.length === 0) { + return asSuggestionsRef(getCommandsSuggestions(COMMANDS_LIST, range), false) + } - if (command && !isCommandSupported) return asSuggestionsRef([]) + const commandName = (firstArg || currentOffsetArg)?.toUpperCase() + const command = COMMANDS_LIST.find(({ name }) => commandName === name) if (!command) { - if ((position.lineNumber === 1 && position.column === 1) || beforeOffsetArgs.length === 0) { - return asSuggestionsRef(getCommandsSuggestions(supportedCommands, range), false) - } - helpWidgetRef.current.isOpen = false - return asSuggestionsRef([], false) + return asSuggestionsRef([]) + } + + if (COMMANDS_TO_GET_INDEX_INFO.some((name) => name === commandName)) { + setSelectedIndex(allArgs[1] || '') } - // TODO: change to more generic logic - setSelectedIndex(allArgs[1] || '') setSelectedCommand(commandName) const cursorContext: CursorContext = { ...cursor, currentOffsetArg, offset } @@ -234,7 +233,7 @@ const Query = (props: Props) => { switch (foundArg?.stopArg?.name) { case DefinedArgumentName.index: { - return handleIndexSuggestions(command, foundArg, beforeOffsetArgs.length, currentOffsetArg, range) + return handleIndexSuggestions(command, foundArg, currentOffsetArg, range) } case DefinedArgumentName.query: { return handleQuerySuggestions(command, foundArg) @@ -254,7 +253,6 @@ const Query = (props: Props) => { const handleIndexSuggestions = ( command: SearchCommand, foundArg: FoundCommandArgument, - lastArgIndex: number, currentOffsetArg: Nullable, range: monacoEditor.IRange ) => { @@ -263,7 +261,11 @@ const Query = (props: Props) => { const isIndex = indexesRef.current.length > 0 if (!isIndex || currentOffsetArg) return asSuggestionsRef([], !currentOffsetArg) - const isNextArgQuery = command?.arguments?.[lastArgIndex]?.name === DefinedArgumentName.query + const argumentIndex = command?.arguments + ?.findIndex(({ name }) => foundArg?.stopArg?.name === name) + const isNextArgQuery = isNumber(argumentIndex) + && command?.arguments?.[argumentIndex + 1]?.name === DefinedArgumentName.query + return asSuggestionsRef(getIndexesSuggestions(indexesRef.current, range, isNextArgQuery)) } diff --git a/redisinsight/ui/src/pages/search/components/query/constants.ts b/redisinsight/ui/src/pages/search/components/query/constants.ts index 589f6f1484..53d83e8e3f 100644 --- a/redisinsight/ui/src/pages/search/components/query/constants.ts +++ b/redisinsight/ui/src/pages/search/components/query/constants.ts @@ -12,7 +12,44 @@ export const options = merge(defaultMonacoOptions, } }) -export const SUPPORTED_COMMANDS_LIST = ['FT.SEARCH', 'FT.AGGREGATE', 'FT.PROFILE', 'FT.EXPLAIN'] +export const SUPPORTED_COMMANDS_LIST = [ + 'FT.SEARCH', + 'FT.AGGREGATE', + 'FT.PROFILE', + 'FT.EXPLAIN', + 'FT.INFO', + 'FT._LIST', + 'FT.ALIASADD', + 'FT.ALIASDEL', + 'FT.ALIASUPDATE', + 'FT.ALTER', + 'FT.CONFIG GET', + 'FT.CONFIG SET', + 'FT.CURSOR DEL', + 'FT.CURSOR READ', + 'FT.DICTADD', + 'FT.DICTDEL', +] + +export const COMMANDS_TO_GET_INDEX_INFO = [ + 'FT.SEARCH', + 'FT.AGGREGATE', + 'FT.EXPLAIN', + 'FT.EXPLAINCLI', + 'FT.PROFILE', + 'FT.SPELLCHECK', + 'FT.TAGVALS', + 'FT.ALTER', + 'FT.CREATE' +] + +export const COMPOSITE_ARGS = [ + 'LOAD *', + 'FT.CONFIG GET', + 'FT.CONFIG SET', + 'FT.CURSOR DEL', + 'FT.CURSOR READ', +] export enum DefinedArgumentName { index = 'index', diff --git a/redisinsight/ui/src/pages/search/mocks/mocks.ts b/redisinsight/ui/src/pages/search/mocks/mocks.ts index 01a67f5c19..9ebacd0a47 100644 --- a/redisinsight/ui/src/pages/search/mocks/mocks.ts +++ b/redisinsight/ui/src/pages/search/mocks/mocks.ts @@ -423,7 +423,6 @@ export const MOCKED_SUPPORTED_COMMANDS = { since: '1.0.0', group: 'search' }, - 'FT.AGGREGATE': { summary: 'Run a search query on an index and perform aggregate transformations on the results', complexity: 'O(1)', @@ -673,7 +672,6 @@ export const MOCKED_SUPPORTED_COMMANDS = { since: '1.1.0', group: 'search' }, - 'FT.PROFILE': { summary: 'Performs a `FT.SEARCH` or `FT.AGGREGATE` command and collects performance information', complexity: 'O(N)', @@ -717,5 +715,807 @@ export const MOCKED_SUPPORTED_COMMANDS = { since: '2.2.0', group: 'search', provider: 'redisearch' - } + }, + 'FT.ALIASADD': { + summary: 'Adds an alias to the index', + complexity: 'O(1)', + arguments: [ + { + name: 'alias', + type: 'string' + }, + { + name: 'index', + type: 'string' + } + ], + since: '1.0.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.ALIASDEL': { + summary: 'Deletes an alias from the index', + complexity: 'O(1)', + arguments: [ + { + name: 'alias', + type: 'string' + } + ], + since: '1.0.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.ALIASUPDATE': { + summary: 'Adds or updates an alias to the index', + complexity: 'O(1)', + arguments: [ + { + name: 'alias', + type: 'string' + }, + { + name: 'index', + type: 'string' + } + ], + since: '1.0.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.ALTER': { + summary: 'Adds a new field to the index', + complexity: 'O(N) where N is the number of keys in the keyspace', + arguments: [ + { + name: 'index', + type: 'string' + }, + { + name: 'skipinitialscan', + type: 'pure-token', + token: 'SKIPINITIALSCAN', + optional: true + }, + { + name: 'schema', + type: 'pure-token', + token: 'SCHEMA' + }, + { + name: 'add', + type: 'pure-token', + token: 'ADD' + }, + { + name: 'field', + type: 'string' + }, + { + name: 'options', + type: 'string' + } + ], + since: '1.0.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.CONFIG GET': { + summary: 'Retrieves runtime configuration options', + complexity: 'O(1)', + arguments: [ + { + name: 'option', + type: 'string' + } + ], + since: '1.0.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.CONFIG HELP': { + summary: 'Help description of runtime configuration options', + complexity: 'O(1)', + arguments: [ + { + name: 'option', + type: 'string' + } + ], + since: '1.0.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.CONFIG SET': { + summary: 'Sets runtime configuration options', + complexity: 'O(1)', + arguments: [ + { + name: 'option', + type: 'string' + }, + { + name: 'value', + type: 'string' + } + ], + since: '1.0.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.CREATE': { + summary: 'Creates an index with the given spec', + complexity: 'O(K) at creation where K is the number of fields, O(N) if scanning the keyspace is triggered, where N is the number of keys in the keyspace', + history: [ + [ + '2.0.0', + 'Added `PAYLOAD_FIELD` argument for backward support of `FT.SEARCH` deprecated `WITHPAYLOADS` argument' + ], + [ + '2.0.0', + 'Deprecated `PAYLOAD_FIELD` argument' + ] + ], + arguments: [ + { + name: 'index', + type: 'string' + }, + { + name: 'data_type', + token: 'ON', + type: 'oneof', + arguments: [ + { + name: 'hash', + type: 'pure-token', + token: 'HASH' + }, + { + name: 'json', + type: 'pure-token', + token: 'JSON' + } + ], + optional: true + }, + { + name: 'prefix', + type: 'block', + optional: true, + arguments: [ + { + name: 'count', + type: 'integer', + token: 'PREFIX' + }, + { + name: 'prefix', + type: 'string', + multiple: true + } + ] + }, + { + name: 'filter', + type: 'string', + optional: true, + token: 'FILTER' + }, + { + name: 'default_lang', + type: 'string', + token: 'LANGUAGE', + optional: true + }, + { + name: 'lang_attribute', + type: 'string', + token: 'LANGUAGE_FIELD', + optional: true + }, + { + name: 'default_score', + type: 'double', + token: 'SCORE', + optional: true + }, + { + name: 'score_attribute', + type: 'string', + token: 'SCORE_FIELD', + optional: true + }, + { + name: 'payload_attribute', + type: 'string', + token: 'PAYLOAD_FIELD', + optional: true + }, + { + name: 'maxtextfields', + type: 'pure-token', + token: 'MAXTEXTFIELDS', + optional: true + }, + { + name: 'seconds', + type: 'double', + token: 'TEMPORARY', + optional: true + }, + { + name: 'nooffsets', + type: 'pure-token', + token: 'NOOFFSETS', + optional: true + }, + { + name: 'nohl', + type: 'pure-token', + token: 'NOHL', + optional: true + }, + { + name: 'nofields', + type: 'pure-token', + token: 'NOFIELDS', + optional: true + }, + { + name: 'nofreqs', + type: 'pure-token', + token: 'NOFREQS', + optional: true + }, + { + name: 'stopwords', + type: 'block', + optional: true, + token: 'STOPWORDS', + arguments: [ + { + name: 'count', + type: 'integer' + }, + { + name: 'stopword', + type: 'string', + multiple: true, + optional: true + } + ] + }, + { + name: 'skipinitialscan', + type: 'pure-token', + token: 'SKIPINITIALSCAN', + optional: true + }, + { + name: 'schema', + type: 'pure-token', + token: 'SCHEMA' + }, + { + name: 'field', + type: 'block', + multiple: true, + arguments: [ + { + name: 'field_name', + type: 'string' + }, + { + name: 'alias', + type: 'string', + token: 'AS', + optional: true + }, + { + name: 'field_type', + type: 'oneof', + arguments: [ + { + name: 'text', + type: 'pure-token', + token: 'TEXT' + }, + { + name: 'tag', + type: 'pure-token', + token: 'TAG' + }, + { + name: 'numeric', + type: 'pure-token', + token: 'NUMERIC' + }, + { + name: 'geo', + type: 'pure-token', + token: 'GEO' + }, + { + name: 'vector', + type: 'pure-token', + token: 'VECTOR' + } + ] + }, + { + name: 'withsuffixtrie', + type: 'pure-token', + token: 'WITHSUFFIXTRIE', + optional: true + }, + { + name: 'INDEXEMPTY', + type: 'pure-token', + token: 'INDEXEMPTY', + optional: true + }, + { + name: 'indexmissing', + type: 'pure-token', + token: 'INDEXMISSING', + optional: true + }, + { + name: 'sortable', + type: 'block', + optional: true, + arguments: [ + { + name: 'sortable', + type: 'pure-token', + token: 'SORTABLE' + }, + { + name: 'UNF', + type: 'pure-token', + token: 'UNF', + optional: true + } + ] + }, + { + name: 'noindex', + type: 'pure-token', + token: 'NOINDEX', + optional: true + } + ] + } + ], + since: '1.0.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.CURSOR DEL': { + summary: 'Deletes a cursor', + complexity: 'O(1)', + arguments: [ + { + name: 'index', + type: 'string' + }, + { + name: 'cursor_id', + type: 'integer' + } + ], + since: '1.1.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.CURSOR READ': { + summary: 'Reads from a cursor', + complexity: 'O(1)', + arguments: [ + { + name: 'index', + type: 'string' + }, + { + name: 'cursor_id', + type: 'integer' + }, + { + name: 'read size', + type: 'integer', + optional: true, + token: 'COUNT' + } + ], + since: '1.1.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.DICTADD': { + summary: 'Adds terms to a dictionary', + complexity: 'O(1)', + arguments: [ + { + name: 'dict', + type: 'string' + }, + { + name: 'term', + type: 'string', + multiple: true + } + ], + since: '1.4.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.DICTDEL': { + summary: 'Deletes terms from a dictionary', + complexity: 'O(1)', + arguments: [ + { + name: 'dict', + type: 'string' + }, + { + name: 'term', + type: 'string', + multiple: true + } + ], + since: '1.4.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.DICTDUMP': { + summary: 'Dumps all terms in the given dictionary', + complexity: 'O(N), where N is the size of the dictionary', + arguments: [ + { + name: 'dict', + type: 'string' + } + ], + since: '1.4.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.DROPINDEX': { + summary: 'Deletes the index', + complexity: 'O(1) or O(N) if documents are deleted, where N is the number of keys in the keyspace', + arguments: [ + { + name: 'index', + type: 'string' + }, + { + name: 'delete docs', + type: 'oneof', + arguments: [ + { + name: 'delete docs', + type: 'pure-token', + token: 'DD' + } + ], + optional: true + } + ], + since: '2.0.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.EXPLAIN': { + summary: 'Returns the execution plan for a complex query', + complexity: 'O(1)', + arguments: [ + { + name: 'index', + type: 'string' + }, + { + name: 'query', + type: 'string' + }, + { + name: 'dialect', + type: 'integer', + optional: true, + token: 'DIALECT', + since: '2.4.3' + } + ], + since: '1.0.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.EXPLAINCLI': { + summary: 'Returns the execution plan for a complex query', + complexity: 'O(1)', + arguments: [ + { + name: 'index', + type: 'string' + }, + { + name: 'query', + type: 'string' + }, + { + name: 'dialect', + type: 'integer', + optional: true, + token: 'DIALECT', + since: '2.4.3' + } + ], + since: '1.0.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.INFO': { + summary: 'Returns information and statistics on the index', + complexity: 'O(1)', + arguments: [ + { + name: 'index', + type: 'string' + } + ], + since: '1.0.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.SPELLCHECK': { + summary: 'Performs spelling correction on a query, returning suggestions for misspelled terms', + complexity: 'O(1)', + arguments: [ + { + name: 'index', + type: 'string' + }, + { + name: 'query', + type: 'string' + }, + { + name: 'distance', + token: 'DISTANCE', + type: 'integer', + optional: true + }, + { + name: 'terms', + token: 'TERMS', + type: 'block', + optional: true, + arguments: [ + { + name: 'inclusion', + type: 'oneof', + arguments: [ + { + name: 'include', + type: 'pure-token', + token: 'INCLUDE' + }, + { + name: 'exclude', + type: 'pure-token', + token: 'EXCLUDE' + } + ] + }, + { + name: 'dictionary', + type: 'string' + }, + { + name: 'terms', + type: 'string', + multiple: true, + optional: true + } + ] + }, + { + name: 'dialect', + type: 'integer', + optional: true, + token: 'DIALECT', + since: '2.4.3' + } + ], + since: '1.4.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.SUGADD': { + summary: 'Adds a suggestion string to an auto-complete suggestion dictionary', + complexity: 'O(1)', + history: [ + [ + '2.0.0', + 'Deprecated `PAYLOAD` argument' + ] + ], + arguments: [ + { + name: 'key', + type: 'string' + }, + { + name: 'string', + type: 'string' + }, + { + name: 'score', + type: 'double' + }, + { + name: 'increment score', + type: 'oneof', + arguments: [ + { + name: 'incr', + type: 'pure-token', + token: 'INCR' + } + ], + optional: true + }, + { + name: 'payload', + token: 'PAYLOAD', + type: 'string', + optional: true + } + ], + since: '1.0.0', + group: 'suggestion', + provider: 'redisearch' + }, + 'FT.SUGDEL': { + summary: 'Deletes a string from a suggestion index', + complexity: 'O(1)', + arguments: [ + { + name: 'key', + type: 'string' + }, + { + name: 'string', + type: 'string' + } + ], + since: '1.0.0', + group: 'suggestion', + provider: 'redisearch' + }, + 'FT.SUGGET': { + summary: 'Gets completion suggestions for a prefix', + complexity: 'O(1)', + history: [ + [ + '2.0.0', + 'Deprecated `WITHPAYLOADS` argument' + ] + ], + arguments: [ + { + name: 'key', + type: 'string' + }, + { + name: 'prefix', + type: 'string' + }, + { + name: 'fuzzy', + type: 'pure-token', + token: 'FUZZY', + optional: true + }, + { + name: 'withscores', + type: 'pure-token', + token: 'WITHSCORES', + optional: true + }, + { + name: 'withpayloads', + type: 'pure-token', + token: 'WITHPAYLOADS', + optional: true + }, + { + name: 'max', + token: 'MAX', + type: 'integer', + optional: true + } + ], + since: '1.0.0', + group: 'suggestion', + provider: 'redisearch' + }, + 'FT.SUGLEN': { + summary: 'Gets the size of an auto-complete suggestion dictionary', + complexity: 'O(1)', + arguments: [ + { + name: 'key', + type: 'string' + } + ], + since: '1.0.0', + group: 'suggestion', + provider: 'redisearch' + }, + 'FT.SYNDUMP': { + summary: 'Dumps the contents of a synonym group', + complexity: 'O(1)', + arguments: [ + { + name: 'index', + type: 'string' + } + ], + since: '1.2.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.SYNUPDATE': { + summary: 'Creates or updates a synonym group with additional terms', + complexity: 'O(1)', + arguments: [ + { + name: 'index', + type: 'string' + }, + { + name: 'synonym_group_id', + type: 'string' + }, + { + name: 'skipinitialscan', + type: 'pure-token', + token: 'SKIPINITIALSCAN', + optional: true + }, + { + name: 'term', + type: 'string', + multiple: true + } + ], + since: '1.2.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.TAGVALS': { + summary: 'Returns the distinct tags indexed in a Tag field', + complexity: 'O(N)', + arguments: [ + { + name: 'index', + type: 'string' + }, + { + name: 'field_name', + type: 'string' + } + ], + since: '1.0.0', + group: 'search', + provider: 'redisearch' + }, + 'FT._LIST': { + summary: 'Returns a list of all existing indexes', + complexity: 'O(1)', + since: '2.0.0', + group: 'search', + provider: 'redisearch' + }, } diff --git a/redisinsight/ui/src/pages/search/utils/query.ts b/redisinsight/ui/src/pages/search/utils/query.ts index 17fbffcb53..2b50127bc6 100644 --- a/redisinsight/ui/src/pages/search/utils/query.ts +++ b/redisinsight/ui/src/pages/search/utils/query.ts @@ -3,6 +3,7 @@ import { isNumber, toNumber } from 'lodash' import { generateArgsNames, Maybe, Nullable } from 'uiSrc/utils' import { CommandProvider } from 'uiSrc/constants' +import { COMPOSITE_ARGS } from 'uiSrc/pages/search/components/query/constants' import { ArgName, FoundCommandArgument, SearchCommand, SearchCommandTree, TokenType } from '../types' export const splitQueryByArgs = (query: string, position: number = 0) => { @@ -142,6 +143,7 @@ const findStopArgumentInQuery = ( stopArgIndex: number argumentsIntered?: number isBlocked: boolean + parent?: SearchCommand } => { let currentCommandArgIndex = 0 let argumentsIntered = 0 @@ -173,7 +175,7 @@ const findStopArgumentInQuery = ( if (!isBlockedOnCommand && currentCommandArg?.optional) { const isNotToken = currentCommandArg?.token && currentCommandArg.token !== arg.toUpperCase() - const isNotOneOfToken = currentCommandArg?.type === TokenType.OneOf + const isNotOneOfToken = !currentCommandArg?.token && currentCommandArg?.type === TokenType.OneOf && currentCommandArg?.arguments?.every(({ token }) => token !== arg.toUpperCase()) if (isNotToken || isNotOneOfToken) { @@ -183,37 +185,53 @@ const findStopArgumentInQuery = ( } } - // if we are on token - that requires one more argument - if (currentCommandArg?.token === arg.toUpperCase()) { - blockCommand() - continue - } - if (currentCommandArg?.type === TokenType.Block) { - let blockArguments = currentCommandArg.arguments + let blockArguments = currentCommandArg.arguments ? [...currentCommandArg.arguments] : [] const nArgs = toNumber(queryArgs[i - 1]) || 0 + // if block is multiple - we duplicate nArgs inner arguments - if (currentCommandArg?.multiple) { + if (currentCommandArg?.multiple && nArgs) { blockArguments = Array(nArgs).fill(currentCommandArg.arguments).flat() } + const currentQueryArg = queryArgs.slice(i)?.[0]?.toUpperCase() + const isBlockHasToken = blockArguments?.[0]?.token === currentQueryArg + + if (currentCommandArg.token && !isBlockHasToken && currentQueryArg) { + blockArguments.unshift({ + type: TokenType.PureToken, + token: currentQueryArg + }) + } + const blockSuggestion = findStopArgumentInQuery(queryArgs.slice(i), blockArguments) const stopArg = blockSuggestion.restArguments?.[blockSuggestion.stopArgIndex] const { argumentsIntered } = blockSuggestion - if (isNumber(argumentsIntered) && argumentsIntered >= nArgs) { + if (nArgs && currentCommandArg?.multiple && isNumber(argumentsIntered) && argumentsIntered >= nArgs) { i += queryArgs.slice(i).length - 1 skipArg() continue } - if (blockSuggestion.isBlocked || stopArg) return blockSuggestion + if (blockSuggestion.isBlocked || stopArg) { + return { + ...blockSuggestion, + parent: currentCommandArg + } + } i += queryArgs.slice(i).length - 1 skipArg() continue } + // if we are on token - that requires one more argument + if (currentCommandArg?.token === arg.toUpperCase()) { + blockCommand() + continue + } + if (currentCommandArg?.name === ArgName.NArgs) { const numberOfArgs = toNumber(arg) @@ -283,9 +301,11 @@ export const getArgumentSuggestions = ( const { restArguments, stopArgIndex, - isBlocked: isWasBlocked + isBlocked: isWasBlocked, + parent } = findStopArgumentInQuery(tokenArgs, pastCommandArgs) + const prevArg = restArguments[stopArgIndex - 1] const stopArgument = restArguments[stopArgIndex] const restNotFilledArgs = restArguments.slice(stopArgIndex) @@ -301,7 +321,8 @@ export const getArgumentSuggestions = ( } } - if (stopArgument && !stopArgument.optional) { + const isPrevArgWasMandatory = prevArg && !prevArg.optional + if (isPrevArgWasMandatory && stopArgument && !stopArgument.optional) { const isCanAppend = stopArgument?.token || isOneOfArgument const append = isCanAppend ? [[isOneOfArgument ? stopArgument.arguments! : stopArgument].flat()] : [] @@ -315,8 +336,11 @@ export const getArgumentSuggestions = ( // if we finished argument - stopArgument will be undefined, then we get it as token const lastArgument = stopArgument ?? restArguments[0] + const isBlockHasParent = current?.arguments?.some(({ name }) => parent?.name && name === parent?.name) + const foundParent = isBlockHasParent ? { ...parent, parent: current } : (parent || current) + const isBlockComplete = !stopArgument && current?.name === lastArgument?.name - const beforeMandatoryOptionalArgs = getAllRestArguments(current, lastArgument, levelArgs, isBlockComplete) + const beforeMandatoryOptionalArgs = getAllRestArguments(foundParent, lastArgument, levelArgs, isBlockComplete) const requiredArgsLength = restNotFilledArgs.filter((arg) => !arg.optional).length return { @@ -335,11 +359,14 @@ export const getRestArguments = ( ?.findIndex(({ name }) => name === stopArgument?.name) const nextMandatoryIndex = argumentIndexInArg && argumentIndexInArg > -1 ? current?.arguments ?.findIndex(({ optional }, i) => !optional && i > argumentIndexInArg) : -1 + const prevMandatory = current?.arguments?.slice(0, argumentIndexInArg).reverse() + .find(({ optional }) => !optional) + const prevMandatoryIndex = current?.arguments?.findIndex(({ name }) => name === prevMandatory?.name) const beforeMandatoryOptionalArgs = ( nextMandatoryIndex && nextMandatoryIndex > -1 - ? current?.arguments?.slice(argumentIndexInArg, nextMandatoryIndex) - : current?.arguments?.filter(({ optional }) => optional) + ? current?.arguments?.slice(prevMandatoryIndex, nextMandatoryIndex) + : current?.arguments?.slice((prevMandatoryIndex || 0) + 1) ) || [] const nextMandatoryArg = nextMandatoryIndex && nextMandatoryIndex > -1 @@ -350,6 +377,10 @@ export const getRestArguments = ( beforeMandatoryOptionalArgs.unshift(nextMandatoryArg) } + if (nextMandatoryArg?.type === TokenType.OneOf) { + beforeMandatoryOptionalArgs.unshift(...(nextMandatoryArg.arguments || [])) + } + return fillArgsByType(beforeMandatoryOptionalArgs) .map((arg) => ({ ...arg, @@ -364,7 +395,6 @@ export const getAllRestArguments = ( skipLevel = false ) => { const appendArgs: Array = [] - const currentLvlNextArgs = removeNotSuggestedArgs( prevStringArgs, getRestArguments(current, stopArgument) @@ -394,7 +424,10 @@ export const fillArgsByType = (args: SearchCommand[], expandBlock = true): Searc for (let i = 0; i < args.length; i++) { const currentArg = args[i] - if (expandBlock && currentArg.type === TokenType.OneOf) result.push(...(currentArg?.arguments || [])) + if (expandBlock && currentArg.type === TokenType.OneOf && !currentArg.token) { + result.push(...(currentArg?.arguments || [])) + } + if (currentArg.type === TokenType.Block) { result.push({ multiple: currentArg.multiple, @@ -414,7 +447,8 @@ export const findArgByToken = (list: SearchCommand[], arg: string): Maybe oneOfArg?.token?.toLowerCase() === arg?.toLowerCase()) : cArg.arguments?.[0]?.token?.toLowerCase() === arg.toLowerCase())) -export const isCompositeArgument = (arg: string, prevArg?: string) => arg === '*' && prevArg === 'LOAD' +export const isCompositeArgument = (arg: string, prevArg?: string) => + COMPOSITE_ARGS.includes([prevArg?.toUpperCase(), arg?.toUpperCase()].join(' ')) export const generateDetail = (command: Maybe) => { if (!command) return '' diff --git a/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts b/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts index b6b4a49991..ccd15cc8a2 100644 --- a/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts +++ b/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts @@ -1,603 +1,19 @@ import { addOwnTokenToArgs, findCurrentArgument, generateDetail, splitQueryByArgs } from 'uiSrc/pages/search/utils' import { SearchCommand, TokenType } from 'uiSrc/pages/search/types' import { Maybe } from 'uiSrc/utils' +import { + commonfindCurrentArgumentCases, + findArgumentftAggreageTests, + findArgumentftSearchTests +} from './test-cases' import { MOCKED_SUPPORTED_COMMANDS } from '../../mocks/mocks' const ftSearchCommand = MOCKED_SUPPORTED_COMMANDS['FT.SEARCH'] const ftAggregateCommand = MOCKED_SUPPORTED_COMMANDS['FT.AGGREGATE'] -const COMMANDS = [ - { - name: 'FT.SEARCH', - ...ftSearchCommand - }, - { - name: 'FT.AGGREGATE', - ...ftAggregateCommand - } -] - -const ftAggreageTests = [ - { args: [''], result: null }, - { args: ['', ''], result: null }, - { - args: ['index', '"query"', 'APPLY'], - result: { - stopArg: { name: 'expression', token: 'APPLY', type: 'string' }, - append: [], - isBlocked: true, - isComplete: false, - parent: expect.any(Object) - } - }, - { - args: ['index', '"query"', 'APPLY', 'expression'], - result: { - stopArg: { name: 'name', token: 'AS', type: 'string' }, - append: expect.any(Array), - isBlocked: false, - isComplete: false, - parent: expect.any(Object) - } - }, - { - args: ['index', '"query"', 'APPLY', 'expression', 'AS'], - result: { - stopArg: { name: 'name', token: 'AS', type: 'string' }, - append: expect.any(Array), - isBlocked: true, - isComplete: false, - parent: expect.any(Object) - } - }, - { - args: ['index', '"query"', 'APPLY', 'expression', 'AS', 'name'], - result: { - stopArg: undefined, - append: expect.any(Array), - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - } - }, - { - args: ['""', '""', 'GROUPBY', '2', 'p1', 'p2', 'REDUCE', 'f'], - result: { - stopArg: { name: 'nargs', type: 'integer' }, - append: [], - isBlocked: true, - isComplete: false, - parent: expect.any(Object) - } - }, - { - args: ['""', '""', 'GROUPBY', '2', 'p1', 'p2', 'REDUCE', 'f', '0'], - result: { - stopArg: { - name: 'name', - type: 'string', - token: 'AS', - optional: true - }, - append: [ - [ - { - name: 'name', - type: 'string', - token: 'AS', - optional: true, - parent: { - name: 'reduce', - type: 'block', - optional: true, - multiple: true, - arguments: [ - { - name: 'function', - token: 'REDUCE', - type: 'string' - }, - { - name: 'nargs', - type: 'integer' - }, - { - name: 'arg', - type: 'string', - multiple: true - }, - { - name: 'name', - type: 'string', - token: 'AS', - optional: true - } - ], - parent: expect.any(Object) - } - } - ], - [ - { - name: 'function', - token: 'REDUCE', - type: 'string', - multiple: true, - optional: true, - parent: { - name: 'groupby', - type: 'block', - optional: true, - multiple: true, - arguments: [ - { - name: 'nargs', - type: 'integer', - token: 'GROUPBY' - }, - { - name: 'property', - type: 'string', - multiple: true - }, - { - name: 'reduce', - type: 'block', - optional: true, - multiple: true, - arguments: expect.any(Array) - } - ] - } - } - ] - ], - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - } - }, - { - args: ['""', '""', 'GROUPBY', '2', 'p1', 'p2', 'REDUCE', 'f', '1', 'AS', 'name'], - result: { - stopArg: undefined, - append: [ - [], - [ - { - name: 'function', - token: 'REDUCE', - type: 'string', - multiple: true, - optional: true, - parent: expect.any(Object) - } - ] - ], - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - } - }, - { - args: ['index', '"query"', 'SORTBY'], - result: { - stopArg: { name: 'nargs', token: 'SORTBY', type: 'integer' }, - append: expect.any(Array), - isBlocked: true, - isComplete: false, - parent: expect.any(Object) - } - }, - { - args: ['index', '"query"', 'SORTBY', '1', 'p1'], - result: { - stopArg: { - name: 'num', - type: 'integer', - token: 'MAX', - optional: true - }, - append: [ - [ - { - name: 'num', - type: 'integer', - token: 'MAX', - optional: true, - parent: expect.any(Object) - } - ] - ], - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - } - }, - { - args: ['index', '"query"', 'SORTBY', '2', 'p1', 'ASC'], - result: { - stopArg: { - name: 'num', - type: 'integer', - token: 'MAX', - optional: true - }, - append: [ - [ - { - name: 'num', - type: 'integer', - token: 'MAX', - optional: true, - parent: expect.any(Object) - } - ] - ], - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - } - }, - { - args: ['index', '"query"', 'SORTBY', '0'], - result: { - stopArg: { - name: 'num', - type: 'integer', - token: 'MAX', - optional: true - }, - append: [ - [{ - name: 'num', - type: 'integer', - token: 'MAX', - optional: true, - parent: expect.any(Object) - }] - ], - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - } - }, - { - args: ['index', '"query"', 'SORTBY', '2', 'p1', 'ASC', 'MAX'], - result: { - stopArg: { - name: 'num', - type: 'integer', - token: 'MAX', - optional: true - }, - append: [], - isBlocked: true, - isComplete: false, - parent: expect.any(Object) - } - }, - { - args: ['index', '"query"', 'LOAD', '4'], - result: { - stopArg: { multiple: true, name: 'field', type: 'string' }, - append: [], - isBlocked: true, - isComplete: false, - parent: expect.any(Object) - } - }, - { - args: ['index', '"query"', 'LOAD', '4', '1', '2', '3'], - result: { - stopArg: { multiple: true, name: 'field', type: 'string' }, - append: [], - isBlocked: true, - isComplete: false, - parent: expect.any(Object) - } - }, - { - args: ['index', '"query"', 'LOAD', '4', '1', '2', '3', '4'], - result: { - stopArg: undefined, - append: [[]], - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - } - }, -] - -const ftSearchTests = [ - { args: [''], result: null }, - { args: ['', ''], result: null }, - { - args: ['', '', 'SUMMARIZE'], - result: { - stopArg: { - name: 'fields', - type: 'block', - optional: true, - arguments: [ - { - name: 'count', - type: 'string', - token: 'FIELDS' - }, - { - name: 'field', - type: 'string', - multiple: true - } - ] - }, - append: [[ - { - name: 'count', - type: 'string', - token: 'FIELDS', - parent: expect.any(Object), - optional: true - }, - { - name: 'num', - type: 'integer', - token: 'FRAGS', - optional: true, - parent: expect.any(Object) - }, - { - name: 'fragsize', - type: 'integer', - token: 'LEN', - optional: true, - parent: expect.any(Object) - }, - { - name: 'separator', - type: 'string', - token: 'SEPARATOR', - optional: true, - parent: expect.any(Object) - } - ]], - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - } - }, - { - args: ['', '', 'SUMMARIZE', 'FIELDS'], - result: { - stopArg: { - name: 'count', - type: 'string', - token: 'FIELDS' - }, - append: [], - isBlocked: true, - isComplete: false, - parent: expect.any(Object) - } - }, - { - args: ['', '', 'SUMMARIZE', 'FIELDS', '1'], - result: { - stopArg: { - name: 'field', - type: 'string', - multiple: true - }, - append: [], - isBlocked: true, - isComplete: false, - parent: expect.any(Object) - } - }, - { - args: ['', '', 'SUMMARIZE', 'FIELDS', '1', 'f', 'FRAGS'], - result: { - stopArg: { - name: 'num', - type: 'integer', - token: 'FRAGS', - optional: true - }, - append: [], - isBlocked: true, - isComplete: false, - parent: expect.any(Object) - } - }, - { - args: ['', '', 'SUMMARIZE', 'FIELDS', '1', 'f', 'FRAGS', '10'], - result: { - stopArg: { - name: 'fragsize', - type: 'integer', - token: 'LEN', - optional: true - }, - append: [[ - { - name: 'fragsize', - type: 'integer', - token: 'LEN', - optional: true, - parent: expect.any(Object) - }, - { - name: 'separator', - type: 'string', - token: 'SEPARATOR', - optional: true, - parent: expect.any(Object) - } - ]], - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - } - }, - { - args: ['', '', 'RETURN', '1', 'iden'], - result: { - stopArg: undefined, - // TODO: append may have AS token, since it is optional - we skip for now - append: [ - [] - ], - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - } - }, - { - args: ['', '', 'RETURN', '2', 'iden'], - result: { - stopArg: { - name: 'property', - type: 'string', - token: 'AS', - optional: true - }, - append: [[]], - isBlocked: false, - isComplete: false, - parent: expect.any(Object) - } - }, - { - args: ['', '', 'RETURN', '2', 'iden', 'iden'], - result: { - stopArg: undefined, - append: [ - [] - ], - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - } - }, - { - args: ['', '', 'RETURN', '3', 'iden', 'iden'], - result: { - stopArg: { - name: 'property', - type: 'string', - token: 'AS', - optional: true - }, - append: [[]], - isBlocked: false, - isComplete: false, - parent: expect.any(Object) - } - }, - { - args: ['', '', 'RETURN', '3', 'iden', 'iden', 'AS', 'iden2'], - result: { - stopArg: undefined, - append: [ - [] - ], - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - } - }, - { - args: ['', '', 'SORTBY', 'f'], - result: { - stopArg: { - name: 'order', - type: 'oneof', - optional: true, - arguments: [ - { - name: 'asc', - type: 'pure-token', - token: 'ASC' - }, - { - name: 'desc', - type: 'pure-token', - token: 'DESC' - } - ] - }, - append: [ - [ - { - name: 'asc', - type: 'pure-token', - token: 'ASC', - parent: expect.any(Object) - }, - { - name: 'desc', - type: 'pure-token', - token: 'DESC', - parent: expect.any(Object) - } - ] - ], - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - } - }, - { - args: ['', '', 'SORTBY', 'f', 'DESC'], - result: { - stopArg: undefined, - append: [], - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - } - }, - { - args: ['', '', 'DIALECT', '1'], - result: { - stopArg: undefined, - append: [ - [] - ], - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - } - }, -] - -// Common test cases - provides list of suggestions -const commonfindCurrentArgumentCases = [ - { - input: 'FT.SEARCH index "" DIALECT 1', - result: { - stopArg: undefined, - append: expect.any(Array), - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - }, - appendIncludes: ['WITHSCORES', 'VERBATIM', 'FILTER', 'SORTBY', 'RETURN'], - appendNotIncludes: ['DIALECT'] - }, - { - input: 'FT.AGGREGATE "idx:schools" "" GROUPBY 1 p REDUCE AVG 1 a1 AS name ', - result: { - stopArg: undefined, - append: expect.any(Array), - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - }, - appendIncludes: ['REDUCE', 'APPLY', 'SORTBY', 'GROUPBY'], - appendNotIncludes: ['AS'], - }, -] +const COMMANDS = Object.keys(MOCKED_SUPPORTED_COMMANDS).map((name) => ({ + name, + ...MOCKED_SUPPORTED_COMMANDS[name] +})) describe('findCurrentArgument', () => { describe('with list of commands', () => { @@ -620,17 +36,20 @@ describe('findCurrentArgument', () => { ).toEqual( expect.arrayContaining(appendIncludes) ) - expect( - testResult?.append?.flat()?.map((arg) => arg.token) - ).toEqual( - expect.not.arrayContaining(appendNotIncludes) - ) + + if (appendNotIncludes) { + expect( + testResult?.append?.flat()?.map((arg) => arg.token) + ).toEqual( + expect.not.arrayContaining(appendNotIncludes) + ) + } }) }) }) describe('FT.AGGREGATE', () => { - ftAggreageTests.forEach(({ args, result: testResult }) => { + findArgumentftAggreageTests.forEach(({ args, result: testResult }) => { it(`should return proper suggestions for ${args.join(' ')}`, () => { const result = findCurrentArgument( ftAggregateCommand.arguments as SearchCommand[], @@ -642,7 +61,7 @@ describe('findCurrentArgument', () => { }) describe('FT.SEARCH', () => { - ftSearchTests.forEach(({ args, result: testResult }) => { + findArgumentftSearchTests.forEach(({ args, result: testResult }) => { it(`should return proper suggestions for ${args.join(' ')}`, () => { const result = findCurrentArgument( ftSearchCommand.arguments as SearchCommand[], diff --git a/redisinsight/ui/src/pages/search/utils/tests/test-cases/common.ts b/redisinsight/ui/src/pages/search/utils/tests/test-cases/common.ts new file mode 100644 index 0000000000..3322cb9cc9 --- /dev/null +++ b/redisinsight/ui/src/pages/search/utils/tests/test-cases/common.ts @@ -0,0 +1,177 @@ +// Common test cases +export const commonfindCurrentArgumentCases = [ + { + input: 'FT.SEARCH index "" DIALECT 1', + result: { + stopArg: undefined, + append: expect.any(Array), + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + }, + appendIncludes: ['WITHSCORES', 'VERBATIM', 'FILTER', 'SORTBY', 'RETURN'], + appendNotIncludes: ['DIALECT'] + }, + { + input: 'FT.AGGREGATE "idx:schools" "" GROUPBY 1 p REDUCE AVG 1 a1 AS name ', + result: { + stopArg: undefined, + append: expect.any(Array), + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + }, + appendIncludes: ['REDUCE', 'APPLY', 'SORTBY', 'GROUPBY'], + appendNotIncludes: ['AS'], + }, + { + input: 'FT.SEARCH "idx:bicycle" "*" ', + result: { + stopArg: expect.any(Object), + append: expect.any(Array), + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + }, + appendIncludes: ['DIALECT', 'EXPANDER', 'INKEYS', 'LIMIT'], + appendNotIncludes: ['ASC'], + }, + { + input: 'FT.SEARCH "idx:bicycle" "*" DIALECT 2', + result: expect.any(Object), + appendIncludes: ['EXPANDER', 'INKEYS', 'LIMIT'], + appendNotIncludes: ['DIALECT'], + }, + { + input: 'FT.CREATE "idx:schools" ', + result: expect.any(Object), + appendIncludes: ['FILTER', 'ON', 'SCHEMA', 'SCORE', 'NOHL'], + appendNotIncludes: ['HASH', 'JSON'], + }, + { + input: 'FT.CREATE "idx:schools" ON', + result: expect.any(Object), + appendIncludes: ['HASH', 'JSON'], + appendNotIncludes: ['SCHEMA', 'SCORE', 'NOHL'], + }, + { + input: 'FT.CREATE "idx:schools" ON JSON NOFREQS', + result: expect.any(Object), + appendIncludes: ['TEMPORARY', 'NOFIELDS', 'PAYLOAD_FIELD', 'MAXTEXTFIELDS', 'PREFIX', 'SKIPINITIALSCAN'], + appendNotIncludes: ['ON', 'JSON', 'NOFREQS'], + }, + { + input: 'FT.CREATE "idx:schools" ON JSON NOFREQS SKIPINITIALSCAN', + result: expect.any(Object), + appendIncludes: ['TEMPORARY', 'NOFIELDS', 'PAYLOAD_FIELD', 'MAXTEXTFIELDS', 'PREFIX'], + appendNotIncludes: ['ON', 'JSON', 'NOFREQS', 'SKIPINITIALSCAN'], + }, + { + input: 'FT.CREATE "idx:schools" ON JSON SCHEMA address ', + result: { + stopArg: expect.any(Object), + append: expect.any(Array), + isBlocked: false, + isComplete: false, + parent: expect.any(Object) + }, + appendIncludes: ['AS', 'GEO', 'TEXT', 'VECTOR'], + appendNotIncludes: ['SCHEMA', 'SCORE', 'NOHL'], + }, + { + input: 'FT.CREATE "idx:schools" ON JSON SCHEMA address TEXT NOINDEX INDEXMISSING ', + result: { + stopArg: expect.any(Object), + append: expect.any(Array), + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + }, + appendIncludes: ['INDEXEMPTY', 'SORTABLE', 'WITHSUFFIXTRIE'], + appendNotIncludes: ['SCHEMA', 'SCORE', 'NOHL'], + }, + { + input: 'FT.ALTER "idx:schools" ', + result: { + stopArg: expect.any(Object), + append: expect.any(Array), + isBlocked: false, + isComplete: false, + parent: expect.any(Object) + }, + appendIncludes: ['SCHEMA', 'SKIPINITIALSCAN'], + appendNotIncludes: ['ADD'], + }, + { + input: 'FT.ALTER "idx:schools" SCHEMA', + result: expect.any(Object), + appendIncludes: ['ADD'], + appendNotIncludes: ['SKIPINITIALSCAN'], + }, + { + input: 'FT.CONFIG SET ', + result: { + stopArg: { + name: 'option', + type: 'string' + }, + append: [], + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + }, + appendIncludes: [], + appendNotIncludes: [expect.any(String)], + }, + { + input: 'FT.CURSOR READ "idx:schools" 1 ', + result: expect.any(Object), + appendIncludes: ['COUNT'], + }, + { + input: 'FT.DICTADD dict term1 ', + result: { + append: [], + isBlocked: true, + isComplete: false, + parent: expect.any(Object), + stopArg: { + multiple: true, + name: 'term', + type: 'string' + } + }, + appendIncludes: [], + }, + { + input: 'FT.SUGADD key string ', + result: { + append: [], + isBlocked: true, + isComplete: false, + parent: expect.any(Object), + stopArg: { + name: 'score', + type: 'double' + } + }, + appendIncludes: [], + }, + { + input: 'FT.SUGADD key string 1.0 ', + result: expect.any(Object), + appendIncludes: ['INCR', 'PAYLOAD'], + }, + { + input: 'FT.SUGADD key string 1.0 PAYLOAD 1 ', + result: expect.any(Object), + appendIncludes: ['INCR'], + appendNotIncludes: ['PAYLOAD'], + }, + { + input: 'FT.SUGGET k p FUZZY MAX 2 ', + result: expect.any(Object), + appendIncludes: ['WITHPAYLOADS', 'WITHSCORES'], + appendNotIncludes: ['FUZZY', 'MAX'], + }, +] diff --git a/redisinsight/ui/src/pages/search/utils/tests/test-cases/ft-aggregate.ts b/redisinsight/ui/src/pages/search/utils/tests/test-cases/ft-aggregate.ts new file mode 100644 index 0000000000..7601af7b29 --- /dev/null +++ b/redisinsight/ui/src/pages/search/utils/tests/test-cases/ft-aggregate.ts @@ -0,0 +1,291 @@ +export const findArgumentftAggreageTests = [ + { args: [''], result: null }, + { args: ['', ''], result: null }, + { + args: ['index', '"query"', 'APPLY'], + result: { + stopArg: { name: 'expression', token: 'APPLY', type: 'string' }, + append: [], + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'APPLY', 'expression'], + result: { + stopArg: { name: 'name', token: 'AS', type: 'string' }, + append: expect.any(Array), + isBlocked: false, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'APPLY', 'expression', 'AS'], + result: { + stopArg: { name: 'name', token: 'AS', type: 'string' }, + append: expect.any(Array), + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'APPLY', 'expression', 'AS', 'name'], + result: { + stopArg: undefined, + append: expect.any(Array), + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['""', '""', 'GROUPBY', '2', 'p1', 'p2', 'REDUCE', 'f'], + result: { + stopArg: { name: 'nargs', type: 'integer' }, + append: [], + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['""', '""', 'GROUPBY', '2', 'p1', 'p2', 'REDUCE', 'f', '0'], + result: { + stopArg: { + name: 'name', + type: 'string', + token: 'AS', + optional: true + }, + append: [ + [ + { + name: 'name', + type: 'string', + token: 'AS', + optional: true, + parent: { + name: 'reduce', + type: 'block', + optional: true, + multiple: true, + arguments: [ + { + name: 'function', + token: 'REDUCE', + type: 'string' + }, + { + name: 'nargs', + type: 'integer' + }, + { + name: 'arg', + type: 'string', + multiple: true + }, + { + name: 'name', + type: 'string', + token: 'AS', + optional: true + } + ], + parent: expect.any(Object) + } + } + ], + [ + { + name: 'function', + token: 'REDUCE', + type: 'string', + multiple: true, + optional: true, + parent: { + name: 'groupby', + type: 'block', + optional: true, + multiple: true, + arguments: [ + { + name: 'nargs', + type: 'integer', + token: 'GROUPBY' + }, + { + name: 'property', + type: 'string', + multiple: true + }, + { + name: 'reduce', + type: 'block', + optional: true, + multiple: true, + arguments: expect.any(Array) + } + ] + } + } + ] + ], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['""', '""', 'GROUPBY', '2', 'p1', 'p2', 'REDUCE', 'f', '1', 'AS', 'name'], + result: { + stopArg: undefined, + append: [ + [], + [ + { + name: 'function', + token: 'REDUCE', + type: 'string', + multiple: true, + optional: true, + parent: expect.any(Object) + } + ] + ], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'SORTBY'], + result: { + stopArg: { name: 'nargs', token: 'SORTBY', type: 'integer' }, + append: expect.any(Array), + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'SORTBY', '1', 'p1'], + result: { + stopArg: { + name: 'num', + type: 'integer', + token: 'MAX', + optional: true + }, + append: [ + [ + { + name: 'num', + type: 'integer', + token: 'MAX', + optional: true, + parent: expect.any(Object) + } + ] + ], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'SORTBY', '2', 'p1', 'ASC'], + result: { + stopArg: { + name: 'num', + type: 'integer', + token: 'MAX', + optional: true + }, + append: [ + [ + { + name: 'num', + type: 'integer', + token: 'MAX', + optional: true, + parent: expect.any(Object) + } + ] + ], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'SORTBY', '0'], + result: { + stopArg: { + name: 'num', + type: 'integer', + token: 'MAX', + optional: true + }, + append: [ + [{ + name: 'num', + type: 'integer', + token: 'MAX', + optional: true, + parent: expect.any(Object) + }] + ], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'SORTBY', '2', 'p1', 'ASC', 'MAX'], + result: { + stopArg: { + name: 'num', + type: 'integer', + token: 'MAX', + optional: true + }, + append: [], + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'LOAD', '4'], + result: { + stopArg: { multiple: true, name: 'field', type: 'string' }, + append: [], + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'LOAD', '4', '1', '2', '3'], + result: { + stopArg: { multiple: true, name: 'field', type: 'string' }, + append: [], + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'LOAD', '4', '1', '2', '3', '4'], + result: { + stopArg: undefined, + append: [[]], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, +] diff --git a/redisinsight/ui/src/pages/search/utils/tests/test-cases/ft-search.ts b/redisinsight/ui/src/pages/search/utils/tests/test-cases/ft-search.ts new file mode 100644 index 0000000000..0e7650f5ad --- /dev/null +++ b/redisinsight/ui/src/pages/search/utils/tests/test-cases/ft-search.ts @@ -0,0 +1,283 @@ +export const findArgumentftSearchTests = [ + { args: [''], result: null }, + { args: ['', ''], result: null }, + { + args: ['', '', 'SUMMARIZE'], + result: { + stopArg: { + name: 'fields', + type: 'block', + optional: true, + arguments: [ + { + name: 'count', + type: 'string', + token: 'FIELDS' + }, + { + name: 'field', + type: 'string', + multiple: true + } + ] + }, + append: [[ + { + name: 'count', + type: 'string', + token: 'FIELDS', + parent: expect.any(Object), + optional: true + }, + { + name: 'num', + type: 'integer', + token: 'FRAGS', + optional: true, + parent: expect.any(Object) + }, + { + name: 'fragsize', + type: 'integer', + token: 'LEN', + optional: true, + parent: expect.any(Object) + }, + { + name: 'separator', + type: 'string', + token: 'SEPARATOR', + optional: true, + parent: expect.any(Object) + } + ]], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'SUMMARIZE', 'FIELDS'], + result: { + stopArg: { + name: 'count', + type: 'string', + token: 'FIELDS' + }, + append: [], + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'SUMMARIZE', 'FIELDS', '1'], + result: { + stopArg: { + name: 'field', + type: 'string', + multiple: true + }, + append: [], + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'SUMMARIZE', 'FIELDS', '1', 'f', 'FRAGS'], + result: { + stopArg: { + name: 'num', + type: 'integer', + token: 'FRAGS', + optional: true + }, + append: [], + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'SUMMARIZE', 'FIELDS', '1', 'f', 'FRAGS', '10'], + result: { + stopArg: { + name: 'fragsize', + type: 'integer', + token: 'LEN', + optional: true + }, + append: [[ + { + name: 'fragsize', + type: 'integer', + token: 'LEN', + optional: true, + parent: expect.any(Object) + }, + { + name: 'separator', + type: 'string', + token: 'SEPARATOR', + optional: true, + parent: expect.any(Object) + } + ]], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'RETURN', '1', 'iden'], + result: { + stopArg: undefined, + append: [ + [] + ], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'RETURN', '2', 'iden'], + result: { + stopArg: { + name: 'property', + type: 'string', + token: 'AS', + optional: true + }, + append: [ + [ + { + name: 'property', + type: 'string', + token: 'AS', + optional: true, + parent: expect.any(Object) + } + ], + [] + ], + isBlocked: false, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'RETURN', '2', 'iden', 'iden'], + result: { + stopArg: undefined, + append: [ + [] + ], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'RETURN', '3', 'iden', 'iden'], + result: { + stopArg: { + name: 'property', + type: 'string', + token: 'AS', + optional: true + }, + append: [ + [ + { + name: 'property', + type: 'string', + token: 'AS', + optional: true, + parent: expect.any(Object) + } + ], + [] + ], + isBlocked: false, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'RETURN', '3', 'iden', 'iden', 'AS', 'iden2'], + result: { + stopArg: undefined, + append: [ + [] + ], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'SORTBY', 'f'], + result: { + stopArg: { + name: 'order', + type: 'oneof', + optional: true, + arguments: [ + { + name: 'asc', + type: 'pure-token', + token: 'ASC' + }, + { + name: 'desc', + type: 'pure-token', + token: 'DESC' + } + ] + }, + append: [ + [ + { + name: 'asc', + type: 'pure-token', + token: 'ASC', + parent: expect.any(Object) + }, + { + name: 'desc', + type: 'pure-token', + token: 'DESC', + parent: expect.any(Object) + } + ] + ], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'SORTBY', 'f', 'DESC'], + result: { + stopArg: undefined, + append: [], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'DIALECT', '1'], + result: { + stopArg: undefined, + append: [ + [] + ], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, +] diff --git a/redisinsight/ui/src/pages/search/utils/tests/test-cases/index.ts b/redisinsight/ui/src/pages/search/utils/tests/test-cases/index.ts new file mode 100644 index 0000000000..42889e7be5 --- /dev/null +++ b/redisinsight/ui/src/pages/search/utils/tests/test-cases/index.ts @@ -0,0 +1,3 @@ +export * from './ft-aggregate' +export * from './ft-search' +export * from './common' diff --git a/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts b/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts index 187fe305fe..d9c1c3b881 100644 --- a/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts +++ b/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts @@ -4,7 +4,7 @@ import { generateKeywords, generateTokens, generateTokensWithFunctions, - getBlockTokens, + getBlockTokens, isIndexAfterKeyword, isQueryAfterIndex } from 'uiSrc/utils/monaco/redisearch/utils' import { generateQuery } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokensTemplates' @@ -18,6 +18,7 @@ export const getRediSearchMonarchTokensProvider = ( const currentCommand = commands.find(({ name }) => name === command) const keywords = generateKeywords(commands) + const isHighlightIndex = isIndexAfterKeyword(currentCommand) const argTokens = generateTokens(currentCommand) const isHighlightQuery = isQueryAfterIndex(currentCommand) @@ -55,7 +56,7 @@ export const getRediSearchMonarchTokensProvider = ( [/[<>=!%&+\-*/|~^]/, 'operator'], ], keyword: [ - [`(${keywords.join('|')})\\b`, { token: 'keyword', next: '@index' }] + [`(${keywords.join('|')})\\b`, { token: 'keyword', next: isHighlightIndex ? '@index' : '@root' }] ], 'argument.block': getBlockTokens(argTokens?.pureTokens), ...generateTokensWithFunctions(argTokens?.tokensWithQueryAfter), diff --git a/redisinsight/ui/src/utils/monaco/redisearch/utils.ts b/redisinsight/ui/src/utils/monaco/redisearch/utils.ts index c65b4bb80a..d04f47a717 100644 --- a/redisinsight/ui/src/utils/monaco/redisearch/utils.ts +++ b/redisinsight/ui/src/utils/monaco/redisearch/utils.ts @@ -55,6 +55,13 @@ export const generateTokens = (command?: SearchCommand): Nullable<{ return { pureTokens, tokensWithQueryAfter } } +export const isIndexAfterKeyword = (command?: SearchCommand) => { + if (!command) return false + + const index = command.arguments?.findIndex(({ name }) => name === DefinedArgumentName.index) + return isNumber(index) && index === 0 +} + export const isQueryAfterIndex = (command?: SearchCommand) => { if (!command) return false From a9b72cec4590f85abee540668aae680039df6745 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Tue, 1 Oct 2024 15:07:40 +0200 Subject: [PATCH 076/112] #RI-6089 - fix suggestions --- .../ui/src/pages/search/utils/query.ts | 32 ++++++++++++------- .../pages/search/utils/tests/query.spec.ts | 12 ++++--- .../search/utils/tests/test-cases/common.ts | 6 ++++ .../utils/tests/test-cases/ft-aggregate.ts | 26 +-------------- .../utils/tests/test-cases/ft-search.ts | 2 +- 5 files changed, 36 insertions(+), 42 deletions(-) diff --git a/redisinsight/ui/src/pages/search/utils/query.ts b/redisinsight/ui/src/pages/search/utils/query.ts index 2b50127bc6..92bc72179e 100644 --- a/redisinsight/ui/src/pages/search/utils/query.ts +++ b/redisinsight/ui/src/pages/search/utils/query.ts @@ -381,11 +381,7 @@ export const getRestArguments = ( beforeMandatoryOptionalArgs.unshift(...(nextMandatoryArg.arguments || [])) } - return fillArgsByType(beforeMandatoryOptionalArgs) - .map((arg) => ({ - ...arg, - parent: current - })) + return beforeMandatoryOptionalArgs.map((arg) => ({ ...arg, parent: current })) } export const getAllRestArguments = ( @@ -401,7 +397,7 @@ export const getAllRestArguments = ( ) if (!skipLevel) { - appendArgs.push(currentLvlNextArgs) + appendArgs.push(fillArgsByType(currentLvlNextArgs)) } if (current?.parent) { @@ -415,23 +411,37 @@ export const getAllRestArguments = ( } export const removeNotSuggestedArgs = (args: string[], commandArgs: SearchCommandTree[]) => - commandArgs.filter((arg) => arg.token - && (arg.multiple || !args.some((queryArg) => queryArg.toUpperCase() === arg.token?.toUpperCase()))) + commandArgs.filter((arg) => { + if (arg.token && arg.multiple) return true -export const fillArgsByType = (args: SearchCommand[], expandBlock = true): SearchCommand[] => { - const result: SearchCommand[] = [] + if (arg.type === TokenType.OneOf) { + return !args + .some((queryArg) => arg.arguments + ?.some((oneOfArg) => oneOfArg.token?.toUpperCase() === queryArg.toUpperCase())) + } + + if (arg.type === TokenType.Block) { + return arg.arguments?.[0]?.token && !args.includes(arg.arguments?.[0]?.token?.toUpperCase()) + } + + return arg.token && !args.includes(arg.token) + }) + +export const fillArgsByType = (args: SearchCommand[], expandBlock = true): SearchCommandTree[] => { + const result: SearchCommandTree[] = [] for (let i = 0; i < args.length; i++) { const currentArg = args[i] if (expandBlock && currentArg.type === TokenType.OneOf && !currentArg.token) { - result.push(...(currentArg?.arguments || [])) + result.push(...(currentArg?.arguments?.map((arg) => ({ ...arg, parent: currentArg })) || [])) } if (currentArg.type === TokenType.Block) { result.push({ multiple: currentArg.multiple, optional: currentArg.optional, + parent: currentArg, ...(currentArg?.arguments?.[0] as SearchCommand || {}), }) } diff --git a/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts b/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts index ccd15cc8a2..0c3fcd858d 100644 --- a/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts +++ b/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts @@ -38,11 +38,13 @@ describe('findCurrentArgument', () => { ) if (appendNotIncludes) { - expect( - testResult?.append?.flat()?.map((arg) => arg.token) - ).toEqual( - expect.not.arrayContaining(appendNotIncludes) - ) + appendNotIncludes.forEach((token) => { + expect( + testResult?.append?.flat()?.map((arg) => arg.token) + ).not.toEqual( + expect.arrayContaining([token]) + ) + }) } }) }) diff --git a/redisinsight/ui/src/pages/search/utils/tests/test-cases/common.ts b/redisinsight/ui/src/pages/search/utils/tests/test-cases/common.ts index 3322cb9cc9..7946e4baa6 100644 --- a/redisinsight/ui/src/pages/search/utils/tests/test-cases/common.ts +++ b/redisinsight/ui/src/pages/search/utils/tests/test-cases/common.ts @@ -42,6 +42,12 @@ export const commonfindCurrentArgumentCases = [ appendIncludes: ['EXPANDER', 'INKEYS', 'LIMIT'], appendNotIncludes: ['DIALECT'], }, + { + input: 'FT.PROFILE \'idx:schools\' SEARCH ', + result: expect.any(Object), + appendIncludes: ['LIMITED', 'QUERY'], + appendNotIncludes: ['AGGREGATE', 'SEARCH'], + }, { input: 'FT.CREATE "idx:schools" ', result: expect.any(Object), diff --git a/redisinsight/ui/src/pages/search/utils/tests/test-cases/ft-aggregate.ts b/redisinsight/ui/src/pages/search/utils/tests/test-cases/ft-aggregate.ts index 7601af7b29..e1411809a9 100644 --- a/redisinsight/ui/src/pages/search/utils/tests/test-cases/ft-aggregate.ts +++ b/redisinsight/ui/src/pages/search/utils/tests/test-cases/ft-aggregate.ts @@ -105,31 +105,7 @@ export const findArgumentftAggreageTests = [ type: 'string', multiple: true, optional: true, - parent: { - name: 'groupby', - type: 'block', - optional: true, - multiple: true, - arguments: [ - { - name: 'nargs', - type: 'integer', - token: 'GROUPBY' - }, - { - name: 'property', - type: 'string', - multiple: true - }, - { - name: 'reduce', - type: 'block', - optional: true, - multiple: true, - arguments: expect.any(Array) - } - ] - } + parent: expect.any(Object) } ] ], diff --git a/redisinsight/ui/src/pages/search/utils/tests/test-cases/ft-search.ts b/redisinsight/ui/src/pages/search/utils/tests/test-cases/ft-search.ts index 0e7650f5ad..28137bf8e9 100644 --- a/redisinsight/ui/src/pages/search/utils/tests/test-cases/ft-search.ts +++ b/redisinsight/ui/src/pages/search/utils/tests/test-cases/ft-search.ts @@ -26,8 +26,8 @@ export const findArgumentftSearchTests = [ name: 'count', type: 'string', token: 'FIELDS', + optional: true, parent: expect.any(Object), - optional: true }, { name: 'num', From 6e936b866f4670200cd49df898eece590267afbd Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Wed, 2 Oct 2024 10:28:21 +0200 Subject: [PATCH 077/112] #RI-6137 - update quotes and colors --- redisinsight/ui/src/constants/monaco/theme.ts | 26 +++++++++---------- .../pages/search/components/query/utils.ts | 8 +++--- .../ui/src/pages/search/utils/monaco.ts | 2 +- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/redisinsight/ui/src/constants/monaco/theme.ts b/redisinsight/ui/src/constants/monaco/theme.ts index db0e6521fa..b3ac2f83cb 100644 --- a/redisinsight/ui/src/constants/monaco/theme.ts +++ b/redisinsight/ui/src/constants/monaco/theme.ts @@ -1,16 +1,16 @@ import { monaco } from 'react-monaco-editor' export const redisearchDarKThemeRules = [ - { token: 'keyword', foreground: '#8094B1', fontStyle: 'bold' }, - { token: 'argument.block.0', foreground: '#BDE8D7' }, - { token: 'argument.block.1', foreground: '#8CD7B9' }, - { token: 'argument.block.2', foreground: '#5BC69B' }, - { token: 'argument.block.3', foreground: '#3A8365' }, - { token: 'argument.block.withToken.0', foreground: '#BDE8D7' }, - { token: 'argument.block.withToken.1', foreground: '#8CD7B9' }, - { token: 'argument.block.withToken.2', foreground: '#5BC69B' }, - { token: 'argument.block.withToken.3', foreground: '#3A8365' }, - { token: 'loadAll', foreground: '#BDE8D7' }, + { token: 'keyword', foreground: '#C8B5F2' }, + { token: 'argument.block.0', foreground: '#8CD7B9' }, + { token: 'argument.block.1', foreground: '#72B59B' }, + { token: 'argument.block.2', foreground: '#3A8365' }, + { token: 'argument.block.3', foreground: '#244F3E' }, + { token: 'argument.block.withToken.0', foreground: '#8CD7B9' }, + { token: 'argument.block.withToken.1', foreground: '#72B59B' }, + { token: 'argument.block.withToken.2', foreground: '#3A8365' }, + { token: 'argument.block.withToken.3', foreground: '#244F3E' }, + { token: 'loadAll', foreground: '#8CD7B9' }, { token: 'index', foreground: '#DE47BB' }, { token: 'query', foreground: '#7B90E0' }, { token: 'field', foreground: '#B02C30' }, @@ -19,13 +19,13 @@ export const redisearchDarKThemeRules = [ ] export const redisearchLightThemeRules = [ - { token: 'keyword', foreground: '#8094B1', fontStyle: 'bold' }, + { token: 'keyword', foreground: '#7547DE' }, { token: 'argument.block.0', foreground: '#8CD7B9' }, - { token: 'argument.block.1', foreground: '#5BC69B' }, + { token: 'argument.block.1', foreground: '#72B59B' }, { token: 'argument.block.2', foreground: '#3A8365' }, { token: 'argument.block.3', foreground: '#244F3E' }, { token: 'argument.block.withToken.0', foreground: '#8CD7B9' }, - { token: 'argument.block.withToken.1', foreground: '#5BC69B' }, + { token: 'argument.block.withToken.1', foreground: '#72B59B' }, { token: 'argument.block.withToken.2', foreground: '#3A8365' }, { token: 'argument.block.withToken.3', foreground: '#244F3E' }, { token: 'loadAll', foreground: '#8CD7B9' }, diff --git a/redisinsight/ui/src/pages/search/components/query/utils.ts b/redisinsight/ui/src/pages/search/components/query/utils.ts index 581b85c2d2..49d7976cde 100644 --- a/redisinsight/ui/src/pages/search/components/query/utils.ts +++ b/redisinsight/ui/src/pages/search/components/query/utils.ts @@ -20,15 +20,15 @@ export const asSuggestionsRef = ( forceShow }) -export const getIndexesSuggestions = (indexes: RedisResponseBuffer[], range: monaco.IRange, nextQuotes = true) => +export const getIndexesSuggestions = (indexes: RedisResponseBuffer[], range: monaco.IRange, isNextArgQuery = true) => indexes.map((index) => { const value = formatLongName(bufferToString(index)) - const insertQueryQuotes = nextQuotes ? ' "$1"' : '' + const insertQueryQuotes = isNextArgQuery ? " '\${1:query to search}'" : '' return { label: value || ' ', kind: monacoEditor.languages.CompletionItemKind.Snippet, - insertText: `"${value}"${insertQueryQuotes} `, + insertText: `'${value}'${insertQueryQuotes} `, insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, range, detail: value || ' ', @@ -54,7 +54,7 @@ export const getFieldsSuggestions = ( ) => fields.map((field) => { const { attribute, type } = field - const attibuteText = attribute.trim() ? attribute : `\\"${attribute}\\"` + const attibuteText = attribute.trim() ? attribute : `\\'${attribute}\\'` const insertText = withType ? addFieldAttribute(attibuteText, type) : attibuteText return { diff --git a/redisinsight/ui/src/pages/search/utils/monaco.ts b/redisinsight/ui/src/pages/search/utils/monaco.ts index bfc1d6ea08..2f8c713e78 100644 --- a/redisinsight/ui/src/pages/search/utils/monaco.ts +++ b/redisinsight/ui/src/pages/search/utils/monaco.ts @@ -26,7 +26,7 @@ export const getRange = (position: monaco.Position, word: monaco.editor.IWordAtP }) export const buildSuggestion = (arg: SearchCommand, range: monaco.IRange, options: any = {}) => { - const extraQuotes = arg.expression ? '"$1"' : '' + const extraQuotes = arg.expression ? '\'$1\'' : '' return { label: isString(arg) ? arg : arg.token || arg.arguments?.[0].token || arg.name || '', insertText: `${arg.token || arg.arguments?.[0].token || arg.name?.toUpperCase() || ''} ${extraQuotes}`, From 9febe2276bae14afa82aa70641eb29b2eaf0ce44 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Thu, 3 Oct 2024 13:57:15 +0200 Subject: [PATCH 078/112] #RI-6135 - cover no indexes case * fix last page context --- .../pages/search/components/query/Query.tsx | 32 ++++++++++++++++--- .../search/components/query/constants.ts | 3 ++ .../pages/search/components/query/utils.ts | 17 +++++++++- .../ui/src/pages/search/utils/query.ts | 2 +- redisinsight/ui/src/slices/app/context.ts | 4 +-- redisinsight/ui/src/slices/interfaces/app.ts | 2 +- .../ui/src/slices/tests/app/context.spec.ts | 2 +- 7 files changed, 52 insertions(+), 10 deletions(-) diff --git a/redisinsight/ui/src/pages/search/components/query/Query.tsx b/redisinsight/ui/src/pages/search/components/query/Query.tsx index 0e73ae51bc..32e992a49f 100644 --- a/redisinsight/ui/src/pages/search/components/query/Query.tsx +++ b/redisinsight/ui/src/pages/search/components/query/Query.tsx @@ -20,7 +20,13 @@ import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' import { fetchRedisearchInfoAction } from 'uiSrc/slices/browser/redisearch' import { getRediSearchMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokens' import { useDebouncedEffect } from 'uiSrc/services' -import { options, DefinedArgumentName, FIELD_START_SYMBOL, COMMANDS_TO_GET_INDEX_INFO } from './constants' +import { + options, + DefinedArgumentName, + FIELD_START_SYMBOL, + COMMANDS_TO_GET_INDEX_INFO, + EmptySuggestionsIds +} from './constants' import { getFieldsSuggestions, getIndexesSuggestions, @@ -29,6 +35,7 @@ import { isIndexComplete, getGeneralSuggestions, getFunctionsSuggestions, + getNoIndexesSuggestion, } from './utils' export interface Props { @@ -123,6 +130,15 @@ const Query = (props: Props) => { } }) + const suggestionWidget = editor.getContribution('editor.contrib.suggestController') + suggestionWidget.onWillInsertSuggestItem(({ item }: Record<'item', any>) => { + if (item.completion.id === EmptySuggestionsIds.NoIndexes) { + updateHelpWidget(true) + editor.trigger('', 'hideSuggestWidget', null) + editor.trigger('', 'editor.action.triggerParameterHints', '') + } + }) + suggestionsRef.current = getSuggestions(editor).data if (value) { setCursorPositionAtTheEnd(editor) @@ -182,7 +198,10 @@ const Query = (props: Props) => { } const updateHelpWidget = (isOpen: boolean, parent?: SearchCommand, currentArg?: SearchCommand) => { - helpWidgetRef.current = { isOpen, parent, currentArg } + helpWidgetRef.current = { + isOpen, + parent: parent || helpWidgetRef.current.parent, + currentArg: currentArg || helpWidgetRef.current.currentArg } } const getSuggestions = ( @@ -256,9 +275,14 @@ const Query = (props: Props) => { currentOffsetArg: Nullable, range: monacoEditor.IRange ) => { - updateHelpWidget(true, command, foundArg?.stopArg) - const isIndex = indexesRef.current.length > 0 + updateHelpWidget(isIndex, command, foundArg?.stopArg) + + if (!isIndex) { + updateHelpWidget(!!currentOffsetArg) + return asSuggestionsRef(!currentOffsetArg ? getNoIndexesSuggestion(range) : [], true) + } + if (!isIndex || currentOffsetArg) return asSuggestionsRef([], !currentOffsetArg) const argumentIndex = command?.arguments diff --git a/redisinsight/ui/src/pages/search/components/query/constants.ts b/redisinsight/ui/src/pages/search/components/query/constants.ts index 53d83e8e3f..e3c7be2574 100644 --- a/redisinsight/ui/src/pages/search/components/query/constants.ts +++ b/redisinsight/ui/src/pages/search/components/query/constants.ts @@ -59,3 +59,6 @@ export enum DefinedArgumentName { } export const FIELD_START_SYMBOL = '@' +export enum EmptySuggestionsIds { + NoIndexes = 'no-indexes' +} diff --git a/redisinsight/ui/src/pages/search/components/query/utils.ts b/redisinsight/ui/src/pages/search/components/query/utils.ts index 49d7976cde..1ae87452be 100644 --- a/redisinsight/ui/src/pages/search/components/query/utils.ts +++ b/redisinsight/ui/src/pages/search/components/query/utils.ts @@ -8,7 +8,7 @@ import { removeNotSuggestedArgs } from 'uiSrc/pages/search/utils' import { FoundCommandArgument, SearchCommand } from 'uiSrc/pages/search/types' -import { DefinedArgumentName } from 'uiSrc/pages/search/components/query/constants' +import { DefinedArgumentName, EmptySuggestionsIds } from 'uiSrc/pages/search/components/query/constants' export const asSuggestionsRef = ( suggestions: monacoEditor.languages.CompletionItem[], @@ -20,6 +20,21 @@ export const asSuggestionsRef = ( forceShow }) +export const getNoIndexesSuggestion = (range: monaco.IRange) => [ + { + id: EmptySuggestionsIds.NoIndexes, + label: 'No indexes to display', + kind: monacoEditor.languages.CompletionItemKind.Issue, + insertText: '', + insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, + range, + detail: 'Create an index', + documentation: { + value: 'See the [documentation](https://redis.io/docs/latest/commands/ft.create/?utm_source=redisinsight&utm_medium=app&utm_campaign=workbench) for detailed instructions on how to create an index.', + } + } +] + export const getIndexesSuggestions = (indexes: RedisResponseBuffer[], range: monaco.IRange, isNextArgQuery = true) => indexes.map((index) => { const value = formatLongName(bufferToString(index)) diff --git a/redisinsight/ui/src/pages/search/utils/query.ts b/redisinsight/ui/src/pages/search/utils/query.ts index 92bc72179e..a18498d5d5 100644 --- a/redisinsight/ui/src/pages/search/utils/query.ts +++ b/redisinsight/ui/src/pages/search/utils/query.ts @@ -465,7 +465,7 @@ export const generateDetail = (command: Maybe) => { if (command.arguments) return generateArgsNames(CommandProvider.Main, command.arguments).join(' ') if (command.token) { if (command.type === TokenType.PureToken) return command.token - return `${command.token} ${command.name}` + return `${command.token}` } return '' diff --git a/redisinsight/ui/src/slices/app/context.ts b/redisinsight/ui/src/slices/app/context.ts index 4fce0714a5..4421886dff 100644 --- a/redisinsight/ui/src/slices/app/context.ts +++ b/redisinsight/ui/src/slices/app/context.ts @@ -35,7 +35,7 @@ export const initialState: StateAppContext = { : AppWorkspace.Databases, contextInstanceId: '', contextRdiInstanceId: '', - lastBrowserPage: '', + lastPage: '', dbConfig: { treeViewDelimiter: DEFAULT_DELIMITER, treeViewSort: DEFAULT_TREE_SORTING, @@ -191,7 +191,7 @@ const appContextSlice = createSlice({ state.searchAndQuery.script = payload }, setLastPageContext: (state, { payload }: { payload: string }) => { - state.lastBrowserPage = payload + state.lastPage = payload }, resetBrowserTree: (state) => { state.browser.tree.selectedLeaf = null diff --git a/redisinsight/ui/src/slices/interfaces/app.ts b/redisinsight/ui/src/slices/interfaces/app.ts index ba7602c322..994b3c41e2 100644 --- a/redisinsight/ui/src/slices/interfaces/app.ts +++ b/redisinsight/ui/src/slices/interfaces/app.ts @@ -62,7 +62,7 @@ export interface StateAppContext { workspace: AppWorkspace contextInstanceId: string contextRdiInstanceId: string - lastBrowserPage: string + lastPage: string dbConfig: { treeViewDelimiter: string treeViewSort: SortOrder diff --git a/redisinsight/ui/src/slices/tests/app/context.spec.ts b/redisinsight/ui/src/slices/tests/app/context.spec.ts index 91b8898d17..874ac9da0b 100644 --- a/redisinsight/ui/src/slices/tests/app/context.spec.ts +++ b/redisinsight/ui/src/slices/tests/app/context.spec.ts @@ -370,7 +370,7 @@ describe('slices', () => { const lastPage = 'workbench' const state = { ...initialState, - lastBrowserPage: lastPage + lastPage } // Act From ef19afe7ae9958b1ad3bc55f3ef8a99027fb65bf Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Thu, 3 Oct 2024 14:00:11 +0200 Subject: [PATCH 079/112] #RI-6135 - update link --- redisinsight/ui/src/pages/search/components/query/utils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/redisinsight/ui/src/pages/search/components/query/utils.ts b/redisinsight/ui/src/pages/search/components/query/utils.ts index 1ae87452be..5ec9f867aa 100644 --- a/redisinsight/ui/src/pages/search/components/query/utils.ts +++ b/redisinsight/ui/src/pages/search/components/query/utils.ts @@ -9,6 +9,7 @@ import { } from 'uiSrc/pages/search/utils' import { FoundCommandArgument, SearchCommand } from 'uiSrc/pages/search/types' import { DefinedArgumentName, EmptySuggestionsIds } from 'uiSrc/pages/search/components/query/constants' +import { getUtmExternalLink } from 'uiSrc/utils/links' export const asSuggestionsRef = ( suggestions: monacoEditor.languages.CompletionItem[], @@ -20,6 +21,7 @@ export const asSuggestionsRef = ( forceShow }) +const NO_INDEXES_DOC_LINK = getUtmExternalLink('https://redis.io/docs/latest/commands/ft.create/', { campaign: 'workbench' }) export const getNoIndexesSuggestion = (range: monaco.IRange) => [ { id: EmptySuggestionsIds.NoIndexes, @@ -30,7 +32,7 @@ export const getNoIndexesSuggestion = (range: monaco.IRange) => [ range, detail: 'Create an index', documentation: { - value: 'See the [documentation](https://redis.io/docs/latest/commands/ft.create/?utm_source=redisinsight&utm_medium=app&utm_campaign=workbench) for detailed instructions on how to create an index.', + value: `See the [documentation](${NO_INDEXES_DOC_LINK}) for detailed instructions on how to create an index.`, } } ] From 2b79713d57f4b4e6dec61deb6dc61ad36c2bf07c Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Thu, 3 Oct 2024 15:31:05 +0200 Subject: [PATCH 080/112] #RI-6135 - fix tests --- redisinsight/ui/src/pages/search/components/query/Query.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisinsight/ui/src/pages/search/components/query/Query.tsx b/redisinsight/ui/src/pages/search/components/query/Query.tsx index 32e992a49f..20aae087c4 100644 --- a/redisinsight/ui/src/pages/search/components/query/Query.tsx +++ b/redisinsight/ui/src/pages/search/components/query/Query.tsx @@ -131,7 +131,7 @@ const Query = (props: Props) => { }) const suggestionWidget = editor.getContribution('editor.contrib.suggestController') - suggestionWidget.onWillInsertSuggestItem(({ item }: Record<'item', any>) => { + suggestionWidget?.onWillInsertSuggestItem(({ item }: Record<'item', any>) => { if (item.completion.id === EmptySuggestionsIds.NoIndexes) { updateHelpWidget(true) editor.trigger('', 'hideSuggestWidget', null) From e63b3fbde86752aaf52d170a7e0adcdb5f4c5dbc Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Fri, 4 Oct 2024 09:37:18 +0200 Subject: [PATCH 081/112] add test when there is no index --- .../no-indexes-suggestions.e2e.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 tests/e2e/tests/web/regression/search-and-query/no-indexes-suggestions.e2e.ts diff --git a/tests/e2e/tests/web/regression/search-and-query/no-indexes-suggestions.e2e.ts b/tests/e2e/tests/web/regression/search-and-query/no-indexes-suggestions.e2e.ts new file mode 100644 index 0000000000..b076bd565c --- /dev/null +++ b/tests/e2e/tests/web/regression/search-and-query/no-indexes-suggestions.e2e.ts @@ -0,0 +1,36 @@ +import { DatabaseHelper } from '../../../../helpers/database'; +import { BrowserPage } from '../../../../pageObjects'; +import { rte } from '../../../../helpers/constants'; +import { commonUrl, ossClusterConfig, ossStandaloneConfig, ossStandaloneRedisearch } from '../../../../helpers/conf'; +import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; +import { SearchAndQueryPage } from '../../../../pageObjects/search-and-query-page'; + +const browserPage = new BrowserPage(); +const databaseHelper = new DatabaseHelper(); +const databaseAPIRequests = new DatabaseAPIRequests(); +const searchAndQueryPage = new SearchAndQueryPage(); + +fixture `Search and Query Raw mode` + .meta({ type: 'critical_path', rte: rte.standalone }) + .page(commonUrl); + +test + .before(async t => { + await databaseHelper.acceptLicenseTermsAndAddOSSClusterDatabase(ossClusterConfig); + await browserPage.Cli.sendCommandInCli('flushdb'); + }) + .after(async t => { + await databaseAPIRequests.deleteOSSClusterDatabaseApi(ossClusterConfig); + + })('Verify suggestions when there are no indexes', async t => { + + // TODO add navigation to search and query Monaco + + await t.typeText(searchAndQueryPage.queryInput, 'FT.SE', { replace: true }); + await t.pressKey('enter'); + + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('No indexes to display').exists).ok('info text is not displayed'); + + await t.pressKey('ctrl+space'); + await t.expect(await searchAndQueryPage.MonacoEditor.monacoCommandDetails.find('a').exists).ok('no link in the details') + }); From 1359d7d1b702d02a149af0a6c23a8853f111209d Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Mon, 7 Oct 2024 11:54:31 +0200 Subject: [PATCH 082/112] RI-6089_support-ft-commands --- .../search-and-query-tab.e2e.ts | 52 +++++++++++++++++-- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts index e2be296bf2..6050175477 100644 --- a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts +++ b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts @@ -16,6 +16,7 @@ const keyName = Common.generateWord(10); let keyNames: string[]; let indexName1: string; let indexName2: string; +let indexName3: string; fixture `Autocomplete for entered commands in search and query` .meta({ type: 'regression', rte: rte.standalone }) @@ -24,6 +25,7 @@ fixture `Autocomplete for entered commands in search and query` await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); indexName1 = `idx1:${keyName}`; indexName2 = `idx2:${keyName}`; + indexName3 = `idx3:${keyName}`; keyNames = [`${keyName}:1`, `${keyName}:2`, `${keyName}:3`]; const commands = [ `HSET ${keyNames[0]} "name" "Hall School" "description" " Spanning 10 states" "class" "independent" "type" "traditional" "address_city" "London" "address_street" "Manor Street" "students" 342 "location" "51.445417, -0.258352"`, @@ -247,7 +249,7 @@ test('Verify commands suggestions for APPLY and FILTER', async t => { await t.typeText(searchAndQueryPage.queryInput, '*'); await t.pressKey('right'); await t.pressKey('space'); - //Verify APPLY command + // Verify APPLY command await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('APPLY').exists).ok('Apply is not suggested'); await t.pressKey('enter'); @@ -266,7 +268,7 @@ test('Verify commands suggestions for APPLY and FILTER', async t => { await t.typeText(searchAndQueryPage.queryInput, 'apply_key', { replace: false }); await t.pressKey('space'); - //Verify Filter command + // Verify Filter command await t.typeText(searchAndQueryPage.queryInput, 'F'); await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('FILTER').exists).ok('FILTER is not suggested'); await t.pressKey('enter'); @@ -296,7 +298,7 @@ test('Verify REDUCE commands', async t => { await t.pressKey('enter'); await t.typeText(searchAndQueryPage.queryInput, 'item_count '); - //add additional reduce + // add additional reduce await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('REDUCE').exists).ok('Apply is not suggested'); await t.typeText(searchAndQueryPage.queryInput, 'R'); await t.pressKey('enter'); @@ -320,12 +322,12 @@ test('Verify suggestions for fields', async t => { await t.typeText(searchAndQueryPage.queryInput, '@'); await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed'); - //verify suggestions for geo + // verify suggestions for geo await t.typeText(searchAndQueryPage.queryInput, 'l'); await t.pressKey('tab'); await t.expect((await searchAndQueryPage.MonacoEditor.getTextFromMonaco()).trim()).eql(`FT.AGGREGATE "${indexName1}" "@location:[lon lat radius unit]"`); - //verify for numeric + // verify for numeric await t.typeText(searchAndQueryPage.queryInput, 'FT.AGGREGATE ', { replace: true }); await t.typeText(searchAndQueryPage.queryInput, 'idx1'); await t.pressKey('enter'); @@ -336,3 +338,43 @@ test('Verify suggestions for fields', async t => { await t.pressKey('tab'); await t.expect((await searchAndQueryPage.MonacoEditor.getTextFromMonaco()).trim()).eql(`FT.AGGREGATE "${indexName1}" "@students:[range]"`); }); + +test + .after(async() => { + // Clear and delete database + await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName); + await browserPage.Cli.sendCommandsInCli([`DEL ${keyNames.join(' ')}`, `FT.DROPINDEX ${indexName1}`]); + await browserPage.Cli.sendCommandsInCli([`DEL ${keyNames.join(' ')}`, `FT.DROPINDEX ${indexName2}`]); + await browserPage.Cli.sendCommandsInCli([`DEL ${keyNames.join(' ')}`, `FT.DROPINDEX ${indexName3}`]); + await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); + })('Verify commands suggestions for CREATE', async t => { + await t.typeText(searchAndQueryPage.queryInput, 'FT.CREATE ', { replace: true }); + await t.pressKey('enter'); + // Verify that indexes are not suggested for FT.CREATE + await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.exists).notOk('Existing index suggested'); + + // Enter index name + await t.typeText(searchAndQueryPage.queryInput, indexName3); + await t.pressKey('space'); + + // Select FILTER keyword + await t.typeText(searchAndQueryPage.queryInput, 'FI'); + await t.pressKey('tab'); + await t.typeText(searchAndQueryPage.queryInput, 'filterNew', { replace: false }); + await t.pressKey('space'); + + // Select SCHEMA keyword + await t.typeText(searchAndQueryPage.queryInput, 'SCH'); + await t.pressKey('tab'); + await t.typeText(searchAndQueryPage.queryInput, 'field_name', { replace: false }); + await t.pressKey('space'); + + // Select TEXT keyword + await t.typeText(searchAndQueryPage.queryInput, 'te', { replace: false }); + await t.pressKey('tab'); + + // Select SORTABLE + await t.typeText(searchAndQueryPage.queryInput, 'so', { replace: false }); + await t.pressKey('tab'); + await t.expect((await searchAndQueryPage.MonacoEditor.getTextFromMonaco()).trim()).eql(`FT.CREATE ${indexName3} FILTER filterNew SCHEMA field_name TEXT SORTABLE`); + }); From e70808cbfb73aee4cc865e201c675cda40795286 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Wed, 9 Oct 2024 13:30:31 +0200 Subject: [PATCH 083/112] #RI-6151 - initial implementation of search suggestions in the workbench --- .../monaco-laguages/MonacoLanguages.tsx | 20 +- redisinsight/ui/src/constants/monaco/theme.ts | 4 +- .../ui/src/pages/search/utils/monaco.ts | 1 - .../components/query/Query/Query.tsx | 320 ++++- .../components/query/Query/constants.ts | 12 +- .../components/query/QueryWrapper.tsx | 22 +- .../ui/src/pages/workbench/constants.ts | 51 + .../workbench/data/supported_commands.json | 1156 +++++++++++++++++ redisinsight/ui/src/pages/workbench/types.ts | 45 + .../ui/src/pages/workbench/utils/monaco.ts | 64 + .../ui/src/pages/workbench/utils/profile.ts | 40 + .../ui/src/pages/workbench/utils/query.ts | 479 +++++++ .../src/pages/workbench/utils/suggestions.ts | 198 +++ .../ui/src/utils/monaco/monacoInterfaces.ts | 10 +- .../monacoRedisMonarchTokensProvider.ts | 56 +- .../ui/src/utils/monaco/monacoUtils.ts | 102 +- .../monaco/monarchTokens/redisearchTokens.ts | 2 +- .../monarchTokens/redisearchTokensSubRedis.ts | 121 ++ .../ui/src/utils/monaco/redisearch/utils.ts | 16 +- .../src/utils/monaco/redisearch/utils_old.ts | 143 ++ .../monaco/subTokens/redisearchSubTokens.ts | 0 21 files changed, 2772 insertions(+), 90 deletions(-) create mode 100644 redisinsight/ui/src/pages/workbench/data/supported_commands.json create mode 100644 redisinsight/ui/src/pages/workbench/types.ts create mode 100644 redisinsight/ui/src/pages/workbench/utils/monaco.ts create mode 100644 redisinsight/ui/src/pages/workbench/utils/profile.ts create mode 100644 redisinsight/ui/src/pages/workbench/utils/query.ts create mode 100644 redisinsight/ui/src/pages/workbench/utils/suggestions.ts create mode 100644 redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokensSubRedis.ts create mode 100644 redisinsight/ui/src/utils/monaco/redisearch/utils_old.ts create mode 100644 redisinsight/ui/src/utils/monaco/subTokens/redisearchSubTokens.ts diff --git a/redisinsight/ui/src/components/monaco-laguages/MonacoLanguages.tsx b/redisinsight/ui/src/components/monaco-laguages/MonacoLanguages.tsx index e87c71dba4..db84c5556f 100644 --- a/redisinsight/ui/src/components/monaco-laguages/MonacoLanguages.tsx +++ b/redisinsight/ui/src/components/monaco-laguages/MonacoLanguages.tsx @@ -7,10 +7,14 @@ import { MonacoLanguage, redisLanguageConfig, Theme } from 'uiSrc/constants' import { getRedisMonarchTokensProvider } from 'uiSrc/utils' import { darkTheme, lightTheme, MonacoThemes } from 'uiSrc/constants/monaco' import { ThemeContext } from 'uiSrc/contexts/themeContext' +import { TokenType } from 'uiSrc/pages/workbench/types' + +import { getRediSearchSubRedisMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokensSubRedis' +import SEARCH_COMMANDS_SPEC from 'uiSrc/pages/workbench/data/supported_commands.json' const MonacoLanguages = () => { const { theme } = useContext(ThemeContext) - const { commandsArray: REDIS_COMMANDS_ARRAY } = useSelector(appRedisCommandsSelector) + const { commandsArray: REDIS_COMMANDS_ARRAY, spec: COMMANDS_SPEC } = useSelector(appRedisCommandsSelector) useEffect(() => { if (monaco?.editor) { @@ -34,12 +38,24 @@ const MonacoLanguages = () => { const isRedisLangRegistered = findIndex(languages, { id: MonacoLanguage.Redis }) > -1 if (!isRedisLangRegistered) { monaco.languages.register({ id: MonacoLanguage.Redis }) + monaco.languages.register({ id: MonacoLanguage.RediSearch }) } monaco.languages.setLanguageConfiguration(MonacoLanguage.Redis, redisLanguageConfig) + const REDIS_COMMANDS = Object.keys(COMMANDS_SPEC).map((name) => ({ + ...(name in SEARCH_COMMANDS_SPEC ? SEARCH_COMMANDS_SPEC[name] : (COMMANDS_SPEC[name] || {})), + name, + token: name, + type: TokenType.Block + })) + + monaco.languages.setMonarchTokensProvider( + MonacoLanguage.RediSearch, + getRediSearchSubRedisMonarchTokensProvider(REDIS_COMMANDS.filter(({ name }) => name.startsWith('FT.'))) + ) monaco.languages.setMonarchTokensProvider( MonacoLanguage.Redis, - getRedisMonarchTokensProvider(REDIS_COMMANDS_ARRAY) + getRedisMonarchTokensProvider(REDIS_COMMANDS) ) } diff --git a/redisinsight/ui/src/constants/monaco/theme.ts b/redisinsight/ui/src/constants/monaco/theme.ts index b3ac2f83cb..9cb3e860af 100644 --- a/redisinsight/ui/src/constants/monaco/theme.ts +++ b/redisinsight/ui/src/constants/monaco/theme.ts @@ -10,7 +10,6 @@ export const redisearchDarKThemeRules = [ { token: 'argument.block.withToken.1', foreground: '#72B59B' }, { token: 'argument.block.withToken.2', foreground: '#3A8365' }, { token: 'argument.block.withToken.3', foreground: '#244F3E' }, - { token: 'loadAll', foreground: '#8CD7B9' }, { token: 'index', foreground: '#DE47BB' }, { token: 'query', foreground: '#7B90E0' }, { token: 'field', foreground: '#B02C30' }, @@ -28,7 +27,6 @@ export const redisearchLightThemeRules = [ { token: 'argument.block.withToken.1', foreground: '#72B59B' }, { token: 'argument.block.withToken.2', foreground: '#3A8365' }, { token: 'argument.block.withToken.3', foreground: '#244F3E' }, - { token: 'loadAll', foreground: '#8CD7B9' }, { token: 'index', foreground: '#DE47BB' }, { token: 'query', foreground: '#7B90E0' }, { token: 'field', foreground: '#B02C30' }, @@ -40,7 +38,7 @@ export const darkThemeRules = [ { token: 'function', foreground: 'BFBC4E' }, ...redisearchDarKThemeRules.map((rule) => ({ ...rule, - token: `${rule.token}.redisearch` + token: `${rule.token}` })) ] diff --git a/redisinsight/ui/src/pages/search/utils/monaco.ts b/redisinsight/ui/src/pages/search/utils/monaco.ts index 2f8c713e78..4eda87a868 100644 --- a/redisinsight/ui/src/pages/search/utils/monaco.ts +++ b/redisinsight/ui/src/pages/search/utils/monaco.ts @@ -43,7 +43,6 @@ export const getRediSearchSignutureProvider = (options: Maybe<{ parent: Maybe }>) => { const { isOpen, currentArg, parent } = options || {} - if (!isOpen) return null const label = generateDetail(parent) diff --git a/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx b/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx index f415fa85ca..440dd63215 100644 --- a/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx +++ b/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx @@ -1,6 +1,6 @@ import React, { useContext, useEffect, useRef, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { compact, first } from 'lodash' +import { compact, first, isNumber } from 'lodash' import cx from 'classnames' import MonacoEditor, { monaco as monacoEditor } from 'react-monaco-editor' import { useParams } from 'react-router-dom' @@ -17,8 +17,7 @@ import { findArgIndexByCursor, findCompleteQuery, getMonacoAction, - getRedisCompletionProvider, - getRedisSignatureHelpProvider, + IMonacoQuery, isParamsLine, MonacoAction, Nullable, @@ -27,13 +26,39 @@ import { import { ThemeContext } from 'uiSrc/contexts/themeContext' import { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands' import { IEditorMount, ISnippetController } from 'uiSrc/pages/workbench/interfaces' -import { CommandExecutionUI } from 'uiSrc/slices/interfaces' +import { CommandExecutionUI, RedisResponseBuffer } from 'uiSrc/slices/interfaces' import { RunQueryMode, ResultsMode } from 'uiSrc/slices/interfaces/workbench' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { stopProcessing, workbenchResultsSelector } from 'uiSrc/slices/workbench/wb-results' import DedicatedEditor from 'uiSrc/components/monaco-editor/components/dedicated-editor' import { QueryActions, QueryTutorials } from 'uiSrc/components/query' +import { + addOwnTokenToArgs, + findCurrentArgument, + splitQueryByArgs +} from 'uiSrc/pages/workbench/utils/query' +import { getRange, getRediSearchSignutureProvider, } from 'uiSrc/pages/workbench/utils/monaco' +import { CursorContext, FoundCommandArgument, SearchCommand, TokenType } from 'uiSrc/pages/workbench/types' +import SEARCH_COMMANDS_SPEC from 'uiSrc/pages/workbench/data/supported_commands.json' +import { + asSuggestionsRef, + getCommandsSuggestions, + getFieldsSuggestions, + getFunctionsSuggestions, + getGeneralSuggestions, + getIndexesSuggestions, + getNoIndexesSuggestion, + isIndexComplete +} from 'uiSrc/pages/workbench/utils/suggestions' +import { + COMMANDS_TO_GET_INDEX_INFO, + DefinedArgumentName, + EmptySuggestionsIds, + FIELD_START_SYMBOL +} from 'uiSrc/pages/workbench/constants' +import { useDebouncedEffect } from 'uiSrc/services' +import { fetchRedisearchInfoAction } from 'uiSrc/slices/browser/redisearch' import { aroundQuotesRegExp, argInQuotesRegExp, @@ -42,11 +67,11 @@ import { options, TUTORIALS } from './constants' - import styles from './styles.module.scss' export interface Props { query: string + indexes: RedisResponseBuffer[] activeMode: RunQueryMode resultsMode?: ResultsMode setQueryEl: Function @@ -64,6 +89,7 @@ let decorationCollection: Nullable { const { query = '', + indexes = [], activeMode, resultsMode, setQuery = () => {}, @@ -75,6 +101,17 @@ const Query = (props: Props) => { } = props let contribution: Nullable = null const [isDedicatedEditorOpen, setIsDedicatedEditorOpen] = useState(false) + const [selectedIndex, setSelectedIndex] = useState('') + + const suggestionsRef = useRef([]) + const helpWidgetRef = useRef({ + isOpen: false, + parent: null, + currentArg: null + }) + const indexesRef = useRef([]) + const attributesRef = useRef([]) + const isWidgetOpen = useRef(false) const input = useRef(null) const isWidgetEscaped = useRef(false) @@ -88,6 +125,17 @@ const Query = (props: Props) => { const { theme } = useContext(ThemeContext) const monacoObjects = useRef>(null) + const getCommandByName = (name: string) => + (name in SEARCH_COMMANDS_SPEC ? SEARCH_COMMANDS_SPEC[name] : (REDIS_COMMANDS_SPEC[name] || {})) + + const REDIS_COMMANDS = REDIS_COMMANDS_ARRAY + .map((name) => ({ ...getCommandByName(name), name })) + .map((command) => ({ + ...addOwnTokenToArgs(command.name!, command), + token: command.name!, + type: TokenType.Block + })) + const { instanceId = '' } = useParams<{ instanceId: string }>() const dispatch = useDispatch() @@ -104,6 +152,10 @@ const Query = (props: Props) => { disposeSignatureHelpProvider() }, []) + useEffect(() => { + indexesRef.current = indexes + }, [indexes]) + useEffect(() => { // HACK: The Monaco editor memoize the state and ignores updates to it execHistory = execHistoryItems @@ -141,6 +193,17 @@ const Query = (props: Props) => { isDedicatedEditorOpenRef.current = isDedicatedEditorOpen }, [isDedicatedEditorOpen]) + useDebouncedEffect(() => { + attributesRef.current = [] + if (!isIndexComplete(selectedIndex)) return + + const index = selectedIndex.replace(/^(['"])(.*)\1$/, '$2') + dispatch(fetchRedisearchInfoAction(index, + (data: any) => { + attributesRef.current = data?.attributes || [] + })) + }, 200, [selectedIndex]) + const triggerUpdateCursorPosition = (editor: monacoEditor.editor.IStandaloneCodeEditor) => { const position = editor.getPosition() isDedicatedEditorOpenRef.current = false @@ -251,37 +314,31 @@ const Query = (props: Props) => { } } - const onKeyChangeCursorMonaco = (e: monacoEditor.editor.ICursorPositionChangedEvent) => { - if (!monacoObjects.current) return - const { editor } = monacoObjects?.current - const model = editor.getModel() - - isWidgetOpen.current && hideSyntaxWidget(editor) - - if (!model || isDedicatedEditorOpenRef.current) { - return - } - - const command = findCompleteQuery(model, e.position, REDIS_COMMANDS_SPEC, REDIS_COMMANDS_ARRAY) - if (!command) { + const handleDslSyntax = ( + e: monacoEditor.editor.ICursorPositionChangedEvent, + command: Nullable + ) => { + const { editor } = monacoObjects?.current || {} + if (!command || !editor) { isWidgetEscaped.current = false return } const queryArgIndex = command.info?.arguments?.findIndex((arg) => arg.dsl) || -1 const cursorPosition = command.commandCursorPosition || 0 - if (!command.args?.length || queryArgIndex < 0) { + const { allArgs } = command || {} + if (!allArgs.length || queryArgIndex < 0) { isWidgetEscaped.current = false return } - const argIndex = findArgIndexByCursor(command.args, command.fullQuery, cursorPosition) + const argIndex = findArgIndexByCursor(allArgs, command.fullQuery, cursorPosition) if (argIndex === null) { isWidgetEscaped.current = false return } - const queryArg = command.args[argIndex] + const queryArg = allArgs[argIndex] const argDSL = command.info?.arguments?.[argIndex]?.dsl || '' if (queryArgIndex === argIndex && argInQuotesRegExp.test(queryArg)) { @@ -297,6 +354,57 @@ const Query = (props: Props) => { } } + const isSuggestionsOpened = () => { + const { editor } = monacoObjects.current || {} + if (!editor) return false + const suggestController = editor.getContribution('editor.contrib.suggestController') + return suggestController?.model?.state === 1 + } + + const onKeyChangeCursorMonaco = (e: monacoEditor.editor.ICursorPositionChangedEvent) => { + if (!monacoObjects.current) return + const { editor } = monacoObjects?.current + const model = editor.getModel() + + isWidgetOpen.current && hideSyntaxWidget(editor) + + if (!model || isDedicatedEditorOpenRef.current) { + return + } + + const command = findCompleteQuery(model, e.position, REDIS_COMMANDS_SPEC, REDIS_COMMANDS_ARRAY) + + const { data, forceHide, forceShow } = getSuggestions(editor, command) + + suggestionsRef.current = data + + if (!forceShow) { + editor.trigger('', 'editor.action.triggerParameterHints', '') + return + } + + if (data.length) { + helpWidgetRef.current.isOpen = false + triggerSuggestions() + return + } + + editor.trigger('', 'editor.action.triggerParameterHints', '') + + if (forceHide) { + setTimeout(() => editor?.trigger('', 'hideSuggestWidget', null), 0) + } else { + helpWidgetRef.current.isOpen = !isSuggestionsOpened() && helpWidgetRef.current.isOpen + } + + handleDslSyntax(e, command) + } + + const triggerSuggestions = () => { + const { editor } = monacoObjects.current || {} + setTimeout(() => editor?.trigger('', 'editor.action.triggerSuggest', { auto: false })) + } + const onExitSnippetMode = () => { if (!monacoObjects.current) return const { editor } = monacoObjects?.current @@ -414,18 +522,170 @@ const Query = (props: Props) => { }, SYNTAX_CONTEXT_ID) decorationCollection = editor.createDecorationsCollection() + + const suggestionWidget = editor.getContribution('editor.contrib.suggestController') + suggestionWidget?.onWillInsertSuggestItem(({ item }: Record<'item', any>) => { + if (item.completion.id === EmptySuggestionsIds.NoIndexes) { + updateHelpWidget(true) + editor.trigger('', 'hideSuggestWidget', null) + editor.trigger('', 'editor.action.triggerParameterHints', '') + } + }) + suggestionsRef.current = getSuggestions(editor).data } const setupMonacoRedisLang = (monaco: typeof monacoEditor) => { - disposeCompletionItemProvider = monaco.languages.registerCompletionItemProvider( - MonacoLanguage.Redis, - getRedisCompletionProvider(REDIS_COMMANDS_SPEC) - ).dispose - - disposeSignatureHelpProvider = monaco.languages.registerSignatureHelpProvider( - MonacoLanguage.Redis, - getRedisSignatureHelpProvider(REDIS_COMMANDS_SPEC, REDIS_COMMANDS_ARRAY, isWidgetOpen) - ).dispose + disposeCompletionItemProvider = monaco.languages.registerCompletionItemProvider(MonacoLanguage.Redis, { + provideCompletionItems: (): monacoEditor.languages.CompletionList => ({ suggestions: suggestionsRef.current }) + }).dispose + + disposeSignatureHelpProvider = monaco.languages.registerSignatureHelpProvider(MonacoLanguage.Redis, { + provideSignatureHelp: (): any => getRediSearchSignutureProvider(helpWidgetRef?.current) + }).dispose + } + + const updateHelpWidget = (isOpen: boolean, parent?: SearchCommand, currentArg?: SearchCommand) => { + helpWidgetRef.current = { + isOpen, + parent: parent || helpWidgetRef.current.parent, + currentArg: currentArg || helpWidgetRef.current.currentArg } + } + + const getSuggestions = ( + editor: monacoEditor.editor.IStandaloneCodeEditor, + command?: Nullable + ): { + forceHide: boolean + forceShow: boolean + data: monacoEditor.languages.CompletionItem[] + } => { + const position = editor.getPosition() + const model = editor.getModel() + + if (!position || !model) return asSuggestionsRef([]) + const word = model.getWordUntilPosition(position) + const range = getRange(position, word) + + if (position.column === 1) { + if (command) return asSuggestionsRef([]) + + return asSuggestionsRef(getCommandsSuggestions(REDIS_COMMANDS, range), false) + } + + if (!command) { + return asSuggestionsRef([], false) + } + + const { allArgs, args, cursor } = command + const { prevCursorChar } = cursor + const [beforeOffsetArgs, [currentOffsetArg]] = args + + if (COMMANDS_TO_GET_INDEX_INFO.some((name) => name === command.name)) { + setSelectedIndex(allArgs[1] || '') + } + + const cursorContext: CursorContext = { ...cursor, currentOffsetArg, offset: command.commandCursorPosition || 0 } + const foundArg = findCurrentArgument(REDIS_COMMANDS, beforeOffsetArgs) + + if (!command.name.startsWith('FT.')) { + updateHelpWidget(true, foundArg?.parent, foundArg?.stopArg) + return asSuggestionsRef([]) + } + + if (prevCursorChar === FIELD_START_SYMBOL) return handleFieldSuggestions(foundArg, range) + + switch (foundArg?.stopArg?.name) { + case DefinedArgumentName.index: { + return handleIndexSuggestions(command.info as SearchCommand, foundArg, currentOffsetArg, range) + } + case DefinedArgumentName.query: { + return handleQuerySuggestions(command.info as SearchCommand, foundArg) + } + default: { + return handleCommonSuggestions(command.fullQuery, foundArg, allArgs, cursorContext, range) + } + } + } + + const handleFieldSuggestions = (foundArg: Nullable, range: monacoEditor.IRange) => { + const isInQuery = foundArg?.stopArg?.name === DefinedArgumentName.query + const fieldSuggestions = getFieldsSuggestions(attributesRef.current, range, true, isInQuery) + return asSuggestionsRef(fieldSuggestions, true) + } + + const handleIndexSuggestions = ( + command: SearchCommand, + foundArg: FoundCommandArgument, + currentOffsetArg: Nullable, + range: monacoEditor.IRange + ) => { + const isIndex = indexesRef.current.length > 0 + updateHelpWidget(isIndex, command, foundArg?.stopArg) + + if (!isIndex) { + updateHelpWidget(!!currentOffsetArg) + return asSuggestionsRef(!currentOffsetArg ? getNoIndexesSuggestion(range) : [], true) + } + + if (!isIndex || currentOffsetArg) return asSuggestionsRef([], !currentOffsetArg) + + const argumentIndex = command?.arguments + ?.findIndex(({ name }) => foundArg?.stopArg?.name === name) + const isNextArgQuery = isNumber(argumentIndex) + && command?.arguments?.[argumentIndex + 1]?.name === DefinedArgumentName.query + + return asSuggestionsRef(getIndexesSuggestions(indexesRef.current, range, isNextArgQuery)) + } + + const handleQuerySuggestions = (command: SearchCommand, foundArg: FoundCommandArgument) => { + updateHelpWidget(true, command, foundArg?.stopArg) + return asSuggestionsRef([], false) + } + + const handleExpressionSuggestions = ( + value: string, + foundArg: FoundCommandArgument, + cursorContext: CursorContext, + range: monacoEditor.IRange + ) => { + updateHelpWidget(true, foundArg?.parent, foundArg?.stopArg) + + const { isCursorInQuotes, offset, argLeftOffset } = cursorContext + if (!isCursorInQuotes) return asSuggestionsRef([]) + + const stringBeforeCursor = value.substring(argLeftOffset, offset) || '' + const expression = stringBeforeCursor.replace(/^["']|["']$/g, '') + const { args } = splitQueryByArgs(expression, offset - argLeftOffset) + const [, [currentArg]] = args + + const functions = foundArg?.stopArg?.arguments ?? [] + const suggestions = getFunctionsSuggestions(functions, range) + const isStartsWithFunction = functions.some(({ token }) => token?.startsWith(currentArg)) + + return asSuggestionsRef(suggestions, true, isStartsWithFunction) + } + + const handleCommonSuggestions = ( + value: string, + foundArg: Nullable, + allArgs: string[], + cursorContext: CursorContext, + range: monacoEditor.IRange + ) => { + if (foundArg?.stopArg?.expression) return handleExpressionSuggestions(value, foundArg, cursorContext, range) + + const { prevCursorChar, nextCursorChar, isCursorInQuotes } = cursorContext + const shouldHideSuggestions = isCursorInQuotes || nextCursorChar || (prevCursorChar) + if (shouldHideSuggestions) return asSuggestionsRef([]) + + const { + suggestions, + forceHide, + helpWidgetData + } = getGeneralSuggestions(foundArg, allArgs, range, attributesRef.current) + + if (helpWidgetData) updateHelpWidget(helpWidgetData.isOpen, helpWidgetData.parent, helpWidgetData.currentArg) + return asSuggestionsRef(suggestions, forceHide) } const isLoading = loading || processing diff --git a/redisinsight/ui/src/pages/workbench/components/query/Query/constants.ts b/redisinsight/ui/src/pages/workbench/components/query/Query/constants.ts index 3b5e92e88b..eb3b518284 100644 --- a/redisinsight/ui/src/pages/workbench/components/query/Query/constants.ts +++ b/redisinsight/ui/src/pages/workbench/components/query/Query/constants.ts @@ -1,3 +1,4 @@ +import { merge } from 'lodash' import { defaultMonacoOptions, TutorialsIds } from 'uiSrc/constants' export const argInQuotesRegExp = /^['"](.|[\r\n])*['"]$/ @@ -6,7 +7,16 @@ export const aroundQuotesRegExp = /(^["']|["']$)/g export const SYNTAX_CONTEXT_ID = 'syntaxWidgetContext' export const SYNTAX_WIDGET_ID = 'syntax.content.widget' -export const options = { ...defaultMonacoOptions } +export const options = merge(defaultMonacoOptions, + { + suggest: { + showWords: false, + showIcons: true, + insertMode: 'replace', + filterGraceful: false, + matchOnWordStartOnly: true + } + }) export const TUTORIALS = [ { diff --git a/redisinsight/ui/src/pages/workbench/components/query/QueryWrapper.tsx b/redisinsight/ui/src/pages/workbench/components/query/QueryWrapper.tsx index 56a7d9e974..d6e1d2f351 100644 --- a/redisinsight/ui/src/pages/workbench/components/query/QueryWrapper.tsx +++ b/redisinsight/ui/src/pages/workbench/components/query/QueryWrapper.tsx @@ -1,9 +1,11 @@ -import React from 'react' -import { useSelector } from 'react-redux' +import React, { useEffect } from 'react' +import { useDispatch, useSelector } from 'react-redux' import { EuiLoadingContent } from '@elastic/eui' import { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands' import { RunQueryMode, ResultsMode } from 'uiSrc/slices/interfaces/workbench' +import { fetchRedisearchListAction, redisearchListSelector } from 'uiSrc/slices/browser/redisearch' +import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import Query from './Query' import styles from './Query/styles.module.scss' @@ -31,9 +33,18 @@ const QueryWrapper = (props: Props) => { onQueryChangeMode, onChangeGroupMode } = props - const { - loading: isCommandsLoading, - } = useSelector(appRedisCommandsSelector) + const { loading: isCommandsLoading, } = useSelector(appRedisCommandsSelector) + const { id: connectedIndstanceId } = useSelector(connectedInstanceSelector) + const { data: indexes = [] } = useSelector(redisearchListSelector) + + const dispatch = useDispatch() + + useEffect(() => { + if (!connectedIndstanceId) return + + // fetch indexes + dispatch(fetchRedisearchListAction()) + }, [connectedIndstanceId]) const Placeholder = (
@@ -47,6 +58,7 @@ const QueryWrapper = (props: Props) => { ) : ( + isBlocked: boolean + append: Maybe> + parent: Maybe +} + +export interface CursorContext { + prevCursorChar: string + nextCursorChar: string + isCursorInQuotes: boolean + currentOffsetArg: string + offset: number + argLeftOffset: number + argRightOffset: number +} diff --git a/redisinsight/ui/src/pages/workbench/utils/monaco.ts b/redisinsight/ui/src/pages/workbench/utils/monaco.ts new file mode 100644 index 0000000000..3bce209dd4 --- /dev/null +++ b/redisinsight/ui/src/pages/workbench/utils/monaco.ts @@ -0,0 +1,64 @@ +import { monaco } from 'react-monaco-editor' +import * as monacoEditor from 'monaco-editor' +import { isString } from 'lodash' +import { generateDetail } from 'uiSrc/pages/workbench/utils/query' +import { SearchCommand, TokenType } from 'uiSrc/pages/workbench/types' +import { Maybe } from 'uiSrc/utils' + +export const setCursorPositionAtTheEnd = (editor: monacoEditor.editor.IStandaloneCodeEditor) => { + if (!editor) return + + const rows = editor.getValue().split('\n') + + editor.setPosition({ + column: rows[rows.length - 1].trimEnd().length + 1, + lineNumber: rows.length + }) + + editor.focus() +} + +export const getRange = (position: monaco.Position, word: monaco.editor.IWordAtPosition): monaco.IRange => ({ + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + endColumn: word.endColumn, + startColumn: word.startColumn, +}) + +export const buildSuggestion = (arg: SearchCommand, range: monaco.IRange, options: any = {}) => { + const extraQuotes = arg.expression ? '\'$1\'' : '' + return { + label: isString(arg) ? arg : arg.token || arg.arguments?.[0].token || arg.name || '', + insertText: `${arg.token || arg.arguments?.[0].token || arg.name?.toUpperCase() || ''} ${extraQuotes}`, + insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, + range, + kind: options?.kind || monacoEditor.languages.CompletionItemKind.Function, + ...options, + } +} + +export const getRediSearchSignutureProvider = (options: Maybe<{ + isOpen: boolean + currentArg: SearchCommand + parent: Maybe +}>) => { + const { isOpen, currentArg, parent } = options || {} + if (!isOpen) return null + + const label = generateDetail(parent) + const arg = currentArg?.type === TokenType.Block + ? currentArg?.arguments?.[0]?.name + : (currentArg?.name || currentArg?.type || '') + + return { + dispose: () => {}, + value: { + activeParameter: 0, + activeSignature: 0, + signatures: [{ + label: label || '', + parameters: [{ label: arg }] + }] + } + } +} diff --git a/redisinsight/ui/src/pages/workbench/utils/profile.ts b/redisinsight/ui/src/pages/workbench/utils/profile.ts new file mode 100644 index 0000000000..5ba0ae140c --- /dev/null +++ b/redisinsight/ui/src/pages/workbench/utils/profile.ts @@ -0,0 +1,40 @@ +import { ProfileQueryType, SEARCH_COMMANDS, GRAPH_COMMANDS } from '../constants' + +export const generateGraphProfileQuery = (query: string, type: ProfileQueryType) => { + const q = query?.split(' ')?.slice(1) + + if (q) { + return [`graph.${type.toLowerCase()}`, ...q].join(' ') + } + + return null +} + +export const generateSearchProfileQuery = (query: string, type: ProfileQueryType) => { + const commandSplit = query?.split(' ') + const cmd = commandSplit?.[0] + + if (!commandSplit || !cmd) { + return null + } + + if (type === ProfileQueryType.Explain) { + return [`ft.${type.toLowerCase()}`, ...commandSplit?.slice(1)].join(' ') + } + const index = commandSplit?.[1] + + const queryType = cmd.split('.')?.[1] // SEARCH / AGGREGATE + return [`ft.${type.toLowerCase()}`, index, queryType, 'QUERY', ...commandSplit?.slice(2)].join(' ') +} + +export const generateProfileQueryForCommand = (query: string, type: ProfileQueryType) => { + const cmd = query?.split(' ')?.[0]?.toLowerCase() + + if (GRAPH_COMMANDS.includes(cmd)) { + return generateGraphProfileQuery(query, type) + } if (SEARCH_COMMANDS.includes(cmd)) { + return generateSearchProfileQuery(query, type) + } + + return null +} diff --git a/redisinsight/ui/src/pages/workbench/utils/query.ts b/redisinsight/ui/src/pages/workbench/utils/query.ts new file mode 100644 index 0000000000..c87887fbb8 --- /dev/null +++ b/redisinsight/ui/src/pages/workbench/utils/query.ts @@ -0,0 +1,479 @@ +/* eslint-disable no-continue */ + +import { isNumber, toNumber } from 'lodash' +import { generateArgsNames, Maybe, Nullable } from 'uiSrc/utils' +import { CommandProvider } from 'uiSrc/constants' +import { COMPOSITE_ARGS } from 'uiSrc/pages/workbench/constants' +import { ArgName, FoundCommandArgument, SearchCommand, SearchCommandTree, TokenType } from '../types' + +export const splitQueryByArgs = (query: string, position: number = 0) => { + const args: [string[], string[]] = [[], []] + let arg = '' + let inQuotes = false + let escapeNextChar = false + let quoteChar = '' + let isCursorInQuotes = false + let lastArg = '' + let argLeftOffset = 0 + let argRightOffset = 0 + + const pushToProperTuple = (isAfterOffset: boolean, arg: string) => { + lastArg = arg + isAfterOffset ? args[1].push(arg) : args[0].push(arg) + } + + const updateLastArgument = (isAfterOffset: boolean, arg: string) => { + const argsBySide = args[isAfterOffset ? 1 : 0] + argsBySide[argsBySide.length - 1] = `${argsBySide[argsBySide.length - 1]} ${arg}` + } + + const updateArgOffsets = (left: number, right: number) => { + argLeftOffset = left + argRightOffset = right + } + + for (let i = 0; i < query.length; i++) { + const char = query[i] + const isAfterOffset = i >= position + (inQuotes ? -1 : 0) + + if (escapeNextChar) { + arg += char + escapeNextChar = !quoteChar + } else if (char === '\\') { + escapeNextChar = true + } else if (inQuotes) { + if (char === quoteChar) { + inQuotes = false + const argWithChat = arg + char + + if (isAfterOffset && !argLeftOffset) { + updateArgOffsets(i - arg.length, i + 1) + } + + if (isCompositeArgument(argWithChat, lastArg)) { + updateLastArgument(isAfterOffset, argWithChat) + } else { + pushToProperTuple(isAfterOffset, argWithChat) + } + + arg = '' + } else { + arg += char + } + } else if (char === '"' || char === "'") { + inQuotes = true + quoteChar = char + arg += char + } else if (char === ' ' || char === '\n') { + if (arg.length > 0) { + if (isAfterOffset && !argLeftOffset) { + updateArgOffsets(i - arg.length, i) + } + + if (isCompositeArgument(arg, lastArg)) { + updateLastArgument(isAfterOffset, arg) + } else { + pushToProperTuple(isAfterOffset, arg) + } + + arg = '' + } + } else { + arg += char + } + + if (i === position - 1) isCursorInQuotes = inQuotes + } + + if (arg.length > 0) { + if (!argLeftOffset) updateArgOffsets(query.length - arg.length, query.length) + pushToProperTuple(true, arg) + } + + const cursor = { + isCursorInQuotes, + prevCursorChar: query[position - 1]?.trim() || '', + nextCursorChar: query[position]?.trim() || '', + argLeftOffset, + argRightOffset + } + + return { args, cursor } +} + +export const findCurrentArgument = ( + args: SearchCommand[], + prev: string[], + parent?: SearchCommandTree +): Nullable => { + for (let i = prev.length - 1; i >= 0; i--) { + const arg = prev[i] + const currentArg = findArgByToken(args, arg) + const currentWithParent: SearchCommandTree = { ...currentArg, parent } + + if (currentArg?.arguments && currentArg?.type === TokenType.Block) { + return findCurrentArgument(currentArg.arguments, prev.slice(i), currentWithParent) + } + + const tokenIndex = args.findIndex((cArg) => + cArg.token?.toLowerCase() === arg.toLowerCase()) + const token = args[tokenIndex] + + if (token) { + const pastArgs = prev.slice(i) + const commandArgs = parent ? args.slice(tokenIndex, args.length) : [token] + + // getArgByRest - here we preparing the list of arguments which can be inserted, + // this is the main function which creates the list of arguments + return { + ...getArgumentSuggestions({ tokenArgs: pastArgs, levelArgs: prev }, commandArgs, parent), + parent: parent || token + } + } + } + + return null +} + +const findStopArgumentInQuery = ( + queryArgs: string[], + restCommandArgs: Maybe = [], +): { + restArguments: SearchCommand[] + stopArgIndex: number + argumentsIntered?: number + isBlocked: boolean + parent?: SearchCommand +} => { + let currentCommandArgIndex = 0 + let argumentsIntered = 0 + let isBlockedOnCommand = false + let multipleIndexStart = 0 + let multipleCountNumber = 0 + + const moveToNextCommandArg = () => { + currentCommandArgIndex++ + argumentsIntered++ + } + const blockCommand = () => { isBlockedOnCommand = true } + const unBlockCommand = () => { isBlockedOnCommand = false } + + const skipArg = () => { + argumentsIntered -= 1 + moveToNextCommandArg() + unBlockCommand() + } + + for (let i = 0; i < queryArgs.length; i++) { + const arg = queryArgs[i] + const currentCommandArg = restCommandArgs[currentCommandArgIndex] + + if (currentCommandArg?.type === TokenType.PureToken) { + skipArg() + continue + } + + if (!isBlockedOnCommand && currentCommandArg?.optional) { + const isNotToken = currentCommandArg?.token && currentCommandArg.token !== arg.toUpperCase() + const isNotOneOfToken = !currentCommandArg?.token && currentCommandArg?.type === TokenType.OneOf + && currentCommandArg?.arguments?.every(({ token }) => token !== arg.toUpperCase()) + + if (isNotToken || isNotOneOfToken) { + moveToNextCommandArg() + skipArg() + continue + } + } + + if (currentCommandArg?.type === TokenType.Block) { + let blockArguments = currentCommandArg.arguments ? [...currentCommandArg.arguments] : [] + const nArgs = toNumber(queryArgs[i - 1]) || 0 + + // if block is multiple - we duplicate nArgs inner arguments + if (currentCommandArg?.multiple && nArgs) { + blockArguments = Array(nArgs).fill(currentCommandArg.arguments).flat() + } + + const currentQueryArg = queryArgs.slice(i)?.[0]?.toUpperCase() + const isBlockHasToken = blockArguments?.[0]?.token === currentQueryArg + + if (currentCommandArg.token && !isBlockHasToken && currentQueryArg) { + blockArguments.unshift({ + type: TokenType.PureToken, + token: currentQueryArg + }) + } + + const blockSuggestion = findStopArgumentInQuery(queryArgs.slice(i), blockArguments) + const stopArg = blockSuggestion.restArguments?.[blockSuggestion.stopArgIndex] + const { argumentsIntered } = blockSuggestion + + if (nArgs && currentCommandArg?.multiple && isNumber(argumentsIntered) && argumentsIntered >= nArgs) { + i += queryArgs.slice(i).length - 1 + skipArg() + continue + } + + if (blockSuggestion.isBlocked || stopArg) { + return { + ...blockSuggestion, + parent: currentCommandArg + } + } + + i += queryArgs.slice(i).length - 1 + skipArg() + continue + } + + // if we are on token - that requires one more argument + if (currentCommandArg?.token === arg.toUpperCase()) { + blockCommand() + continue + } + + if (currentCommandArg?.name === ArgName.NArgs) { + const numberOfArgs = toNumber(arg) + + if (numberOfArgs === 0) { + moveToNextCommandArg() + skipArg() + continue + } + + moveToNextCommandArg() + blockCommand() + continue + } + + if (currentCommandArg?.type === TokenType.OneOf && currentCommandArg?.optional) { + // if oneof is optional then we can switch to another argument + if (!currentCommandArg?.arguments?.some(({ token }) => token === arg)) { + moveToNextCommandArg() + } + + skipArg() + continue + } + + if (currentCommandArg?.multiple) { + if (!multipleIndexStart) { + multipleCountNumber = toNumber(queryArgs[i - 1]) + multipleIndexStart = i - 1 + } + + if (i - multipleIndexStart >= multipleCountNumber) { + skipArg() + multipleIndexStart = 0 + continue + } + + blockCommand() + continue + } + + moveToNextCommandArg() + + isBlockedOnCommand = false + } + + return { + restArguments: restCommandArgs, + stopArgIndex: currentCommandArgIndex, + argumentsIntered, + isBlocked: isBlockedOnCommand + } +} + +export const getArgumentSuggestions = ( + { tokenArgs, levelArgs }: { + tokenArgs: string[], + levelArgs: string[] + }, + pastCommandArgs: SearchCommand[], + current?: SearchCommandTree +): { + isComplete: boolean + stopArg: Maybe, + isBlocked: boolean, + append: Array, +} => { + const { + restArguments, + stopArgIndex, + isBlocked: isWasBlocked, + parent + } = findStopArgumentInQuery(tokenArgs, pastCommandArgs) + + const prevArg = restArguments[stopArgIndex - 1] + const stopArgument = restArguments[stopArgIndex] + const restNotFilledArgs = restArguments.slice(stopArgIndex) + + const isOneOfArgument = stopArgument?.type === TokenType.OneOf + || (stopArgument?.type === TokenType.PureToken && current?.parent?.type === TokenType.OneOf) + + if (isWasBlocked) { + return { + isComplete: false, + stopArg: stopArgument, + isBlocked: !isOneOfArgument, + append: isOneOfArgument ? [stopArgument.arguments!] : [], + } + } + + const isPrevArgWasMandatory = prevArg && !prevArg.optional + if (isPrevArgWasMandatory && stopArgument && !stopArgument.optional) { + const isCanAppend = stopArgument?.token || isOneOfArgument + const append = isCanAppend ? [[isOneOfArgument ? stopArgument.arguments! : stopArgument].flat()] : [] + + return { + isComplete: false, + stopArg: stopArgument, + isBlocked: !isCanAppend, + append, + } + } + + // if we finished argument - stopArgument will be undefined, then we get it as token + const lastArgument = stopArgument ?? restArguments[0] + const isBlockHasParent = current?.arguments?.some(({ name }) => parent?.name && name === parent?.name) + const foundParent = isBlockHasParent ? { ...parent, parent: current } : (parent || current) + + const isBlockComplete = !stopArgument && current?.name === lastArgument?.name + const beforeMandatoryOptionalArgs = getAllRestArguments(foundParent, lastArgument, levelArgs, isBlockComplete) + const requiredArgsLength = restNotFilledArgs.filter((arg) => !arg.optional).length + + return { + isComplete: requiredArgsLength === 0, + stopArg: stopArgument, + isBlocked: false, + append: beforeMandatoryOptionalArgs, + } +} + +export const getRestArguments = ( + current: Maybe, + stopArgument: Nullable +): SearchCommandTree[] => { + const argumentIndexInArg = current?.arguments + ?.findIndex(({ name }) => name === stopArgument?.name) + const nextMandatoryIndex = argumentIndexInArg && argumentIndexInArg > -1 ? current?.arguments + ?.findIndex(({ optional }, i) => !optional && i > argumentIndexInArg) : -1 + const prevMandatory = current?.arguments?.slice(0, argumentIndexInArg).reverse() + .find(({ optional }) => !optional) + const prevMandatoryIndex = current?.arguments?.findIndex(({ name }) => name === prevMandatory?.name) + + const beforeMandatoryOptionalArgs = ( + nextMandatoryIndex && nextMandatoryIndex > -1 + ? current?.arguments?.slice(prevMandatoryIndex, nextMandatoryIndex) + : current?.arguments?.slice((prevMandatoryIndex || 0) + 1) + ) || [] + + const nextMandatoryArg = nextMandatoryIndex && nextMandatoryIndex > -1 + ? current?.arguments?.[nextMandatoryIndex] + : undefined + + if (nextMandatoryArg?.token) { + beforeMandatoryOptionalArgs.unshift(nextMandatoryArg) + } + + if (nextMandatoryArg?.type === TokenType.OneOf) { + beforeMandatoryOptionalArgs.unshift(...(nextMandatoryArg.arguments || [])) + } + + return beforeMandatoryOptionalArgs.map((arg) => ({ ...arg, parent: current })) +} + +export const getAllRestArguments = ( + current: Maybe, + stopArgument: Nullable, + prevStringArgs: string[] = [], + skipLevel = false +) => { + const appendArgs: Array = [] + const currentLvlNextArgs = removeNotSuggestedArgs( + prevStringArgs, + getRestArguments(current, stopArgument) + ) + + if (!skipLevel) { + appendArgs.push(fillArgsByType(currentLvlNextArgs)) + } + + if (current?.parent) { + const parentArgs = getAllRestArguments(current.parent, current, skipLevel ? prevStringArgs : []) + if (parentArgs?.length) { + appendArgs.push(...parentArgs) + } + } + + return appendArgs +} + +export const removeNotSuggestedArgs = (args: string[], commandArgs: SearchCommandTree[]) => + commandArgs.filter((arg) => { + if (arg.token && arg.multiple) return true + + if (arg.type === TokenType.OneOf) { + return !args + .some((queryArg) => arg.arguments + ?.some((oneOfArg) => oneOfArg.token?.toUpperCase() === queryArg.toUpperCase())) + } + + if (arg.type === TokenType.Block) { + return arg.arguments?.[0]?.token && !args.includes(arg.arguments?.[0]?.token?.toUpperCase()) + } + + return arg.token && !args.includes(arg.token) + }) + +export const fillArgsByType = (args: SearchCommand[], expandBlock = true): SearchCommandTree[] => { + const result: SearchCommandTree[] = [] + + for (let i = 0; i < args.length; i++) { + const currentArg = args[i] + + if (expandBlock && currentArg.type === TokenType.OneOf && !currentArg.token) { + result.push(...(currentArg?.arguments?.map((arg) => ({ ...arg, parent: currentArg })) || [])) + } + + if (currentArg.type === TokenType.Block) { + result.push({ + multiple: currentArg.multiple, + optional: currentArg.optional, + parent: currentArg, + ...(currentArg?.arguments?.[0] as SearchCommand || {}), + }) + } + if (currentArg.token) result.push(currentArg) + } + + return result +} + +export const findArgByToken = (list: SearchCommand[], arg: string): Maybe => + list.find((cArg) => + (cArg.type === TokenType.OneOf + ? cArg.arguments?.some((oneOfArg: SearchCommand) => oneOfArg?.token?.toLowerCase() === arg?.toLowerCase()) + : cArg.arguments?.[0]?.token?.toLowerCase() === arg.toLowerCase())) + +export const isCompositeArgument = (arg: string, prevArg?: string) => + COMPOSITE_ARGS.includes([prevArg?.toUpperCase(), arg?.toUpperCase()].join(' ')) + +export const generateDetail = (command: Maybe) => { + if (!command) return '' + if (command.arguments) return generateArgsNames(CommandProvider.Main, command.arguments).join(' ') + if (command.token) { + if (command.type === TokenType.PureToken) return command.token + return `${command.token}` + } + + return '' +} + +export const addOwnTokenToArgs = (token: string, command: SearchCommand) => { + if (command.arguments) { + return ({ ...command, arguments: [{ token, type: TokenType.PureToken }, ...command.arguments] }) + } + return command +} diff --git a/redisinsight/ui/src/pages/workbench/utils/suggestions.ts b/redisinsight/ui/src/pages/workbench/utils/suggestions.ts new file mode 100644 index 0000000000..288ae6dc69 --- /dev/null +++ b/redisinsight/ui/src/pages/workbench/utils/suggestions.ts @@ -0,0 +1,198 @@ +import { monaco } from 'react-monaco-editor' +import * as monacoEditor from 'monaco-editor' +import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' +import { bufferToString, formatLongName, generateArgsForInsertText, getCommandMarkdown, Nullable } from 'uiSrc/utils' +import { FoundCommandArgument, SearchCommand } from 'uiSrc/pages/workbench/types' +import { DefinedArgumentName, EmptySuggestionsIds } from 'uiSrc/pages/workbench/constants' +import { getUtmExternalLink } from 'uiSrc/utils/links' +import { removeNotSuggestedArgs, generateDetail } from './query' +import { buildSuggestion, } from './monaco' + +export const asSuggestionsRef = ( + suggestions: monacoEditor.languages.CompletionItem[], + forceHide = true, + forceShow = true +) => ({ + data: suggestions, + forceHide, + forceShow +}) + +const NO_INDEXES_DOC_LINK = getUtmExternalLink('https://redis.io/docs/latest/commands/ft.create/', { campaign: 'workbench' }) +export const getNoIndexesSuggestion = (range: monaco.IRange) => [ + { + id: EmptySuggestionsIds.NoIndexes, + label: 'No indexes to display', + kind: monacoEditor.languages.CompletionItemKind.Issue, + insertText: '', + insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, + range, + detail: 'Create an index', + documentation: { + value: `See the [documentation](${NO_INDEXES_DOC_LINK}) for detailed instructions on how to create an index.`, + } + } +] + +export const getIndexesSuggestions = (indexes: RedisResponseBuffer[], range: monaco.IRange, isNextArgQuery = true) => + indexes.map((index) => { + const value = formatLongName(bufferToString(index)) + const insertQueryQuotes = isNextArgQuery ? " '\${1:query to search}'" : '' + + return { + label: value || ' ', + kind: monacoEditor.languages.CompletionItemKind.Snippet, + insertText: `'${value}'${insertQueryQuotes} `, + insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, + range, + detail: value || ' ', + } + }) + +export const addFieldAttribute = (attribute: string, type: string) => { + switch (type) { + case 'TAG': return `${attribute}:{\${1:tag}}` + case 'TEXT': return `${attribute}:(\${1:term})` + case 'NUMERIC': return `${attribute}:[\${1:range}]` + case 'GEO': return `${attribute}:[\${1:lon} \${2:lat} \${3:radius} \${4:unit}]` + case 'VECTOR': return `${attribute} \\$\${1:vector}` + default: return attribute + } +} + +export const getFieldsSuggestions = ( + fields: any[], + range: monaco.IRange, + spaceAfter = false, + withType = false +) => + fields.map((field) => { + const { attribute, type } = field + const attibuteText = attribute.trim() ? attribute : `\\'${attribute}\\'` + const insertText = withType ? addFieldAttribute(attibuteText, type) : attibuteText + + return { + label: attribute || ' ', + kind: monacoEditor.languages.CompletionItemKind.Reference, + insertText: `${insertText}${spaceAfter ? ' ' : ''}`, + insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, + range, + detail: attribute || ' ', + } + }) + +const insertFunctionArguments = (args: SearchCommand[]) => + generateArgsForInsertText( + args.map(({ token, optional }) => (optional ? `[${token}]` : (token || ''))) as string[], + ', ' + ) + +export const getFunctionsSuggestions = (functions: SearchCommand[], range: monaco.IRange) => functions + .map(({ token, summary, arguments: args }) => ({ + label: token || '', + insertText: `${token}(${insertFunctionArguments(args || [])})`, + insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, + range, + kind: monacoEditor.languages.CompletionItemKind.Function, + detail: summary + })) + +export const getCommandsSuggestions = (commands: SearchCommand[], range: monaco.IRange) => + commands.map((command) => buildSuggestion(command, range, { + detail: generateDetail(command), + insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, + documentation: { + value: getCommandMarkdown(command as any) + }, + })) + +export const getMandatoryArgumentSuggestions = ( + foundArg: FoundCommandArgument, + fields: any[], + range: monaco.IRange +): monacoEditor.languages.CompletionItem[] => { + if (foundArg.stopArg?.name === DefinedArgumentName.field) { + if (!fields.length) return [] + return getFieldsSuggestions(fields, range, true) + } + + if (foundArg.isBlocked) return [] + if (foundArg.append?.length) { + return foundArg.append[0].map((arg: any) => buildSuggestion(arg, range, { + kind: monacoEditor.languages.CompletionItemKind.Property, + detail: generateDetail(foundArg?.parent) + })) + } + + return [] +} + +export const getCommandSuggestions = ( + foundArg: Nullable, + allArgs: string[], + range: monaco.IRange, +) => { + const appendCommands = foundArg?.append ?? [] + const suggestions = [] + + for (let i = 0; i < appendCommands.length; i++) { + const isLastLevel = i === appendCommands.length - 1 + const filteredFileldArgs = isLastLevel + ? removeNotSuggestedArgs(allArgs, appendCommands[i]) + : appendCommands[i] + + const leveledSuggestions = filteredFileldArgs + .map((arg) => buildSuggestion(arg, range, { + sortText: `${i}`, + kind: isLastLevel + ? monacoEditor.languages.CompletionItemKind.Reference + : monacoEditor.languages.CompletionItemKind.Property, + detail: generateDetail(arg?.parent) + })) + + suggestions.push(leveledSuggestions) + } + + return suggestions.flat() +} + +export const getGeneralSuggestions = ( + foundArg: Nullable, + allArgs: string[], + range: monacoEditor.IRange, + fields: any[] +): { + suggestions: monacoEditor.languages.CompletionItem[], + forceHide?: boolean + helpWidgetData?: any +} => { + if (foundArg && !foundArg.isComplete) { + return { + suggestions: getMandatoryArgumentSuggestions(foundArg, fields, range), + helpWidgetData: { isOpen: !!foundArg?.stopArg, parent: foundArg?.parent, currentArg: foundArg?.stopArg } + } + } + + return { + suggestions: getCommandSuggestions(foundArg, allArgs, range), + helpWidgetData: { isOpen: false } + } +} + +export const isIndexComplete = (index: string) => { + if (index.length === 0) return false + + const firstChar = index[0] + const lastChar = index[index.length - 1] + + if (firstChar !== '"' && firstChar !== "'") return true + if (index.length === 1 && (firstChar === '"' || firstChar === "'")) return false + if (firstChar !== lastChar) return false + + let escape = false + for (let i = 1; i < index.length - 1; i++) { + escape = index[i] === '\\' && !escape + } + + return !escape +} diff --git a/redisinsight/ui/src/utils/monaco/monacoInterfaces.ts b/redisinsight/ui/src/utils/monaco/monacoInterfaces.ts index fac8c23166..e092777374 100644 --- a/redisinsight/ui/src/utils/monaco/monacoInterfaces.ts +++ b/redisinsight/ui/src/utils/monaco/monacoInterfaces.ts @@ -10,7 +10,15 @@ export interface IMonacoCommand { export interface IMonacoQuery { name: string fullQuery: string - args?: string[] + args: [string[], string[]], + cursor: { + isCursorInQuotes: boolean, + prevCursorChar: string, + nextCursorChar: string, + argLeftOffset: number, + argRightOffset: number + } + allArgs: string[] info?: ICommand commandPosition: any position?: monacoEditor.Position diff --git a/redisinsight/ui/src/utils/monaco/monacoRedisMonarchTokensProvider.ts b/redisinsight/ui/src/utils/monaco/monacoRedisMonarchTokensProvider.ts index 9f41113289..a007c4cf3c 100644 --- a/redisinsight/ui/src/utils/monaco/monacoRedisMonarchTokensProvider.ts +++ b/redisinsight/ui/src/utils/monaco/monacoRedisMonarchTokensProvider.ts @@ -1,9 +1,16 @@ import { monaco as monacoEditor } from 'react-monaco-editor' +import { remove } from 'lodash' +import { SearchCommand } from 'uiSrc/pages/workbench/types' const STRING_DOUBLE = 'string.double' -export const getRedisMonarchTokensProvider = (commands: string[]): monacoEditor.languages.IMonarchLanguage => ( - { +export const getRedisMonarchTokensProvider = (commands: SearchCommand[]): monacoEditor.languages.IMonarchLanguage => { + const commandRedisCommands = [...commands] + const searchCommands = remove(commandRedisCommands, ({ token }) => token?.startsWith('FT.')) + const COMMON_COMMANDS_REGEX = `(${commandRedisCommands.map(({ token }) => token).join('|')})\\b` + const SEARCH_COMMANDS_REGEX = `(${searchCommands.map(({ token }) => token).join('|')})\\b` + + return { defaultToken: '', tokenPostfix: '.redis', ignoreCase: true, @@ -11,26 +18,13 @@ export const getRedisMonarchTokensProvider = (commands: string[]): monacoEditor. { open: '[', close: ']', token: 'delimiter.square' }, { open: '(', close: ')', token: 'delimiter.parenthesis' }, ], - keywords: commands, - operators: [ - // NOT SUPPORTED - ], - builtinFunctions: [ - // NOT SUPPORTED - ], - builtinVariables: [ - // NOT SUPPORTED - ], - pseudoColumns: [ - // NOT SUPPORTED - ], + keywords: commands.map(({ token }) => token), + operators: [], tokenizer: { root: [ { include: '@whitespace' }, - { include: '@pseudoColumns' }, { include: '@numbers' }, { include: '@strings' }, - { include: '@scopes' }, { include: '@keyword' }, [/[;,.]/, 'delimiter'], [/[()]/, '@brackets'], @@ -40,8 +34,6 @@ export const getRedisMonarchTokensProvider = (commands: string[]): monacoEditor. cases: { '@keywords': 'keyword', '@operators': 'operator', - '@builtinVariables': 'predefined', - '@builtinFunctions': 'predefined', '@default': 'identifier', }, }, @@ -49,26 +41,13 @@ export const getRedisMonarchTokensProvider = (commands: string[]): monacoEditor. [/[<>=!%&+\-*/|~^]/, 'operator'], ], keyword: [ - [ - `(${commands.join('|')})\\b`, - 'keyword' - ] + [COMMON_COMMANDS_REGEX, { token: 'keyword' }], + [SEARCH_COMMANDS_REGEX, { token: '@rematch', nextEmbedded: 'redisearch', next: '@endRedisearch' }], ], whitespace: [ [/\s+/, 'white'], [/\/\/.*$/, 'comment'], ], - pseudoColumns: [ - [ - /[$][A-Za-z_][\w@#$]*/, - { - cases: { - '@pseudoColumns': 'predefined', - '@default': 'identifier', - }, - }, - ], - ], numbers: [ [/0[xX][0-9a-fA-F]*/, 'number'], [/[$][+-]*\d*(\.\d*)?/, 'number'], @@ -88,9 +67,10 @@ export const getRedisMonarchTokensProvider = (commands: string[]): monacoEditor. [/"/, { token: STRING_DOUBLE, next: '@pop' }], [/[^\\"]+/, STRING_DOUBLE], ], - scopes: [ - // NOT SUPPORTED - ], + // TODO: can be tokens or functions the same - need to think how to avoid wrong ending + endRedisearch: [ + [`^\\s*${COMMON_COMMANDS_REGEX}`, { token: '@rematch', next: '@root', nextEmbedded: '@pop', log: 'end' }], + ] }, } -) +} diff --git a/redisinsight/ui/src/utils/monaco/monacoUtils.ts b/redisinsight/ui/src/utils/monaco/monacoUtils.ts index dadcb5234d..4ff48b893e 100644 --- a/redisinsight/ui/src/utils/monaco/monacoUtils.ts +++ b/redisinsight/ui/src/utils/monaco/monacoUtils.ts @@ -10,6 +10,7 @@ import { IMonacoQuery } from 'uiSrc/utils' import { TJMESPathFunctions } from 'uiSrc/slices/interfaces' +import { isCompositeArgument } from 'uiSrc/pages/search/utils' import { Nullable } from '../types' import { getCommandRepeat, isRepeatCountCorrect } from '../commands' @@ -105,6 +106,101 @@ export const findCommandEarlier = ( return command } +export const splitQueryByArgs = (query: string, position: number = 0) => { + const args: [string[], string[]] = [[], []] + let arg = '' + let inQuotes = false + let escapeNextChar = false + let quoteChar = '' + let isCursorInQuotes = false + let lastArg = '' + let argLeftOffset = 0 + let argRightOffset = 0 + + const pushToProperTuple = (isAfterOffset: boolean, arg: string) => { + lastArg = arg + isAfterOffset ? args[1].push(arg) : args[0].push(arg) + } + + const updateLastArgument = (isAfterOffset: boolean, arg: string) => { + const argsBySide = args[isAfterOffset ? 1 : 0] + argsBySide[argsBySide.length - 1] = `${argsBySide[argsBySide.length - 1]} ${arg}` + } + + const updateArgOffsets = (left: number, right: number) => { + argLeftOffset = left + argRightOffset = right + } + + for (let i = 0; i < query.length; i++) { + const char = query[i] + const isAfterOffset = i >= position + (inQuotes ? -1 : 0) + + if (escapeNextChar) { + arg += char + escapeNextChar = !quoteChar + } else if (char === '\\') { + escapeNextChar = true + } else if (inQuotes) { + if (char === quoteChar) { + inQuotes = false + const argWithChat = arg + char + + if (isAfterOffset && !argLeftOffset) { + updateArgOffsets(i - arg.length, i + 1) + } + + if (isCompositeArgument(argWithChat, lastArg)) { + updateLastArgument(isAfterOffset, argWithChat) + } else { + pushToProperTuple(isAfterOffset, argWithChat) + } + + arg = '' + } else { + arg += char + } + } else if (char === '"' || char === "'") { + inQuotes = true + quoteChar = char + arg += char + } else if (char === ' ' || char === '\n') { + if (arg.length > 0) { + if (isAfterOffset && !argLeftOffset) { + updateArgOffsets(i - arg.length, i) + } + + if (isCompositeArgument(arg, lastArg)) { + updateLastArgument(isAfterOffset, arg) + } else { + pushToProperTuple(isAfterOffset, arg) + } + + arg = '' + } + } else { + arg += char + } + + if (i === position - 1) isCursorInQuotes = inQuotes + } + + if (arg.length > 0) { + if (!argLeftOffset) updateArgOffsets(query.length - arg.length, query.length) + pushToProperTuple(true, arg) + } + + const cursor = { + isCursorInQuotes, + prevCursorChar: query[position - 1]?.trim() || '', + nextCursorChar: query[position]?.trim() || '', + argLeftOffset, + argRightOffset + } + + return { args, cursor } +} + export const findCompleteQuery = ( model: monacoEditor.editor.ITextModel, position: monacoEditor.Position, @@ -166,9 +262,7 @@ export const findCompleteQuery = ( fullQuery += lineAfterPosition } - const args = fullQuery - .replace(matchedCommand, '') - .match(/(?:[^\s"']+|["][^"]*["]|['][^']*['])+/g) + const { args, cursor } = splitQueryByArgs(fullQuery, commandCursorPosition) return { position, @@ -176,6 +270,8 @@ export const findCompleteQuery = ( commandCursorPosition, fullQuery, args, + cursor, + allArgs: args.flat(), name: matchedCommand, info: commandsSpec[matchedCommand] } as IMonacoQuery diff --git a/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts b/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts index d9c1c3b881..e73734b113 100644 --- a/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts +++ b/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts @@ -6,7 +6,7 @@ import { generateTokensWithFunctions, getBlockTokens, isIndexAfterKeyword, isQueryAfterIndex -} from 'uiSrc/utils/monaco/redisearch/utils' +} from 'uiSrc/utils/monaco/redisearch/utils_old' import { generateQuery } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokensTemplates' const STRING_DOUBLE = 'string.double' diff --git a/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokensSubRedis.ts b/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokensSubRedis.ts new file mode 100644 index 0000000000..9be0444673 --- /dev/null +++ b/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokensSubRedis.ts @@ -0,0 +1,121 @@ +import { monaco as monacoEditor } from 'react-monaco-editor' +import { remove } from 'lodash' +import { SearchCommand } from 'uiSrc/pages/search/types' +import { + generateKeywords, + generateTokens, + generateTokensWithFunctions, + getBlockTokens, isIndexAfterKeyword, + isQueryAfterIndex +} from 'uiSrc/utils/monaco/redisearch/utils' +import { generateQuery } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokensTemplates' + +const STRING_DOUBLE = 'string.double' + +export const getRediSearchSubRedisMonarchTokensProvider = ( + commands: SearchCommand[], +): monacoEditor.languages.IMonarchLanguage => { + const withoutIndexSuggestions = [...commands] + const withNextIndexSuggestions = remove(withoutIndexSuggestions, isIndexAfterKeyword) + const withNextQueryIndexSuggestions = remove([...withNextIndexSuggestions], isQueryAfterIndex) + + const generateTokensForCommands = () => { + let commandTokens: any = {} + + withNextIndexSuggestions.forEach((command) => { + const isIndexAfterCommand = isIndexAfterKeyword(command) + const argTokens = generateTokens(command) + const tokenName = command.token?.replace(/(\.| )/g, '_') + + if (isIndexAfterCommand) { + commandTokens[`argument.block.${tokenName}`] = getBlockTokens(tokenName, argTokens?.pureTokens) + commandTokens = { + ...commandTokens, + ...generateTokensWithFunctions(tokenName, argTokens?.tokensWithQueryAfter) + } + } + }) + + return commandTokens + } + + const keywords = generateKeywords(commands) + const tokens = generateTokensForCommands() + + const includeTokens = () => { + const tokensToInclude = Object.keys(tokens).filter((name) => name.startsWith('argument.block')) + return tokensToInclude.map((include) => ({ include: `@${include}` })) + } + + return ( + { + defaultToken: '', + tokenPostfix: '.redisearch', + ignoreCase: true, + brackets: [ + { open: '[', close: ']', token: 'delimiter.square' }, + { open: '(', close: ')', token: 'delimiter.parenthesis' }, + ], + keywords, + tokenizer: { + root: [ + { include: '@keywords' }, + ...includeTokens(), + { include: '@fields' }, + { include: '@whitespace' }, + { include: '@numbers' }, + { include: '@strings' }, + [/[;,.]/, 'delimiter'], + [/[()]/, '@brackets'], + [/[<>=!%&+\-*/|~^]/, 'operator'], + [/[\w@#$.]+/, 'identifier'] + ], + keywords: [ + [`(${generateKeywords(withNextQueryIndexSuggestions).join('|')})\\b`, { token: 'keyword', next: '@index.query' }], + [`(${generateKeywords(withNextIndexSuggestions).join('|')})\\b`, { token: 'keyword', next: '@index' }], + [`(${generateKeywords(withoutIndexSuggestions).join('|')})\\b`, { token: 'keyword', next: '@root' }], + ], + ...tokens, + ...generateQuery(), + index: [ + [/"([^"\\]|\\.)*"/, { token: 'index', next: '@root' }], + [/'([^'\\]|\\.)*'/, { token: 'index', next: '@root' }], + [/[\w:]+/, { token: 'index', next: '@root' }], + { include: 'root' } // Fallback to the root state if nothing matches + ], + 'index.query': [ + [/"([^"\\]|\\.)*"/, { token: 'index', next: '@query' }], + [/'([^'\\]|\\.)*'/, { token: 'index', next: '@query' }], + [/[\w:]+/, { token: 'index', next: '@query' }], + { include: 'root' } // Fallback to the root state if nothing matches + ], + fields: [ + [/@\w+/, { token: 'field' }] + ], + whitespace: [ + [/\s+/, 'white'], + [/\/\/.*$/, 'comment'], + ], + numbers: [ + [/0[xX][0-9a-fA-F]*/, 'number'], + [/[$][+-]*\d*(\.\d*)?/, 'number'], + [/((\d+(\.\d*)?)|(\.\d+))([eE][-+]?\d+)?/, 'number'], + ], + strings: [ + [/'/, { token: 'string', next: '@string' }], + [/"/, { token: STRING_DOUBLE, next: '@stringDouble' }], + ], + string: [ + [/\\./, 'string'], + [/'/, { token: 'string', next: '@pop' }], + [/[^\\']+/, 'string'], + ], + stringDouble: [ + [/\\./, STRING_DOUBLE], + [/"/, { token: STRING_DOUBLE, next: '@pop' }], + [/[^\\"]+/, STRING_DOUBLE], + ] + }, + } + ) +} diff --git a/redisinsight/ui/src/utils/monaco/redisearch/utils.ts b/redisinsight/ui/src/utils/monaco/redisearch/utils.ts index d04f47a717..25b0fd0b19 100644 --- a/redisinsight/ui/src/utils/monaco/redisearch/utils.ts +++ b/redisinsight/ui/src/utils/monaco/redisearch/utils.ts @@ -75,7 +75,9 @@ export const appendTokenWithQuery = ( ): languages.IMonarchLanguageRule[] => args.map(({ token }) => [`(${token.token})\\b`, { token: `argument.block.${level}`, next: `@query.${token.token}` }]) -export const appendQueryWithNextFunctions = (tokens: Array<{ token: SearchCommand, arguments: SearchCommand[] }>): { +export const appendQueryWithNextFunctions = ( + tokens: Array<{ token: SearchCommand, arguments: SearchCommand[] }> +): { [name: string]: languages.IMonarchLanguageRule[] } => { let result: { [name: string]: languages.IMonarchLanguageRule[] } = {} @@ -91,16 +93,19 @@ export const appendQueryWithNextFunctions = (tokens: Array<{ token: SearchComman } export const generateTokensWithFunctions = ( + name: string = '', tokens?: Array> ): { [name: string]: languages.IMonarchLanguageRule[] } => { - if (!tokens) return { 'argument.block.withFunctions': [] } + if (!tokens) return {} const actualTokens = tokens.filter((tokens) => tokens.length) + if (!actualTokens.length) return {} + return { - 'argument.block.withFunctions': [ + [`argument.block.${name}.withFunctions`]: [ ...actualTokens .map((tokens, lvl) => appendTokenWithQuery(tokens, lvl)) .flat() @@ -110,6 +115,7 @@ export const generateTokensWithFunctions = ( } export const getBlockTokens = ( + name: string = '', pureTokens: Maybe[]> ): languages.IMonarchLanguageRule[] => { if (!pureTokens) return [] @@ -126,14 +132,14 @@ export const getBlockTokens = ( result.push([ `(${tokensWithNextExpression.map(({ token }) => token).join('|')})\\b`, { - token: `argument.block.${lvl}`, + token: `argument.block.${lvl}.${name}`, next: '@query' }, ]) } if (restTokens.length) { - result.push([`(${restTokens.map(({ token }) => token).join('|')})\\b`, { token: `argument.block.${lvl}`, next: '@root' }]) + result.push([`(${restTokens.map(({ token }) => token).join('|')})\\b`, { token: `argument.block.${lvl}.${name}`, next: '@root' }]) } return result diff --git a/redisinsight/ui/src/utils/monaco/redisearch/utils_old.ts b/redisinsight/ui/src/utils/monaco/redisearch/utils_old.ts new file mode 100644 index 0000000000..d04f47a717 --- /dev/null +++ b/redisinsight/ui/src/utils/monaco/redisearch/utils_old.ts @@ -0,0 +1,143 @@ +import { isNumber, remove } from 'lodash' +import { languages } from 'monaco-editor' +import { SearchCommand, TokenType } from 'uiSrc/pages/search/types' +import { Maybe, Nullable } from 'uiSrc/utils' +import { DefinedArgumentName } from 'uiSrc/pages/search/components/query/constants' +import { generateQuery } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokensTemplates' + +export const generateKeywords = (commands: SearchCommand[]) => commands.map(({ name }) => name) +export const generateTokens = (command?: SearchCommand): Nullable<{ + pureTokens: Array> + tokensWithQueryAfter: Array> +}> => { + if (!command) return null + const pureTokens: Array> = [] + const tokensWithQueryAfter: Array> = [] + + function processArguments(args: SearchCommand[], level = 0) { + if (!pureTokens[level]) pureTokens[level] = [] + if (!tokensWithQueryAfter[level]) tokensWithQueryAfter[level] = [] + + args.forEach((arg) => { + if (arg.token) pureTokens[level].push(arg) + + if (arg.type === TokenType.Block && arg.arguments) { + const blockToken = arg.arguments[0] + const nextArgs = arg.arguments + const isArgHasOwnSyntax = arg.arguments[0].expression && !!arg.arguments[0].arguments?.length + + if (blockToken?.token) { + if (isArgHasOwnSyntax) { + tokensWithQueryAfter[level].push({ + token: blockToken, + arguments: arg.arguments[0].arguments as SearchCommand[] + }) + } else { + pureTokens[level].push(blockToken) + } + } + + processArguments(blockToken ? nextArgs.slice(1, nextArgs.length) : nextArgs, level + 1) + } + + if (arg.type === TokenType.OneOf && arg.arguments) { + arg.arguments.forEach((choice) => { + if (choice?.token) pureTokens[level].push(choice) + }) + } + }) + } + + if (command.arguments) { + processArguments(command.arguments, 0) + } + + return { pureTokens, tokensWithQueryAfter } +} + +export const isIndexAfterKeyword = (command?: SearchCommand) => { + if (!command) return false + + const index = command.arguments?.findIndex(({ name }) => name === DefinedArgumentName.index) + return isNumber(index) && index === 0 +} + +export const isQueryAfterIndex = (command?: SearchCommand) => { + if (!command) return false + + const index = command.arguments?.findIndex(({ name }) => name === DefinedArgumentName.index) + return isNumber(index) && index > -1 ? command.arguments?.[index + 1]?.name === DefinedArgumentName.query : false +} + +export const appendTokenWithQuery = ( + args: Array<{ token: SearchCommand, arguments: SearchCommand[] }>, + level: number +): languages.IMonarchLanguageRule[] => + args.map(({ token }) => [`(${token.token})\\b`, { token: `argument.block.${level}`, next: `@query.${token.token}` }]) + +export const appendQueryWithNextFunctions = (tokens: Array<{ token: SearchCommand, arguments: SearchCommand[] }>): { + [name: string]: languages.IMonarchLanguageRule[] +} => { + let result: { [name: string]: languages.IMonarchLanguageRule[] } = {} + + tokens.forEach(({ token, arguments: args }) => { + result = { + ...result, + ...generateQuery(token, args) + } + }) + + return result +} + +export const generateTokensWithFunctions = ( + tokens?: Array> +): { + [name: string]: languages.IMonarchLanguageRule[] +} => { + if (!tokens) return { 'argument.block.withFunctions': [] } + + const actualTokens = tokens.filter((tokens) => tokens.length) + + return { + 'argument.block.withFunctions': [ + ...actualTokens + .map((tokens, lvl) => appendTokenWithQuery(tokens, lvl)) + .flat() + ], + ...appendQueryWithNextFunctions(actualTokens.flat()) + } +} + +export const getBlockTokens = ( + pureTokens: Maybe[]> +): languages.IMonarchLanguageRule[] => { + if (!pureTokens) return [] + + const getLeveledToken = ( + tokens: SearchCommand[], + lvl: number + ): languages.IMonarchLanguageRule[] => { + const result: languages.IMonarchLanguageRule[] = [] + const restTokens = [...tokens] + const tokensWithNextExpression = remove(restTokens, (({ expression }) => expression)) + + if (tokensWithNextExpression.length) { + result.push([ + `(${tokensWithNextExpression.map(({ token }) => token).join('|')})\\b`, + { + token: `argument.block.${lvl}`, + next: '@query' + }, + ]) + } + + if (restTokens.length) { + result.push([`(${restTokens.map(({ token }) => token).join('|')})\\b`, { token: `argument.block.${lvl}`, next: '@root' }]) + } + + return result + } + + return pureTokens.map((tokens, lvl) => getLeveledToken(tokens, lvl)).flat() +} diff --git a/redisinsight/ui/src/utils/monaco/subTokens/redisearchSubTokens.ts b/redisinsight/ui/src/utils/monaco/subTokens/redisearchSubTokens.ts new file mode 100644 index 0000000000..e69de29bb2 From b8437649a71706091beb6029edfb0052061e7c10 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Thu, 10 Oct 2024 16:13:15 +0200 Subject: [PATCH 084/112] #RI-6151 - search suggestions in the workbench, refactoring --- .../monaco-laguages/MonacoLanguages.tsx | 13 +- redisinsight/ui/src/constants/commands.ts | 66 +- .../src/mocks/data/mocked_redis_commands.ts | 1521 +++++++++++++++++ .../results-history/ResultsHistory.tsx | 2 +- .../components/query/Query/Query.tsx | 443 ++--- .../components/query/QueryWrapper.tsx | 8 +- .../wb-results/WBResults/WBResults.tsx | 2 +- redisinsight/ui/src/pages/workbench/types.ts | 31 +- redisinsight/ui/src/pages/workbench/utils.ts | 41 - .../ui/src/pages/workbench/utils/monaco.ts | 10 +- .../ui/src/pages/workbench/utils/query.ts | 84 +- .../workbench/utils/searchSuggestions.ts | 186 ++ .../workbench/utils/tests/monaco.spec.ts | 60 + .../tests/profile.spec.ts} | 36 +- .../pages/workbench/utils/tests/query.spec.ts | 189 ++ .../utils/tests/test-cases/common.ts | 183 ++ .../utils/tests/test-cases/ft-aggregate.ts | 267 +++ .../utils/tests/test-cases/ft-search.ts | 283 +++ .../workbench/utils/tests/test-cases/index.ts | 3 + .../ui/src/utils/monaco/monacoInterfaces.ts | 8 +- .../src/utils/transformers/redisCommands.ts | 12 + 21 files changed, 3011 insertions(+), 437 deletions(-) create mode 100644 redisinsight/ui/src/mocks/data/mocked_redis_commands.ts delete mode 100644 redisinsight/ui/src/pages/workbench/utils.ts create mode 100644 redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts create mode 100644 redisinsight/ui/src/pages/workbench/utils/tests/monaco.spec.ts rename redisinsight/ui/src/pages/workbench/{utils.spec.ts => utils/tests/profile.spec.ts} (90%) create mode 100644 redisinsight/ui/src/pages/workbench/utils/tests/query.spec.ts create mode 100644 redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts create mode 100644 redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-aggregate.ts create mode 100644 redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-search.ts create mode 100644 redisinsight/ui/src/pages/workbench/utils/tests/test-cases/index.ts create mode 100644 redisinsight/ui/src/utils/transformers/redisCommands.ts diff --git a/redisinsight/ui/src/components/monaco-laguages/MonacoLanguages.tsx b/redisinsight/ui/src/components/monaco-laguages/MonacoLanguages.tsx index db84c5556f..4d3b9563a8 100644 --- a/redisinsight/ui/src/components/monaco-laguages/MonacoLanguages.tsx +++ b/redisinsight/ui/src/components/monaco-laguages/MonacoLanguages.tsx @@ -7,10 +7,11 @@ import { MonacoLanguage, redisLanguageConfig, Theme } from 'uiSrc/constants' import { getRedisMonarchTokensProvider } from 'uiSrc/utils' import { darkTheme, lightTheme, MonacoThemes } from 'uiSrc/constants/monaco' import { ThemeContext } from 'uiSrc/contexts/themeContext' -import { TokenType } from 'uiSrc/pages/workbench/types' import { getRediSearchSubRedisMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokensSubRedis' import SEARCH_COMMANDS_SPEC from 'uiSrc/pages/workbench/data/supported_commands.json' +import { mergeRedisCommandsSpecs } from 'uiSrc/utils/transformers/redisCommands' +import { SearchCommandTree } from 'uiSrc/pages/search/types' const MonacoLanguages = () => { const { theme } = useContext(ThemeContext) @@ -42,16 +43,12 @@ const MonacoLanguages = () => { } monaco.languages.setLanguageConfiguration(MonacoLanguage.Redis, redisLanguageConfig) - const REDIS_COMMANDS = Object.keys(COMMANDS_SPEC).map((name) => ({ - ...(name in SEARCH_COMMANDS_SPEC ? SEARCH_COMMANDS_SPEC[name] : (COMMANDS_SPEC[name] || {})), - name, - token: name, - type: TokenType.Block - })) + const REDIS_COMMANDS = mergeRedisCommandsSpecs(COMMANDS_SPEC, SEARCH_COMMANDS_SPEC) as SearchCommandTree[] + const REDIS_SEARCH_COMMANDS = REDIS_COMMANDS.filter(({ name }) => name?.startsWith('FT.')) monaco.languages.setMonarchTokensProvider( MonacoLanguage.RediSearch, - getRediSearchSubRedisMonarchTokensProvider(REDIS_COMMANDS.filter(({ name }) => name.startsWith('FT.'))) + getRediSearchSubRedisMonarchTokensProvider(REDIS_SEARCH_COMMANDS) ) monaco.languages.setMonarchTokensProvider( MonacoLanguage.Redis, diff --git a/redisinsight/ui/src/constants/commands.ts b/redisinsight/ui/src/constants/commands.ts index 28d6ba575b..3130ada6f7 100644 --- a/redisinsight/ui/src/constants/commands.ts +++ b/redisinsight/ui/src/constants/commands.ts @@ -1,15 +1,15 @@ export interface ICommands { - [key: string]: ICommand; + [key: string]: ICommand } export interface ICommand { - name?: string; - summary: string; - complexity?: string; - arguments?: ICommandArg[]; - since: string; - group: CommandGroup | string; - provider?: string; + name?: string + summary: string + complexity?: string + arguments?: ICommandArg[] + since: string + group: CommandGroup | string + provider?: string } export enum CommandProvider { @@ -18,19 +18,49 @@ export enum CommandProvider { } export interface ICommandArg { - name?: string[] | string; - type?: CommandArgsType[] | CommandArgsType | string | string[]; - optional?: boolean; - enum?: string[]; - block?: ICommandArg[]; - command?: string; - multiple?: boolean; - variadic?: boolean; - dsl?: string; + name?: string[] | string + type?: CommandArgsType[] | CommandArgsType | string | string[] + optional?: boolean + enum?: string[] + block?: ICommandArg[] + command?: string + multiple?: boolean + variadic?: boolean + dsl?: string +} + +export enum ICommandTokenType { + PureToken = 'pure-token', + Block = 'block', + OneOf = 'oneof', + String = 'string', + Double = 'double', + Enum = 'enum', + Integer = 'integer', + Key = 'key', + POSIXTime = 'posix time', + Pattern = 'pattern', +} + +export interface IRedisCommand { + name?: string + summary?: string + expression?: boolean + type?: ICommandTokenType + token?: string + optional?: boolean + multiple?: boolean + arguments?: IRedisCommand[] + variadic?: boolean + dsl?: string +} + +export interface IRedisCommandTree extends IRedisCommand { + parent?: IRedisCommandTree } export interface ICommandArgGenerated extends ICommandArg { - generatedName?: string | string[]; + generatedName?: string | string[] } export enum CommandArgsType { diff --git a/redisinsight/ui/src/mocks/data/mocked_redis_commands.ts b/redisinsight/ui/src/mocks/data/mocked_redis_commands.ts new file mode 100644 index 0000000000..359c3b12ed --- /dev/null +++ b/redisinsight/ui/src/mocks/data/mocked_redis_commands.ts @@ -0,0 +1,1521 @@ +export const MOCKED_REDIS_COMMANDS = { + 'FT.SEARCH': { + summary: 'Searches the index with a textual query, returning either documents or just ids', + complexity: 'O(N)', + history: [ + [ + '2.0.0', + 'Deprecated `WITHPAYLOADS` and `PAYLOAD` arguments' + ] + ], + arguments: [ + { + name: 'index', + type: 'string' + }, + { + name: 'query', + type: 'string' + }, + { + name: 'nocontent', + type: 'pure-token', + token: 'NOCONTENT', + optional: true + }, + { + name: 'verbatim', + type: 'pure-token', + token: 'VERBATIM', + optional: true + }, + { + name: 'nostopwords', + type: 'pure-token', + token: 'NOSTOPWORDS', + optional: true + }, + { + name: 'withscores', + type: 'pure-token', + token: 'WITHSCORES', + optional: true + }, + { + name: 'withpayloads', + type: 'pure-token', + token: 'WITHPAYLOADS', + optional: true + }, + { + name: 'withsortkeys', + type: 'pure-token', + token: 'WITHSORTKEYS', + optional: true + }, + { + name: 'filter', + type: 'block', + optional: true, + multiple: true, + arguments: [ + { + name: 'numeric_field', + type: 'string', + token: 'FILTER' + }, + { + name: 'min', + type: 'double' + }, + { + name: 'max', + type: 'double' + } + ] + }, + { + name: 'geo_filter', + type: 'block', + optional: true, + multiple: true, + arguments: [ + { + name: 'geo_field', + type: 'string', + token: 'GEOFILTER' + }, + { + name: 'lon', + type: 'double' + }, + { + name: 'lat', + type: 'double' + }, + { + name: 'radius', + type: 'double' + }, + { + name: 'radius_type', + type: 'oneof', + arguments: [ + { + name: 'm', + type: 'pure-token', + token: 'm' + }, + { + name: 'km', + type: 'pure-token', + token: 'km' + }, + { + name: 'mi', + type: 'pure-token', + token: 'mi' + }, + { + name: 'ft', + type: 'pure-token', + token: 'ft' + } + ] + } + ] + }, + { + name: 'in_keys', + type: 'block', + optional: true, + arguments: [ + { + name: 'count', + type: 'string', + token: 'INKEYS' + }, + { + name: 'key', + type: 'string', + multiple: true + } + ] + }, + { + name: 'in_fields', + type: 'block', + optional: true, + arguments: [ + { + name: 'count', + type: 'string', + token: 'INFIELDS' + }, + { + name: 'field', + type: 'string', + multiple: true + } + ] + }, + { + name: 'return', + type: 'block', + optional: true, + arguments: [ + { + name: 'count', + type: 'string', + token: 'RETURN' + }, + { + name: 'identifiers', + type: 'block', + multiple: true, + arguments: [ + { + name: 'identifier', + type: 'string' + }, + { + name: 'property', + type: 'string', + token: 'AS', + optional: true + } + ] + } + ] + }, + { + name: 'summarize', + type: 'block', + optional: true, + arguments: [ + { + name: 'summarize', + type: 'pure-token', + token: 'SUMMARIZE' + }, + { + name: 'fields', + type: 'block', + optional: true, + arguments: [ + { + name: 'count', + type: 'string', + token: 'FIELDS' + }, + { + name: 'field', + type: 'string', + multiple: true + } + ] + }, + { + name: 'num', + type: 'integer', + token: 'FRAGS', + optional: true + }, + { + name: 'fragsize', + type: 'integer', + token: 'LEN', + optional: true + }, + { + name: 'separator', + type: 'string', + token: 'SEPARATOR', + optional: true + } + ] + }, + { + name: 'highlight', + type: 'block', + optional: true, + arguments: [ + { + name: 'highlight', + type: 'pure-token', + token: 'HIGHLIGHT' + }, + { + name: 'fields', + type: 'block', + optional: true, + arguments: [ + { + name: 'count', + type: 'string', + token: 'FIELDS' + }, + { + name: 'field', + type: 'string', + multiple: true + } + ] + }, + { + name: 'tags', + type: 'block', + optional: true, + arguments: [ + { + name: 'tags', + type: 'pure-token', + token: 'TAGS' + }, + { + name: 'open', + type: 'string' + }, + { + name: 'close', + type: 'string' + } + ] + } + ] + }, + { + name: 'slop', + type: 'integer', + optional: true, + token: 'SLOP' + }, + { + name: 'timeout', + type: 'integer', + optional: true, + token: 'TIMEOUT' + }, + { + name: 'inorder', + type: 'pure-token', + token: 'INORDER', + optional: true + }, + { + name: 'language', + type: 'string', + optional: true, + token: 'LANGUAGE' + }, + { + name: 'expander', + type: 'string', + optional: true, + token: 'EXPANDER' + }, + { + name: 'scorer', + type: 'string', + optional: true, + token: 'SCORER' + }, + { + name: 'explainscore', + type: 'pure-token', + token: 'EXPLAINSCORE', + optional: true + }, + { + name: 'payload', + type: 'string', + optional: true, + token: 'PAYLOAD' + }, + { + name: 'sortby', + type: 'block', + optional: true, + arguments: [ + { + name: 'sortby', + type: 'string', + token: 'SORTBY' + }, + { + name: 'order', + type: 'oneof', + optional: true, + arguments: [ + { + name: 'asc', + type: 'pure-token', + token: 'ASC' + }, + { + name: 'desc', + type: 'pure-token', + token: 'DESC' + } + ] + } + ] + }, + { + name: 'limit', + type: 'block', + optional: true, + arguments: [ + { + name: 'limit', + type: 'pure-token', + token: 'LIMIT' + }, + { + name: 'offset', + type: 'integer' + }, + { + name: 'num', + type: 'integer' + } + ] + }, + { + name: 'params', + type: 'block', + optional: true, + arguments: [ + { + name: 'params', + type: 'pure-token', + token: 'PARAMS' + }, + { + name: 'nargs', + type: 'integer' + }, + { + name: 'values', + type: 'block', + multiple: true, + arguments: [ + { + name: 'name', + type: 'string' + }, + { + name: 'value', + type: 'string' + } + ] + } + ] + }, + { + name: 'dialect', + type: 'integer', + optional: true, + token: 'DIALECT', + since: '2.4.3' + } + ], + since: '1.0.0', + group: 'search' + }, + 'FT.AGGREGATE': { + summary: 'Run a search query on an index and perform aggregate transformations on the results', + complexity: 'O(1)', + arguments: [ + { + name: 'index', + type: 'string' + }, + { + name: 'query', + type: 'string' + }, + { + name: 'verbatim', + type: 'pure-token', + token: 'VERBATIM', + optional: true + }, + { + name: 'load', + type: 'block', + optional: true, + arguments: [ + { + name: 'count', + type: 'string', + token: 'LOAD' + }, + { + name: 'field', + type: 'string', + multiple: true + } + ] + }, + { + name: 'timeout', + type: 'integer', + optional: true, + token: 'TIMEOUT' + }, + { + name: 'loadall', + type: 'pure-token', + token: 'LOAD *', + optional: true + }, + { + name: 'groupby', + type: 'block', + optional: true, + multiple: true, + arguments: [ + { + name: 'nargs', + type: 'integer', + token: 'GROUPBY' + }, + { + name: 'property', + type: 'string', + multiple: true + }, + { + name: 'reduce', + type: 'block', + optional: true, + multiple: true, + arguments: [ + { + name: 'function', + type: 'string', + token: 'REDUCE' + }, + { + name: 'nargs', + type: 'integer' + }, + { + name: 'arg', + type: 'string', + multiple: true + }, + { + name: 'name', + type: 'string', + token: 'AS', + optional: true + } + ] + } + ] + }, + { + name: 'sortby', + type: 'block', + optional: true, + arguments: [ + { + name: 'nargs', + type: 'integer', + token: 'SORTBY' + }, + { + name: 'fields', + type: 'block', + optional: true, + multiple: true, + arguments: [ + { + name: 'property', + type: 'string' + }, + { + name: 'order', + type: 'oneof', + arguments: [ + { + name: 'asc', + type: 'pure-token', + token: 'ASC' + }, + { + name: 'desc', + type: 'pure-token', + token: 'DESC' + } + ] + } + ] + }, + { + name: 'num', + type: 'integer', + token: 'MAX', + optional: true + } + ] + }, + { + name: 'apply', + type: 'block', + optional: true, + multiple: true, + arguments: [ + { + name: 'expression', + type: 'string', + token: 'APPLY' + }, + { + name: 'name', + type: 'string', + token: 'AS' + } + ] + }, + { + name: 'limit', + type: 'block', + optional: true, + arguments: [ + { + name: 'limit', + type: 'pure-token', + token: 'LIMIT' + }, + { + name: 'offset', + type: 'integer' + }, + { + name: 'num', + type: 'integer' + } + ] + }, + { + name: 'filter', + type: 'string', + optional: true, + token: 'FILTER' + }, + { + name: 'cursor', + type: 'block', + optional: true, + arguments: [ + { + name: 'withcursor', + type: 'pure-token', + token: 'WITHCURSOR' + }, + { + name: 'read_size', + type: 'integer', + optional: true, + token: 'COUNT' + }, + { + name: 'idle_time', + type: 'integer', + optional: true, + token: 'MAXIDLE' + } + ] + }, + { + name: 'params', + type: 'block', + optional: true, + arguments: [ + { + name: 'params', + type: 'pure-token', + token: 'PARAMS' + }, + { + name: 'nargs', + type: 'integer' + }, + { + name: 'values', + type: 'block', + multiple: true, + arguments: [ + { + name: 'name', + type: 'string' + }, + { + name: 'value', + type: 'string' + } + ] + } + ] + }, + { + name: 'dialect', + type: 'integer', + optional: true, + token: 'DIALECT', + since: '2.4.3' + } + ], + since: '1.1.0', + group: 'search' + }, + 'FT.PROFILE': { + summary: 'Performs a `FT.SEARCH` or `FT.AGGREGATE` command and collects performance information', + complexity: 'O(N)', + arguments: [ + { + name: 'index', + type: 'string' + }, + { + name: 'querytype', + type: 'oneof', + arguments: [ + { + name: 'search', + type: 'pure-token', + token: 'SEARCH' + }, + { + name: 'aggregate', + type: 'pure-token', + token: 'AGGREGATE' + } + ] + }, + { + name: 'limited', + type: 'pure-token', + token: 'LIMITED', + optional: true + }, + { + name: 'queryword', + type: 'pure-token', + token: 'QUERY' + }, + { + name: 'query', + type: 'string' + } + ], + since: '2.2.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.ALIASADD': { + summary: 'Adds an alias to the index', + complexity: 'O(1)', + arguments: [ + { + name: 'alias', + type: 'string' + }, + { + name: 'index', + type: 'string' + } + ], + since: '1.0.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.ALIASDEL': { + summary: 'Deletes an alias from the index', + complexity: 'O(1)', + arguments: [ + { + name: 'alias', + type: 'string' + } + ], + since: '1.0.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.ALIASUPDATE': { + summary: 'Adds or updates an alias to the index', + complexity: 'O(1)', + arguments: [ + { + name: 'alias', + type: 'string' + }, + { + name: 'index', + type: 'string' + } + ], + since: '1.0.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.ALTER': { + summary: 'Adds a new field to the index', + complexity: 'O(N) where N is the number of keys in the keyspace', + arguments: [ + { + name: 'index', + type: 'string' + }, + { + name: 'skipinitialscan', + type: 'pure-token', + token: 'SKIPINITIALSCAN', + optional: true + }, + { + name: 'schema', + type: 'pure-token', + token: 'SCHEMA' + }, + { + name: 'add', + type: 'pure-token', + token: 'ADD' + }, + { + name: 'field', + type: 'string' + }, + { + name: 'options', + type: 'string' + } + ], + since: '1.0.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.CONFIG GET': { + summary: 'Retrieves runtime configuration options', + complexity: 'O(1)', + arguments: [ + { + name: 'option', + type: 'string' + } + ], + since: '1.0.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.CONFIG HELP': { + summary: 'Help description of runtime configuration options', + complexity: 'O(1)', + arguments: [ + { + name: 'option', + type: 'string' + } + ], + since: '1.0.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.CONFIG SET': { + summary: 'Sets runtime configuration options', + complexity: 'O(1)', + arguments: [ + { + name: 'option', + type: 'string' + }, + { + name: 'value', + type: 'string' + } + ], + since: '1.0.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.CREATE': { + summary: 'Creates an index with the given spec', + complexity: 'O(K) at creation where K is the number of fields, O(N) if scanning the keyspace is triggered, where N is the number of keys in the keyspace', + history: [ + [ + '2.0.0', + 'Added `PAYLOAD_FIELD` argument for backward support of `FT.SEARCH` deprecated `WITHPAYLOADS` argument' + ], + [ + '2.0.0', + 'Deprecated `PAYLOAD_FIELD` argument' + ] + ], + arguments: [ + { + name: 'index', + type: 'string' + }, + { + name: 'data_type', + token: 'ON', + type: 'oneof', + arguments: [ + { + name: 'hash', + type: 'pure-token', + token: 'HASH' + }, + { + name: 'json', + type: 'pure-token', + token: 'JSON' + } + ], + optional: true + }, + { + name: 'prefix', + type: 'block', + optional: true, + arguments: [ + { + name: 'count', + type: 'integer', + token: 'PREFIX' + }, + { + name: 'prefix', + type: 'string', + multiple: true + } + ] + }, + { + name: 'filter', + type: 'string', + optional: true, + token: 'FILTER' + }, + { + name: 'default_lang', + type: 'string', + token: 'LANGUAGE', + optional: true + }, + { + name: 'lang_attribute', + type: 'string', + token: 'LANGUAGE_FIELD', + optional: true + }, + { + name: 'default_score', + type: 'double', + token: 'SCORE', + optional: true + }, + { + name: 'score_attribute', + type: 'string', + token: 'SCORE_FIELD', + optional: true + }, + { + name: 'payload_attribute', + type: 'string', + token: 'PAYLOAD_FIELD', + optional: true + }, + { + name: 'maxtextfields', + type: 'pure-token', + token: 'MAXTEXTFIELDS', + optional: true + }, + { + name: 'seconds', + type: 'double', + token: 'TEMPORARY', + optional: true + }, + { + name: 'nooffsets', + type: 'pure-token', + token: 'NOOFFSETS', + optional: true + }, + { + name: 'nohl', + type: 'pure-token', + token: 'NOHL', + optional: true + }, + { + name: 'nofields', + type: 'pure-token', + token: 'NOFIELDS', + optional: true + }, + { + name: 'nofreqs', + type: 'pure-token', + token: 'NOFREQS', + optional: true + }, + { + name: 'stopwords', + type: 'block', + optional: true, + token: 'STOPWORDS', + arguments: [ + { + name: 'count', + type: 'integer' + }, + { + name: 'stopword', + type: 'string', + multiple: true, + optional: true + } + ] + }, + { + name: 'skipinitialscan', + type: 'pure-token', + token: 'SKIPINITIALSCAN', + optional: true + }, + { + name: 'schema', + type: 'pure-token', + token: 'SCHEMA' + }, + { + name: 'field', + type: 'block', + multiple: true, + arguments: [ + { + name: 'field_name', + type: 'string' + }, + { + name: 'alias', + type: 'string', + token: 'AS', + optional: true + }, + { + name: 'field_type', + type: 'oneof', + arguments: [ + { + name: 'text', + type: 'pure-token', + token: 'TEXT' + }, + { + name: 'tag', + type: 'pure-token', + token: 'TAG' + }, + { + name: 'numeric', + type: 'pure-token', + token: 'NUMERIC' + }, + { + name: 'geo', + type: 'pure-token', + token: 'GEO' + }, + { + name: 'vector', + type: 'pure-token', + token: 'VECTOR' + } + ] + }, + { + name: 'withsuffixtrie', + type: 'pure-token', + token: 'WITHSUFFIXTRIE', + optional: true + }, + { + name: 'INDEXEMPTY', + type: 'pure-token', + token: 'INDEXEMPTY', + optional: true + }, + { + name: 'indexmissing', + type: 'pure-token', + token: 'INDEXMISSING', + optional: true + }, + { + name: 'sortable', + type: 'block', + optional: true, + arguments: [ + { + name: 'sortable', + type: 'pure-token', + token: 'SORTABLE' + }, + { + name: 'UNF', + type: 'pure-token', + token: 'UNF', + optional: true + } + ] + }, + { + name: 'noindex', + type: 'pure-token', + token: 'NOINDEX', + optional: true + } + ] + } + ], + since: '1.0.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.CURSOR DEL': { + summary: 'Deletes a cursor', + complexity: 'O(1)', + arguments: [ + { + name: 'index', + type: 'string' + }, + { + name: 'cursor_id', + type: 'integer' + } + ], + since: '1.1.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.CURSOR READ': { + summary: 'Reads from a cursor', + complexity: 'O(1)', + arguments: [ + { + name: 'index', + type: 'string' + }, + { + name: 'cursor_id', + type: 'integer' + }, + { + name: 'read size', + type: 'integer', + optional: true, + token: 'COUNT' + } + ], + since: '1.1.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.DICTADD': { + summary: 'Adds terms to a dictionary', + complexity: 'O(1)', + arguments: [ + { + name: 'dict', + type: 'string' + }, + { + name: 'term', + type: 'string', + multiple: true + } + ], + since: '1.4.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.DICTDEL': { + summary: 'Deletes terms from a dictionary', + complexity: 'O(1)', + arguments: [ + { + name: 'dict', + type: 'string' + }, + { + name: 'term', + type: 'string', + multiple: true + } + ], + since: '1.4.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.DICTDUMP': { + summary: 'Dumps all terms in the given dictionary', + complexity: 'O(N), where N is the size of the dictionary', + arguments: [ + { + name: 'dict', + type: 'string' + } + ], + since: '1.4.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.DROPINDEX': { + summary: 'Deletes the index', + complexity: 'O(1) or O(N) if documents are deleted, where N is the number of keys in the keyspace', + arguments: [ + { + name: 'index', + type: 'string' + }, + { + name: 'delete docs', + type: 'oneof', + arguments: [ + { + name: 'delete docs', + type: 'pure-token', + token: 'DD' + } + ], + optional: true + } + ], + since: '2.0.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.EXPLAIN': { + summary: 'Returns the execution plan for a complex query', + complexity: 'O(1)', + arguments: [ + { + name: 'index', + type: 'string' + }, + { + name: 'query', + type: 'string' + }, + { + name: 'dialect', + type: 'integer', + optional: true, + token: 'DIALECT', + since: '2.4.3' + } + ], + since: '1.0.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.EXPLAINCLI': { + summary: 'Returns the execution plan for a complex query', + complexity: 'O(1)', + arguments: [ + { + name: 'index', + type: 'string' + }, + { + name: 'query', + type: 'string' + }, + { + name: 'dialect', + type: 'integer', + optional: true, + token: 'DIALECT', + since: '2.4.3' + } + ], + since: '1.0.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.INFO': { + summary: 'Returns information and statistics on the index', + complexity: 'O(1)', + arguments: [ + { + name: 'index', + type: 'string' + } + ], + since: '1.0.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.SPELLCHECK': { + summary: 'Performs spelling correction on a query, returning suggestions for misspelled terms', + complexity: 'O(1)', + arguments: [ + { + name: 'index', + type: 'string' + }, + { + name: 'query', + type: 'string' + }, + { + name: 'distance', + token: 'DISTANCE', + type: 'integer', + optional: true + }, + { + name: 'terms', + token: 'TERMS', + type: 'block', + optional: true, + arguments: [ + { + name: 'inclusion', + type: 'oneof', + arguments: [ + { + name: 'include', + type: 'pure-token', + token: 'INCLUDE' + }, + { + name: 'exclude', + type: 'pure-token', + token: 'EXCLUDE' + } + ] + }, + { + name: 'dictionary', + type: 'string' + }, + { + name: 'terms', + type: 'string', + multiple: true, + optional: true + } + ] + }, + { + name: 'dialect', + type: 'integer', + optional: true, + token: 'DIALECT', + since: '2.4.3' + } + ], + since: '1.4.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.SUGADD': { + summary: 'Adds a suggestion string to an auto-complete suggestion dictionary', + complexity: 'O(1)', + history: [ + [ + '2.0.0', + 'Deprecated `PAYLOAD` argument' + ] + ], + arguments: [ + { + name: 'key', + type: 'string' + }, + { + name: 'string', + type: 'string' + }, + { + name: 'score', + type: 'double' + }, + { + name: 'increment score', + type: 'oneof', + arguments: [ + { + name: 'incr', + type: 'pure-token', + token: 'INCR' + } + ], + optional: true + }, + { + name: 'payload', + token: 'PAYLOAD', + type: 'string', + optional: true + } + ], + since: '1.0.0', + group: 'suggestion', + provider: 'redisearch' + }, + 'FT.SUGDEL': { + summary: 'Deletes a string from a suggestion index', + complexity: 'O(1)', + arguments: [ + { + name: 'key', + type: 'string' + }, + { + name: 'string', + type: 'string' + } + ], + since: '1.0.0', + group: 'suggestion', + provider: 'redisearch' + }, + 'FT.SUGGET': { + summary: 'Gets completion suggestions for a prefix', + complexity: 'O(1)', + history: [ + [ + '2.0.0', + 'Deprecated `WITHPAYLOADS` argument' + ] + ], + arguments: [ + { + name: 'key', + type: 'string' + }, + { + name: 'prefix', + type: 'string' + }, + { + name: 'fuzzy', + type: 'pure-token', + token: 'FUZZY', + optional: true + }, + { + name: 'withscores', + type: 'pure-token', + token: 'WITHSCORES', + optional: true + }, + { + name: 'withpayloads', + type: 'pure-token', + token: 'WITHPAYLOADS', + optional: true + }, + { + name: 'max', + token: 'MAX', + type: 'integer', + optional: true + } + ], + since: '1.0.0', + group: 'suggestion', + provider: 'redisearch' + }, + 'FT.SUGLEN': { + summary: 'Gets the size of an auto-complete suggestion dictionary', + complexity: 'O(1)', + arguments: [ + { + name: 'key', + type: 'string' + } + ], + since: '1.0.0', + group: 'suggestion', + provider: 'redisearch' + }, + 'FT.SYNDUMP': { + summary: 'Dumps the contents of a synonym group', + complexity: 'O(1)', + arguments: [ + { + name: 'index', + type: 'string' + } + ], + since: '1.2.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.SYNUPDATE': { + summary: 'Creates or updates a synonym group with additional terms', + complexity: 'O(1)', + arguments: [ + { + name: 'index', + type: 'string' + }, + { + name: 'synonym_group_id', + type: 'string' + }, + { + name: 'skipinitialscan', + type: 'pure-token', + token: 'SKIPINITIALSCAN', + optional: true + }, + { + name: 'term', + type: 'string', + multiple: true + } + ], + since: '1.2.0', + group: 'search', + provider: 'redisearch' + }, + 'FT.TAGVALS': { + summary: 'Returns the distinct tags indexed in a Tag field', + complexity: 'O(N)', + arguments: [ + { + name: 'index', + type: 'string' + }, + { + name: 'field_name', + type: 'string' + } + ], + since: '1.0.0', + group: 'search', + provider: 'redisearch' + }, + 'FT._LIST': { + summary: 'Returns a list of all existing indexes', + complexity: 'O(1)', + since: '2.0.0', + group: 'search', + provider: 'redisearch' + }, +} diff --git a/redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.tsx b/redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.tsx index 9d05a11205..ca85d28c35 100644 --- a/redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.tsx +++ b/redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.tsx @@ -20,7 +20,7 @@ import { searchAndQuerySelector } from 'uiSrc/slices/search/searchAndQuery' import { CommandExecutionType, RunQueryMode } from 'uiSrc/slices/interfaces' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { ProfileQueryType } from 'uiSrc/pages/workbench/constants' -import { generateProfileQueryForCommand } from 'uiSrc/pages/workbench/utils' +import { generateProfileQueryForCommand } from 'uiSrc/pages/workbench/utils/profile' import { CodeButtonParams } from 'uiSrc/constants' import styles from './styles.module.scss' diff --git a/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx b/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx index 440dd63215..6541e6c389 100644 --- a/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx +++ b/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx @@ -1,6 +1,6 @@ import React, { useContext, useEffect, useRef, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { compact, first, isNumber } from 'lodash' +import { compact, first } from 'lodash' import cx from 'classnames' import MonacoEditor, { monaco as monacoEditor } from 'react-monaco-editor' import { useParams } from 'react-router-dom' @@ -9,6 +9,7 @@ import { Theme, MonacoLanguage, DSLNaming, + IRedisCommand, } from 'uiSrc/constants' import { actionTriggerParameterHints, @@ -35,30 +36,21 @@ import { QueryActions, QueryTutorials } from 'uiSrc/components/query' import { addOwnTokenToArgs, - findCurrentArgument, - splitQueryByArgs } from 'uiSrc/pages/workbench/utils/query' import { getRange, getRediSearchSignutureProvider, } from 'uiSrc/pages/workbench/utils/monaco' -import { CursorContext, FoundCommandArgument, SearchCommand, TokenType } from 'uiSrc/pages/workbench/types' -import SEARCH_COMMANDS_SPEC from 'uiSrc/pages/workbench/data/supported_commands.json' +import { CursorContext } from 'uiSrc/pages/workbench/types' import { asSuggestionsRef, getCommandsSuggestions, - getFieldsSuggestions, - getFunctionsSuggestions, - getGeneralSuggestions, - getIndexesSuggestions, - getNoIndexesSuggestion, isIndexComplete } from 'uiSrc/pages/workbench/utils/suggestions' import { COMMANDS_TO_GET_INDEX_INFO, - DefinedArgumentName, EmptySuggestionsIds, - FIELD_START_SYMBOL } from 'uiSrc/pages/workbench/constants' import { useDebouncedEffect } from 'uiSrc/services' import { fetchRedisearchInfoAction } from 'uiSrc/slices/browser/redisearch' +import { findSuggestionsByArg } from 'uiSrc/pages/workbench/utils/searchSuggestions' import { aroundQuotesRegExp, argInQuotesRegExp, @@ -71,6 +63,7 @@ import styles from './styles.module.scss' export interface Props { query: string + commands: IRedisCommand[] indexes: RedisResponseBuffer[] activeMode: RunQueryMode resultsMode?: ResultsMode @@ -89,6 +82,7 @@ let decorationCollection: Nullable { const { query = '', + commands = [], indexes = [], activeMode, resultsMode, @@ -125,16 +119,7 @@ const Query = (props: Props) => { const { theme } = useContext(ThemeContext) const monacoObjects = useRef>(null) - const getCommandByName = (name: string) => - (name in SEARCH_COMMANDS_SPEC ? SEARCH_COMMANDS_SPEC[name] : (REDIS_COMMANDS_SPEC[name] || {})) - - const REDIS_COMMANDS = REDIS_COMMANDS_ARRAY - .map((name) => ({ ...getCommandByName(name), name })) - .map((command) => ({ - ...addOwnTokenToArgs(command.name!, command), - token: command.name!, - type: TokenType.Block - })) + const REDIS_COMMANDS = commands.map((command) => ({ ...addOwnTokenToArgs(command.name!, command) })) const { instanceId = '' } = useParams<{ instanceId: string }>() @@ -204,28 +189,54 @@ const Query = (props: Props) => { })) }, 200, [selectedIndex]) - const triggerUpdateCursorPosition = (editor: monacoEditor.editor.IStandaloneCodeEditor) => { - const position = editor.getPosition() - isDedicatedEditorOpenRef.current = false - editor.trigger('mouse', '_moveTo', { position: { lineNumber: 1, column: 1 } }) - editor.trigger('mouse', '_moveTo', { position }) + const editorDidMount = ( + editor: monacoEditor.editor.IStandaloneCodeEditor, + monaco: typeof monacoEditor + ) => { + monacoObjects.current = { editor, monaco } + + // hack for exit from snippet mode after click Enter until no answer from monaco authors + // https://github.com/microsoft/monaco-editor/issues/2756 + contribution = editor.getContribution('snippetController2') + + syntaxWidgetContext = editor.createContextKey(SYNTAX_CONTEXT_ID, false) editor.focus() - } + setQueryEl(editor) - const onPressWidget = () => { - if (!monacoObjects.current) return - const { editor } = monacoObjects?.current + editor.onKeyDown(onKeyDownMonaco) + editor.onDidChangeCursorPosition(onKeyChangeCursorMonaco) - setIsDedicatedEditorOpen(true) - editor.updateOptions({ readOnly: true }) - hideSyntaxWidget(editor) - sendEventTelemetry({ - event: TelemetryEvent.WORKBENCH_NON_REDIS_EDITOR_OPENED, - eventData: { - databaseId: instanceId, - lang: syntaxCommand.current.lang, + setupMonacoRedisLang(monaco) + editor.addAction( + getMonacoAction(MonacoAction.Submit, (editor) => handleSubmit(editor.getValue()), monaco) + ) + + editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Space, () => { + onPressWidget() + }, SYNTAX_CONTEXT_ID) + + editor.onMouseDown((e: monacoEditor.editor.IEditorMouseEvent) => { + if ((e.target as monacoEditor.editor.IMouseTargetContentWidget)?.detail === SYNTAX_WIDGET_ID) { + onPressWidget() + } + }) + + editor.addCommand(monaco.KeyCode.Escape, () => { + hideSyntaxWidget(editor) + isWidgetEscaped.current = true + }, SYNTAX_CONTEXT_ID) + + decorationCollection = editor.createDecorationsCollection() + + const suggestionWidget = editor.getContribution('editor.contrib.suggestController') + suggestionWidget?.onWillInsertSuggestItem(({ item }: Record<'item', any>) => { + if (item.completion.id === EmptySuggestionsIds.NoIndexes) { + helpWidgetRef.current.isOpen = true + editor.trigger('', 'hideSuggestWidget', null) + editor.trigger('', 'editor.action.triggerParameterHints', '') } }) + suggestionsRef.current = getSuggestions(editor).data } const onChange = (value: string = '') => { @@ -237,15 +248,6 @@ const Query = (props: Props) => { } } - const handleKeyDown = (e: React.KeyboardEvent) => { - onKeyDown?.(e, query) - } - - const handleSubmit = (value?: string) => { - execHistoryPos = 0 - onSubmit(value) - } - const onTriggerParameterHints = () => { if (!monacoObjects.current) return @@ -314,6 +316,102 @@ const Query = (props: Props) => { } } + const onExitSnippetMode = () => { + if (!monacoObjects.current) return + const { editor } = monacoObjects?.current + + if (contribution?.isInSnippet?.()) { + const { lineNumber = 0, column = 0 } = editor?.getPosition() ?? {} + editor.setSelection(new monacoEditor.Selection(lineNumber, column, lineNumber, column)) + contribution?.cancel?.() + } + } + + const onKeyChangeCursorMonaco = (e: monacoEditor.editor.ICursorPositionChangedEvent) => { + if (!monacoObjects.current) return + const { editor } = monacoObjects?.current + const model = editor.getModel() + + isWidgetOpen.current && hideSyntaxWidget(editor) + + if (!model || isDedicatedEditorOpenRef.current) { + return + } + + const command = findCompleteQuery(model, e.position, REDIS_COMMANDS_SPEC, REDIS_COMMANDS_ARRAY) + handleSuggestions(editor, command) + handleDslSyntax(e, command) + } + + const onPressWidget = () => { + if (!monacoObjects.current) return + const { editor } = monacoObjects?.current + + setIsDedicatedEditorOpen(true) + editor.updateOptions({ readOnly: true }) + hideSyntaxWidget(editor) + sendEventTelemetry({ + event: TelemetryEvent.WORKBENCH_NON_REDIS_EDITOR_OPENED, + eventData: { + databaseId: instanceId, + lang: syntaxCommand.current.lang, + } + }) + } + + const onCancelDedicatedEditor = () => { + setIsDedicatedEditorOpen(false) + if (!monacoObjects.current) return + const { editor } = monacoObjects?.current + + editor.updateOptions({ readOnly: false }) + triggerUpdateCursorPosition(editor) + + sendEventTelemetry({ + event: TelemetryEvent.WORKBENCH_NON_REDIS_EDITOR_CANCELLED, + eventData: { + databaseId: instanceId, + lang: syntaxCommand.current.lang, + } + }) + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + onKeyDown?.(e, query) + } + + const handleSubmit = (value?: string) => { + execHistoryPos = 0 + onSubmit(value) + } + + const handleSuggestions = ( + editor: monacoEditor.editor.IStandaloneCodeEditor, + command?: Nullable + ) => { + const { data, forceHide, forceShow } = getSuggestions(editor, command) + suggestionsRef.current = data + + if (!forceShow) { + editor.trigger('', 'editor.action.triggerParameterHints', '') + return + } + + if (data.length) { + helpWidgetRef.current.isOpen = false + triggerSuggestions() + return + } + + editor.trigger('', 'editor.action.triggerParameterHints', '') + + if (forceHide) { + setTimeout(() => editor?.trigger('', 'hideSuggestWidget', null), 0) + } else { + helpWidgetRef.current.isOpen = !isSuggestionsOpened() && helpWidgetRef.current.isOpen + } + } + const handleDslSyntax = ( e: monacoEditor.editor.ICursorPositionChangedEvent, command: Nullable @@ -354,6 +452,14 @@ const Query = (props: Props) => { } } + const triggerUpdateCursorPosition = (editor: monacoEditor.editor.IStandaloneCodeEditor) => { + const position = editor.getPosition() + isDedicatedEditorOpenRef.current = false + editor.trigger('mouse', '_moveTo', { position: { lineNumber: 1, column: 1 } }) + editor.trigger('mouse', '_moveTo', { position }) + editor.focus() + } + const isSuggestionsOpened = () => { const { editor } = monacoObjects.current || {} if (!editor) return false @@ -361,61 +467,11 @@ const Query = (props: Props) => { return suggestController?.model?.state === 1 } - const onKeyChangeCursorMonaco = (e: monacoEditor.editor.ICursorPositionChangedEvent) => { - if (!monacoObjects.current) return - const { editor } = monacoObjects?.current - const model = editor.getModel() - - isWidgetOpen.current && hideSyntaxWidget(editor) - - if (!model || isDedicatedEditorOpenRef.current) { - return - } - - const command = findCompleteQuery(model, e.position, REDIS_COMMANDS_SPEC, REDIS_COMMANDS_ARRAY) - - const { data, forceHide, forceShow } = getSuggestions(editor, command) - - suggestionsRef.current = data - - if (!forceShow) { - editor.trigger('', 'editor.action.triggerParameterHints', '') - return - } - - if (data.length) { - helpWidgetRef.current.isOpen = false - triggerSuggestions() - return - } - - editor.trigger('', 'editor.action.triggerParameterHints', '') - - if (forceHide) { - setTimeout(() => editor?.trigger('', 'hideSuggestWidget', null), 0) - } else { - helpWidgetRef.current.isOpen = !isSuggestionsOpened() && helpWidgetRef.current.isOpen - } - - handleDslSyntax(e, command) - } - const triggerSuggestions = () => { const { editor } = monacoObjects.current || {} setTimeout(() => editor?.trigger('', 'editor.action.triggerSuggest', { auto: false })) } - const onExitSnippetMode = () => { - if (!monacoObjects.current) return - const { editor } = monacoObjects?.current - - if (contribution?.isInSnippet?.()) { - const { lineNumber = 0, column = 0 } = editor?.getPosition() ?? {} - editor.setSelection(new monacoEditor.Selection(lineNumber, column, lineNumber, column)) - contribution?.cancel?.() - } - } - const hideSyntaxWidget = (editor: monacoEditor.editor.IStandaloneCodeEditor) => { editor.removeContentWidget(onTriggerContentWidget(null)) syntaxWidgetContext?.set(false) @@ -432,23 +488,6 @@ const Query = (props: Props) => { syntaxWidgetContext?.set(true) } - const onCancelDedicatedEditor = () => { - setIsDedicatedEditorOpen(false) - if (!monacoObjects.current) return - const { editor } = monacoObjects?.current - - editor.updateOptions({ readOnly: false }) - triggerUpdateCursorPosition(editor) - - sendEventTelemetry({ - event: TelemetryEvent.WORKBENCH_NON_REDIS_EDITOR_CANCELLED, - eventData: { - databaseId: instanceId, - lang: syntaxCommand.current.lang, - } - }) - } - const updateArgFromDedicatedEditor = (value: string = '') => { if (!syntaxCommand.current || !monacoObjects.current) return const { editor } = monacoObjects?.current @@ -484,56 +523,6 @@ const Query = (props: Props) => { }) } - const editorDidMount = ( - editor: monacoEditor.editor.IStandaloneCodeEditor, - monaco: typeof monacoEditor - ) => { - monacoObjects.current = { editor, monaco } - - // hack for exit from snippet mode after click Enter until no answer from monaco authors - // https://github.com/microsoft/monaco-editor/issues/2756 - contribution = editor.getContribution('snippetController2') - - syntaxWidgetContext = editor.createContextKey(SYNTAX_CONTEXT_ID, false) - editor.focus() - setQueryEl(editor) - - editor.onKeyDown(onKeyDownMonaco) - editor.onDidChangeCursorPosition(onKeyChangeCursorMonaco) - - setupMonacoRedisLang(monaco) - editor.addAction( - getMonacoAction(MonacoAction.Submit, (editor) => handleSubmit(editor.getValue()), monaco) - ) - - editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Space, () => { - onPressWidget() - }, SYNTAX_CONTEXT_ID) - - editor.onMouseDown((e: monacoEditor.editor.IEditorMouseEvent) => { - if ((e.target as monacoEditor.editor.IMouseTargetContentWidget)?.detail === SYNTAX_WIDGET_ID) { - onPressWidget() - } - }) - - editor.addCommand(monaco.KeyCode.Escape, () => { - hideSyntaxWidget(editor) - isWidgetEscaped.current = true - }, SYNTAX_CONTEXT_ID) - - decorationCollection = editor.createDecorationsCollection() - - const suggestionWidget = editor.getContribution('editor.contrib.suggestController') - suggestionWidget?.onWillInsertSuggestItem(({ item }: Record<'item', any>) => { - if (item.completion.id === EmptySuggestionsIds.NoIndexes) { - updateHelpWidget(true) - editor.trigger('', 'hideSuggestWidget', null) - editor.trigger('', 'editor.action.triggerParameterHints', '') - } - }) - suggestionsRef.current = getSuggestions(editor).data - } - const setupMonacoRedisLang = (monaco: typeof monacoEditor) => { disposeCompletionItemProvider = monaco.languages.registerCompletionItemProvider(MonacoLanguage.Redis, { provideCompletionItems: (): monacoEditor.languages.CompletionList => ({ suggestions: suggestionsRef.current }) @@ -544,13 +533,6 @@ const Query = (props: Props) => { }).dispose } - const updateHelpWidget = (isOpen: boolean, parent?: SearchCommand, currentArg?: SearchCommand) => { - helpWidgetRef.current = { - isOpen, - parent: parent || helpWidgetRef.current.parent, - currentArg: currentArg || helpWidgetRef.current.currentArg } - } - const getSuggestions = ( editor: monacoEditor.editor.IStandaloneCodeEditor, command?: Nullable @@ -567,6 +549,7 @@ const Query = (props: Props) => { const range = getRange(position, word) if (position.column === 1) { + helpWidgetRef.current.isOpen = false if (command) return asSuggestionsRef([]) return asSuggestionsRef(getCommandsSuggestions(REDIS_COMMANDS, range), false) @@ -577,115 +560,31 @@ const Query = (props: Props) => { } const { allArgs, args, cursor } = command - const { prevCursorChar } = cursor - const [beforeOffsetArgs, [currentOffsetArg]] = args + const [, [currentOffsetArg]] = args if (COMMANDS_TO_GET_INDEX_INFO.some((name) => name === command.name)) { setSelectedIndex(allArgs[1] || '') } - const cursorContext: CursorContext = { ...cursor, currentOffsetArg, offset: command.commandCursorPosition || 0 } - const foundArg = findCurrentArgument(REDIS_COMMANDS, beforeOffsetArgs) - - if (!command.name.startsWith('FT.')) { - updateHelpWidget(true, foundArg?.parent, foundArg?.stopArg) - return asSuggestionsRef([]) - } - - if (prevCursorChar === FIELD_START_SYMBOL) return handleFieldSuggestions(foundArg, range) - - switch (foundArg?.stopArg?.name) { - case DefinedArgumentName.index: { - return handleIndexSuggestions(command.info as SearchCommand, foundArg, currentOffsetArg, range) - } - case DefinedArgumentName.query: { - return handleQuerySuggestions(command.info as SearchCommand, foundArg) - } - default: { - return handleCommonSuggestions(command.fullQuery, foundArg, allArgs, cursorContext, range) - } - } - } - - const handleFieldSuggestions = (foundArg: Nullable, range: monacoEditor.IRange) => { - const isInQuery = foundArg?.stopArg?.name === DefinedArgumentName.query - const fieldSuggestions = getFieldsSuggestions(attributesRef.current, range, true, isInQuery) - return asSuggestionsRef(fieldSuggestions, true) - } + const cursorContext: CursorContext = { ...cursor, currentOffsetArg, offset: command.commandCursorPosition, range } + const { suggestions, helpWidget } = findSuggestionsByArg( + REDIS_COMMANDS, + command, + cursorContext, + { fields: attributesRef.current, indexes: indexesRef.current } + ) - const handleIndexSuggestions = ( - command: SearchCommand, - foundArg: FoundCommandArgument, - currentOffsetArg: Nullable, - range: monacoEditor.IRange - ) => { - const isIndex = indexesRef.current.length > 0 - updateHelpWidget(isIndex, command, foundArg?.stopArg) + console.log(helpWidgetRef) - if (!isIndex) { - updateHelpWidget(!!currentOffsetArg) - return asSuggestionsRef(!currentOffsetArg ? getNoIndexesSuggestion(range) : [], true) + if (helpWidget) { + const { isOpen, parent, currentArg } = helpWidget + helpWidgetRef.current = { + isOpen, + parent: parent || helpWidgetRef.current.parent, + currentArg: currentArg || helpWidgetRef.current.currentArg } } - if (!isIndex || currentOffsetArg) return asSuggestionsRef([], !currentOffsetArg) - - const argumentIndex = command?.arguments - ?.findIndex(({ name }) => foundArg?.stopArg?.name === name) - const isNextArgQuery = isNumber(argumentIndex) - && command?.arguments?.[argumentIndex + 1]?.name === DefinedArgumentName.query - - return asSuggestionsRef(getIndexesSuggestions(indexesRef.current, range, isNextArgQuery)) - } - - const handleQuerySuggestions = (command: SearchCommand, foundArg: FoundCommandArgument) => { - updateHelpWidget(true, command, foundArg?.stopArg) - return asSuggestionsRef([], false) - } - - const handleExpressionSuggestions = ( - value: string, - foundArg: FoundCommandArgument, - cursorContext: CursorContext, - range: monacoEditor.IRange - ) => { - updateHelpWidget(true, foundArg?.parent, foundArg?.stopArg) - - const { isCursorInQuotes, offset, argLeftOffset } = cursorContext - if (!isCursorInQuotes) return asSuggestionsRef([]) - - const stringBeforeCursor = value.substring(argLeftOffset, offset) || '' - const expression = stringBeforeCursor.replace(/^["']|["']$/g, '') - const { args } = splitQueryByArgs(expression, offset - argLeftOffset) - const [, [currentArg]] = args - - const functions = foundArg?.stopArg?.arguments ?? [] - const suggestions = getFunctionsSuggestions(functions, range) - const isStartsWithFunction = functions.some(({ token }) => token?.startsWith(currentArg)) - - return asSuggestionsRef(suggestions, true, isStartsWithFunction) - } - - const handleCommonSuggestions = ( - value: string, - foundArg: Nullable, - allArgs: string[], - cursorContext: CursorContext, - range: monacoEditor.IRange - ) => { - if (foundArg?.stopArg?.expression) return handleExpressionSuggestions(value, foundArg, cursorContext, range) - - const { prevCursorChar, nextCursorChar, isCursorInQuotes } = cursorContext - const shouldHideSuggestions = isCursorInQuotes || nextCursorChar || (prevCursorChar) - if (shouldHideSuggestions) return asSuggestionsRef([]) - - const { - suggestions, - forceHide, - helpWidgetData - } = getGeneralSuggestions(foundArg, allArgs, range, attributesRef.current) - - if (helpWidgetData) updateHelpWidget(helpWidgetData.isOpen, helpWidgetData.parent, helpWidgetData.currentArg) - return asSuggestionsRef(suggestions, forceHide) + return suggestions } const isLoading = loading || processing diff --git a/redisinsight/ui/src/pages/workbench/components/query/QueryWrapper.tsx b/redisinsight/ui/src/pages/workbench/components/query/QueryWrapper.tsx index d6e1d2f351..033eabf914 100644 --- a/redisinsight/ui/src/pages/workbench/components/query/QueryWrapper.tsx +++ b/redisinsight/ui/src/pages/workbench/components/query/QueryWrapper.tsx @@ -6,8 +6,10 @@ import { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands' import { RunQueryMode, ResultsMode } from 'uiSrc/slices/interfaces/workbench' import { fetchRedisearchListAction, redisearchListSelector } from 'uiSrc/slices/browser/redisearch' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' -import Query from './Query' +import { mergeRedisCommandsSpecs } from 'uiSrc/utils/transformers/redisCommands' +import SEARCH_COMMANDS_SPEC from 'uiSrc/pages/workbench/data/supported_commands.json' import styles from './Query/styles.module.scss' +import Query from './Query' export interface Props { query: string @@ -36,6 +38,9 @@ const QueryWrapper = (props: Props) => { const { loading: isCommandsLoading, } = useSelector(appRedisCommandsSelector) const { id: connectedIndstanceId } = useSelector(connectedInstanceSelector) const { data: indexes = [] } = useSelector(redisearchListSelector) + const { spec: COMMANDS_SPEC } = useSelector(appRedisCommandsSelector) + + const REDIS_COMMANDS = mergeRedisCommandsSpecs(COMMANDS_SPEC, SEARCH_COMMANDS_SPEC) const dispatch = useDispatch() @@ -58,6 +63,7 @@ const QueryWrapper = (props: Props) => { ) : ( + stopArg: Maybe isBlocked: boolean - append: Maybe> - parent: Maybe + append: Maybe> + parent: Maybe } export interface CursorContext { @@ -42,4 +22,5 @@ export interface CursorContext { offset: number argLeftOffset: number argRightOffset: number + range: monacoEditor.IRange } diff --git a/redisinsight/ui/src/pages/workbench/utils.ts b/redisinsight/ui/src/pages/workbench/utils.ts deleted file mode 100644 index 2bca2b87c2..0000000000 --- a/redisinsight/ui/src/pages/workbench/utils.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { ProfileQueryType, SEARCH_COMMANDS, GRAPH_COMMANDS } from './constants' - -export const generateGraphProfileQuery = (query: string, type: ProfileQueryType) => { - const q = query?.split(' ')?.slice(1) - - if (q) { - return [`graph.${type.toLowerCase()}`, ...q].join(' ') - } - - return null -} - -export const generateSearchProfileQuery = (query: string, type: ProfileQueryType) => { - const commandSplit = query?.split(' ') - const cmd = commandSplit?.[0] - - if (!commandSplit || !cmd) { - return null - } - - if (type === ProfileQueryType.Explain) { - return [`ft.${type.toLowerCase()}`, ...commandSplit?.slice(1)].join(' ') - } else { - let index = commandSplit?.[1] - - const queryType = cmd.split('.')?.[1] // SEARCH / AGGREGATE - return [`ft.${type.toLowerCase()}`, index, queryType, 'QUERY', ...commandSplit?.slice(2)].join(' ') - } -} - -export const generateProfileQueryForCommand = (query: string, type: ProfileQueryType) => { - const cmd = query?.split(' ')?.[0]?.toLowerCase() - - if (GRAPH_COMMANDS.includes(cmd)) { - return generateGraphProfileQuery(query, type) - } else if (SEARCH_COMMANDS.includes(cmd)) { - return generateSearchProfileQuery(query, type) - } - - return null -} diff --git a/redisinsight/ui/src/pages/workbench/utils/monaco.ts b/redisinsight/ui/src/pages/workbench/utils/monaco.ts index 3bce209dd4..c6cd9bb58f 100644 --- a/redisinsight/ui/src/pages/workbench/utils/monaco.ts +++ b/redisinsight/ui/src/pages/workbench/utils/monaco.ts @@ -2,8 +2,8 @@ import { monaco } from 'react-monaco-editor' import * as monacoEditor from 'monaco-editor' import { isString } from 'lodash' import { generateDetail } from 'uiSrc/pages/workbench/utils/query' -import { SearchCommand, TokenType } from 'uiSrc/pages/workbench/types' import { Maybe } from 'uiSrc/utils' +import { IRedisCommand, ICommandTokenType } from 'uiSrc/constants' export const setCursorPositionAtTheEnd = (editor: monacoEditor.editor.IStandaloneCodeEditor) => { if (!editor) return @@ -25,7 +25,7 @@ export const getRange = (position: monaco.Position, word: monaco.editor.IWordAtP startColumn: word.startColumn, }) -export const buildSuggestion = (arg: SearchCommand, range: monaco.IRange, options: any = {}) => { +export const buildSuggestion = (arg: IRedisCommand, range: monaco.IRange, options: any = {}) => { const extraQuotes = arg.expression ? '\'$1\'' : '' return { label: isString(arg) ? arg : arg.token || arg.arguments?.[0].token || arg.name || '', @@ -39,14 +39,14 @@ export const buildSuggestion = (arg: SearchCommand, range: monaco.IRange, option export const getRediSearchSignutureProvider = (options: Maybe<{ isOpen: boolean - currentArg: SearchCommand - parent: Maybe + currentArg: IRedisCommand + parent: Maybe }>) => { const { isOpen, currentArg, parent } = options || {} if (!isOpen) return null const label = generateDetail(parent) - const arg = currentArg?.type === TokenType.Block + const arg = currentArg?.type === ICommandTokenType.Block ? currentArg?.arguments?.[0]?.name : (currentArg?.name || currentArg?.type || '') diff --git a/redisinsight/ui/src/pages/workbench/utils/query.ts b/redisinsight/ui/src/pages/workbench/utils/query.ts index c87887fbb8..ca9bcda8c8 100644 --- a/redisinsight/ui/src/pages/workbench/utils/query.ts +++ b/redisinsight/ui/src/pages/workbench/utils/query.ts @@ -2,9 +2,9 @@ import { isNumber, toNumber } from 'lodash' import { generateArgsNames, Maybe, Nullable } from 'uiSrc/utils' -import { CommandProvider } from 'uiSrc/constants' +import { CommandProvider, IRedisCommand, IRedisCommandTree, ICommandTokenType } from 'uiSrc/constants' import { COMPOSITE_ARGS } from 'uiSrc/pages/workbench/constants' -import { ArgName, FoundCommandArgument, SearchCommand, SearchCommandTree, TokenType } from '../types' +import { ArgName, FoundCommandArgument } from '../types' export const splitQueryByArgs = (query: string, position: number = 0) => { const args: [string[], string[]] = [[], []] @@ -102,16 +102,16 @@ export const splitQueryByArgs = (query: string, position: number = 0) => { } export const findCurrentArgument = ( - args: SearchCommand[], + args: IRedisCommand[], prev: string[], - parent?: SearchCommandTree + parent?: IRedisCommandTree ): Nullable => { for (let i = prev.length - 1; i >= 0; i--) { const arg = prev[i] const currentArg = findArgByToken(args, arg) - const currentWithParent: SearchCommandTree = { ...currentArg, parent } + const currentWithParent: IRedisCommandTree = { ...currentArg, parent } - if (currentArg?.arguments && currentArg?.type === TokenType.Block) { + if (currentArg?.arguments && currentArg?.type === ICommandTokenType.Block) { return findCurrentArgument(currentArg.arguments, prev.slice(i), currentWithParent) } @@ -137,13 +137,13 @@ export const findCurrentArgument = ( const findStopArgumentInQuery = ( queryArgs: string[], - restCommandArgs: Maybe = [], + restCommandArgs: Maybe = [], ): { - restArguments: SearchCommand[] + restArguments: IRedisCommand[] stopArgIndex: number argumentsIntered?: number isBlocked: boolean - parent?: SearchCommand + parent?: IRedisCommand } => { let currentCommandArgIndex = 0 let argumentsIntered = 0 @@ -168,14 +168,14 @@ const findStopArgumentInQuery = ( const arg = queryArgs[i] const currentCommandArg = restCommandArgs[currentCommandArgIndex] - if (currentCommandArg?.type === TokenType.PureToken) { + if (currentCommandArg?.type === ICommandTokenType.PureToken) { skipArg() continue } if (!isBlockedOnCommand && currentCommandArg?.optional) { const isNotToken = currentCommandArg?.token && currentCommandArg.token !== arg.toUpperCase() - const isNotOneOfToken = !currentCommandArg?.token && currentCommandArg?.type === TokenType.OneOf + const isNotOneOfToken = !currentCommandArg?.token && currentCommandArg?.type === ICommandTokenType.OneOf && currentCommandArg?.arguments?.every(({ token }) => token !== arg.toUpperCase()) if (isNotToken || isNotOneOfToken) { @@ -185,7 +185,7 @@ const findStopArgumentInQuery = ( } } - if (currentCommandArg?.type === TokenType.Block) { + if (currentCommandArg?.type === ICommandTokenType.Block) { let blockArguments = currentCommandArg.arguments ? [...currentCommandArg.arguments] : [] const nArgs = toNumber(queryArgs[i - 1]) || 0 @@ -199,7 +199,7 @@ const findStopArgumentInQuery = ( if (currentCommandArg.token && !isBlockHasToken && currentQueryArg) { blockArguments.unshift({ - type: TokenType.PureToken, + type: ICommandTokenType.PureToken, token: currentQueryArg }) } @@ -246,7 +246,7 @@ const findStopArgumentInQuery = ( continue } - if (currentCommandArg?.type === TokenType.OneOf && currentCommandArg?.optional) { + if (currentCommandArg?.type === ICommandTokenType.OneOf && currentCommandArg?.optional) { // if oneof is optional then we can switch to another argument if (!currentCommandArg?.arguments?.some(({ token }) => token === arg)) { moveToNextCommandArg() @@ -290,13 +290,13 @@ export const getArgumentSuggestions = ( tokenArgs: string[], levelArgs: string[] }, - pastCommandArgs: SearchCommand[], - current?: SearchCommandTree + pastCommandArgs: IRedisCommand[], + current?: IRedisCommandTree ): { isComplete: boolean - stopArg: Maybe, + stopArg: Maybe, isBlocked: boolean, - append: Array, + append: Array, } => { const { restArguments, @@ -309,8 +309,8 @@ export const getArgumentSuggestions = ( const stopArgument = restArguments[stopArgIndex] const restNotFilledArgs = restArguments.slice(stopArgIndex) - const isOneOfArgument = stopArgument?.type === TokenType.OneOf - || (stopArgument?.type === TokenType.PureToken && current?.parent?.type === TokenType.OneOf) + const isOneOfArgument = stopArgument?.type === ICommandTokenType.OneOf + || (stopArgument?.type === ICommandTokenType.PureToken && current?.parent?.type === ICommandTokenType.OneOf) if (isWasBlocked) { return { @@ -352,9 +352,9 @@ export const getArgumentSuggestions = ( } export const getRestArguments = ( - current: Maybe, - stopArgument: Nullable -): SearchCommandTree[] => { + current: Maybe, + stopArgument: Nullable +): IRedisCommandTree[] => { const argumentIndexInArg = current?.arguments ?.findIndex(({ name }) => name === stopArgument?.name) const nextMandatoryIndex = argumentIndexInArg && argumentIndexInArg > -1 ? current?.arguments @@ -377,7 +377,7 @@ export const getRestArguments = ( beforeMandatoryOptionalArgs.unshift(nextMandatoryArg) } - if (nextMandatoryArg?.type === TokenType.OneOf) { + if (nextMandatoryArg?.type === ICommandTokenType.OneOf) { beforeMandatoryOptionalArgs.unshift(...(nextMandatoryArg.arguments || [])) } @@ -385,12 +385,12 @@ export const getRestArguments = ( } export const getAllRestArguments = ( - current: Maybe, - stopArgument: Nullable, + current: Maybe, + stopArgument: Nullable, prevStringArgs: string[] = [], skipLevel = false ) => { - const appendArgs: Array = [] + const appendArgs: Array = [] const currentLvlNextArgs = removeNotSuggestedArgs( prevStringArgs, getRestArguments(current, stopArgument) @@ -410,39 +410,39 @@ export const getAllRestArguments = ( return appendArgs } -export const removeNotSuggestedArgs = (args: string[], commandArgs: SearchCommandTree[]) => +export const removeNotSuggestedArgs = (args: string[], commandArgs: IRedisCommandTree[]) => commandArgs.filter((arg) => { if (arg.token && arg.multiple) return true - if (arg.type === TokenType.OneOf) { + if (arg.type === ICommandTokenType.OneOf) { return !args .some((queryArg) => arg.arguments ?.some((oneOfArg) => oneOfArg.token?.toUpperCase() === queryArg.toUpperCase())) } - if (arg.type === TokenType.Block) { + if (arg.type === ICommandTokenType.Block) { return arg.arguments?.[0]?.token && !args.includes(arg.arguments?.[0]?.token?.toUpperCase()) } return arg.token && !args.includes(arg.token) }) -export const fillArgsByType = (args: SearchCommand[], expandBlock = true): SearchCommandTree[] => { - const result: SearchCommandTree[] = [] +export const fillArgsByType = (args: IRedisCommand[], expandBlock = true): IRedisCommandTree[] => { + const result: IRedisCommandTree[] = [] for (let i = 0; i < args.length; i++) { const currentArg = args[i] - if (expandBlock && currentArg.type === TokenType.OneOf && !currentArg.token) { + if (expandBlock && currentArg.type === ICommandTokenType.OneOf && !currentArg.token) { result.push(...(currentArg?.arguments?.map((arg) => ({ ...arg, parent: currentArg })) || [])) } - if (currentArg.type === TokenType.Block) { + if (currentArg.type === ICommandTokenType.Block) { result.push({ multiple: currentArg.multiple, optional: currentArg.optional, parent: currentArg, - ...(currentArg?.arguments?.[0] as SearchCommand || {}), + ...(currentArg?.arguments?.[0] as IRedisCommand || {}), }) } if (currentArg.token) result.push(currentArg) @@ -451,29 +451,29 @@ export const fillArgsByType = (args: SearchCommand[], expandBlock = true): Searc return result } -export const findArgByToken = (list: SearchCommand[], arg: string): Maybe => +export const findArgByToken = (list: IRedisCommand[], arg: string): Maybe => list.find((cArg) => - (cArg.type === TokenType.OneOf - ? cArg.arguments?.some((oneOfArg: SearchCommand) => oneOfArg?.token?.toLowerCase() === arg?.toLowerCase()) + (cArg.type === ICommandTokenType.OneOf + ? cArg.arguments?.some((oneOfArg: IRedisCommand) => oneOfArg?.token?.toLowerCase() === arg?.toLowerCase()) : cArg.arguments?.[0]?.token?.toLowerCase() === arg.toLowerCase())) export const isCompositeArgument = (arg: string, prevArg?: string) => COMPOSITE_ARGS.includes([prevArg?.toUpperCase(), arg?.toUpperCase()].join(' ')) -export const generateDetail = (command: Maybe) => { +export const generateDetail = (command: Maybe) => { if (!command) return '' if (command.arguments) return generateArgsNames(CommandProvider.Main, command.arguments).join(' ') if (command.token) { - if (command.type === TokenType.PureToken) return command.token + if (command.type === ICommandTokenType.PureToken) return command.token return `${command.token}` } return '' } -export const addOwnTokenToArgs = (token: string, command: SearchCommand) => { +export const addOwnTokenToArgs = (token: string, command: IRedisCommand) => { if (command.arguments) { - return ({ ...command, arguments: [{ token, type: TokenType.PureToken }, ...command.arguments] }) + return ({ ...command, arguments: [{ token, type: ICommandTokenType.PureToken }, ...command.arguments] }) } return command } diff --git a/redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts b/redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts new file mode 100644 index 0000000000..91eead84fa --- /dev/null +++ b/redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts @@ -0,0 +1,186 @@ +import { monaco as monacoEditor } from 'react-monaco-editor' +import { isNumber } from 'lodash' +import { IMonacoQuery, Nullable } from 'uiSrc/utils' +import { CursorContext, FoundCommandArgument } from 'uiSrc/pages/workbench/types' +import { findCurrentArgument, splitQueryByArgs } from 'uiSrc/pages/workbench/utils/query' +import { IRedisCommand } from 'uiSrc/constants' +import { + asSuggestionsRef, + getFieldsSuggestions, + getFunctionsSuggestions, + getGeneralSuggestions, + getIndexesSuggestions, + getNoIndexesSuggestion +} from 'uiSrc/pages/workbench/utils/suggestions' +import { DefinedArgumentName, FIELD_START_SYMBOL } from 'uiSrc/pages/workbench/constants' + +export const findSuggestionsByArg = ( + listOfCommands: IRedisCommand[], + command: IMonacoQuery, + cursorContext: CursorContext, + additionData: { + indexes?: any[] + fields?: any[], + } +): { + suggestions: any, + helpWidget?: any +} => { + const { allArgs, args, cursor } = command + const { prevCursorChar } = cursor + const [beforeOffsetArgs, [currentOffsetArg]] = args + + const foundArg = findCurrentArgument(listOfCommands, beforeOffsetArgs) + + console.log(foundArg) + + if (!command.name.startsWith('FT.')) { + return { + helpWidget: { isOpen: !!foundArg, parent: foundArg?.parent, currentArg: foundArg?.stopArg }, + suggestions: asSuggestionsRef([]) + } + } + + if (prevCursorChar === FIELD_START_SYMBOL) { + return handleFieldSuggestions(additionData.fields || [], foundArg, cursorContext.range) + } + + switch (foundArg?.stopArg?.name) { + case DefinedArgumentName.index: { + return handleIndexSuggestions( + additionData.indexes || [], + command.info as IRedisCommand, + foundArg, + currentOffsetArg, + cursorContext.range + ) + } + case DefinedArgumentName.query: { + console.log('handle query') + return handleQuerySuggestions(foundArg) + } + default: { + return handleCommonSuggestions( + command.fullQuery, + foundArg, + allArgs, + additionData.fields || [], + cursorContext + ) + } + } +} + +const handleFieldSuggestions = ( + fields: any[], + foundArg: Nullable, + range: monacoEditor.IRange +) => { + const isInQuery = foundArg?.stopArg?.name === DefinedArgumentName.query + const fieldSuggestions = getFieldsSuggestions(fields, range, true, isInQuery) + return { + suggestions: asSuggestionsRef(fieldSuggestions, true) + } +} + +const handleIndexSuggestions = ( + indexes: any[], + command: IRedisCommand, + foundArg: FoundCommandArgument, + currentOffsetArg: Nullable, + range: monacoEditor.IRange +) => { + const isIndex = indexes.length > 0 + const helpWidget = { isOpen: isIndex, parent: command, currentArg: foundArg?.stopArg } + + if (!isIndex) { + helpWidget.isOpen = !!currentOffsetArg + + return { + suggestions: asSuggestionsRef(!currentOffsetArg ? getNoIndexesSuggestion(range) : [], true), + helpWidget + } + } + + if (!isIndex || currentOffsetArg) { + return { + suggestions: asSuggestionsRef([], !currentOffsetArg), + helpWidget + } + } + + const argumentIndex = command?.arguments + ?.findIndex(({ name }) => foundArg?.stopArg?.name === name) + const isNextArgQuery = isNumber(argumentIndex) + && command?.arguments?.[argumentIndex + 1]?.name === DefinedArgumentName.query + + return { + suggestions: asSuggestionsRef(getIndexesSuggestions(indexes, range, isNextArgQuery)), + helpWidget + } +} + +const handleQuerySuggestions = (foundArg: FoundCommandArgument) => ({ + helpWidget: { isOpen: true, parent: foundArg?.parent, currentArg: foundArg?.stopArg }, + suggestions: asSuggestionsRef([], false) +}) + +const handleExpressionSuggestions = ( + value: string, + foundArg: FoundCommandArgument, + cursorContext: CursorContext, +) => { + const helpWidget = { isOpen: true, parent: foundArg?.parent, currentArg: foundArg?.stopArg } + + const { isCursorInQuotes, offset, argLeftOffset } = cursorContext + if (!isCursorInQuotes) { + return { + suggestions: asSuggestionsRef([]), + helpWidget + } + } + + const stringBeforeCursor = value.substring(argLeftOffset, offset) || '' + const expression = stringBeforeCursor.replace(/^["']|["']$/g, '') + const { args } = splitQueryByArgs(expression, offset - argLeftOffset) + const [, [currentArg]] = args + + const functions = foundArg?.stopArg?.arguments ?? [] + const suggestions = getFunctionsSuggestions(functions, cursorContext.range) + const isStartsWithFunction = functions.some(({ token }) => token?.startsWith(currentArg)) + + return { + suggestions: asSuggestionsRef(suggestions, true, isStartsWithFunction), + helpWidget + } +} + +const handleCommonSuggestions = ( + value: string, + foundArg: Nullable, + allArgs: string[], + fields: any[], + cursorContext: CursorContext, +) => { + if (foundArg?.stopArg?.expression) return handleExpressionSuggestions(value, foundArg, cursorContext) + + const { prevCursorChar, nextCursorChar, isCursorInQuotes } = cursorContext + const shouldHideSuggestions = isCursorInQuotes || nextCursorChar || (prevCursorChar) + if (shouldHideSuggestions) { + return { + helpWidget: { isOpen: true, parent: foundArg?.parent, currentArg: foundArg?.stopArg }, + suggestions: asSuggestionsRef([]) + } + } + + const { + suggestions, + forceHide, + helpWidgetData + } = getGeneralSuggestions(foundArg, allArgs, cursorContext.range, fields) + + return { + suggestions: asSuggestionsRef(suggestions, forceHide), + helpWidget: helpWidgetData + } +} diff --git a/redisinsight/ui/src/pages/workbench/utils/tests/monaco.spec.ts b/redisinsight/ui/src/pages/workbench/utils/tests/monaco.spec.ts new file mode 100644 index 0000000000..c72864bc06 --- /dev/null +++ b/redisinsight/ui/src/pages/workbench/utils/tests/monaco.spec.ts @@ -0,0 +1,60 @@ +import { getRediSearchSignutureProvider } from 'uiSrc/pages/search/utils' +import { MOCKED_REDIS_COMMANDS } from 'uiSrc/mocks/data/mocked_redis_commands' +import { SearchCommand } from 'uiSrc/pages/search/types' + +const ftAggregateCommand = MOCKED_REDIS_COMMANDS['FT.AGGREGATE'] + +const getRediSearchSignatureProviderTests = [ + { + input: { + isOpen: false, + currentArg: {}, + parent: {} + }, + result: null + }, + { + input: { + isOpen: true, + currentArg: ftAggregateCommand.arguments.find(({ name }) => name === 'groupby') as SearchCommand, + parent: null + }, + result: { + dispose: expect.any(Function), + value: { + activeParameter: 0, + activeSignature: 0, + signatures: [{ + label: '', + parameters: [{ label: 'nargs' }] + }] + } + } + }, + { + input: { + isOpen: true, + currentArg: { name: 'expression' }, + parent: ftAggregateCommand.arguments.find(({ name }) => name === 'apply') as SearchCommand + }, + result: { + dispose: expect.any(Function), + value: { + activeParameter: 0, + activeSignature: 0, + signatures: [{ + label: 'APPLY expression AS name', + parameters: [{ label: 'expression' }] + }] + } + } + } +] + +describe('getRediSearchSignatureProvider', () => { + it.each(getRediSearchSignatureProviderTests)('should properly return result', ({ input, result }) => { + const testResult = getRediSearchSignutureProvider(input) + + expect(result).toEqual(testResult) + }) +}) diff --git a/redisinsight/ui/src/pages/workbench/utils.spec.ts b/redisinsight/ui/src/pages/workbench/utils/tests/profile.spec.ts similarity index 90% rename from redisinsight/ui/src/pages/workbench/utils.spec.ts rename to redisinsight/ui/src/pages/workbench/utils/tests/profile.spec.ts index 3d91dad1d8..e7aef603fe 100644 --- a/redisinsight/ui/src/pages/workbench/utils.spec.ts +++ b/redisinsight/ui/src/pages/workbench/utils/tests/profile.spec.ts @@ -1,11 +1,10 @@ -import { ProfileQueryType, SEARCH_COMMANDS, GRAPH_COMMANDS } from './constants' +import { ProfileQueryType } from '../../constants' import { generateGraphProfileQuery, generateSearchProfileQuery, generateProfileQueryForCommand, -} from './utils' - +} from '../profile' const generateGraphProfileQueryTests: Record[] = [ { input: 'GRAPH.QUERY key "MATCH (n) RETURN n"', output: 'graph.profile key "MATCH (n) RETURN n"', type: ProfileQueryType.Profile }, @@ -17,14 +16,13 @@ const generateGraphProfileQueryTests: Record[] = [ ] describe('generateGraphProfileQuery', () => { - generateGraphProfileQueryTests.forEach(test => { + generateGraphProfileQueryTests.forEach((test) => { it(`should be output: ${test.output} for input: ${test.input} and type: ${test.type}`, () => { - const result = generateGraphProfileQuery(test.input, test.type); - expect(result).toEqual(test.output); - }); + const result = generateGraphProfileQuery(test.input, test.type) + expect(result).toEqual(test.output) + }) }) -}); - +}) const generateSearchProfileQueryTests: Record[] = [ { input: 'FT.SEARCH index tomatoes', output: 'ft.profile index SEARCH QUERY tomatoes', type: ProfileQueryType.Profile }, @@ -44,13 +42,13 @@ const generateSearchProfileQueryTests: Record[] = [ ] describe('generateSearchProfileQuery', () => { - generateSearchProfileQueryTests.forEach(test => { + generateSearchProfileQueryTests.forEach((test) => { it(`should be output: ${test.output} for input: ${test.input} and type: ${test.type}`, () => { - const result = generateSearchProfileQuery(test.input, test.type); - expect(result).toEqual(test.output); - }); + const result = generateSearchProfileQuery(test.input, test.type) + expect(result).toEqual(test.output) + }) }) -}); +}) const generateProfileQueryForCommandTests: Record[] = [ ...generateGraphProfileQueryTests, @@ -69,11 +67,11 @@ const generateProfileQueryForCommandTests: Record[] = [ { input: 'ft.explain index tomatoes', output: null, type: ProfileQueryType.Explain }, ] describe('generateProfileQueryForCommand', () => { - generateProfileQueryForCommandTests.forEach(test => { + generateProfileQueryForCommandTests.forEach((test) => { it(`should be output: ${test.output} for input: ${test.input} and type: ${test.type}`, () => { - const result = generateProfileQueryForCommand(test.input, test.type); + const result = generateProfileQueryForCommand(test.input, test.type) - expect(result).toEqual(test.output); - }); + expect(result).toEqual(test.output) + }) }) -}); +}) diff --git a/redisinsight/ui/src/pages/workbench/utils/tests/query.spec.ts b/redisinsight/ui/src/pages/workbench/utils/tests/query.spec.ts new file mode 100644 index 0000000000..5afe4fe26b --- /dev/null +++ b/redisinsight/ui/src/pages/workbench/utils/tests/query.spec.ts @@ -0,0 +1,189 @@ +import { SearchCommand, TokenType } from 'uiSrc/pages/search/types' +import { Maybe } from 'uiSrc/utils' +import { MOCKED_REDIS_COMMANDS } from 'uiSrc/mocks/data/mocked_redis_commands' +import { IRedisCommand } from 'uiSrc/constants' +import { + commonfindCurrentArgumentCases, + findArgumentftAggreageTests, + findArgumentftSearchTests +} from './test-cases' +import { addOwnTokenToArgs, findCurrentArgument, generateDetail, splitQueryByArgs } from '../query' + +const ftSearchCommand = MOCKED_REDIS_COMMANDS['FT.SEARCH'] +const ftAggregateCommand = MOCKED_REDIS_COMMANDS['FT.AGGREGATE'] +const COMMANDS = Object.keys(MOCKED_REDIS_COMMANDS).map((name) => ({ + name, + ...MOCKED_REDIS_COMMANDS[name] +})) + +describe('findCurrentArgument', () => { + describe('with list of commands', () => { + commonfindCurrentArgumentCases.forEach(({ input, result, appendIncludes, appendNotIncludes }) => { + it(`should return proper suggestions for ${input}`, () => { + const { args } = splitQueryByArgs(input) + const COMMANDS_LIST = COMMANDS.map((command) => ({ + ...addOwnTokenToArgs(command.name!, command), + token: command.name!, + type: TokenType.Block + })) + + const testResult = findCurrentArgument( + COMMANDS_LIST, + args.flat() + ) + expect(testResult).toEqual(result) + expect( + testResult?.append?.flat()?.map((arg) => arg.token) + ).toEqual( + expect.arrayContaining(appendIncludes) + ) + + if (appendNotIncludes) { + appendNotIncludes.forEach((token) => { + expect( + testResult?.append?.flat()?.map((arg) => arg.token) + ).not.toEqual( + expect.arrayContaining([token]) + ) + }) + } + }) + }) + }) + + describe('FT.AGGREGATE', () => { + findArgumentftAggreageTests.forEach(({ args, result: testResult }) => { + it(`should return proper suggestions for ${args.join(' ')}`, () => { + const result = findCurrentArgument( + ftAggregateCommand.arguments as IRedisCommand[], + args + ) + expect(testResult).toEqual(result) + }) + }) + }) + + describe('FT.SEARCH', () => { + findArgumentftSearchTests.forEach(({ args, result: testResult }) => { + it(`should return proper suggestions for ${args.join(' ')}`, () => { + const result = findCurrentArgument( + ftSearchCommand.arguments as IRedisCommand[], + args + ) + expect(testResult).toEqual(result) + }) + }) + }) +}) + +const splitQueryByArgsTests: Array<{ + input: [string, number?] + result: any +}> = [ + { + input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS'], + result: { + args: [[], ['FT.SEARCH', '"idx:bicycle"', '""', 'WITHSORTKEYS']], + cursor: { + argLeftOffset: 10, + argRightOffset: 23, + isCursorInQuotes: false, + nextCursorChar: 'F', + prevCursorChar: '' + } + } + }, + { + input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS', 17], + result: { + args: [['FT.SEARCH'], ['"idx:bicycle"', '""', 'WITHSORTKEYS']], + cursor: { + argLeftOffset: 10, + argRightOffset: 23, + isCursorInQuotes: true, + nextCursorChar: 'c', + prevCursorChar: 'i' + } + } + }, + { + input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS', 39], + result: { + args: [['FT.SEARCH', '"idx:bicycle"', '""'], ['WITHSORTKEYS']], + cursor: { + argLeftOffset: 27, + argRightOffset: 39, + isCursorInQuotes: false, + nextCursorChar: '', + prevCursorChar: 'S' + } + } + }, + { + input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS ', 40], + result: { + args: [['FT.SEARCH', '"idx:bicycle"', '""', 'WITHSORTKEYS'], []], + cursor: { + argLeftOffset: 0, + argRightOffset: 0, + isCursorInQuotes: false, + nextCursorChar: '', + prevCursorChar: '' + } + } + }, + { + input: ['FT.SEARCH "idx:bicycle \\" \\"" "" WITHSORTKEYS ', 46], + result: { + args: [['FT.SEARCH', '"idx:bicycle " ""', '""', 'WITHSORTKEYS'], []], + cursor: { + argLeftOffset: 0, + argRightOffset: 0, + isCursorInQuotes: false, + nextCursorChar: '', + prevCursorChar: '' + } + } + } +] + +describe('splitQueryByArgs', () => { + it.each(splitQueryByArgsTests)('should return for %input proper result', ({ input, result }) => { + const testResult = splitQueryByArgs(...input) + expect(testResult).toEqual(result) + }) +}) + +const generateDetailTests: Array<{ input: Maybe, result: any }> = [ + { + input: ftSearchCommand.arguments.find(({ name }) => name === 'nocontent') as SearchCommand, + result: 'NOCONTENT' + }, + { + input: ftSearchCommand.arguments.find(({ name }) => name === 'filter') as SearchCommand, + result: 'FILTER numeric_field min max' + }, + { + input: ftSearchCommand.arguments.find(({ name }) => name === 'geo_filter') as SearchCommand, + result: 'GEOFILTER geo_field lon lat radius m | km | mi | ft' + }, + { + input: ftAggregateCommand.arguments.find(({ name }) => name === 'groupby') as SearchCommand, + result: 'GROUPBY nargs property [property ...] [REDUCE function nargs arg [arg ...] [AS name] [REDUCE function nargs arg [arg ...] [AS name] ...]]' + }, +] + +describe('generateDetail', () => { + it.each(generateDetailTests)('should return for %input proper result', ({ input, result }) => { + const testResult = generateDetail(input) + expect(testResult).toEqual(result) + }) +}) + +describe('addOwnTokenToArgs', () => { + it('should add FT.SEARCH to args', () => { + const result = addOwnTokenToArgs('FT.SEARCH', { arguments: [] }) + + expect({ arguments: [{ token: 'FT.SEARCH', type: 'pure-token' }] }).toEqual(result) + }) +}) diff --git a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts new file mode 100644 index 0000000000..7946e4baa6 --- /dev/null +++ b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts @@ -0,0 +1,183 @@ +// Common test cases +export const commonfindCurrentArgumentCases = [ + { + input: 'FT.SEARCH index "" DIALECT 1', + result: { + stopArg: undefined, + append: expect.any(Array), + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + }, + appendIncludes: ['WITHSCORES', 'VERBATIM', 'FILTER', 'SORTBY', 'RETURN'], + appendNotIncludes: ['DIALECT'] + }, + { + input: 'FT.AGGREGATE "idx:schools" "" GROUPBY 1 p REDUCE AVG 1 a1 AS name ', + result: { + stopArg: undefined, + append: expect.any(Array), + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + }, + appendIncludes: ['REDUCE', 'APPLY', 'SORTBY', 'GROUPBY'], + appendNotIncludes: ['AS'], + }, + { + input: 'FT.SEARCH "idx:bicycle" "*" ', + result: { + stopArg: expect.any(Object), + append: expect.any(Array), + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + }, + appendIncludes: ['DIALECT', 'EXPANDER', 'INKEYS', 'LIMIT'], + appendNotIncludes: ['ASC'], + }, + { + input: 'FT.SEARCH "idx:bicycle" "*" DIALECT 2', + result: expect.any(Object), + appendIncludes: ['EXPANDER', 'INKEYS', 'LIMIT'], + appendNotIncludes: ['DIALECT'], + }, + { + input: 'FT.PROFILE \'idx:schools\' SEARCH ', + result: expect.any(Object), + appendIncludes: ['LIMITED', 'QUERY'], + appendNotIncludes: ['AGGREGATE', 'SEARCH'], + }, + { + input: 'FT.CREATE "idx:schools" ', + result: expect.any(Object), + appendIncludes: ['FILTER', 'ON', 'SCHEMA', 'SCORE', 'NOHL'], + appendNotIncludes: ['HASH', 'JSON'], + }, + { + input: 'FT.CREATE "idx:schools" ON', + result: expect.any(Object), + appendIncludes: ['HASH', 'JSON'], + appendNotIncludes: ['SCHEMA', 'SCORE', 'NOHL'], + }, + { + input: 'FT.CREATE "idx:schools" ON JSON NOFREQS', + result: expect.any(Object), + appendIncludes: ['TEMPORARY', 'NOFIELDS', 'PAYLOAD_FIELD', 'MAXTEXTFIELDS', 'PREFIX', 'SKIPINITIALSCAN'], + appendNotIncludes: ['ON', 'JSON', 'NOFREQS'], + }, + { + input: 'FT.CREATE "idx:schools" ON JSON NOFREQS SKIPINITIALSCAN', + result: expect.any(Object), + appendIncludes: ['TEMPORARY', 'NOFIELDS', 'PAYLOAD_FIELD', 'MAXTEXTFIELDS', 'PREFIX'], + appendNotIncludes: ['ON', 'JSON', 'NOFREQS', 'SKIPINITIALSCAN'], + }, + { + input: 'FT.CREATE "idx:schools" ON JSON SCHEMA address ', + result: { + stopArg: expect.any(Object), + append: expect.any(Array), + isBlocked: false, + isComplete: false, + parent: expect.any(Object) + }, + appendIncludes: ['AS', 'GEO', 'TEXT', 'VECTOR'], + appendNotIncludes: ['SCHEMA', 'SCORE', 'NOHL'], + }, + { + input: 'FT.CREATE "idx:schools" ON JSON SCHEMA address TEXT NOINDEX INDEXMISSING ', + result: { + stopArg: expect.any(Object), + append: expect.any(Array), + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + }, + appendIncludes: ['INDEXEMPTY', 'SORTABLE', 'WITHSUFFIXTRIE'], + appendNotIncludes: ['SCHEMA', 'SCORE', 'NOHL'], + }, + { + input: 'FT.ALTER "idx:schools" ', + result: { + stopArg: expect.any(Object), + append: expect.any(Array), + isBlocked: false, + isComplete: false, + parent: expect.any(Object) + }, + appendIncludes: ['SCHEMA', 'SKIPINITIALSCAN'], + appendNotIncludes: ['ADD'], + }, + { + input: 'FT.ALTER "idx:schools" SCHEMA', + result: expect.any(Object), + appendIncludes: ['ADD'], + appendNotIncludes: ['SKIPINITIALSCAN'], + }, + { + input: 'FT.CONFIG SET ', + result: { + stopArg: { + name: 'option', + type: 'string' + }, + append: [], + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + }, + appendIncludes: [], + appendNotIncludes: [expect.any(String)], + }, + { + input: 'FT.CURSOR READ "idx:schools" 1 ', + result: expect.any(Object), + appendIncludes: ['COUNT'], + }, + { + input: 'FT.DICTADD dict term1 ', + result: { + append: [], + isBlocked: true, + isComplete: false, + parent: expect.any(Object), + stopArg: { + multiple: true, + name: 'term', + type: 'string' + } + }, + appendIncludes: [], + }, + { + input: 'FT.SUGADD key string ', + result: { + append: [], + isBlocked: true, + isComplete: false, + parent: expect.any(Object), + stopArg: { + name: 'score', + type: 'double' + } + }, + appendIncludes: [], + }, + { + input: 'FT.SUGADD key string 1.0 ', + result: expect.any(Object), + appendIncludes: ['INCR', 'PAYLOAD'], + }, + { + input: 'FT.SUGADD key string 1.0 PAYLOAD 1 ', + result: expect.any(Object), + appendIncludes: ['INCR'], + appendNotIncludes: ['PAYLOAD'], + }, + { + input: 'FT.SUGGET k p FUZZY MAX 2 ', + result: expect.any(Object), + appendIncludes: ['WITHPAYLOADS', 'WITHSCORES'], + appendNotIncludes: ['FUZZY', 'MAX'], + }, +] diff --git a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-aggregate.ts b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-aggregate.ts new file mode 100644 index 0000000000..e1411809a9 --- /dev/null +++ b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-aggregate.ts @@ -0,0 +1,267 @@ +export const findArgumentftAggreageTests = [ + { args: [''], result: null }, + { args: ['', ''], result: null }, + { + args: ['index', '"query"', 'APPLY'], + result: { + stopArg: { name: 'expression', token: 'APPLY', type: 'string' }, + append: [], + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'APPLY', 'expression'], + result: { + stopArg: { name: 'name', token: 'AS', type: 'string' }, + append: expect.any(Array), + isBlocked: false, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'APPLY', 'expression', 'AS'], + result: { + stopArg: { name: 'name', token: 'AS', type: 'string' }, + append: expect.any(Array), + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'APPLY', 'expression', 'AS', 'name'], + result: { + stopArg: undefined, + append: expect.any(Array), + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['""', '""', 'GROUPBY', '2', 'p1', 'p2', 'REDUCE', 'f'], + result: { + stopArg: { name: 'nargs', type: 'integer' }, + append: [], + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['""', '""', 'GROUPBY', '2', 'p1', 'p2', 'REDUCE', 'f', '0'], + result: { + stopArg: { + name: 'name', + type: 'string', + token: 'AS', + optional: true + }, + append: [ + [ + { + name: 'name', + type: 'string', + token: 'AS', + optional: true, + parent: { + name: 'reduce', + type: 'block', + optional: true, + multiple: true, + arguments: [ + { + name: 'function', + token: 'REDUCE', + type: 'string' + }, + { + name: 'nargs', + type: 'integer' + }, + { + name: 'arg', + type: 'string', + multiple: true + }, + { + name: 'name', + type: 'string', + token: 'AS', + optional: true + } + ], + parent: expect.any(Object) + } + } + ], + [ + { + name: 'function', + token: 'REDUCE', + type: 'string', + multiple: true, + optional: true, + parent: expect.any(Object) + } + ] + ], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['""', '""', 'GROUPBY', '2', 'p1', 'p2', 'REDUCE', 'f', '1', 'AS', 'name'], + result: { + stopArg: undefined, + append: [ + [], + [ + { + name: 'function', + token: 'REDUCE', + type: 'string', + multiple: true, + optional: true, + parent: expect.any(Object) + } + ] + ], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'SORTBY'], + result: { + stopArg: { name: 'nargs', token: 'SORTBY', type: 'integer' }, + append: expect.any(Array), + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'SORTBY', '1', 'p1'], + result: { + stopArg: { + name: 'num', + type: 'integer', + token: 'MAX', + optional: true + }, + append: [ + [ + { + name: 'num', + type: 'integer', + token: 'MAX', + optional: true, + parent: expect.any(Object) + } + ] + ], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'SORTBY', '2', 'p1', 'ASC'], + result: { + stopArg: { + name: 'num', + type: 'integer', + token: 'MAX', + optional: true + }, + append: [ + [ + { + name: 'num', + type: 'integer', + token: 'MAX', + optional: true, + parent: expect.any(Object) + } + ] + ], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'SORTBY', '0'], + result: { + stopArg: { + name: 'num', + type: 'integer', + token: 'MAX', + optional: true + }, + append: [ + [{ + name: 'num', + type: 'integer', + token: 'MAX', + optional: true, + parent: expect.any(Object) + }] + ], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'SORTBY', '2', 'p1', 'ASC', 'MAX'], + result: { + stopArg: { + name: 'num', + type: 'integer', + token: 'MAX', + optional: true + }, + append: [], + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'LOAD', '4'], + result: { + stopArg: { multiple: true, name: 'field', type: 'string' }, + append: [], + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'LOAD', '4', '1', '2', '3'], + result: { + stopArg: { multiple: true, name: 'field', type: 'string' }, + append: [], + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['index', '"query"', 'LOAD', '4', '1', '2', '3', '4'], + result: { + stopArg: undefined, + append: [[]], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, +] diff --git a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-search.ts b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-search.ts new file mode 100644 index 0000000000..28137bf8e9 --- /dev/null +++ b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-search.ts @@ -0,0 +1,283 @@ +export const findArgumentftSearchTests = [ + { args: [''], result: null }, + { args: ['', ''], result: null }, + { + args: ['', '', 'SUMMARIZE'], + result: { + stopArg: { + name: 'fields', + type: 'block', + optional: true, + arguments: [ + { + name: 'count', + type: 'string', + token: 'FIELDS' + }, + { + name: 'field', + type: 'string', + multiple: true + } + ] + }, + append: [[ + { + name: 'count', + type: 'string', + token: 'FIELDS', + optional: true, + parent: expect.any(Object), + }, + { + name: 'num', + type: 'integer', + token: 'FRAGS', + optional: true, + parent: expect.any(Object) + }, + { + name: 'fragsize', + type: 'integer', + token: 'LEN', + optional: true, + parent: expect.any(Object) + }, + { + name: 'separator', + type: 'string', + token: 'SEPARATOR', + optional: true, + parent: expect.any(Object) + } + ]], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'SUMMARIZE', 'FIELDS'], + result: { + stopArg: { + name: 'count', + type: 'string', + token: 'FIELDS' + }, + append: [], + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'SUMMARIZE', 'FIELDS', '1'], + result: { + stopArg: { + name: 'field', + type: 'string', + multiple: true + }, + append: [], + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'SUMMARIZE', 'FIELDS', '1', 'f', 'FRAGS'], + result: { + stopArg: { + name: 'num', + type: 'integer', + token: 'FRAGS', + optional: true + }, + append: [], + isBlocked: true, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'SUMMARIZE', 'FIELDS', '1', 'f', 'FRAGS', '10'], + result: { + stopArg: { + name: 'fragsize', + type: 'integer', + token: 'LEN', + optional: true + }, + append: [[ + { + name: 'fragsize', + type: 'integer', + token: 'LEN', + optional: true, + parent: expect.any(Object) + }, + { + name: 'separator', + type: 'string', + token: 'SEPARATOR', + optional: true, + parent: expect.any(Object) + } + ]], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'RETURN', '1', 'iden'], + result: { + stopArg: undefined, + append: [ + [] + ], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'RETURN', '2', 'iden'], + result: { + stopArg: { + name: 'property', + type: 'string', + token: 'AS', + optional: true + }, + append: [ + [ + { + name: 'property', + type: 'string', + token: 'AS', + optional: true, + parent: expect.any(Object) + } + ], + [] + ], + isBlocked: false, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'RETURN', '2', 'iden', 'iden'], + result: { + stopArg: undefined, + append: [ + [] + ], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'RETURN', '3', 'iden', 'iden'], + result: { + stopArg: { + name: 'property', + type: 'string', + token: 'AS', + optional: true + }, + append: [ + [ + { + name: 'property', + type: 'string', + token: 'AS', + optional: true, + parent: expect.any(Object) + } + ], + [] + ], + isBlocked: false, + isComplete: false, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'RETURN', '3', 'iden', 'iden', 'AS', 'iden2'], + result: { + stopArg: undefined, + append: [ + [] + ], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'SORTBY', 'f'], + result: { + stopArg: { + name: 'order', + type: 'oneof', + optional: true, + arguments: [ + { + name: 'asc', + type: 'pure-token', + token: 'ASC' + }, + { + name: 'desc', + type: 'pure-token', + token: 'DESC' + } + ] + }, + append: [ + [ + { + name: 'asc', + type: 'pure-token', + token: 'ASC', + parent: expect.any(Object) + }, + { + name: 'desc', + type: 'pure-token', + token: 'DESC', + parent: expect.any(Object) + } + ] + ], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'SORTBY', 'f', 'DESC'], + result: { + stopArg: undefined, + append: [], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, + { + args: ['', '', 'DIALECT', '1'], + result: { + stopArg: undefined, + append: [ + [] + ], + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + } + }, +] diff --git a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/index.ts b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/index.ts new file mode 100644 index 0000000000..42889e7be5 --- /dev/null +++ b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/index.ts @@ -0,0 +1,3 @@ +export * from './ft-aggregate' +export * from './ft-search' +export * from './common' diff --git a/redisinsight/ui/src/utils/monaco/monacoInterfaces.ts b/redisinsight/ui/src/utils/monaco/monacoInterfaces.ts index e092777374..1832854dc3 100644 --- a/redisinsight/ui/src/utils/monaco/monacoInterfaces.ts +++ b/redisinsight/ui/src/utils/monaco/monacoInterfaces.ts @@ -1,9 +1,9 @@ import { monaco as monacoEditor } from 'react-monaco-editor' -import { ICommand } from 'uiSrc/constants' +import { IRedisCommand } from 'uiSrc/constants' export interface IMonacoCommand { name: string - info?: ICommand + info?: IRedisCommand position?: monacoEditor.Position } @@ -19,8 +19,8 @@ export interface IMonacoQuery { argRightOffset: number } allArgs: string[] - info?: ICommand + info?: IRedisCommand commandPosition: any position?: monacoEditor.Position - commandCursorPosition?: number + commandCursorPosition: number } diff --git a/redisinsight/ui/src/utils/transformers/redisCommands.ts b/redisinsight/ui/src/utils/transformers/redisCommands.ts new file mode 100644 index 0000000000..3dd444ff5a --- /dev/null +++ b/redisinsight/ui/src/utils/transformers/redisCommands.ts @@ -0,0 +1,12 @@ +import { ICommand, ICommands, ICommandTokenType } from 'uiSrc/constants' + +export const mergeRedisCommandsSpecs = ( + initialSpec: ICommands, + updatedSpec: ICommands +): ICommand[] => + Object.keys(initialSpec).map((name) => ({ + name, + token: name, + type: ICommandTokenType.Block, + ...(name in updatedSpec ? updatedSpec[name] : (initialSpec[name] || {})), + })) From 2e29665bf92e9c77d70bf3ed30a681f1331aa5d2 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Thu, 10 Oct 2024 16:34:17 +0200 Subject: [PATCH 085/112] #RI-6151 - fix pr comments --- .../monaco-laguages/MonacoLanguages.tsx | 3 ++- .../components/query-wrapper/QueryWrapper.tsx | 4 ++-- .../search/components/query/constants.ts | 19 ------------------- .../components/query/Query/Query.tsx | 2 -- .../ui/src/pages/workbench/constants.ts | 19 ------------------- .../workbench/utils/searchSuggestions.ts | 7 ++----- .../monacoRedisMonarchTokensProvider.ts | 7 ++++--- 7 files changed, 10 insertions(+), 51 deletions(-) diff --git a/redisinsight/ui/src/components/monaco-laguages/MonacoLanguages.tsx b/redisinsight/ui/src/components/monaco-laguages/MonacoLanguages.tsx index 4d3b9563a8..568775e2d4 100644 --- a/redisinsight/ui/src/components/monaco-laguages/MonacoLanguages.tsx +++ b/redisinsight/ui/src/components/monaco-laguages/MonacoLanguages.tsx @@ -12,6 +12,7 @@ import { getRediSearchSubRedisMonarchTokensProvider } from 'uiSrc/utils/monaco/m import SEARCH_COMMANDS_SPEC from 'uiSrc/pages/workbench/data/supported_commands.json' import { mergeRedisCommandsSpecs } from 'uiSrc/utils/transformers/redisCommands' import { SearchCommandTree } from 'uiSrc/pages/search/types' +import { ModuleCommandPrefix } from 'uiSrc/pages/workbench/constants' const MonacoLanguages = () => { const { theme } = useContext(ThemeContext) @@ -44,7 +45,7 @@ const MonacoLanguages = () => { monaco.languages.setLanguageConfiguration(MonacoLanguage.Redis, redisLanguageConfig) const REDIS_COMMANDS = mergeRedisCommandsSpecs(COMMANDS_SPEC, SEARCH_COMMANDS_SPEC) as SearchCommandTree[] - const REDIS_SEARCH_COMMANDS = REDIS_COMMANDS.filter(({ name }) => name?.startsWith('FT.')) + const REDIS_SEARCH_COMMANDS = REDIS_COMMANDS.filter(({ name }) => name?.startsWith(ModuleCommandPrefix.RediSearch)) monaco.languages.setMonarchTokensProvider( MonacoLanguage.RediSearch, diff --git a/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.tsx b/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.tsx index 88d2b7be62..accad99537 100644 --- a/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.tsx +++ b/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.tsx @@ -12,8 +12,8 @@ import { appContextSearchAndQuery, setSQScript } from 'uiSrc/slices/app/context' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { fetchRedisearchListAction, redisearchListSelector } from 'uiSrc/slices/browser/redisearch' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' -import { SUPPORTED_COMMANDS_LIST } from 'uiSrc/pages/search/components/query/constants' import { SearchCommand } from 'uiSrc/pages/search/types' +import { ModuleCommandPrefix } from 'uiSrc/pages/workbench/constants' import { TUTORIALS } from './constants' import REDIS_COMMANDS_SPEC from '../constants/supported_commands.json' @@ -49,7 +49,7 @@ const QueryWrapper = (props: Props) => { (name in REDIS_COMMANDS_SPEC ? REDIS_COMMANDS_SPEC[name] : (spec[name] || {})) const SUPPORTED_COMMANDS = commandsArray - .filter((item) => item.startsWith('FT.')) + .filter((item) => item.startsWith(ModuleCommandPrefix.RediSearch)) .map((name) => ({ ...getCommandByName(name), name })) as unknown as SearchCommand[] const { instanceId } = useParams<{ instanceId: string }>() diff --git a/redisinsight/ui/src/pages/search/components/query/constants.ts b/redisinsight/ui/src/pages/search/components/query/constants.ts index e3c7be2574..6617df7940 100644 --- a/redisinsight/ui/src/pages/search/components/query/constants.ts +++ b/redisinsight/ui/src/pages/search/components/query/constants.ts @@ -12,25 +12,6 @@ export const options = merge(defaultMonacoOptions, } }) -export const SUPPORTED_COMMANDS_LIST = [ - 'FT.SEARCH', - 'FT.AGGREGATE', - 'FT.PROFILE', - 'FT.EXPLAIN', - 'FT.INFO', - 'FT._LIST', - 'FT.ALIASADD', - 'FT.ALIASDEL', - 'FT.ALIASUPDATE', - 'FT.ALTER', - 'FT.CONFIG GET', - 'FT.CONFIG SET', - 'FT.CURSOR DEL', - 'FT.CURSOR READ', - 'FT.DICTADD', - 'FT.DICTDEL', -] - export const COMMANDS_TO_GET_INDEX_INFO = [ 'FT.SEARCH', 'FT.AGGREGATE', diff --git a/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx b/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx index 6541e6c389..c4eb8a232d 100644 --- a/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx +++ b/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx @@ -574,8 +574,6 @@ const Query = (props: Props) => { { fields: attributesRef.current, indexes: indexesRef.current } ) - console.log(helpWidgetRef) - if (helpWidget) { const { isOpen, parent, currentArg } = helpWidget helpWidgetRef.current = { diff --git a/redisinsight/ui/src/pages/workbench/constants.ts b/redisinsight/ui/src/pages/workbench/constants.ts index 9f84da285f..4453342d69 100644 --- a/redisinsight/ui/src/pages/workbench/constants.ts +++ b/redisinsight/ui/src/pages/workbench/constants.ts @@ -68,25 +68,6 @@ export enum ModuleCommandPrefix { TDIGEST = 'TDIGEST.', } -export const SUPPORTED_COMMANDS_LIST = [ - 'FT.SEARCH', - 'FT.AGGREGATE', - 'FT.PROFILE', - 'FT.EXPLAIN', - 'FT.INFO', - 'FT._LIST', - 'FT.ALIASADD', - 'FT.ALIASDEL', - 'FT.ALIASUPDATE', - 'FT.ALTER', - 'FT.CONFIG GET', - 'FT.CONFIG SET', - 'FT.CURSOR DEL', - 'FT.CURSOR READ', - 'FT.DICTADD', - 'FT.DICTDEL', -] - export const COMMANDS_TO_GET_INDEX_INFO = [ 'FT.SEARCH', 'FT.AGGREGATE', diff --git a/redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts b/redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts index 91eead84fa..cc236d3a66 100644 --- a/redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts +++ b/redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts @@ -12,7 +12,7 @@ import { getIndexesSuggestions, getNoIndexesSuggestion } from 'uiSrc/pages/workbench/utils/suggestions' -import { DefinedArgumentName, FIELD_START_SYMBOL } from 'uiSrc/pages/workbench/constants' +import { DefinedArgumentName, FIELD_START_SYMBOL, ModuleCommandPrefix } from 'uiSrc/pages/workbench/constants' export const findSuggestionsByArg = ( listOfCommands: IRedisCommand[], @@ -32,9 +32,7 @@ export const findSuggestionsByArg = ( const foundArg = findCurrentArgument(listOfCommands, beforeOffsetArgs) - console.log(foundArg) - - if (!command.name.startsWith('FT.')) { + if (!command.name.startsWith(ModuleCommandPrefix.RediSearch)) { return { helpWidget: { isOpen: !!foundArg, parent: foundArg?.parent, currentArg: foundArg?.stopArg }, suggestions: asSuggestionsRef([]) @@ -56,7 +54,6 @@ export const findSuggestionsByArg = ( ) } case DefinedArgumentName.query: { - console.log('handle query') return handleQuerySuggestions(foundArg) } default: { diff --git a/redisinsight/ui/src/utils/monaco/monacoRedisMonarchTokensProvider.ts b/redisinsight/ui/src/utils/monaco/monacoRedisMonarchTokensProvider.ts index a007c4cf3c..0c4b914333 100644 --- a/redisinsight/ui/src/utils/monaco/monacoRedisMonarchTokensProvider.ts +++ b/redisinsight/ui/src/utils/monaco/monacoRedisMonarchTokensProvider.ts @@ -1,12 +1,13 @@ import { monaco as monacoEditor } from 'react-monaco-editor' import { remove } from 'lodash' -import { SearchCommand } from 'uiSrc/pages/workbench/types' +import { ModuleCommandPrefix } from 'uiSrc/pages/workbench/constants' +import { IRedisCommand } from 'uiSrc/constants' const STRING_DOUBLE = 'string.double' -export const getRedisMonarchTokensProvider = (commands: SearchCommand[]): monacoEditor.languages.IMonarchLanguage => { +export const getRedisMonarchTokensProvider = (commands: IRedisCommand[]): monacoEditor.languages.IMonarchLanguage => { const commandRedisCommands = [...commands] - const searchCommands = remove(commandRedisCommands, ({ token }) => token?.startsWith('FT.')) + const searchCommands = remove(commandRedisCommands, ({ token }) => token?.startsWith(ModuleCommandPrefix.RediSearch)) const COMMON_COMMANDS_REGEX = `(${commandRedisCommands.map(({ token }) => token).join('|')})\\b` const SEARCH_COMMANDS_REGEX = `(${searchCommands.map(({ token }) => token).join('|')})\\b` From 050fd3c757a3120a9975de47e004a44af58edf69 Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Fri, 11 Oct 2024 10:40:47 +0200 Subject: [PATCH 086/112] remove separate tab --- tests/e2e/helpers/constants.ts | 6 - .../e2e/pageObjects/base-run-commands-page.ts | 105 -------- tests/e2e/pageObjects/browser-page.ts | 2 - .../components/keys-interaction-panel.ts | 59 ----- .../components/navigation-panel.ts | 2 - .../e2e/pageObjects/search-and-query-page.ts | 20 -- tests/e2e/pageObjects/workbench-page.ts | 128 +++++++-- .../enablement-area-autoupdate.e2e.ts | 7 +- .../critical-path/monitor/monitor.e2e.ts | 4 +- .../workbench/index-schema.e2e.ts | 5 +- .../workbench/json-workbench.e2e.ts | 5 +- .../workbench/redis-stack-commands.e2e.ts | 5 +- .../workbench/workbench-re-cluster.e2e.ts | 5 +- .../critical-path/browser/bulk-delete.e2e.ts | 6 +- .../browser/search-capabilities.e2e.ts | 6 +- .../database-overview/database-index.e2e.ts | 6 +- .../database-overview.e2e.ts | 5 +- .../web/critical-path/monitor/monitor.e2e.ts | 4 +- .../pub-sub/subscribe-unsubscribe.e2e.ts | 5 +- .../workbench/autocomplete.e2e.ts | 5 +- .../workbench/command-results.e2e.ts | 5 +- .../critical-path/workbench/context.e2e.ts | 9 +- .../web/critical-path/workbench/cypher.e2e.ts | 5 +- .../workbench/default-scripts-area.e2e.ts | 5 +- .../workbench/scripting-area.e2e.ts | 5 +- .../web/regression/browser/context.e2e.ts | 6 +- .../regression/browser/key-messages.e2e.ts | 4 +- .../browser/keys-all-databases.e2e.ts | 6 +- .../web/regression/browser/onboarding.e2e.ts | 9 +- .../regression/browser/resize-columns.e2e.ts | 6 +- .../web/regression/browser/survey-link.e2e.ts | 4 +- .../cli/cli-promote-workbench.e2e.ts | 5 +- .../database-overview/database-info.e2e.ts | 4 +- .../database-overview.e2e.ts | 4 +- .../web/regression/database/github.e2e.ts | 5 +- .../insights/import-tutorials.e2e.ts | 14 +- .../insights/live-recommendations.e2e.ts | 5 +- .../insights/open-insights-panel.e2e.ts | 4 +- .../web/regression/monitor/monitor.e2e.ts | 2 +- .../search-and-query/commands-history.e2e.ts | 163 ------------ .../no-indexes-suggestions.e2e.ts | 11 +- .../search-and-query/raw-mode.e2e.ts | 69 ----- .../search-and-query-tab.e2e.ts | 245 +++++++++--------- .../regression/workbench/autocomplete.e2e.ts | 4 +- .../workbench/autoexecute-button.e2e.ts | 4 +- .../workbench/command-results.e2e.ts | 6 +- .../web/regression/workbench/context.e2e.ts | 12 +- .../web/regression/workbench/cypher.e2e.ts | 4 +- .../workbench/default-scripts-area.e2e.ts | 6 +- .../workbench/editor-cleanup.e2e.ts | 7 +- .../workbench/empty-command-history.e2e.ts | 4 +- .../regression/workbench/group-mode.e2e.ts | 4 +- .../workbench/history-of-results.e2e.ts | 4 +- .../web/regression/workbench/raw-mode.e2e.ts | 10 +- .../workbench/redis-stack-commands.e2e.ts | 4 +- .../redisearch-module-not-available.e2e.ts | 4 +- .../workbench/scripting-area.e2e.ts | 10 +- .../workbench/workbench-all-db-types.e2e.ts | 4 +- .../workbench-non-auto-guides.e2e.ts | 6 +- .../workbench/workbench-pipeline.e2e.ts | 12 +- .../regression/workbench/workbench-tab.e2e.ts | 27 -- .../web/smoke/workbench/json-workbench.e2e.ts | 4 +- .../web/smoke/workbench/scripting-area.e2e.ts | 4 +- 63 files changed, 362 insertions(+), 763 deletions(-) delete mode 100644 tests/e2e/pageObjects/base-run-commands-page.ts delete mode 100644 tests/e2e/pageObjects/components/keys-interaction-panel.ts delete mode 100644 tests/e2e/pageObjects/search-and-query-page.ts delete mode 100644 tests/e2e/tests/web/regression/search-and-query/commands-history.e2e.ts delete mode 100644 tests/e2e/tests/web/regression/search-and-query/raw-mode.e2e.ts delete mode 100644 tests/e2e/tests/web/regression/workbench/workbench-tab.e2e.ts diff --git a/tests/e2e/helpers/constants.ts b/tests/e2e/helpers/constants.ts index b1b211106d..327c0eaa90 100644 --- a/tests/e2e/helpers/constants.ts +++ b/tests/e2e/helpers/constants.ts @@ -83,12 +83,6 @@ export enum ExploreTabs { Tips = 'Tips', } -export enum KeysInteractionTabs { - BrowserAndFilter = 'Browser and filter', - SearchAndQuery = 'Search and query', - Workbench = 'Workbench' -} - export enum Compatibility { SearchAndQuery = 'search', Json = 'json', diff --git a/tests/e2e/pageObjects/base-run-commands-page.ts b/tests/e2e/pageObjects/base-run-commands-page.ts deleted file mode 100644 index d40b647560..0000000000 --- a/tests/e2e/pageObjects/base-run-commands-page.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { Selector, t } from 'testcafe'; -import { InstancePage } from './instance-page'; - -export class BaseRunCommandsPage extends InstancePage { - - submitCommandButton = Selector('[data-testid=btn-submit]'); - queryInput = Selector('[data-testid=query-input-container]'); - queryInputForText = Selector('[data-testid=query-input-container] .view-lines'); - - // History containers - queryCardCommand = Selector('[data-testid=query-card-command]'); - fullScreenButton = Selector('[data-testid=toggle-full-screen]'); - rawModeBtn = Selector('[data-testid="btn-change-mode"]'); - queryCardContainer = Selector('[data-testid^=query-card-container]'); - reRunCommandButton = Selector('[data-testid=re-run-command]'); - copyBtn = Selector('[data-testid^=copy-btn-]'); - copyCommand = Selector('[data-testid=copy-command]'); - - runButtonToolTip = Selector('[data-testid=run-query-tooltip]'); - loadedCommand = Selector('[class=euiLoadingContent__singleLine]'); - runButtonSpinner = Selector('[data-testid=loading-spinner]'); - commandExecutionDateAndTime = Selector('[data-testid=command-execution-date-time]'); - executionCommandTime = Selector('[data-testid=command-execution-time-value]'); - executionCommandIcon = Selector('[data-testid=command-execution-time-icon]'); - executedCommandTitle = Selector('[data-testid=query-card-tooltip-anchor]', { timeout: 500 }); - queryResult = Selector('[data-testid=query-common-result]'); - queryInputScriptArea = Selector('[data-testid=query-input-container] .view-line'); - parametersAnchor = Selector('[data-testid=parameters-anchor]'); - - iframe = Selector('[data-testid=pluginIframe]'); - - //OPTIONS - selectViewType = Selector('[data-testid=select-view-type]'); - queryTableResult = Selector('[data-testid^=query-table-result-]'); - textViewTypeOption = Selector('[data-test-subj^=view-type-option-Text]'); - tableViewTypeOption = Selector('[data-test-subj^=view-type-option-Plugin]'); - - clearResultsBtn = Selector('[data-testid=clear-history-btn]'); - rawModeIcon = Selector('[data-testid=raw-mode-tooltip]'); - - cssQueryCardCommand = '[data-testid=query-card-command]'; - cssQueryCardContainer = '[data-testid^="query-card-container-"]'; - cssQueryTextResult = '[data-testid=query-cli-result]'; - cssReRunCommandButton = '[data-testid=re-run-command]'; - cssDeleteCommandButton = '[data-testid=delete-command]'; - cssJsonViewTypeOption = '[data-testid=view-type-selected-Plugin-client-list__json-view]'; - cssQueryTableResult = '[data-testid^=query-table-result-]'; - cssTableViewTypeOption = '[data-testid=view-type-selected-Plugin-redisearch__redisearch]'; - cssCommandExecutionDateTime = '[data-testid=command-execution-date-time]'; - - queryTextResult = Selector(this.cssQueryTextResult); - - getTutorialLinkLocator = (tutorialName: string): Selector => - Selector(`[data-testid=query-tutorials-link_${tutorialName}]`); - - /** - * Get card container by command - * @param command The command - */ - async getCardContainerByCommand(command: string): Promise { - return this.queryCardCommand.withExactText(command).parent(this.cssQueryCardContainer); - } - - /** - * Send a command in Workbench - * @param command The command - * @param speed The speed in seconds. Default is 1 - * @param paste - */ - 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); - } - - /** - * Check the last command and result in workbench - * @param command The command to check - * @param result The result to check - * @param childNum Indicator which command result need to check - */ - async checkWorkbenchCommandResult(command: string, result: string, childNum = 0): Promise { - // Compare the command with executed command - const actualCommand = await this.queryCardContainer.nth(childNum).find(this.cssQueryCardCommand).textContent; - await t.expect(actualCommand).contains(command, 'Actual command is not equal to executed'); - // Compare the command result with executed command - const actualCommandResult = await this.queryCardContainer.nth(childNum).find(this.cssQueryTextResult).textContent; - await t.expect(actualCommandResult).contains(result, 'Actual command result is not equal to executed'); - } - - // Select Text view option in Workbench results - async selectViewTypeText(): Promise { - await t - .click(this.selectViewType) - .click(this.textViewTypeOption); - } - - // Select Table view option in Workbench results - async selectViewTypeTable(): Promise { - await t - .click(this.selectViewType) - .doubleClick(this.tableViewTypeOption); - } -} diff --git a/tests/e2e/pageObjects/browser-page.ts b/tests/e2e/pageObjects/browser-page.ts index 7d71dc8475..297bdded5f 100644 --- a/tests/e2e/pageObjects/browser-page.ts +++ b/tests/e2e/pageObjects/browser-page.ts @@ -2,12 +2,10 @@ import { t, Selector } from 'testcafe'; import { Common } from '../helpers/common'; import { InstancePage } from './instance-page'; import { BulkActions, TreeView } from './components/browser'; -import { KeysInteractionPanel } from './components/keys-interaction-panel'; export class BrowserPage extends InstancePage { BulkActions = new BulkActions(); TreeView = new TreeView(); - KeysInteractionPanel = new KeysInteractionPanel(); //CSS Selectors cssSelectorGrid = '[aria-label="grid"]'; diff --git a/tests/e2e/pageObjects/components/keys-interaction-panel.ts b/tests/e2e/pageObjects/components/keys-interaction-panel.ts deleted file mode 100644 index 6f2ef1276c..0000000000 --- a/tests/e2e/pageObjects/components/keys-interaction-panel.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Selector, t } from 'testcafe'; -import { KeysInteractionTabs } from '../../helpers/constants'; -import { WorkbenchPage } from '../workbench-page'; -import { BrowserPage } from '../browser-page'; -import { SearchAndQueryPage } from '../search-and-query-page'; - -export class KeysInteractionPanel { - // CONTAINERS - horizontalPanel = Selector('[data-testid=browser-tabs]'); - activeTab = this.horizontalPanel.find('[class*="euiTab-isSelected"]'); - - searchTab = Selector('[data-testid=browser-tab-search]'); - browserTab = Selector('[data-testid=browser-tab-browser]'); - workbenchTab = Selector('[data-testid=browser-tab-workbench]'); - - /** - * get active tab - */ - async getActiveTabName(): Promise { - return this.activeTab.textContent; - } - - /** - * Click on Panel tab - * @param type of the tab - */ - async setActiveTab(type: KeysInteractionTabs.BrowserAndFilter): Promise - async setActiveTab(type: KeysInteractionTabs.SearchAndQuery): Promise - async setActiveTab(type: KeysInteractionTabs.Workbench): Promise - async setActiveTab(type: KeysInteractionTabs): Promise { - const activeTabName = await this.getActiveTabName(); - - let tabSelector; - let pageClass; - - switch (type) { - case KeysInteractionTabs.BrowserAndFilter: - tabSelector = this.browserTab; - pageClass = BrowserPage; - break; - case KeysInteractionTabs.Workbench: - tabSelector = this.workbenchTab; - pageClass = WorkbenchPage; - break; - case KeysInteractionTabs.SearchAndQuery: - tabSelector = this.searchTab; - pageClass = SearchAndQueryPage; - break; - default: - throw new Error(`Unknown tab type: ${type}`); - } - - if (type !== activeTabName) { - await t.click(tabSelector); - } - - return new pageClass(); - } -} diff --git a/tests/e2e/pageObjects/components/navigation-panel.ts b/tests/e2e/pageObjects/components/navigation-panel.ts index bf772e7442..b4edad4b30 100644 --- a/tests/e2e/pageObjects/components/navigation-panel.ts +++ b/tests/e2e/pageObjects/components/navigation-panel.ts @@ -7,6 +7,4 @@ export class NavigationPanel extends BaseNavigationPanel{ analysisPageButton = Selector('[data-testid=analytics-page-btn]'); browserButton = Selector('[data-testid=browser-page-btn]'); pubSubButton = Selector('[data-testid=pub-sub-page-btn]'); - - triggeredFunctionsButton = Selector('[data-testid=triggered-functions-page-btn]'); } diff --git a/tests/e2e/pageObjects/search-and-query-page.ts b/tests/e2e/pageObjects/search-and-query-page.ts deleted file mode 100644 index b4fa96b8e0..0000000000 --- a/tests/e2e/pageObjects/search-and-query-page.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { t } from 'testcafe'; -import { BaseRunCommandsPage } from './base-run-commands-page'; - -export class SearchAndQueryPage extends BaseRunCommandsPage { - - /** - * Select query using autosuggest - * @param query Value of query - */ - async selectFieldUsingAutosuggest(value: string): Promise { - await t.wait(200); - await t.typeText(this.queryInput, '@', { replace: false }); - await t.expect(this.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed'); - await t.typeText(this.queryInput, value, { replace: false }); - // Select query option into autosuggest and go out of quotes - await t.pressKey('tab'); - await t.pressKey('right'); - await t.pressKey('space'); - } -} diff --git a/tests/e2e/pageObjects/workbench-page.ts b/tests/e2e/pageObjects/workbench-page.ts index 3d4987677d..633af2da2b 100644 --- a/tests/e2e/pageObjects/workbench-page.ts +++ b/tests/e2e/pageObjects/workbench-page.ts @@ -1,7 +1,7 @@ import { Selector, t } from 'testcafe'; -import { BaseRunCommandsPage } from './base-run-commands-page'; +import { InstancePage } from './instance-page'; -export class WorkbenchPage extends BaseRunCommandsPage { +export class WorkbenchPage extends InstancePage { //CSS selectors cssSelectorPaginationButtonPrevious = '[data-test-subj=pagination-button-previous]'; cssSelectorPaginationButtonNext = '[data-test-subj=pagination-button-next]'; @@ -11,6 +11,14 @@ export class WorkbenchPage extends BaseRunCommandsPage { cssQueryCardCommand = '[data-testid=query-card-command]'; cssRowInVirtualizedTable = '[data-testid^=row-]'; cssClientListViewTypeOption = '[data-testid=view-type-selected-Plugin-client-list__clients-list]'; + cssQueryCardContainer = '[data-testid^="query-card-container-"]'; + cssQueryTextResult = '[data-testid=query-cli-result]'; + cssReRunCommandButton = '[data-testid=re-run-command]'; + cssDeleteCommandButton = '[data-testid=delete-command]'; + cssJsonViewTypeOption = '[data-testid=view-type-selected-Plugin-client-list__json-view]'; + cssQueryTableResult = '[data-testid^=query-table-result-]'; + cssTableViewTypeOption = '[data-testid=view-type-selected-Plugin-redisearch__redisearch]'; + cssCommandExecutionDateTime = '[data-testid=command-execution-date-time]'; //------------------------------------------------------------------------------------------- //DECLARATION OF SELECTORS //*Declare all elements/components of the relevant page. @@ -18,28 +26,35 @@ export class WorkbenchPage extends BaseRunCommandsPage { //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.). //------------------------------------------------------------------------------------------- //BUTTON + submitCommandButton = Selector('[data-testid=btn-submit]'); + queryInput = Selector('[data-testid=query-input-container]'); + queryInputForText = Selector('[data-testid=query-input-container] .view-lines'); resizeButtonForScriptingAndResults = Selector('[data-test-subj=resize-btn-scripting-area-and-results]'); - collapsePreselectAreaButton = Selector('[data-testid=collapse-enablement-area]'); - expandPreselectAreaButton = Selector('[data-testid=expand-enablement-area]'); paginationButtonPrevious = Selector(this.cssSelectorPaginationButtonPrevious); paginationButtonNext = Selector(this.cssSelectorPaginationButtonNext); - preselectIndexInformation = Selector('[data-testid="preselect-Additional index information"]'); - preselectExactSearch = Selector('[data-testid="preselect-Exact text search"]'); - preselectCreateHashIndex = Selector('[data-testid="preselect-Create a hash index"]'); - preselectGroupBy = Selector('[data-testid*=preselect-Group]'); preselectButtons = Selector('[data-testid^=preselect-]'); preselectManual = Selector('[data-testid=preselect-Manual]'); queryCardNoModuleButton = Selector('[data-testid=query-card-no-module-button] a'); - closeEnablementPage = Selector('[data-testid=enablement-area__page-close]'); groupMode = Selector('[data-testid=btn-change-group-mode]'); + runButtonToolTip = Selector('[data-testid=run-query-tooltip]'); + loadedCommand = Selector('[class=euiLoadingContent__singleLine]'); + runButtonSpinner = Selector('[data-testid=loading-spinner]'); + commandExecutionDateAndTime = Selector('[data-testid=command-execution-date-time]'); + executionCommandTime = Selector('[data-testid=command-execution-time-value]'); + executionCommandIcon = Selector('[data-testid=command-execution-time-icon]'); + executedCommandTitle = Selector('[data-testid=query-card-tooltip-anchor]', { timeout: 500 }); + queryResult = Selector('[data-testid=query-common-result]'); + queryInputScriptArea = Selector('[data-testid=query-input-container] .view-line'); + parametersAnchor = Selector('[data-testid=parameters-anchor]'); + clearResultsBtn = Selector('[data-testid=clear-history-btn]'); //ICONS noCommandHistoryIcon = Selector('[data-testid=wb_no-results__icon]'); groupModeIcon = Selector('[data-testid=group-mode-tooltip]'); silentModeIcon = Selector('[data-testid=silent-mode-tooltip]'); + rawModeIcon = Selector('[data-testid=raw-mode-tooltip]'); //TEXT ELEMENTS - queryPluginResult = Selector('[data-testid=query-plugin-result]'); responseInfo = Selector('[class="responseInfo"]'); parsedRedisReply = Selector('[class="parsedRedisReply"]'); mainEditorArea = Selector('[data-testid=main-input-container-area]'); @@ -60,6 +75,29 @@ export class WorkbenchPage extends BaseRunCommandsPage { viewTypeOptionClientList = Selector('[data-test-subj=view-type-option-Plugin-client-list__clients-list]'); viewTypeOptionsText = Selector('[data-test-subj=view-type-option-Text-default__Text]'); + // History containers + queryCardCommand = Selector('[data-testid=query-card-command]'); + fullScreenButton = Selector('[data-testid=toggle-full-screen]'); + rawModeBtn = Selector('[data-testid="btn-change-mode"]'); + queryCardContainer = Selector('[data-testid^=query-card-container]'); + reRunCommandButton = Selector('[data-testid=re-run-command]'); + copyBtn = Selector('[data-testid^=copy-btn-]'); + copyCommand = Selector('[data-testid=copy-command]'); + + //OPTIONS + selectViewType = Selector('[data-testid=select-view-type]'); + queryTableResult = Selector('[data-testid^=query-table-result-]'); + textViewTypeOption = Selector('[data-test-subj^=view-type-option-Text]'); + tableViewTypeOption = Selector('[data-test-subj^=view-type-option-Plugin]'); + + iframe = Selector('[data-testid=pluginIframe]'); + + queryTextResult = Selector(this.cssQueryTextResult); + + getTutorialLinkLocator = (tutorialName: string): Selector => + Selector(`[data-testid=query-tutorials-link_${tutorialName}]`); + + // Select view option in Workbench results async selectViewTypeGraph(): Promise { await t @@ -90,26 +128,74 @@ export class WorkbenchPage extends BaseRunCommandsPage { } } + // Select Json view option in Workbench results + async selectViewTypeJson(): Promise { + await t + .click(this.selectViewType) + .click(this.jsonStringViewTypeOption); + } /** - * Get internal tutorial link with .md name - * @param internalLink name of the .md file + * Get card container by command + * @param command The command */ - getInternalLinkWithManifest(internalLink: string): Selector { - return Selector(`[data-testid="internal-link-${internalLink}.md"]`); + async getCardContainerByCommand(command: string): Promise { + return this.queryCardCommand.withExactText(command).parent(this.cssQueryCardContainer); } /** - * Get internal tutorial link without .md name - * @param internalLink name of the label + * Send a command in Workbench + * @param command The command + * @param speed The speed in seconds. Default is 1 + * @param paste */ - getInternalLinkWithoutManifest(internalLink: string): Selector { - return Selector(`[data-testid="internal-link-${internalLink}"]`); + 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); } - // Select Json view option in Workbench results - async selectViewTypeJson(): Promise { + /** + * Check the last command and result in workbench + * @param command The command to check + * @param result The result to check + * @param childNum Indicator which command result need to check + */ + async checkWorkbenchCommandResult(command: string, result: string, childNum = 0): Promise { + // Compare the command with executed command + const actualCommand = await this.queryCardContainer.nth(childNum).find(this.cssQueryCardCommand).textContent; + await t.expect(actualCommand).contains(command, 'Actual command is not equal to executed'); + // Compare the command result with executed command + const actualCommandResult = await this.queryCardContainer.nth(childNum).find(this.cssQueryTextResult).textContent; + await t.expect(actualCommandResult).contains(result, 'Actual command result is not equal to executed'); + } + + // Select Text view option in Workbench results + async selectViewTypeText(): Promise { await t .click(this.selectViewType) - .click(this.jsonStringViewTypeOption); + .click(this.textViewTypeOption); + } + + // Select Table view option in Workbench results + async selectViewTypeTable(): Promise { + await t + .click(this.selectViewType) + .doubleClick(this.tableViewTypeOption); + } + + /** + * Select query using autosuggest + * @param query Value of query + */ + async selectFieldUsingAutosuggest(value: string): Promise { + await t.wait(200); + await t.typeText(this.queryInput, '@', { replace: false }); + await t.expect(this.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed'); + await t.typeText(this.queryInput, value, { replace: false }); + // Select query option into autosuggest and go out of quotes + await t.pressKey('tab'); + await t.pressKey('right'); + await t.pressKey('space'); } } diff --git a/tests/e2e/tests/electron/critical-path/files-auto-update/enablement-area-autoupdate.e2e.ts b/tests/e2e/tests/electron/critical-path/files-auto-update/enablement-area-autoupdate.e2e.ts index dc6a29752b..673236c4e1 100644 --- a/tests/e2e/tests/electron/critical-path/files-auto-update/enablement-area-autoupdate.e2e.ts +++ b/tests/e2e/tests/electron/critical-path/files-auto-update/enablement-area-autoupdate.e2e.ts @@ -1,11 +1,9 @@ -// import { join } from 'path'; -// import * as os from 'os'; import * as fs from 'fs'; import * as editJsonFile from 'edit-json-file'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig, workingDirectory } from '../../../../helpers/conf'; -import { ExploreTabs, KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { ExploreTabs, rte } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); @@ -47,8 +45,7 @@ if (fs.existsSync(workingDirectory)) { const tutorialsTimestampFileNew = editJsonFile(tutorialsTimestampPath); // Open Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); // Check Enablement area and validate that removed file is existed in Guides await workbenchPage.NavigationHeader.togglePanel(true); diff --git a/tests/e2e/tests/electron/critical-path/monitor/monitor.e2e.ts b/tests/e2e/tests/electron/critical-path/monitor/monitor.e2e.ts index 26613c2564..2cdf3c82f9 100644 --- a/tests/e2e/tests/electron/critical-path/monitor/monitor.e2e.ts +++ b/tests/e2e/tests/electron/critical-path/monitor/monitor.e2e.ts @@ -5,7 +5,7 @@ import { BrowserPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; import { APIKeyRequests } from '../../../../helpers/api/api-keys'; @@ -65,7 +65,7 @@ test('Verify that user can see the list of all commands from all clients ran for await browserPage.addHashKey(keyName); await browserPage.Profiler.checkCommandInMonitorResults(browser_command); //Open Workbench page to create new client - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); //Send command in Workbench await workbenchPage.sendCommandInWorkbench(workbench_command); //Check that command from Workbench is displayed in monitor diff --git a/tests/e2e/tests/electron/critical-path/workbench/index-schema.e2e.ts b/tests/e2e/tests/electron/critical-path/workbench/index-schema.e2e.ts index 9f55e7f9f3..8e6065c04b 100644 --- a/tests/e2e/tests/electron/critical-path/workbench/index-schema.e2e.ts +++ b/tests/e2e/tests/electron/critical-path/workbench/index-schema.e2e.ts @@ -1,4 +1,4 @@ -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneRedisearch } from '../../../../helpers/conf'; @@ -19,8 +19,7 @@ fixture `Index Schema at Workbench` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); }) .afterEach(async t => { // Drop index, documents and database diff --git a/tests/e2e/tests/electron/critical-path/workbench/json-workbench.e2e.ts b/tests/e2e/tests/electron/critical-path/workbench/json-workbench.e2e.ts index f7d44417fe..cd3c0bea8a 100644 --- a/tests/e2e/tests/electron/critical-path/workbench/json-workbench.e2e.ts +++ b/tests/e2e/tests/electron/critical-path/workbench/json-workbench.e2e.ts @@ -1,4 +1,4 @@ -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneRedisearch } from '../../../../helpers/conf'; @@ -19,8 +19,7 @@ fixture `JSON verifications at Workbench` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); }) .afterEach(async t => { // Drop index, documents and database diff --git a/tests/e2e/tests/electron/regression/workbench/redis-stack-commands.e2e.ts b/tests/e2e/tests/electron/regression/workbench/redis-stack-commands.e2e.ts index a1d9923aa6..4e7ee6c634 100644 --- a/tests/e2e/tests/electron/regression/workbench/redis-stack-commands.e2e.ts +++ b/tests/e2e/tests/electron/regression/workbench/redis-stack-commands.e2e.ts @@ -2,7 +2,7 @@ import { t } from 'testcafe'; import { DatabaseHelper } from '../../../../helpers/database'; import { WorkbenchPage, MyRedisDatabasePage, BrowserPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; -import { ExploreTabs, KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { ExploreTabs, rte } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); @@ -18,8 +18,7 @@ fixture `Redis Stack command in Workbench` .page(commonUrl) .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); - await t.click(myRedisDatabasePage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Drop key and database diff --git a/tests/e2e/tests/electron/regression/workbench/workbench-re-cluster.e2e.ts b/tests/e2e/tests/electron/regression/workbench/workbench-re-cluster.e2e.ts index b6f207a0db..985e935ad0 100644 --- a/tests/e2e/tests/electron/regression/workbench/workbench-re-cluster.e2e.ts +++ b/tests/e2e/tests/electron/regression/workbench/workbench-re-cluster.e2e.ts @@ -1,5 +1,5 @@ import { t } from 'testcafe'; -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, redisEnterpriseClusterConfig } from '../../../../helpers/conf'; @@ -18,8 +18,7 @@ const verifyCommandsInWorkbench = async(): Promise => { 'FT.SEARCH idx *' ]; - await t.click(myRedisDatabasePage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); // Send commands await workbenchPage.sendCommandInWorkbench(commandForSend1); await workbenchPage.sendCommandInWorkbench(commandForSend2); diff --git a/tests/e2e/tests/web/critical-path/browser/bulk-delete.e2e.ts b/tests/e2e/tests/web/critical-path/browser/bulk-delete.e2e.ts index af77a10ed5..b077e82397 100644 --- a/tests/e2e/tests/web/critical-path/browser/bulk-delete.e2e.ts +++ b/tests/e2e/tests/web/critical-path/browser/bulk-delete.e2e.ts @@ -1,4 +1,4 @@ -import { KeysInteractionTabs, KeyTypesTexts, rte } from '../../../../helpers/constants'; +import { KeyTypesTexts, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneRedisearch } from '../../../../helpers/conf'; @@ -99,9 +99,9 @@ test await t.click(browserPage.bulkActionsButton); await browserPage.BulkActions.startBulkDelete(); // Go to Workbench page - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); // Go to Browser Page - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.BrowserAndFilter); + await t.click(browserPage.NavigationPanel.browserButton); await t.expect(browserPage.BulkActions.bulkStatusInProgress.exists).ok('Progress value not displayed', { timeout: 5000 }); }); test diff --git a/tests/e2e/tests/web/critical-path/browser/search-capabilities.e2e.ts b/tests/e2e/tests/web/critical-path/browser/search-capabilities.e2e.ts index 3aa3195286..a649a07185 100644 --- a/tests/e2e/tests/web/critical-path/browser/search-capabilities.e2e.ts +++ b/tests/e2e/tests/web/critical-path/browser/search-capabilities.e2e.ts @@ -7,7 +7,7 @@ import { ossStandaloneConfig, ossStandaloneV5Config } from '../../../../helpers/conf'; -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; import { verifyKeysDisplayingInTheList } from '../../../../helpers/keys'; @@ -242,8 +242,8 @@ test await t.click(browserPage.getKeySelectorByName(keyName)); // Verify that Redisearch context (inputs, key selected, scroll, key details) saved after switching between pages - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.BrowserAndFilter); + await t.click(browserPage.NavigationPanel.workbenchButton); + await t.click(browserPage.NavigationPanel.browserButton); await verifyContext(); // Verify that Redisearch context saved when switching between browser/tree view diff --git a/tests/e2e/tests/web/critical-path/database-overview/database-index.e2e.ts b/tests/e2e/tests/web/critical-path/database-overview/database-index.e2e.ts index 59a7c860e3..3a701cea09 100644 --- a/tests/e2e/tests/web/critical-path/database-overview/database-index.e2e.ts +++ b/tests/e2e/tests/web/critical-path/database-overview/database-index.e2e.ts @@ -1,5 +1,5 @@ import { DatabaseHelper } from '../../../../helpers/database'; -import { KeysInteractionTabs, KeyTypesTexts, rte } from '../../../../helpers/constants'; +import { KeyTypesTexts, rte } from '../../../../helpers/constants'; import { Common } from '../../../../helpers/common'; import { MyRedisDatabasePage, @@ -105,13 +105,13 @@ test('Switching between indexed databases', async t => { await verifySearchFilterValue('Hall School'); // Open Workbench page - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); await workbenchPage.sendCommandInWorkbench(command); // Verify that user can see the database index before the command name executed in Workbench await workbenchPage.checkWorkbenchCommandResult(`[db1] ${command}`, '8'); // Open Browser page - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); + await t.click(browserPage.NavigationPanel.browserButton); // Clear filter await t.click(browserPage.clearFilterButton); // Verify that data changed for indexed db on Workbench page (on Search capability page) diff --git a/tests/e2e/tests/web/critical-path/database-overview/database-overview.e2e.ts b/tests/e2e/tests/web/critical-path/database-overview/database-overview.e2e.ts index 53d84c7d95..19ac32068a 100644 --- a/tests/e2e/tests/web/critical-path/database-overview/database-overview.e2e.ts +++ b/tests/e2e/tests/web/critical-path/database-overview/database-overview.e2e.ts @@ -1,6 +1,6 @@ import { Chance } from 'chance'; import { DatabaseHelper } from '../../../../helpers/database'; -import { ExploreTabs, KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { ExploreTabs, rte } from '../../../../helpers/constants'; import { Common } from '../../../../helpers/common'; import { MyRedisDatabasePage, @@ -145,8 +145,7 @@ test }) .after(async t => { //Delete database and index - await t.click(myRedisDatabasePage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); await workbenchPage.sendCommandInWorkbench('FT.DROPINDEX idx:schools DD'); await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneBigConfig); })('Verify that user can see additional information in Overview: Connected Clients, Commands/Sec, CPU (%) using Standalone DB connection type', async t => { diff --git a/tests/e2e/tests/web/critical-path/monitor/monitor.e2e.ts b/tests/e2e/tests/web/critical-path/monitor/monitor.e2e.ts index 24895ef253..766baed5d1 100644 --- a/tests/e2e/tests/web/critical-path/monitor/monitor.e2e.ts +++ b/tests/e2e/tests/web/critical-path/monitor/monitor.e2e.ts @@ -5,7 +5,7 @@ import { BrowserPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; import { APIKeyRequests } from '../../../../helpers/api/api-keys'; @@ -64,7 +64,7 @@ test('Verify that user can see the list of all commands from all clients ran for await browserPage.addHashKey(keyName); await browserPage.Profiler.checkCommandInMonitorResults(browser_command); //Open Workbench page to create new client - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); //Send command in Workbench await workbenchPage.sendCommandInWorkbench(workbench_command); //Check that command from Workbench is displayed in monitor diff --git a/tests/e2e/tests/web/critical-path/pub-sub/subscribe-unsubscribe.e2e.ts b/tests/e2e/tests/web/critical-path/pub-sub/subscribe-unsubscribe.e2e.ts index 74b3bdc5a2..450e698f1b 100644 --- a/tests/e2e/tests/web/critical-path/pub-sub/subscribe-unsubscribe.e2e.ts +++ b/tests/e2e/tests/web/critical-path/pub-sub/subscribe-unsubscribe.e2e.ts @@ -1,7 +1,7 @@ import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage, PubSubPage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig, ossStandaloneV5Config } from '../../../../helpers/conf'; -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { verifyMessageDisplayingInPubSub } from '../../../../helpers/pub-sub'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; @@ -121,8 +121,7 @@ test('Verify that user can see a internal link to pubsub window under word “Pu await t.expect(pubSubPage.pubSubPageContainer.exists).ok('Pubsub page is opened'); // Verify that user can see a custom message when he tries to run SUBSCRIBE command in Workbench: “Use Pub/Sub tool to subscribe to channels.” - await t.click(pubSubPage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); await workbenchPage.sendCommandInWorkbench(commandSecond); await t.expect(await workbenchPage.queryResult.textContent).eql('Use Pub/Sub tool to subscribe to channels.', 'Message is not displayed', { timeout: 10000 }); diff --git a/tests/e2e/tests/web/critical-path/workbench/autocomplete.e2e.ts b/tests/e2e/tests/web/critical-path/workbench/autocomplete.e2e.ts index 6bcb31367c..31968f0f40 100644 --- a/tests/e2e/tests/web/critical-path/workbench/autocomplete.e2e.ts +++ b/tests/e2e/tests/web/critical-path/workbench/autocomplete.e2e.ts @@ -1,4 +1,4 @@ -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; @@ -16,8 +16,7 @@ fixture `Autocomplete for entered commands` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Delete database diff --git a/tests/e2e/tests/web/critical-path/workbench/command-results.e2e.ts b/tests/e2e/tests/web/critical-path/workbench/command-results.e2e.ts index bb954b91b1..2bcd079210 100644 --- a/tests/e2e/tests/web/critical-path/workbench/command-results.e2e.ts +++ b/tests/e2e/tests/web/critical-path/workbench/command-results.e2e.ts @@ -1,4 +1,4 @@ -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; @@ -21,8 +21,7 @@ fixture `Command results at Workbench` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); }) .afterEach(async t => { await t.switchToMainWindow(); diff --git a/tests/e2e/tests/web/critical-path/workbench/context.e2e.ts b/tests/e2e/tests/web/critical-path/workbench/context.e2e.ts index 10038dd65c..5a638cb0e9 100644 --- a/tests/e2e/tests/web/critical-path/workbench/context.e2e.ts +++ b/tests/e2e/tests/web/critical-path/workbench/context.e2e.ts @@ -1,4 +1,4 @@ -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; @@ -20,8 +20,7 @@ fixture `Workbench Context` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Drop index, documents and database @@ -33,8 +32,8 @@ test('Verify that user can see saved input in Editor when navigates away to any const command = `FT.CREATE ${indexName} ON HASH PREFIX 1 product: SCHEMA name TEXT`; // Enter the command in the Workbench editor and navigate to Browser await t.typeText(workbenchPage.queryInput, command, { replace: true, speed: speed }); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.BrowserAndFilter); + await t.click(browserPage.NavigationPanel.browserButton); // Return back to Workbench and check input in editor - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); await t.expect((await workbenchPage.queryInputScriptArea.textContent).replace(/\s/g, ' ')).eql(command, 'Input in Editor is saved'); }); diff --git a/tests/e2e/tests/web/critical-path/workbench/cypher.e2e.ts b/tests/e2e/tests/web/critical-path/workbench/cypher.e2e.ts index 1dc48e7762..b572a05ed8 100644 --- a/tests/e2e/tests/web/critical-path/workbench/cypher.e2e.ts +++ b/tests/e2e/tests/web/critical-path/workbench/cypher.e2e.ts @@ -1,4 +1,4 @@ -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; @@ -16,8 +16,7 @@ fixture `Cypher syntax at Workbench` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Drop database diff --git a/tests/e2e/tests/web/critical-path/workbench/default-scripts-area.e2e.ts b/tests/e2e/tests/web/critical-path/workbench/default-scripts-area.e2e.ts index f994002218..89deed9dc1 100644 --- a/tests/e2e/tests/web/critical-path/workbench/default-scripts-area.e2e.ts +++ b/tests/e2e/tests/web/critical-path/workbench/default-scripts-area.e2e.ts @@ -1,7 +1,7 @@ import { Chance } from 'chance'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; -import { ExploreTabs, KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { ExploreTabs, rte } from '../../../../helpers/constants'; import { commonUrl, ossStandaloneRedisearch } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Telemetry } from '../../../../helpers/telemetry'; @@ -31,8 +31,7 @@ fixture `Default scripts area at Workbench` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); }) .afterEach(async t => { // Drop index, documents and database diff --git a/tests/e2e/tests/web/critical-path/workbench/scripting-area.e2e.ts b/tests/e2e/tests/web/critical-path/workbench/scripting-area.e2e.ts index 1374c4a9f3..ea74348a0a 100644 --- a/tests/e2e/tests/web/critical-path/workbench/scripting-area.e2e.ts +++ b/tests/e2e/tests/web/critical-path/workbench/scripting-area.e2e.ts @@ -1,4 +1,4 @@ -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; @@ -20,8 +20,7 @@ fixture `Scripting area at Workbench` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); //Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); }) .afterEach(async t => { await t.switchToMainWindow(); diff --git a/tests/e2e/tests/web/regression/browser/context.e2e.ts b/tests/e2e/tests/web/regression/browser/context.e2e.ts index 13e11ab855..84b983ae52 100644 --- a/tests/e2e/tests/web/regression/browser/context.e2e.ts +++ b/tests/e2e/tests/web/regression/browser/context.e2e.ts @@ -3,7 +3,7 @@ import { MyRedisDatabasePage, BrowserPage } from '../../../../pageObjects'; -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; @@ -53,9 +53,9 @@ test('Verify that when user reload the window with saved context(on any page), c // Create context modificaions and navigate to Workbench await browserPage.addStringKey(keyName); await browserPage.openKeyDetails(keyName); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); // Open Browser page and verify context - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.BrowserAndFilter); + await t.click(browserPage.NavigationPanel.browserButton); await verifySearchFilterValue(keyName); await t.expect(browserPage.keyNameFormDetails.withExactText(keyName).exists).ok('The key details is not selected'); // Navigate to Workbench and reload the window diff --git a/tests/e2e/tests/web/regression/browser/key-messages.e2e.ts b/tests/e2e/tests/web/regression/browser/key-messages.e2e.ts index 867b8c6b86..6ea552e431 100644 --- a/tests/e2e/tests/web/regression/browser/key-messages.e2e.ts +++ b/tests/e2e/tests/web/regression/browser/key-messages.e2e.ts @@ -1,4 +1,4 @@ -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; @@ -62,7 +62,7 @@ test('Verify that user can see link to Workbench under word “Workbench” in t // Add key and verify Workbench link await browserPage.Cli.sendCommandInCli(commands[i]); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.BrowserAndFilter); + await t.click(browserPage.NavigationPanel.browserButton); await browserPage.searchByKeyName(keyName); await t.click(browserPage.keyNameInTheList); await t.click(browserPage.internalLinkToWorkbench); diff --git a/tests/e2e/tests/web/regression/browser/keys-all-databases.e2e.ts b/tests/e2e/tests/web/regression/browser/keys-all-databases.e2e.ts index c0767b9456..182f558bb8 100644 --- a/tests/e2e/tests/web/regression/browser/keys-all-databases.e2e.ts +++ b/tests/e2e/tests/web/regression/browser/keys-all-databases.e2e.ts @@ -1,5 +1,5 @@ import { Selector, t } from 'testcafe'; -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage } from '../../../../pageObjects'; import { @@ -108,9 +108,9 @@ test await browserActions.verifyAllRenderedKeysHasText(); // Go to Workbench page - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); // Go to Browser Page - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.BrowserAndFilter); + await t.click(browserPage.NavigationPanel.browserButton); // Verify that keys info in row not empty after switching between pages await browserActions.verifyAllRenderedKeysHasText(); }); diff --git a/tests/e2e/tests/web/regression/browser/onboarding.e2e.ts b/tests/e2e/tests/web/regression/browser/onboarding.e2e.ts index a38078a197..3d1a270a48 100644 --- a/tests/e2e/tests/web/regression/browser/onboarding.e2e.ts +++ b/tests/e2e/tests/web/regression/browser/onboarding.e2e.ts @@ -2,7 +2,7 @@ import { DatabaseHelper } from '../../../../helpers/database'; import { commonUrl, ossStandaloneConfigEmpty } from '../../../../helpers/conf'; -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { Common } from '../../../../helpers/common'; import { MemoryEfficiencyPage, @@ -132,12 +132,12 @@ test('Verify onboard new user skip tour', async(t) => { await onboardingCardsDialog.clickNextStep(); // verify tree view step is visible await onboardingCardsDialog.verifyStepVisible('Tree view'); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); await t.click(myRedisDatabasePage.NavigationPanel.helpCenterButton); await t.expect(myRedisDatabasePage.NavigationPanel.HelpCenter.helpCenterPanel.visible).ok('help center panel is not opened'); await t.click(onboardingCardsDialog.resetOnboardingBtn); await t.click(myRedisDatabasePage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.BrowserAndFilter); + await t.click(browserPage.NavigationPanel.browserButton); // Verify that when user reset onboarding, user can see the onboarding triggered when user open the Browser page. await t.expect(onboardingCardsDialog.showMeAroundButton.visible).ok('onboarding starting is not visible'); // click skip tour @@ -160,7 +160,7 @@ test.requestHooks(logger)('Verify that the final onboarding step is closed when await onboardingCardsDialog.verifyStepVisible('Great job!'); // Go to Workbench page await t.click(myRedisDatabasePage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); // Verify that “ONBOARDING_TOUR_FINISHED” event is sent when user opens another page (or close the app) await telemetry.verifyEventHasProperties(telemetryEvent, expectedProperties, logger); @@ -172,7 +172,6 @@ test.requestHooks(logger)('Verify that the final onboarding step is closed when await t.expect(onboardingCardsDialog.stepTitle.exists).notOk('Onboarding tooltip still visible'); // Go to Browser Page await t.click(myRedisDatabasePage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.BrowserAndFilter); // Verify onboarding completed successfully await onboardingCardsDialog.completeOnboarding(); await t.expect(browserPage.patternModeBtn.visible).ok('Browser page is not opened'); diff --git a/tests/e2e/tests/web/regression/browser/resize-columns.e2e.ts b/tests/e2e/tests/web/regression/browser/resize-columns.e2e.ts index 446c4b45a6..ffdec4b48e 100644 --- a/tests/e2e/tests/web/regression/browser/resize-columns.e2e.ts +++ b/tests/e2e/tests/web/regression/browser/resize-columns.e2e.ts @@ -3,7 +3,7 @@ import { MyRedisDatabasePage, BrowserPage } from '../../../../pageObjects'; -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; @@ -80,8 +80,8 @@ test('Resize of columns in Hash, List, Zset Key details', async t => { } // Verify that resize saved when switching between pages - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.BrowserAndFilter); + await t.click(browserPage.NavigationPanel.workbenchButton); + await t.click(browserPage.NavigationPanel.browserButton); await browserPage.openKeyDetails(keys[0].name); await t.expect(field.clientWidth).within(keys[0].fieldWidthEnd - 5, keys[0].fieldWidthEnd + 5, 'Resize context not saved for key when switching between pages'); diff --git a/tests/e2e/tests/web/regression/browser/survey-link.e2e.ts b/tests/e2e/tests/web/regression/browser/survey-link.e2e.ts index 84471aae6e..e89ee99259 100644 --- a/tests/e2e/tests/web/regression/browser/survey-link.e2e.ts +++ b/tests/e2e/tests/web/regression/browser/survey-link.e2e.ts @@ -1,5 +1,5 @@ import { DatabaseHelper } from '../../../../helpers/database'; -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { BrowserPage, MyRedisDatabasePage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; @@ -32,7 +32,7 @@ test('Verify that user can use survey link', async t => { // await Common.checkURL(externalPageLink); // await goBackHistory(); // Workbench page - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); await t.expect(browserPage.userSurveyLink.visible).ok('Survey Link is not displayed'); // Slow Log page await t.click(myRedisDatabasePage.NavigationPanel.analysisPageButton); diff --git a/tests/e2e/tests/web/regression/cli/cli-promote-workbench.e2e.ts b/tests/e2e/tests/web/regression/cli/cli-promote-workbench.e2e.ts index 30be6c7e77..cc0b7a428b 100644 --- a/tests/e2e/tests/web/regression/cli/cli-promote-workbench.e2e.ts +++ b/tests/e2e/tests/web/regression/cli/cli-promote-workbench.e2e.ts @@ -1,7 +1,7 @@ import { DatabaseHelper } from '../../../../helpers/database'; import { WorkbenchPage, MyRedisDatabasePage, BrowserPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; const browserPage = new BrowserPage(); @@ -22,8 +22,7 @@ fixture `Promote workbench in CLI` }); test('Verify that user can see saved workbench context after redirection from CLI to workbench', async t => { // Open Workbench - await t.click(myRedisDatabasePage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); const command = 'INFO'; await t.typeText(workbenchPage.queryInput, command, { replace: true, speed: 1, paste: true }); await t.click(myRedisDatabasePage.NavigationPanel.browserButton); diff --git a/tests/e2e/tests/web/regression/database-overview/database-info.e2e.ts b/tests/e2e/tests/web/regression/database-overview/database-info.e2e.ts index 522babd78a..d691abc7b1 100644 --- a/tests/e2e/tests/web/regression/database-overview/database-info.e2e.ts +++ b/tests/e2e/tests/web/regression/database-overview/database-info.e2e.ts @@ -4,7 +4,7 @@ import { WorkbenchPage, BrowserPage } from '../../../../pageObjects'; -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; @@ -41,6 +41,6 @@ test('Verify that user can see DB name, endpoint, connection type, Redis version // Verify that user can see an (i) icon next to the database name on Browser and Workbench pages await t.expect(browserPage.OverviewPanel.databaseInfoIcon.visible).ok('User can not see (i) icon on Browser page', { timeout: 10000 }); // Move to the Workbench page and check icon - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); await t.expect(workbenchPage.OverviewPanel.overviewTotalMemory.visible).ok('User can not see (i) icon on Workbench page', { timeout: 10000 }); }); diff --git a/tests/e2e/tests/web/regression/database-overview/database-overview.e2e.ts b/tests/e2e/tests/web/regression/database-overview/database-overview.e2e.ts index a09cc88a66..919691ce06 100644 --- a/tests/e2e/tests/web/regression/database-overview/database-overview.e2e.ts +++ b/tests/e2e/tests/web/regression/database-overview/database-overview.e2e.ts @@ -4,7 +4,7 @@ import { WorkbenchPage, BrowserPage } from '../../../../pageObjects'; -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { Common } from '../../../../helpers/common'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; @@ -35,7 +35,7 @@ test('Verify that user can connect to DB and see breadcrumbs at the top of the a // Verify that user can see breadcrumbs in Browser and Workbench views await t.expect(browserPage.breadcrumbsContainer.visible).ok('User can not see breadcrumbs in Browser page', { timeout: 10000 }); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); await t.expect(browserPage.breadcrumbsContainer.visible).ok('User can not see breadcrumbs in Workbench page', { timeout: 10000 }); // Verify that user can see total memory and total number of keys updated in DB header in Workbench page diff --git a/tests/e2e/tests/web/regression/database/github.e2e.ts b/tests/e2e/tests/web/regression/database/github.e2e.ts index d1e5c008d2..c8ef0c3575 100644 --- a/tests/e2e/tests/web/regression/database/github.e2e.ts +++ b/tests/e2e/tests/web/regression/database/github.e2e.ts @@ -1,4 +1,4 @@ -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; @@ -29,8 +29,7 @@ test('Verify that user can work with Github link in the application', async t => await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName); await t.expect(myRedisDatabasePage.NavigationPanel.githubButton.visible).ok('Github button not found'); // Verify that user can see the icon for GitHub reference at the bottom of the left side bar on the Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); await t.expect(myRedisDatabasePage.NavigationPanel.githubButton.visible).ok('Github button'); // Verify that when user clicks on Github icon he redirects to the URL: https://github.com/RedisInsight/RedisInsight await t.click(myRedisDatabasePage.NavigationPanel.githubButton); diff --git a/tests/e2e/tests/web/regression/insights/import-tutorials.e2e.ts b/tests/e2e/tests/web/regression/insights/import-tutorials.e2e.ts index c7fa7f9178..4a047986e9 100644 --- a/tests/e2e/tests/web/regression/insights/import-tutorials.e2e.ts +++ b/tests/e2e/tests/web/regression/insights/import-tutorials.e2e.ts @@ -2,7 +2,7 @@ import * as path from 'path'; import * as fs from 'fs'; import { join as joinPath } from 'path'; import { t } from 'testcafe'; -import { ExploreTabs, KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { ExploreTabs, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, MemoryEfficiencyPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig, ossStandaloneRedisearch, fileDownloadPath } from '../../../../helpers/conf'; @@ -41,8 +41,7 @@ fixture `Upload custom tutorials` .page(commonUrl) .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); - await t.click(myRedisDatabasePage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); }) .afterEach(async() => { await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); @@ -53,8 +52,7 @@ https://redislabs.atlassian.net/browse/RI-4302, https://redislabs.atlassian.net/ test .before(async() => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); - await t.click(myRedisDatabasePage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); tutorialName = `${zipFolderName}${Common.generateWord(5)}`; zipFilePath = path.join('..', 'test-data', 'upload-tutorials', `${tutorialName}.zip`); @@ -169,8 +167,7 @@ test test .before(async() => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); - await t.click(myRedisDatabasePage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); tutorialName = `${zipFolderName}${Common.generateWord(5)}`; zipFilePath = path.join('..', 'test-data', 'upload-tutorials', `${tutorialName}.zip`); @@ -183,8 +180,7 @@ test await Common.deleteFileFromFolder(zipFilePath); await deleteAllKeysFromDB(ossStandaloneRedisearch.host, ossStandaloneRedisearch.port); // Clear and delete database - await t.click(browserPage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); await workbenchPage.NavigationHeader.togglePanel(true); const tutorials = await workbenchPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials); diff --git a/tests/e2e/tests/web/regression/insights/live-recommendations.e2e.ts b/tests/e2e/tests/web/regression/insights/live-recommendations.e2e.ts index b6b5de168c..782b61a556 100644 --- a/tests/e2e/tests/web/regression/insights/live-recommendations.e2e.ts +++ b/tests/e2e/tests/web/regression/insights/live-recommendations.e2e.ts @@ -1,6 +1,6 @@ import * as path from 'path'; import { BrowserPage, MemoryEfficiencyPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; -import { ExploreTabs, KeysInteractionTabs, RecommendationIds, rte } from '../../../../helpers/constants'; +import { ExploreTabs, RecommendationIds, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { commonUrl, ossStandaloneConfig, ossStandaloneV5Config, ossStandaloneV7Config } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; @@ -246,8 +246,7 @@ test //Verify that user is navigated to DB Analysis page via Analyze button and new report is generated await t.click(memoryEfficiencyPage.selectedReport); await t.expect(memoryEfficiencyPage.reportItem.visible).ok('Database analysis page not opened'); - await t.click(memoryEfficiencyPage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(memoryEfficiencyPage.NavigationPanel.workbenchButton); await workbenchPage.NavigationHeader.togglePanel(true); tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tips); await t.click(tab.analyzeDatabaseLink); diff --git a/tests/e2e/tests/web/regression/insights/open-insights-panel.e2e.ts b/tests/e2e/tests/web/regression/insights/open-insights-panel.e2e.ts index c603f30c5c..d1346c1f85 100644 --- a/tests/e2e/tests/web/regression/insights/open-insights-panel.e2e.ts +++ b/tests/e2e/tests/web/regression/insights/open-insights-panel.e2e.ts @@ -1,5 +1,5 @@ import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; -import { Compatibility, ExploreTabs, KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { Compatibility, ExploreTabs, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { commonUrl, @@ -47,7 +47,7 @@ test await t.click(browserPage.NavigationPanel.myRedisDBButton); await myRedisDatabasePage.clickOnDBByName(ossStandaloneV5Config.databaseName); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); await workbenchPage.sendCommandInWorkbench('TS.'); await t.click(browserPage.NavigationPanel.myRedisDBButton); diff --git a/tests/e2e/tests/web/regression/monitor/monitor.e2e.ts b/tests/e2e/tests/web/regression/monitor/monitor.e2e.ts index de46faf847..761050361d 100644 --- a/tests/e2e/tests/web/regression/monitor/monitor.e2e.ts +++ b/tests/e2e/tests/web/regression/monitor/monitor.e2e.ts @@ -10,7 +10,7 @@ import { ossStandaloneBigConfig, ossStandaloneConfig, } from '../../../../helpers/conf'; -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; const myRedisDatabasePage = new MyRedisDatabasePage(); diff --git a/tests/e2e/tests/web/regression/search-and-query/commands-history.e2e.ts b/tests/e2e/tests/web/regression/search-and-query/commands-history.e2e.ts deleted file mode 100644 index f41f2daea5..0000000000 --- a/tests/e2e/tests/web/regression/search-and-query/commands-history.e2e.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; -import { DatabaseHelper } from '../../../../helpers/database'; -import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; -import { SearchAndQueryPage } from '../../../../pageObjects/search-and-query-page'; -import { commonUrl, ossClusterConfig } from '../../../../helpers/conf'; -import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; -import { Common } from '../../../../helpers/common'; - -const myRedisDatabasePage = new MyRedisDatabasePage(); -const searchAndQueryPage = new SearchAndQueryPage(); -const databaseHelper = new DatabaseHelper(); -const databaseAPIRequests = new DatabaseAPIRequests(); -const browserPage = new BrowserPage(); -const workbenchPage = new WorkbenchPage(); - -const commandForSend1 = 'FT.INFO'; -const commandForSend2 = 'FT._LIST'; -let indexName = Common.generateWord(5); - -const commandsForIndex = [ - `FT.CREATE ${indexName} ON HASH PREFIX 1 product: SCHEMA price NUMERIC SORTABLE`, - 'HMSET product:1 price 20', - 'HMSET product:2 price 100' -]; - -fixture `Command results at Search and Query` - .meta({ type: 'critical_path', rte: rte.standalone }) - .page(commonUrl) - .beforeEach(async t => { - await databaseHelper.acceptLicenseTermsAndAddOSSClusterDatabase(ossClusterConfig); - // Go to Workbench page - await t.click(myRedisDatabasePage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); - }) - .afterEach(async t => { - await t.switchToMainWindow(); - await workbenchPage.sendCommandInWorkbench(`FT.DROPINDEX ${indexName} DD`); - await databaseAPIRequests.deleteOSSClusterDatabaseApi(ossClusterConfig); - }); -test('Verify that user can see re-run icon near the already executed command and re-execute the command by clicking on the icon in Workbench page', async t => { - // Send commands - await searchAndQueryPage.sendCommandInWorkbench(commandForSend1); - await searchAndQueryPage.sendCommandInWorkbench(commandForSend2); - const containerOfCommand = await searchAndQueryPage.getCardContainerByCommand(commandForSend1); - const containerOfCommand2 = await searchAndQueryPage.getCardContainerByCommand(commandForSend2); - // Verify that re-run icon is displayed - await t.expect(await searchAndQueryPage.reRunCommandButton.visible).ok('Re-run icon is not displayed'); - // Re-run the last command in results - await t.click(containerOfCommand.find(searchAndQueryPage.cssReRunCommandButton)); - // Verify that command is re-executed - await t.expect(searchAndQueryPage.queryCardCommand.textContent).eql(commandForSend1, 'The command is not re-executed'); - - // Verify that user can see expanded result after command re-run at the top of results table in Workbench - await t.expect(await searchAndQueryPage.queryCardContainer.nth(0).find(searchAndQueryPage.cssQueryTextResult).visible) - .ok('Re-executed command is not expanded'); - await t.expect(searchAndQueryPage.queryCardCommand.nth(0).textContent).eql(commandForSend1, 'The re-executed command is not at the top of results table'); - - // Delete the command from results - await t.click(containerOfCommand2.find(searchAndQueryPage.cssDeleteCommandButton)); - // Verify that user can delete command with result from table with results in Workbench - await t.expect(searchAndQueryPage.queryCardCommand.withExactText(commandForSend2).exists).notOk(`Command ${commandForSend2} is not deleted from table with results`); -}); -test('Verify that user can see the results found in the table view by default for FT.INFO, FT.SEARCH and FT.AGGREGATE', async t => { - const commands = [ - 'FT.INFO', - 'FT.SEARCH', - 'FT.AGGREGATE' - ]; - // Send commands and check table view is default - for(const command of commands) { - await searchAndQueryPage.sendCommandInWorkbench(command); - await t.expect(searchAndQueryPage.queryCardContainer.nth(0).find(searchAndQueryPage.cssTableViewTypeOption).exists).ok(`The table view is not selected by default for command ${command}`); - } -}); -test - .after(async() => { - await searchAndQueryPage.sendCommandInWorkbench(`FT.DROPINDEX ${indexName} DD`); - })('Verify that user can switches between views and see results according to the view rules in Workbench in results', async t => { - indexName = Common.generateWord(5); - const commands = [ - 'hset doc:10 title "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud" url "redis.io" author "Test" rate "undefined" review "0" comment "Test comment"', - `FT.CREATE ${indexName} ON HASH PREFIX 1 doc: SCHEMA title TEXT WEIGHT 5.0 body TEXT url TEXT author TEXT rate TEXT review TEXT comment TEXT`, - `FT.SEARCH ${indexName} * limit 0 10000` - ]; - // Send commands and check table view is default for Search command - for (const command of commands) { - await searchAndQueryPage.sendCommandInWorkbench(command); - } - await t.expect(await searchAndQueryPage.queryCardContainer.nth(0).find(searchAndQueryPage.cssTableViewTypeOption).exists) - .ok('The table view is not selected by default for command FT.SEARCH'); - await t.switchToIframe(searchAndQueryPage.iframe); - await t.expect(await searchAndQueryPage.queryTableResult.visible).ok('The table result is not displayed for command FT.SEARCH'); - // Select Text view and check result - await t.switchToMainWindow(); - await searchAndQueryPage.selectViewTypeText(); - await t.expect(await searchAndQueryPage.queryCardContainer.nth(0).find(searchAndQueryPage.cssQueryTextResult).visible).ok('The result is not displayed in Text view'); - }); - -test('Verify that user can clear all results at once.', async t => { - await t.click(searchAndQueryPage.clearResultsBtn); - await t.expect(searchAndQueryPage.queryTextResult.exists).notOk('Clear all button does not remove commands'); -}); - -test('Verify that user can switches between Table and Text for FT.AGGREGATE and see results corresponding to their views', async t => { - - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); - await workbenchPage.sendCommandsArrayInWorkbench(commandsForIndex); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); - const aggregateCommand = `FT.Aggregate ${indexName} * GROUPBY 0 REDUCE MAX 1 @price AS max_price`; - - // Send FT.AGGREGATE and switch to Text view - await searchAndQueryPage.sendCommandInWorkbench(aggregateCommand); - await searchAndQueryPage.selectViewTypeText(); - await t.expect(searchAndQueryPage.queryCardContainer.nth(0).find(searchAndQueryPage.cssQueryTextResult).visible).ok('The text view is not switched for command FT.AGGREGATE'); - // Switch to Table view and check result - await searchAndQueryPage.selectViewTypeTable(); - await t.switchToIframe(searchAndQueryPage.iframe); - await t.expect(searchAndQueryPage.queryTableResult.exists).ok('The table view is not switched for command FT.AGGREGATE'); -}); - -test('Verify that user can switches between Table and Text for FT.SEARCH and see results corresponding to their views', async t => { - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); - await workbenchPage.sendCommandsArrayInWorkbench(commandsForIndex); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); - const searchCommand = `FT.SEARCH ${indexName} *`; - - // Send FT.SEARCH and switch to Text view - await searchAndQueryPage.sendCommandInWorkbench(searchCommand); - await searchAndQueryPage.selectViewTypeText(); - await t.expect(searchAndQueryPage.queryCardContainer.nth(0).find(searchAndQueryPage.cssQueryTextResult).visible).ok('The text view is not switched for command FT.SEARCH'); - // Switch to Table view and check result - await searchAndQueryPage.selectViewTypeTable(); - await t.switchToIframe(searchAndQueryPage.iframe); - await t.expect(searchAndQueryPage.queryTableResult.exists).ok('The table view is not switched for command FT.SEARCH'); -}); -test('Verify that user can switches between Table and Text for FT.INFO and see results corresponding to their views', async t => { - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); - await workbenchPage.sendCommandsArrayInWorkbench(commandsForIndex); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); - const infoCommand = `FT.INFO ${indexName}`; - - // Send FT.INFO and switch to Text view - await searchAndQueryPage.sendCommandInWorkbench(infoCommand); - await searchAndQueryPage.selectViewTypeText(); - await t.expect(searchAndQueryPage.queryCardContainer.nth(0).find(searchAndQueryPage.cssQueryTextResult).exists).ok('The text view is not switched for command FT.INFO'); - // Switch to Table view and check result - await searchAndQueryPage.selectViewTypeTable(); - await t.switchToIframe(searchAndQueryPage.iframe); - await t.expect(searchAndQueryPage.queryTableResult.exists).ok('The table view is not switched for command FT.INFO'); -}); -test('Verify that user can see original date and time of command execution in Workbench history after the page update', async t => { - const keyName = Common.generateWord(5); - const command = `set ${keyName} test`; - - // Send command and remember the time - await searchAndQueryPage.sendCommandInWorkbench(command); - const dateTime = await searchAndQueryPage.queryCardContainer.nth(0).find(searchAndQueryPage.cssCommandExecutionDateTime).textContent; - // Wait fo 1 minute, refresh page and check results - await t.wait(60000); - await searchAndQueryPage.reloadPage(); - await t.expect(searchAndQueryPage.queryCardContainer.nth(0).find(searchAndQueryPage.cssCommandExecutionDateTime).textContent).eql(dateTime, 'The original date and time of command execution is not saved after the page update'); -}); - diff --git a/tests/e2e/tests/web/regression/search-and-query/no-indexes-suggestions.e2e.ts b/tests/e2e/tests/web/regression/search-and-query/no-indexes-suggestions.e2e.ts index b076bd565c..3b6f4ec665 100644 --- a/tests/e2e/tests/web/regression/search-and-query/no-indexes-suggestions.e2e.ts +++ b/tests/e2e/tests/web/regression/search-and-query/no-indexes-suggestions.e2e.ts @@ -1,14 +1,13 @@ import { DatabaseHelper } from '../../../../helpers/database'; -import { BrowserPage } from '../../../../pageObjects'; +import { BrowserPage, WorkbenchPage } from '../../../../pageObjects'; import { rte } from '../../../../helpers/constants'; import { commonUrl, ossClusterConfig, ossStandaloneConfig, ossStandaloneRedisearch } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; -import { SearchAndQueryPage } from '../../../../pageObjects/search-and-query-page'; const browserPage = new BrowserPage(); const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); -const searchAndQueryPage = new SearchAndQueryPage(); +const workbenchPage = new WorkbenchPage(); fixture `Search and Query Raw mode` .meta({ type: 'critical_path', rte: rte.standalone }) @@ -26,11 +25,11 @@ test // TODO add navigation to search and query Monaco - await t.typeText(searchAndQueryPage.queryInput, 'FT.SE', { replace: true }); + await t.typeText(workbenchPage.queryInput, 'FT.SE', { replace: true }); await t.pressKey('enter'); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('No indexes to display').exists).ok('info text is not displayed'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('No indexes to display').exists).ok('info text is not displayed'); await t.pressKey('ctrl+space'); - await t.expect(await searchAndQueryPage.MonacoEditor.monacoCommandDetails.find('a').exists).ok('no link in the details') + await t.expect(await workbenchPage.MonacoEditor.monacoCommandDetails.find('a').exists).ok('no link in the details') }); diff --git a/tests/e2e/tests/web/regression/search-and-query/raw-mode.e2e.ts b/tests/e2e/tests/web/regression/search-and-query/raw-mode.e2e.ts deleted file mode 100644 index 32d7ed7a05..0000000000 --- a/tests/e2e/tests/web/regression/search-and-query/raw-mode.e2e.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { DatabaseHelper } from '../../../../helpers/database'; -import { WorkbenchPage, MyRedisDatabasePage, BrowserPage } from '../../../../pageObjects'; -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; -import { commonUrl, ossStandaloneConfig, ossStandaloneRedisearch } from '../../../../helpers/conf'; -import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; -import { Common } from '../../../../helpers/common'; -import { SearchAndQueryPage } from '../../../../pageObjects/search-and-query-page'; - -const myRedisDatabasePage = new MyRedisDatabasePage(); -const workbenchPage = new WorkbenchPage(); -const browserPage = new BrowserPage(); -const databaseHelper = new DatabaseHelper(); -const databaseAPIRequests = new DatabaseAPIRequests(); -const searchAndQueryPage = new SearchAndQueryPage(); - -const indexName = Common.generateWord(5); -const unicodeValue = '山女馬 / 马目 abc 123'; - -const databasesForAdding = - { host: ossStandaloneConfig.host, port: ossStandaloneConfig.port, databaseName: 'testDB2' }; - -fixture `Search and Query Raw mode` - .meta({ type: 'critical_path', rte: rte.standalone }) - .page(commonUrl); - -test - .before(async t => { - await databaseHelper.acceptLicenseTerms(); - await databaseAPIRequests.addNewStandaloneDatabaseApi( - databasesForAdding); - await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); - // Go to Workbench page - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); - }) - .after(async t => { - // Drop index, documents and database - await t.switchToMainWindow(); - await workbenchPage.sendCommandInWorkbench(`FT.DROPINDEX ${indexName} DD`); - await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneRedisearch); - - })('Display Raw mode for plugins and save state', async t => { - const commandsForSend = [ - `FT.CREATE ${indexName} ON HASH PREFIX 1 product: SCHEMA name TEXT`, - `HMSET product:1 name "${unicodeValue}"` - ]; - const commandSearch = `FT.SEARCH ${indexName} "${unicodeValue}"`; - - await workbenchPage.sendCommandsArrayInWorkbench(commandsForSend); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); - // Send command in raw mode - await t.click(searchAndQueryPage.rawModeBtn); - await searchAndQueryPage.sendCommandInWorkbench(commandSearch); - // Check the FT.SEARCH result - await t.switchToIframe(workbenchPage.iframe); - let name = workbenchPage.queryTableResult.withText(unicodeValue); - await t.expect(name.exists).ok('The added key name field is not converted to Unicode'); - await t.switchToMainWindow(); - await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton); - await myRedisDatabasePage.clickOnDBByName(databasesForAdding.databaseName); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); - - await workbenchPage.sendCommandsArrayInWorkbench(commandsForSend); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); - await searchAndQueryPage.sendCommandInWorkbench(commandSearch); - // Check the FT.SEARCH result - await t.switchToIframe(workbenchPage.iframe); - name = workbenchPage.queryTableResult.withText(unicodeValue); - await t.expect(name.exists).ok('The added key name field is not converted to Unicode'); - }); diff --git a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts index 6050175477..0670de33ea 100644 --- a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts +++ b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts @@ -1,15 +1,14 @@ import { Common, DatabaseHelper } from '../../../../helpers'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; -import { BrowserPage } from '../../../../pageObjects'; -import { ExploreTabs, KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { BrowserPage, WorkbenchPage } from '../../../../pageObjects'; +import { ExploreTabs, rte } from '../../../../helpers/constants'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; -import { SearchAndQueryPage } from '../../../../pageObjects/search-and-query-page'; import { APIKeyRequests } from '../../../../helpers/api/api-keys'; const databaseHelper = new DatabaseHelper(); const databaseAPIRequests = new DatabaseAPIRequests(); const browserPage = new BrowserPage(); -const searchAndQueryPage = new SearchAndQueryPage(); +const workbenchPage = new WorkbenchPage(); const apiKeyRequests = new APIKeyRequests(); const keyName = Common.generateWord(10); @@ -21,7 +20,7 @@ let indexName3: string; fixture `Autocomplete for entered commands in search and query` .meta({ type: 'regression', rte: rte.standalone }) .page(commonUrl) - .beforeEach(async() => { + .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); indexName1 = `idx1:${keyName}`; indexName2 = `idx2:${keyName}`; @@ -37,7 +36,7 @@ fixture `Autocomplete for entered commands in search and query` // Create 3 keys and index await browserPage.Cli.sendCommandsInCli(commands); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); + await t.click(browserPage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Clear and delete database @@ -47,9 +46,9 @@ fixture `Autocomplete for entered commands in search and query` await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that tutorials can be opened from Workbench', async t => { - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.SearchAndQuery); - await t.click(searchAndQueryPage.getTutorialLinkLocator('sq-exact-match')); - await t.expect(searchAndQueryPage.InsightsPanel.sidePanel.exists).ok('Insight panel is not opened'); + await t.click(browserPage.NavigationPanel.workbenchButton); + await t.click(workbenchPage.getTutorialLinkLocator('sq-exact-match')); + await t.expect(workbenchPage.InsightsPanel.sidePanel.exists).ok('Insight panel is not opened'); const tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials); await t.expect(tab.preselectArea.textContent).contains('EXACT MATCH', 'the tutorial page is incorrect'); }); @@ -62,16 +61,16 @@ test('Verify that user can use show more to see command fully in 2nd tooltip', a 'required query', 'optional [verbatim]' ]; - await t.typeText(searchAndQueryPage.queryInput, 'FT', { replace: true }); + await t.typeText(workbenchPage.queryInput, 'FT', { replace: true }); // Verify that user can use show more to see command fully in 2nd tooltip await t.pressKey('ctrl+space'); - await t.expect(searchAndQueryPage.MonacoEditor.monacoCommandDetails.exists).ok('The "read more" about the command is not opened'); + await t.expect(workbenchPage.MonacoEditor.monacoCommandDetails.exists).ok('The "read more" about the command is not opened'); for(const detail of commandDetails) { - await t.expect(searchAndQueryPage.MonacoEditor.monacoCommandDetails.textContent).contains(detail, `The ${detail} command detail is not displayed`); + await t.expect(workbenchPage.MonacoEditor.monacoCommandDetails.textContent).contains(detail, `The ${detail} command detail is not displayed`); } // Verify that user can close show more tooltip by 'x' or 'Show less' await t.pressKey('ctrl+space'); - await t.expect(searchAndQueryPage.MonacoEditor.monacoCommandDetails.exists).notOk('The "read more" about the command is not closed'); + await t.expect(workbenchPage.MonacoEditor.monacoCommandDetails.exists).notOk('The "read more" about the command is not closed'); }); test('Verify full commands suggestions with index and query for FT.AGGREGATE', async t => { const groupByArgInfo = 'GROUPBY nargs property [property ...] [REDUCE '; @@ -86,257 +85,257 @@ test('Verify full commands suggestions with index and query for FT.AGGREGATE', a 'type' ]; // Verify basic commands suggestions FT.SEARCH and FT.AGGREGATE - await t.typeText(searchAndQueryPage.queryInput, 'FT', { replace: true }); + await t.typeText(workbenchPage.queryInput, 'FT', { replace: true }); // Verify that the list with FT.SEARCH and FT.AGGREGATE auto-suggestions is displayed - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withText('FT.SEARCH').exists).ok('FT.SEARCH auto-suggestions are not displayed'); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withText('FT.AGGREGATE').exists).ok('FT.AGGREGATE auto-suggestions are not displayed'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withText('FT.SEARCH').exists).ok('FT.SEARCH auto-suggestions are not displayed'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withText('FT.AGGREGATE').exists).ok('FT.AGGREGATE auto-suggestions are not displayed'); // Select command and check result await t.pressKey('enter'); - let script = await searchAndQueryPage.queryInputScriptArea.textContent; + let script = await workbenchPage.queryInputScriptArea.textContent; await t.expect(script.replace(/\s/g, ' ')).contains('FT.AGGREGATE ', 'Result of sent command exists'); // Verify that user can see the list of all the indexes in database when put a space after only FT.SEARCH and FT.AGGREGATE commands await t.expect(script.replace(/\s/g, ' ')).contains(`"${indexName1}" "" `, 'Index not suggested into input'); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText(indexName1).exists).ok('Index not auto-suggested'); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText(indexName2).exists).ok('All indexes not auto-suggested'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText(indexName1).exists).ok('Index not auto-suggested'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText(indexName2).exists).ok('All indexes not auto-suggested'); await t.pressKey('tab'); await t.wait(200); - await t.typeText(searchAndQueryPage.queryInput, '@', { replace: false }); - script = await searchAndQueryPage.queryInputScriptArea.textContent; + await t.typeText(workbenchPage.queryInput, '@', { replace: false }); + script = await workbenchPage.queryInputScriptArea.textContent; // Verify that user can see the list of fields from the index selected when type in “@” await t.expect(script.replace(/\s/g, ' ')).contains('address', 'Index not suggested into input'); for(const field of indexFields) { - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText(field).exists).ok(`${field} Index field not auto-suggested`); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText(field).exists).ok(`${field} Index field not auto-suggested`); } // Verify that user can use autosuggestions by typing fields from index after "@" - await t.typeText(searchAndQueryPage.queryInput, 'ci', { replace: false }); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('city').exists).ok('Index field not auto-suggested after starting typing'); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.count).eql(1, 'Wrong index fields suggested after typing first letter'); + await t.typeText(workbenchPage.queryInput, 'ci', { replace: false }); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('city').exists).ok('Index field not auto-suggested after starting typing'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.count).eql(1, 'Wrong index fields suggested after typing first letter'); // Verify contextual suggestions after typing letters for commands await t.pressKey('tab'); await t.pressKey('right'); await t.pressKey('space'); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('APPLY').exists).ok('FT.AGGREGATE arguments not suggested'); - await t.typeText(searchAndQueryPage.queryInput, 'g', { replace: false }); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.nth(0).textContent).contains('GROUPBY', 'Argument not suggested after typing first letters'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('APPLY').exists).ok('FT.AGGREGATE arguments not suggested'); + await t.typeText(workbenchPage.queryInput, 'g', { replace: false }); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.nth(0).textContent).contains('GROUPBY', 'Argument not suggested after typing first letters'); await t.pressKey('tab'); // Verify that user can see widget about entered argument - await t.expect(searchAndQueryPage.MonacoEditor.monacoHintWithArguments.textContent).contains(groupByArgInfo, 'Widget with info about entered argument not displayed'); + await t.expect(workbenchPage.MonacoEditor.monacoHintWithArguments.textContent).contains(groupByArgInfo, 'Widget with info about entered argument not displayed'); - await t.typeText(searchAndQueryPage.queryInput, '1 "London"', { replace: false }); + await t.typeText(workbenchPage.queryInput, '1 "London"', { replace: false }); await t.pressKey('space'); // Verify correct order of suggested arguments like LOAD, GROUPBY, SORTBY - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.nth(0).textContent).contains('REDUCE', 'Incorrect order of suggested arguments'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.nth(0).textContent).contains('REDUCE', 'Incorrect order of suggested arguments'); await t.pressKey('tab'); - await t.typeText(searchAndQueryPage.queryInput, 'SUM 1 @students', { replace: false }); + await t.typeText(workbenchPage.queryInput, 'SUM 1 @students', { replace: false }); await t.pressKey('space'); // Verify expression and function suggestions like AS for APPLY/GROUPBY - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.nth(0).textContent).contains('AS', 'Incorrect order of suggested arguments'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.nth(0).textContent).contains('AS', 'Incorrect order of suggested arguments'); await t.pressKey('tab'); - await t.typeText(searchAndQueryPage.queryInput, 'stud', { replace: false }); + await t.typeText(workbenchPage.queryInput, 'stud', { replace: false }); await t.pressKey('space'); await t.debug(); // Verify multiple argument option suggestions - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.nth(0).textContent).contains('REDUCE', 'Incorrect order of suggested arguments'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.nth(0).textContent).contains('REDUCE', 'Incorrect order of suggested arguments'); // Verify complex command sequences like nargs and properties are suggested accurately for GROUPBY const expectedText = `FT.AGGREGATE "${indexName1}" "@city" GROUPBY 1 "London" REDUCE SUM 1 @students AS stud REDUCE`.trim().replace(/\s+/g, ' '); - await t.expect((await searchAndQueryPage.queryInputForText.innerText).trim().replace(/\s+/g, ' ')).contains(expectedText, 'Incorrect order of entered arguments'); + await t.expect((await workbenchPage.queryInputForText.innerText).trim().replace(/\s+/g, ' ')).contains(expectedText, 'Incorrect order of entered arguments'); }); test('Verify full commands suggestions with index and query for FT.SEARCH', async t => { - await t.typeText(searchAndQueryPage.queryInput, 'FT.SE', { replace: true }); + await t.typeText(workbenchPage.queryInput, 'FT.SE', { replace: true }); // Select command and check result await t.pressKey('enter'); - const script = await searchAndQueryPage.queryInputScriptArea.textContent; + const script = await workbenchPage.queryInputScriptArea.textContent; await t.expect(script.replace(/\s/g, ' ')).contains('FT.SEARCH ', 'Result of sent command exists'); await t.pressKey('tab'); // Select '@city' field - await searchAndQueryPage.selectFieldUsingAutosuggest('city'); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('DIALECT').exists).ok('FT.SEARCH arguments not suggested'); - await t.typeText(searchAndQueryPage.queryInput, 'n', { replace: false }); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.nth(0).textContent).contains('NOCONTENT', 'Argument not suggested after typing first letters'); + await workbenchPage.selectFieldUsingAutosuggest('city'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('DIALECT').exists).ok('FT.SEARCH arguments not suggested'); + await t.typeText(workbenchPage.queryInput, 'n', { replace: false }); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.nth(0).textContent).contains('NOCONTENT', 'Argument not suggested after typing first letters'); await t.pressKey('tab'); // Verify that FT.SEARCH and FT.AGGREGATE non-multiple arguments are suggested only once await t.pressKey('space'); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withText('NOCONTENT').exists).notOk('Non-multiple arguments are suggested not only once'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withText('NOCONTENT').exists).notOk('Non-multiple arguments are suggested not only once'); // Verify that suggestions correct to closest valid commands or options for invalid typing like WRONGCOMMAND - await t.typeText(searchAndQueryPage.queryInput, 'WRONGCOMMAND', { replace: false }); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('WITHSORTKEYS').exists).ok('Closest suggestions not displayed'); + await t.typeText(workbenchPage.queryInput, 'WRONGCOMMAND', { replace: false }); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('WITHSORTKEYS').exists).ok('Closest suggestions not displayed'); await t.pressKey('space'); await t.pressKey('backspace'); await t.pressKey('backspace'); // Verify that 'No suggestions' tooltip is displayed when returning to invalid typing like WRONGCOMMAND - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestWidget.textContent).contains('No suggestions.', 'Index not auto-suggested'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestWidget.textContent).contains('No suggestions.', 'Index not auto-suggested'); }); test('Verify full commands suggestions with index and query for FT.PROFILE(SEARCH)', async t => { - await t.typeText(searchAndQueryPage.queryInput, 'FT.PR', { replace: true }); + await t.typeText(workbenchPage.queryInput, 'FT.PR', { replace: true }); // Select command and check result await t.pressKey('enter'); - const script = await searchAndQueryPage.queryInputScriptArea.textContent; + const script = await workbenchPage.queryInputScriptArea.textContent; await t.expect(script.replace(/\s/g, ' ')).contains('FT.PROFILE ', 'Result of sent command exists'); await t.pressKey('tab'); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('AGGREGATE').exists).ok('FT.PROFILE aggregate argument not suggested'); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('SEARCH').exists).ok('FT.PROFILE search argument not suggested'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('AGGREGATE').exists).ok('FT.PROFILE aggregate argument not suggested'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('SEARCH').exists).ok('FT.PROFILE search argument not suggested'); // Select SEARCH command - await t.typeText(searchAndQueryPage.queryInput, 'SEA', { replace: false }); + await t.typeText(workbenchPage.queryInput, 'SEA', { replace: false }); await t.pressKey('enter'); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('LIMITED').exists).ok('FT.PROFILE SEARCH arguments not suggested'); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('QUERY').exists).ok('FT.PROFILE SEARCH arguments not suggested'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('LIMITED').exists).ok('FT.PROFILE SEARCH arguments not suggested'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('QUERY').exists).ok('FT.PROFILE SEARCH arguments not suggested'); // Select QUERY - await t.typeText(searchAndQueryPage.queryInput, 'QUE', { replace: false }); + await t.typeText(workbenchPage.queryInput, 'QUE', { replace: false }); await t.pressKey('enter'); - await searchAndQueryPage.selectFieldUsingAutosuggest('city'); + await workbenchPage.selectFieldUsingAutosuggest('city'); // Verify that there are no more suggestions - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.exists).notOk('Additional invalid commands suggested'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.exists).notOk('Additional invalid commands suggested'); const expectedText = `FT.PROFILE "${indexName1}" SEARCH QUERY "@city"`.trim().replace(/\s+/g, ' '); // Verify command entered correctly - await t.expect((await searchAndQueryPage.queryInputForText.innerText).trim().replace(/\s+/g, ' ')).contains(expectedText, 'Incorrect order of entered arguments'); + await t.expect((await workbenchPage.queryInputForText.innerText).trim().replace(/\s+/g, ' ')).contains(expectedText, 'Incorrect order of entered arguments'); }); test('Verify full commands suggestions with index and query for FT.PROFILE(AGGREGATE)', async t => { - await t.typeText(searchAndQueryPage.queryInput, 'FT.PR', { replace: true }); + await t.typeText(workbenchPage.queryInput, 'FT.PR', { replace: true }); // Select command and check result await t.pressKey('enter'); await t.pressKey('tab'); // Select AGGREGATE command - await t.typeText(searchAndQueryPage.queryInput, 'AGG', { replace: false }); + await t.typeText(workbenchPage.queryInput, 'AGG', { replace: false }); await t.pressKey('enter'); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('LIMITED').exists).ok('FT.PROFILE AGGREGATE arguments not suggested'); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('QUERY').exists).ok('FT.PROFILE AGGREGATE arguments not suggested'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('LIMITED').exists).ok('FT.PROFILE AGGREGATE arguments not suggested'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('QUERY').exists).ok('FT.PROFILE AGGREGATE arguments not suggested'); // Select QUERY - await t.typeText(searchAndQueryPage.queryInput, 'QUE', { replace: false }); + await t.typeText(workbenchPage.queryInput, 'QUE', { replace: false }); await t.pressKey('enter'); - await searchAndQueryPage.selectFieldUsingAutosuggest('city'); + await workbenchPage.selectFieldUsingAutosuggest('city'); // Verify that there are no more suggestions - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.exists).notOk('Additional invalid commands suggested'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.exists).notOk('Additional invalid commands suggested'); const expectedText = `FT.PROFILE "${indexName1}" AGGREGATE QUERY "@city"`.trim().replace(/\s+/g, ' '); // Verify command entered correctly - await t.expect((await searchAndQueryPage.queryInputForText.innerText).trim().replace(/\s+/g, ' ')).contains(expectedText, 'Incorrect order of entered arguments'); + await t.expect((await workbenchPage.queryInputForText.innerText).trim().replace(/\s+/g, ' ')).contains(expectedText, 'Incorrect order of entered arguments'); }); test('Verify full commands suggestions with index and query for FT.EXPLAIN', async t => { - await t.typeText(searchAndQueryPage.queryInput, 'FT.EX', { replace: true }); + await t.typeText(workbenchPage.queryInput, 'FT.EX', { replace: true }); // Select command and check result await t.pressKey('enter'); await t.pressKey('tab'); - await searchAndQueryPage.selectFieldUsingAutosuggest('city'); + await workbenchPage.selectFieldUsingAutosuggest('city'); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('DIALECT').exists).ok('FT.EXPLAIN arguments not suggested'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('DIALECT').exists).ok('FT.EXPLAIN arguments not suggested'); // Add DIALECT await t.pressKey('enter'); - await t.typeText(searchAndQueryPage.queryInput, 'dialectTest', { replace: false }); + await t.typeText(workbenchPage.queryInput, 'dialectTest', { replace: false }); // Verify that there are no more suggestions await t.pressKey('space'); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.exists).notOk('Additional invalid commands suggested'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.exists).notOk('Additional invalid commands suggested'); const expectedText = `FT.EXPLAIN "${indexName1}" "@city" DIALECT dialectTest`.trim().replace(/\s+/g, ' '); // Verify command entered correctly - await t.expect((await searchAndQueryPage.queryInputForText.innerText).trim().replace(/\s+/g, ' ')).contains(expectedText, 'Incorrect order of entered arguments'); + await t.expect((await workbenchPage.queryInputForText.innerText).trim().replace(/\s+/g, ' ')).contains(expectedText, 'Incorrect order of entered arguments'); }); test('Verify commands suggestions for APPLY and FILTER', async t => { - await t.typeText(searchAndQueryPage.queryInput, 'FT.AGGREGATE ', { replace: true }); + await t.typeText(workbenchPage.queryInput, 'FT.AGGREGATE ', { replace: true }); await t.pressKey('enter'); - await t.typeText(searchAndQueryPage.queryInput, '*'); + await t.typeText(workbenchPage.queryInput, '*'); await t.pressKey('right'); await t.pressKey('space'); // Verify APPLY command - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('APPLY').exists).ok('Apply is not suggested'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('APPLY').exists).ok('Apply is not suggested'); await t.pressKey('enter'); - await t.typeText(searchAndQueryPage.queryInput, 'g'); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.exists).ok('commands is not suggested'); + await t.typeText(workbenchPage.queryInput, 'g'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.exists).ok('commands is not suggested'); await t.pressKey('enter'); - await t.typeText(searchAndQueryPage.queryInput, '@', { replace: false }); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed'); - await t.typeText(searchAndQueryPage.queryInput, 'location', { replace: false }); - await t.typeText(searchAndQueryPage.queryInput, ', \'40.7128,-74.0060\''); + await t.typeText(workbenchPage.queryInput, '@', { replace: false }); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed'); + await t.typeText(workbenchPage.queryInput, 'location', { replace: false }); + await t.typeText(workbenchPage.queryInput, ', \'40.7128,-74.0060\''); for (let i = 0; i < 3; i++) { await t.pressKey('right'); } await t.pressKey('space'); await t.pressKey('tab'); - await t.typeText(searchAndQueryPage.queryInput, 'apply_key', { replace: false }); + await t.typeText(workbenchPage.queryInput, 'apply_key', { replace: false }); await t.pressKey('space'); // Verify Filter command - await t.typeText(searchAndQueryPage.queryInput, 'F'); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('FILTER').exists).ok('FILTER is not suggested'); + await t.typeText(workbenchPage.queryInput, 'F'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('FILTER').exists).ok('FILTER is not suggested'); await t.pressKey('enter'); - await t.typeText(searchAndQueryPage.queryInput, 'apply_key < 5000', { replace: false }); + await t.typeText(workbenchPage.queryInput, 'apply_key < 5000', { replace: false }); await t.pressKey('right'); await t.pressKey('space'); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('GROUPBY').exists).ok('query can not be prolong'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('GROUPBY').exists).ok('query can not be prolong'); }); test('Verify REDUCE commands', async t => { - await t.typeText(searchAndQueryPage.queryInput, `FT.AGGREGATE ${indexName1} "*" GROUPBY 1 @location`, { replace: true }); + await t.typeText(workbenchPage.queryInput, `FT.AGGREGATE ${indexName1} "*" GROUPBY 1 @location`, { replace: true }); await t.pressKey('space'); // select Reduce - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('REDUCE').exists).ok('REDUCE is not suggested'); - await t.typeText(searchAndQueryPage.queryInput, 'R'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('REDUCE').exists).ok('REDUCE is not suggested'); + await t.typeText(workbenchPage.queryInput, 'R'); await t.pressKey('enter'); // set value of reduce - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed'); - await t.typeText(searchAndQueryPage.queryInput, 'CO'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed'); + await t.typeText(workbenchPage.queryInput, 'CO'); await t.pressKey('enter'); - await t.typeText(searchAndQueryPage.queryInput, '0'); + await t.typeText(workbenchPage.queryInput, '0'); // verify that count of nargs is correct await t.pressKey('space'); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('AS').exists).ok('AS is not suggested'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('AS').exists).ok('AS is not suggested'); await t.pressKey('enter'); - await t.typeText(searchAndQueryPage.queryInput, 'item_count '); + await t.typeText(workbenchPage.queryInput, 'item_count '); // add additional reduce - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('REDUCE').exists).ok('Apply is not suggested'); - await t.typeText(searchAndQueryPage.queryInput, 'R'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('REDUCE').exists).ok('Apply is not suggested'); + await t.typeText(workbenchPage.queryInput, 'R'); await t.pressKey('enter'); - await t.typeText(searchAndQueryPage.queryInput, 'SUM'); + await t.typeText(workbenchPage.queryInput, 'SUM'); await t.pressKey('enter'); - await t.typeText(searchAndQueryPage.queryInput, '1 '); + await t.typeText(workbenchPage.queryInput, '1 '); - await t.typeText(searchAndQueryPage.queryInput, '@', { replace: false }); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed'); - await t.typeText(searchAndQueryPage.queryInput, 'students ', { replace: false }); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.withExactText('AS').exists).ok('AS is not suggested'); + await t.typeText(workbenchPage.queryInput, '@', { replace: false }); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed'); + await t.typeText(workbenchPage.queryInput, 'students ', { replace: false }); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('AS').exists).ok('AS is not suggested'); await t.pressKey('enter'); - await t.typeText(searchAndQueryPage.queryInput, 'total_students'); + await t.typeText(workbenchPage.queryInput, 'total_students'); }); test('Verify suggestions for fields', async t => { - await t.typeText(searchAndQueryPage.queryInput, 'FT.AGGREGATE ', { replace: true }); - await t.typeText(searchAndQueryPage.queryInput, 'idx1'); + await t.typeText(workbenchPage.queryInput, 'FT.AGGREGATE ', { replace: true }); + await t.typeText(workbenchPage.queryInput, 'idx1'); await t.pressKey('enter'); await t.wait(200); - await t.typeText(searchAndQueryPage.queryInput, '@'); - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed'); + await t.typeText(workbenchPage.queryInput, '@'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed'); // verify suggestions for geo - await t.typeText(searchAndQueryPage.queryInput, 'l'); + await t.typeText(workbenchPage.queryInput, 'l'); await t.pressKey('tab'); - await t.expect((await searchAndQueryPage.MonacoEditor.getTextFromMonaco()).trim()).eql(`FT.AGGREGATE "${indexName1}" "@location:[lon lat radius unit]"`); + await t.expect((await workbenchPage.MonacoEditor.getTextFromMonaco()).trim()).eql(`FT.AGGREGATE "${indexName1}" "@location:[lon lat radius unit]"`); // verify for numeric - await t.typeText(searchAndQueryPage.queryInput, 'FT.AGGREGATE ', { replace: true }); - await t.typeText(searchAndQueryPage.queryInput, 'idx1'); + await t.typeText(workbenchPage.queryInput, 'FT.AGGREGATE ', { replace: true }); + await t.typeText(workbenchPage.queryInput, 'idx1'); await t.pressKey('enter'); await t.wait(200); - await t.typeText(searchAndQueryPage.queryInput, '@'); - await t.typeText(searchAndQueryPage.queryInput, 's'); + await t.typeText(workbenchPage.queryInput, '@'); + await t.typeText(workbenchPage.queryInput, 's'); await t.pressKey('tab'); - await t.expect((await searchAndQueryPage.MonacoEditor.getTextFromMonaco()).trim()).eql(`FT.AGGREGATE "${indexName1}" "@students:[range]"`); + await t.expect((await workbenchPage.MonacoEditor.getTextFromMonaco()).trim()).eql(`FT.AGGREGATE "${indexName1}" "@students:[range]"`); }); test @@ -348,33 +347,33 @@ test await browserPage.Cli.sendCommandsInCli([`DEL ${keyNames.join(' ')}`, `FT.DROPINDEX ${indexName3}`]); await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify commands suggestions for CREATE', async t => { - await t.typeText(searchAndQueryPage.queryInput, 'FT.CREATE ', { replace: true }); + await t.typeText(workbenchPage.queryInput, 'FT.CREATE ', { replace: true }); await t.pressKey('enter'); // Verify that indexes are not suggested for FT.CREATE - await t.expect(searchAndQueryPage.MonacoEditor.monacoSuggestion.exists).notOk('Existing index suggested'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.exists).notOk('Existing index suggested'); // Enter index name - await t.typeText(searchAndQueryPage.queryInput, indexName3); + await t.typeText(workbenchPage.queryInput, indexName3); await t.pressKey('space'); // Select FILTER keyword - await t.typeText(searchAndQueryPage.queryInput, 'FI'); + await t.typeText(workbenchPage.queryInput, 'FI'); await t.pressKey('tab'); - await t.typeText(searchAndQueryPage.queryInput, 'filterNew', { replace: false }); + await t.typeText(workbenchPage.queryInput, 'filterNew', { replace: false }); await t.pressKey('space'); // Select SCHEMA keyword - await t.typeText(searchAndQueryPage.queryInput, 'SCH'); + await t.typeText(workbenchPage.queryInput, 'SCH'); await t.pressKey('tab'); - await t.typeText(searchAndQueryPage.queryInput, 'field_name', { replace: false }); + await t.typeText(workbenchPage.queryInput, 'field_name', { replace: false }); await t.pressKey('space'); // Select TEXT keyword - await t.typeText(searchAndQueryPage.queryInput, 'te', { replace: false }); + await t.typeText(workbenchPage.queryInput, 'te', { replace: false }); await t.pressKey('tab'); // Select SORTABLE - await t.typeText(searchAndQueryPage.queryInput, 'so', { replace: false }); + await t.typeText(workbenchPage.queryInput, 'so', { replace: false }); await t.pressKey('tab'); - await t.expect((await searchAndQueryPage.MonacoEditor.getTextFromMonaco()).trim()).eql(`FT.CREATE ${indexName3} FILTER filterNew SCHEMA field_name TEXT SORTABLE`); + await t.expect((await workbenchPage.MonacoEditor.getTextFromMonaco()).trim()).eql(`FT.CREATE ${indexName3} FILTER filterNew SCHEMA field_name TEXT SORTABLE`); }); diff --git a/tests/e2e/tests/web/regression/workbench/autocomplete.e2e.ts b/tests/e2e/tests/web/regression/workbench/autocomplete.e2e.ts index db496c2bc3..6e52835a10 100644 --- a/tests/e2e/tests/web/regression/workbench/autocomplete.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/autocomplete.e2e.ts @@ -1,4 +1,4 @@ -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; @@ -15,7 +15,7 @@ fixture `Autocomplete for entered commands` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Delete database diff --git a/tests/e2e/tests/web/regression/workbench/autoexecute-button.e2e.ts b/tests/e2e/tests/web/regression/workbench/autoexecute-button.e2e.ts index bff30ffe92..938ffae5c5 100644 --- a/tests/e2e/tests/web/regression/workbench/autoexecute-button.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/autoexecute-button.e2e.ts @@ -1,6 +1,6 @@ import { DatabaseHelper } from '../../../../helpers/database'; import { WorkbenchPage, BrowserPage } from '../../../../pageObjects'; -import { ExploreTabs, KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { ExploreTabs, rte } from '../../../../helpers/constants'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; @@ -14,7 +14,7 @@ fixture `Workbench Auto-Execute button` .page(commonUrl) .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Clear and delete database diff --git a/tests/e2e/tests/web/regression/workbench/command-results.e2e.ts b/tests/e2e/tests/web/regression/workbench/command-results.e2e.ts index 88b58e051e..4d75faf820 100644 --- a/tests/e2e/tests/web/regression/workbench/command-results.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/command-results.e2e.ts @@ -1,7 +1,7 @@ import { DatabaseHelper } from '../../../../helpers/database'; import { WorkbenchPage, BrowserPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneRedisearch } from '../../../../helpers/conf'; -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; import { WorkbenchActions } from '../../../../common-actions/workbench-actions'; @@ -25,7 +25,7 @@ fixture `Command results at Workbench` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); // Add index and data - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); await workbenchPage.sendCommandsArrayInWorkbench(commandsForIndex); }) .afterEach(async t => { @@ -114,7 +114,7 @@ test('Big output in workbench is visible in virtualized table', async t => { test .before(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); }) .after(async t => { await t.switchToMainWindow(); diff --git a/tests/e2e/tests/web/regression/workbench/context.e2e.ts b/tests/e2e/tests/web/regression/workbench/context.e2e.ts index 16a0160f95..914efa16e4 100644 --- a/tests/e2e/tests/web/regression/workbench/context.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/context.e2e.ts @@ -1,4 +1,4 @@ -import { ExploreTabs, KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { ExploreTabs, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; @@ -18,7 +18,7 @@ fixture `Workbench Context` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Delete database @@ -27,9 +27,7 @@ fixture `Workbench Context` test('Verify that user can see saved CLI state when navigates away to any other page', async t => { // Expand CLI and navigate to Browser await t.click(workbenchPage.Cli.cliExpandButton); - await t.click(myRedisDatabasePage.NavigationPanel.browserButton); - // Return back to Workbench and check CLI - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); await t.expect(workbenchPage.Cli.cliCollapseButton.exists).ok('CLI is not expanded'); }); // Update after resolving https://redislabs.atlassian.net/browse/RI-3299 @@ -53,9 +51,7 @@ test('Verify that user can see all the information removed when reloads the page // Create context modificaions and navigate to Browser await t.typeText(workbenchPage.queryInput, command, { replace: true, speed: speed }); await t.click(workbenchPage.Cli.cliExpandButton); - await t.click(myRedisDatabasePage.NavigationPanel.browserButton); - // Open Workbench page and verify context - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); await t.expect(workbenchPage.Cli.cliCollapseButton.exists).ok('CLI is not expanded'); await t.expect(workbenchPage.queryInputScriptArea.textContent).eql(command, 'Input in Editor is not saved'); // Reload the window and chek context diff --git a/tests/e2e/tests/web/regression/workbench/cypher.e2e.ts b/tests/e2e/tests/web/regression/workbench/cypher.e2e.ts index 13575f7249..2a383a2217 100644 --- a/tests/e2e/tests/web/regression/workbench/cypher.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/cypher.e2e.ts @@ -1,4 +1,4 @@ -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; @@ -17,7 +17,7 @@ fixture `Cypher syntax at Workbench` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Drop database diff --git a/tests/e2e/tests/web/regression/workbench/default-scripts-area.e2e.ts b/tests/e2e/tests/web/regression/workbench/default-scripts-area.e2e.ts index a0b3e7ff84..7055117953 100644 --- a/tests/e2e/tests/web/regression/workbench/default-scripts-area.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/default-scripts-area.e2e.ts @@ -1,4 +1,4 @@ -import { ExploreTabs, KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { ExploreTabs, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; @@ -16,7 +16,7 @@ fixture `Default scripts area at Workbench` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Delete database @@ -70,7 +70,7 @@ test('Verify that user can see saved article in Enablement area when he leaves W // Go to Browser page await t.click(myRedisDatabasePage.NavigationPanel.browserButton); // Go back to Workbench page - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); // Verify that the same article is opened in Enablement area selector = tutorials.getRunSelector('Create a hash'); await t.expect(selector.visible).ok('The end of the page is not visible'); diff --git a/tests/e2e/tests/web/regression/workbench/editor-cleanup.e2e.ts b/tests/e2e/tests/web/regression/workbench/editor-cleanup.e2e.ts index 244a827ed5..00a0e7908c 100644 --- a/tests/e2e/tests/web/regression/workbench/editor-cleanup.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/editor-cleanup.e2e.ts @@ -1,6 +1,6 @@ import { DatabaseHelper } from '../../../../helpers/database'; import { WorkbenchPage, MyRedisDatabasePage, SettingsPage, BrowserPage } from '../../../../pageObjects'; -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; @@ -36,8 +36,7 @@ test('Disabled Editor Cleanup toggle behavior', async t => { // Verify that user can see text "Clear the Editor after running commands" for Editor Cleanup In Settings await t.expect(settingsPage.switchEditorCleanupOption.sibling(0).withExactText('Clear the Editor after running commands').visible).ok('Cleanup text is not correct'); // Go to Workbench page - await t.click(settingsPage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); // Send commands await workbenchPage.sendCommandInWorkbench(commandToSend); await workbenchPage.sendCommandInWorkbench(commandToSend); @@ -46,7 +45,7 @@ test('Disabled Editor Cleanup toggle behavior', async t => { }); test('Enabled Editor Cleanup toggle behavior', async t => { // Go to Workbench page - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); // Send commands await workbenchPage.sendCommandInWorkbench(commandToSend); await workbenchPage.sendCommandInWorkbench(commandToSend); diff --git a/tests/e2e/tests/web/regression/workbench/empty-command-history.e2e.ts b/tests/e2e/tests/web/regression/workbench/empty-command-history.e2e.ts index 5071c5ae1d..1dd753d9f3 100644 --- a/tests/e2e/tests/web/regression/workbench/empty-command-history.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/empty-command-history.e2e.ts @@ -1,5 +1,5 @@ import { Selector } from 'testcafe'; -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; @@ -16,7 +16,7 @@ fixture `Empty command history in Workbench` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Delete database diff --git a/tests/e2e/tests/web/regression/workbench/group-mode.e2e.ts b/tests/e2e/tests/web/regression/workbench/group-mode.e2e.ts index 64c0cd0bb7..80f2e24fc5 100644 --- a/tests/e2e/tests/web/regression/workbench/group-mode.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/group-mode.e2e.ts @@ -1,5 +1,5 @@ import { Selector } from 'testcafe'; -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneBigConfig } from '../../../../helpers/conf'; @@ -23,7 +23,7 @@ fixture `Workbench Group Mode` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig); // Go to Workbench page - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Delete database diff --git a/tests/e2e/tests/web/regression/workbench/history-of-results.e2e.ts b/tests/e2e/tests/web/regression/workbench/history-of-results.e2e.ts index fe0a0d024a..21303ac99e 100644 --- a/tests/e2e/tests/web/regression/workbench/history-of-results.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/history-of-results.e2e.ts @@ -1,7 +1,7 @@ import { getRandomParagraph } from '../../../../helpers/keys'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, WorkbenchPage } from '../../../../pageObjects'; -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; @@ -21,7 +21,7 @@ fixture `History of results at Workbench` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Clear and delete database diff --git a/tests/e2e/tests/web/regression/workbench/raw-mode.e2e.ts b/tests/e2e/tests/web/regression/workbench/raw-mode.e2e.ts index 94b08c001e..efdec7bc15 100644 --- a/tests/e2e/tests/web/regression/workbench/raw-mode.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/raw-mode.e2e.ts @@ -1,6 +1,6 @@ import { DatabaseHelper } from '../../../../helpers/database'; import { WorkbenchPage, MyRedisDatabasePage, BrowserPage } from '../../../../pageObjects'; -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { commonUrl, ossStandaloneConfig, ossStandaloneRedisearch } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; @@ -32,7 +32,7 @@ fixture `Workbench Raw mode` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); }) .afterEach(async t => { // Clear and delete database @@ -65,7 +65,7 @@ test await myRedisDatabasePage.reloadPage(); await myRedisDatabasePage.clickOnDBByName(databasesForAdding[0].databaseName); // Go to Workbench page - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); }) .after(async() => { // Clear and delete database @@ -82,7 +82,7 @@ test await t.click(myRedisDatabasePage.NavigationPanel.myRedisDBButton); await myRedisDatabasePage.clickOnDBByName(databasesForAdding[1].databaseName); // Go to Workbench page - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); // Verify that user can see saved Raw mode state after re-connection to another DB await workbenchPage.sendCommandInWorkbench(commandsForSend[1]); await workbenchPage.checkWorkbenchCommandResult(commandsForSend[1], `"${unicodeValue}"`); @@ -94,7 +94,7 @@ test .before(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); // Go to Workbench page - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); }) .after(async t => { // Drop index, documents and database diff --git a/tests/e2e/tests/web/regression/workbench/redis-stack-commands.e2e.ts b/tests/e2e/tests/web/regression/workbench/redis-stack-commands.e2e.ts index 7921974f72..2e40dc6d5c 100644 --- a/tests/e2e/tests/web/regression/workbench/redis-stack-commands.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/redis-stack-commands.e2e.ts @@ -2,7 +2,7 @@ import { t } from 'testcafe'; import { DatabaseHelper } from '../../../../helpers/database'; import { WorkbenchPage, BrowserPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; -import { ExploreTabs, KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { ExploreTabs, rte } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; const browserPage = new BrowserPage(); @@ -17,7 +17,7 @@ fixture `Redis Stack command in Workbench` .page(commonUrl) .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Drop key and database diff --git a/tests/e2e/tests/web/regression/workbench/redisearch-module-not-available.e2e.ts b/tests/e2e/tests/web/regression/workbench/redisearch-module-not-available.e2e.ts index 19db310ce0..6aff416aa2 100644 --- a/tests/e2e/tests/web/regression/workbench/redisearch-module-not-available.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/redisearch-module-not-available.e2e.ts @@ -1,5 +1,5 @@ import { ClientFunction } from 'testcafe'; -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneV5Config } from '../../../../helpers/conf'; @@ -18,7 +18,7 @@ fixture `Redisearch module not available` .page(commonUrl) .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneV5Config); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Delete database diff --git a/tests/e2e/tests/web/regression/workbench/scripting-area.e2e.ts b/tests/e2e/tests/web/regression/workbench/scripting-area.e2e.ts index 20e5783eaa..c15dbec54d 100644 --- a/tests/e2e/tests/web/regression/workbench/scripting-area.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/scripting-area.e2e.ts @@ -1,4 +1,4 @@ -import { ExploreTabs, KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { ExploreTabs, rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { MyRedisDatabasePage, WorkbenchPage, SettingsPage, BrowserPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; @@ -21,7 +21,7 @@ fixture `Scripting area at Workbench` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Clear and delete database @@ -42,8 +42,7 @@ test('Verify that user can run multiple commands written in multiple lines in Wo await t.click(settingsPage.accordionWorkbenchSettings); await settingsPage.changeCommandsInPipeline('1'); // Go to Workbench page - await t.click(settingsPage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); // Send commands in multiple lines await workbenchPage.sendCommandInWorkbench(commandsForSend.join('\n'), 0.5); // Check the result @@ -70,8 +69,7 @@ test await t.click(settingsPage.accordionWorkbenchSettings); await settingsPage.changeCommandsInPipeline('1'); // Go to Workbench page - await t.click(settingsPage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(settingsPage.NavigationPanel.workbenchButton); // Send commands in multiple lines with double slashes (//) wrapped in double quotes await workbenchPage.sendCommandInWorkbench(commandsForSend.join('\n"//"'), 0.5); // Check that all commands are executed diff --git a/tests/e2e/tests/web/regression/workbench/workbench-all-db-types.e2e.ts b/tests/e2e/tests/web/regression/workbench/workbench-all-db-types.e2e.ts index 59de3d7644..b199d73987 100644 --- a/tests/e2e/tests/web/regression/workbench/workbench-all-db-types.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/workbench-all-db-types.e2e.ts @@ -1,5 +1,5 @@ import { t } from 'testcafe'; -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage, WorkbenchPage } from '../../../../pageObjects'; import { cloudDatabaseConfig, commonUrl, ossClusterConfig, ossSentinelConfig } from '../../../../helpers/conf'; @@ -21,7 +21,7 @@ const verifyCommandsInWorkbench = async(): Promise => { ]; await t.click(myRedisDatabasePage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); // Send commands await workbenchPage.sendCommandInWorkbench(commandForSend1); await workbenchPage.sendCommandInWorkbench(commandForSend2); diff --git a/tests/e2e/tests/web/regression/workbench/workbench-non-auto-guides.e2e.ts b/tests/e2e/tests/web/regression/workbench/workbench-non-auto-guides.e2e.ts index b920f2b265..e5d8b550e6 100644 --- a/tests/e2e/tests/web/regression/workbench/workbench-non-auto-guides.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/workbench-non-auto-guides.e2e.ts @@ -1,6 +1,6 @@ import { DatabaseHelper } from '../../../../helpers/database'; import { WorkbenchPage, MyRedisDatabasePage, BrowserPage } from '../../../../pageObjects'; -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; import { Common } from '../../../../helpers/common'; @@ -38,7 +38,7 @@ fixture `Workbench modes to non-auto guides` .page(commonUrl) .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Delete database @@ -47,7 +47,7 @@ fixture `Workbench modes to non-auto guides` test .before(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); await workbenchPage.sendCommandInWorkbench(`set ${keyName} "${keyValue}"`); }) .after(async t => { diff --git a/tests/e2e/tests/web/regression/workbench/workbench-pipeline.e2e.ts b/tests/e2e/tests/web/regression/workbench/workbench-pipeline.e2e.ts index fc97f27f8a..de0906c84d 100644 --- a/tests/e2e/tests/web/regression/workbench/workbench-pipeline.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/workbench-pipeline.e2e.ts @@ -1,5 +1,4 @@ -// import { ClientFunction } from 'testcafe'; -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage, SettingsPage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneBigConfig } from '../../../../helpers/conf'; @@ -51,7 +50,7 @@ test.skip('Verify that only chosen in pipeline number of commands is loading at await settingsPage.changeCommandsInPipeline(pipelineValues[1]); // Go to Workbench page await t.click(settingsPage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); await workbenchPage.sendCommandInWorkbench(commandForSend, 0.01); // Verify that only selected pipeline number of commands are loaded at the same time await t.expect(workbenchPage.loadedCommand.count).eql(Number(pipelineValues[1]), 'The number of sending commands is incorrect'); @@ -60,7 +59,7 @@ test.skip('Verify that user can see spinner over Run button and grey preloader f await settingsPage.changeCommandsInPipeline(pipelineValues[3]); // Go to Workbench page await t.click(settingsPage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); await workbenchPage.sendCommandInWorkbench(commandForSend, 0.01); // Verify that user can`t start new commands from the Workbench while command(s) is executing await t.expect(workbenchPage.submitCommandButton.withAttribute('disabled').exists).ok('Run button is not disabled', { timeout: 5000 }); @@ -74,7 +73,7 @@ test('Verify that user can interact with the Editor while command(s) in progress await settingsPage.changeCommandsInPipeline(pipelineValues[2]); // Go to Workbench page await t.click(settingsPage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); await workbenchPage.sendCommandInWorkbench(commandForSend); await t.typeText(workbenchPage.queryInput, commandForSend, { replace: true, paste: true }); await t.pressKey('enter'); @@ -93,8 +92,7 @@ test('Verify that command results are added to history in order most recent - on await settingsPage.changeCommandsInPipeline(pipelineValues[2]); // Go to Workbench page - await t.click(settingsPage.NavigationPanel.browserButton); - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); await workbenchPage.sendCommandInWorkbench(multipleCommands.join('\n')); // Check that the results for all commands are displayed in workbench history in reverse order (most recent - on top) for (let i = 0; i < multipleCommands.length; i++) { diff --git a/tests/e2e/tests/web/regression/workbench/workbench-tab.e2e.ts b/tests/e2e/tests/web/regression/workbench/workbench-tab.e2e.ts deleted file mode 100644 index 1091652ead..0000000000 --- a/tests/e2e/tests/web/regression/workbench/workbench-tab.e2e.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ExploreTabs, KeysInteractionTabs, rte } from '../../../../helpers/constants'; -import { DatabaseHelper } from '../../../../helpers/database'; -import { BrowserPage } from '../../../../pageObjects'; -import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; -import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; - -const databaseHelper = new DatabaseHelper(); -const databaseAPIRequests = new DatabaseAPIRequests(); -const browserPage = new BrowserPage(); - -fixture `Autocomplete for entered commands` - .meta({ type: 'regression', rte: rte.standalone }) - .page(commonUrl) - .beforeEach(async t => { - await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); - }) - .afterEach(async() => { - // Delete database - await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); - }); -test('Verify that tutorials can be opened from Workbench', async t => { - const workbench = await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); - await t.click(workbench.getTutorialLinkLocator('redis_use_cases_basic')); - await t.expect(workbench.InsightsPanel.sidePanel.exists).ok('Insight panel is not opened'); - const tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials); - await t.expect(tab.preselectArea.textContent).contains('BASIC REDIS USE CASES', 'the tutorial page is incorrect'); -}); diff --git a/tests/e2e/tests/web/smoke/workbench/json-workbench.e2e.ts b/tests/e2e/tests/web/smoke/workbench/json-workbench.e2e.ts index 735d3226a2..f1f39d04cc 100644 --- a/tests/e2e/tests/web/smoke/workbench/json-workbench.e2e.ts +++ b/tests/e2e/tests/web/smoke/workbench/json-workbench.e2e.ts @@ -1,4 +1,4 @@ -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneRedisearch } from '../../../../helpers/conf'; @@ -18,7 +18,7 @@ fixture `JSON verifications at Workbench` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneRedisearch); // Go to Workbench page - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); }) .afterEach(async t => { // Clear and delete database diff --git a/tests/e2e/tests/web/smoke/workbench/scripting-area.e2e.ts b/tests/e2e/tests/web/smoke/workbench/scripting-area.e2e.ts index 0a8eae3019..8220170b74 100644 --- a/tests/e2e/tests/web/smoke/workbench/scripting-area.e2e.ts +++ b/tests/e2e/tests/web/smoke/workbench/scripting-area.e2e.ts @@ -1,7 +1,7 @@ import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, WorkbenchPage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; -import { KeysInteractionTabs, rte } from '../../../../helpers/constants'; +import { rte } from '../../../../helpers/constants'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; const browserPage = new BrowserPage(); @@ -15,7 +15,7 @@ fixture `Scripting area at Workbench` .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); // Go to Workbench page - await browserPage.KeysInteractionPanel.setActiveTab(KeysInteractionTabs.Workbench); + await t.click(browserPage.NavigationPanel.workbenchButton); }) .afterEach(async() => { // Delete database From 7ae2b2e35f09885b85172161600a71dfb0628dfd Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Fri, 11 Oct 2024 13:58:03 +0200 Subject: [PATCH 087/112] fix part of tests --- tests/e2e/pageObjects/workbench-page.ts | 2 +- .../regression/search-and-query/no-indexes-suggestions.e2e.ts | 4 ++-- .../regression/search-and-query/search-and-query-tab.e2e.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/e2e/pageObjects/workbench-page.ts b/tests/e2e/pageObjects/workbench-page.ts index 633af2da2b..60ba3cea29 100644 --- a/tests/e2e/pageObjects/workbench-page.ts +++ b/tests/e2e/pageObjects/workbench-page.ts @@ -95,7 +95,7 @@ export class WorkbenchPage extends InstancePage { queryTextResult = Selector(this.cssQueryTextResult); getTutorialLinkLocator = (tutorialName: string): Selector => - Selector(`[data-testid=query-tutorials-link_${tutorialName}]`); + Selector(`[data-testid=query-tutorials-link_${tutorialName}]`, { timeout: 1000 } ); // Select view option in Workbench results diff --git a/tests/e2e/tests/web/regression/search-and-query/no-indexes-suggestions.e2e.ts b/tests/e2e/tests/web/regression/search-and-query/no-indexes-suggestions.e2e.ts index 3b6f4ec665..ac2a27038c 100644 --- a/tests/e2e/tests/web/regression/search-and-query/no-indexes-suggestions.e2e.ts +++ b/tests/e2e/tests/web/regression/search-and-query/no-indexes-suggestions.e2e.ts @@ -23,10 +23,10 @@ test })('Verify suggestions when there are no indexes', async t => { - // TODO add navigation to search and query Monaco + await t.click(browserPage.NavigationPanel.workbenchButton); await t.typeText(workbenchPage.queryInput, 'FT.SE', { replace: true }); - await t.pressKey('enter'); + await t.pressKey('tab'); await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('No indexes to display').exists).ok('info text is not displayed'); diff --git a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts index 0670de33ea..d7a8db68f3 100644 --- a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts +++ b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts @@ -61,7 +61,7 @@ test('Verify that user can use show more to see command fully in 2nd tooltip', a 'required query', 'optional [verbatim]' ]; - await t.typeText(workbenchPage.queryInput, 'FT', { replace: true }); + await t.typeText(workbenchPage.queryInput, 'FT.A', { replace: true }); // Verify that user can use show more to see command fully in 2nd tooltip await t.pressKey('ctrl+space'); await t.expect(workbenchPage.MonacoEditor.monacoCommandDetails.exists).ok('The "read more" about the command is not opened'); @@ -87,7 +87,7 @@ test('Verify full commands suggestions with index and query for FT.AGGREGATE', a // Verify basic commands suggestions FT.SEARCH and FT.AGGREGATE await t.typeText(workbenchPage.queryInput, 'FT', { replace: true }); // Verify that the list with FT.SEARCH and FT.AGGREGATE auto-suggestions is displayed - await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withText('FT.SEARCH').exists).ok('FT.SEARCH auto-suggestions are not displayed'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withText('FT._LIST').exists).ok('FT._LIST auto-suggestions are not displayed'); await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withText('FT.AGGREGATE').exists).ok('FT.AGGREGATE auto-suggestions are not displayed'); // Select command and check result From ae8bb0e2668c4067e0926605f46699537a186f84 Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Fri, 11 Oct 2024 15:02:25 +0200 Subject: [PATCH 088/112] temp fix --- .../regression/search-and-query/no-indexes-suggestions.e2e.ts | 1 + .../web/regression/search-and-query/search-and-query-tab.e2e.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/e2e/tests/web/regression/search-and-query/no-indexes-suggestions.e2e.ts b/tests/e2e/tests/web/regression/search-and-query/no-indexes-suggestions.e2e.ts index ac2a27038c..f8d39232be 100644 --- a/tests/e2e/tests/web/regression/search-and-query/no-indexes-suggestions.e2e.ts +++ b/tests/e2e/tests/web/regression/search-and-query/no-indexes-suggestions.e2e.ts @@ -24,6 +24,7 @@ test })('Verify suggestions when there are no indexes', async t => { await t.click(browserPage.NavigationPanel.workbenchButton); + await t.pressKey('ctrl+space'); await t.typeText(workbenchPage.queryInput, 'FT.SE', { replace: true }); await t.pressKey('tab'); diff --git a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts index d7a8db68f3..870252c6cf 100644 --- a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts +++ b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts @@ -37,6 +37,7 @@ fixture `Autocomplete for entered commands in search and query` // Create 3 keys and index await browserPage.Cli.sendCommandsInCli(commands); await t.click(browserPage.NavigationPanel.workbenchButton); + await t.pressKey('ctrl+space'); }) .afterEach(async() => { // Clear and delete database From 53652d5b6d89724c812f8f65603a639ef1a59641 Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Fri, 11 Oct 2024 15:07:17 +0200 Subject: [PATCH 089/112] recommendation fix --- .../tests/web/regression/insights/live-recommendations.e2e.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e/tests/web/regression/insights/live-recommendations.e2e.ts b/tests/e2e/tests/web/regression/insights/live-recommendations.e2e.ts index 782b61a556..a2bf9b3db3 100644 --- a/tests/e2e/tests/web/regression/insights/live-recommendations.e2e.ts +++ b/tests/e2e/tests/web/regression/insights/live-recommendations.e2e.ts @@ -263,6 +263,7 @@ test await databaseAPIRequests.deleteStandaloneDatabasesApi(databasesForAdding); })('Verify that key name is displayed for Insights and DA recommendations', async t => { const cliCommand = `JSON.SET ${keyName} $ '{ "model": "Hyperion", "brand": "Velorim"}'`; + await browserPage.Cli.sendCommandInCli('flushdb'); await browserPage.Cli.sendCommandInCli(cliCommand); await t.click(browserPage.refreshKeysButton); await browserPage.NavigationHeader.togglePanel(true); From 7ee2d73abe7412c905b6a31c3abea751658a591a Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Fri, 11 Oct 2024 15:08:59 +0200 Subject: [PATCH 090/112] fix for tutorials --- .../web/regression/search-and-query/search-and-query-tab.e2e.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts index 870252c6cf..2ea8711d2f 100644 --- a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts +++ b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts @@ -48,7 +48,7 @@ fixture `Autocomplete for entered commands in search and query` }); test('Verify that tutorials can be opened from Workbench', async t => { await t.click(browserPage.NavigationPanel.workbenchButton); - await t.click(workbenchPage.getTutorialLinkLocator('sq-exact-match')); + await t.click(workbenchPage.getTutorialLinkLocator('_sq-intro')); await t.expect(workbenchPage.InsightsPanel.sidePanel.exists).ok('Insight panel is not opened'); const tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials); await t.expect(tab.preselectArea.textContent).contains('EXACT MATCH', 'the tutorial page is incorrect'); From 1906e15358ebfc5453916d008b303c4983a220c9 Mon Sep 17 00:00:00 2001 From: mariasergeenko Date: Fri, 11 Oct 2024 15:10:10 +0200 Subject: [PATCH 091/112] fix for tutorials#2 --- .../web/regression/search-and-query/search-and-query-tab.e2e.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts index 2ea8711d2f..494b74aa5c 100644 --- a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts +++ b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts @@ -48,7 +48,7 @@ fixture `Autocomplete for entered commands in search and query` }); test('Verify that tutorials can be opened from Workbench', async t => { await t.click(browserPage.NavigationPanel.workbenchButton); - await t.click(workbenchPage.getTutorialLinkLocator('_sq-intro')); + await t.click(workbenchPage.getTutorialLinkLocator('sq-intro')); await t.expect(workbenchPage.InsightsPanel.sidePanel.exists).ok('Insight panel is not opened'); const tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials); await t.expect(tab.preselectArea.textContent).contains('EXACT MATCH', 'the tutorial page is incorrect'); From ba3001b604e92d298f39eb5bd6e93362eae41b3f Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Fri, 11 Oct 2024 16:31:00 +0200 Subject: [PATCH 092/112] #RI-6173, #RI-6177, #RI-6178, #RI-6182, #RI-6183 - fix bugs --- .../components/query/Query/Query.tsx | 10 +++++- .../ui/src/pages/workbench/constants.ts | 5 ++- redisinsight/ui/src/pages/workbench/types.ts | 3 +- .../ui/src/pages/workbench/utils/query.ts | 35 ++++++++++++------- .../workbench/utils/searchSuggestions.ts | 34 +++++++++++++----- .../src/pages/workbench/utils/suggestions.ts | 11 +++--- .../utils/tests/test-cases/common.ts | 20 ++++++++++- 7 files changed, 87 insertions(+), 31 deletions(-) diff --git a/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx b/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx index c4eb8a232d..2708b01c04 100644 --- a/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx +++ b/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx @@ -112,6 +112,7 @@ const Query = (props: Props) => { const selectedArg = useRef('') const syntaxCommand = useRef(null) const isDedicatedEditorOpenRef = useRef(isDedicatedEditorOpen) + const isEscapedSuggestions = useRef(false) let syntaxWidgetContext: Nullable> = null const { commandsArray: REDIS_COMMANDS_ARRAY, spec: REDIS_COMMANDS_SPEC } = useSelector(appRedisCommandsSelector) @@ -314,6 +315,10 @@ const Query = (props: Props) => { if (e.keyCode === monacoEditor.KeyCode.Enter || e.keyCode === monacoEditor.KeyCode.Space) { onExitSnippetMode() } + + if (e.keyCode === monacoEditor.KeyCode.Escape && isSuggestionsOpened()) { + isEscapedSuggestions.current = true + } } const onExitSnippetMode = () => { @@ -564,6 +569,8 @@ const Query = (props: Props) => { if (COMMANDS_TO_GET_INDEX_INFO.some((name) => name === command.name)) { setSelectedIndex(allArgs[1] || '') + } else { + setSelectedIndex('') } const cursorContext: CursorContext = { ...cursor, currentOffsetArg, offset: command.commandCursorPosition, range } @@ -571,7 +578,8 @@ const Query = (props: Props) => { REDIS_COMMANDS, command, cursorContext, - { fields: attributesRef.current, indexes: indexesRef.current } + { fields: attributesRef.current, indexes: indexesRef.current }, + isEscapedSuggestions.current ) if (helpWidget) { diff --git a/redisinsight/ui/src/pages/workbench/constants.ts b/redisinsight/ui/src/pages/workbench/constants.ts index 4453342d69..86cdf3045c 100644 --- a/redisinsight/ui/src/pages/workbench/constants.ts +++ b/redisinsight/ui/src/pages/workbench/constants.ts @@ -76,7 +76,10 @@ export const COMMANDS_TO_GET_INDEX_INFO = [ 'FT.PROFILE', 'FT.SPELLCHECK', 'FT.TAGVALS', - 'FT.ALTER', + 'FT.ALTER' +] + +export const COMMANDS_WITHOUT_INDEX_PROPOSE = [ 'FT.CREATE' ] diff --git a/redisinsight/ui/src/pages/workbench/types.ts b/redisinsight/ui/src/pages/workbench/types.ts index 18f674fc67..7962623bbc 100644 --- a/redisinsight/ui/src/pages/workbench/types.ts +++ b/redisinsight/ui/src/pages/workbench/types.ts @@ -3,7 +3,8 @@ import { Maybe } from 'uiSrc/utils' import { IRedisCommand, IRedisCommandTree } from 'uiSrc/constants' export enum ArgName { - NArgs = 'nargs' + NArgs = 'nargs', + Count = 'count' } export interface FoundCommandArgument { diff --git a/redisinsight/ui/src/pages/workbench/utils/query.ts b/redisinsight/ui/src/pages/workbench/utils/query.ts index ca9bcda8c8..3bda2a3bc0 100644 --- a/redisinsight/ui/src/pages/workbench/utils/query.ts +++ b/redisinsight/ui/src/pages/workbench/utils/query.ts @@ -104,6 +104,7 @@ export const splitQueryByArgs = (query: string, position: number = 0) => { export const findCurrentArgument = ( args: IRedisCommand[], prev: string[], + untilTokenArgs: string[] = [], parent?: IRedisCommandTree ): Nullable => { for (let i = prev.length - 1; i >= 0; i--) { @@ -112,7 +113,7 @@ export const findCurrentArgument = ( const currentWithParent: IRedisCommandTree = { ...currentArg, parent } if (currentArg?.arguments && currentArg?.type === ICommandTokenType.Block) { - return findCurrentArgument(currentArg.arguments, prev.slice(i), currentWithParent) + return findCurrentArgument(currentArg.arguments, prev.slice(i), prev, currentWithParent) } const tokenIndex = args.findIndex((cArg) => @@ -126,7 +127,7 @@ export const findCurrentArgument = ( // getArgByRest - here we preparing the list of arguments which can be inserted, // this is the main function which creates the list of arguments return { - ...getArgumentSuggestions({ tokenArgs: pastArgs, levelArgs: prev }, commandArgs, parent), + ...getArgumentSuggestions({ tokenArgs: pastArgs, untilTokenArgs }, commandArgs, parent), parent: parent || token } } @@ -232,7 +233,7 @@ const findStopArgumentInQuery = ( continue } - if (currentCommandArg?.name === ArgName.NArgs) { + if (currentCommandArg?.name === ArgName.NArgs || currentCommandArg?.name === ArgName.Count) { const numberOfArgs = toNumber(arg) if (numberOfArgs === 0) { @@ -286,9 +287,9 @@ const findStopArgumentInQuery = ( } export const getArgumentSuggestions = ( - { tokenArgs, levelArgs }: { + { tokenArgs, untilTokenArgs }: { tokenArgs: string[], - levelArgs: string[] + untilTokenArgs: string[] }, pastCommandArgs: IRedisCommand[], current?: IRedisCommandTree @@ -340,7 +341,7 @@ export const getArgumentSuggestions = ( const foundParent = isBlockHasParent ? { ...parent, parent: current } : (parent || current) const isBlockComplete = !stopArgument && current?.name === lastArgument?.name - const beforeMandatoryOptionalArgs = getAllRestArguments(foundParent, lastArgument, levelArgs, isBlockComplete) + const beforeMandatoryOptionalArgs = getAllRestArguments(foundParent, lastArgument, untilTokenArgs, isBlockComplete) const requiredArgsLength = restNotFilledArgs.filter((arg) => !arg.optional).length return { @@ -357,8 +358,11 @@ export const getRestArguments = ( ): IRedisCommandTree[] => { const argumentIndexInArg = current?.arguments ?.findIndex(({ name }) => name === stopArgument?.name) - const nextMandatoryIndex = argumentIndexInArg && argumentIndexInArg > -1 ? current?.arguments - ?.findIndex(({ optional }, i) => !optional && i > argumentIndexInArg) : -1 + const nextMandatoryIndex = stopArgument && !stopArgument.optional + ? argumentIndexInArg + : argumentIndexInArg && argumentIndexInArg > -1 ? current?.arguments + ?.findIndex(({ optional }, i) => !optional && i > argumentIndexInArg) : -1 + const prevMandatory = current?.arguments?.slice(0, argumentIndexInArg).reverse() .find(({ optional }) => !optional) const prevMandatoryIndex = current?.arguments?.findIndex(({ name }) => name === prevMandatory?.name) @@ -387,12 +391,12 @@ export const getRestArguments = ( export const getAllRestArguments = ( current: Maybe, stopArgument: Nullable, - prevStringArgs: string[] = [], + untilTokenArgs: string[] = [], skipLevel = false ) => { const appendArgs: Array = [] const currentLvlNextArgs = removeNotSuggestedArgs( - prevStringArgs, + untilTokenArgs, getRestArguments(current, stopArgument) ) @@ -401,7 +405,7 @@ export const getAllRestArguments = ( } if (current?.parent) { - const parentArgs = getAllRestArguments(current.parent, current, skipLevel ? prevStringArgs : []) + const parentArgs = getAllRestArguments(current.parent, current, untilTokenArgs) if (parentArgs?.length) { appendArgs.push(...parentArgs) } @@ -421,7 +425,8 @@ export const removeNotSuggestedArgs = (args: string[], commandArgs: IRedisComman } if (arg.type === ICommandTokenType.Block) { - return arg.arguments?.[0]?.token && !args.includes(arg.arguments?.[0]?.token?.toUpperCase()) + if (arg.token) return !args.includes(arg.token) || arg.multiple + return arg.arguments?.[0]?.token && (!args.includes(arg.arguments?.[0]?.token?.toUpperCase()) || arg.multiple) } return arg.token && !args.includes(arg.token) @@ -437,6 +442,11 @@ export const fillArgsByType = (args: IRedisCommand[], expandBlock = true): IRedi result.push(...(currentArg?.arguments?.map((arg) => ({ ...arg, parent: currentArg })) || [])) } + if (currentArg.token) { + result.push(currentArg) + continue + } + if (currentArg.type === ICommandTokenType.Block) { result.push({ multiple: currentArg.multiple, @@ -445,7 +455,6 @@ export const fillArgsByType = (args: IRedisCommand[], expandBlock = true): IRedi ...(currentArg?.arguments?.[0] as IRedisCommand || {}), }) } - if (currentArg.token) result.push(currentArg) } return result diff --git a/redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts b/redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts index cc236d3a66..12c8e314db 100644 --- a/redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts +++ b/redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts @@ -12,7 +12,12 @@ import { getIndexesSuggestions, getNoIndexesSuggestion } from 'uiSrc/pages/workbench/utils/suggestions' -import { DefinedArgumentName, FIELD_START_SYMBOL, ModuleCommandPrefix } from 'uiSrc/pages/workbench/constants' +import { + COMMANDS_WITHOUT_INDEX_PROPOSE, + DefinedArgumentName, + FIELD_START_SYMBOL, + ModuleCommandPrefix +} from 'uiSrc/pages/workbench/constants' export const findSuggestionsByArg = ( listOfCommands: IRedisCommand[], @@ -21,7 +26,8 @@ export const findSuggestionsByArg = ( additionData: { indexes?: any[] fields?: any[], - } + }, + isEscaped: boolean = false ): { suggestions: any, helpWidget?: any @@ -47,7 +53,7 @@ export const findSuggestionsByArg = ( case DefinedArgumentName.index: { return handleIndexSuggestions( additionData.indexes || [], - command.info as IRedisCommand, + command, foundArg, currentOffsetArg, cursorContext.range @@ -62,7 +68,8 @@ export const findSuggestionsByArg = ( foundArg, allArgs, additionData.fields || [], - cursorContext + cursorContext, + isEscaped ) } } @@ -82,13 +89,21 @@ const handleFieldSuggestions = ( const handleIndexSuggestions = ( indexes: any[], - command: IRedisCommand, + command: IMonacoQuery, foundArg: FoundCommandArgument, currentOffsetArg: Nullable, range: monacoEditor.IRange ) => { const isIndex = indexes.length > 0 - const helpWidget = { isOpen: isIndex, parent: command, currentArg: foundArg?.stopArg } + const helpWidget = { isOpen: isIndex, parent: command.info, currentArg: foundArg?.stopArg } + const currentCommand = command.info + + if (COMMANDS_WITHOUT_INDEX_PROPOSE.includes(command.name || '')) { + return { + suggestions: asSuggestionsRef([]), + helpWidget + } + } if (!isIndex) { helpWidget.isOpen = !!currentOffsetArg @@ -106,10 +121,10 @@ const handleIndexSuggestions = ( } } - const argumentIndex = command?.arguments + const argumentIndex = currentCommand?.arguments ?.findIndex(({ name }) => foundArg?.stopArg?.name === name) const isNextArgQuery = isNumber(argumentIndex) - && command?.arguments?.[argumentIndex + 1]?.name === DefinedArgumentName.query + && currentCommand?.arguments?.[argumentIndex + 1]?.name === DefinedArgumentName.query return { suggestions: asSuggestionsRef(getIndexesSuggestions(indexes, range, isNextArgQuery)), @@ -158,11 +173,12 @@ const handleCommonSuggestions = ( allArgs: string[], fields: any[], cursorContext: CursorContext, + isEscaped: boolean ) => { if (foundArg?.stopArg?.expression) return handleExpressionSuggestions(value, foundArg, cursorContext) const { prevCursorChar, nextCursorChar, isCursorInQuotes } = cursorContext - const shouldHideSuggestions = isCursorInQuotes || nextCursorChar || (prevCursorChar) + const shouldHideSuggestions = isCursorInQuotes || nextCursorChar || (prevCursorChar && isEscaped) if (shouldHideSuggestions) { return { helpWidget: { isOpen: true, parent: foundArg?.parent, currentArg: foundArg?.stopArg }, diff --git a/redisinsight/ui/src/pages/workbench/utils/suggestions.ts b/redisinsight/ui/src/pages/workbench/utils/suggestions.ts index 288ae6dc69..b90823f0ff 100644 --- a/redisinsight/ui/src/pages/workbench/utils/suggestions.ts +++ b/redisinsight/ui/src/pages/workbench/utils/suggestions.ts @@ -2,10 +2,11 @@ import { monaco } from 'react-monaco-editor' import * as monacoEditor from 'monaco-editor' import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' import { bufferToString, formatLongName, generateArgsForInsertText, getCommandMarkdown, Nullable } from 'uiSrc/utils' -import { FoundCommandArgument, SearchCommand } from 'uiSrc/pages/workbench/types' +import { FoundCommandArgument } from 'uiSrc/pages/workbench/types' import { DefinedArgumentName, EmptySuggestionsIds } from 'uiSrc/pages/workbench/constants' import { getUtmExternalLink } from 'uiSrc/utils/links' -import { removeNotSuggestedArgs, generateDetail } from './query' +import { IRedisCommand } from 'uiSrc/constants' +import { generateDetail, removeNotSuggestedArgs } from './query' import { buildSuggestion, } from './monaco' export const asSuggestionsRef = ( @@ -81,13 +82,13 @@ export const getFieldsSuggestions = ( } }) -const insertFunctionArguments = (args: SearchCommand[]) => +const insertFunctionArguments = (args: IRedisCommand[]) => generateArgsForInsertText( args.map(({ token, optional }) => (optional ? `[${token}]` : (token || ''))) as string[], ', ' ) -export const getFunctionsSuggestions = (functions: SearchCommand[], range: monaco.IRange) => functions +export const getFunctionsSuggestions = (functions: IRedisCommand[], range: monaco.IRange) => functions .map(({ token, summary, arguments: args }) => ({ label: token || '', insertText: `${token}(${insertFunctionArguments(args || [])})`, @@ -97,7 +98,7 @@ export const getFunctionsSuggestions = (functions: SearchCommand[], range: monac detail: summary })) -export const getCommandsSuggestions = (commands: SearchCommand[], range: monaco.IRange) => +export const getCommandsSuggestions = (commands: IRedisCommand[], range: monaco.IRange) => commands.map((command) => buildSuggestion(command, range, { detail: generateDetail(command), insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, diff --git a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts index 7946e4baa6..2f8b3f3a1d 100644 --- a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts +++ b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts @@ -51,7 +51,7 @@ export const commonfindCurrentArgumentCases = [ { input: 'FT.CREATE "idx:schools" ', result: expect.any(Object), - appendIncludes: ['FILTER', 'ON', 'SCHEMA', 'SCORE', 'NOHL'], + appendIncludes: ['FILTER', 'ON', 'SCHEMA', 'SCORE', 'NOHL', 'STOPWORDS'], appendNotIncludes: ['HASH', 'JSON'], }, { @@ -180,4 +180,22 @@ export const commonfindCurrentArgumentCases = [ appendIncludes: ['WITHPAYLOADS', 'WITHSCORES'], appendNotIncludes: ['FUZZY', 'MAX'], }, + { + input: 'FT.ALTER index SKIPINITIALSCAN ', + result: expect.any(Object), + appendIncludes: ['SCHEMA'], + appendNotIncludes: ['ADD'], + }, + { + input: 'FT.SPELLCHECK idx "" ', + result: expect.any(Object), + appendIncludes: ['DIALECT', 'DISTANCE', 'TERMS'], + appendNotIncludes: ['EXCLUDE', 'INCLUDE'], + }, + { + input: 'FT.SEARCH index "" HIGHLIGHT FIELDS 1 f1 ', + result: expect.any(Object), + appendIncludes: ['TAGS', 'SUMMARIZE', 'DIALECT', 'FILTER', 'WITHSCORES', 'INKEYS'], + appendNotIncludes: ['FIELDS'], + }, ] From fef8ce3feb13a89ac2331d3202da83906109247a Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Mon, 14 Oct 2024 10:50:55 +0200 Subject: [PATCH 093/112] #RI-6203 - fix suggestions --- .../ui/src/pages/workbench/components/query/Query/Query.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx b/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx index 2708b01c04..7670e4b8f2 100644 --- a/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx +++ b/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx @@ -556,12 +556,11 @@ const Query = (props: Props) => { if (position.column === 1) { helpWidgetRef.current.isOpen = false if (command) return asSuggestionsRef([]) - return asSuggestionsRef(getCommandsSuggestions(REDIS_COMMANDS, range), false) } if (!command) { - return asSuggestionsRef([], false) + return asSuggestionsRef(getCommandsSuggestions(REDIS_COMMANDS, range), false) } const { allArgs, args, cursor } = command From ee3d1d4e21629dbaa6242300fa4bfa442bf9668c Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Tue, 15 Oct 2024 14:55:31 +0200 Subject: [PATCH 094/112] #RI-6217 - fix suggestions #RI-6218 - fix cypher editor * small refactoring --- .../ui/src/pages/search/utils/query.ts | 4 +- .../components/query/Query/Query.tsx | 68 ++++------- .../workbench/data/supported_commands.json | 8 +- .../ui/src/pages/workbench/utils/query.ts | 113 ++---------------- .../workbench/utils/searchSuggestions.ts | 47 ++++---- .../pages/workbench/utils/tests/query.spec.ts | 85 +------------ .../utils/tests/test-cases/common.ts | 28 +++++ .../utils/tests/test-cases/ft-aggregate.ts | 2 +- .../utils/tests/test-cases/ft-search.ts | 14 +-- .../ui/src/utils/monaco/monacoUtils.ts | 35 ++++-- .../monaco/subTokens/redisearchSubTokens.ts | 0 .../utils/tests/monaco/monacoUtils.spec.ts | 82 ++++++++++++- 12 files changed, 203 insertions(+), 283 deletions(-) delete mode 100644 redisinsight/ui/src/utils/monaco/subTokens/redisearchSubTokens.ts diff --git a/redisinsight/ui/src/pages/search/utils/query.ts b/redisinsight/ui/src/pages/search/utils/query.ts index a18498d5d5..b3e0f984f6 100644 --- a/redisinsight/ui/src/pages/search/utils/query.ts +++ b/redisinsight/ui/src/pages/search/utils/query.ts @@ -457,8 +457,8 @@ export const findArgByToken = (list: SearchCommand[], arg: string): Maybe oneOfArg?.token?.toLowerCase() === arg?.toLowerCase()) : cArg.arguments?.[0]?.token?.toLowerCase() === arg.toLowerCase())) -export const isCompositeArgument = (arg: string, prevArg?: string) => - COMPOSITE_ARGS.includes([prevArg?.toUpperCase(), arg?.toUpperCase()].join(' ')) +export const isCompositeArgument = (arg: string, prevArg?: string, args: string[] = []) => + args.includes([prevArg?.toUpperCase(), arg?.toUpperCase()].join(' ')) export const generateDetail = (command: Maybe) => { if (!command) return '' diff --git a/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx b/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx index 7670e4b8f2..e9fdbce023 100644 --- a/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx +++ b/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx @@ -5,17 +5,11 @@ import cx from 'classnames' import MonacoEditor, { monaco as monacoEditor } from 'react-monaco-editor' import { useParams } from 'react-router-dom' -import { - Theme, - MonacoLanguage, - DSLNaming, - IRedisCommand, -} from 'uiSrc/constants' +import { DSLNaming, ICommandTokenType, IRedisCommand, MonacoLanguage, Theme, } from 'uiSrc/constants' import { actionTriggerParameterHints, createSyntaxWidget, decoration, - findArgIndexByCursor, findCompleteQuery, getMonacoAction, IMonacoQuery, @@ -28,35 +22,26 @@ import { ThemeContext } from 'uiSrc/contexts/themeContext' import { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands' import { IEditorMount, ISnippetController } from 'uiSrc/pages/workbench/interfaces' import { CommandExecutionUI, RedisResponseBuffer } from 'uiSrc/slices/interfaces' -import { RunQueryMode, ResultsMode } from 'uiSrc/slices/interfaces/workbench' +import { ResultsMode, RunQueryMode } from 'uiSrc/slices/interfaces/workbench' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { stopProcessing, workbenchResultsSelector } from 'uiSrc/slices/workbench/wb-results' import DedicatedEditor from 'uiSrc/components/monaco-editor/components/dedicated-editor' import { QueryActions, QueryTutorials } from 'uiSrc/components/query' -import { - addOwnTokenToArgs, -} from 'uiSrc/pages/workbench/utils/query' +import { addOwnTokenToArgs, findCurrentArgument, } from 'uiSrc/pages/workbench/utils/query' import { getRange, getRediSearchSignutureProvider, } from 'uiSrc/pages/workbench/utils/monaco' import { CursorContext } from 'uiSrc/pages/workbench/types' -import { - asSuggestionsRef, - getCommandsSuggestions, - isIndexComplete -} from 'uiSrc/pages/workbench/utils/suggestions' -import { - COMMANDS_TO_GET_INDEX_INFO, - EmptySuggestionsIds, -} from 'uiSrc/pages/workbench/constants' +import { asSuggestionsRef, getCommandsSuggestions, isIndexComplete } from 'uiSrc/pages/workbench/utils/suggestions' +import { COMMANDS_TO_GET_INDEX_INFO, COMPOSITE_ARGS, EmptySuggestionsIds, } from 'uiSrc/pages/workbench/constants' import { useDebouncedEffect } from 'uiSrc/services' import { fetchRedisearchInfoAction } from 'uiSrc/slices/browser/redisearch' import { findSuggestionsByArg } from 'uiSrc/pages/workbench/utils/searchSuggestions' import { - aroundQuotesRegExp, argInQuotesRegExp, + aroundQuotesRegExp, + options, SYNTAX_CONTEXT_ID, SYNTAX_WIDGET_ID, - options, TUTORIALS } from './constants' import styles from './styles.module.scss' @@ -343,7 +328,7 @@ const Query = (props: Props) => { return } - const command = findCompleteQuery(model, e.position, REDIS_COMMANDS_SPEC, REDIS_COMMANDS_ARRAY) + const command = findCompleteQuery(model, e.position, REDIS_COMMANDS_SPEC, REDIS_COMMANDS_ARRAY, COMPOSITE_ARGS) handleSuggestions(editor, command) handleDslSyntax(e, command) } @@ -427,33 +412,30 @@ const Query = (props: Props) => { return } - const queryArgIndex = command.info?.arguments?.findIndex((arg) => arg.dsl) || -1 - const cursorPosition = command.commandCursorPosition || 0 - const { allArgs } = command || {} - if (!allArgs.length || queryArgIndex < 0) { + const isContainsDSL = command.info?.arguments?.some((arg) => arg.dsl) + if (!isContainsDSL) { isWidgetEscaped.current = false return } - const argIndex = findArgIndexByCursor(allArgs, command.fullQuery, cursorPosition) - if (argIndex === null) { - isWidgetEscaped.current = false - return - } - - const queryArg = allArgs[argIndex] - const argDSL = command.info?.arguments?.[argIndex]?.dsl || '' + const [beforeOffsetArgs, [currentOffsetArg]] = command.args + const foundArg = findCurrentArgument([{ + ...command.info, + type: ICommandTokenType.Block, + token: command.name, + arguments: command.info?.arguments + }], beforeOffsetArgs) - if (queryArgIndex === argIndex && argInQuotesRegExp.test(queryArg)) { + const DSL = foundArg?.stopArg?.dsl + if (DSL && argInQuotesRegExp.test(currentOffsetArg)) { if (isWidgetEscaped.current) return - const lang = DSLNaming[argDSL] ?? null + + const lang = DSLNaming[DSL] ?? null lang && showSyntaxWidget(editor, e.position, lang) - selectedArg.current = queryArg - syntaxCommand.current = { - ...command, - lang: argDSL, - argToReplace: queryArg - } + selectedArg.current = currentOffsetArg + syntaxCommand.current = { ...command, lang: DSL, argToReplace: currentOffsetArg } + } else { + isWidgetEscaped.current = false } } diff --git a/redisinsight/ui/src/pages/workbench/data/supported_commands.json b/redisinsight/ui/src/pages/workbench/data/supported_commands.json index fa08d11556..6765dceb75 100644 --- a/redisinsight/ui/src/pages/workbench/data/supported_commands.json +++ b/redisinsight/ui/src/pages/workbench/data/supported_commands.json @@ -714,14 +714,10 @@ "optional": true }, { - "name": "queryword", - "type": "pure-token", + "name": "query", + "type": "token", "token": "QUERY", "expression": true - }, - { - "name": "query", - "type": "string" } ], "since": "2.2.0", diff --git a/redisinsight/ui/src/pages/workbench/utils/query.ts b/redisinsight/ui/src/pages/workbench/utils/query.ts index 3bda2a3bc0..9120c3d876 100644 --- a/redisinsight/ui/src/pages/workbench/utils/query.ts +++ b/redisinsight/ui/src/pages/workbench/utils/query.ts @@ -1,106 +1,10 @@ /* eslint-disable no-continue */ -import { isNumber, toNumber } from 'lodash' +import { findLastIndex, isNumber, toNumber } from 'lodash' import { generateArgsNames, Maybe, Nullable } from 'uiSrc/utils' import { CommandProvider, IRedisCommand, IRedisCommandTree, ICommandTokenType } from 'uiSrc/constants' -import { COMPOSITE_ARGS } from 'uiSrc/pages/workbench/constants' import { ArgName, FoundCommandArgument } from '../types' -export const splitQueryByArgs = (query: string, position: number = 0) => { - const args: [string[], string[]] = [[], []] - let arg = '' - let inQuotes = false - let escapeNextChar = false - let quoteChar = '' - let isCursorInQuotes = false - let lastArg = '' - let argLeftOffset = 0 - let argRightOffset = 0 - - const pushToProperTuple = (isAfterOffset: boolean, arg: string) => { - lastArg = arg - isAfterOffset ? args[1].push(arg) : args[0].push(arg) - } - - const updateLastArgument = (isAfterOffset: boolean, arg: string) => { - const argsBySide = args[isAfterOffset ? 1 : 0] - argsBySide[argsBySide.length - 1] = `${argsBySide[argsBySide.length - 1]} ${arg}` - } - - const updateArgOffsets = (left: number, right: number) => { - argLeftOffset = left - argRightOffset = right - } - - for (let i = 0; i < query.length; i++) { - const char = query[i] - const isAfterOffset = i >= position + (inQuotes ? -1 : 0) - - if (escapeNextChar) { - arg += char - escapeNextChar = !quoteChar - } else if (char === '\\') { - escapeNextChar = true - } else if (inQuotes) { - if (char === quoteChar) { - inQuotes = false - const argWithChat = arg + char - - if (isAfterOffset && !argLeftOffset) { - updateArgOffsets(i - arg.length, i + 1) - } - - if (isCompositeArgument(argWithChat, lastArg)) { - updateLastArgument(isAfterOffset, argWithChat) - } else { - pushToProperTuple(isAfterOffset, argWithChat) - } - - arg = '' - } else { - arg += char - } - } else if (char === '"' || char === "'") { - inQuotes = true - quoteChar = char - arg += char - } else if (char === ' ' || char === '\n') { - if (arg.length > 0) { - if (isAfterOffset && !argLeftOffset) { - updateArgOffsets(i - arg.length, i) - } - - if (isCompositeArgument(arg, lastArg)) { - updateLastArgument(isAfterOffset, arg) - } else { - pushToProperTuple(isAfterOffset, arg) - } - - arg = '' - } - } else { - arg += char - } - - if (i === position - 1) isCursorInQuotes = inQuotes - } - - if (arg.length > 0) { - if (!argLeftOffset) updateArgOffsets(query.length - arg.length, query.length) - pushToProperTuple(true, arg) - } - - const cursor = { - isCursorInQuotes, - prevCursorChar: query[position - 1]?.trim() || '', - nextCursorChar: query[position]?.trim() || '', - argLeftOffset, - argRightOffset - } - - return { args, cursor } -} - export const findCurrentArgument = ( args: IRedisCommand[], prev: string[], @@ -340,7 +244,7 @@ export const getArgumentSuggestions = ( const isBlockHasParent = current?.arguments?.some(({ name }) => parent?.name && name === parent?.name) const foundParent = isBlockHasParent ? { ...parent, parent: current } : (parent || current) - const isBlockComplete = !stopArgument && current?.name === lastArgument?.name + const isBlockComplete = !stopArgument && isPrevArgWasMandatory const beforeMandatoryOptionalArgs = getAllRestArguments(foundParent, lastArgument, untilTokenArgs, isBlockComplete) const requiredArgsLength = restNotFilledArgs.filter((arg) => !arg.optional).length @@ -395,8 +299,14 @@ export const getAllRestArguments = ( skipLevel = false ) => { const appendArgs: Array = [] - const currentLvlNextArgs = removeNotSuggestedArgs( + + const currentToken = current?.type === ICommandTokenType.Block ? current?.arguments?.[0].token : current?.token + const lastTokenIndex = findLastIndex( untilTokenArgs, + (arg) => arg?.toLowerCase() === currentToken?.toLowerCase() + ) + const currentLvlNextArgs = removeNotSuggestedArgs( + untilTokenArgs.slice(lastTokenIndex > 0 ? lastTokenIndex : 0), getRestArguments(current, stopArgument) ) @@ -464,10 +374,7 @@ export const findArgByToken = (list: IRedisCommand[], arg: string): Maybe (cArg.type === ICommandTokenType.OneOf ? cArg.arguments?.some((oneOfArg: IRedisCommand) => oneOfArg?.token?.toLowerCase() === arg?.toLowerCase()) - : cArg.arguments?.[0]?.token?.toLowerCase() === arg.toLowerCase())) - -export const isCompositeArgument = (arg: string, prevArg?: string) => - COMPOSITE_ARGS.includes([prevArg?.toUpperCase(), arg?.toUpperCase()].join(' ')) + : cArg.arguments?.[0]?.token?.toLowerCase() === arg?.toLowerCase())) export const generateDetail = (command: Maybe) => { if (!command) return '' diff --git a/redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts b/redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts index 12c8e314db..3ca7542f9a 100644 --- a/redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts +++ b/redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts @@ -1,8 +1,8 @@ import { monaco as monacoEditor } from 'react-monaco-editor' import { isNumber } from 'lodash' -import { IMonacoQuery, Nullable } from 'uiSrc/utils' +import { IMonacoQuery, Nullable, splitQueryByArgs } from 'uiSrc/utils' import { CursorContext, FoundCommandArgument } from 'uiSrc/pages/workbench/types' -import { findCurrentArgument, splitQueryByArgs } from 'uiSrc/pages/workbench/utils/query' +import { findCurrentArgument } from 'uiSrc/pages/workbench/utils/query' import { IRedisCommand } from 'uiSrc/constants' import { asSuggestionsRef, @@ -49,28 +49,27 @@ export const findSuggestionsByArg = ( return handleFieldSuggestions(additionData.fields || [], foundArg, cursorContext.range) } + if (foundArg?.stopArg?.token && !foundArg?.isBlocked) { + return handleCommonSuggestions( + command.fullQuery, + foundArg, + allArgs, + additionData.fields || [], + cursorContext, + isEscaped + ) + } + + const { indexes, fields } = additionData switch (foundArg?.stopArg?.name) { case DefinedArgumentName.index: { - return handleIndexSuggestions( - additionData.indexes || [], - command, - foundArg, - currentOffsetArg, - cursorContext.range - ) + return handleIndexSuggestions(indexes, command, foundArg, currentOffsetArg, cursorContext) } case DefinedArgumentName.query: { return handleQuerySuggestions(foundArg) } default: { - return handleCommonSuggestions( - command.fullQuery, - foundArg, - allArgs, - additionData.fields || [], - cursorContext, - isEscaped - ) + return handleCommonSuggestions(command.fullQuery, foundArg, allArgs, fields, cursorContext, isEscaped) } } } @@ -88,11 +87,11 @@ const handleFieldSuggestions = ( } const handleIndexSuggestions = ( - indexes: any[], + indexes: any[] = [], command: IMonacoQuery, foundArg: FoundCommandArgument, currentOffsetArg: Nullable, - range: monacoEditor.IRange + cursorContext: CursorContext ) => { const isIndex = indexes.length > 0 const helpWidget = { isOpen: isIndex, parent: command.info, currentArg: foundArg?.stopArg } @@ -109,7 +108,7 @@ const handleIndexSuggestions = ( helpWidget.isOpen = !!currentOffsetArg return { - suggestions: asSuggestionsRef(!currentOffsetArg ? getNoIndexesSuggestion(range) : [], true), + suggestions: asSuggestionsRef(!currentOffsetArg ? getNoIndexesSuggestion(cursorContext.range) : [], true), helpWidget } } @@ -127,7 +126,7 @@ const handleIndexSuggestions = ( && currentCommand?.arguments?.[argumentIndex + 1]?.name === DefinedArgumentName.query return { - suggestions: asSuggestionsRef(getIndexesSuggestions(indexes, range, isNextArgQuery)), + suggestions: asSuggestionsRef(getIndexesSuggestions(indexes, cursorContext.range, isNextArgQuery)), helpWidget } } @@ -171,11 +170,13 @@ const handleCommonSuggestions = ( value: string, foundArg: Nullable, allArgs: string[], - fields: any[], + fields: any[] = [], cursorContext: CursorContext, isEscaped: boolean ) => { - if (foundArg?.stopArg?.expression) return handleExpressionSuggestions(value, foundArg, cursorContext) + if (foundArg?.stopArg?.expression && foundArg.isBlocked) { + return handleExpressionSuggestions(value, foundArg, cursorContext) + } const { prevCursorChar, nextCursorChar, isCursorInQuotes } = cursorContext const shouldHideSuggestions = isCursorInQuotes || nextCursorChar || (prevCursorChar && isEscaped) diff --git a/redisinsight/ui/src/pages/workbench/utils/tests/query.spec.ts b/redisinsight/ui/src/pages/workbench/utils/tests/query.spec.ts index 5afe4fe26b..a3ab9fc6fd 100644 --- a/redisinsight/ui/src/pages/workbench/utils/tests/query.spec.ts +++ b/redisinsight/ui/src/pages/workbench/utils/tests/query.spec.ts @@ -1,13 +1,14 @@ import { SearchCommand, TokenType } from 'uiSrc/pages/search/types' -import { Maybe } from 'uiSrc/utils' +import { Maybe, splitQueryByArgs } from 'uiSrc/utils' import { MOCKED_REDIS_COMMANDS } from 'uiSrc/mocks/data/mocked_redis_commands' import { IRedisCommand } from 'uiSrc/constants' +import { COMPOSITE_ARGS } from 'uiSrc/pages/workbench/constants' import { commonfindCurrentArgumentCases, findArgumentftAggreageTests, findArgumentftSearchTests } from './test-cases' -import { addOwnTokenToArgs, findCurrentArgument, generateDetail, splitQueryByArgs } from '../query' +import { addOwnTokenToArgs, findCurrentArgument, generateDetail } from '../query' const ftSearchCommand = MOCKED_REDIS_COMMANDS['FT.SEARCH'] const ftAggregateCommand = MOCKED_REDIS_COMMANDS['FT.AGGREGATE'] @@ -20,7 +21,7 @@ describe('findCurrentArgument', () => { describe('with list of commands', () => { commonfindCurrentArgumentCases.forEach(({ input, result, appendIncludes, appendNotIncludes }) => { it(`should return proper suggestions for ${input}`, () => { - const { args } = splitQueryByArgs(input) + const { args } = splitQueryByArgs(input, 0, COMPOSITE_ARGS) const COMMANDS_LIST = COMMANDS.map((command) => ({ ...addOwnTokenToArgs(command.name!, command), token: command.name!, @@ -76,84 +77,6 @@ describe('findCurrentArgument', () => { }) }) -const splitQueryByArgsTests: Array<{ - input: [string, number?] - result: any -}> = [ - { - input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS'], - result: { - args: [[], ['FT.SEARCH', '"idx:bicycle"', '""', 'WITHSORTKEYS']], - cursor: { - argLeftOffset: 10, - argRightOffset: 23, - isCursorInQuotes: false, - nextCursorChar: 'F', - prevCursorChar: '' - } - } - }, - { - input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS', 17], - result: { - args: [['FT.SEARCH'], ['"idx:bicycle"', '""', 'WITHSORTKEYS']], - cursor: { - argLeftOffset: 10, - argRightOffset: 23, - isCursorInQuotes: true, - nextCursorChar: 'c', - prevCursorChar: 'i' - } - } - }, - { - input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS', 39], - result: { - args: [['FT.SEARCH', '"idx:bicycle"', '""'], ['WITHSORTKEYS']], - cursor: { - argLeftOffset: 27, - argRightOffset: 39, - isCursorInQuotes: false, - nextCursorChar: '', - prevCursorChar: 'S' - } - } - }, - { - input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS ', 40], - result: { - args: [['FT.SEARCH', '"idx:bicycle"', '""', 'WITHSORTKEYS'], []], - cursor: { - argLeftOffset: 0, - argRightOffset: 0, - isCursorInQuotes: false, - nextCursorChar: '', - prevCursorChar: '' - } - } - }, - { - input: ['FT.SEARCH "idx:bicycle \\" \\"" "" WITHSORTKEYS ', 46], - result: { - args: [['FT.SEARCH', '"idx:bicycle " ""', '""', 'WITHSORTKEYS'], []], - cursor: { - argLeftOffset: 0, - argRightOffset: 0, - isCursorInQuotes: false, - nextCursorChar: '', - prevCursorChar: '' - } - } - } -] - -describe('splitQueryByArgs', () => { - it.each(splitQueryByArgsTests)('should return for %input proper result', ({ input, result }) => { - const testResult = splitQueryByArgs(...input) - expect(testResult).toEqual(result) - }) -}) - const generateDetailTests: Array<{ input: Maybe, result: any }> = [ { input: ftSearchCommand.arguments.find(({ name }) => name === 'nocontent') as SearchCommand, diff --git a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts index 2f8b3f3a1d..f7a9ff23b8 100644 --- a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts +++ b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts @@ -24,6 +24,22 @@ export const commonfindCurrentArgumentCases = [ appendIncludes: ['REDUCE', 'APPLY', 'SORTBY', 'GROUPBY'], appendNotIncludes: ['AS'], }, + { + input: 'FT.AGGREGATE \'idx1:vd\' "*" GROUPBY 1 @location REDUCE COUNT 0 AS item_count REDUCE SUM 1 @students ', + result: { + stopArg: { + name: 'name', + optional: true, + token: 'AS', + type: 'string' + }, + append: expect.any(Array), + isBlocked: false, + isComplete: true, + parent: expect.any(Object) + }, + appendIncludes: ['AS', 'REDUCE', 'APPLY', 'SORTBY', 'GROUPBY'], + }, { input: 'FT.SEARCH "idx:bicycle" "*" ', result: { @@ -48,6 +64,18 @@ export const commonfindCurrentArgumentCases = [ appendIncludes: ['LIMITED', 'QUERY'], appendNotIncludes: ['AGGREGATE', 'SEARCH'], }, + { + input: 'FT.PROFILE idx AGGREGATE LIMITED ', + result: expect.any(Object), + appendIncludes: ['QUERY'], + appendNotIncludes: ['LIMITED', 'SEARCH'], + }, + { + input: 'FT.PROFILE \'idx:schools\' SEARCH QUERY \'q\' ', + result: expect.any(Object), + appendIncludes: [], + appendNotIncludes: ['LIMITED'], + }, { input: 'FT.CREATE "idx:schools" ', result: expect.any(Object), diff --git a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-aggregate.ts b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-aggregate.ts index e1411809a9..fcee36b2c8 100644 --- a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-aggregate.ts +++ b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-aggregate.ts @@ -258,7 +258,7 @@ export const findArgumentftAggreageTests = [ args: ['index', '"query"', 'LOAD', '4', '1', '2', '3', '4'], result: { stopArg: undefined, - append: [[]], + append: [], isBlocked: false, isComplete: true, parent: expect.any(Object) diff --git a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-search.ts b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-search.ts index 28137bf8e9..a729a10fa5 100644 --- a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-search.ts +++ b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-search.ts @@ -133,9 +133,7 @@ export const findArgumentftSearchTests = [ args: ['', '', 'RETURN', '1', 'iden'], result: { stopArg: undefined, - append: [ - [] - ], + append: [], isBlocked: false, isComplete: true, parent: expect.any(Object) @@ -171,9 +169,7 @@ export const findArgumentftSearchTests = [ args: ['', '', 'RETURN', '2', 'iden', 'iden'], result: { stopArg: undefined, - append: [ - [] - ], + append: [], isBlocked: false, isComplete: true, parent: expect.any(Object) @@ -209,9 +205,7 @@ export const findArgumentftSearchTests = [ args: ['', '', 'RETURN', '3', 'iden', 'iden', 'AS', 'iden2'], result: { stopArg: undefined, - append: [ - [] - ], + append: [], isBlocked: false, isComplete: true, parent: expect.any(Object) @@ -262,7 +256,7 @@ export const findArgumentftSearchTests = [ args: ['', '', 'SORTBY', 'f', 'DESC'], result: { stopArg: undefined, - append: [], + append: [[]], isBlocked: false, isComplete: true, parent: expect.any(Object) diff --git a/redisinsight/ui/src/utils/monaco/monacoUtils.ts b/redisinsight/ui/src/utils/monaco/monacoUtils.ts index 4ff48b893e..db18fedb03 100644 --- a/redisinsight/ui/src/utils/monaco/monacoUtils.ts +++ b/redisinsight/ui/src/utils/monaco/monacoUtils.ts @@ -1,7 +1,7 @@ import { monaco as monacoEditor } from 'react-monaco-editor' import { first, isEmpty, isUndefined, reject, without } from 'lodash' import { decode } from 'html-entities' -import { ICommands, ICommand } from 'uiSrc/constants' +import { ICommand, ICommands } from 'uiSrc/constants' import { generateArgsForInsertText, generateArgsNames, @@ -10,7 +10,6 @@ import { IMonacoQuery } from 'uiSrc/utils' import { TJMESPathFunctions } from 'uiSrc/slices/interfaces' -import { isCompositeArgument } from 'uiSrc/pages/search/utils' import { Nullable } from '../types' import { getCommandRepeat, isRepeatCountCorrect } from '../commands' @@ -97,16 +96,21 @@ export const findCommandEarlier = ( return null } - const command:IMonacoCommand = { + return { position, name: matchedCommand, info: commandsSpec[matchedCommand] } - - return command } -export const splitQueryByArgs = (query: string, position: number = 0) => { +export const isCompositeArgument = (arg: string, prevArg?: string, args: string[] = []) => + args.includes([prevArg?.toUpperCase(), arg?.toUpperCase()].join(' ')) + +export const splitQueryByArgs = ( + query: string, + position: number = 0, + compositeArgs: string[] = [] +) => { const args: [string[], string[]] = [[], []] let arg = '' let inQuotes = false @@ -144,16 +148,16 @@ export const splitQueryByArgs = (query: string, position: number = 0) => { } else if (inQuotes) { if (char === quoteChar) { inQuotes = false - const argWithChat = arg + char + const argWithChar = arg + char if (isAfterOffset && !argLeftOffset) { updateArgOffsets(i - arg.length, i + 1) } - if (isCompositeArgument(argWithChat, lastArg)) { - updateLastArgument(isAfterOffset, argWithChat) + if (isCompositeArgument(argWithChar, lastArg, compositeArgs)) { + updateLastArgument(isAfterOffset, argWithChar) } else { - pushToProperTuple(isAfterOffset, argWithChat) + pushToProperTuple(isAfterOffset, argWithChar) } arg = '' @@ -170,7 +174,7 @@ export const splitQueryByArgs = (query: string, position: number = 0) => { updateArgOffsets(i - arg.length, i) } - if (isCompositeArgument(arg, lastArg)) { + if (isCompositeArgument(arg, lastArg, compositeArgs)) { updateLastArgument(isAfterOffset, arg) } else { pushToProperTuple(isAfterOffset, arg) @@ -205,7 +209,8 @@ export const findCompleteQuery = ( model: monacoEditor.editor.ITextModel, position: monacoEditor.Position, commandsSpec: ICommands = {}, - commandsArray: string[] = [] + commandsArray: string[] = [], + compositeArgs: string[] = [] ): Nullable => { const { lineNumber } = position let commandName = '' @@ -262,7 +267,11 @@ export const findCompleteQuery = ( fullQuery += lineAfterPosition } - const { args, cursor } = splitQueryByArgs(fullQuery, commandCursorPosition) + const { args, cursor } = splitQueryByArgs( + fullQuery, + commandCursorPosition, + compositeArgs, + ) return { position, diff --git a/redisinsight/ui/src/utils/monaco/subTokens/redisearchSubTokens.ts b/redisinsight/ui/src/utils/monaco/subTokens/redisearchSubTokens.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/redisinsight/ui/src/utils/tests/monaco/monacoUtils.spec.ts b/redisinsight/ui/src/utils/tests/monaco/monacoUtils.spec.ts index 89c21ed025..89ddbf8245 100644 --- a/redisinsight/ui/src/utils/tests/monaco/monacoUtils.spec.ts +++ b/redisinsight/ui/src/utils/tests/monaco/monacoUtils.spec.ts @@ -4,7 +4,9 @@ import { splitMonacoValuePerLines, findArgIndexByCursor, isParamsLine, - getMonacoLines, getCommandsFromQuery + getMonacoLines, + getCommandsFromQuery, + splitQueryByArgs } from 'uiSrc/utils' describe('removeMonacoComments', () => { @@ -209,3 +211,81 @@ describe('getCommandsFromQuery', () => { } ) }) + +const splitQueryByArgsTests: Array<{ + input: [string, number?] + result: any +}> = [ + { + input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS'], + result: { + args: [[], ['FT.SEARCH', '"idx:bicycle"', '""', 'WITHSORTKEYS']], + cursor: { + argLeftOffset: 10, + argRightOffset: 23, + isCursorInQuotes: false, + nextCursorChar: 'F', + prevCursorChar: '' + } + } + }, + { + input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS', 17], + result: { + args: [['FT.SEARCH'], ['"idx:bicycle"', '""', 'WITHSORTKEYS']], + cursor: { + argLeftOffset: 10, + argRightOffset: 23, + isCursorInQuotes: true, + nextCursorChar: 'c', + prevCursorChar: 'i' + } + } + }, + { + input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS', 39], + result: { + args: [['FT.SEARCH', '"idx:bicycle"', '""'], ['WITHSORTKEYS']], + cursor: { + argLeftOffset: 27, + argRightOffset: 39, + isCursorInQuotes: false, + nextCursorChar: '', + prevCursorChar: 'S' + } + } + }, + { + input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS ', 40], + result: { + args: [['FT.SEARCH', '"idx:bicycle"', '""', 'WITHSORTKEYS'], []], + cursor: { + argLeftOffset: 0, + argRightOffset: 0, + isCursorInQuotes: false, + nextCursorChar: '', + prevCursorChar: '' + } + } + }, + { + input: ['FT.SEARCH "idx:bicycle \\" \\"" "" WITHSORTKEYS ', 46], + result: { + args: [['FT.SEARCH', '"idx:bicycle " ""', '""', 'WITHSORTKEYS'], []], + cursor: { + argLeftOffset: 0, + argRightOffset: 0, + isCursorInQuotes: false, + nextCursorChar: '', + prevCursorChar: '' + } + } + } +] + +describe('splitQueryByArgs', () => { + it.each(splitQueryByArgsTests)('should return for %input proper result', ({ input, result }) => { + const testResult = splitQueryByArgs(...input) + expect(testResult).toEqual(result) + }) +}) From 61b151a38b31e004e67cb28b3b8be60a551e46ba Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Tue, 15 Oct 2024 15:48:47 +0200 Subject: [PATCH 095/112] update existing tests --- tests/e2e/pageObjects/workbench-page.ts | 2 + .../workbench/autocomplete.e2e.ts | 23 +++++---- .../workbench/command-results.e2e.ts | 3 +- .../workbench/scripting-area.e2e.ts | 10 ++-- .../insights/live-recommendations.e2e.ts | 2 +- .../search-and-query-tab.e2e.ts | 47 ++++++++++++------- .../regression/workbench/autocomplete.e2e.ts | 8 ++-- .../workbench/editor-cleanup.e2e.ts | 1 + .../workbench/workbench-pipeline.e2e.ts | 7 ++- 9 files changed, 60 insertions(+), 43 deletions(-) diff --git a/tests/e2e/pageObjects/workbench-page.ts b/tests/e2e/pageObjects/workbench-page.ts index 60ba3cea29..4666e9c03d 100644 --- a/tests/e2e/pageObjects/workbench-page.ts +++ b/tests/e2e/pageObjects/workbench-page.ts @@ -113,6 +113,7 @@ export class WorkbenchPage extends InstancePage { for (const command of commands) { await t .typeText(this.queryInput, command, { replace: false, speed: 1, paste: true }) + .pressKey('esc') .pressKey('enter'); } await t.click(this.submitCommandButton); @@ -195,6 +196,7 @@ export class WorkbenchPage extends InstancePage { await t.typeText(this.queryInput, value, { replace: false }); // Select query option into autosuggest and go out of quotes await t.pressKey('tab'); + await t.pressKey('tab'); await t.pressKey('right'); await t.pressKey('space'); } diff --git a/tests/e2e/tests/web/critical-path/workbench/autocomplete.e2e.ts b/tests/e2e/tests/web/critical-path/workbench/autocomplete.e2e.ts index 31968f0f40..6a50246372 100644 --- a/tests/e2e/tests/web/critical-path/workbench/autocomplete.e2e.ts +++ b/tests/e2e/tests/web/critical-path/workbench/autocomplete.e2e.ts @@ -36,15 +36,15 @@ test('Verify that when user have selected a command (via “Enter” from the li await t.pressKey('enter'); const script = await workbenchPage.queryInputScriptArea.textContent; // Verify that user can select a command from the list with auto-suggestions when type in any character in the Editor - await t.expect(script.replace(/\s/g, ' ')).contains('LINDEX ', 'Result of sent command exists'); + await t.expect(script.replace(/\s/g, ' ')).eql('LINDEX ', 'Result of sent command not exists'); - // Check the required arguments inserted + // Check the required arguments suggested for (const argument of commandArguments) { - await t.expect(script).contains(argument, `The required argument ${argument} is inserted`); + await t.expect(workbenchPage.MonacoEditor.monacoHintWithArguments.textContent).contains(argument, `The required argument ${argument} is not suggested`); } }); test('Verify that user can change any required argument inserted', async t => { - const command = 'HMGET'; + const command = 'HMGE'; const commandArguments = [ 'key', 'field' @@ -54,7 +54,7 @@ test('Verify that user can change any required argument inserted', async t => { 'secondArgument' ]; - // Select command via Enter + // Select HMGET command via Enter await t.typeText(workbenchPage.queryInput, command, { replace: true }); await t.pressKey('enter'); // Change required arguments @@ -68,15 +68,18 @@ test('Verify that user can change any required argument inserted', async t => { await t.expect(scriptAfterEdit).notContains(commandArguments[0], `The argument ${commandArguments[0]} is not changed`); }); test('Verify that the list of optional arguments will not be inserted with autocomplete', async t => { - const command = 'ZPOPMAX'; + const command = 'ZPOPMA'; const commandRequiredArgument = 'key'; - const commandOptionalArgument = 'count'; + const commandOptionalArgument = '[count]'; - // Select command via Enter + // Select ZPOPMAX command via Enter await t.typeText(workbenchPage.queryInput, command, { replace: true }); await t.pressKey('enter'); // Verify the command arguments inserted const script = await workbenchPage.queryInputScriptArea.textContent; - await t.expect(script).contains(commandRequiredArgument, 'The required argument is inserted'); - await t.expect(script).notContains(commandOptionalArgument, 'The optional argument is not inserted'); + await t.expect(script.replace(/\s/g, ' ')).eql('ZPOPMAX ', 'Result of sent command not exists'); + + // Check the required and optional arguments suggested + await t.expect(workbenchPage.MonacoEditor.monacoHintWithArguments.textContent).contains(commandRequiredArgument, `The required argument is not suggested`); + await t.expect(workbenchPage.MonacoEditor.monacoHintWithArguments.textContent).contains(commandOptionalArgument, `The optional argument is not suggested in blocks`); }); diff --git a/tests/e2e/tests/web/critical-path/workbench/command-results.e2e.ts b/tests/e2e/tests/web/critical-path/workbench/command-results.e2e.ts index 2bcd079210..a7ccd3a7f1 100644 --- a/tests/e2e/tests/web/critical-path/workbench/command-results.e2e.ts +++ b/tests/e2e/tests/web/critical-path/workbench/command-results.e2e.ts @@ -175,7 +175,8 @@ test await t .click(workbenchPage.queryInput) .pressKey('ctrl+a') - .pressKey('delete'); + .pressKey('delete') + .pressKey('esc'); // Verify the quick access to command history by up button for (const command of commands.reverse()) { await t.pressKey('up'); diff --git a/tests/e2e/tests/web/critical-path/workbench/scripting-area.e2e.ts b/tests/e2e/tests/web/critical-path/workbench/scripting-area.e2e.ts index ea74348a0a..afee921100 100644 --- a/tests/e2e/tests/web/critical-path/workbench/scripting-area.e2e.ts +++ b/tests/e2e/tests/web/critical-path/workbench/scripting-area.e2e.ts @@ -112,9 +112,9 @@ test('Verify that user can run one command in multiple lines in Workbench page', 'ON HASH PREFIX 1 product:', 'SCHEMA price NUMERIC SORTABLE' ]; - //Send command in multiple lines + // Send command in multiple lines await workbenchPage.sendCommandInWorkbench(multipleLinesCommand.join('\n\t'), 0.5); - //Check the result + // Check the result const resultCommand = await workbenchPage.queryCardCommand.nth(0).textContent; for(const commandPart of multipleLinesCommand) { await t.expect(resultCommand).contains(commandPart, 'The multiple lines command is in the result'); @@ -126,12 +126,12 @@ test('Verify that user can use one indent to indicate command in several lines i `FT.CREATE ${indexName}`, 'ON HASH PREFIX 1 product: SCHEMA price NUMERIC SORTABLE' ]; - //Send command in multiple lines + // Send command in multiple lines await t.typeText(workbenchPage.queryInput, multipleLinesCommand[0]); - await t.pressKey('enter tab'); + await t.pressKey('enter esc tab'); await t.typeText(workbenchPage.queryInput, multipleLinesCommand[1]); await t.click(workbenchPage.submitCommandButton); - //Check the result + // Check the result const resultCommand = await workbenchPage.queryCardCommand.nth(0).textContent; for(const commandPart of multipleLinesCommand) { await t.expect(resultCommand).contains(commandPart, 'The multiple lines command is in the result'); diff --git a/tests/e2e/tests/web/regression/insights/live-recommendations.e2e.ts b/tests/e2e/tests/web/regression/insights/live-recommendations.e2e.ts index a2bf9b3db3..ccf8a82447 100644 --- a/tests/e2e/tests/web/regression/insights/live-recommendations.e2e.ts +++ b/tests/e2e/tests/web/regression/insights/live-recommendations.e2e.ts @@ -246,7 +246,7 @@ test //Verify that user is navigated to DB Analysis page via Analyze button and new report is generated await t.click(memoryEfficiencyPage.selectedReport); await t.expect(memoryEfficiencyPage.reportItem.visible).ok('Database analysis page not opened'); - await t.click(memoryEfficiencyPage.NavigationPanel.workbenchButton); + await t.click(memoryEfficiencyPage.NavigationPanel.browserButton); await workbenchPage.NavigationHeader.togglePanel(true); tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tips); await t.click(tab.analyzeDatabaseLink); diff --git a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts index 494b74aa5c..d01568a958 100644 --- a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts +++ b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts @@ -51,7 +51,7 @@ test('Verify that tutorials can be opened from Workbench', async t => { await t.click(workbenchPage.getTutorialLinkLocator('sq-intro')); await t.expect(workbenchPage.InsightsPanel.sidePanel.exists).ok('Insight panel is not opened'); const tab = await browserPage.InsightsPanel.setActiveTab(ExploreTabs.Tutorials); - await t.expect(tab.preselectArea.textContent).contains('EXACT MATCH', 'the tutorial page is incorrect'); + await t.expect(tab.preselectArea.textContent).contains('INTRODUCTION', 'the tutorial page is incorrect'); }); test('Verify that user can use show more to see command fully in 2nd tooltip', async t => { const commandDetails = [ @@ -92,12 +92,13 @@ test('Verify full commands suggestions with index and query for FT.AGGREGATE', a await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withText('FT.AGGREGATE').exists).ok('FT.AGGREGATE auto-suggestions are not displayed'); // Select command and check result + await t.typeText(workbenchPage.queryInput, '.AG', { replace: false }); await t.pressKey('enter'); let script = await workbenchPage.queryInputScriptArea.textContent; await t.expect(script.replace(/\s/g, ' ')).contains('FT.AGGREGATE ', 'Result of sent command exists'); // Verify that user can see the list of all the indexes in database when put a space after only FT.SEARCH and FT.AGGREGATE commands - await t.expect(script.replace(/\s/g, ' ')).contains(`"${indexName1}" "" `, 'Index not suggested into input'); + await t.expect(script.replace(/\s/g, ' ')).contains(`'${indexName1}' 'query to search' `, 'Index not suggested into input'); await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText(indexName1).exists).ok('Index not auto-suggested'); await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText(indexName2).exists).ok('All indexes not auto-suggested'); @@ -115,10 +116,12 @@ test('Verify full commands suggestions with index and query for FT.AGGREGATE', a await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('city').exists).ok('Index field not auto-suggested after starting typing'); await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.count).eql(1, 'Wrong index fields suggested after typing first letter'); - // Verify contextual suggestions after typing letters for commands + // Go out of index field + await t.pressKey('tab'); await t.pressKey('tab'); await t.pressKey('right'); await t.pressKey('space'); + // Verify contextual suggestions after typing letters for commands await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('APPLY').exists).ok('FT.AGGREGATE arguments not suggested'); await t.typeText(workbenchPage.queryInput, 'g', { replace: false }); await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.nth(0).textContent).contains('GROUPBY', 'Argument not suggested after typing first letters'); @@ -141,23 +144,22 @@ test('Verify full commands suggestions with index and query for FT.AGGREGATE', a await t.typeText(workbenchPage.queryInput, 'stud', { replace: false }); await t.pressKey('space'); - await t.debug(); // Verify multiple argument option suggestions await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.nth(0).textContent).contains('REDUCE', 'Incorrect order of suggested arguments'); // Verify complex command sequences like nargs and properties are suggested accurately for GROUPBY - const expectedText = `FT.AGGREGATE "${indexName1}" "@city" GROUPBY 1 "London" REDUCE SUM 1 @students AS stud REDUCE`.trim().replace(/\s+/g, ' '); + const expectedText = `FT.AGGREGATE '${indexName1}' '@city:{tag} ' GROUPBY 1 "London" REDUCE SUM 1 @students AS stud REDUCE`.trim().replace(/\s+/g, ' '); await t.expect((await workbenchPage.queryInputForText.innerText).trim().replace(/\s+/g, ' ')).contains(expectedText, 'Incorrect order of entered arguments'); }); test('Verify full commands suggestions with index and query for FT.SEARCH', async t => { - await t.typeText(workbenchPage.queryInput, 'FT.SE', { replace: true }); + await t.typeText(workbenchPage.queryInput, '', { replace: true }); // Select command and check result await t.pressKey('enter'); const script = await workbenchPage.queryInputScriptArea.textContent; await t.expect(script.replace(/\s/g, ' ')).contains('FT.SEARCH ', 'Result of sent command exists'); - await t.pressKey('tab'); + await t.pressKey('tab') // Select '@city' field - await workbenchPage.selectFieldUsingAutosuggest('city'); + await workbenchPage.selectFieldUsingAutosuggest('city') await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('DIALECT').exists).ok('FT.SEARCH arguments not suggested'); await t.typeText(workbenchPage.queryInput, 'n', { replace: false }); await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.nth(0).textContent).contains('NOCONTENT', 'Argument not suggested after typing first letters'); @@ -199,7 +201,7 @@ test('Verify full commands suggestions with index and query for FT.PROFILE(SEARC await workbenchPage.selectFieldUsingAutosuggest('city'); // Verify that there are no more suggestions await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.exists).notOk('Additional invalid commands suggested'); - const expectedText = `FT.PROFILE "${indexName1}" SEARCH QUERY "@city"`.trim().replace(/\s+/g, ' '); + const expectedText = `FT.PROFILE '${indexName1}' SEARCH QUERY '@city:{tag} '`.trim().replace(/\s+/g, ' '); // Verify command entered correctly await t.expect((await workbenchPage.queryInputForText.innerText).trim().replace(/\s+/g, ' ')).contains(expectedText, 'Incorrect order of entered arguments'); }); @@ -220,7 +222,7 @@ test('Verify full commands suggestions with index and query for FT.PROFILE(AGGRE await workbenchPage.selectFieldUsingAutosuggest('city'); // Verify that there are no more suggestions await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.exists).notOk('Additional invalid commands suggested'); - const expectedText = `FT.PROFILE "${indexName1}" AGGREGATE QUERY "@city"`.trim().replace(/\s+/g, ' '); + const expectedText = `FT.PROFILE '${indexName1}' AGGREGATE QUERY '@city:{tag} '`.trim().replace(/\s+/g, ' '); // Verify command entered correctly await t.expect((await workbenchPage.queryInputForText.innerText).trim().replace(/\s+/g, ' ')).contains(expectedText, 'Incorrect order of entered arguments'); }); @@ -238,7 +240,7 @@ test('Verify full commands suggestions with index and query for FT.EXPLAIN', asy // Verify that there are no more suggestions await t.pressKey('space'); await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.exists).notOk('Additional invalid commands suggested'); - const expectedText = `FT.EXPLAIN "${indexName1}" "@city" DIALECT dialectTest`.trim().replace(/\s+/g, ' '); + const expectedText = `FT.EXPLAIN '${indexName1}' '@city:{tag} ' DIALECT dialectTest`.trim().replace(/\s+/g, ' '); // Verify command entered correctly await t.expect((await workbenchPage.queryInputForText.innerText).trim().replace(/\s+/g, ' ')).contains(expectedText, 'Incorrect order of entered arguments'); }); @@ -259,11 +261,12 @@ test('Verify commands suggestions for APPLY and FILTER', async t => { await t.typeText(workbenchPage.queryInput, '@', { replace: false }); await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed'); await t.typeText(workbenchPage.queryInput, 'location', { replace: false }); - await t.typeText(workbenchPage.queryInput, ', \'40.7128,-74.0060\''); + await t.typeText(workbenchPage.queryInput, ', "40.7128,-74.0060"'); for (let i = 0; i < 3; i++) { await t.pressKey('right'); } await t.pressKey('space'); + await t.typeText(workbenchPage.queryInput, 'a'); await t.pressKey('tab'); await t.typeText(workbenchPage.queryInput, 'apply_key', { replace: false }); @@ -277,7 +280,6 @@ test('Verify commands suggestions for APPLY and FILTER', async t => { await t.pressKey('space'); await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('GROUPBY').exists).ok('query can not be prolong'); }); - test('Verify REDUCE commands', async t => { await t.typeText(workbenchPage.queryInput, `FT.AGGREGATE ${indexName1} "*" GROUPBY 1 @location`, { replace: true }); await t.pressKey('space'); @@ -288,6 +290,7 @@ test('Verify REDUCE commands', async t => { // set value of reduce await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.visible).ok('Suggestions not displayed'); + // Select COUNT await t.typeText(workbenchPage.queryInput, 'CO'); await t.pressKey('enter'); await t.typeText(workbenchPage.queryInput, '0'); @@ -325,7 +328,7 @@ test('Verify suggestions for fields', async t => { // verify suggestions for geo await t.typeText(workbenchPage.queryInput, 'l'); await t.pressKey('tab'); - await t.expect((await workbenchPage.MonacoEditor.getTextFromMonaco()).trim()).eql(`FT.AGGREGATE "${indexName1}" "@location:[lon lat radius unit]"`); + await t.expect((await workbenchPage.MonacoEditor.getTextFromMonaco()).trim()).eql(`FT.AGGREGATE '${indexName1}' '@location:[lon lat radius unit] '`); // verify for numeric await t.typeText(workbenchPage.queryInput, 'FT.AGGREGATE ', { replace: true }); @@ -336,9 +339,8 @@ test('Verify suggestions for fields', async t => { await t.typeText(workbenchPage.queryInput, '@'); await t.typeText(workbenchPage.queryInput, 's'); await t.pressKey('tab'); - await t.expect((await workbenchPage.MonacoEditor.getTextFromMonaco()).trim()).eql(`FT.AGGREGATE "${indexName1}" "@students:[range]"`); + await t.expect((await workbenchPage.MonacoEditor.getTextFromMonaco()).trim()).eql(`FT.AGGREGATE '${indexName1}' '@students:[range] '`); }); - test .after(async() => { // Clear and delete database @@ -349,7 +351,6 @@ test await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify commands suggestions for CREATE', async t => { await t.typeText(workbenchPage.queryInput, 'FT.CREATE ', { replace: true }); - await t.pressKey('enter'); // Verify that indexes are not suggested for FT.CREATE await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.exists).notOk('Existing index suggested'); @@ -376,5 +377,15 @@ test // Select SORTABLE await t.typeText(workbenchPage.queryInput, 'so', { replace: false }); await t.pressKey('tab'); - await t.expect((await workbenchPage.MonacoEditor.getTextFromMonaco()).trim()).eql(`FT.CREATE ${indexName3} FILTER filterNew SCHEMA field_name TEXT SORTABLE`); + + // Enter second field to SCHEMA + await t.typeText(workbenchPage.queryInput, 'field2_num', { replace: false }); + await t.pressKey('space'); + await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withExactText('NUMERIC').exists).ok('query can not be prolong'); + + // Select NUMERIC keyword + await t.typeText(workbenchPage.queryInput, 'so', { replace: false }); + await t.pressKey('tab'); + + await t.expect((await workbenchPage.MonacoEditor.getTextFromMonaco()).trim()).eql(`FT.CREATE ${indexName3} FILTER filterNew SCHEMA field_name TEXT SORTABLE field2_num NUMERIC`); }); diff --git a/tests/e2e/tests/web/regression/workbench/autocomplete.e2e.ts b/tests/e2e/tests/web/regression/workbench/autocomplete.e2e.ts index 6e52835a10..42fbe8fa00 100644 --- a/tests/e2e/tests/web/regression/workbench/autocomplete.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/autocomplete.e2e.ts @@ -22,7 +22,7 @@ fixture `Autocomplete for entered commands` await databaseAPIRequests.deleteStandaloneDatabaseApi(ossStandaloneConfig); }); test('Verify that user can open the "read more" about the command by clicking on the ">" icon or "ctrl+space"', async t => { - const command = 'HSET'; + const command = 'HSE'; const commandDetails = [ 'HSET key field value [field value ...]', 'Creates or modifies the value of a field in a hash.', @@ -66,7 +66,7 @@ test('Verify that user can see static list of arguments is displayed when he ent await t.expect(workbenchPage.MonacoEditor.monacoHintWithArguments.visible).notOk('Hints with arguments are still displayed'); }); test('Verify that user can see the static list of arguments when he uses “Ctrl+Shift+Space” combination for already entered command for Windows', async t => { - const command = 'JSON.ARRAPPEND'; + const command = 'JSON.ARRAPPEN'; await t.typeText(workbenchPage.queryInput, command, { replace: true }); // Verify that the list with auto-suggestions is displayed await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.exists).ok('Auto-suggestions are not displayed'); @@ -74,9 +74,9 @@ test('Verify that user can see the static list of arguments when he uses “Ctrl await t.pressKey('enter'); // Check that the command is displayed in Editing area after selecting const script = await workbenchPage.queryInputScriptArea.textContent; - await t.expect(script.replace(/\s/g, ' ')).eql('JSON.ARRAPPEND key value', 'Result of sent command not exists'); + await t.expect(script.replace(/\s/g, ' ')).eql('JSON.ARRAPPEND ', 'Result of sent command not exists'); // Check that hint with arguments are displayed - await t.expect(workbenchPage.MonacoEditor.monacoHintWithArguments.visible).ok('Hints with arguments are not displayed'); + await t.expect(workbenchPage.MonacoEditor.monacoHintWithArguments.textContent).contains('JSON.ARRAPPEND key [path] value', `The required argument is not suggested`); // Remove hints with arguments await t.pressKey('esc'); // Check no hints are displayed diff --git a/tests/e2e/tests/web/regression/workbench/editor-cleanup.e2e.ts b/tests/e2e/tests/web/regression/workbench/editor-cleanup.e2e.ts index 00a0e7908c..e8f9709587 100644 --- a/tests/e2e/tests/web/regression/workbench/editor-cleanup.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/editor-cleanup.e2e.ts @@ -50,6 +50,7 @@ test('Enabled Editor Cleanup toggle behavior', async t => { await workbenchPage.sendCommandInWorkbench(commandToSend); await workbenchPage.sendCommandInWorkbench(commandToSend); // Verify that Editor input is cleared after running command + await t.pressKey('esc'); await t.expect(await workbenchPage.queryInputScriptArea.textContent).eql('', 'Input in Editor is saved'); }); test diff --git a/tests/e2e/tests/web/regression/workbench/workbench-pipeline.e2e.ts b/tests/e2e/tests/web/regression/workbench/workbench-pipeline.e2e.ts index de0906c84d..0b2be65d70 100644 --- a/tests/e2e/tests/web/regression/workbench/workbench-pipeline.e2e.ts +++ b/tests/e2e/tests/web/regression/workbench/workbench-pipeline.e2e.ts @@ -72,12 +72,11 @@ test('Verify that user can interact with the Editor while command(s) in progress await settingsPage.changeCommandsInPipeline(pipelineValues[2]); // Go to Workbench page - await t.click(settingsPage.NavigationPanel.browserButton); await t.click(browserPage.NavigationPanel.workbenchButton); await workbenchPage.sendCommandInWorkbench(commandForSend); - await t.typeText(workbenchPage.queryInput, commandForSend, { replace: true, paste: true }); - await t.pressKey('enter'); - // 'Verify that user can interact with the Editor + await t.typeText(workbenchPage.queryInput, commandForSend, { replace: true }); + // await t.pressKey('enter'); + // Verify that user can interact with the Editor await t.expect(workbenchPage.queryInputScriptArea.textContent).contains(valueInEditor, { timeout: 5000 }); }); test('Verify that command results are added to history in order most recent - on top', async t => { From 098f474e9c658dc22a7ca1f43218c799e322f14d Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Tue, 15 Oct 2024 17:04:58 +0200 Subject: [PATCH 096/112] update --- .../regression/search-and-query/search-and-query-tab.e2e.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts index d01568a958..498a3c7c61 100644 --- a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts +++ b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts @@ -151,7 +151,7 @@ test('Verify full commands suggestions with index and query for FT.AGGREGATE', a await t.expect((await workbenchPage.queryInputForText.innerText).trim().replace(/\s+/g, ' ')).contains(expectedText, 'Incorrect order of entered arguments'); }); test('Verify full commands suggestions with index and query for FT.SEARCH', async t => { - await t.typeText(workbenchPage.queryInput, '', { replace: true }); + await t.typeText(workbenchPage.queryInput, 'FT.SEA', { replace: true }); // Select command and check result await t.pressKey('enter'); const script = await workbenchPage.queryInputScriptArea.textContent; @@ -341,7 +341,8 @@ test('Verify suggestions for fields', async t => { await t.pressKey('tab'); await t.expect((await workbenchPage.MonacoEditor.getTextFromMonaco()).trim()).eql(`FT.AGGREGATE '${indexName1}' '@students:[range] '`); }); -test +// Unskip after fixing https://redislabs.atlassian.net/browse/RI-6212 +test.skip .after(async() => { // Clear and delete database await apiKeyRequests.deleteKeyByNameApi(keyName, ossStandaloneConfig.databaseName); From 6c028595248c3f06aa0b862dfeb2334ce71a28f1 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Tue, 15 Oct 2024 19:02:30 +0200 Subject: [PATCH 097/112] remove opening by keyboard --- .../regression/search-and-query/no-indexes-suggestions.e2e.ts | 1 - .../web/regression/search-and-query/search-and-query-tab.e2e.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/e2e/tests/web/regression/search-and-query/no-indexes-suggestions.e2e.ts b/tests/e2e/tests/web/regression/search-and-query/no-indexes-suggestions.e2e.ts index f8d39232be..ac2a27038c 100644 --- a/tests/e2e/tests/web/regression/search-and-query/no-indexes-suggestions.e2e.ts +++ b/tests/e2e/tests/web/regression/search-and-query/no-indexes-suggestions.e2e.ts @@ -24,7 +24,6 @@ test })('Verify suggestions when there are no indexes', async t => { await t.click(browserPage.NavigationPanel.workbenchButton); - await t.pressKey('ctrl+space'); await t.typeText(workbenchPage.queryInput, 'FT.SE', { replace: true }); await t.pressKey('tab'); diff --git a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts index 498a3c7c61..2bbb08ab01 100644 --- a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts +++ b/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts @@ -37,7 +37,6 @@ fixture `Autocomplete for entered commands in search and query` // Create 3 keys and index await browserPage.Cli.sendCommandsInCli(commands); await t.click(browserPage.NavigationPanel.workbenchButton); - await t.pressKey('ctrl+space'); }) .afterEach(async() => { // Clear and delete database From 43ba936861b33620dd6e02094f7e4b7ddbaa1381 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Wed, 16 Oct 2024 10:41:30 +0200 Subject: [PATCH 098/112] #RI-6221 - fix arguments in details --- .../ui/src/pages/workbench/components/query/Query/Query.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx b/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx index e9fdbce023..82200d2e5d 100644 --- a/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx +++ b/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx @@ -105,6 +105,7 @@ const Query = (props: Props) => { const { theme } = useContext(ThemeContext) const monacoObjects = useRef>(null) + // TODO: need refactor to avoid this const REDIS_COMMANDS = commands.map((command) => ({ ...addOwnTokenToArgs(command.name!, command) })) const { instanceId = '' } = useParams<{ instanceId: string }>() @@ -538,11 +539,11 @@ const Query = (props: Props) => { if (position.column === 1) { helpWidgetRef.current.isOpen = false if (command) return asSuggestionsRef([]) - return asSuggestionsRef(getCommandsSuggestions(REDIS_COMMANDS, range), false) + return asSuggestionsRef(getCommandsSuggestions(commands, range), false) } if (!command) { - return asSuggestionsRef(getCommandsSuggestions(REDIS_COMMANDS, range), false) + return asSuggestionsRef(getCommandsSuggestions(commands, range), false) } const { allArgs, args, cursor } = command From f054a0458dd0ff433aef60fe41a597d129e0f52c Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Wed, 16 Oct 2024 15:38:42 +0200 Subject: [PATCH 099/112] #RI-6222 - fix auto-suggest at first column * start refactoring --- .../components/query/Query/Query.tsx | 27 +++++-- .../components/query/QueryWrapper.tsx | 7 +- .../ui/src/pages/workbench/constants.ts | 8 --- .../ui/src/pages/workbench/utils/query.ts | 24 +++---- .../src/pages/workbench/utils/queryUtils.ts | 17 +++++ .../pages/workbench/utils/query_refactor.ts | 71 +++++++++++++++++++ .../workbench/utils/searchSuggestions.ts | 2 +- .../pages/workbench/utils/tests/query.spec.ts | 4 +- 8 files changed, 130 insertions(+), 30 deletions(-) create mode 100644 redisinsight/ui/src/pages/workbench/utils/queryUtils.ts create mode 100644 redisinsight/ui/src/pages/workbench/utils/query_refactor.ts diff --git a/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx b/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx index 82200d2e5d..67059c2088 100644 --- a/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx +++ b/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useRef, useState } from 'react' +import React, { useContext, useEffect, useMemo, useRef, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { compact, first } from 'lodash' import cx from 'classnames' @@ -32,7 +32,7 @@ import { addOwnTokenToArgs, findCurrentArgument, } from 'uiSrc/pages/workbench/u import { getRange, getRediSearchSignutureProvider, } from 'uiSrc/pages/workbench/utils/monaco' import { CursorContext } from 'uiSrc/pages/workbench/types' import { asSuggestionsRef, getCommandsSuggestions, isIndexComplete } from 'uiSrc/pages/workbench/utils/suggestions' -import { COMMANDS_TO_GET_INDEX_INFO, COMPOSITE_ARGS, EmptySuggestionsIds, } from 'uiSrc/pages/workbench/constants' +import { COMMANDS_TO_GET_INDEX_INFO, EmptySuggestionsIds, } from 'uiSrc/pages/workbench/constants' import { useDebouncedEffect } from 'uiSrc/services' import { fetchRedisearchInfoAction } from 'uiSrc/slices/browser/redisearch' import { findSuggestionsByArg } from 'uiSrc/pages/workbench/utils/searchSuggestions' @@ -106,7 +106,15 @@ const Query = (props: Props) => { const monacoObjects = useRef>(null) // TODO: need refactor to avoid this - const REDIS_COMMANDS = commands.map((command) => ({ ...addOwnTokenToArgs(command.name!, command) })) + const REDIS_COMMANDS = useMemo( + () => commands.map((command) => ({ ...addOwnTokenToArgs(command.name!, command) })), + [commands] + ) + + const COMPOSITE_ARGS = useMemo(() => commands + .filter((command) => command.name && command.name.includes(' ')) + .map(({ name }) => name), + [commands]) const { instanceId = '' } = useParams<{ instanceId: string }>() @@ -329,7 +337,13 @@ const Query = (props: Props) => { return } - const command = findCompleteQuery(model, e.position, REDIS_COMMANDS_SPEC, REDIS_COMMANDS_ARRAY, COMPOSITE_ARGS) + const command = findCompleteQuery( + model, + e.position, + REDIS_COMMANDS_SPEC, + REDIS_COMMANDS_ARRAY, + COMPOSITE_ARGS as string[] + ) handleSuggestions(editor, command) handleDslSyntax(e, command) } @@ -539,7 +553,7 @@ const Query = (props: Props) => { if (position.column === 1) { helpWidgetRef.current.isOpen = false if (command) return asSuggestionsRef([]) - return asSuggestionsRef(getCommandsSuggestions(commands, range), false) + return asSuggestionsRef(getCommandsSuggestions(commands, range), false, false) } if (!command) { @@ -569,7 +583,8 @@ const Query = (props: Props) => { helpWidgetRef.current = { isOpen, parent: parent || helpWidgetRef.current.parent, - currentArg: currentArg || helpWidgetRef.current.currentArg } + currentArg: currentArg || helpWidgetRef.current.currentArg + } } return suggestions diff --git a/redisinsight/ui/src/pages/workbench/components/query/QueryWrapper.tsx b/redisinsight/ui/src/pages/workbench/components/query/QueryWrapper.tsx index 033eabf914..f34b78ec2c 100644 --- a/redisinsight/ui/src/pages/workbench/components/query/QueryWrapper.tsx +++ b/redisinsight/ui/src/pages/workbench/components/query/QueryWrapper.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react' +import React, { useEffect, useMemo } from 'react' import { useDispatch, useSelector } from 'react-redux' import { EuiLoadingContent } from '@elastic/eui' @@ -40,7 +40,10 @@ const QueryWrapper = (props: Props) => { const { data: indexes = [] } = useSelector(redisearchListSelector) const { spec: COMMANDS_SPEC } = useSelector(appRedisCommandsSelector) - const REDIS_COMMANDS = mergeRedisCommandsSpecs(COMMANDS_SPEC, SEARCH_COMMANDS_SPEC) + const REDIS_COMMANDS = useMemo( + () => mergeRedisCommandsSpecs(COMMANDS_SPEC, SEARCH_COMMANDS_SPEC), + [COMMANDS_SPEC, SEARCH_COMMANDS_SPEC] + ) const dispatch = useDispatch() diff --git a/redisinsight/ui/src/pages/workbench/constants.ts b/redisinsight/ui/src/pages/workbench/constants.ts index 86cdf3045c..645b11baf4 100644 --- a/redisinsight/ui/src/pages/workbench/constants.ts +++ b/redisinsight/ui/src/pages/workbench/constants.ts @@ -83,14 +83,6 @@ export const COMMANDS_WITHOUT_INDEX_PROPOSE = [ 'FT.CREATE' ] -export const COMPOSITE_ARGS = [ - 'LOAD *', - 'FT.CONFIG GET', - 'FT.CONFIG SET', - 'FT.CURSOR DEL', - 'FT.CURSOR READ', -] - export enum DefinedArgumentName { index = 'index', query = 'query', diff --git a/redisinsight/ui/src/pages/workbench/utils/query.ts b/redisinsight/ui/src/pages/workbench/utils/query.ts index 9120c3d876..d3ffba6480 100644 --- a/redisinsight/ui/src/pages/workbench/utils/query.ts +++ b/redisinsight/ui/src/pages/workbench/utils/query.ts @@ -3,6 +3,7 @@ import { findLastIndex, isNumber, toNumber } from 'lodash' import { generateArgsNames, Maybe, Nullable } from 'uiSrc/utils' import { CommandProvider, IRedisCommand, IRedisCommandTree, ICommandTokenType } from 'uiSrc/constants' +import { isStringsEqual } from 'uiSrc/pages/workbench/utils/queryUtils' import { ArgName, FoundCommandArgument } from '../types' export const findCurrentArgument = ( @@ -20,8 +21,7 @@ export const findCurrentArgument = ( return findCurrentArgument(currentArg.arguments, prev.slice(i), prev, currentWithParent) } - const tokenIndex = args.findIndex((cArg) => - cArg.token?.toLowerCase() === arg.toLowerCase()) + const tokenIndex = args.findIndex((cArg) => isStringsEqual(cArg.token, arg)) const token = args[tokenIndex] if (token) { @@ -79,9 +79,9 @@ const findStopArgumentInQuery = ( } if (!isBlockedOnCommand && currentCommandArg?.optional) { - const isNotToken = currentCommandArg?.token && currentCommandArg.token !== arg.toUpperCase() + const isNotToken = currentCommandArg?.token && !isStringsEqual(currentCommandArg.token, arg) const isNotOneOfToken = !currentCommandArg?.token && currentCommandArg?.type === ICommandTokenType.OneOf - && currentCommandArg?.arguments?.every(({ token }) => token !== arg.toUpperCase()) + && currentCommandArg?.arguments?.every(({ token }) => !isStringsEqual(token, arg)) if (isNotToken || isNotOneOfToken) { moveToNextCommandArg() @@ -99,8 +99,8 @@ const findStopArgumentInQuery = ( blockArguments = Array(nArgs).fill(currentCommandArg.arguments).flat() } - const currentQueryArg = queryArgs.slice(i)?.[0]?.toUpperCase() - const isBlockHasToken = blockArguments?.[0]?.token === currentQueryArg + const currentQueryArg = queryArgs.slice(i)?.[0] + const isBlockHasToken = isStringsEqual(blockArguments?.[0]?.token, currentQueryArg) if (currentCommandArg.token && !isBlockHasToken && currentQueryArg) { blockArguments.unshift({ @@ -132,7 +132,7 @@ const findStopArgumentInQuery = ( } // if we are on token - that requires one more argument - if (currentCommandArg?.token === arg.toUpperCase()) { + if (isStringsEqual(currentCommandArg?.token, arg)) { blockCommand() continue } @@ -153,7 +153,7 @@ const findStopArgumentInQuery = ( if (currentCommandArg?.type === ICommandTokenType.OneOf && currentCommandArg?.optional) { // if oneof is optional then we can switch to another argument - if (!currentCommandArg?.arguments?.some(({ token }) => token === arg)) { + if (!currentCommandArg?.arguments?.some(({ token }) => isStringsEqual(token, arg))) { moveToNextCommandArg() } @@ -303,7 +303,7 @@ export const getAllRestArguments = ( const currentToken = current?.type === ICommandTokenType.Block ? current?.arguments?.[0].token : current?.token const lastTokenIndex = findLastIndex( untilTokenArgs, - (arg) => arg?.toLowerCase() === currentToken?.toLowerCase() + (arg) => isStringsEqual(arg, currentToken) ) const currentLvlNextArgs = removeNotSuggestedArgs( untilTokenArgs.slice(lastTokenIndex > 0 ? lastTokenIndex : 0), @@ -331,7 +331,7 @@ export const removeNotSuggestedArgs = (args: string[], commandArgs: IRedisComman if (arg.type === ICommandTokenType.OneOf) { return !args .some((queryArg) => arg.arguments - ?.some((oneOfArg) => oneOfArg.token?.toUpperCase() === queryArg.toUpperCase())) + ?.some((oneOfArg) => isStringsEqual(oneOfArg.token, queryArg))) } if (arg.type === ICommandTokenType.Block) { @@ -373,8 +373,8 @@ export const fillArgsByType = (args: IRedisCommand[], expandBlock = true): IRedi export const findArgByToken = (list: IRedisCommand[], arg: string): Maybe => list.find((cArg) => (cArg.type === ICommandTokenType.OneOf - ? cArg.arguments?.some((oneOfArg: IRedisCommand) => oneOfArg?.token?.toLowerCase() === arg?.toLowerCase()) - : cArg.arguments?.[0]?.token?.toLowerCase() === arg?.toLowerCase())) + ? cArg.arguments?.some((oneOfArg: IRedisCommand) => isStringsEqual(oneOfArg?.token, arg)) + : isStringsEqual(cArg.arguments?.[0].token, arg))) export const generateDetail = (command: Maybe) => { if (!command) return '' diff --git a/redisinsight/ui/src/pages/workbench/utils/queryUtils.ts b/redisinsight/ui/src/pages/workbench/utils/queryUtils.ts new file mode 100644 index 0000000000..72617c0d7c --- /dev/null +++ b/redisinsight/ui/src/pages/workbench/utils/queryUtils.ts @@ -0,0 +1,17 @@ +import { ICommandTokenType, IRedisCommand } from 'uiSrc/constants' +import { Maybe } from 'uiSrc/utils' + +export const isStringsEqual = (str1?: string, str2?: string) => str1?.toLowerCase() === str2?.toLowerCase() + +export const isTokenEqualsArg = (token: IRedisCommand, arg: string) => { + if (token.type === ICommandTokenType.OneOf) { + return token.arguments + ?.some((oneOfArg: IRedisCommand) => isStringsEqual(oneOfArg?.token, arg)) + } + if (isStringsEqual(token.token, arg)) return true + if (token.type === ICommandTokenType.Block) return isStringsEqual(token.arguments?.[0]?.token, arg) + return false +} + +export const findArgByToken = (list: IRedisCommand[], arg: string): Maybe => + list.find((command) => isTokenEqualsArg(command, arg)) diff --git a/redisinsight/ui/src/pages/workbench/utils/query_refactor.ts b/redisinsight/ui/src/pages/workbench/utils/query_refactor.ts new file mode 100644 index 0000000000..5166fa2841 --- /dev/null +++ b/redisinsight/ui/src/pages/workbench/utils/query_refactor.ts @@ -0,0 +1,71 @@ +/* eslint-disable no-continue */ +import { ICommandTokenType, IRedisCommand } from 'uiSrc/constants' +import { Maybe } from 'uiSrc/utils' +import { isStringsEqual, isTokenEqualsArg } from 'uiSrc/pages/workbench/utils/queryUtils' + +interface BlockTokensTree { + queryArgs: string[] + command?: IRedisCommand + parent?: BlockTokensTree +} + +export const findSuggestionsByQueryArgs = ( + commands: IRedisCommand[], + queryArgs: string[], +) => { + const firstQueryArg = queryArgs[0] + const scopeCommand = firstQueryArg + ? commands.find((command) => isStringsEqual(command.token, firstQueryArg)) + : undefined + + const getLastBlock = ( + args: string[], + command?: IRedisCommand, + parent?: any, + ): BlockTokensTree => { + for (let i = args.length - 1; i >= 0; i--) { + const arg = args[i] + const currentArg = findArgByToken(command?.arguments || [], arg) + + if (currentArg?.type === ICommandTokenType.Block) { + return getLastBlock(args.slice(i), currentArg, { queryArgs: queryArgs.slice(i), command: currentArg, parent }) + } + } + + return parent + } + + const blockToken: BlockTokensTree = { queryArgs: queryArgs.slice(scopeCommand ? 1 : 0), command: scopeCommand } + const currentBlock = getLastBlock(queryArgs, scopeCommand, blockToken) + const stopArgument = findStopArgumentWithSuggestions(currentBlock) + + console.log(stopArgument) + + return null +} + +const getStopArgument = ( + queryArgs: string[], + command: Maybe +) => { + let currentCommandArgIndex = 0 + + for (let i = 0; i < queryArgs.length; i++) { + const arg = queryArgs[i] + const currentCommandArg = command?.arguments?.[currentCommandArgIndex] + + currentCommandArgIndex++ + } + + return null +} + +const findStopArgumentWithSuggestions = (currentBlock: BlockTokensTree) => { + console.log(currentBlock) + const stopArgument = getStopArgument(currentBlock.queryArgs, currentBlock.command) + + return null +} + +const findArgByToken = (list: IRedisCommand[], arg: string): Maybe => + list.find((command) => isTokenEqualsArg(command, arg)) diff --git a/redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts b/redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts index 3ca7542f9a..09e60723ca 100644 --- a/redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts +++ b/redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts @@ -182,7 +182,7 @@ const handleCommonSuggestions = ( const shouldHideSuggestions = isCursorInQuotes || nextCursorChar || (prevCursorChar && isEscaped) if (shouldHideSuggestions) { return { - helpWidget: { isOpen: true, parent: foundArg?.parent, currentArg: foundArg?.stopArg }, + helpWidget: { isOpen: !!foundArg, parent: foundArg?.parent, currentArg: foundArg?.stopArg }, suggestions: asSuggestionsRef([]) } } diff --git a/redisinsight/ui/src/pages/workbench/utils/tests/query.spec.ts b/redisinsight/ui/src/pages/workbench/utils/tests/query.spec.ts index a3ab9fc6fd..0f705de920 100644 --- a/redisinsight/ui/src/pages/workbench/utils/tests/query.spec.ts +++ b/redisinsight/ui/src/pages/workbench/utils/tests/query.spec.ts @@ -2,7 +2,6 @@ import { SearchCommand, TokenType } from 'uiSrc/pages/search/types' import { Maybe, splitQueryByArgs } from 'uiSrc/utils' import { MOCKED_REDIS_COMMANDS } from 'uiSrc/mocks/data/mocked_redis_commands' import { IRedisCommand } from 'uiSrc/constants' -import { COMPOSITE_ARGS } from 'uiSrc/pages/workbench/constants' import { commonfindCurrentArgumentCases, findArgumentftAggreageTests, @@ -16,6 +15,9 @@ const COMMANDS = Object.keys(MOCKED_REDIS_COMMANDS).map((name) => ({ name, ...MOCKED_REDIS_COMMANDS[name] })) +const COMPOSITE_ARGS = COMMANDS + .filter((command) => command.name && command.name.includes(' ')) + .map(({ name }) => name) describe('findCurrentArgument', () => { describe('with list of commands', () => { From bb553adcf338ab46b39a33f59c25993e78570f7d Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Wed, 16 Oct 2024 15:46:56 +0200 Subject: [PATCH 100/112] fix pr comments --- .../ui/src/pages/workbench/utils/{queryUtils.ts => helpers.ts} | 0 redisinsight/ui/src/pages/workbench/utils/query.ts | 2 +- redisinsight/ui/src/pages/workbench/utils/query_refactor.ts | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename redisinsight/ui/src/pages/workbench/utils/{queryUtils.ts => helpers.ts} (100%) diff --git a/redisinsight/ui/src/pages/workbench/utils/queryUtils.ts b/redisinsight/ui/src/pages/workbench/utils/helpers.ts similarity index 100% rename from redisinsight/ui/src/pages/workbench/utils/queryUtils.ts rename to redisinsight/ui/src/pages/workbench/utils/helpers.ts diff --git a/redisinsight/ui/src/pages/workbench/utils/query.ts b/redisinsight/ui/src/pages/workbench/utils/query.ts index d3ffba6480..e0c633407f 100644 --- a/redisinsight/ui/src/pages/workbench/utils/query.ts +++ b/redisinsight/ui/src/pages/workbench/utils/query.ts @@ -3,7 +3,7 @@ import { findLastIndex, isNumber, toNumber } from 'lodash' import { generateArgsNames, Maybe, Nullable } from 'uiSrc/utils' import { CommandProvider, IRedisCommand, IRedisCommandTree, ICommandTokenType } from 'uiSrc/constants' -import { isStringsEqual } from 'uiSrc/pages/workbench/utils/queryUtils' +import { isStringsEqual } from './helpers' import { ArgName, FoundCommandArgument } from '../types' export const findCurrentArgument = ( diff --git a/redisinsight/ui/src/pages/workbench/utils/query_refactor.ts b/redisinsight/ui/src/pages/workbench/utils/query_refactor.ts index 5166fa2841..774892d4a3 100644 --- a/redisinsight/ui/src/pages/workbench/utils/query_refactor.ts +++ b/redisinsight/ui/src/pages/workbench/utils/query_refactor.ts @@ -1,7 +1,7 @@ /* eslint-disable no-continue */ import { ICommandTokenType, IRedisCommand } from 'uiSrc/constants' import { Maybe } from 'uiSrc/utils' -import { isStringsEqual, isTokenEqualsArg } from 'uiSrc/pages/workbench/utils/queryUtils' +import { isStringsEqual, isTokenEqualsArg } from './helpers' interface BlockTokensTree { queryArgs: string[] From 326723c014d65205e5507ae37963959987d18baa Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Thu, 17 Oct 2024 10:16:16 +0200 Subject: [PATCH 101/112] #RI-6228 - fix load all, composite args --- .../pages/workbench/components/query/Query/Query.tsx | 12 +++++++----- redisinsight/ui/src/pages/workbench/constants.ts | 4 ++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx b/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx index 67059c2088..6a8511b7e1 100644 --- a/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx +++ b/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx @@ -32,7 +32,7 @@ import { addOwnTokenToArgs, findCurrentArgument, } from 'uiSrc/pages/workbench/u import { getRange, getRediSearchSignutureProvider, } from 'uiSrc/pages/workbench/utils/monaco' import { CursorContext } from 'uiSrc/pages/workbench/types' import { asSuggestionsRef, getCommandsSuggestions, isIndexComplete } from 'uiSrc/pages/workbench/utils/suggestions' -import { COMMANDS_TO_GET_INDEX_INFO, EmptySuggestionsIds, } from 'uiSrc/pages/workbench/constants' +import { COMMANDS_TO_GET_INDEX_INFO, COMPOSITE_ARGS, EmptySuggestionsIds, } from 'uiSrc/pages/workbench/constants' import { useDebouncedEffect } from 'uiSrc/services' import { fetchRedisearchInfoAction } from 'uiSrc/slices/browser/redisearch' import { findSuggestionsByArg } from 'uiSrc/pages/workbench/utils/searchSuggestions' @@ -111,9 +111,11 @@ const Query = (props: Props) => { [commands] ) - const COMPOSITE_ARGS = useMemo(() => commands - .filter((command) => command.name && command.name.includes(' ')) - .map(({ name }) => name), + const compositeTokens = useMemo(() => + commands + .filter((command) => command.token && command.token.includes(' ')) + .map(({ token }) => token) + .concat(...COMPOSITE_ARGS), [commands]) const { instanceId = '' } = useParams<{ instanceId: string }>() @@ -342,7 +344,7 @@ const Query = (props: Props) => { e.position, REDIS_COMMANDS_SPEC, REDIS_COMMANDS_ARRAY, - COMPOSITE_ARGS as string[] + compositeTokens as string[] ) handleSuggestions(editor, command) handleDslSyntax(e, command) diff --git a/redisinsight/ui/src/pages/workbench/constants.ts b/redisinsight/ui/src/pages/workbench/constants.ts index 645b11baf4..1c732c67ce 100644 --- a/redisinsight/ui/src/pages/workbench/constants.ts +++ b/redisinsight/ui/src/pages/workbench/constants.ts @@ -83,6 +83,10 @@ export const COMMANDS_WITHOUT_INDEX_PROPOSE = [ 'FT.CREATE' ] +export const COMPOSITE_ARGS = [ + 'LOAD *', +] + export enum DefinedArgumentName { index = 'index', query = 'query', From e711c2a9d2555a66c7821710473f02cc24328b30 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Thu, 17 Oct 2024 10:30:48 +0200 Subject: [PATCH 102/112] add ui tests --- .../utils/tests/test-cases/common.ts | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts index f7a9ff23b8..cc7a9ea168 100644 --- a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts +++ b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts @@ -226,4 +226,120 @@ export const commonfindCurrentArgumentCases = [ appendIncludes: ['TAGS', 'SUMMARIZE', 'DIALECT', 'FILTER', 'WITHSCORES', 'INKEYS'], appendNotIncludes: ['FIELDS'], }, + { + input: 'FT.SEARCH index "*" SORTBY price ', + result: expect.any(Object), + appendIncludes: ['ASC', 'DESC', 'FILTER', 'LIMIT', 'DIALECT', 'WITHSCORES', 'INFIELDS'], + appendNotIncludes: ['SORTBY'], + }, + { + input: 'FT.SEARCH textVehicles "(-@make:Toyota)" FILTER @year 2021 2022 ', + result: expect.any(Object), + appendIncludes: ['FILTER', 'GEOFILTER', 'TIMEOUT', 'WITHSORTKEYS'], + appendNotIncludes: ['AS', 'ASC'], + }, + { + input: 'FT.SEARCH textVehicles "*" GEOFILTER geo_field lon lat radius ', + result: expect.any(Object), + appendIncludes: ['ft', 'km', 'm', 'mi'], + appendNotIncludes: ['SORTBY', 'FILTER', 'LIMIT', 'DIALECT', 'AS', 'ASC'], + }, + { + input: 'FT.SEARCH textVehicles "*" RETURN 2 test ', + result: expect.any(Object), + appendIncludes: ['AS'], + appendNotIncludes: ['SORTBY', 'FILTER', 'LIMIT', 'DIALECT', 'AS', 'ASC'], + }, + { + input: 'FT.CREATE textVehicles ON ', + result: expect.any(Object), + appendIncludes: ['HASH', 'JSON'], + appendNotIncludes: ['SORTBY', 'FILTER', 'LIMIT', 'DIALECT', 'WITHSCORES', 'INFIELDS'], + }, + { + input: 'FT.CREATE textVehicles SCHEMA make ', + result: expect.any(Object), + appendIncludes: ['AS', 'GEO', 'NUMERIC', 'TAG', 'TEXT', 'VECTOR'], + appendNotIncludes: ['FILTER', 'LIMIT', 'DIALECT', 'WITHSCORES', 'INFIELDS'], + }, + { + input: 'FT.AGGREGATE \'idx:articles\' \'@body:(term) \' APPLY \'test\' ', + result: expect.any(Object), + appendIncludes: ['AS'], + appendNotIncludes: ['REDUCE', 'APPLY', 'LOAD', 'SORTBY', 'GROUPBY'], + }, + { + input: 'FT.AGGREGATE \'idx:articles\' \'@body:(term) \' APPLY \'test\' AS test1', + result: expect.any(Object), + appendIncludes: ['APPLY', 'LOAD', 'SORTBY', 'GROUPBY'], + }, + { + input: 'FT.AGGREGATE \'idx:articles\' \'@body:(term) \' LOAD * ', + result: expect.any(Object), + appendIncludes: ['APPLY', 'LOAD', 'SORTBY', 'GROUPBY'], + }, + { + input: 'FT.AGGREGATE \'idx:articles\' \'@body:(term) \' SORTBY nargs property ', + result: expect.any(Object), + appendIncludes: ['ASC', 'DESC'], + appendNotIncludes: ['REDUCE', 'APPLY', 'LOAD', 'SORTBY', 'GROUPBY'], + }, + { + input: 'FT.AGGREGATE \'idx:articles\' \'@body:(term) \' SORTBY nargs property ASC ', + result: expect.any(Object), + appendIncludes: ['MAX', 'APPLY', 'LOAD', 'SORTBY', 'GROUPBY'], + }, + { + input: 'FT.AGGREGATE \'idx:articles\' \'@body:(term) \' PARAMS 4 name1 value1 name2 value2 ', + result: expect.any(Object), + appendIncludes: ['APPLY', 'LOAD', 'SORTBY', 'GROUPBY'], + appendNotIncludes: ['PARAMS', 'REDUCE'], + }, + { + input: 'FT.ALTER index SCHEMA ADD sdfsd fsdfsd ', + result: expect.any(Object), + appendIncludes: [], + appendNotIncludes: ['SKIPINITIALSCAN', 'ADD', 'SCHEMA'], + }, + { + input: 'FT.DROPINDEX \'vd\' ', + result: expect.any(Object), + appendIncludes: ['DD'], + }, + { + input: 'FT.EXPLAIN index query ', + result: expect.any(Object), + appendIncludes: ['DIALECT'], + appendNotIncludes: ['SKIPINITIALSCAN', 'ADD', 'SCHEMA', 'APPLY', 'LOAD', 'SORTBY', 'GROUPBY'], + }, + { + input: 'FT.EXPLAINCLI index query ', + result: expect.any(Object), + appendIncludes: ['DIALECT'], + appendNotIncludes: ['SKIPINITIALSCAN', 'ADD', 'SCHEMA', 'APPLY', 'LOAD', 'SORTBY', 'GROUPBY'], + }, + { + input: 'FT.INFO index ', + result: expect.any(Object), + appendIncludes: [], + appendNotIncludes: ['ADD', 'SCHEMA', 'APPLY', 'LOAD', 'SORTBY', 'GROUPBY'], + }, + { + input: 'FT.PROFILE \'idx:schools\' ', + result: expect.any(Object), + appendIncludes: ['AGGREGATE', 'SEARCH'], + appendNotIncludes: ['LIMITED'], + }, + { + input: 'FT.SPELLCHECK \'idx:articles\' \'test\' DIALECT dialect DISTANCE distance TERMS ', + result: expect.any(Object), + appendIncludes: ['EXCLUDE', 'INCLUDE'], + appendNotIncludes: ['DIALECT', 'DISTANCE', 'TERMS'], + }, + { + input: 'FT.SYNUPDATE \'idx:products\' synonym_group_id ', + result: expect.any(Object), + appendIncludes: ['SKIPINITIALSCAN'], + appendNotIncludes: ['DIALECT', 'DISTANCE', 'TERMS', 'INCLUDE', 'SCHEMA', 'APPLY', 'LOAD', 'SORTBY', 'GROUPBY'], + }, ] From 09ec2ee6b84e11e14a29c05601210cd829a38ad0 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Thu, 17 Oct 2024 10:49:54 +0200 Subject: [PATCH 103/112] fix --- .../ui/src/pages/workbench/utils/tests/test-cases/common.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts index cc7a9ea168..b39b3e5945 100644 --- a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts +++ b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts @@ -248,7 +248,7 @@ export const commonfindCurrentArgumentCases = [ input: 'FT.SEARCH textVehicles "*" RETURN 2 test ', result: expect.any(Object), appendIncludes: ['AS'], - appendNotIncludes: ['SORTBY', 'FILTER', 'LIMIT', 'DIALECT', 'AS', 'ASC'], + appendNotIncludes: ['SORTBY', 'FILTER', 'LIMIT', 'DIALECT', 'ASC'], }, { input: 'FT.CREATE textVehicles ON ', @@ -287,7 +287,8 @@ export const commonfindCurrentArgumentCases = [ { input: 'FT.AGGREGATE \'idx:articles\' \'@body:(term) \' SORTBY nargs property ASC ', result: expect.any(Object), - appendIncludes: ['MAX', 'APPLY', 'LOAD', 'SORTBY', 'GROUPBY'], + appendIncludes: ['MAX', 'APPLY', 'LOAD', 'GROUPBY'], + appendNotIncludes: ['SORTBY'], }, { input: 'FT.AGGREGATE \'idx:articles\' \'@body:(term) \' PARAMS 4 name1 value1 name2 value2 ', From 159f739b0f6d490ea5a8fa3e59ec611ce8470539 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Thu, 17 Oct 2024 17:05:41 +0200 Subject: [PATCH 104/112] #RRI-6172 - remove search page --- .../main-router/constants/defaultRoutes.ts | 6 - .../main-router/constants/redisStackRoutes.ts | 7 - .../monaco-laguages/MonacoLanguages.tsx | 5 +- .../navigation-menu/NavigationMenu.spec.tsx | 1 - .../navigation-menu/NavigationMenu.tsx | 17 - .../components/query/query-card/QueryCard.tsx | 10 +- .../QueryCardCliPlugin/QueryCardCliPlugin.tsx | 5 +- .../QueryCardHeader/QueryCardHeader.tsx | 69 +- .../components/code-block/CodeBlock.spec.tsx | 1 - .../components/code-block/CodeBlock.tsx | 2 - .../enablement-area/EnablementAreaWrapper.tsx | 2 - .../ui/src/pages/search/SearchPage.spec.tsx | 79 - .../ui/src/pages/search/SearchPage.tsx | 128 -- .../constants/supported_commands.json | 1156 ------------- .../ui/src/pages/search/components/index.ts | 7 - .../query-wrapper/QueryWrapper.spec.tsx | 91 - .../components/query-wrapper/QueryWrapper.tsx | 135 -- .../components/query-wrapper/constants.ts | 16 - .../search/components/query-wrapper/index.ts | 3 - .../query-wrapper/styles.module.scss | 76 - .../search/components/query/Query.spec.tsx | 10 - .../pages/search/components/query/Query.tsx | 359 ---- .../search/components/query/constants.ts | 45 - .../pages/search/components/query/index.ts | 3 - .../components/query/styles.module.scss | 0 .../search/components/query/utils.spec.ts | 173 -- .../pages/search/components/query/utils.ts | 201 --- .../results-history/ResultsHistory.spec.tsx | 157 -- .../results-history/ResultsHistory.tsx | 172 -- .../components/results-history/index.ts | 3 - .../results-history/styles.module.scss | 44 - redisinsight/ui/src/pages/search/index.ts | 3 - .../ui/src/pages/search/mocks/mocks.ts | 1521 ----------------- .../ui/src/pages/search/styles.module.scss | 32 - redisinsight/ui/src/pages/search/types.ts | 45 - .../ui/src/pages/search/utils/index.ts | 2 - .../ui/src/pages/search/utils/monaco.ts | 64 - .../ui/src/pages/search/utils/query.ts | 479 ------ .../pages/search/utils/tests/monaco.spec.ts | 60 - .../pages/search/utils/tests/query.spec.ts | 188 -- .../search/utils/tests/test-cases/common.ts | 183 -- .../utils/tests/test-cases/ft-aggregate.ts | 267 --- .../utils/tests/test-cases/ft-search.ts | 283 --- .../search/utils/tests/test-cases/index.ts | 3 - .../ui/src/pages/workbench/WorkbenchPage.tsx | 9 +- .../components/wb-view/WBViewWrapper.tsx | 3 +- .../workbench/utils/tests/monaco.spec.ts | 7 +- .../pages/workbench/utils/tests/query.spec.ts | 17 +- .../utils/tests/test-cases/common.ts | 13 +- .../ui/src/slices/interfaces/workbench.ts | 1 - .../slices/tests/workbench/wb-results.spec.ts | 26 - .../ui/src/slices/workbench/wb-results.ts | 20 +- redisinsight/ui/src/telemetry/events.ts | 12 - .../monaco/monarchTokens/redisearchTokens.ts | 99 -- .../monarchTokens/redisearchTokensSubRedis.ts | 7 +- .../redisearchTokensTemplates.ts | 6 +- .../ui/src/utils/monaco/redisearch/utils.ts | 38 +- .../src/utils/monaco/redisearch/utils_old.ts | 143 -- .../cyber/monarchTokensProvider.spec.ts | 14 +- .../ui/src/utils/tests/routing.spec.ts | 2 - 60 files changed, 84 insertions(+), 6446 deletions(-) delete mode 100644 redisinsight/ui/src/pages/search/SearchPage.spec.tsx delete mode 100644 redisinsight/ui/src/pages/search/SearchPage.tsx delete mode 100644 redisinsight/ui/src/pages/search/components/constants/supported_commands.json delete mode 100644 redisinsight/ui/src/pages/search/components/index.ts delete mode 100644 redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.spec.tsx delete mode 100644 redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.tsx delete mode 100644 redisinsight/ui/src/pages/search/components/query-wrapper/constants.ts delete mode 100644 redisinsight/ui/src/pages/search/components/query-wrapper/index.ts delete mode 100644 redisinsight/ui/src/pages/search/components/query-wrapper/styles.module.scss delete mode 100644 redisinsight/ui/src/pages/search/components/query/Query.spec.tsx delete mode 100644 redisinsight/ui/src/pages/search/components/query/Query.tsx delete mode 100644 redisinsight/ui/src/pages/search/components/query/constants.ts delete mode 100644 redisinsight/ui/src/pages/search/components/query/index.ts delete mode 100644 redisinsight/ui/src/pages/search/components/query/styles.module.scss delete mode 100644 redisinsight/ui/src/pages/search/components/query/utils.spec.ts delete mode 100644 redisinsight/ui/src/pages/search/components/query/utils.ts delete mode 100644 redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.spec.tsx delete mode 100644 redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.tsx delete mode 100644 redisinsight/ui/src/pages/search/components/results-history/index.ts delete mode 100644 redisinsight/ui/src/pages/search/components/results-history/styles.module.scss delete mode 100644 redisinsight/ui/src/pages/search/index.ts delete mode 100644 redisinsight/ui/src/pages/search/mocks/mocks.ts delete mode 100644 redisinsight/ui/src/pages/search/styles.module.scss delete mode 100644 redisinsight/ui/src/pages/search/types.ts delete mode 100644 redisinsight/ui/src/pages/search/utils/index.ts delete mode 100644 redisinsight/ui/src/pages/search/utils/monaco.ts delete mode 100644 redisinsight/ui/src/pages/search/utils/query.ts delete mode 100644 redisinsight/ui/src/pages/search/utils/tests/monaco.spec.ts delete mode 100644 redisinsight/ui/src/pages/search/utils/tests/query.spec.ts delete mode 100644 redisinsight/ui/src/pages/search/utils/tests/test-cases/common.ts delete mode 100644 redisinsight/ui/src/pages/search/utils/tests/test-cases/ft-aggregate.ts delete mode 100644 redisinsight/ui/src/pages/search/utils/tests/test-cases/ft-search.ts delete mode 100644 redisinsight/ui/src/pages/search/utils/tests/test-cases/index.ts delete mode 100644 redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts delete mode 100644 redisinsight/ui/src/utils/monaco/redisearch/utils_old.ts diff --git a/redisinsight/ui/src/components/main-router/constants/defaultRoutes.ts b/redisinsight/ui/src/components/main-router/constants/defaultRoutes.ts index 600f89c5e5..030df4cf24 100644 --- a/redisinsight/ui/src/components/main-router/constants/defaultRoutes.ts +++ b/redisinsight/ui/src/components/main-router/constants/defaultRoutes.ts @@ -16,7 +16,6 @@ import RdiPage from 'uiSrc/pages/rdi/home' import RdiInstancePage from 'uiSrc/pages/rdi/instance' import RdiStatisticsPage from 'uiSrc/pages/rdi/statistics' import PipelineManagementPage from 'uiSrc/pages/rdi/pipeline-management' -import SearchPage from 'uiSrc/pages/search' import { ANALYTICS_ROUTES, RDI_PIPELINE_MANAGEMENT_ROUTES } from './sub-routes' import COMMON_ROUTES from './commonRoutes' @@ -32,11 +31,6 @@ const INSTANCE_ROUTES: IRoute[] = [ path: Pages.workbench(':instanceId'), component: WorkbenchPage, }, - { - pageName: PageNames.search, - path: Pages.search(':instanceId'), - component: SearchPage, - }, { pageName: PageNames.pubSub, path: Pages.pubSub(':instanceId'), diff --git a/redisinsight/ui/src/components/main-router/constants/redisStackRoutes.ts b/redisinsight/ui/src/components/main-router/constants/redisStackRoutes.ts index 9697c50110..88502af3bc 100644 --- a/redisinsight/ui/src/components/main-router/constants/redisStackRoutes.ts +++ b/redisinsight/ui/src/components/main-router/constants/redisStackRoutes.ts @@ -9,7 +9,6 @@ import EditConnection from 'uiSrc/pages/redis-stack/components/edit-connection' import ClusterDetailsPage from 'uiSrc/pages/cluster-details' import AnalyticsPage from 'uiSrc/pages/analytics' import DatabaseAnalysisPage from 'uiSrc/pages/database-analysis' -import SearchPage from 'uiSrc/pages/search' import COMMON_ROUTES from './commonRoutes' const ANALYTICS_ROUTES: IRoute[] = [ @@ -46,12 +45,6 @@ const INSTANCE_ROUTES: IRoute[] = [ path: Pages.workbench(':instanceId'), component: WorkbenchPage, }, - { - pageName: PageNames.search, - protected: true, - path: Pages.search(':instanceId'), - component: SearchPage, - }, { pageName: PageNames.pubSub, protected: true, diff --git a/redisinsight/ui/src/components/monaco-laguages/MonacoLanguages.tsx b/redisinsight/ui/src/components/monaco-laguages/MonacoLanguages.tsx index 568775e2d4..f5cc4c1003 100644 --- a/redisinsight/ui/src/components/monaco-laguages/MonacoLanguages.tsx +++ b/redisinsight/ui/src/components/monaco-laguages/MonacoLanguages.tsx @@ -3,7 +3,7 @@ import { useSelector } from 'react-redux' import { monaco } from 'react-monaco-editor' import { findIndex } from 'lodash' import { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands' -import { MonacoLanguage, redisLanguageConfig, Theme } from 'uiSrc/constants' +import { MonacoLanguage, redisLanguageConfig, Theme, IRedisCommandTree } from 'uiSrc/constants' import { getRedisMonarchTokensProvider } from 'uiSrc/utils' import { darkTheme, lightTheme, MonacoThemes } from 'uiSrc/constants/monaco' import { ThemeContext } from 'uiSrc/contexts/themeContext' @@ -11,7 +11,6 @@ import { ThemeContext } from 'uiSrc/contexts/themeContext' import { getRediSearchSubRedisMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokensSubRedis' import SEARCH_COMMANDS_SPEC from 'uiSrc/pages/workbench/data/supported_commands.json' import { mergeRedisCommandsSpecs } from 'uiSrc/utils/transformers/redisCommands' -import { SearchCommandTree } from 'uiSrc/pages/search/types' import { ModuleCommandPrefix } from 'uiSrc/pages/workbench/constants' const MonacoLanguages = () => { @@ -44,7 +43,7 @@ const MonacoLanguages = () => { } monaco.languages.setLanguageConfiguration(MonacoLanguage.Redis, redisLanguageConfig) - const REDIS_COMMANDS = mergeRedisCommandsSpecs(COMMANDS_SPEC, SEARCH_COMMANDS_SPEC) as SearchCommandTree[] + const REDIS_COMMANDS = mergeRedisCommandsSpecs(COMMANDS_SPEC, SEARCH_COMMANDS_SPEC) as IRedisCommandTree[] const REDIS_SEARCH_COMMANDS = REDIS_COMMANDS.filter(({ name }) => name?.startsWith(ModuleCommandPrefix.RediSearch)) monaco.languages.setMonarchTokensProvider( diff --git a/redisinsight/ui/src/components/navigation-menu/NavigationMenu.spec.tsx b/redisinsight/ui/src/components/navigation-menu/NavigationMenu.spec.tsx index c8a07c32a4..3e964542a5 100644 --- a/redisinsight/ui/src/components/navigation-menu/NavigationMenu.spec.tsx +++ b/redisinsight/ui/src/components/navigation-menu/NavigationMenu.spec.tsx @@ -129,7 +129,6 @@ describe('NavigationMenu', () => { expect(screen.getByTestId('browser-page-btn')).toBeTruthy() expect(screen.getByTestId('workbench-page-btn')).toBeTruthy() - expect(screen.getByTestId('search-page-btn')).toBeTruthy() }) it('should render public routes', () => { diff --git a/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx b/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx index ef97859d0d..9711375054 100644 --- a/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx +++ b/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx @@ -26,8 +26,6 @@ import BrowserSVG from 'uiSrc/assets/img/sidebar/browser.svg' import BrowserActiveSVG from 'uiSrc/assets/img/sidebar/browser_active.svg' import WorkbenchSVG from 'uiSrc/assets/img/sidebar/workbench.svg' import WorkbenchActiveSVG from 'uiSrc/assets/img/sidebar/workbench_active.svg' -import SearchSVG from 'uiSrc/assets/img/sidebar/search.svg' -import SearchActiveSVG from 'uiSrc/assets/img/sidebar/search_active.svg' import SlowLogSVG from 'uiSrc/assets/img/sidebar/slowlog.svg' import SlowLogActiveSVG from 'uiSrc/assets/img/sidebar/slowlog_active.svg' import PubSubSVG from 'uiSrc/assets/img/sidebar/pubsub.svg' @@ -124,21 +122,6 @@ const NavigationMenu = () => { }, onboard: ONBOARDING_FEATURES.BROWSER_PAGE }, - { - tooltipText: 'Search and Query', - pageName: PageNames.search, - ariaLabel: 'Search and Query page button', - onClick: () => handleGoPage(Pages.search(connectedInstanceId)), - dataTestId: 'search-page-btn', - connectedInstanceId, - isActivePage: activePage === `/${PageNames.search}`, - getClassName() { - return cx(styles.navigationButton, { [styles.active]: this.isActivePage }) - }, - getIconType() { - return this.isActivePage ? SearchSVG : SearchActiveSVG - }, - }, { tooltipText: 'Workbench', pageName: PageNames.workbench, diff --git a/redisinsight/ui/src/components/query/query-card/QueryCard.tsx b/redisinsight/ui/src/components/query/query-card/QueryCard.tsx index 29b9192bbe..3f778e6859 100644 --- a/redisinsight/ui/src/components/query/query-card/QueryCard.tsx +++ b/redisinsight/ui/src/components/query/query-card/QueryCard.tsx @@ -6,7 +6,7 @@ import { useParams } from 'react-router-dom' import { isNull } from 'lodash' import { DEFAULT_TEXT_VIEW_TYPE, ProfileQueryType, WBQueryType } from 'uiSrc/pages/workbench/constants' -import { CommandExecutionType, ResultsMode, ResultsSummary, RunQueryMode } from 'uiSrc/slices/interfaces/workbench' +import { ResultsMode, ResultsSummary, RunQueryMode } from 'uiSrc/slices/interfaces/workbench' import { getVisualizationsByCommand, getWBQueryType, isGroupResults, isSilentModeWithoutError, Maybe, } from 'uiSrc/utils' import { appPluginsSelector } from 'uiSrc/slices/app/plugins' import { CommandExecutionResult, IPluginVisualization } from 'uiSrc/slices/interfaces' @@ -30,7 +30,6 @@ export interface Props { activeResultsMode?: ResultsMode resultsMode?: ResultsMode emptyCommand?: boolean - executionType?: CommandExecutionType summary?: ResultsSummary createdAt?: Date loading?: boolean @@ -68,7 +67,6 @@ const QueryCard = (props: Props) => { mode, activeResultsMode, resultsMode, - executionType = CommandExecutionType.Workbench, summary, isOpen, createdAt, @@ -113,9 +111,7 @@ const QueryCard = (props: Props) => { const toggleFullScreen = () => { setIsFullScreen((isFull) => { sendEventTelemetry({ - event: executionType === CommandExecutionType.Search - ? TelemetryEvent.SEARCH_RESULTS_IN_FULL_SCREEN - : TelemetryEvent.WORKBENCH_RESULTS_IN_FULL_SCREEN, + event: TelemetryEvent.WORKBENCH_RESULTS_IN_FULL_SCREEN, eventData: { databaseId: instanceId, state: isFull ? 'Close' : 'Open' @@ -182,7 +178,6 @@ const QueryCard = (props: Props) => { mode={mode} resultsMode={resultsMode} activeResultsMode={activeResultsMode} - executionType={executionType} emptyCommand={emptyCommand} summary={summary} summaryText={getSummaryText(summary, resultsMode)} @@ -223,7 +218,6 @@ const QueryCard = (props: Props) => { result={result} query={command} mode={mode} - executionType={executionType} setMessage={setMessage} commandId={id} /> diff --git a/redisinsight/ui/src/components/query/query-card/QueryCardCliPlugin/QueryCardCliPlugin.tsx b/redisinsight/ui/src/components/query/query-card/QueryCardCliPlugin/QueryCardCliPlugin.tsx index 629b849408..edb4cdc390 100644 --- a/redisinsight/ui/src/components/query/query-card/QueryCardCliPlugin/QueryCardCliPlugin.tsx +++ b/redisinsight/ui/src/components/query/query-card/QueryCardCliPlugin/QueryCardCliPlugin.tsx @@ -7,7 +7,7 @@ import { pluginApi } from 'uiSrc/services/PluginAPI' import { ThemeContext } from 'uiSrc/contexts/themeContext' import { getBaseApiUrl, Nullable, formatToText, replaceEmptyValue } from 'uiSrc/utils' import { Theme } from 'uiSrc/constants' -import { CommandExecutionResult, CommandExecutionType, IPluginVisualization, RunQueryMode } from 'uiSrc/slices/interfaces' +import { CommandExecutionResult, IPluginVisualization, RunQueryMode } from 'uiSrc/slices/interfaces' import { PluginEvents } from 'uiSrc/plugins/pluginEvents' import { prepareIframeHtml } from 'uiSrc/plugins/pluginImport' import { @@ -28,7 +28,6 @@ export interface Props { setMessage: (text: string) => void commandId: string mode?: RunQueryMode - executionType?: CommandExecutionType } enum StylesNamePostfix { @@ -52,7 +51,6 @@ const QueryCardCliPlugin = (props: Props) => { setMessage, commandId, mode = RunQueryMode.Raw, - executionType } = props const { visualizations = [], staticPath } = useSelector(appPluginsSelector) const { modules = [] } = useSelector(connectedInstanceSelector) @@ -93,7 +91,6 @@ const QueryCardCliPlugin = (props: Props) => { dispatch( sendPluginCommandAction({ command, - executionType, onSuccessAction: (response) => { sendMessageToPlugin({ ...commonOptions, diff --git a/redisinsight/ui/src/components/query/query-card/QueryCardHeader/QueryCardHeader.tsx b/redisinsight/ui/src/components/query/query-card/QueryCardHeader/QueryCardHeader.tsx index 64df5b56b4..23a0fe5eb9 100644 --- a/redisinsight/ui/src/components/query/query-card/QueryCardHeader/QueryCardHeader.tsx +++ b/redisinsight/ui/src/components/query/query-card/QueryCardHeader/QueryCardHeader.tsx @@ -19,27 +19,21 @@ import { getCommandNameFromQuery, getVisualizationsByCommand, isGroupMode, - isGroupResults, + truncateText, + urlForAsset, + truncateMilliseconds, isRawMode, isSilentMode, isSilentModeWithoutError, - truncateMilliseconds, - truncateText, - urlForAsset, + isGroupResults, } from 'uiSrc/utils' import { numberWithSpaces } from 'uiSrc/utils/numbers' import { ThemeContext } from 'uiSrc/contexts/themeContext' import { appPluginsSelector } from 'uiSrc/slices/app/plugins' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import { - getProfileViewTypeOptions, - getViewTypeOptions, - isCommandAllowedForProfile, - ProfileQueryType, - WBQueryType -} from 'uiSrc/pages/workbench/constants' -import { CommandExecutionType, IPluginVisualization } from 'uiSrc/slices/interfaces' -import { ResultsMode, ResultsSummary, RunQueryMode } from 'uiSrc/slices/interfaces/workbench' +import { getViewTypeOptions, WBQueryType, getProfileViewTypeOptions, ProfileQueryType, isCommandAllowedForProfile } from 'uiSrc/pages/workbench/constants' +import { IPluginVisualization } from 'uiSrc/slices/interfaces' +import { RunQueryMode, ResultsMode, ResultsSummary } from 'uiSrc/slices/interfaces/workbench' import { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands' import { FormatedDate, FullScreen } from 'uiSrc/components' @@ -63,7 +57,6 @@ export interface Props { mode?: RunQueryMode resultsMode?: ResultsMode activeResultsMode?: ResultsMode - executionType?: CommandExecutionType summary?: ResultsSummary summaryText?: string queryType: WBQueryType @@ -109,7 +102,6 @@ const QueryCardHeader = (props: Props) => { createdAt, mode, resultsMode, - executionType, summary, activeResultsMode, summaryText, @@ -129,7 +121,6 @@ const QueryCardHeader = (props: Props) => { const { instanceId = '' } = useParams<{ instanceId: string }>() const { theme } = useContext(ThemeContext) - const isExecuteTypeSearch = executionType === CommandExecutionType.Search const eventStop = (event: React.MouseEvent) => { event.preventDefault() @@ -148,12 +139,7 @@ const QueryCardHeader = (props: Props) => { } const handleCopy = (event: React.MouseEvent, query: string) => { - sendEvent( - isExecuteTypeSearch - ? TelemetryEvent.SEARCH_COMMAND_COPIED - : TelemetryEvent.WORKBENCH_COMMAND_COPIED, - query - ) + sendEvent(TelemetryEvent.WORKBENCH_COMMAND_COPIED, query) eventStop(event) navigator.clipboard?.writeText?.(query) } @@ -168,29 +154,24 @@ const QueryCardHeader = (props: Props) => { const previousView = options.find(({ id }) => id === selectedValue) const type = currentView.value setSelectedValue(type as WBQueryType, initValue) - sendEvent(isExecuteTypeSearch - ? TelemetryEvent.SEARCH_RESULT_VIEW_CHANGED - : TelemetryEvent.WORKBENCH_RESULT_VIEW_CHANGED, - query, - { - rawMode: isRawMode(activeMode), - group: isGroupMode(activeResultsMode), - previousView: previousView?.name, - isPreviousViewInternal: !!previousView?.internal, - currentView: currentView?.name, - isCurrentViewInternal: !!currentView?.internal, - }) + sendEvent( + TelemetryEvent.WORKBENCH_RESULT_VIEW_CHANGED, + query, + { + rawMode: isRawMode(activeMode), + group: isGroupMode(activeResultsMode), + previousView: previousView?.name, + isPreviousViewInternal: !!previousView?.internal, + currentView: currentView?.name, + isCurrentViewInternal: !!currentView?.internal, + } + ) } const handleQueryDelete = (event: React.MouseEvent) => { eventStop(event) onQueryDelete() - sendEvent( - isExecuteTypeSearch - ? TelemetryEvent.SEARCH_CLEAR_RESULT_CLICKED - : TelemetryEvent.WORKBENCH_CLEAR_RESULT_CLICKED, - query - ) + sendEvent(TelemetryEvent.WORKBENCH_CLEAR_RESULT_CLICKED, query) } const handleQueryReRun = (event: React.MouseEvent) => { @@ -200,14 +181,8 @@ const QueryCardHeader = (props: Props) => { const handleToggleOpen = () => { if (!isFullScreen && !isSilentModeWithoutError(resultsMode, summary?.fail)) { - const collapsedEventName = isExecuteTypeSearch - ? TelemetryEvent.SEARCH_RESULTS_COLLAPSED - : TelemetryEvent.WORKBENCH_RESULTS_COLLAPSED - const expandedEventName = isExecuteTypeSearch - ? TelemetryEvent.SEARCH_RESULTS_EXPANDED - : TelemetryEvent.WORKBENCH_RESULTS_EXPANDED sendEvent( - isOpen ? collapsedEventName : expandedEventName, + isOpen ? TelemetryEvent.WORKBENCH_RESULTS_COLLAPSED : TelemetryEvent.WORKBENCH_RESULTS_EXPANDED, query ) } diff --git a/redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/markdown-message/components/code-block/CodeBlock.spec.tsx b/redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/markdown-message/components/code-block/CodeBlock.spec.tsx index 98bbebd6a9..df43dc81bb 100644 --- a/redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/markdown-message/components/code-block/CodeBlock.spec.tsx +++ b/redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/markdown-message/components/code-block/CodeBlock.spec.tsx @@ -29,7 +29,6 @@ describe('CodeBlock', () => { sendWBCommand({ commandId: expect.any(String), commands: ['info'], - executionType: CommandExecutionType.Workbench }), setDbIndexState(true) ]) diff --git a/redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/markdown-message/components/code-block/CodeBlock.tsx b/redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/markdown-message/components/code-block/CodeBlock.tsx index 582e9b94c8..eb319636af 100644 --- a/redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/markdown-message/components/code-block/CodeBlock.tsx +++ b/redisinsight/ui/src/components/side-panels/panels/ai-assistant/components/shared/markdown-message/components/code-block/CodeBlock.tsx @@ -4,7 +4,6 @@ import { CodeButtonParams } from 'uiSrc/constants' import { sendWbQueryAction } from 'uiSrc/slices/workbench/wb-results' import { CodeButtonBlock } from 'uiSrc/components/markdown' import { ButtonLang } from 'uiSrc/utils/formatters/markdown/remarkCode' -import { CommandExecutionType } from 'uiSrc/slices/interfaces' import { AdditionalRedisModule } from 'apiSrc/modules/database/models/additional.redis.module' export interface Props { @@ -25,7 +24,6 @@ const CodeBlock = (props: Props) => { children, null, params, - CommandExecutionType.Workbench, { afterAll: onFinish }, onFinish )) diff --git a/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementAreaWrapper.tsx b/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementAreaWrapper.tsx index e94b0ee661..a4816a17ad 100644 --- a/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementAreaWrapper.tsx +++ b/redisinsight/ui/src/components/side-panels/panels/enablement-area/EnablementAreaWrapper.tsx @@ -7,7 +7,6 @@ import { workbenchCustomTutorialsSelector } from 'uiSrc/slices/workbench/wb-cust import { sendEventTelemetry, TELEMETRY_EMPTY_VALUE, TelemetryEvent } from 'uiSrc/telemetry' import { CodeButtonParams } from 'uiSrc/constants' import { sendWbQueryAction } from 'uiSrc/slices/workbench/wb-results' -import { CommandExecutionType } from 'uiSrc/slices/interfaces' import { getTutorialSection } from './EnablementArea/utils' import EnablementArea from './EnablementArea' @@ -31,7 +30,6 @@ const EnablementAreaWrapper = () => { script, null, params, - CommandExecutionType.Workbench, { afterAll: onFinish }, onFinish )) diff --git a/redisinsight/ui/src/pages/search/SearchPage.spec.tsx b/redisinsight/ui/src/pages/search/SearchPage.spec.tsx deleted file mode 100644 index c160ed2a87..0000000000 --- a/redisinsight/ui/src/pages/search/SearchPage.spec.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import React from 'react' -import { cloneDeep } from 'lodash' -import { cleanup, fireEvent, mockedStore, render, screen } from 'uiSrc/utils/test-utils' - -import { loadWBHistory, sendWBCommand, setExecutionType } from 'uiSrc/slices/workbench/wb-results' -import { setDbIndexState } from 'uiSrc/slices/app/context' -import { sendPageViewTelemetry, TelemetryPageView } from 'uiSrc/telemetry' -import { CommandExecutionType } from 'uiSrc/slices/interfaces' -import SearchPage from './SearchPage' - -jest.mock('uiSrc/slices/app/context', () => ({ - ...jest.requireActual('uiSrc/slices/app/context'), - appContextSearchAndQuery: jest.fn().mockReturnValue({ - script: 'value', - panelSizes: { vertical: 100 } - }) -})) - -jest.mock('uiSrc/slices/instances/instances', () => ({ - ...jest.requireActual('uiSrc/slices/instances/instances'), - connectedInstanceSelector: jest.fn().mockReturnValue({ - name: 'db_name', - }), -})) - -jest.mock('uiSrc/telemetry', () => ({ - ...jest.requireActual('uiSrc/telemetry'), - sendEventTelemetry: jest.fn(), - sendPageViewTelemetry: jest.fn() -})) - -let store: typeof mockedStore -beforeEach(() => { - cleanup() - store = cloneDeep(mockedStore) - store.clearActions() -}) - -/** - * SearchPage tests - * - * @group component - */ -describe('SearchPage', () => { - it('should render', () => { - expect(render()).toBeTruthy() - }) - - it('should send page event', () => { - const sendPageViewTelemetryMock = jest.fn(); - (sendPageViewTelemetry as jest.Mock).mockImplementation(() => sendPageViewTelemetryMock) - - render() - - expect(sendPageViewTelemetry).toBeCalledWith({ - name: TelemetryPageView.SEARCH_AND_QUERY_PAGE, - eventData: { - databaseId: 'instanceId' - } - }) - }) - - it('should call proper actions on submit', () => { - render() - - fireEvent.click(screen.getByTestId('btn-submit')) - - expect(store.getActions()).toEqual([ - loadWBHistory(), - setExecutionType(CommandExecutionType.Search), - sendWBCommand({ - commandId: expect.any(String), - commands: ['value'], - executionType: CommandExecutionType.Search - }), - setDbIndexState(true) - ]) - }) -}) diff --git a/redisinsight/ui/src/pages/search/SearchPage.tsx b/redisinsight/ui/src/pages/search/SearchPage.tsx deleted file mode 100644 index 2ff8291b8a..0000000000 --- a/redisinsight/ui/src/pages/search/SearchPage.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react' -import cx from 'classnames' -import { EuiResizableContainer } from '@elastic/eui' -import { useDispatch, useSelector } from 'react-redux' - -import { useParams } from 'react-router-dom' -import { appContextSearchAndQuery, setSQVerticalPanelSizes, } from 'uiSrc/slices/app/context' -import { QueryWrapper, ResultsHistory } from 'uiSrc/pages/search/components' - -import { sendWbQueryAction, setExecutionType } from 'uiSrc/slices/workbench/wb-results' -import { formatLongName, getDbIndex, Nullable, setTitle } from 'uiSrc/utils' - -import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' -import { sendPageViewTelemetry, TelemetryPageView } from 'uiSrc/telemetry' -import { CodeButtonParams } from 'uiSrc/constants' - -import { CommandExecutionType } from 'uiSrc/slices/interfaces' -import { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands' -import styles from './styles.module.scss' - -const verticalPanelIds = { - firstPanelId: 'scriptingArea', - secondPanelId: 'resultsArea' -} - -const SearchPage = () => { - const { name: connectedInstanceName, db } = useSelector(connectedInstanceSelector) - const { commandsArray, spec } = useSelector(appRedisCommandsSelector) - - const { panelSizes: { vertical } } = useSelector(appContextSearchAndQuery) - const [isPageViewSent, setIsPageViewSent] = useState(false) - - const { instanceId } = useParams<{ instanceId: string }>() - const verticalSizesRef = useRef(vertical) - - const dispatch = useDispatch() - - setTitle(`${formatLongName(connectedInstanceName, 33, 0, '...')} ${getDbIndex(db)} - Search and Query`) - - useEffect(() => { - dispatch(setExecutionType(CommandExecutionType.Search)) - return () => { - dispatch(setSQVerticalPanelSizes(verticalSizesRef.current)) - } - }, []) - - useEffect(() => { - if (connectedInstanceName && !isPageViewSent) { - sendPageView(instanceId) - } - }, [connectedInstanceName, isPageViewSent]) - - const onVerticalPanelWidthChange = useCallback((newSizes: any) => { - verticalSizesRef.current = newSizes - }, []) - - const sendPageView = (instanceId: string) => { - sendPageViewTelemetry({ - name: TelemetryPageView.SEARCH_AND_QUERY_PAGE, - eventData: { - databaseId: instanceId - } - }) - setIsPageViewSent(true) - } - - const handleSubmit = ( - commandInit: string, - commandId?: Nullable, - executeParams: CodeButtonParams = {} - ) => { - dispatch(sendWbQueryAction( - commandInit, - commandId, - { - ...executeParams, - results: 'single', - }, - CommandExecutionType.Search - )) - } - - return ( -
-
-
- - {(EuiResizablePanel, EuiResizableButton) => ( - <> - - - - - - - - - - - )} - -
-
-
- ) -} - -export default SearchPage diff --git a/redisinsight/ui/src/pages/search/components/constants/supported_commands.json b/redisinsight/ui/src/pages/search/components/constants/supported_commands.json deleted file mode 100644 index fa08d11556..0000000000 --- a/redisinsight/ui/src/pages/search/components/constants/supported_commands.json +++ /dev/null @@ -1,1156 +0,0 @@ -{ - "FT.AGGREGATE": { - "summary": "Run a search query on an index and perform aggregate transformations on the results", - "complexity": "O(1)", - "arguments": [ - { - "name": "index", - "type": "string" - }, - { - "name": "query", - "type": "string" - }, - { - "name": "verbatim", - "type": "pure-token", - "token": "VERBATIM", - "optional": true - }, - { - "name": "load", - "type": "block", - "optional": true, - "arguments": [ - { - "name": "count", - "type": "string", - "token": "LOAD" - }, - { - "name": "field", - "type": "string", - "multiple": true - } - ] - }, - { - "name": "timeout", - "type": "integer", - "optional": true, - "token": "TIMEOUT" - }, - { - "name": "loadall", - "type": "pure-token", - "token": "LOAD *", - "optional": true - }, - { - "name": "groupby", - "type": "block", - "optional": true, - "multiple": true, - "arguments": [ - { - "name": "nargs", - "type": "integer", - "token": "GROUPBY" - }, - { - "name": "property", - "type": "string", - "multiple": true - }, - { - "name": "reduce", - "type": "block", - "optional": true, - "multiple": true, - "arguments": [ - { - "name": "reduce", - "token": "REDUCE", - "type": "pure-token" - }, - { - "name": "function", - "type": "oneof", - "arguments": [ - { - "name": "count", - "type": "pure-token", - "token": "COUNT" - }, - { - "name": "count_distinct", - "type": "pure-token", - "token": "COUNT_DISTINCT" - }, - { - "name": "count_distinctish", - "type": "pure-token", - "token": "COUNT_DISTINCTISH" - }, - { - "name": "sum", - "type": "pure-token", - "token": "SUM" - }, - { - "name": "min", - "type": "pure-token", - "token": "MIN" - }, - { - "name": "max", - "type": "pure-token", - "token": "MAX" - }, - { - "name": "avg", - "type": "pure-token", - "token": "AVG" - }, - { - "name": "stddev", - "type": "pure-token", - "token": "STDDEV" - }, - { - "name": "quantile", - "type": "pure-token", - "token": "QUANTILE" - }, - { - "name": "tolist", - "type": "pure-token", - "token": "TOLIST" - }, - { - "name": "first_value", - "type": "pure-token", - "token": "FIRST_VALUE" - }, - { - "name": "random_sample", - "type": "pure-token", - "token": "RANDOM_SAMPLE" - } - ] - }, - { - "name": "nargs", - "type": "integer" - }, - { - "name": "arg", - "type": "string", - "multiple": true - }, - { - "name": "name", - "type": "string", - "token": "AS", - "optional": true - } - ] - } - ] - }, - { - "name": "sortby", - "type": "block", - "optional": true, - "arguments": [ - { - "name": "nargs", - "type": "integer", - "token": "SORTBY" - }, - { - "name": "fields", - "type": "block", - "optional": true, - "multiple": true, - "arguments": [ - { - "name": "property", - "type": "string" - }, - { - "name": "order", - "type": "oneof", - "arguments": [ - { - "name": "asc", - "type": "pure-token", - "token": "ASC" - }, - { - "name": "desc", - "type": "pure-token", - "token": "DESC" - } - ] - } - ] - }, - { - "name": "num", - "type": "integer", - "token": "MAX", - "optional": true - } - ] - }, - { - "name": "apply", - "type": "block", - "optional": true, - "multiple": true, - "arguments": [ - { - "name": "expression", - "type": "string", - "expression": true, - "token": "APPLY", - "arguments": [ - { - "name": "exists", - "token": "exists", - "type": "function", - "summary": "Checks whether a field exists in a document.", - "arguments": [ - { - "token": "s" - } - ] - }, - { - "name": "log", - "token": "log", - "type": "function", - "summary": "Return the logarithm of a number, property or subexpression", - "arguments": [ - { - "token": "x" - } - ] - }, - { - "name": "abs", - "token": "abs", - "type": "function", - "summary": "Return the absolute number of a numeric expression", - "arguments": [ - { - "token": "x" - } - ] - }, - { - "name": "ceil", - "token": "ceil", - "type": "function", - "summary": "Round to the smallest value not less than x", - "arguments": [ - { - "token": "x" - } - ] - }, - { - "name": "floor", - "token": "floor", - "type": "function", - "summary": "Round to largest value not greater than x", - "arguments": [ - { - "token": "x" - } - ] - }, - { - "name": "log2", - "token": "log2", - "type": "function", - "summary": "Return the logarithm of x to base 2", - "arguments": [ - { - "token": "x" - } - ] - }, - { - "name": "exp", - "token": "exp", - "type": "function", - "summary": "Return the exponent of x, e.g., e^x", - "arguments": [ - { - "token": "x" - } - ] - }, - { - "name": "sqrt", - "token": "sqrt", - "type": "function", - "summary": "Return the square root of x", - "arguments": [ - { - "token": "x" - } - ] - }, - { - "name": "upper", - "token": "upper", - "type": "function", - "summary": "Return the uppercase conversion of s", - "arguments": [ - { - "token": "s" - } - ] - }, - { - "name": "lower", - "token": "lower", - "type": "function", - "summary": "Return the lowercase conversion of s", - "arguments": [ - { - "token": "s" - } - ] - }, - { - "name": "startswith", - "token": "startswith", - "type": "function", - "summary": "Return 1 if s2 is the prefix of s1, 0 otherwise.", - "arguments": [ - { - "token": "s1" - }, - { - "token": "s2" - } - ] - }, - { - "name": "contains", - "token": "contains", - "type": "function", - "summary": "Return the number of occurrences of s2 in s1, 0 otherwise. If s2 is an empty string, return length(s1) + 1.", - "arguments": [ - { - "token": "s1" - }, - { - "token": "s2" - } - ] - }, - { - "name": "strlen", - "token": "strlen", - "type": "function", - "summary": "Return the length of s", - "arguments": [ - { - "token": "s" - } - ] - }, - { - "name": "substr", - "token": "substr", - "type": "function", - "summary": "Return the substring of s, starting at offset and having count characters.If offset is negative, it represents the distance from the end of the string.If count is -1, it means \"the rest of the string starting at offset\".", - "arguments": [ - { - "token": "s" - }, - { - "token": "offset" - }, - { - "token": "count" - } - ] - }, - { - "name": "format", - "token": "format", - "type": "function", - "summary": "Use the arguments following fmt to format a string.Currently the only format argument supported is %s and it applies to all types of arguments.", - "arguments": [ - { - "token": "fmt" - } - ] - }, - { - "name": "matched_terms", - "token": "matched_terms", - "type": "function", - "summary": "Return the query terms that matched for each record (up to 100), as a list. If a limit is specified, Redis will return the first N matches found, based on query order.", - "arguments": [ - { - "token": "max_terms=100", - "optional": true - } - ] - }, - { - "name": "split", - "token": "split", - "type": "function", - "summary": "Split a string by any character in the string sep, and strip any characters in strip. If only s is specified, it is split by commas and spaces are stripped. The output is an array.", - "arguments": [ - { - "token": "s" - } - ] - }, - { - "name": "timefmt", - "token": "timefmt", - "type": "function", - "summary": "Return a formatted time string based on a numeric timestamp value x.", - "arguments": [ - { - "token": "x" - }, - { - "token": "fmt", - "optional": true - } - ] - }, - { - "name": "parsetime", - "token": "parsetime", - "type": "function", - "summary": "The opposite of timefmt() - parse a time format using a given format string", - "arguments": [ - { - "token": "timesharing" - }, - { - "token": "fmt", - "optional": true - } - ] - }, - { - "name": "day", - "token": "day", - "type": "function", - "summary": "Round a Unix timestamp to midnight (00:00) start of the current day.", - "arguments": [ - { - "token": "timestamp" - } - ] - }, - { - "name": "hour", - "token": "hour", - "type": "function", - "summary": "Round a Unix timestamp to the beginning of the current hour.", - "arguments": [ - { - "token": "timestamp" - } - ] - }, - { - "name": "minute", - "token": "minute", - "type": "function", - "summary": "Round a Unix timestamp to the beginning of the current minute.", - "arguments": [ - { - "token": "timestamp" - } - ] - }, - { - "name": "month", - "token": "month", - "type": "function", - "summary": "Round a unix timestamp to the beginning of the current month.", - "arguments": [ - { - "token": "timestamp" - } - ] - }, - { - "name": "dayofweek", - "token": "dayofweek", - "type": "function", - "summary": "Convert a Unix timestamp to the day number (Sunday = 0).", - "arguments": [ - { - "token": "timestamp" - } - ] - }, - { - "name": "dayofmonth", - "token": "dayofmonth", - "type": "function", - "summary": "Convert a Unix timestamp to the day of month number (1 .. 31).", - "arguments": [ - { - "token": "timestamp" - } - ] - }, - { - "name": "dayofyear", - "token": "dayofyear", - "type": "function", - "summary": "Convert a Unix timestamp to the day of year number (0 .. 365).", - "arguments": [ - { - "token": "timestamp" - } - ] - }, - { - "name": "year", - "token": "year", - "type": "function", - "summary": "Convert a Unix timestamp to the current year (e.g. 2018).", - "arguments": [ - { - "token": "timestamp" - } - ] - }, - { - "name": "monthofyear", - "token": "monthofyear", - "type": "function", - "summary": "Convert a Unix timestamp to the current month (0 .. 11).", - "arguments": [ - { - "token": "timestamp" - } - ] - }, - { - "name": "geodistance", - "token": "geodistance", - "type": "function", - "summary": "Return distance in meters.", - "arguments": [ - { - "token": "" - } - ] - } - ] - }, - { - "name": "name", - "type": "string", - "token": "AS" - } - ] - }, - { - "name": "limit", - "type": "block", - "optional": true, - "arguments": [ - { - "name": "limit", - "type": "pure-token", - "token": "LIMIT" - }, - { - "name": "offset", - "type": "integer" - }, - { - "name": "num", - "type": "integer" - } - ] - }, - { - "name": "filter", - "type": "string", - "optional": true, - "expression": true, - "token": "FILTER" - }, - { - "name": "cursor", - "type": "block", - "optional": true, - "arguments": [ - { - "name": "withcursor", - "type": "pure-token", - "token": "WITHCURSOR" - }, - { - "name": "read_size", - "type": "integer", - "optional": true, - "token": "COUNT" - }, - { - "name": "idle_time", - "type": "integer", - "optional": true, - "token": "MAXIDLE" - } - ] - }, - { - "name": "params", - "type": "block", - "optional": true, - "arguments": [ - { - "name": "params", - "type": "pure-token", - "token": "PARAMS" - }, - { - "name": "nargs", - "type": "integer" - }, - { - "name": "values", - "type": "block", - "multiple": true, - "arguments": [ - { - "name": "name", - "type": "string" - }, - { - "name": "value", - "type": "string" - } - ] - } - ] - }, - { - "name": "dialect", - "type": "integer", - "optional": true, - "token": "DIALECT", - "since": "2.4.3" - } - ], - "since": "1.1.0", - "group": "search", - "provider": "redisearch" - }, - "FT.EXPLAIN": { - "summary": "Returns the execution plan for a complex query", - "complexity": "O(1)", - "arguments": [ - { - "name": "index", - "type": "string" - }, - { - "name": "query", - "type": "string" - }, - { - "name": "dialect", - "type": "integer", - "optional": true, - "token": "DIALECT", - "since": "2.4.3" - } - ], - "since": "1.0.0", - "group": "search", - "provider": "redisearch" - }, - "FT.PROFILE": { - "summary": "Performs a `FT.SEARCH` or `FT.AGGREGATE` command and collects performance information", - "complexity": "O(N)", - "arguments": [ - { - "name": "index", - "type": "string" - }, - { - "name": "querytype", - "type": "oneof", - "arguments": [ - { - "name": "search", - "type": "pure-token", - "token": "SEARCH" - }, - { - "name": "aggregate", - "type": "pure-token", - "token": "AGGREGATE" - } - ] - }, - { - "name": "limited", - "type": "pure-token", - "token": "LIMITED", - "optional": true - }, - { - "name": "queryword", - "type": "pure-token", - "token": "QUERY", - "expression": true - }, - { - "name": "query", - "type": "string" - } - ], - "since": "2.2.0", - "group": "search", - "provider": "redisearch" - }, - "FT.SEARCH": { - "summary": "Searches the index with a textual query, returning either documents or just ids", - "complexity": "O(N)", - "history": [ - [ - "2.0.0", - "Deprecated `WITHPAYLOADS` and `PAYLOAD` arguments" - ] - ], - "arguments": [ - { - "name": "index", - "type": "string" - }, - { - "name": "query", - "type": "string" - }, - { - "name": "nocontent", - "type": "pure-token", - "token": "NOCONTENT", - "optional": true - }, - { - "name": "verbatim", - "type": "pure-token", - "token": "VERBATIM", - "optional": true - }, - { - "name": "nostopwords", - "type": "pure-token", - "token": "NOSTOPWORDS", - "optional": true - }, - { - "name": "withscores", - "type": "pure-token", - "token": "WITHSCORES", - "optional": true - }, - { - "name": "withpayloads", - "type": "pure-token", - "token": "WITHPAYLOADS", - "optional": true - }, - { - "name": "withsortkeys", - "type": "pure-token", - "token": "WITHSORTKEYS", - "optional": true - }, - { - "name": "filter", - "type": "block", - "optional": true, - "multiple": true, - "arguments": [ - { - "name": "numeric_field", - "type": "string", - "token": "FILTER" - }, - { - "name": "min", - "type": "double" - }, - { - "name": "max", - "type": "double" - } - ] - }, - { - "name": "geo_filter", - "type": "block", - "optional": true, - "multiple": true, - "arguments": [ - { - "name": "geo_field", - "type": "string", - "token": "GEOFILTER" - }, - { - "name": "lon", - "type": "double" - }, - { - "name": "lat", - "type": "double" - }, - { - "name": "radius", - "type": "double" - }, - { - "name": "radius_type", - "type": "oneof", - "arguments": [ - { - "name": "m", - "type": "pure-token", - "token": "m" - }, - { - "name": "km", - "type": "pure-token", - "token": "km" - }, - { - "name": "mi", - "type": "pure-token", - "token": "mi" - }, - { - "name": "ft", - "type": "pure-token", - "token": "ft" - } - ] - } - ] - }, - { - "name": "in_keys", - "type": "block", - "optional": true, - "arguments": [ - { - "name": "count", - "type": "string", - "token": "INKEYS" - }, - { - "name": "key", - "type": "string", - "multiple": true - } - ] - }, - { - "name": "in_fields", - "type": "block", - "optional": true, - "arguments": [ - { - "name": "count", - "type": "string", - "token": "INFIELDS" - }, - { - "name": "field", - "type": "string", - "multiple": true - } - ] - }, - { - "name": "return", - "type": "block", - "optional": true, - "arguments": [ - { - "name": "count", - "type": "string", - "token": "RETURN" - }, - { - "name": "identifiers", - "type": "block", - "multiple": true, - "arguments": [ - { - "name": "identifier", - "type": "string" - }, - { - "name": "property", - "type": "string", - "token": "AS", - "optional": true - } - ] - } - ] - }, - { - "name": "summarize", - "type": "block", - "optional": true, - "arguments": [ - { - "name": "summarize", - "type": "pure-token", - "token": "SUMMARIZE" - }, - { - "name": "fields", - "type": "block", - "optional": true, - "arguments": [ - { - "name": "count", - "type": "string", - "token": "FIELDS" - }, - { - "name": "field", - "type": "string", - "multiple": true - } - ] - }, - { - "name": "num", - "type": "integer", - "token": "FRAGS", - "optional": true - }, - { - "name": "fragsize", - "type": "integer", - "token": "LEN", - "optional": true - }, - { - "name": "separator", - "type": "string", - "token": "SEPARATOR", - "optional": true - } - ] - }, - { - "name": "highlight", - "type": "block", - "optional": true, - "arguments": [ - { - "name": "highlight", - "type": "pure-token", - "token": "HIGHLIGHT" - }, - { - "name": "fields", - "type": "block", - "optional": true, - "arguments": [ - { - "name": "count", - "type": "string", - "token": "FIELDS" - }, - { - "name": "field", - "type": "string", - "multiple": true - } - ] - }, - { - "name": "tags", - "type": "block", - "optional": true, - "arguments": [ - { - "name": "tags", - "type": "pure-token", - "token": "TAGS" - }, - { - "name": "open", - "type": "string" - }, - { - "name": "close", - "type": "string" - } - ] - } - ] - }, - { - "name": "slop", - "type": "integer", - "optional": true, - "token": "SLOP" - }, - { - "name": "timeout", - "type": "integer", - "optional": true, - "token": "TIMEOUT" - }, - { - "name": "inorder", - "type": "pure-token", - "token": "INORDER", - "optional": true - }, - { - "name": "language", - "type": "string", - "optional": true, - "token": "LANGUAGE" - }, - { - "name": "expander", - "type": "string", - "optional": true, - "token": "EXPANDER" - }, - { - "name": "scorer", - "type": "string", - "optional": true, - "token": "SCORER" - }, - { - "name": "explainscore", - "type": "pure-token", - "token": "EXPLAINSCORE", - "optional": true - }, - { - "name": "payload", - "type": "string", - "optional": true, - "token": "PAYLOAD" - }, - { - "name": "sortby", - "type": "block", - "optional": true, - "arguments": [ - { - "name": "sortby", - "type": "string", - "token": "SORTBY" - }, - { - "name": "order", - "type": "oneof", - "optional": true, - "arguments": [ - { - "name": "asc", - "type": "pure-token", - "token": "ASC" - }, - { - "name": "desc", - "type": "pure-token", - "token": "DESC" - } - ] - } - ] - }, - { - "name": "limit", - "type": "block", - "optional": true, - "arguments": [ - { - "name": "limit", - "type": "pure-token", - "token": "LIMIT" - }, - { - "name": "offset", - "type": "integer" - }, - { - "name": "num", - "type": "integer" - } - ] - }, - { - "name": "params", - "type": "block", - "optional": true, - "arguments": [ - { - "name": "params", - "type": "pure-token", - "token": "PARAMS" - }, - { - "name": "nargs", - "type": "integer" - }, - { - "name": "values", - "type": "block", - "multiple": true, - "arguments": [ - { - "name": "name", - "type": "string" - }, - { - "name": "value", - "type": "string" - } - ] - } - ] - }, - { - "name": "dialect", - "type": "integer", - "optional": true, - "token": "DIALECT", - "since": "2.4.3" - } - ], - "since": "1.0.0", - "group": "search", - "provider": "redisearch" - } -} diff --git a/redisinsight/ui/src/pages/search/components/index.ts b/redisinsight/ui/src/pages/search/components/index.ts deleted file mode 100644 index 0d65962608..0000000000 --- a/redisinsight/ui/src/pages/search/components/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import QueryWrapper from './query-wrapper' -import ResultsHistory from './results-history' - -export { - QueryWrapper, - ResultsHistory, -} diff --git a/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.spec.tsx b/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.spec.tsx deleted file mode 100644 index cb604dcca3..0000000000 --- a/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.spec.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React from 'react' -import { cloneDeep } from 'lodash' -import { cleanup, mockedStore, render, fireEvent, screen } from 'uiSrc/utils/test-utils' - -import { loadList } from 'uiSrc/slices/browser/redisearch' -import { changeSQActiveRunQueryMode } from 'uiSrc/slices/search/searchAndQuery' -import { RunQueryMode } from 'uiSrc/slices/interfaces' -import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import QueryWrapper from './QueryWrapper' - -jest.mock('uiSrc/slices/instances/instances', () => ({ - ...jest.requireActual('uiSrc/slices/instances/instances'), - connectedInstanceSelector: jest.fn().mockReturnValue({ - id: '123', - connectionType: 'STANDALONE', - db: 0, - }), -})) - -jest.mock('uiSrc/slices/app/context', () => ({ - ...jest.requireActual('uiSrc/slices/app/context'), - appContextSearchAndQuery: jest.fn().mockReturnValue({ - script: 'value' - }) -})) - -jest.mock('uiSrc/telemetry', () => ({ - ...jest.requireActual('uiSrc/telemetry'), - sendEventTelemetry: jest.fn(), -})) - -let store: typeof mockedStore -beforeEach(() => { - cleanup() - store = cloneDeep(mockedStore) - store.clearActions() -}) - -describe('Query', () => { - it('should render', () => { - expect(render()).toBeTruthy() - }) - - it('should fetch list of indexes', () => { - render() - - expect(store.getActions()).toEqual([loadList()]) - }) - - it('should call proper actions after change mode', () => { - render() - - fireEvent.click(screen.getByTestId('btn-change-mode')) - - expect(store.getActions()).toEqual([loadList(), changeSQActiveRunQueryMode(RunQueryMode.Raw)]) - }) - - it('should call proper actions after submit', () => { - const sendEventTelemetryMock = jest.fn(); - (sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock) - - const onSubmit = jest.fn() - render() - - fireEvent.click(screen.getByTestId('btn-submit')) - - expect(onSubmit).toBeCalledWith('value', undefined, { mode: RunQueryMode.ASCII }) - - expect(sendEventTelemetry).toBeCalledWith({ - event: TelemetryEvent.SEARCH_COMMAND_SUBMITTED, - eventData: { - databaseId: 'instanceId', - mode: RunQueryMode.ASCII, - command: 'value' - } - }) - }) - - it('should call onSubmit with proper value', () => { - const sendEventTelemetryMock = jest.fn(); - (sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock) - - const onSubmit = jest.fn() - render() - - fireEvent.change(screen.getByTestId('monaco'), { target: { value: 'set\ra\rb\n\nc \nd' } }) - fireEvent.click(screen.getByTestId('btn-submit')) - - expect(onSubmit).toBeCalledWith('set a b c d', undefined, { mode: RunQueryMode.ASCII }) - }) -}) diff --git a/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.tsx b/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.tsx deleted file mode 100644 index accad99537..0000000000 --- a/redisinsight/ui/src/pages/search/components/query-wrapper/QueryWrapper.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import React, { useEffect, useRef, useState } from 'react' -import { useDispatch, useSelector } from 'react-redux' -import { useParams } from 'react-router-dom' -import { QueryActions, QueryTutorials } from 'uiSrc/components/query' - -import { RunQueryMode } from 'uiSrc/slices/interfaces' -import { CodeButtonParams, ICommands } from 'uiSrc/constants' - -import { getCommandsFromQuery, Nullable } from 'uiSrc/utils' -import { changeSQActiveRunQueryMode, searchAndQuerySelector } from 'uiSrc/slices/search/searchAndQuery' -import { appContextSearchAndQuery, setSQScript } from 'uiSrc/slices/app/context' -import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import { fetchRedisearchListAction, redisearchListSelector } from 'uiSrc/slices/browser/redisearch' -import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' -import { SearchCommand } from 'uiSrc/pages/search/types' -import { ModuleCommandPrefix } from 'uiSrc/pages/workbench/constants' -import { TUTORIALS } from './constants' - -import REDIS_COMMANDS_SPEC from '../constants/supported_commands.json' - -import Query from '../query' - -import styles from './styles.module.scss' - -export interface Props { - commandsArray?: string[] - spec?: ICommands - onSubmit: ( - commandInit: string, - commandId?: Nullable, - executeParams?: CodeButtonParams - ) => void -} - -const QueryWrapper = (props: Props) => { - const { commandsArray = [], spec = {}, onSubmit } = props - - const { id: connectedIndstanceId } = useSelector(connectedInstanceSelector) - const { script: scriptContext } = useSelector(appContextSearchAndQuery) - const { activeRunQueryMode } = useSelector(searchAndQuerySelector) - const { data: indexes = [] } = useSelector(redisearchListSelector) - - const [value, setValue] = useState(scriptContext) - - const input = useRef(null) - const scriptRef = useRef('') - - const getCommandByName = (name: string) => - (name in REDIS_COMMANDS_SPEC ? REDIS_COMMANDS_SPEC[name] : (spec[name] || {})) - - const SUPPORTED_COMMANDS = commandsArray - .filter((item) => item.startsWith(ModuleCommandPrefix.RediSearch)) - .map((name) => ({ ...getCommandByName(name), name })) as unknown as SearchCommand[] - - const { instanceId } = useParams<{ instanceId: string }>() - - const dispatch = useDispatch() - - useEffect(() => () => { - dispatch(setSQScript(scriptRef.current)) - }, []) - - useEffect(() => { - if (!connectedIndstanceId) return - - // fetch indexes - dispatch(fetchRedisearchListAction()) - }, [connectedIndstanceId]) - - useEffect(() => { - scriptRef.current = value - }, [value]) - - const handleChangeQueryRunMode = () => { - dispatch(changeSQActiveRunQueryMode( - activeRunQueryMode === RunQueryMode.ASCII - ? RunQueryMode.Raw - : RunQueryMode.ASCII - )) - } - - const handleSubmit = () => { - const val = value - .replace(/[\r\n?]{2}|\n\n/g, ' ') - .replace(/\n/g, ' ') - if (!val) return - - onSubmit(val, undefined, { mode: activeRunQueryMode }) - sendEventTelemetry({ - event: TelemetryEvent.SEARCH_COMMAND_SUBMITTED, - eventData: { - databaseId: instanceId, - mode: activeRunQueryMode, - command: getCommandsFromQuery(value, commandsArray) || '' - } - }) - } - - const handleChange = (val: string) => { - setValue(val) - } - - return ( -
-
{}} - role="textbox" - tabIndex={0} - data-testid="main-input-container-area" - > -
- -
-
- - -
-
-
- ) -} - -export default React.memo(QueryWrapper) diff --git a/redisinsight/ui/src/pages/search/components/query-wrapper/constants.ts b/redisinsight/ui/src/pages/search/components/query-wrapper/constants.ts deleted file mode 100644 index 322307978f..0000000000 --- a/redisinsight/ui/src/pages/search/components/query-wrapper/constants.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { TutorialsIds } from 'uiSrc/constants' - -export const TUTORIALS = [ - { - id: TutorialsIds.ExactMatch, - title: 'Exact match' - }, - { - id: TutorialsIds.FullTextSearch, - title: 'Full-text search' - }, - { - id: TutorialsIds.IntroVectorSearch, - title: 'Intro to vector search' - }, -] diff --git a/redisinsight/ui/src/pages/search/components/query-wrapper/index.ts b/redisinsight/ui/src/pages/search/components/query-wrapper/index.ts deleted file mode 100644 index cf4185d43b..0000000000 --- a/redisinsight/ui/src/pages/search/components/query-wrapper/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import QueryWrapper from './QueryWrapper' - -export default QueryWrapper diff --git a/redisinsight/ui/src/pages/search/components/query-wrapper/styles.module.scss b/redisinsight/ui/src/pages/search/components/query-wrapper/styles.module.scss deleted file mode 100644 index 18b46e1931..0000000000 --- a/redisinsight/ui/src/pages/search/components/query-wrapper/styles.module.scss +++ /dev/null @@ -1,76 +0,0 @@ -.wrapper { - position: relative; - height: 100%; - - :global(.editorBounder) { - bottom: 6px; - left: 18px; - right: 46px; - } -} - -.container { - display: flex; - flex-direction: column; - padding: 8px 16px; - width: 100%; - height: 100%; - word-break: break-word; - text-align: left; - letter-spacing: 0; - background-color: var(--rsInputWrapperColor); - color: var(--euiTextSubduedColor) !important; - border: 1px solid var(--euiColorLightShade); -} - -.disabled { - opacity: 0.8; -} - -.disabledActions { - pointer-events: none; - user-select: none; -} - -.containerPlaceholder { - display: flex; - padding: 8px 16px 8px 16px; - width: 100%; - height: 100%; - background-color: var(--rsInputWrapperColor); - color: var(--euiTextSubduedColor) !important; - border: 1px solid var(--euiColorLightShade); - > div { - border: 1px solid var(--euiColorLightShade); - background-color: var(--euiColorEmptyShade); - padding: 8px 20px; - width: 100%; - } -} - -.input { - // cannot use overflow since suggestions are absolute - max-height: calc(100% - 32px); - flex-grow: 1; - width: 100%; - border: 1px solid var(--euiColorLightShade); - background-color: var(--rsInputColor); -} - -.queryFooter { - display: flex; - align-items: center; - justify-content: space-between; - - margin-top: 8px; - flex-shrink: 0; -} - -#script { - font: normal normal bold 14px/17px Inconsolata !important; - color: var(--textColorShade); - caret-color: var(--euiColorFullShade); - min-width: 5px; - display: inline; -} - diff --git a/redisinsight/ui/src/pages/search/components/query/Query.spec.tsx b/redisinsight/ui/src/pages/search/components/query/Query.spec.tsx deleted file mode 100644 index 1f57ca4c4a..0000000000 --- a/redisinsight/ui/src/pages/search/components/query/Query.spec.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react' -import { render } from 'uiSrc/utils/test-utils' - -import Query from './Query' - -describe('Query', () => { - it('should render', () => { - expect(render()).toBeTruthy() - }) -}) diff --git a/redisinsight/ui/src/pages/search/components/query/Query.tsx b/redisinsight/ui/src/pages/search/components/query/Query.tsx deleted file mode 100644 index 20aae087c4..0000000000 --- a/redisinsight/ui/src/pages/search/components/query/Query.tsx +++ /dev/null @@ -1,359 +0,0 @@ -import React, { useContext, useEffect, useRef, useState } from 'react' -import MonacoEditor, { monaco as monacoEditor } from 'react-monaco-editor' -import { useDispatch } from 'react-redux' - -import { isNumber } from 'lodash' -import { MonacoLanguage, Theme } from 'uiSrc/constants' -import { ThemeContext } from 'uiSrc/contexts/themeContext' -import { Nullable } from 'uiSrc/utils' -import { IEditorMount } from 'uiSrc/pages/workbench/interfaces' -import { - addOwnTokenToArgs, - findCurrentArgument, - getRange, - getRediSearchSignutureProvider, - setCursorPositionAtTheEnd, - splitQueryByArgs -} from 'uiSrc/pages/search/utils' -import { CursorContext, FoundCommandArgument, SearchCommand, TokenType } from 'uiSrc/pages/search/types' -import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' -import { fetchRedisearchInfoAction } from 'uiSrc/slices/browser/redisearch' -import { getRediSearchMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokens' -import { useDebouncedEffect } from 'uiSrc/services' -import { - options, - DefinedArgumentName, - FIELD_START_SYMBOL, - COMMANDS_TO_GET_INDEX_INFO, - EmptySuggestionsIds -} from './constants' -import { - getFieldsSuggestions, - getIndexesSuggestions, - asSuggestionsRef, - getCommandsSuggestions, - isIndexComplete, - getGeneralSuggestions, - getFunctionsSuggestions, - getNoIndexesSuggestion, -} from './utils' - -export interface Props { - value: string - onChange: (val: string) => void - indexes: RedisResponseBuffer[] - supportedCommands?: SearchCommand[] -} - -const Query = (props: Props) => { - const { value, onChange, indexes, supportedCommands = [] } = props - - const [selectedCommand, setSelectedCommand] = useState('') - const [selectedIndex, setSelectedIndex] = useState('') - - const monacoObjects = useRef>(null) - const disposeCompletionItemProvider = useRef(() => {}) - const disposeSignatureHelpProvider = useRef(() => {}) - const suggestionsRef = useRef([]) - const helpWidgetRef = useRef({ - isOpen: false, - parent: null, - currentArg: null - }) - const indexesRef = useRef([]) - const attributesRef = useRef([]) - const isEscapedSuggestions = useRef(false) - - const COMMANDS_LIST = supportedCommands.map((command) => ({ - ...addOwnTokenToArgs(command.name!, command), - token: command.name!, - type: TokenType.Block - })) - - const { theme } = useContext(ThemeContext) - const dispatch = useDispatch() - - useEffect(() => () => { - disposeCompletionItemProvider.current?.() - disposeSignatureHelpProvider.current?.() - }, []) - - useEffect(() => { - indexesRef.current = indexes - }, [indexes]) - - useEffect(() => { - monacoEditor.languages.setMonarchTokensProvider( - MonacoLanguage.RediSearch, - getRediSearchMonarchTokensProvider(supportedCommands, selectedCommand) - ) - }, [selectedCommand]) - - useDebouncedEffect(() => { - attributesRef.current = [] - if (!isIndexComplete(selectedIndex)) return - - const index = selectedIndex.replace(/^(['"])(.*)\1$/, '$2') - dispatch(fetchRedisearchInfoAction(index, - (data: any) => { - attributesRef.current = data?.attributes || [] - })) - }, 200, [selectedIndex]) - - const editorDidMount = ( - editor: monacoEditor.editor.IStandaloneCodeEditor, - monaco: typeof monacoEditor - ) => { - monaco.languages.register({ id: MonacoLanguage.RediSearch }) - monacoObjects.current = { editor, monaco } - - monaco.languages.setMonarchTokensProvider( - MonacoLanguage.RediSearch, - getRediSearchMonarchTokensProvider(supportedCommands) - ) - - disposeSignatureHelpProvider.current?.() - disposeSignatureHelpProvider.current = monaco.languages.registerSignatureHelpProvider(MonacoLanguage.RediSearch, { - provideSignatureHelp: (): any => getRediSearchSignutureProvider(helpWidgetRef?.current) - }).dispose - - disposeCompletionItemProvider.current?.() - disposeCompletionItemProvider.current = monaco.languages.registerCompletionItemProvider(MonacoLanguage.RediSearch, { - provideCompletionItems: (): monacoEditor.languages.CompletionList => - ({ suggestions: suggestionsRef.current }) - }).dispose - - editor.onDidChangeCursorPosition(handleCursorChange) - editor.onKeyDown((e: monacoEditor.IKeyboardEvent) => { - if (e.keyCode === monacoEditor.KeyCode.Escape && isSuggestionsOpened()) { - isEscapedSuggestions.current = true - } - }) - - const suggestionWidget = editor.getContribution('editor.contrib.suggestController') - suggestionWidget?.onWillInsertSuggestItem(({ item }: Record<'item', any>) => { - if (item.completion.id === EmptySuggestionsIds.NoIndexes) { - updateHelpWidget(true) - editor.trigger('', 'hideSuggestWidget', null) - editor.trigger('', 'editor.action.triggerParameterHints', '') - } - }) - - suggestionsRef.current = getSuggestions(editor).data - if (value) { - setCursorPositionAtTheEnd(editor) - return - } - - const position = editor.getPosition() - if (position?.column === 1 && position?.lineNumber === 1) { - editor.focus() - triggerSuggestions() - } - } - - const isSuggestionsOpened = () => { - const { editor } = monacoObjects.current || {} - if (!editor) return false - const suggestController = editor.getContribution('editor.contrib.suggestController') - return suggestController?.model?.state === 1 - } - - const handleCursorChange = () => { - const { editor } = monacoObjects.current || {} - suggestionsRef.current = [] - - if (!editor) return - if (!editor.getSelection()?.isEmpty()) { - editor?.trigger('', 'hideSuggestWidget', null) - return - } - - const { data, forceHide, forceShow } = getSuggestions(editor) - suggestionsRef.current = data - - if (!forceShow) { - editor.trigger('', 'editor.action.triggerParameterHints', '') - return - } - - if (data.length) { - helpWidgetRef.current.isOpen = false - triggerSuggestions() - return - } - - editor.trigger('', 'editor.action.triggerParameterHints', '') - - if (forceHide) { - setTimeout(() => editor?.trigger('', 'hideSuggestWidget', null), 0) - } else { - helpWidgetRef.current.isOpen = !isSuggestionsOpened() && helpWidgetRef.current.isOpen - } - } - - const triggerSuggestions = () => { - const { editor } = monacoObjects.current || {} - setTimeout(() => editor?.trigger('', 'editor.action.triggerSuggest', { auto: false })) - } - - const updateHelpWidget = (isOpen: boolean, parent?: SearchCommand, currentArg?: SearchCommand) => { - helpWidgetRef.current = { - isOpen, - parent: parent || helpWidgetRef.current.parent, - currentArg: currentArg || helpWidgetRef.current.currentArg } - } - - const getSuggestions = ( - editor: monacoEditor.editor.IStandaloneCodeEditor - ): { - forceHide: boolean - forceShow: boolean - data: monacoEditor.languages.CompletionItem[] - } => { - const position = editor.getPosition() - const model = editor.getModel() - - if (!position || !model) return asSuggestionsRef([]) - - const value = editor.getValue() - const offset = model.getOffsetAt(position) - const word = model.getWordUntilPosition(position) - const range = getRange(position, word) - - const { args, cursor } = splitQueryByArgs(value, offset) - const { prevCursorChar } = cursor - - const allArgs = args.flat() - const [beforeOffsetArgs, [currentOffsetArg]] = args - const [firstArg] = beforeOffsetArgs - - if ((position.lineNumber === 1 && position.column === 1) || beforeOffsetArgs.length === 0) { - return asSuggestionsRef(getCommandsSuggestions(COMMANDS_LIST, range), false) - } - - const commandName = (firstArg || currentOffsetArg)?.toUpperCase() - const command = COMMANDS_LIST.find(({ name }) => commandName === name) - if (!command) { - helpWidgetRef.current.isOpen = false - return asSuggestionsRef([]) - } - - if (COMMANDS_TO_GET_INDEX_INFO.some((name) => name === commandName)) { - setSelectedIndex(allArgs[1] || '') - } - - setSelectedCommand(commandName) - - const cursorContext: CursorContext = { ...cursor, currentOffsetArg, offset } - const foundArg = findCurrentArgument(COMMANDS_LIST, beforeOffsetArgs) - - if (prevCursorChar === FIELD_START_SYMBOL) return handleFieldSuggestions(foundArg, range) - - switch (foundArg?.stopArg?.name) { - case DefinedArgumentName.index: { - return handleIndexSuggestions(command, foundArg, currentOffsetArg, range) - } - case DefinedArgumentName.query: { - return handleQuerySuggestions(command, foundArg) - } - default: { - return handleCommonSuggestions(value, foundArg, allArgs, cursorContext, range) - } - } - } - - const handleFieldSuggestions = (foundArg: Nullable, range: monacoEditor.IRange) => { - const isInQuery = foundArg?.stopArg?.name === DefinedArgumentName.query - const fieldSuggestions = getFieldsSuggestions(attributesRef.current, range, true, isInQuery) - return asSuggestionsRef(fieldSuggestions, true) - } - - const handleIndexSuggestions = ( - command: SearchCommand, - foundArg: FoundCommandArgument, - currentOffsetArg: Nullable, - range: monacoEditor.IRange - ) => { - const isIndex = indexesRef.current.length > 0 - updateHelpWidget(isIndex, command, foundArg?.stopArg) - - if (!isIndex) { - updateHelpWidget(!!currentOffsetArg) - return asSuggestionsRef(!currentOffsetArg ? getNoIndexesSuggestion(range) : [], true) - } - - if (!isIndex || currentOffsetArg) return asSuggestionsRef([], !currentOffsetArg) - - const argumentIndex = command?.arguments - ?.findIndex(({ name }) => foundArg?.stopArg?.name === name) - const isNextArgQuery = isNumber(argumentIndex) - && command?.arguments?.[argumentIndex + 1]?.name === DefinedArgumentName.query - - return asSuggestionsRef(getIndexesSuggestions(indexesRef.current, range, isNextArgQuery)) - } - - const handleQuerySuggestions = (command: SearchCommand, foundArg: FoundCommandArgument) => { - updateHelpWidget(true, command, foundArg?.stopArg) - return asSuggestionsRef([], false) - } - - const handleExpressionSuggestions = ( - value: string, - foundArg: FoundCommandArgument, - cursorContext: CursorContext, - range: monacoEditor.IRange - ) => { - updateHelpWidget(true, foundArg?.parent, foundArg?.stopArg) - - const { isCursorInQuotes, offset, argLeftOffset } = cursorContext - if (!isCursorInQuotes) return asSuggestionsRef([]) - - const stringBeforeCursor = value.substring(argLeftOffset, offset) || '' - const expression = stringBeforeCursor.replace(/^["']|["']$/g, '') - const { args } = splitQueryByArgs(expression, offset - argLeftOffset) - const [, [currentArg]] = args - - const functions = foundArg?.stopArg?.arguments ?? [] - const suggestions = getFunctionsSuggestions(functions, range) - const isStartsWithFunction = functions.some(({ token }) => token?.startsWith(currentArg)) - - return asSuggestionsRef(suggestions, true, isStartsWithFunction) - } - - const handleCommonSuggestions = ( - value: string, - foundArg: Nullable, - allArgs: string[], - cursorContext: CursorContext, - range: monacoEditor.IRange - ) => { - if (foundArg?.stopArg?.expression) return handleExpressionSuggestions(value, foundArg, cursorContext, range) - - const { prevCursorChar, nextCursorChar, isCursorInQuotes } = cursorContext - const shouldHideSuggestions = isCursorInQuotes || nextCursorChar || (prevCursorChar && isEscapedSuggestions.current) - if (shouldHideSuggestions) return asSuggestionsRef([]) - - const { - suggestions, - forceHide, - helpWidgetData - } = getGeneralSuggestions(foundArg, allArgs, range, attributesRef.current) - - if (helpWidgetData) updateHelpWidget(helpWidgetData.isOpen, helpWidgetData.parent, helpWidgetData.currentArg) - return asSuggestionsRef(suggestions, forceHide) - } - - return ( - - ) -} - -export default Query diff --git a/redisinsight/ui/src/pages/search/components/query/constants.ts b/redisinsight/ui/src/pages/search/components/query/constants.ts deleted file mode 100644 index 6617df7940..0000000000 --- a/redisinsight/ui/src/pages/search/components/query/constants.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { merge } from 'lodash' -import { defaultMonacoOptions } from 'uiSrc/constants' - -export const options = merge(defaultMonacoOptions, - { - suggest: { - showWords: false, - showIcons: true, - insertMode: 'replace', - filterGraceful: false, - matchOnWordStartOnly: true - } - }) - -export const COMMANDS_TO_GET_INDEX_INFO = [ - 'FT.SEARCH', - 'FT.AGGREGATE', - 'FT.EXPLAIN', - 'FT.EXPLAINCLI', - 'FT.PROFILE', - 'FT.SPELLCHECK', - 'FT.TAGVALS', - 'FT.ALTER', - 'FT.CREATE' -] - -export const COMPOSITE_ARGS = [ - 'LOAD *', - 'FT.CONFIG GET', - 'FT.CONFIG SET', - 'FT.CURSOR DEL', - 'FT.CURSOR READ', -] - -export enum DefinedArgumentName { - index = 'index', - query = 'query', - field = 'field', - expression = 'expression' -} - -export const FIELD_START_SYMBOL = '@' -export enum EmptySuggestionsIds { - NoIndexes = 'no-indexes' -} diff --git a/redisinsight/ui/src/pages/search/components/query/index.ts b/redisinsight/ui/src/pages/search/components/query/index.ts deleted file mode 100644 index 611583bbb6..0000000000 --- a/redisinsight/ui/src/pages/search/components/query/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Query from './Query' - -export default Query diff --git a/redisinsight/ui/src/pages/search/components/query/styles.module.scss b/redisinsight/ui/src/pages/search/components/query/styles.module.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/redisinsight/ui/src/pages/search/components/query/utils.spec.ts b/redisinsight/ui/src/pages/search/components/query/utils.spec.ts deleted file mode 100644 index 9e08a40283..0000000000 --- a/redisinsight/ui/src/pages/search/components/query/utils.spec.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { - addFieldAttribute, - getFieldsSuggestions, - getGeneralSuggestions, - isIndexComplete -} from 'uiSrc/pages/search/components/query/utils' -import { MOCKED_SUPPORTED_COMMANDS } from 'uiSrc/pages/search/mocks/mocks' -import { addOwnTokenToArgs, buildSuggestion, findCurrentArgument } from 'uiSrc/pages/search/utils' -import { SearchCommand, TokenType } from 'uiSrc/pages/search/types' - -const ftAggregate = MOCKED_SUPPORTED_COMMANDS['FT.AGGREGATE'] - -const commands = Object.keys(MOCKED_SUPPORTED_COMMANDS) - .map((key) => ({ - ...addOwnTokenToArgs(key, MOCKED_SUPPORTED_COMMANDS[key]), - token: key, - type: TokenType.Block - })) - -const ftAggregateAppend = ftAggregate.arguments.slice(2) - .map((arg) => ({ ...arg, parent: ftAggregate })) - -const getGeneralSuggestionsTests = [ - { - input: { - foundArg: findCurrentArgument( - commands, - ['FT.AGGREGATE', '""', '""'] - ), - allArgs: ['FT.AGGREGATE', '""', '""'] - }, - result: { - helpWidgetData: expect.any(Object), - suggestions: ftAggregateAppend - .map((arg) => ({ - ...buildSuggestion(arg as SearchCommand, {} as any), - sortText: expect.any(String), - kind: undefined, - detail: expect.any(String) - })) - } - }, - { - input: { - foundArg: findCurrentArgument( - commands, - ['FT.AGGREGATE', '""', '""', 'APPLY', 'expression'] - ), - allArgs: ['FT.AGGREGATE', '""', '""', 'APPLY', 'expression'] - }, - result: { - helpWidgetData: expect.any(Object), - suggestions: [ - { - label: 'AS', - insertText: 'AS ', - insertTextRules: 4, - range: expect.any(Object), - kind: undefined, - detail: 'APPLY expression AS name', - } - ] - } - }, - { - input: { - foundArg: findCurrentArgument( - commands, - ['FT.PROFILE', '""'] - ), - allArgs: ['FT.PROFILE', '""'] - }, - result: { - helpWidgetData: expect.any(Object), - suggestions: [ - { - label: 'SEARCH', - insertText: 'SEARCH ', - insertTextRules: 4, - range: expect.any(Object), - kind: undefined, - detail: expect.any(String), - }, - { - label: 'AGGREGATE', - insertText: 'AGGREGATE ', - insertTextRules: 4, - range: expect.any(Object), - kind: undefined, - detail: expect.any(String), - } - ] - } - }, -] - -describe('getGeneralSuggestions', () => { - it.each(getGeneralSuggestionsTests)('should properly return suggestions', ({ input, result }) => { - const testResult = getGeneralSuggestions( - input.foundArg as any, - input.allArgs, - {} as any, - [] - ) - - expect(testResult).toEqual(result) - }) -}) - -const isIndexCompleteTests: Array<[string, boolean]> = [ - ['', false], - ['"', false], - ['\"\\"', false], - ['""', true], - ["'", false], - ["''", true], - ["'index\\'", false], - ["'index'", true], - ['"index \\\\"', true], - ['index', true], -] - -describe('isIndexComplete', () => { - it.each(isIndexCompleteTests)('should properly return value for %s', (index, result) => { - const testResult = isIndexComplete(index) - - expect(testResult).toEqual(result) - }) -}) - -const mockedFields = [ - { identifier: 'name', attribute: 'name', type: 'TEXT', WEIGHT: '1', SORTABLE: true, NOSTEM: true }, - { identifier: 'description', attribute: 'description', type: 'TEXT', WEIGHT: '1' }, - { identifier: 'class', attribute: 'class', type: 'TAG', SEPARATOR: ',' }, - { identifier: 'type', attribute: 'type', type: 'TAG', SEPARATOR: ';' }, - { identifier: 'address_city', attribute: 'city', type: 'TAG', SEPARATOR: ',' }, - { identifier: 'address_street', attribute: 'address', type: 'TEXT', WEIGHT: '1', NOSTEM: true }, - { identifier: 'students', attribute: 'students', type: 'NUMERIC', SORTABLE: true }, - { identifier: 'location', attribute: 'location', type: 'GEO' } -] - -const getFieldsSuggestionsTests = [ - [ - [mockedFields, {}], - mockedFields.map((field) => ({ - detail: field.attribute, - insertText: field.attribute, - insertTextRules: 4, - kind: undefined, - label: field.attribute, - range: expect.any(Object), - })) - ], - [ - [mockedFields, {}, false, true], - mockedFields.map((field) => ({ - detail: field.attribute, - insertText: addFieldAttribute(field.attribute, field.type), - insertTextRules: 4, - kind: undefined, - label: field.attribute, - range: expect.any(Object), - })) - ], -] - -describe('getFieldsSuggestions', () => { - it.each(getFieldsSuggestionsTests)('should properly return value for %s', (input, result) => { - const testResult = getFieldsSuggestions(...input) - - expect(testResult).toEqual(result) - }) -}) diff --git a/redisinsight/ui/src/pages/search/components/query/utils.ts b/redisinsight/ui/src/pages/search/components/query/utils.ts deleted file mode 100644 index 5ec9f867aa..0000000000 --- a/redisinsight/ui/src/pages/search/components/query/utils.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { monaco } from 'react-monaco-editor' -import * as monacoEditor from 'monaco-editor' -import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' -import { bufferToString, formatLongName, generateArgsForInsertText, getCommandMarkdown, Nullable } from 'uiSrc/utils' -import { - buildSuggestion, - generateDetail, - removeNotSuggestedArgs -} from 'uiSrc/pages/search/utils' -import { FoundCommandArgument, SearchCommand } from 'uiSrc/pages/search/types' -import { DefinedArgumentName, EmptySuggestionsIds } from 'uiSrc/pages/search/components/query/constants' -import { getUtmExternalLink } from 'uiSrc/utils/links' - -export const asSuggestionsRef = ( - suggestions: monacoEditor.languages.CompletionItem[], - forceHide = true, - forceShow = true -) => ({ - data: suggestions, - forceHide, - forceShow -}) - -const NO_INDEXES_DOC_LINK = getUtmExternalLink('https://redis.io/docs/latest/commands/ft.create/', { campaign: 'workbench' }) -export const getNoIndexesSuggestion = (range: monaco.IRange) => [ - { - id: EmptySuggestionsIds.NoIndexes, - label: 'No indexes to display', - kind: monacoEditor.languages.CompletionItemKind.Issue, - insertText: '', - insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, - range, - detail: 'Create an index', - documentation: { - value: `See the [documentation](${NO_INDEXES_DOC_LINK}) for detailed instructions on how to create an index.`, - } - } -] - -export const getIndexesSuggestions = (indexes: RedisResponseBuffer[], range: monaco.IRange, isNextArgQuery = true) => - indexes.map((index) => { - const value = formatLongName(bufferToString(index)) - const insertQueryQuotes = isNextArgQuery ? " '\${1:query to search}'" : '' - - return { - label: value || ' ', - kind: monacoEditor.languages.CompletionItemKind.Snippet, - insertText: `'${value}'${insertQueryQuotes} `, - insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, - range, - detail: value || ' ', - } - }) - -export const addFieldAttribute = (attribute: string, type: string) => { - switch (type) { - case 'TAG': return `${attribute}:{\${1:tag}}` - case 'TEXT': return `${attribute}:(\${1:term})` - case 'NUMERIC': return `${attribute}:[\${1:range}]` - case 'GEO': return `${attribute}:[\${1:lon} \${2:lat} \${3:radius} \${4:unit}]` - case 'VECTOR': return `${attribute} \\$\${1:vector}` - default: return attribute - } -} - -export const getFieldsSuggestions = ( - fields: any[], - range: monaco.IRange, - spaceAfter = false, - withType = false -) => - fields.map((field) => { - const { attribute, type } = field - const attibuteText = attribute.trim() ? attribute : `\\'${attribute}\\'` - const insertText = withType ? addFieldAttribute(attibuteText, type) : attibuteText - - return { - label: attribute || ' ', - kind: monacoEditor.languages.CompletionItemKind.Reference, - insertText: `${insertText}${spaceAfter ? ' ' : ''}`, - insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, - range, - detail: attribute || ' ', - } - }) - -const insertFunctionArguments = (args: SearchCommand[]) => - generateArgsForInsertText( - args.map(({ token, optional }) => (optional ? `[${token}]` : (token || ''))) as string[], - ', ' - ) - -export const getFunctionsSuggestions = (functions: SearchCommand[], range: monaco.IRange) => functions - .map(({ token, summary, arguments: args }) => ({ - label: token || '', - insertText: `${token}(${insertFunctionArguments(args || [])})`, - insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, - range, - kind: monacoEditor.languages.CompletionItemKind.Function, - detail: summary - })) - -export const getCommandsSuggestions = (commands: SearchCommand[], range: monaco.IRange) => - commands.map((command) => buildSuggestion(command, range, { - detail: generateDetail(command), - insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, - documentation: { - value: getCommandMarkdown(command as any) - }, - })) - -export const getMandatoryArgumentSuggestions = ( - foundArg: FoundCommandArgument, - fields: any[], - range: monaco.IRange -): monacoEditor.languages.CompletionItem[] => { - if (foundArg.stopArg?.name === DefinedArgumentName.field) { - if (!fields.length) return [] - return getFieldsSuggestions(fields, range, true) - } - - if (foundArg.isBlocked) return [] - if (foundArg.append?.length) { - return foundArg.append[0].map((arg: any) => buildSuggestion(arg, range, { - kind: monacoEditor.languages.CompletionItemKind.Property, - detail: generateDetail(foundArg?.parent) - })) - } - - return [] -} - -export const getCommandSuggestions = ( - foundArg: Nullable, - allArgs: string[], - range: monaco.IRange, -) => { - const appendCommands = foundArg?.append ?? [] - const suggestions = [] - - for (let i = 0; i < appendCommands.length; i++) { - const isLastLevel = i === appendCommands.length - 1 - const filteredFileldArgs = isLastLevel - ? removeNotSuggestedArgs(allArgs, appendCommands[i]) - : appendCommands[i] - - const leveledSuggestions = filteredFileldArgs - .map((arg) => buildSuggestion(arg, range, { - sortText: `${i}`, - kind: isLastLevel - ? monacoEditor.languages.CompletionItemKind.Reference - : monacoEditor.languages.CompletionItemKind.Property, - detail: generateDetail(arg?.parent) - })) - - suggestions.push(leveledSuggestions) - } - - return suggestions.flat() -} - -export const getGeneralSuggestions = ( - foundArg: Nullable, - allArgs: string[], - range: monacoEditor.IRange, - fields: any[] -): { - suggestions: monacoEditor.languages.CompletionItem[], - forceHide?: boolean - helpWidgetData?: any -} => { - if (foundArg && !foundArg.isComplete) { - return { - suggestions: getMandatoryArgumentSuggestions(foundArg, fields, range), - helpWidgetData: { isOpen: !!foundArg?.stopArg, parent: foundArg?.parent, currentArg: foundArg?.stopArg } - } - } - - return { - suggestions: getCommandSuggestions(foundArg, allArgs, range), - helpWidgetData: { isOpen: false } - } -} - -export const isIndexComplete = (index: string) => { - if (index.length === 0) return false - - const firstChar = index[0] - const lastChar = index[index.length - 1] - - if (firstChar !== '"' && firstChar !== "'") return true - if (index.length === 1 && (firstChar === '"' || firstChar === "'")) return false - if (firstChar !== lastChar) return false - - let escape = false - for (let i = 1; i < index.length - 1; i++) { - escape = index[i] === '\\' && !escape - } - - return !escape -} diff --git a/redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.spec.tsx b/redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.spec.tsx deleted file mode 100644 index a152317088..0000000000 --- a/redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.spec.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import React from 'react' -import { cloneDeep } from 'lodash' -import { cleanup, fireEvent, mockedStore, render, screen } from 'uiSrc/utils/test-utils' - -import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import { INSTANCE_ID_MOCK } from 'uiSrc/mocks/handlers/analytics/clusterDetailsHandlers' -import { - clearWbResults, - loadWBHistory, - processWBCommand, - workbenchResultsSelector -} from 'uiSrc/slices/workbench/wb-results' -import ResultsHistory from './ResultsHistory' - -jest.mock('uiSrc/telemetry', () => ({ - ...jest.requireActual('uiSrc/telemetry'), - sendEventTelemetry: jest.fn(), -})) - -jest.mock('uiSrc/slices/workbench/wb-results', () => ({ - ...jest.requireActual('uiSrc/slices/workbench/wb-results'), - workbenchResultsSelector: jest.fn().mockReturnValue({ - loading: false, - items: [] - }) -})) - -let store: typeof mockedStore -beforeEach(() => { - cleanup() - store = cloneDeep(mockedStore) - store.clearActions() -}) - -describe('ResultsHistory', () => { - it('should render', () => { - expect(render()).toBeTruthy() - }) - - it('should call proper actions on rerun', async () => { - const onSubmit = jest.fn() - const sendEventTelemetryMock = jest.fn(); - (sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock); - - (workbenchResultsSelector as jest.Mock).mockReturnValue({ - items: [ - { - mode: 'RAW', - resultsMode: 'DEFAULT', - id: '9dda0f6d-9265-4b15-b627-82d2eb867605', - databaseId: '18c37d1d-bc25-4e46-a20d-a1f9bf228946', - command: 'info', - summary: null, - createdAt: '2022-09-28T18:04:46.000Z', - emptyCommand: false - } - ] - }) - - render() - - fireEvent.click(screen.getByTestId('re-run-command')) - - expect(sendEventTelemetry).toBeCalledWith({ - event: TelemetryEvent.SEARCH_COMMAND_RUN_AGAIN, - eventData: { - command: 'INFO', - databaseId: INSTANCE_ID_MOCK, - mode: 'RAW' - } - }); - (sendEventTelemetry as jest.Mock).mockRestore() - - expect(onSubmit).toBeCalledWith( - 'info', - '9dda0f6d-9265-4b15-b627-82d2eb867605', - { mode: 'RAW' } - ) - }) - - it('should call proper actions on delete', async () => { - const onSubmit = jest.fn() - const sendEventTelemetryMock = jest.fn(); - (sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock); - - (workbenchResultsSelector as jest.Mock).mockReturnValue({ - items: [ - { - mode: 'RAW', - resultsMode: 'DEFAULT', - id: '9dda0f6d-9265-4b15-b627-82d2eb867605', - databaseId: '18c37d1d-bc25-4e46-a20d-a1f9bf228946', - command: 'info', - summary: null, - createdAt: '2022-09-28T18:04:46.000Z', - emptyCommand: false - } - ] - }) - - render() - - fireEvent.click(screen.getByTestId('delete-command')) - - expect(sendEventTelemetry).toBeCalledWith({ - event: TelemetryEvent.SEARCH_CLEAR_RESULT_CLICKED, - eventData: { - command: 'info', - databaseId: INSTANCE_ID_MOCK, - } - }); - (sendEventTelemetry as jest.Mock).mockRestore() - - expect(store.getActions()).toEqual([ - loadWBHistory(), - processWBCommand('9dda0f6d-9265-4b15-b627-82d2eb867605') - ]) - }) - - it('should call proper actions on clear all commands', async () => { - const onSubmit = jest.fn() - const sendEventTelemetryMock = jest.fn(); - (sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock); - - (workbenchResultsSelector as jest.Mock).mockReturnValue({ - items: [ - { - mode: 'RAW', - resultsMode: 'DEFAULT', - id: '9dda0f6d-9265-4b15-b627-82d2eb867605', - databaseId: '18c37d1d-bc25-4e46-a20d-a1f9bf228946', - command: 'info', - summary: null, - createdAt: '2022-09-28T18:04:46.000Z', - emptyCommand: false - } - ] - }) - - render() - - fireEvent.click(screen.getByTestId('clear-history-btn')) - - expect(sendEventTelemetry).toBeCalledWith({ - event: TelemetryEvent.SEARCH_CLEAR_ALL_RESULTS_CLICKED, - eventData: { - databaseId: INSTANCE_ID_MOCK, - } - }); - (sendEventTelemetry as jest.Mock).mockRestore() - - expect(store.getActions()).toEqual([ - loadWBHistory(), - clearWbResults() - ]) - }) -}) diff --git a/redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.tsx b/redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.tsx deleted file mode 100644 index ca85d28c35..0000000000 --- a/redisinsight/ui/src/pages/search/components/results-history/ResultsHistory.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import React, { useEffect } from 'react' -import cx from 'classnames' - -import { useDispatch, useSelector } from 'react-redux' -import { useParams } from 'react-router-dom' - -import { EuiButtonEmpty, EuiProgress } from '@elastic/eui' -import { getCommandsFromQuery, Nullable } from 'uiSrc/utils' -import { QueryCard } from 'uiSrc/components/query' -import { - clearWbResultsAction, - deleteWBCommandAction, - fetchWBCommandAction, - fetchWBHistoryAction, - resetWBHistoryItems, - workbenchResultsSelector -} from 'uiSrc/slices/workbench/wb-results' -import { searchAndQuerySelector } from 'uiSrc/slices/search/searchAndQuery' - -import { CommandExecutionType, RunQueryMode } from 'uiSrc/slices/interfaces' -import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import { ProfileQueryType } from 'uiSrc/pages/workbench/constants' -import { generateProfileQueryForCommand } from 'uiSrc/pages/workbench/utils/profile' -import { CodeButtonParams } from 'uiSrc/constants' -import styles from './styles.module.scss' - -export interface Props { - commandsArray?: string[] - onSubmit: ( - commandInit: string, - commandId?: Nullable, - executeParams?: CodeButtonParams - ) => void -} - -const ResultsHistory = (props: Props) => { - const { commandsArray = [], onSubmit } = props - const { - items, - clearing, - isLoaded - } = useSelector(workbenchResultsSelector) - const { activeRunQueryMode } = useSelector(searchAndQuerySelector) - - const dispatch = useDispatch() - const { instanceId } = useParams<{ instanceId: string }>() - - useEffect(() => { - dispatch(fetchWBHistoryAction(instanceId, CommandExecutionType.Search)) - - return () => { - dispatch(resetWBHistoryItems()) - } - }, []) - - const handleQueryOpen = (commandId: string = '') => { - dispatch(fetchWBCommandAction(commandId)) - } - - const handleQueryDelete = (commandId: string) => { - dispatch(deleteWBCommandAction(commandId)) - } - - const handleAllQueriesDelete = () => { - dispatch(clearWbResultsAction(CommandExecutionType.Search)) - sendEventTelemetry({ - event: TelemetryEvent.SEARCH_CLEAR_ALL_RESULTS_CLICKED, - eventData: { - databaseId: instanceId, - } - }) - } - - const handleQueryReRun = ( - query: string, - commandId?: Nullable, - mode?: RunQueryMode - ) => { - sendEventTelemetry({ - event: TelemetryEvent.SEARCH_COMMAND_RUN_AGAIN, - eventData: { - databaseId: instanceId, - command: getCommandsFromQuery(query, commandsArray) || '', - mode - } - }) - onSubmit(query, commandId, { mode }) - } - - const handleQueryProfile = ( - profileType: ProfileQueryType, - commandExecution: { command: string, mode?: RunQueryMode } - ) => { - const { command, mode } = commandExecution - const profileQuery = generateProfileQueryForCommand(command, profileType) - if (profileQuery) { - onSubmit(profileQuery, null, { mode }) - } - } - - return ( -
- {!isLoaded && ( - - )} - {!!items?.length && ( -
- - Clear Results - -
- )} -
-
- {items?.length ? items.map(( - { - command = '', - isOpen = false, - result = undefined, - summary = undefined, - id = '', - loading, - createdAt, - mode, - emptyCommand, - isNotStored, - executionTime, - db, - } - ) => ( - - handleQueryProfile(profileType, { command, mode })} - onQueryOpen={() => handleQueryOpen(id)} - onQueryReRun={() => handleQueryReRun(command, id, mode)} - onQueryDelete={() => handleQueryDelete(id)} - /> - )) : null} -
-
- ) -} - -export default ResultsHistory diff --git a/redisinsight/ui/src/pages/search/components/results-history/index.ts b/redisinsight/ui/src/pages/search/components/results-history/index.ts deleted file mode 100644 index a38f637f57..0000000000 --- a/redisinsight/ui/src/pages/search/components/results-history/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import ResultsHistory from './ResultsHistory' - -export default ResultsHistory diff --git a/redisinsight/ui/src/pages/search/components/results-history/styles.module.scss b/redisinsight/ui/src/pages/search/components/results-history/styles.module.scss deleted file mode 100644 index 84827c7662..0000000000 --- a/redisinsight/ui/src/pages/search/components/results-history/styles.module.scss +++ /dev/null @@ -1,44 +0,0 @@ -.wrapper { - flex: 1; - height: 100%; - width: 100%; - background-color: var(--euiColorEmptyShade); - border: 1px solid var(--euiColorLightShade); - - display: flex; - flex-direction: column; - - position: relative; -} - -.container { - @include eui.scrollBar; - color: var(--euiTextSubduedColor) !important; - - flex: 1; - width: 100%; - overflow: auto; -} - -.header { - height: 42px; - display: flex; - align-items: center; - justify-content: flex-end; - padding: 0 12px; - - flex-shrink: 0; - border-bottom: 1px solid var(--tableDarkestBorderColor); -} - -.clearAllBtn { - font-size: 14px !important; - - :global { - .euiIcon { - width: 14px !important; - height: 14px !important; - } - } -} - diff --git a/redisinsight/ui/src/pages/search/index.ts b/redisinsight/ui/src/pages/search/index.ts deleted file mode 100644 index 2f6b199b65..0000000000 --- a/redisinsight/ui/src/pages/search/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import SearchPage from './SearchPage' - -export default SearchPage diff --git a/redisinsight/ui/src/pages/search/mocks/mocks.ts b/redisinsight/ui/src/pages/search/mocks/mocks.ts deleted file mode 100644 index 9ebacd0a47..0000000000 --- a/redisinsight/ui/src/pages/search/mocks/mocks.ts +++ /dev/null @@ -1,1521 +0,0 @@ -export const MOCKED_SUPPORTED_COMMANDS = { - 'FT.SEARCH': { - summary: 'Searches the index with a textual query, returning either documents or just ids', - complexity: 'O(N)', - history: [ - [ - '2.0.0', - 'Deprecated `WITHPAYLOADS` and `PAYLOAD` arguments' - ] - ], - arguments: [ - { - name: 'index', - type: 'string' - }, - { - name: 'query', - type: 'string' - }, - { - name: 'nocontent', - type: 'pure-token', - token: 'NOCONTENT', - optional: true - }, - { - name: 'verbatim', - type: 'pure-token', - token: 'VERBATIM', - optional: true - }, - { - name: 'nostopwords', - type: 'pure-token', - token: 'NOSTOPWORDS', - optional: true - }, - { - name: 'withscores', - type: 'pure-token', - token: 'WITHSCORES', - optional: true - }, - { - name: 'withpayloads', - type: 'pure-token', - token: 'WITHPAYLOADS', - optional: true - }, - { - name: 'withsortkeys', - type: 'pure-token', - token: 'WITHSORTKEYS', - optional: true - }, - { - name: 'filter', - type: 'block', - optional: true, - multiple: true, - arguments: [ - { - name: 'numeric_field', - type: 'string', - token: 'FILTER' - }, - { - name: 'min', - type: 'double' - }, - { - name: 'max', - type: 'double' - } - ] - }, - { - name: 'geo_filter', - type: 'block', - optional: true, - multiple: true, - arguments: [ - { - name: 'geo_field', - type: 'string', - token: 'GEOFILTER' - }, - { - name: 'lon', - type: 'double' - }, - { - name: 'lat', - type: 'double' - }, - { - name: 'radius', - type: 'double' - }, - { - name: 'radius_type', - type: 'oneof', - arguments: [ - { - name: 'm', - type: 'pure-token', - token: 'm' - }, - { - name: 'km', - type: 'pure-token', - token: 'km' - }, - { - name: 'mi', - type: 'pure-token', - token: 'mi' - }, - { - name: 'ft', - type: 'pure-token', - token: 'ft' - } - ] - } - ] - }, - { - name: 'in_keys', - type: 'block', - optional: true, - arguments: [ - { - name: 'count', - type: 'string', - token: 'INKEYS' - }, - { - name: 'key', - type: 'string', - multiple: true - } - ] - }, - { - name: 'in_fields', - type: 'block', - optional: true, - arguments: [ - { - name: 'count', - type: 'string', - token: 'INFIELDS' - }, - { - name: 'field', - type: 'string', - multiple: true - } - ] - }, - { - name: 'return', - type: 'block', - optional: true, - arguments: [ - { - name: 'count', - type: 'string', - token: 'RETURN' - }, - { - name: 'identifiers', - type: 'block', - multiple: true, - arguments: [ - { - name: 'identifier', - type: 'string' - }, - { - name: 'property', - type: 'string', - token: 'AS', - optional: true - } - ] - } - ] - }, - { - name: 'summarize', - type: 'block', - optional: true, - arguments: [ - { - name: 'summarize', - type: 'pure-token', - token: 'SUMMARIZE' - }, - { - name: 'fields', - type: 'block', - optional: true, - arguments: [ - { - name: 'count', - type: 'string', - token: 'FIELDS' - }, - { - name: 'field', - type: 'string', - multiple: true - } - ] - }, - { - name: 'num', - type: 'integer', - token: 'FRAGS', - optional: true - }, - { - name: 'fragsize', - type: 'integer', - token: 'LEN', - optional: true - }, - { - name: 'separator', - type: 'string', - token: 'SEPARATOR', - optional: true - } - ] - }, - { - name: 'highlight', - type: 'block', - optional: true, - arguments: [ - { - name: 'highlight', - type: 'pure-token', - token: 'HIGHLIGHT' - }, - { - name: 'fields', - type: 'block', - optional: true, - arguments: [ - { - name: 'count', - type: 'string', - token: 'FIELDS' - }, - { - name: 'field', - type: 'string', - multiple: true - } - ] - }, - { - name: 'tags', - type: 'block', - optional: true, - arguments: [ - { - name: 'tags', - type: 'pure-token', - token: 'TAGS' - }, - { - name: 'open', - type: 'string' - }, - { - name: 'close', - type: 'string' - } - ] - } - ] - }, - { - name: 'slop', - type: 'integer', - optional: true, - token: 'SLOP' - }, - { - name: 'timeout', - type: 'integer', - optional: true, - token: 'TIMEOUT' - }, - { - name: 'inorder', - type: 'pure-token', - token: 'INORDER', - optional: true - }, - { - name: 'language', - type: 'string', - optional: true, - token: 'LANGUAGE' - }, - { - name: 'expander', - type: 'string', - optional: true, - token: 'EXPANDER' - }, - { - name: 'scorer', - type: 'string', - optional: true, - token: 'SCORER' - }, - { - name: 'explainscore', - type: 'pure-token', - token: 'EXPLAINSCORE', - optional: true - }, - { - name: 'payload', - type: 'string', - optional: true, - token: 'PAYLOAD' - }, - { - name: 'sortby', - type: 'block', - optional: true, - arguments: [ - { - name: 'sortby', - type: 'string', - token: 'SORTBY' - }, - { - name: 'order', - type: 'oneof', - optional: true, - arguments: [ - { - name: 'asc', - type: 'pure-token', - token: 'ASC' - }, - { - name: 'desc', - type: 'pure-token', - token: 'DESC' - } - ] - } - ] - }, - { - name: 'limit', - type: 'block', - optional: true, - arguments: [ - { - name: 'limit', - type: 'pure-token', - token: 'LIMIT' - }, - { - name: 'offset', - type: 'integer' - }, - { - name: 'num', - type: 'integer' - } - ] - }, - { - name: 'params', - type: 'block', - optional: true, - arguments: [ - { - name: 'params', - type: 'pure-token', - token: 'PARAMS' - }, - { - name: 'nargs', - type: 'integer' - }, - { - name: 'values', - type: 'block', - multiple: true, - arguments: [ - { - name: 'name', - type: 'string' - }, - { - name: 'value', - type: 'string' - } - ] - } - ] - }, - { - name: 'dialect', - type: 'integer', - optional: true, - token: 'DIALECT', - since: '2.4.3' - } - ], - since: '1.0.0', - group: 'search' - }, - 'FT.AGGREGATE': { - summary: 'Run a search query on an index and perform aggregate transformations on the results', - complexity: 'O(1)', - arguments: [ - { - name: 'index', - type: 'string' - }, - { - name: 'query', - type: 'string' - }, - { - name: 'verbatim', - type: 'pure-token', - token: 'VERBATIM', - optional: true - }, - { - name: 'load', - type: 'block', - optional: true, - arguments: [ - { - name: 'count', - type: 'string', - token: 'LOAD' - }, - { - name: 'field', - type: 'string', - multiple: true - } - ] - }, - { - name: 'timeout', - type: 'integer', - optional: true, - token: 'TIMEOUT' - }, - { - name: 'loadall', - type: 'pure-token', - token: 'LOAD *', - optional: true - }, - { - name: 'groupby', - type: 'block', - optional: true, - multiple: true, - arguments: [ - { - name: 'nargs', - type: 'integer', - token: 'GROUPBY' - }, - { - name: 'property', - type: 'string', - multiple: true - }, - { - name: 'reduce', - type: 'block', - optional: true, - multiple: true, - arguments: [ - { - name: 'function', - type: 'string', - token: 'REDUCE' - }, - { - name: 'nargs', - type: 'integer' - }, - { - name: 'arg', - type: 'string', - multiple: true - }, - { - name: 'name', - type: 'string', - token: 'AS', - optional: true - } - ] - } - ] - }, - { - name: 'sortby', - type: 'block', - optional: true, - arguments: [ - { - name: 'nargs', - type: 'integer', - token: 'SORTBY' - }, - { - name: 'fields', - type: 'block', - optional: true, - multiple: true, - arguments: [ - { - name: 'property', - type: 'string' - }, - { - name: 'order', - type: 'oneof', - arguments: [ - { - name: 'asc', - type: 'pure-token', - token: 'ASC' - }, - { - name: 'desc', - type: 'pure-token', - token: 'DESC' - } - ] - } - ] - }, - { - name: 'num', - type: 'integer', - token: 'MAX', - optional: true - } - ] - }, - { - name: 'apply', - type: 'block', - optional: true, - multiple: true, - arguments: [ - { - name: 'expression', - type: 'string', - token: 'APPLY' - }, - { - name: 'name', - type: 'string', - token: 'AS' - } - ] - }, - { - name: 'limit', - type: 'block', - optional: true, - arguments: [ - { - name: 'limit', - type: 'pure-token', - token: 'LIMIT' - }, - { - name: 'offset', - type: 'integer' - }, - { - name: 'num', - type: 'integer' - } - ] - }, - { - name: 'filter', - type: 'string', - optional: true, - token: 'FILTER' - }, - { - name: 'cursor', - type: 'block', - optional: true, - arguments: [ - { - name: 'withcursor', - type: 'pure-token', - token: 'WITHCURSOR' - }, - { - name: 'read_size', - type: 'integer', - optional: true, - token: 'COUNT' - }, - { - name: 'idle_time', - type: 'integer', - optional: true, - token: 'MAXIDLE' - } - ] - }, - { - name: 'params', - type: 'block', - optional: true, - arguments: [ - { - name: 'params', - type: 'pure-token', - token: 'PARAMS' - }, - { - name: 'nargs', - type: 'integer' - }, - { - name: 'values', - type: 'block', - multiple: true, - arguments: [ - { - name: 'name', - type: 'string' - }, - { - name: 'value', - type: 'string' - } - ] - } - ] - }, - { - name: 'dialect', - type: 'integer', - optional: true, - token: 'DIALECT', - since: '2.4.3' - } - ], - since: '1.1.0', - group: 'search' - }, - 'FT.PROFILE': { - summary: 'Performs a `FT.SEARCH` or `FT.AGGREGATE` command and collects performance information', - complexity: 'O(N)', - arguments: [ - { - name: 'index', - type: 'string' - }, - { - name: 'querytype', - type: 'oneof', - arguments: [ - { - name: 'search', - type: 'pure-token', - token: 'SEARCH' - }, - { - name: 'aggregate', - type: 'pure-token', - token: 'AGGREGATE' - } - ] - }, - { - name: 'limited', - type: 'pure-token', - token: 'LIMITED', - optional: true - }, - { - name: 'queryword', - type: 'pure-token', - token: 'QUERY' - }, - { - name: 'query', - type: 'string' - } - ], - since: '2.2.0', - group: 'search', - provider: 'redisearch' - }, - 'FT.ALIASADD': { - summary: 'Adds an alias to the index', - complexity: 'O(1)', - arguments: [ - { - name: 'alias', - type: 'string' - }, - { - name: 'index', - type: 'string' - } - ], - since: '1.0.0', - group: 'search', - provider: 'redisearch' - }, - 'FT.ALIASDEL': { - summary: 'Deletes an alias from the index', - complexity: 'O(1)', - arguments: [ - { - name: 'alias', - type: 'string' - } - ], - since: '1.0.0', - group: 'search', - provider: 'redisearch' - }, - 'FT.ALIASUPDATE': { - summary: 'Adds or updates an alias to the index', - complexity: 'O(1)', - arguments: [ - { - name: 'alias', - type: 'string' - }, - { - name: 'index', - type: 'string' - } - ], - since: '1.0.0', - group: 'search', - provider: 'redisearch' - }, - 'FT.ALTER': { - summary: 'Adds a new field to the index', - complexity: 'O(N) where N is the number of keys in the keyspace', - arguments: [ - { - name: 'index', - type: 'string' - }, - { - name: 'skipinitialscan', - type: 'pure-token', - token: 'SKIPINITIALSCAN', - optional: true - }, - { - name: 'schema', - type: 'pure-token', - token: 'SCHEMA' - }, - { - name: 'add', - type: 'pure-token', - token: 'ADD' - }, - { - name: 'field', - type: 'string' - }, - { - name: 'options', - type: 'string' - } - ], - since: '1.0.0', - group: 'search', - provider: 'redisearch' - }, - 'FT.CONFIG GET': { - summary: 'Retrieves runtime configuration options', - complexity: 'O(1)', - arguments: [ - { - name: 'option', - type: 'string' - } - ], - since: '1.0.0', - group: 'search', - provider: 'redisearch' - }, - 'FT.CONFIG HELP': { - summary: 'Help description of runtime configuration options', - complexity: 'O(1)', - arguments: [ - { - name: 'option', - type: 'string' - } - ], - since: '1.0.0', - group: 'search', - provider: 'redisearch' - }, - 'FT.CONFIG SET': { - summary: 'Sets runtime configuration options', - complexity: 'O(1)', - arguments: [ - { - name: 'option', - type: 'string' - }, - { - name: 'value', - type: 'string' - } - ], - since: '1.0.0', - group: 'search', - provider: 'redisearch' - }, - 'FT.CREATE': { - summary: 'Creates an index with the given spec', - complexity: 'O(K) at creation where K is the number of fields, O(N) if scanning the keyspace is triggered, where N is the number of keys in the keyspace', - history: [ - [ - '2.0.0', - 'Added `PAYLOAD_FIELD` argument for backward support of `FT.SEARCH` deprecated `WITHPAYLOADS` argument' - ], - [ - '2.0.0', - 'Deprecated `PAYLOAD_FIELD` argument' - ] - ], - arguments: [ - { - name: 'index', - type: 'string' - }, - { - name: 'data_type', - token: 'ON', - type: 'oneof', - arguments: [ - { - name: 'hash', - type: 'pure-token', - token: 'HASH' - }, - { - name: 'json', - type: 'pure-token', - token: 'JSON' - } - ], - optional: true - }, - { - name: 'prefix', - type: 'block', - optional: true, - arguments: [ - { - name: 'count', - type: 'integer', - token: 'PREFIX' - }, - { - name: 'prefix', - type: 'string', - multiple: true - } - ] - }, - { - name: 'filter', - type: 'string', - optional: true, - token: 'FILTER' - }, - { - name: 'default_lang', - type: 'string', - token: 'LANGUAGE', - optional: true - }, - { - name: 'lang_attribute', - type: 'string', - token: 'LANGUAGE_FIELD', - optional: true - }, - { - name: 'default_score', - type: 'double', - token: 'SCORE', - optional: true - }, - { - name: 'score_attribute', - type: 'string', - token: 'SCORE_FIELD', - optional: true - }, - { - name: 'payload_attribute', - type: 'string', - token: 'PAYLOAD_FIELD', - optional: true - }, - { - name: 'maxtextfields', - type: 'pure-token', - token: 'MAXTEXTFIELDS', - optional: true - }, - { - name: 'seconds', - type: 'double', - token: 'TEMPORARY', - optional: true - }, - { - name: 'nooffsets', - type: 'pure-token', - token: 'NOOFFSETS', - optional: true - }, - { - name: 'nohl', - type: 'pure-token', - token: 'NOHL', - optional: true - }, - { - name: 'nofields', - type: 'pure-token', - token: 'NOFIELDS', - optional: true - }, - { - name: 'nofreqs', - type: 'pure-token', - token: 'NOFREQS', - optional: true - }, - { - name: 'stopwords', - type: 'block', - optional: true, - token: 'STOPWORDS', - arguments: [ - { - name: 'count', - type: 'integer' - }, - { - name: 'stopword', - type: 'string', - multiple: true, - optional: true - } - ] - }, - { - name: 'skipinitialscan', - type: 'pure-token', - token: 'SKIPINITIALSCAN', - optional: true - }, - { - name: 'schema', - type: 'pure-token', - token: 'SCHEMA' - }, - { - name: 'field', - type: 'block', - multiple: true, - arguments: [ - { - name: 'field_name', - type: 'string' - }, - { - name: 'alias', - type: 'string', - token: 'AS', - optional: true - }, - { - name: 'field_type', - type: 'oneof', - arguments: [ - { - name: 'text', - type: 'pure-token', - token: 'TEXT' - }, - { - name: 'tag', - type: 'pure-token', - token: 'TAG' - }, - { - name: 'numeric', - type: 'pure-token', - token: 'NUMERIC' - }, - { - name: 'geo', - type: 'pure-token', - token: 'GEO' - }, - { - name: 'vector', - type: 'pure-token', - token: 'VECTOR' - } - ] - }, - { - name: 'withsuffixtrie', - type: 'pure-token', - token: 'WITHSUFFIXTRIE', - optional: true - }, - { - name: 'INDEXEMPTY', - type: 'pure-token', - token: 'INDEXEMPTY', - optional: true - }, - { - name: 'indexmissing', - type: 'pure-token', - token: 'INDEXMISSING', - optional: true - }, - { - name: 'sortable', - type: 'block', - optional: true, - arguments: [ - { - name: 'sortable', - type: 'pure-token', - token: 'SORTABLE' - }, - { - name: 'UNF', - type: 'pure-token', - token: 'UNF', - optional: true - } - ] - }, - { - name: 'noindex', - type: 'pure-token', - token: 'NOINDEX', - optional: true - } - ] - } - ], - since: '1.0.0', - group: 'search', - provider: 'redisearch' - }, - 'FT.CURSOR DEL': { - summary: 'Deletes a cursor', - complexity: 'O(1)', - arguments: [ - { - name: 'index', - type: 'string' - }, - { - name: 'cursor_id', - type: 'integer' - } - ], - since: '1.1.0', - group: 'search', - provider: 'redisearch' - }, - 'FT.CURSOR READ': { - summary: 'Reads from a cursor', - complexity: 'O(1)', - arguments: [ - { - name: 'index', - type: 'string' - }, - { - name: 'cursor_id', - type: 'integer' - }, - { - name: 'read size', - type: 'integer', - optional: true, - token: 'COUNT' - } - ], - since: '1.1.0', - group: 'search', - provider: 'redisearch' - }, - 'FT.DICTADD': { - summary: 'Adds terms to a dictionary', - complexity: 'O(1)', - arguments: [ - { - name: 'dict', - type: 'string' - }, - { - name: 'term', - type: 'string', - multiple: true - } - ], - since: '1.4.0', - group: 'search', - provider: 'redisearch' - }, - 'FT.DICTDEL': { - summary: 'Deletes terms from a dictionary', - complexity: 'O(1)', - arguments: [ - { - name: 'dict', - type: 'string' - }, - { - name: 'term', - type: 'string', - multiple: true - } - ], - since: '1.4.0', - group: 'search', - provider: 'redisearch' - }, - 'FT.DICTDUMP': { - summary: 'Dumps all terms in the given dictionary', - complexity: 'O(N), where N is the size of the dictionary', - arguments: [ - { - name: 'dict', - type: 'string' - } - ], - since: '1.4.0', - group: 'search', - provider: 'redisearch' - }, - 'FT.DROPINDEX': { - summary: 'Deletes the index', - complexity: 'O(1) or O(N) if documents are deleted, where N is the number of keys in the keyspace', - arguments: [ - { - name: 'index', - type: 'string' - }, - { - name: 'delete docs', - type: 'oneof', - arguments: [ - { - name: 'delete docs', - type: 'pure-token', - token: 'DD' - } - ], - optional: true - } - ], - since: '2.0.0', - group: 'search', - provider: 'redisearch' - }, - 'FT.EXPLAIN': { - summary: 'Returns the execution plan for a complex query', - complexity: 'O(1)', - arguments: [ - { - name: 'index', - type: 'string' - }, - { - name: 'query', - type: 'string' - }, - { - name: 'dialect', - type: 'integer', - optional: true, - token: 'DIALECT', - since: '2.4.3' - } - ], - since: '1.0.0', - group: 'search', - provider: 'redisearch' - }, - 'FT.EXPLAINCLI': { - summary: 'Returns the execution plan for a complex query', - complexity: 'O(1)', - arguments: [ - { - name: 'index', - type: 'string' - }, - { - name: 'query', - type: 'string' - }, - { - name: 'dialect', - type: 'integer', - optional: true, - token: 'DIALECT', - since: '2.4.3' - } - ], - since: '1.0.0', - group: 'search', - provider: 'redisearch' - }, - 'FT.INFO': { - summary: 'Returns information and statistics on the index', - complexity: 'O(1)', - arguments: [ - { - name: 'index', - type: 'string' - } - ], - since: '1.0.0', - group: 'search', - provider: 'redisearch' - }, - 'FT.SPELLCHECK': { - summary: 'Performs spelling correction on a query, returning suggestions for misspelled terms', - complexity: 'O(1)', - arguments: [ - { - name: 'index', - type: 'string' - }, - { - name: 'query', - type: 'string' - }, - { - name: 'distance', - token: 'DISTANCE', - type: 'integer', - optional: true - }, - { - name: 'terms', - token: 'TERMS', - type: 'block', - optional: true, - arguments: [ - { - name: 'inclusion', - type: 'oneof', - arguments: [ - { - name: 'include', - type: 'pure-token', - token: 'INCLUDE' - }, - { - name: 'exclude', - type: 'pure-token', - token: 'EXCLUDE' - } - ] - }, - { - name: 'dictionary', - type: 'string' - }, - { - name: 'terms', - type: 'string', - multiple: true, - optional: true - } - ] - }, - { - name: 'dialect', - type: 'integer', - optional: true, - token: 'DIALECT', - since: '2.4.3' - } - ], - since: '1.4.0', - group: 'search', - provider: 'redisearch' - }, - 'FT.SUGADD': { - summary: 'Adds a suggestion string to an auto-complete suggestion dictionary', - complexity: 'O(1)', - history: [ - [ - '2.0.0', - 'Deprecated `PAYLOAD` argument' - ] - ], - arguments: [ - { - name: 'key', - type: 'string' - }, - { - name: 'string', - type: 'string' - }, - { - name: 'score', - type: 'double' - }, - { - name: 'increment score', - type: 'oneof', - arguments: [ - { - name: 'incr', - type: 'pure-token', - token: 'INCR' - } - ], - optional: true - }, - { - name: 'payload', - token: 'PAYLOAD', - type: 'string', - optional: true - } - ], - since: '1.0.0', - group: 'suggestion', - provider: 'redisearch' - }, - 'FT.SUGDEL': { - summary: 'Deletes a string from a suggestion index', - complexity: 'O(1)', - arguments: [ - { - name: 'key', - type: 'string' - }, - { - name: 'string', - type: 'string' - } - ], - since: '1.0.0', - group: 'suggestion', - provider: 'redisearch' - }, - 'FT.SUGGET': { - summary: 'Gets completion suggestions for a prefix', - complexity: 'O(1)', - history: [ - [ - '2.0.0', - 'Deprecated `WITHPAYLOADS` argument' - ] - ], - arguments: [ - { - name: 'key', - type: 'string' - }, - { - name: 'prefix', - type: 'string' - }, - { - name: 'fuzzy', - type: 'pure-token', - token: 'FUZZY', - optional: true - }, - { - name: 'withscores', - type: 'pure-token', - token: 'WITHSCORES', - optional: true - }, - { - name: 'withpayloads', - type: 'pure-token', - token: 'WITHPAYLOADS', - optional: true - }, - { - name: 'max', - token: 'MAX', - type: 'integer', - optional: true - } - ], - since: '1.0.0', - group: 'suggestion', - provider: 'redisearch' - }, - 'FT.SUGLEN': { - summary: 'Gets the size of an auto-complete suggestion dictionary', - complexity: 'O(1)', - arguments: [ - { - name: 'key', - type: 'string' - } - ], - since: '1.0.0', - group: 'suggestion', - provider: 'redisearch' - }, - 'FT.SYNDUMP': { - summary: 'Dumps the contents of a synonym group', - complexity: 'O(1)', - arguments: [ - { - name: 'index', - type: 'string' - } - ], - since: '1.2.0', - group: 'search', - provider: 'redisearch' - }, - 'FT.SYNUPDATE': { - summary: 'Creates or updates a synonym group with additional terms', - complexity: 'O(1)', - arguments: [ - { - name: 'index', - type: 'string' - }, - { - name: 'synonym_group_id', - type: 'string' - }, - { - name: 'skipinitialscan', - type: 'pure-token', - token: 'SKIPINITIALSCAN', - optional: true - }, - { - name: 'term', - type: 'string', - multiple: true - } - ], - since: '1.2.0', - group: 'search', - provider: 'redisearch' - }, - 'FT.TAGVALS': { - summary: 'Returns the distinct tags indexed in a Tag field', - complexity: 'O(N)', - arguments: [ - { - name: 'index', - type: 'string' - }, - { - name: 'field_name', - type: 'string' - } - ], - since: '1.0.0', - group: 'search', - provider: 'redisearch' - }, - 'FT._LIST': { - summary: 'Returns a list of all existing indexes', - complexity: 'O(1)', - since: '2.0.0', - group: 'search', - provider: 'redisearch' - }, -} diff --git a/redisinsight/ui/src/pages/search/styles.module.scss b/redisinsight/ui/src/pages/search/styles.module.scss deleted file mode 100644 index 160dcb9b75..0000000000 --- a/redisinsight/ui/src/pages/search/styles.module.scss +++ /dev/null @@ -1,32 +0,0 @@ -.container { - flex-grow: 1; - display: flex; - flex-direction: column; - max-height: 100%; -} - -.main { - display: flex; - flex: 1; - padding: 0 16px 0; - height: 100%; - width: 100%; -} - -.content { - display: flex; - flex-grow: 1; - width: 100%; -} - -.resizeButton { - z-index: 1 !important; -} - -.queryPanel { - padding-bottom: 8px; -} - -.queryResultsPanel { - padding-top: 8px; -} diff --git a/redisinsight/ui/src/pages/search/types.ts b/redisinsight/ui/src/pages/search/types.ts deleted file mode 100644 index f1d9287048..0000000000 --- a/redisinsight/ui/src/pages/search/types.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Maybe } from 'uiSrc/utils' - -export enum TokenType { - PureToken = 'pure-token', - Block = 'block', - OneOf = 'oneof', - String = 'string', -} - -export enum ArgName { - NArgs = 'nargs' -} - -export interface SearchCommand { - name?: string - summary?: string - expression?: boolean - type?: TokenType - token?: string - optional?: boolean - multiple?: boolean - arguments?: SearchCommand[] -} - -export interface SearchCommandTree extends SearchCommand { - parent?: SearchCommandTree -} - -export interface FoundCommandArgument { - isComplete: boolean - stopArg: Maybe - isBlocked: boolean - append: Maybe> - parent: Maybe -} - -export interface CursorContext { - prevCursorChar: string - nextCursorChar: string - isCursorInQuotes: boolean - currentOffsetArg: string - offset: number - argLeftOffset: number - argRightOffset: number -} diff --git a/redisinsight/ui/src/pages/search/utils/index.ts b/redisinsight/ui/src/pages/search/utils/index.ts deleted file mode 100644 index c820377353..0000000000 --- a/redisinsight/ui/src/pages/search/utils/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './query' -export * from './monaco' diff --git a/redisinsight/ui/src/pages/search/utils/monaco.ts b/redisinsight/ui/src/pages/search/utils/monaco.ts deleted file mode 100644 index 4eda87a868..0000000000 --- a/redisinsight/ui/src/pages/search/utils/monaco.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { monaco } from 'react-monaco-editor' -import * as monacoEditor from 'monaco-editor' -import { isString } from 'lodash' -import { generateDetail } from 'uiSrc/pages/search/utils/query' -import { SearchCommand, TokenType } from 'uiSrc/pages/search/types' -import { Maybe } from 'uiSrc/utils' - -export const setCursorPositionAtTheEnd = (editor: monacoEditor.editor.IStandaloneCodeEditor) => { - if (!editor) return - - const rows = editor.getValue().split('\n') - - editor.setPosition({ - column: rows[rows.length - 1].trimEnd().length + 1, - lineNumber: rows.length - }) - - editor.focus() -} - -export const getRange = (position: monaco.Position, word: monaco.editor.IWordAtPosition): monaco.IRange => ({ - startLineNumber: position.lineNumber, - endLineNumber: position.lineNumber, - endColumn: word.endColumn, - startColumn: word.startColumn, -}) - -export const buildSuggestion = (arg: SearchCommand, range: monaco.IRange, options: any = {}) => { - const extraQuotes = arg.expression ? '\'$1\'' : '' - return { - label: isString(arg) ? arg : arg.token || arg.arguments?.[0].token || arg.name || '', - insertText: `${arg.token || arg.arguments?.[0].token || arg.name?.toUpperCase() || ''} ${extraQuotes}`, - insertTextRules: monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet, - range, - kind: options?.kind || monacoEditor.languages.CompletionItemKind.Function, - ...options, - } -} - -export const getRediSearchSignutureProvider = (options: Maybe<{ - isOpen: boolean - currentArg: SearchCommand - parent: Maybe -}>) => { - const { isOpen, currentArg, parent } = options || {} - if (!isOpen) return null - - const label = generateDetail(parent) - const arg = currentArg?.type === TokenType.Block - ? currentArg?.arguments?.[0]?.name - : (currentArg?.name || currentArg?.type || '') - - return { - dispose: () => {}, - value: { - activeParameter: 0, - activeSignature: 0, - signatures: [{ - label: label || '', - parameters: [{ label: arg }] - }] - } - } -} diff --git a/redisinsight/ui/src/pages/search/utils/query.ts b/redisinsight/ui/src/pages/search/utils/query.ts deleted file mode 100644 index b3e0f984f6..0000000000 --- a/redisinsight/ui/src/pages/search/utils/query.ts +++ /dev/null @@ -1,479 +0,0 @@ -/* eslint-disable no-continue */ - -import { isNumber, toNumber } from 'lodash' -import { generateArgsNames, Maybe, Nullable } from 'uiSrc/utils' -import { CommandProvider } from 'uiSrc/constants' -import { COMPOSITE_ARGS } from 'uiSrc/pages/search/components/query/constants' -import { ArgName, FoundCommandArgument, SearchCommand, SearchCommandTree, TokenType } from '../types' - -export const splitQueryByArgs = (query: string, position: number = 0) => { - const args: [string[], string[]] = [[], []] - let arg = '' - let inQuotes = false - let escapeNextChar = false - let quoteChar = '' - let isCursorInQuotes = false - let lastArg = '' - let argLeftOffset = 0 - let argRightOffset = 0 - - const pushToProperTuple = (isAfterOffset: boolean, arg: string) => { - lastArg = arg - isAfterOffset ? args[1].push(arg) : args[0].push(arg) - } - - const updateLastArgument = (isAfterOffset: boolean, arg: string) => { - const argsBySide = args[isAfterOffset ? 1 : 0] - argsBySide[argsBySide.length - 1] = `${argsBySide[argsBySide.length - 1]} ${arg}` - } - - const updateArgOffsets = (left: number, right: number) => { - argLeftOffset = left - argRightOffset = right - } - - for (let i = 0; i < query.length; i++) { - const char = query[i] - const isAfterOffset = i >= position + (inQuotes ? -1 : 0) - - if (escapeNextChar) { - arg += char - escapeNextChar = !quoteChar - } else if (char === '\\') { - escapeNextChar = true - } else if (inQuotes) { - if (char === quoteChar) { - inQuotes = false - const argWithChat = arg + char - - if (isAfterOffset && !argLeftOffset) { - updateArgOffsets(i - arg.length, i + 1) - } - - if (isCompositeArgument(argWithChat, lastArg)) { - updateLastArgument(isAfterOffset, argWithChat) - } else { - pushToProperTuple(isAfterOffset, argWithChat) - } - - arg = '' - } else { - arg += char - } - } else if (char === '"' || char === "'") { - inQuotes = true - quoteChar = char - arg += char - } else if (char === ' ' || char === '\n') { - if (arg.length > 0) { - if (isAfterOffset && !argLeftOffset) { - updateArgOffsets(i - arg.length, i) - } - - if (isCompositeArgument(arg, lastArg)) { - updateLastArgument(isAfterOffset, arg) - } else { - pushToProperTuple(isAfterOffset, arg) - } - - arg = '' - } - } else { - arg += char - } - - if (i === position - 1) isCursorInQuotes = inQuotes - } - - if (arg.length > 0) { - if (!argLeftOffset) updateArgOffsets(query.length - arg.length, query.length) - pushToProperTuple(true, arg) - } - - const cursor = { - isCursorInQuotes, - prevCursorChar: query[position - 1]?.trim() || '', - nextCursorChar: query[position]?.trim() || '', - argLeftOffset, - argRightOffset - } - - return { args, cursor } -} - -export const findCurrentArgument = ( - args: SearchCommand[], - prev: string[], - parent?: SearchCommandTree -): Nullable => { - for (let i = prev.length - 1; i >= 0; i--) { - const arg = prev[i] - const currentArg = findArgByToken(args, arg) - const currentWithParent: SearchCommandTree = { ...currentArg, parent } - - if (currentArg?.arguments && currentArg?.type === TokenType.Block) { - return findCurrentArgument(currentArg.arguments, prev.slice(i), currentWithParent) - } - - const tokenIndex = args.findIndex((cArg) => - cArg.token?.toLowerCase() === arg.toLowerCase()) - const token = args[tokenIndex] - - if (token) { - const pastArgs = prev.slice(i) - const commandArgs = parent ? args.slice(tokenIndex, args.length) : [token] - - // getArgByRest - here we preparing the list of arguments which can be inserted, - // this is the main function which creates the list of arguments - return { - ...getArgumentSuggestions({ tokenArgs: pastArgs, levelArgs: prev }, commandArgs, parent), - parent: parent || token - } - } - } - - return null -} - -const findStopArgumentInQuery = ( - queryArgs: string[], - restCommandArgs: Maybe = [], -): { - restArguments: SearchCommand[] - stopArgIndex: number - argumentsIntered?: number - isBlocked: boolean - parent?: SearchCommand -} => { - let currentCommandArgIndex = 0 - let argumentsIntered = 0 - let isBlockedOnCommand = false - let multipleIndexStart = 0 - let multipleCountNumber = 0 - - const moveToNextCommandArg = () => { - currentCommandArgIndex++ - argumentsIntered++ - } - const blockCommand = () => { isBlockedOnCommand = true } - const unBlockCommand = () => { isBlockedOnCommand = false } - - const skipArg = () => { - argumentsIntered -= 1 - moveToNextCommandArg() - unBlockCommand() - } - - for (let i = 0; i < queryArgs.length; i++) { - const arg = queryArgs[i] - const currentCommandArg = restCommandArgs[currentCommandArgIndex] - - if (currentCommandArg?.type === TokenType.PureToken) { - skipArg() - continue - } - - if (!isBlockedOnCommand && currentCommandArg?.optional) { - const isNotToken = currentCommandArg?.token && currentCommandArg.token !== arg.toUpperCase() - const isNotOneOfToken = !currentCommandArg?.token && currentCommandArg?.type === TokenType.OneOf - && currentCommandArg?.arguments?.every(({ token }) => token !== arg.toUpperCase()) - - if (isNotToken || isNotOneOfToken) { - moveToNextCommandArg() - skipArg() - continue - } - } - - if (currentCommandArg?.type === TokenType.Block) { - let blockArguments = currentCommandArg.arguments ? [...currentCommandArg.arguments] : [] - const nArgs = toNumber(queryArgs[i - 1]) || 0 - - // if block is multiple - we duplicate nArgs inner arguments - if (currentCommandArg?.multiple && nArgs) { - blockArguments = Array(nArgs).fill(currentCommandArg.arguments).flat() - } - - const currentQueryArg = queryArgs.slice(i)?.[0]?.toUpperCase() - const isBlockHasToken = blockArguments?.[0]?.token === currentQueryArg - - if (currentCommandArg.token && !isBlockHasToken && currentQueryArg) { - blockArguments.unshift({ - type: TokenType.PureToken, - token: currentQueryArg - }) - } - - const blockSuggestion = findStopArgumentInQuery(queryArgs.slice(i), blockArguments) - const stopArg = blockSuggestion.restArguments?.[blockSuggestion.stopArgIndex] - const { argumentsIntered } = blockSuggestion - - if (nArgs && currentCommandArg?.multiple && isNumber(argumentsIntered) && argumentsIntered >= nArgs) { - i += queryArgs.slice(i).length - 1 - skipArg() - continue - } - - if (blockSuggestion.isBlocked || stopArg) { - return { - ...blockSuggestion, - parent: currentCommandArg - } - } - - i += queryArgs.slice(i).length - 1 - skipArg() - continue - } - - // if we are on token - that requires one more argument - if (currentCommandArg?.token === arg.toUpperCase()) { - blockCommand() - continue - } - - if (currentCommandArg?.name === ArgName.NArgs) { - const numberOfArgs = toNumber(arg) - - if (numberOfArgs === 0) { - moveToNextCommandArg() - skipArg() - continue - } - - moveToNextCommandArg() - blockCommand() - continue - } - - if (currentCommandArg?.type === TokenType.OneOf && currentCommandArg?.optional) { - // if oneof is optional then we can switch to another argument - if (!currentCommandArg?.arguments?.some(({ token }) => token === arg)) { - moveToNextCommandArg() - } - - skipArg() - continue - } - - if (currentCommandArg?.multiple) { - if (!multipleIndexStart) { - multipleCountNumber = toNumber(queryArgs[i - 1]) - multipleIndexStart = i - 1 - } - - if (i - multipleIndexStart >= multipleCountNumber) { - skipArg() - multipleIndexStart = 0 - continue - } - - blockCommand() - continue - } - - moveToNextCommandArg() - - isBlockedOnCommand = false - } - - return { - restArguments: restCommandArgs, - stopArgIndex: currentCommandArgIndex, - argumentsIntered, - isBlocked: isBlockedOnCommand - } -} - -export const getArgumentSuggestions = ( - { tokenArgs, levelArgs }: { - tokenArgs: string[], - levelArgs: string[] - }, - pastCommandArgs: SearchCommand[], - current?: SearchCommandTree -): { - isComplete: boolean - stopArg: Maybe, - isBlocked: boolean, - append: Array, -} => { - const { - restArguments, - stopArgIndex, - isBlocked: isWasBlocked, - parent - } = findStopArgumentInQuery(tokenArgs, pastCommandArgs) - - const prevArg = restArguments[stopArgIndex - 1] - const stopArgument = restArguments[stopArgIndex] - const restNotFilledArgs = restArguments.slice(stopArgIndex) - - const isOneOfArgument = stopArgument?.type === TokenType.OneOf - || (stopArgument?.type === TokenType.PureToken && current?.parent?.type === TokenType.OneOf) - - if (isWasBlocked) { - return { - isComplete: false, - stopArg: stopArgument, - isBlocked: !isOneOfArgument, - append: isOneOfArgument ? [stopArgument.arguments!] : [], - } - } - - const isPrevArgWasMandatory = prevArg && !prevArg.optional - if (isPrevArgWasMandatory && stopArgument && !stopArgument.optional) { - const isCanAppend = stopArgument?.token || isOneOfArgument - const append = isCanAppend ? [[isOneOfArgument ? stopArgument.arguments! : stopArgument].flat()] : [] - - return { - isComplete: false, - stopArg: stopArgument, - isBlocked: !isCanAppend, - append, - } - } - - // if we finished argument - stopArgument will be undefined, then we get it as token - const lastArgument = stopArgument ?? restArguments[0] - const isBlockHasParent = current?.arguments?.some(({ name }) => parent?.name && name === parent?.name) - const foundParent = isBlockHasParent ? { ...parent, parent: current } : (parent || current) - - const isBlockComplete = !stopArgument && current?.name === lastArgument?.name - const beforeMandatoryOptionalArgs = getAllRestArguments(foundParent, lastArgument, levelArgs, isBlockComplete) - const requiredArgsLength = restNotFilledArgs.filter((arg) => !arg.optional).length - - return { - isComplete: requiredArgsLength === 0, - stopArg: stopArgument, - isBlocked: false, - append: beforeMandatoryOptionalArgs, - } -} - -export const getRestArguments = ( - current: Maybe, - stopArgument: Nullable -): SearchCommandTree[] => { - const argumentIndexInArg = current?.arguments - ?.findIndex(({ name }) => name === stopArgument?.name) - const nextMandatoryIndex = argumentIndexInArg && argumentIndexInArg > -1 ? current?.arguments - ?.findIndex(({ optional }, i) => !optional && i > argumentIndexInArg) : -1 - const prevMandatory = current?.arguments?.slice(0, argumentIndexInArg).reverse() - .find(({ optional }) => !optional) - const prevMandatoryIndex = current?.arguments?.findIndex(({ name }) => name === prevMandatory?.name) - - const beforeMandatoryOptionalArgs = ( - nextMandatoryIndex && nextMandatoryIndex > -1 - ? current?.arguments?.slice(prevMandatoryIndex, nextMandatoryIndex) - : current?.arguments?.slice((prevMandatoryIndex || 0) + 1) - ) || [] - - const nextMandatoryArg = nextMandatoryIndex && nextMandatoryIndex > -1 - ? current?.arguments?.[nextMandatoryIndex] - : undefined - - if (nextMandatoryArg?.token) { - beforeMandatoryOptionalArgs.unshift(nextMandatoryArg) - } - - if (nextMandatoryArg?.type === TokenType.OneOf) { - beforeMandatoryOptionalArgs.unshift(...(nextMandatoryArg.arguments || [])) - } - - return beforeMandatoryOptionalArgs.map((arg) => ({ ...arg, parent: current })) -} - -export const getAllRestArguments = ( - current: Maybe, - stopArgument: Nullable, - prevStringArgs: string[] = [], - skipLevel = false -) => { - const appendArgs: Array = [] - const currentLvlNextArgs = removeNotSuggestedArgs( - prevStringArgs, - getRestArguments(current, stopArgument) - ) - - if (!skipLevel) { - appendArgs.push(fillArgsByType(currentLvlNextArgs)) - } - - if (current?.parent) { - const parentArgs = getAllRestArguments(current.parent, current, skipLevel ? prevStringArgs : []) - if (parentArgs?.length) { - appendArgs.push(...parentArgs) - } - } - - return appendArgs -} - -export const removeNotSuggestedArgs = (args: string[], commandArgs: SearchCommandTree[]) => - commandArgs.filter((arg) => { - if (arg.token && arg.multiple) return true - - if (arg.type === TokenType.OneOf) { - return !args - .some((queryArg) => arg.arguments - ?.some((oneOfArg) => oneOfArg.token?.toUpperCase() === queryArg.toUpperCase())) - } - - if (arg.type === TokenType.Block) { - return arg.arguments?.[0]?.token && !args.includes(arg.arguments?.[0]?.token?.toUpperCase()) - } - - return arg.token && !args.includes(arg.token) - }) - -export const fillArgsByType = (args: SearchCommand[], expandBlock = true): SearchCommandTree[] => { - const result: SearchCommandTree[] = [] - - for (let i = 0; i < args.length; i++) { - const currentArg = args[i] - - if (expandBlock && currentArg.type === TokenType.OneOf && !currentArg.token) { - result.push(...(currentArg?.arguments?.map((arg) => ({ ...arg, parent: currentArg })) || [])) - } - - if (currentArg.type === TokenType.Block) { - result.push({ - multiple: currentArg.multiple, - optional: currentArg.optional, - parent: currentArg, - ...(currentArg?.arguments?.[0] as SearchCommand || {}), - }) - } - if (currentArg.token) result.push(currentArg) - } - - return result -} - -export const findArgByToken = (list: SearchCommand[], arg: string): Maybe => - list.find((cArg) => - (cArg.type === TokenType.OneOf - ? cArg.arguments?.some((oneOfArg: SearchCommand) => oneOfArg?.token?.toLowerCase() === arg?.toLowerCase()) - : cArg.arguments?.[0]?.token?.toLowerCase() === arg.toLowerCase())) - -export const isCompositeArgument = (arg: string, prevArg?: string, args: string[] = []) => - args.includes([prevArg?.toUpperCase(), arg?.toUpperCase()].join(' ')) - -export const generateDetail = (command: Maybe) => { - if (!command) return '' - if (command.arguments) return generateArgsNames(CommandProvider.Main, command.arguments).join(' ') - if (command.token) { - if (command.type === TokenType.PureToken) return command.token - return `${command.token}` - } - - return '' -} - -export const addOwnTokenToArgs = (token: string, command: SearchCommand) => { - if (command.arguments) { - return ({ ...command, arguments: [{ token, type: TokenType.PureToken }, ...command.arguments] }) - } - return command -} diff --git a/redisinsight/ui/src/pages/search/utils/tests/monaco.spec.ts b/redisinsight/ui/src/pages/search/utils/tests/monaco.spec.ts deleted file mode 100644 index 48b9ff5c3e..0000000000 --- a/redisinsight/ui/src/pages/search/utils/tests/monaco.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { getRediSearchSignutureProvider } from 'uiSrc/pages/search/utils' -import { MOCKED_SUPPORTED_COMMANDS } from 'uiSrc/pages/search/mocks/mocks' -import { SearchCommand } from 'uiSrc/pages/search/types' - -const ftAggregateCommand = MOCKED_SUPPORTED_COMMANDS['FT.AGGREGATE'] - -const getRediSearchSignutureProviderTests = [ - { - input: { - isOpen: false, - currentArg: {}, - parent: {} - }, - result: null - }, - { - input: { - isOpen: true, - currentArg: ftAggregateCommand.arguments.find(({ name }) => name === 'groupby') as SearchCommand, - parent: null - }, - result: { - dispose: expect.any(Function), - value: { - activeParameter: 0, - activeSignature: 0, - signatures: [{ - label: '', - parameters: [{ label: 'nargs' }] - }] - } - } - }, - { - input: { - isOpen: true, - currentArg: { name: 'expression' }, - parent: ftAggregateCommand.arguments.find(({ name }) => name === 'apply') as SearchCommand - }, - result: { - dispose: expect.any(Function), - value: { - activeParameter: 0, - activeSignature: 0, - signatures: [{ - label: 'APPLY expression AS name', - parameters: [{ label: 'expression' }] - }] - } - } - } -] - -describe('getRediSearchSignutureProvider', () => { - it.each(getRediSearchSignutureProviderTests)('should properly return result', ({ input, result }) => { - const testResult = getRediSearchSignutureProvider(input) - - expect(result).toEqual(testResult) - }) -}) diff --git a/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts b/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts deleted file mode 100644 index 0c3fcd858d..0000000000 --- a/redisinsight/ui/src/pages/search/utils/tests/query.spec.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { addOwnTokenToArgs, findCurrentArgument, generateDetail, splitQueryByArgs } from 'uiSrc/pages/search/utils' -import { SearchCommand, TokenType } from 'uiSrc/pages/search/types' -import { Maybe } from 'uiSrc/utils' -import { - commonfindCurrentArgumentCases, - findArgumentftAggreageTests, - findArgumentftSearchTests -} from './test-cases' -import { MOCKED_SUPPORTED_COMMANDS } from '../../mocks/mocks' - -const ftSearchCommand = MOCKED_SUPPORTED_COMMANDS['FT.SEARCH'] -const ftAggregateCommand = MOCKED_SUPPORTED_COMMANDS['FT.AGGREGATE'] -const COMMANDS = Object.keys(MOCKED_SUPPORTED_COMMANDS).map((name) => ({ - name, - ...MOCKED_SUPPORTED_COMMANDS[name] -})) - -describe('findCurrentArgument', () => { - describe('with list of commands', () => { - commonfindCurrentArgumentCases.forEach(({ input, result, appendIncludes, appendNotIncludes }) => { - it(`should return proper suggestions for ${input}`, () => { - const { args } = splitQueryByArgs(input) - const COMMANDS_LIST = COMMANDS.map((command) => ({ - ...addOwnTokenToArgs(command.name!, command), - token: command.name!, - type: TokenType.Block - })) - - const testResult = findCurrentArgument( - COMMANDS_LIST, - args.flat() - ) - expect(testResult).toEqual(result) - expect( - testResult?.append?.flat()?.map((arg) => arg.token) - ).toEqual( - expect.arrayContaining(appendIncludes) - ) - - if (appendNotIncludes) { - appendNotIncludes.forEach((token) => { - expect( - testResult?.append?.flat()?.map((arg) => arg.token) - ).not.toEqual( - expect.arrayContaining([token]) - ) - }) - } - }) - }) - }) - - describe('FT.AGGREGATE', () => { - findArgumentftAggreageTests.forEach(({ args, result: testResult }) => { - it(`should return proper suggestions for ${args.join(' ')}`, () => { - const result = findCurrentArgument( - ftAggregateCommand.arguments as SearchCommand[], - args - ) - expect(testResult).toEqual(result) - }) - }) - }) - - describe('FT.SEARCH', () => { - findArgumentftSearchTests.forEach(({ args, result: testResult }) => { - it(`should return proper suggestions for ${args.join(' ')}`, () => { - const result = findCurrentArgument( - ftSearchCommand.arguments as SearchCommand[], - args - ) - expect(testResult).toEqual(result) - }) - }) - }) -}) - -const splitQueryByArgsTests: Array<{ - input: [string, number?] - result: any -}> = [ - { - input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS'], - result: { - args: [[], ['FT.SEARCH', '"idx:bicycle"', '""', 'WITHSORTKEYS']], - cursor: { - argLeftOffset: 10, - argRightOffset: 23, - isCursorInQuotes: false, - nextCursorChar: 'F', - prevCursorChar: '' - } - } - }, - { - input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS', 17], - result: { - args: [['FT.SEARCH'], ['"idx:bicycle"', '""', 'WITHSORTKEYS']], - cursor: { - argLeftOffset: 10, - argRightOffset: 23, - isCursorInQuotes: true, - nextCursorChar: 'c', - prevCursorChar: 'i' - } - } - }, - { - input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS', 39], - result: { - args: [['FT.SEARCH', '"idx:bicycle"', '""'], ['WITHSORTKEYS']], - cursor: { - argLeftOffset: 27, - argRightOffset: 39, - isCursorInQuotes: false, - nextCursorChar: '', - prevCursorChar: 'S' - } - } - }, - { - input: ['FT.SEARCH "idx:bicycle" "" WITHSORTKEYS ', 40], - result: { - args: [['FT.SEARCH', '"idx:bicycle"', '""', 'WITHSORTKEYS'], []], - cursor: { - argLeftOffset: 0, - argRightOffset: 0, - isCursorInQuotes: false, - nextCursorChar: '', - prevCursorChar: '' - } - } - }, - { - input: ['FT.SEARCH "idx:bicycle \\" \\"" "" WITHSORTKEYS ', 46], - result: { - args: [['FT.SEARCH', '"idx:bicycle " ""', '""', 'WITHSORTKEYS'], []], - cursor: { - argLeftOffset: 0, - argRightOffset: 0, - isCursorInQuotes: false, - nextCursorChar: '', - prevCursorChar: '' - } - } - } -] - -describe('splitQueryByArgs', () => { - it.each(splitQueryByArgsTests)('should return for %input proper result', ({ input, result }) => { - const testResult = splitQueryByArgs(...input) - expect(testResult).toEqual(result) - }) -}) - -const generateDetailTests: Array<{ input: Maybe, result: any }> = [ - { - input: ftSearchCommand.arguments.find(({ name }) => name === 'nocontent') as SearchCommand, - result: 'NOCONTENT' - }, - { - input: ftSearchCommand.arguments.find(({ name }) => name === 'filter') as SearchCommand, - result: 'FILTER numeric_field min max' - }, - { - input: ftSearchCommand.arguments.find(({ name }) => name === 'geo_filter') as SearchCommand, - result: 'GEOFILTER geo_field lon lat radius m | km | mi | ft' - }, - { - input: ftAggregateCommand.arguments.find(({ name }) => name === 'groupby') as SearchCommand, - result: 'GROUPBY nargs property [property ...] [REDUCE function nargs arg [arg ...] [AS name] [REDUCE function nargs arg [arg ...] [AS name] ...]]' - }, -] - -describe('generateDetail', () => { - it.each(generateDetailTests)('should return for %input proper result', ({ input, result }) => { - const testResult = generateDetail(input) - expect(testResult).toEqual(result) - }) -}) - -describe('addOwnTokenToArgs', () => { - it('should add FT.SEARCH to args', () => { - const result = addOwnTokenToArgs('FT.SEARCH', { arguments: [] }) - - expect({ arguments: [{ token: 'FT.SEARCH', type: 'pure-token' }] }).toEqual(result) - }) -}) diff --git a/redisinsight/ui/src/pages/search/utils/tests/test-cases/common.ts b/redisinsight/ui/src/pages/search/utils/tests/test-cases/common.ts deleted file mode 100644 index 7946e4baa6..0000000000 --- a/redisinsight/ui/src/pages/search/utils/tests/test-cases/common.ts +++ /dev/null @@ -1,183 +0,0 @@ -// Common test cases -export const commonfindCurrentArgumentCases = [ - { - input: 'FT.SEARCH index "" DIALECT 1', - result: { - stopArg: undefined, - append: expect.any(Array), - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - }, - appendIncludes: ['WITHSCORES', 'VERBATIM', 'FILTER', 'SORTBY', 'RETURN'], - appendNotIncludes: ['DIALECT'] - }, - { - input: 'FT.AGGREGATE "idx:schools" "" GROUPBY 1 p REDUCE AVG 1 a1 AS name ', - result: { - stopArg: undefined, - append: expect.any(Array), - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - }, - appendIncludes: ['REDUCE', 'APPLY', 'SORTBY', 'GROUPBY'], - appendNotIncludes: ['AS'], - }, - { - input: 'FT.SEARCH "idx:bicycle" "*" ', - result: { - stopArg: expect.any(Object), - append: expect.any(Array), - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - }, - appendIncludes: ['DIALECT', 'EXPANDER', 'INKEYS', 'LIMIT'], - appendNotIncludes: ['ASC'], - }, - { - input: 'FT.SEARCH "idx:bicycle" "*" DIALECT 2', - result: expect.any(Object), - appendIncludes: ['EXPANDER', 'INKEYS', 'LIMIT'], - appendNotIncludes: ['DIALECT'], - }, - { - input: 'FT.PROFILE \'idx:schools\' SEARCH ', - result: expect.any(Object), - appendIncludes: ['LIMITED', 'QUERY'], - appendNotIncludes: ['AGGREGATE', 'SEARCH'], - }, - { - input: 'FT.CREATE "idx:schools" ', - result: expect.any(Object), - appendIncludes: ['FILTER', 'ON', 'SCHEMA', 'SCORE', 'NOHL'], - appendNotIncludes: ['HASH', 'JSON'], - }, - { - input: 'FT.CREATE "idx:schools" ON', - result: expect.any(Object), - appendIncludes: ['HASH', 'JSON'], - appendNotIncludes: ['SCHEMA', 'SCORE', 'NOHL'], - }, - { - input: 'FT.CREATE "idx:schools" ON JSON NOFREQS', - result: expect.any(Object), - appendIncludes: ['TEMPORARY', 'NOFIELDS', 'PAYLOAD_FIELD', 'MAXTEXTFIELDS', 'PREFIX', 'SKIPINITIALSCAN'], - appendNotIncludes: ['ON', 'JSON', 'NOFREQS'], - }, - { - input: 'FT.CREATE "idx:schools" ON JSON NOFREQS SKIPINITIALSCAN', - result: expect.any(Object), - appendIncludes: ['TEMPORARY', 'NOFIELDS', 'PAYLOAD_FIELD', 'MAXTEXTFIELDS', 'PREFIX'], - appendNotIncludes: ['ON', 'JSON', 'NOFREQS', 'SKIPINITIALSCAN'], - }, - { - input: 'FT.CREATE "idx:schools" ON JSON SCHEMA address ', - result: { - stopArg: expect.any(Object), - append: expect.any(Array), - isBlocked: false, - isComplete: false, - parent: expect.any(Object) - }, - appendIncludes: ['AS', 'GEO', 'TEXT', 'VECTOR'], - appendNotIncludes: ['SCHEMA', 'SCORE', 'NOHL'], - }, - { - input: 'FT.CREATE "idx:schools" ON JSON SCHEMA address TEXT NOINDEX INDEXMISSING ', - result: { - stopArg: expect.any(Object), - append: expect.any(Array), - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - }, - appendIncludes: ['INDEXEMPTY', 'SORTABLE', 'WITHSUFFIXTRIE'], - appendNotIncludes: ['SCHEMA', 'SCORE', 'NOHL'], - }, - { - input: 'FT.ALTER "idx:schools" ', - result: { - stopArg: expect.any(Object), - append: expect.any(Array), - isBlocked: false, - isComplete: false, - parent: expect.any(Object) - }, - appendIncludes: ['SCHEMA', 'SKIPINITIALSCAN'], - appendNotIncludes: ['ADD'], - }, - { - input: 'FT.ALTER "idx:schools" SCHEMA', - result: expect.any(Object), - appendIncludes: ['ADD'], - appendNotIncludes: ['SKIPINITIALSCAN'], - }, - { - input: 'FT.CONFIG SET ', - result: { - stopArg: { - name: 'option', - type: 'string' - }, - append: [], - isBlocked: true, - isComplete: false, - parent: expect.any(Object) - }, - appendIncludes: [], - appendNotIncludes: [expect.any(String)], - }, - { - input: 'FT.CURSOR READ "idx:schools" 1 ', - result: expect.any(Object), - appendIncludes: ['COUNT'], - }, - { - input: 'FT.DICTADD dict term1 ', - result: { - append: [], - isBlocked: true, - isComplete: false, - parent: expect.any(Object), - stopArg: { - multiple: true, - name: 'term', - type: 'string' - } - }, - appendIncludes: [], - }, - { - input: 'FT.SUGADD key string ', - result: { - append: [], - isBlocked: true, - isComplete: false, - parent: expect.any(Object), - stopArg: { - name: 'score', - type: 'double' - } - }, - appendIncludes: [], - }, - { - input: 'FT.SUGADD key string 1.0 ', - result: expect.any(Object), - appendIncludes: ['INCR', 'PAYLOAD'], - }, - { - input: 'FT.SUGADD key string 1.0 PAYLOAD 1 ', - result: expect.any(Object), - appendIncludes: ['INCR'], - appendNotIncludes: ['PAYLOAD'], - }, - { - input: 'FT.SUGGET k p FUZZY MAX 2 ', - result: expect.any(Object), - appendIncludes: ['WITHPAYLOADS', 'WITHSCORES'], - appendNotIncludes: ['FUZZY', 'MAX'], - }, -] diff --git a/redisinsight/ui/src/pages/search/utils/tests/test-cases/ft-aggregate.ts b/redisinsight/ui/src/pages/search/utils/tests/test-cases/ft-aggregate.ts deleted file mode 100644 index e1411809a9..0000000000 --- a/redisinsight/ui/src/pages/search/utils/tests/test-cases/ft-aggregate.ts +++ /dev/null @@ -1,267 +0,0 @@ -export const findArgumentftAggreageTests = [ - { args: [''], result: null }, - { args: ['', ''], result: null }, - { - args: ['index', '"query"', 'APPLY'], - result: { - stopArg: { name: 'expression', token: 'APPLY', type: 'string' }, - append: [], - isBlocked: true, - isComplete: false, - parent: expect.any(Object) - } - }, - { - args: ['index', '"query"', 'APPLY', 'expression'], - result: { - stopArg: { name: 'name', token: 'AS', type: 'string' }, - append: expect.any(Array), - isBlocked: false, - isComplete: false, - parent: expect.any(Object) - } - }, - { - args: ['index', '"query"', 'APPLY', 'expression', 'AS'], - result: { - stopArg: { name: 'name', token: 'AS', type: 'string' }, - append: expect.any(Array), - isBlocked: true, - isComplete: false, - parent: expect.any(Object) - } - }, - { - args: ['index', '"query"', 'APPLY', 'expression', 'AS', 'name'], - result: { - stopArg: undefined, - append: expect.any(Array), - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - } - }, - { - args: ['""', '""', 'GROUPBY', '2', 'p1', 'p2', 'REDUCE', 'f'], - result: { - stopArg: { name: 'nargs', type: 'integer' }, - append: [], - isBlocked: true, - isComplete: false, - parent: expect.any(Object) - } - }, - { - args: ['""', '""', 'GROUPBY', '2', 'p1', 'p2', 'REDUCE', 'f', '0'], - result: { - stopArg: { - name: 'name', - type: 'string', - token: 'AS', - optional: true - }, - append: [ - [ - { - name: 'name', - type: 'string', - token: 'AS', - optional: true, - parent: { - name: 'reduce', - type: 'block', - optional: true, - multiple: true, - arguments: [ - { - name: 'function', - token: 'REDUCE', - type: 'string' - }, - { - name: 'nargs', - type: 'integer' - }, - { - name: 'arg', - type: 'string', - multiple: true - }, - { - name: 'name', - type: 'string', - token: 'AS', - optional: true - } - ], - parent: expect.any(Object) - } - } - ], - [ - { - name: 'function', - token: 'REDUCE', - type: 'string', - multiple: true, - optional: true, - parent: expect.any(Object) - } - ] - ], - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - } - }, - { - args: ['""', '""', 'GROUPBY', '2', 'p1', 'p2', 'REDUCE', 'f', '1', 'AS', 'name'], - result: { - stopArg: undefined, - append: [ - [], - [ - { - name: 'function', - token: 'REDUCE', - type: 'string', - multiple: true, - optional: true, - parent: expect.any(Object) - } - ] - ], - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - } - }, - { - args: ['index', '"query"', 'SORTBY'], - result: { - stopArg: { name: 'nargs', token: 'SORTBY', type: 'integer' }, - append: expect.any(Array), - isBlocked: true, - isComplete: false, - parent: expect.any(Object) - } - }, - { - args: ['index', '"query"', 'SORTBY', '1', 'p1'], - result: { - stopArg: { - name: 'num', - type: 'integer', - token: 'MAX', - optional: true - }, - append: [ - [ - { - name: 'num', - type: 'integer', - token: 'MAX', - optional: true, - parent: expect.any(Object) - } - ] - ], - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - } - }, - { - args: ['index', '"query"', 'SORTBY', '2', 'p1', 'ASC'], - result: { - stopArg: { - name: 'num', - type: 'integer', - token: 'MAX', - optional: true - }, - append: [ - [ - { - name: 'num', - type: 'integer', - token: 'MAX', - optional: true, - parent: expect.any(Object) - } - ] - ], - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - } - }, - { - args: ['index', '"query"', 'SORTBY', '0'], - result: { - stopArg: { - name: 'num', - type: 'integer', - token: 'MAX', - optional: true - }, - append: [ - [{ - name: 'num', - type: 'integer', - token: 'MAX', - optional: true, - parent: expect.any(Object) - }] - ], - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - } - }, - { - args: ['index', '"query"', 'SORTBY', '2', 'p1', 'ASC', 'MAX'], - result: { - stopArg: { - name: 'num', - type: 'integer', - token: 'MAX', - optional: true - }, - append: [], - isBlocked: true, - isComplete: false, - parent: expect.any(Object) - } - }, - { - args: ['index', '"query"', 'LOAD', '4'], - result: { - stopArg: { multiple: true, name: 'field', type: 'string' }, - append: [], - isBlocked: true, - isComplete: false, - parent: expect.any(Object) - } - }, - { - args: ['index', '"query"', 'LOAD', '4', '1', '2', '3'], - result: { - stopArg: { multiple: true, name: 'field', type: 'string' }, - append: [], - isBlocked: true, - isComplete: false, - parent: expect.any(Object) - } - }, - { - args: ['index', '"query"', 'LOAD', '4', '1', '2', '3', '4'], - result: { - stopArg: undefined, - append: [[]], - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - } - }, -] diff --git a/redisinsight/ui/src/pages/search/utils/tests/test-cases/ft-search.ts b/redisinsight/ui/src/pages/search/utils/tests/test-cases/ft-search.ts deleted file mode 100644 index 28137bf8e9..0000000000 --- a/redisinsight/ui/src/pages/search/utils/tests/test-cases/ft-search.ts +++ /dev/null @@ -1,283 +0,0 @@ -export const findArgumentftSearchTests = [ - { args: [''], result: null }, - { args: ['', ''], result: null }, - { - args: ['', '', 'SUMMARIZE'], - result: { - stopArg: { - name: 'fields', - type: 'block', - optional: true, - arguments: [ - { - name: 'count', - type: 'string', - token: 'FIELDS' - }, - { - name: 'field', - type: 'string', - multiple: true - } - ] - }, - append: [[ - { - name: 'count', - type: 'string', - token: 'FIELDS', - optional: true, - parent: expect.any(Object), - }, - { - name: 'num', - type: 'integer', - token: 'FRAGS', - optional: true, - parent: expect.any(Object) - }, - { - name: 'fragsize', - type: 'integer', - token: 'LEN', - optional: true, - parent: expect.any(Object) - }, - { - name: 'separator', - type: 'string', - token: 'SEPARATOR', - optional: true, - parent: expect.any(Object) - } - ]], - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - } - }, - { - args: ['', '', 'SUMMARIZE', 'FIELDS'], - result: { - stopArg: { - name: 'count', - type: 'string', - token: 'FIELDS' - }, - append: [], - isBlocked: true, - isComplete: false, - parent: expect.any(Object) - } - }, - { - args: ['', '', 'SUMMARIZE', 'FIELDS', '1'], - result: { - stopArg: { - name: 'field', - type: 'string', - multiple: true - }, - append: [], - isBlocked: true, - isComplete: false, - parent: expect.any(Object) - } - }, - { - args: ['', '', 'SUMMARIZE', 'FIELDS', '1', 'f', 'FRAGS'], - result: { - stopArg: { - name: 'num', - type: 'integer', - token: 'FRAGS', - optional: true - }, - append: [], - isBlocked: true, - isComplete: false, - parent: expect.any(Object) - } - }, - { - args: ['', '', 'SUMMARIZE', 'FIELDS', '1', 'f', 'FRAGS', '10'], - result: { - stopArg: { - name: 'fragsize', - type: 'integer', - token: 'LEN', - optional: true - }, - append: [[ - { - name: 'fragsize', - type: 'integer', - token: 'LEN', - optional: true, - parent: expect.any(Object) - }, - { - name: 'separator', - type: 'string', - token: 'SEPARATOR', - optional: true, - parent: expect.any(Object) - } - ]], - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - } - }, - { - args: ['', '', 'RETURN', '1', 'iden'], - result: { - stopArg: undefined, - append: [ - [] - ], - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - } - }, - { - args: ['', '', 'RETURN', '2', 'iden'], - result: { - stopArg: { - name: 'property', - type: 'string', - token: 'AS', - optional: true - }, - append: [ - [ - { - name: 'property', - type: 'string', - token: 'AS', - optional: true, - parent: expect.any(Object) - } - ], - [] - ], - isBlocked: false, - isComplete: false, - parent: expect.any(Object) - } - }, - { - args: ['', '', 'RETURN', '2', 'iden', 'iden'], - result: { - stopArg: undefined, - append: [ - [] - ], - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - } - }, - { - args: ['', '', 'RETURN', '3', 'iden', 'iden'], - result: { - stopArg: { - name: 'property', - type: 'string', - token: 'AS', - optional: true - }, - append: [ - [ - { - name: 'property', - type: 'string', - token: 'AS', - optional: true, - parent: expect.any(Object) - } - ], - [] - ], - isBlocked: false, - isComplete: false, - parent: expect.any(Object) - } - }, - { - args: ['', '', 'RETURN', '3', 'iden', 'iden', 'AS', 'iden2'], - result: { - stopArg: undefined, - append: [ - [] - ], - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - } - }, - { - args: ['', '', 'SORTBY', 'f'], - result: { - stopArg: { - name: 'order', - type: 'oneof', - optional: true, - arguments: [ - { - name: 'asc', - type: 'pure-token', - token: 'ASC' - }, - { - name: 'desc', - type: 'pure-token', - token: 'DESC' - } - ] - }, - append: [ - [ - { - name: 'asc', - type: 'pure-token', - token: 'ASC', - parent: expect.any(Object) - }, - { - name: 'desc', - type: 'pure-token', - token: 'DESC', - parent: expect.any(Object) - } - ] - ], - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - } - }, - { - args: ['', '', 'SORTBY', 'f', 'DESC'], - result: { - stopArg: undefined, - append: [], - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - } - }, - { - args: ['', '', 'DIALECT', '1'], - result: { - stopArg: undefined, - append: [ - [] - ], - isBlocked: false, - isComplete: true, - parent: expect.any(Object) - } - }, -] diff --git a/redisinsight/ui/src/pages/search/utils/tests/test-cases/index.ts b/redisinsight/ui/src/pages/search/utils/tests/test-cases/index.ts deleted file mode 100644 index 42889e7be5..0000000000 --- a/redisinsight/ui/src/pages/search/utils/tests/test-cases/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './ft-aggregate' -export * from './ft-search' -export * from './common' diff --git a/redisinsight/ui/src/pages/workbench/WorkbenchPage.tsx b/redisinsight/ui/src/pages/workbench/WorkbenchPage.tsx index bdd0a14058..2e92e9c76b 100644 --- a/redisinsight/ui/src/pages/workbench/WorkbenchPage.tsx +++ b/redisinsight/ui/src/pages/workbench/WorkbenchPage.tsx @@ -1,12 +1,10 @@ import React, { useEffect, useState } from 'react' -import { useDispatch, useSelector } from 'react-redux' +import { useSelector } from 'react-redux' import { useParams } from 'react-router-dom' import { formatLongName, getDbIndex, setTitle } from 'uiSrc/utils' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { sendPageViewTelemetry, TelemetryPageView } from 'uiSrc/telemetry' -import { CommandExecutionType } from 'uiSrc/slices/interfaces' -import { setExecutionType } from 'uiSrc/slices/workbench/wb-results' import WBViewWrapper from './components/wb-view' const WorkbenchPage = () => { @@ -15,14 +13,9 @@ const WorkbenchPage = () => { const { name: connectedInstanceName, db } = useSelector(connectedInstanceSelector) const { instanceId } = useParams<{ instanceId: string }>() - const dispatch = useDispatch() setTitle(`${formatLongName(connectedInstanceName, 33, 0, '...')} ${getDbIndex(db)} - Workbench`) - useEffect(() => { - dispatch(setExecutionType(CommandExecutionType.Workbench)) - }, []) - useEffect(() => { if (connectedInstanceName && !isPageViewSent) { sendPageView(instanceId) diff --git a/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.tsx b/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.tsx index bb050d7f89..cb3542ea1f 100644 --- a/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.tsx +++ b/redisinsight/ui/src/pages/workbench/components/wb-view/WBViewWrapper.tsx @@ -15,7 +15,7 @@ import { sendWbQueryAction, workbenchResultsSelector, } from 'uiSrc/slices/workbench/wb-results' -import { CommandExecutionType, Instance, IPluginVisualization } from 'uiSrc/slices/interfaces' +import { Instance, IPluginVisualization } from 'uiSrc/slices/interfaces' import { connectedInstanceSelector, initialState as instanceInitState } from 'uiSrc/slices/instances/instances' import { ResultsMode, RunQueryMode } from 'uiSrc/slices/interfaces/workbench' import { cliSettingsSelector, fetchBlockingCliCommandsAction } from 'uiSrc/slices/cli/cli-settings' @@ -165,7 +165,6 @@ const WBViewWrapper = () => { commandInit, commandId, executeParams, - CommandExecutionType.Workbench, { afterEach: () => { const isNewCommand = !commandId diff --git a/redisinsight/ui/src/pages/workbench/utils/tests/monaco.spec.ts b/redisinsight/ui/src/pages/workbench/utils/tests/monaco.spec.ts index c72864bc06..2ab897b97e 100644 --- a/redisinsight/ui/src/pages/workbench/utils/tests/monaco.spec.ts +++ b/redisinsight/ui/src/pages/workbench/utils/tests/monaco.spec.ts @@ -1,6 +1,5 @@ -import { getRediSearchSignutureProvider } from 'uiSrc/pages/search/utils' import { MOCKED_REDIS_COMMANDS } from 'uiSrc/mocks/data/mocked_redis_commands' -import { SearchCommand } from 'uiSrc/pages/search/types' +import { getRediSearchSignutureProvider } from 'uiSrc/pages/workbench/utils/monaco' const ftAggregateCommand = MOCKED_REDIS_COMMANDS['FT.AGGREGATE'] @@ -16,7 +15,7 @@ const getRediSearchSignatureProviderTests = [ { input: { isOpen: true, - currentArg: ftAggregateCommand.arguments.find(({ name }) => name === 'groupby') as SearchCommand, + currentArg: ftAggregateCommand.arguments.find(({ name }) => name === 'groupby'), parent: null }, result: { @@ -35,7 +34,7 @@ const getRediSearchSignatureProviderTests = [ input: { isOpen: true, currentArg: { name: 'expression' }, - parent: ftAggregateCommand.arguments.find(({ name }) => name === 'apply') as SearchCommand + parent: ftAggregateCommand.arguments.find(({ name }) => name === 'apply') }, result: { dispose: expect.any(Function), diff --git a/redisinsight/ui/src/pages/workbench/utils/tests/query.spec.ts b/redisinsight/ui/src/pages/workbench/utils/tests/query.spec.ts index 0f705de920..07010aaa38 100644 --- a/redisinsight/ui/src/pages/workbench/utils/tests/query.spec.ts +++ b/redisinsight/ui/src/pages/workbench/utils/tests/query.spec.ts @@ -1,7 +1,6 @@ -import { SearchCommand, TokenType } from 'uiSrc/pages/search/types' import { Maybe, splitQueryByArgs } from 'uiSrc/utils' import { MOCKED_REDIS_COMMANDS } from 'uiSrc/mocks/data/mocked_redis_commands' -import { IRedisCommand } from 'uiSrc/constants' +import { IRedisCommand, ICommandTokenType } from 'uiSrc/constants' import { commonfindCurrentArgumentCases, findArgumentftAggreageTests, @@ -23,11 +22,11 @@ describe('findCurrentArgument', () => { describe('with list of commands', () => { commonfindCurrentArgumentCases.forEach(({ input, result, appendIncludes, appendNotIncludes }) => { it(`should return proper suggestions for ${input}`, () => { - const { args } = splitQueryByArgs(input, 0, COMPOSITE_ARGS) + const { args } = splitQueryByArgs(input, 0, COMPOSITE_ARGS.concat('LOAD *')) const COMMANDS_LIST = COMMANDS.map((command) => ({ ...addOwnTokenToArgs(command.name!, command), token: command.name!, - type: TokenType.Block + type: ICommandTokenType.Block })) const testResult = findCurrentArgument( @@ -79,21 +78,21 @@ describe('findCurrentArgument', () => { }) }) -const generateDetailTests: Array<{ input: Maybe, result: any }> = [ +const generateDetailTests: Array<{ input: Maybe, result: any }> = [ { - input: ftSearchCommand.arguments.find(({ name }) => name === 'nocontent') as SearchCommand, + input: ftSearchCommand.arguments.find(({ name }) => name === 'nocontent') as IRedisCommand, result: 'NOCONTENT' }, { - input: ftSearchCommand.arguments.find(({ name }) => name === 'filter') as SearchCommand, + input: ftSearchCommand.arguments.find(({ name }) => name === 'filter') as IRedisCommand, result: 'FILTER numeric_field min max' }, { - input: ftSearchCommand.arguments.find(({ name }) => name === 'geo_filter') as SearchCommand, + input: ftSearchCommand.arguments.find(({ name }) => name === 'geo_filter') as IRedisCommand, result: 'GEOFILTER geo_field lon lat radius m | km | mi | ft' }, { - input: ftAggregateCommand.arguments.find(({ name }) => name === 'groupby') as SearchCommand, + input: ftAggregateCommand.arguments.find(({ name }) => name === 'groupby') as IRedisCommand, result: 'GROUPBY nargs property [property ...] [REDUCE function nargs arg [arg ...] [AS name] [REDUCE function nargs arg [arg ...] [AS name] ...]]' }, ] diff --git a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts index b39b3e5945..0e6d18c670 100644 --- a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts +++ b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts @@ -244,12 +244,13 @@ export const commonfindCurrentArgumentCases = [ appendIncludes: ['ft', 'km', 'm', 'mi'], appendNotIncludes: ['SORTBY', 'FILTER', 'LIMIT', 'DIALECT', 'AS', 'ASC'], }, - { - input: 'FT.SEARCH textVehicles "*" RETURN 2 test ', - result: expect.any(Object), - appendIncludes: ['AS'], - appendNotIncludes: ['SORTBY', 'FILTER', 'LIMIT', 'DIALECT', 'ASC'], - }, + // skip + // { + // input: 'FT.SEARCH textVehicles "*" RETURN 2 test ', + // result: expect.any(Object), + // appendIncludes: ['AS'], + // appendNotIncludes: ['SORTBY', 'FILTER', 'LIMIT', 'DIALECT', 'ASC'], + // }, { input: 'FT.CREATE textVehicles ON ', result: expect.any(Object), diff --git a/redisinsight/ui/src/slices/interfaces/workbench.ts b/redisinsight/ui/src/slices/interfaces/workbench.ts index 4bb7378e84..94652c9768 100644 --- a/redisinsight/ui/src/slices/interfaces/workbench.ts +++ b/redisinsight/ui/src/slices/interfaces/workbench.ts @@ -9,7 +9,6 @@ export interface StateWorkbenchSettings { } export interface StateWorkbenchResults { - type: CommandExecutionType isLoaded: boolean loading: boolean processing: boolean diff --git a/redisinsight/ui/src/slices/tests/workbench/wb-results.spec.ts b/redisinsight/ui/src/slices/tests/workbench/wb-results.spec.ts index e5338ed994..e459ee3011 100644 --- a/redisinsight/ui/src/slices/tests/workbench/wb-results.spec.ts +++ b/redisinsight/ui/src/slices/tests/workbench/wb-results.spec.ts @@ -96,32 +96,6 @@ describe('workbench results slice', () => { }) }) - describe('sendWBCommand with another type', () => { - it('should properly set state', () => { - // Arrange - const mockPayload = { - commands: ['command', 'command2'], - commandId: '123', - executionType: CommandExecutionType.Search - } - const state = { - ...initialState, - items: [] - } - - // Act - const nextState = reducer(initialState, sendWBCommand(mockPayload)) - - // Assert - const rootState = Object.assign(initialStateDefault, { - workbench: { - results: nextState, - }, - }) - expect(workbenchResultsSelector(rootState)).toEqual(state) - }) - }) - describe('toggleOpenWBResult', () => { it('should properly set isOpen = true', () => { // Arrange diff --git a/redisinsight/ui/src/slices/workbench/wb-results.ts b/redisinsight/ui/src/slices/workbench/wb-results.ts index f3a9d177f8..526f416a25 100644 --- a/redisinsight/ui/src/slices/workbench/wb-results.ts +++ b/redisinsight/ui/src/slices/workbench/wb-results.ts @@ -7,7 +7,8 @@ import { addErrorNotification } from 'uiSrc/slices/app/notifications' import { CliOutputFormatterType } from 'uiSrc/constants/cliOutput' import { RunQueryMode, ResultsMode, CommandExecutionType } from 'uiSrc/slices/interfaces/workbench' import { - getApiErrorMessage, getCommandsForExecution, + getApiErrorMessage, + getCommandsForExecution, getExecuteParams, getMultiCommands, getUrl, @@ -29,7 +30,6 @@ import { } from '../interfaces' export const initialState: StateWorkbenchResults = { - type: CommandExecutionType.Workbench, isLoaded: false, loading: false, processing: false, @@ -47,10 +47,6 @@ const workbenchResultsSlice = createSlice({ reducers: { setWBResultsInitialState: () => initialState, - setExecutionType: (state, { payload }: PayloadAction) => { - state.type = payload - }, - // Fetch Workbench history loadWBHistory: (state) => { state.loading = true @@ -118,12 +114,10 @@ const workbenchResultsSlice = createSlice({ sendWBCommand: ( state, { - payload: { commands, commandId, executionType } + payload: { commands, commandId } }: - PayloadAction<{ commands: string[], commandId: string, executionType: CommandExecutionType }> + { payload: { commands: string[], commandId: string } } ) => { - if (executionType !== state.type) return - let newItems = [ ...commands.map((command, i) => ({ command, @@ -227,7 +221,6 @@ const workbenchResultsSlice = createSlice({ // Actions generated from the slice export const { setWBResultsInitialState, - setExecutionType, loadWBHistory, loadWBHistorySuccess, loadWBHistoryFailure, @@ -311,7 +304,6 @@ export function sendWBCommandAction({ dispatch(sendWBCommand({ commands: isGroupResults(resultsMode) ? [`${commands.length} - Command(s)`] : commands, commandId, - executionType })) dispatch(setDbIndexState(true)) @@ -378,7 +370,6 @@ export function sendWBCommandClusterAction({ dispatch(sendWBCommand({ commands: isGroupResults(resultsMode) ? [`${commands.length} - Commands`] : commands, commandId, - executionType })) const { data, status } = await apiService.post( @@ -527,7 +518,6 @@ export function sendWbQueryAction( queryInit: string, commandId?: Nullable, executeParams: CodeButtonParams = {}, - executionType?: CommandExecutionType, onSuccessAction?: { afterEach?: () => void, afterAll?: () => void @@ -563,7 +553,6 @@ export function sendWbQueryAction( commands, multiCommands, mode: activeRunQueryMode, - executionType, onSuccessAction: onSuccess, onFailAction: onFail })) @@ -586,7 +575,6 @@ export function sendWbQueryAction( mode: activeRunQueryMode, resultsMode, multiCommands, - executionType, onSuccessAction: onSuccess, onFailAction: onFail }) diff --git a/redisinsight/ui/src/telemetry/events.ts b/redisinsight/ui/src/telemetry/events.ts index 2ccbe4a3f6..b384e915f8 100644 --- a/redisinsight/ui/src/telemetry/events.ts +++ b/redisinsight/ui/src/telemetry/events.ts @@ -126,18 +126,6 @@ export enum TelemetryEvent { WORKBENCH_CLEAR_RESULT_CLICKED = 'WORKBENCH_CLEAR_RESULT_CLICKED', WORKBENCH_CLEAR_ALL_RESULTS_CLICKED = 'WORKBENCH_CLEAR_ALL_RESULTS_CLICKED', - SEARCH_COMMAND_SUBMITTED = 'SEARCH_COMMAND_SUBMITTED', - SEARCH_RESULT_VIEW_CHANGED = 'SEARCH_RESULT_VIEW_CHANGED', - SEARCH_COMMAND_COPIED = 'SEARCH_COMMAND_COPIED', - SEARCH_COMMAND_RUN_AGAIN = 'SEARCH_COMMAND_RUN_AGAIN', - SEARCH_RESULTS_IN_FULL_SCREEN = 'SEARCH_RESULTS_IN_FULL_SCREEN', - SEARCH_RESULTS_COLLAPSED = 'SEARCH_RESULTS_COLLAPSED', - SEARCH_RESULTS_EXPANDED = 'SEARCH_RESULTS_EXPANDED', - SEARCH_CLEAR_RESULT_CLICKED = 'SEARCH_CLEAR_RESULT_CLICKED', - SEARCH_CLEAR_ALL_RESULTS_CLICKED = 'SEARCH_CLEAR_ALL_RESULTS_CLICKED', - SEARCH_EXPAND_ALL_RESULTS = 'SEARCH_EXPAND_ALL_RESULTS', - SEARCH_FORMATTER_CHANGED = 'SEARCH_FORMATTER_CHANGED', - PROFILER_OPENED = 'PROFILER_OPENED', PROFILER_STARTED = 'PROFILER_STARTED', PROFILER_STOPPED = 'PROFILER_STOPPED', diff --git a/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts b/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts deleted file mode 100644 index e73734b113..0000000000 --- a/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokens.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { monaco as monacoEditor } from 'react-monaco-editor' -import { SearchCommand } from 'uiSrc/pages/search/types' -import { - generateKeywords, - generateTokens, - generateTokensWithFunctions, - getBlockTokens, isIndexAfterKeyword, - isQueryAfterIndex -} from 'uiSrc/utils/monaco/redisearch/utils_old' -import { generateQuery } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokensTemplates' - -const STRING_DOUBLE = 'string.double' - -export const getRediSearchMonarchTokensProvider = ( - commands: SearchCommand[], - command?: string -): monacoEditor.languages.IMonarchLanguage => { - const currentCommand = commands.find(({ name }) => name === command) - - const keywords = generateKeywords(commands) - const isHighlightIndex = isIndexAfterKeyword(currentCommand) - const argTokens = generateTokens(currentCommand) - const isHighlightQuery = isQueryAfterIndex(currentCommand) - - return ( - { - defaultToken: '', - tokenPostfix: '.redisearch', - ignoreCase: true, - brackets: [ - { open: '[', close: ']', token: 'delimiter.square' }, - { open: '(', close: ')', token: 'delimiter.parenthesis' }, - ], - keywords, - tokenizer: { - root: [ - { include: '@fields' }, - { include: '@whitespace' }, - { include: '@numbers' }, - { include: '@strings' }, - { include: '@keyword' }, - [/LOAD\s+\*/, 'loadAll'], - { include: '@argument.block' }, - { include: '@argument.block.withFunctions' }, - [/[;,.]/, 'delimiter'], - [/[()]/, '@brackets'], - [ - /[\w@#$]+/, - { - cases: { - '@keywords': 'keyword', - '@default': 'identifier', - }, - }, - ], - [/[<>=!%&+\-*/|~^]/, 'operator'], - ], - keyword: [ - [`(${keywords.join('|')})\\b`, { token: 'keyword', next: isHighlightIndex ? '@index' : '@root' }] - ], - 'argument.block': getBlockTokens(argTokens?.pureTokens), - ...generateTokensWithFunctions(argTokens?.tokensWithQueryAfter), - index: [ - [/"([^"\\]|\\.)*"/, { token: 'index', next: isHighlightQuery ? '@query' : '@root' }], - [/'([^'\\]|\\.)*'/, { token: 'index', next: isHighlightQuery ? '@query' : '@root' }], - [/[\w:]+/, { token: 'index', next: isHighlightQuery ? '@query' : '@root' }], - { include: 'root' } // Fallback to the root state if nothing matches - ], - ...generateQuery(), - fields: [ - [/@\w+/, { token: 'field', }] - ], - whitespace: [ - [/\s+/, 'white'], - [/\/\/.*$/, 'comment'], - ], - numbers: [ - [/0[xX][0-9a-fA-F]*/, 'number'], - [/[$][+-]*\d*(\.\d*)?/, 'number'], - [/((\d+(\.\d*)?)|(\.\d+))([eE][-+]?\d+)?/, 'number'], - ], - strings: [ - [/'/, { token: 'string', next: '@string' }], - [/"/, { token: STRING_DOUBLE, next: '@stringDouble' }], - ], - string: [ - [/\\./, 'string'], - [/'/, { token: 'string', next: '@pop' }], - [/[^\\']+/, 'string'], - ], - stringDouble: [ - [/\\./, STRING_DOUBLE], - [/"/, { token: STRING_DOUBLE, next: '@pop' }], - [/[^\\"]+/, STRING_DOUBLE], - ], - }, - } - ) -} diff --git a/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokensSubRedis.ts b/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokensSubRedis.ts index 9be0444673..2454443781 100644 --- a/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokensSubRedis.ts +++ b/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokensSubRedis.ts @@ -1,11 +1,12 @@ import { monaco as monacoEditor } from 'react-monaco-editor' import { remove } from 'lodash' -import { SearchCommand } from 'uiSrc/pages/search/types' +import { IRedisCommandTree } from 'uiSrc/constants' import { generateKeywords, generateTokens, generateTokensWithFunctions, - getBlockTokens, isIndexAfterKeyword, + getBlockTokens, + isIndexAfterKeyword, isQueryAfterIndex } from 'uiSrc/utils/monaco/redisearch/utils' import { generateQuery } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokensTemplates' @@ -13,7 +14,7 @@ import { generateQuery } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokens const STRING_DOUBLE = 'string.double' export const getRediSearchSubRedisMonarchTokensProvider = ( - commands: SearchCommand[], + commands: IRedisCommandTree[], ): monacoEditor.languages.IMonarchLanguage => { const withoutIndexSuggestions = [...commands] const withNextIndexSuggestions = remove(withoutIndexSuggestions, isIndexAfterKeyword) diff --git a/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokensTemplates.ts b/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokensTemplates.ts index 9c2901029f..0ebc7c1d36 100644 --- a/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokensTemplates.ts +++ b/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokensTemplates.ts @@ -1,12 +1,12 @@ import { languages } from 'monaco-editor' import { curryRight } from 'lodash' -import { SearchCommand } from 'uiSrc/pages/search/types' import { Maybe } from 'uiSrc/utils' +import { IRedisCommand } from 'uiSrc/constants' const appendToken = (token: string, name: Maybe) => (name ? `${token}.${name}` : token) export const generateQuery = ( - argToken?: SearchCommand, - args?: SearchCommand[] + argToken?: IRedisCommand, + args?: IRedisCommand[] ): { [name: string]: languages.IMonarchLanguageRule[] } => { const curriedAppendToken = curryRight(appendToken) const appendTokenName = curriedAppendToken(argToken?.token) diff --git a/redisinsight/ui/src/utils/monaco/redisearch/utils.ts b/redisinsight/ui/src/utils/monaco/redisearch/utils.ts index 25b0fd0b19..36d05a7c01 100644 --- a/redisinsight/ui/src/utils/monaco/redisearch/utils.ts +++ b/redisinsight/ui/src/utils/monaco/redisearch/utils.ts @@ -1,27 +1,27 @@ import { isNumber, remove } from 'lodash' import { languages } from 'monaco-editor' -import { SearchCommand, TokenType } from 'uiSrc/pages/search/types' import { Maybe, Nullable } from 'uiSrc/utils' -import { DefinedArgumentName } from 'uiSrc/pages/search/components/query/constants' import { generateQuery } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokensTemplates' +import { ICommandTokenType, IRedisCommand } from 'uiSrc/constants' +import { DefinedArgumentName } from 'uiSrc/pages/workbench/constants' -export const generateKeywords = (commands: SearchCommand[]) => commands.map(({ name }) => name) -export const generateTokens = (command?: SearchCommand): Nullable<{ - pureTokens: Array> - tokensWithQueryAfter: Array> +export const generateKeywords = (commands: IRedisCommand[]) => commands.map(({ name }) => name) +export const generateTokens = (command?: IRedisCommand): Nullable<{ + pureTokens: Array> + tokensWithQueryAfter: Array> }> => { if (!command) return null - const pureTokens: Array> = [] - const tokensWithQueryAfter: Array> = [] + const pureTokens: Array> = [] + const tokensWithQueryAfter: Array> = [] - function processArguments(args: SearchCommand[], level = 0) { + function processArguments(args: IRedisCommand[], level = 0) { if (!pureTokens[level]) pureTokens[level] = [] if (!tokensWithQueryAfter[level]) tokensWithQueryAfter[level] = [] args.forEach((arg) => { if (arg.token) pureTokens[level].push(arg) - if (arg.type === TokenType.Block && arg.arguments) { + if (arg.type === ICommandTokenType.Block && arg.arguments) { const blockToken = arg.arguments[0] const nextArgs = arg.arguments const isArgHasOwnSyntax = arg.arguments[0].expression && !!arg.arguments[0].arguments?.length @@ -30,7 +30,7 @@ export const generateTokens = (command?: SearchCommand): Nullable<{ if (isArgHasOwnSyntax) { tokensWithQueryAfter[level].push({ token: blockToken, - arguments: arg.arguments[0].arguments as SearchCommand[] + arguments: arg.arguments[0].arguments as IRedisCommand[] }) } else { pureTokens[level].push(blockToken) @@ -40,7 +40,7 @@ export const generateTokens = (command?: SearchCommand): Nullable<{ processArguments(blockToken ? nextArgs.slice(1, nextArgs.length) : nextArgs, level + 1) } - if (arg.type === TokenType.OneOf && arg.arguments) { + if (arg.type === ICommandTokenType.OneOf && arg.arguments) { arg.arguments.forEach((choice) => { if (choice?.token) pureTokens[level].push(choice) }) @@ -55,14 +55,14 @@ export const generateTokens = (command?: SearchCommand): Nullable<{ return { pureTokens, tokensWithQueryAfter } } -export const isIndexAfterKeyword = (command?: SearchCommand) => { +export const isIndexAfterKeyword = (command?: IRedisCommand) => { if (!command) return false const index = command.arguments?.findIndex(({ name }) => name === DefinedArgumentName.index) return isNumber(index) && index === 0 } -export const isQueryAfterIndex = (command?: SearchCommand) => { +export const isQueryAfterIndex = (command?: IRedisCommand) => { if (!command) return false const index = command.arguments?.findIndex(({ name }) => name === DefinedArgumentName.index) @@ -70,13 +70,13 @@ export const isQueryAfterIndex = (command?: SearchCommand) => { } export const appendTokenWithQuery = ( - args: Array<{ token: SearchCommand, arguments: SearchCommand[] }>, + args: Array<{ token: IRedisCommand, arguments: IRedisCommand[] }>, level: number ): languages.IMonarchLanguageRule[] => args.map(({ token }) => [`(${token.token})\\b`, { token: `argument.block.${level}`, next: `@query.${token.token}` }]) export const appendQueryWithNextFunctions = ( - tokens: Array<{ token: SearchCommand, arguments: SearchCommand[] }> + tokens: Array<{ token: IRedisCommand, arguments: IRedisCommand[] }> ): { [name: string]: languages.IMonarchLanguageRule[] } => { @@ -94,7 +94,7 @@ export const appendQueryWithNextFunctions = ( export const generateTokensWithFunctions = ( name: string = '', - tokens?: Array> + tokens?: Array> ): { [name: string]: languages.IMonarchLanguageRule[] } => { @@ -116,12 +116,12 @@ export const generateTokensWithFunctions = ( export const getBlockTokens = ( name: string = '', - pureTokens: Maybe[]> + pureTokens: Maybe[]> ): languages.IMonarchLanguageRule[] => { if (!pureTokens) return [] const getLeveledToken = ( - tokens: SearchCommand[], + tokens: IRedisCommand[], lvl: number ): languages.IMonarchLanguageRule[] => { const result: languages.IMonarchLanguageRule[] = [] diff --git a/redisinsight/ui/src/utils/monaco/redisearch/utils_old.ts b/redisinsight/ui/src/utils/monaco/redisearch/utils_old.ts deleted file mode 100644 index d04f47a717..0000000000 --- a/redisinsight/ui/src/utils/monaco/redisearch/utils_old.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { isNumber, remove } from 'lodash' -import { languages } from 'monaco-editor' -import { SearchCommand, TokenType } from 'uiSrc/pages/search/types' -import { Maybe, Nullable } from 'uiSrc/utils' -import { DefinedArgumentName } from 'uiSrc/pages/search/components/query/constants' -import { generateQuery } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokensTemplates' - -export const generateKeywords = (commands: SearchCommand[]) => commands.map(({ name }) => name) -export const generateTokens = (command?: SearchCommand): Nullable<{ - pureTokens: Array> - tokensWithQueryAfter: Array> -}> => { - if (!command) return null - const pureTokens: Array> = [] - const tokensWithQueryAfter: Array> = [] - - function processArguments(args: SearchCommand[], level = 0) { - if (!pureTokens[level]) pureTokens[level] = [] - if (!tokensWithQueryAfter[level]) tokensWithQueryAfter[level] = [] - - args.forEach((arg) => { - if (arg.token) pureTokens[level].push(arg) - - if (arg.type === TokenType.Block && arg.arguments) { - const blockToken = arg.arguments[0] - const nextArgs = arg.arguments - const isArgHasOwnSyntax = arg.arguments[0].expression && !!arg.arguments[0].arguments?.length - - if (blockToken?.token) { - if (isArgHasOwnSyntax) { - tokensWithQueryAfter[level].push({ - token: blockToken, - arguments: arg.arguments[0].arguments as SearchCommand[] - }) - } else { - pureTokens[level].push(blockToken) - } - } - - processArguments(blockToken ? nextArgs.slice(1, nextArgs.length) : nextArgs, level + 1) - } - - if (arg.type === TokenType.OneOf && arg.arguments) { - arg.arguments.forEach((choice) => { - if (choice?.token) pureTokens[level].push(choice) - }) - } - }) - } - - if (command.arguments) { - processArguments(command.arguments, 0) - } - - return { pureTokens, tokensWithQueryAfter } -} - -export const isIndexAfterKeyword = (command?: SearchCommand) => { - if (!command) return false - - const index = command.arguments?.findIndex(({ name }) => name === DefinedArgumentName.index) - return isNumber(index) && index === 0 -} - -export const isQueryAfterIndex = (command?: SearchCommand) => { - if (!command) return false - - const index = command.arguments?.findIndex(({ name }) => name === DefinedArgumentName.index) - return isNumber(index) && index > -1 ? command.arguments?.[index + 1]?.name === DefinedArgumentName.query : false -} - -export const appendTokenWithQuery = ( - args: Array<{ token: SearchCommand, arguments: SearchCommand[] }>, - level: number -): languages.IMonarchLanguageRule[] => - args.map(({ token }) => [`(${token.token})\\b`, { token: `argument.block.${level}`, next: `@query.${token.token}` }]) - -export const appendQueryWithNextFunctions = (tokens: Array<{ token: SearchCommand, arguments: SearchCommand[] }>): { - [name: string]: languages.IMonarchLanguageRule[] -} => { - let result: { [name: string]: languages.IMonarchLanguageRule[] } = {} - - tokens.forEach(({ token, arguments: args }) => { - result = { - ...result, - ...generateQuery(token, args) - } - }) - - return result -} - -export const generateTokensWithFunctions = ( - tokens?: Array> -): { - [name: string]: languages.IMonarchLanguageRule[] -} => { - if (!tokens) return { 'argument.block.withFunctions': [] } - - const actualTokens = tokens.filter((tokens) => tokens.length) - - return { - 'argument.block.withFunctions': [ - ...actualTokens - .map((tokens, lvl) => appendTokenWithQuery(tokens, lvl)) - .flat() - ], - ...appendQueryWithNextFunctions(actualTokens.flat()) - } -} - -export const getBlockTokens = ( - pureTokens: Maybe[]> -): languages.IMonarchLanguageRule[] => { - if (!pureTokens) return [] - - const getLeveledToken = ( - tokens: SearchCommand[], - lvl: number - ): languages.IMonarchLanguageRule[] => { - const result: languages.IMonarchLanguageRule[] = [] - const restTokens = [...tokens] - const tokensWithNextExpression = remove(restTokens, (({ expression }) => expression)) - - if (tokensWithNextExpression.length) { - result.push([ - `(${tokensWithNextExpression.map(({ token }) => token).join('|')})\\b`, - { - token: `argument.block.${lvl}`, - next: '@query' - }, - ]) - } - - if (restTokens.length) { - result.push([`(${restTokens.map(({ token }) => token).join('|')})\\b`, { token: `argument.block.${lvl}`, next: '@root' }]) - } - - return result - } - - return pureTokens.map((tokens, lvl) => getLeveledToken(tokens, lvl)).flat() -} diff --git a/redisinsight/ui/src/utils/tests/monaco/cyber/monarchTokensProvider.spec.ts b/redisinsight/ui/src/utils/tests/monaco/cyber/monarchTokensProvider.spec.ts index 711e7bba49..313fd10721 100644 --- a/redisinsight/ui/src/utils/tests/monaco/cyber/monarchTokensProvider.spec.ts +++ b/redisinsight/ui/src/utils/tests/monaco/cyber/monarchTokensProvider.spec.ts @@ -1,8 +1,8 @@ import { getCypherMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/cypherTokens' import { getJmespathMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/jmespathTokens' import { getSqliteFunctionsMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/sqliteFunctionsTokens' -import { getRediSearchMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokens' -import { MOCKED_SUPPORTED_COMMANDS } from 'uiSrc/pages/search/mocks/mocks' +import { getRediSearchSubRedisMonarchTokensProvider } from 'uiSrc/utils/monaco/monarchTokens/redisearchTokensSubRedis' +import { MOCKED_REDIS_COMMANDS } from 'uiSrc/mocks/data/mocked_redis_commands' describe('getCypherMonarchTokensProvider', () => { it('should be truthy', () => { @@ -24,15 +24,15 @@ describe('getSqliteFunctionsMonarchTokensProvider', () => { describe('getRediSearchMonarchTokensProvider', () => { it('should be truthy', () => { - expect(getRediSearchMonarchTokensProvider([])).toBeTruthy() + expect(getRediSearchSubRedisMonarchTokensProvider([])).toBeTruthy() }) it('should be truthy with command', () => { - const commands = Object.keys(MOCKED_SUPPORTED_COMMANDS) + const commands = Object.keys(MOCKED_REDIS_COMMANDS) .map((key) => ({ - ...MOCKED_SUPPORTED_COMMANDS[key], - name: key + ...MOCKED_REDIS_COMMANDS[key], + name: key, })) - expect(getRediSearchMonarchTokensProvider(commands, 'FT.AGGREGATE')).toBeTruthy() + expect(getRediSearchSubRedisMonarchTokensProvider(commands)).toBeTruthy() }) }) diff --git a/redisinsight/ui/src/utils/tests/routing.spec.ts b/redisinsight/ui/src/utils/tests/routing.spec.ts index 6f11ef1ae2..adc3a1a3b3 100644 --- a/redisinsight/ui/src/utils/tests/routing.spec.ts +++ b/redisinsight/ui/src/utils/tests/routing.spec.ts @@ -18,8 +18,6 @@ const getRedirectionPageTests = [ { input: ['/workbench', databaseId], expected: '/1/workbench' }, { input: ['browser', databaseId], expected: '/1/browser' }, { input: ['/browser', databaseId], expected: '/1/browser' }, - { input: ['search', databaseId], expected: '/1/search' }, - { input: ['/search', databaseId], expected: '/1/search' }, { input: ['/analytics/slowlog', databaseId], expected: '/1/analytics/slowlog' }, { input: ['/analytics/slowlog'], expected: null }, { input: ['/analytics', databaseId], expected: '/1/analytics' }, From 739bae9cf25a2195ff4dab03259ef7844fa10207 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Wed, 23 Oct 2024 10:10:13 +0200 Subject: [PATCH 105/112] #RI-6234 - fix error showing when no indexes --- .../ui/src/pages/workbench/components/query/QueryWrapper.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisinsight/ui/src/pages/workbench/components/query/QueryWrapper.tsx b/redisinsight/ui/src/pages/workbench/components/query/QueryWrapper.tsx index f34b78ec2c..09242df622 100644 --- a/redisinsight/ui/src/pages/workbench/components/query/QueryWrapper.tsx +++ b/redisinsight/ui/src/pages/workbench/components/query/QueryWrapper.tsx @@ -51,7 +51,7 @@ const QueryWrapper = (props: Props) => { if (!connectedIndstanceId) return // fetch indexes - dispatch(fetchRedisearchListAction()) + dispatch(fetchRedisearchListAction(undefined, undefined, false)) }, [connectedIndstanceId]) const Placeholder = ( From c5524cd8fc01b139b778107cbc8ac77b4267e02e Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Thu, 24 Oct 2024 13:12:31 +0200 Subject: [PATCH 106/112] #RI-6208 - improve switch between redis and search syntax, add command scope #RI-6211 - fix highlighting when there are several the same args --- .../components/query/Query/Query.tsx | 8 ++-- redisinsight/ui/src/pages/workbench/types.ts | 1 + .../ui/src/pages/workbench/utils/monaco.ts | 28 ++++++++--- .../ui/src/pages/workbench/utils/query.ts | 1 + .../workbench/utils/searchSuggestions.ts | 15 +++--- .../src/pages/workbench/utils/suggestions.ts | 16 ++++++- .../workbench/utils/tests/monaco.spec.ts | 18 ++++--- .../utils/tests/test-cases/common.ts | 28 +++++++---- .../utils/tests/test-cases/ft-aggregate.ts | 47 ++++++++++++------- .../utils/tests/test-cases/ft-search.ts | 36 +++++++++----- .../monacoRedisMonarchTokensProvider.ts | 9 +++- .../monarchTokens/redisearchTokensSubRedis.ts | 22 ++++++--- 12 files changed, 158 insertions(+), 71 deletions(-) diff --git a/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx b/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx index 6a8511b7e1..803549fa50 100644 --- a/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx +++ b/redisinsight/ui/src/pages/workbench/components/query/Query/Query.tsx @@ -85,8 +85,7 @@ const Query = (props: Props) => { const suggestionsRef = useRef([]) const helpWidgetRef = useRef({ isOpen: false, - parent: null, - currentArg: null + data: {} }) const indexesRef = useRef([]) const attributesRef = useRef([]) @@ -581,11 +580,10 @@ const Query = (props: Props) => { ) if (helpWidget) { - const { isOpen, parent, currentArg } = helpWidget + const { isOpen, data } = helpWidget helpWidgetRef.current = { isOpen, - parent: parent || helpWidgetRef.current.parent, - currentArg: currentArg || helpWidgetRef.current.currentArg + data: data || helpWidgetRef.current.data } } diff --git a/redisinsight/ui/src/pages/workbench/types.ts b/redisinsight/ui/src/pages/workbench/types.ts index 7962623bbc..05c961c1a1 100644 --- a/redisinsight/ui/src/pages/workbench/types.ts +++ b/redisinsight/ui/src/pages/workbench/types.ts @@ -13,6 +13,7 @@ export interface FoundCommandArgument { isBlocked: boolean append: Maybe> parent: Maybe + token: Maybe } export interface CursorContext { diff --git a/redisinsight/ui/src/pages/workbench/utils/monaco.ts b/redisinsight/ui/src/pages/workbench/utils/monaco.ts index c6cd9bb58f..8f032ce262 100644 --- a/redisinsight/ui/src/pages/workbench/utils/monaco.ts +++ b/redisinsight/ui/src/pages/workbench/utils/monaco.ts @@ -2,7 +2,7 @@ import { monaco } from 'react-monaco-editor' import * as monacoEditor from 'monaco-editor' import { isString } from 'lodash' import { generateDetail } from 'uiSrc/pages/workbench/utils/query' -import { Maybe } from 'uiSrc/utils' +import { Maybe, Nullable } from 'uiSrc/utils' import { IRedisCommand, ICommandTokenType } from 'uiSrc/constants' export const setCursorPositionAtTheEnd = (editor: monacoEditor.editor.IStandaloneCodeEditor) => { @@ -39,17 +39,31 @@ export const buildSuggestion = (arg: IRedisCommand, range: monaco.IRange, option export const getRediSearchSignutureProvider = (options: Maybe<{ isOpen: boolean - currentArg: IRedisCommand - parent: Maybe + data: { + currentArg: IRedisCommand + parent: Maybe + token: Maybe + } }>) => { - const { isOpen, currentArg, parent } = options || {} + const { isOpen, data } = options || {} + const { currentArg, parent, token } = data || {} if (!isOpen) return null const label = generateDetail(parent) + let signaturePosition: Nullable<[number, number]> = null const arg = currentArg?.type === ICommandTokenType.Block - ? currentArg?.arguments?.[0]?.name + ? (currentArg?.arguments?.[0]?.name || currentArg?.token || '') : (currentArg?.name || currentArg?.type || '') + // we may have several the same args inside documentation, so we get proper arg after token + const numberOfArgsInside = label.split(arg).length - 1 + if (token && numberOfArgsInside > 1) { + const parentToken = token.token || token.arguments?.[0]?.token + const parentTokenPosition = parentToken ? label.indexOf(parentToken) : 0 + const startPosition = label.indexOf(arg, parentTokenPosition) + signaturePosition = [startPosition, startPosition + arg.length] + } + return { dispose: () => {}, value: { @@ -57,7 +71,9 @@ export const getRediSearchSignutureProvider = (options: Maybe<{ activeSignature: 0, signatures: [{ label: label || '', - parameters: [{ label: arg }] + parameters: [ + { label: signaturePosition || arg } + ], }] } } diff --git a/redisinsight/ui/src/pages/workbench/utils/query.ts b/redisinsight/ui/src/pages/workbench/utils/query.ts index e0c633407f..d5489e8e98 100644 --- a/redisinsight/ui/src/pages/workbench/utils/query.ts +++ b/redisinsight/ui/src/pages/workbench/utils/query.ts @@ -32,6 +32,7 @@ export const findCurrentArgument = ( // this is the main function which creates the list of arguments return { ...getArgumentSuggestions({ tokenArgs: pastArgs, untilTokenArgs }, commandArgs, parent), + token, parent: parent || token } } diff --git a/redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts b/redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts index 09e60723ca..3ef7caec10 100644 --- a/redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts +++ b/redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts @@ -36,11 +36,14 @@ export const findSuggestionsByArg = ( const { prevCursorChar } = cursor const [beforeOffsetArgs, [currentOffsetArg]] = args - const foundArg = findCurrentArgument(listOfCommands, beforeOffsetArgs) + const scopedList = command.name + ? listOfCommands.filter(({ name }) => name === command?.name) + : listOfCommands + const foundArg = findCurrentArgument(scopedList, beforeOffsetArgs) if (!command.name.startsWith(ModuleCommandPrefix.RediSearch)) { return { - helpWidget: { isOpen: !!foundArg, parent: foundArg?.parent, currentArg: foundArg?.stopArg }, + helpWidget: { isOpen: !!foundArg, data: { parent: foundArg?.parent, currentArg: foundArg?.stopArg } }, suggestions: asSuggestionsRef([]) } } @@ -94,7 +97,7 @@ const handleIndexSuggestions = ( cursorContext: CursorContext ) => { const isIndex = indexes.length > 0 - const helpWidget = { isOpen: isIndex, parent: command.info, currentArg: foundArg?.stopArg } + const helpWidget = { isOpen: isIndex, data: { parent: command.info, currentArg: foundArg?.stopArg } } const currentCommand = command.info if (COMMANDS_WITHOUT_INDEX_PROPOSE.includes(command.name || '')) { @@ -132,7 +135,7 @@ const handleIndexSuggestions = ( } const handleQuerySuggestions = (foundArg: FoundCommandArgument) => ({ - helpWidget: { isOpen: true, parent: foundArg?.parent, currentArg: foundArg?.stopArg }, + helpWidget: { isOpen: true, data: { parent: foundArg?.parent, currentArg: foundArg?.stopArg } }, suggestions: asSuggestionsRef([], false) }) @@ -141,7 +144,7 @@ const handleExpressionSuggestions = ( foundArg: FoundCommandArgument, cursorContext: CursorContext, ) => { - const helpWidget = { isOpen: true, parent: foundArg?.parent, currentArg: foundArg?.stopArg } + const helpWidget = { isOpen: true, data: { parent: foundArg?.parent, currentArg: foundArg?.stopArg } } const { isCursorInQuotes, offset, argLeftOffset } = cursorContext if (!isCursorInQuotes) { @@ -182,7 +185,7 @@ const handleCommonSuggestions = ( const shouldHideSuggestions = isCursorInQuotes || nextCursorChar || (prevCursorChar && isEscaped) if (shouldHideSuggestions) { return { - helpWidget: { isOpen: !!foundArg, parent: foundArg?.parent, currentArg: foundArg?.stopArg }, + helpWidget: { isOpen: !!foundArg, data: { parent: foundArg?.parent, currentArg: foundArg?.stopArg } }, suggestions: asSuggestionsRef([]) } } diff --git a/redisinsight/ui/src/pages/workbench/utils/suggestions.ts b/redisinsight/ui/src/pages/workbench/utils/suggestions.ts index b90823f0ff..e7e1f5c1cc 100644 --- a/redisinsight/ui/src/pages/workbench/utils/suggestions.ts +++ b/redisinsight/ui/src/pages/workbench/utils/suggestions.ts @@ -1,9 +1,15 @@ import { monaco } from 'react-monaco-editor' import * as monacoEditor from 'monaco-editor' +import { findIndex } from 'lodash' import { RedisResponseBuffer } from 'uiSrc/slices/interfaces' import { bufferToString, formatLongName, generateArgsForInsertText, getCommandMarkdown, Nullable } from 'uiSrc/utils' import { FoundCommandArgument } from 'uiSrc/pages/workbench/types' -import { DefinedArgumentName, EmptySuggestionsIds } from 'uiSrc/pages/workbench/constants' +import { + DefinedArgumentName, + EmptySuggestionsIds, + ModuleCommandPrefix, + SORTED_SEARCH_COMMANDS +} from 'uiSrc/pages/workbench/constants' import { getUtmExternalLink } from 'uiSrc/utils/links' import { IRedisCommand } from 'uiSrc/constants' import { generateDetail, removeNotSuggestedArgs } from './query' @@ -170,7 +176,13 @@ export const getGeneralSuggestions = ( if (foundArg && !foundArg.isComplete) { return { suggestions: getMandatoryArgumentSuggestions(foundArg, fields, range), - helpWidgetData: { isOpen: !!foundArg?.stopArg, parent: foundArg?.parent, currentArg: foundArg?.stopArg } + helpWidgetData: { isOpen: !!foundArg?.stopArg, + data: { + parent: foundArg?.parent, + currentArg: foundArg?.stopArg, + token: foundArg?.token + } + } } } diff --git a/redisinsight/ui/src/pages/workbench/utils/tests/monaco.spec.ts b/redisinsight/ui/src/pages/workbench/utils/tests/monaco.spec.ts index 2ab897b97e..01978197c0 100644 --- a/redisinsight/ui/src/pages/workbench/utils/tests/monaco.spec.ts +++ b/redisinsight/ui/src/pages/workbench/utils/tests/monaco.spec.ts @@ -7,16 +7,20 @@ const getRediSearchSignatureProviderTests = [ { input: { isOpen: false, - currentArg: {}, - parent: {} + data: { + currentArg: {}, + parent: {} + } }, result: null }, { input: { isOpen: true, - currentArg: ftAggregateCommand.arguments.find(({ name }) => name === 'groupby'), - parent: null + data: { + currentArg: ftAggregateCommand.arguments.find(({ name }) => name === 'groupby'), + parent: null + } }, result: { dispose: expect.any(Function), @@ -33,8 +37,10 @@ const getRediSearchSignatureProviderTests = [ { input: { isOpen: true, - currentArg: { name: 'expression' }, - parent: ftAggregateCommand.arguments.find(({ name }) => name === 'apply') + data: { + currentArg: { name: 'expression' }, + parent: ftAggregateCommand.arguments.find(({ name }) => name === 'apply') + } }, result: { dispose: expect.any(Function), diff --git a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts index 0e6d18c670..02ace2613c 100644 --- a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts +++ b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/common.ts @@ -7,7 +7,8 @@ export const commonfindCurrentArgumentCases = [ append: expect.any(Array), isBlocked: false, isComplete: true, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) }, appendIncludes: ['WITHSCORES', 'VERBATIM', 'FILTER', 'SORTBY', 'RETURN'], appendNotIncludes: ['DIALECT'] @@ -19,7 +20,8 @@ export const commonfindCurrentArgumentCases = [ append: expect.any(Array), isBlocked: false, isComplete: true, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) }, appendIncludes: ['REDUCE', 'APPLY', 'SORTBY', 'GROUPBY'], appendNotIncludes: ['AS'], @@ -36,7 +38,8 @@ export const commonfindCurrentArgumentCases = [ append: expect.any(Array), isBlocked: false, isComplete: true, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) }, appendIncludes: ['AS', 'REDUCE', 'APPLY', 'SORTBY', 'GROUPBY'], }, @@ -47,7 +50,8 @@ export const commonfindCurrentArgumentCases = [ append: expect.any(Array), isBlocked: false, isComplete: true, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) }, appendIncludes: ['DIALECT', 'EXPANDER', 'INKEYS', 'LIMIT'], appendNotIncludes: ['ASC'], @@ -107,7 +111,8 @@ export const commonfindCurrentArgumentCases = [ append: expect.any(Array), isBlocked: false, isComplete: false, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) }, appendIncludes: ['AS', 'GEO', 'TEXT', 'VECTOR'], appendNotIncludes: ['SCHEMA', 'SCORE', 'NOHL'], @@ -119,7 +124,8 @@ export const commonfindCurrentArgumentCases = [ append: expect.any(Array), isBlocked: false, isComplete: true, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) }, appendIncludes: ['INDEXEMPTY', 'SORTABLE', 'WITHSUFFIXTRIE'], appendNotIncludes: ['SCHEMA', 'SCORE', 'NOHL'], @@ -131,7 +137,8 @@ export const commonfindCurrentArgumentCases = [ append: expect.any(Array), isBlocked: false, isComplete: false, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) }, appendIncludes: ['SCHEMA', 'SKIPINITIALSCAN'], appendNotIncludes: ['ADD'], @@ -152,7 +159,8 @@ export const commonfindCurrentArgumentCases = [ append: [], isBlocked: true, isComplete: false, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) }, appendIncludes: [], appendNotIncludes: [expect.any(String)], @@ -169,6 +177,7 @@ export const commonfindCurrentArgumentCases = [ isBlocked: true, isComplete: false, parent: expect.any(Object), + token: expect.any(Object), stopArg: { multiple: true, name: 'term', @@ -187,7 +196,8 @@ export const commonfindCurrentArgumentCases = [ stopArg: { name: 'score', type: 'double' - } + }, + token: expect.any(Object) }, appendIncludes: [], }, diff --git a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-aggregate.ts b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-aggregate.ts index fcee36b2c8..03211cafbe 100644 --- a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-aggregate.ts +++ b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-aggregate.ts @@ -8,7 +8,8 @@ export const findArgumentftAggreageTests = [ append: [], isBlocked: true, isComplete: false, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) } }, { @@ -18,7 +19,8 @@ export const findArgumentftAggreageTests = [ append: expect.any(Array), isBlocked: false, isComplete: false, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) } }, { @@ -28,7 +30,8 @@ export const findArgumentftAggreageTests = [ append: expect.any(Array), isBlocked: true, isComplete: false, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) } }, { @@ -38,7 +41,8 @@ export const findArgumentftAggreageTests = [ append: expect.any(Array), isBlocked: false, isComplete: true, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) } }, { @@ -48,7 +52,8 @@ export const findArgumentftAggreageTests = [ append: [], isBlocked: true, isComplete: false, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) } }, { @@ -111,7 +116,8 @@ export const findArgumentftAggreageTests = [ ], isBlocked: false, isComplete: true, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) } }, { @@ -133,7 +139,8 @@ export const findArgumentftAggreageTests = [ ], isBlocked: false, isComplete: true, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) } }, { @@ -143,7 +150,8 @@ export const findArgumentftAggreageTests = [ append: expect.any(Array), isBlocked: true, isComplete: false, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) } }, { @@ -168,7 +176,8 @@ export const findArgumentftAggreageTests = [ ], isBlocked: false, isComplete: true, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) } }, { @@ -193,8 +202,9 @@ export const findArgumentftAggreageTests = [ ], isBlocked: false, isComplete: true, - parent: expect.any(Object) - } + parent: expect.any(Object), + token: expect.any(Object) + }, }, { args: ['index', '"query"', 'SORTBY', '0'], @@ -216,7 +226,8 @@ export const findArgumentftAggreageTests = [ ], isBlocked: false, isComplete: true, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) } }, { @@ -231,7 +242,8 @@ export const findArgumentftAggreageTests = [ append: [], isBlocked: true, isComplete: false, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) } }, { @@ -241,7 +253,8 @@ export const findArgumentftAggreageTests = [ append: [], isBlocked: true, isComplete: false, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) } }, { @@ -251,7 +264,8 @@ export const findArgumentftAggreageTests = [ append: [], isBlocked: true, isComplete: false, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) } }, { @@ -261,7 +275,8 @@ export const findArgumentftAggreageTests = [ append: [], isBlocked: false, isComplete: true, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) } }, ] diff --git a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-search.ts b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-search.ts index a729a10fa5..436308a7a3 100644 --- a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-search.ts +++ b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-search.ts @@ -53,7 +53,8 @@ export const findArgumentftSearchTests = [ ]], isBlocked: false, isComplete: true, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) } }, { @@ -67,7 +68,8 @@ export const findArgumentftSearchTests = [ append: [], isBlocked: true, isComplete: false, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) } }, { @@ -81,7 +83,8 @@ export const findArgumentftSearchTests = [ append: [], isBlocked: true, isComplete: false, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) } }, { @@ -96,7 +99,8 @@ export const findArgumentftSearchTests = [ append: [], isBlocked: true, isComplete: false, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) } }, { @@ -126,7 +130,8 @@ export const findArgumentftSearchTests = [ ]], isBlocked: false, isComplete: true, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) } }, { @@ -136,7 +141,8 @@ export const findArgumentftSearchTests = [ append: [], isBlocked: false, isComplete: true, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) } }, { @@ -162,7 +168,8 @@ export const findArgumentftSearchTests = [ ], isBlocked: false, isComplete: false, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) } }, { @@ -198,7 +205,8 @@ export const findArgumentftSearchTests = [ ], isBlocked: false, isComplete: false, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) } }, { @@ -208,7 +216,8 @@ export const findArgumentftSearchTests = [ append: [], isBlocked: false, isComplete: true, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) } }, { @@ -249,7 +258,8 @@ export const findArgumentftSearchTests = [ ], isBlocked: false, isComplete: true, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) } }, { @@ -259,7 +269,8 @@ export const findArgumentftSearchTests = [ append: [[]], isBlocked: false, isComplete: true, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) } }, { @@ -271,7 +282,8 @@ export const findArgumentftSearchTests = [ ], isBlocked: false, isComplete: true, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) } }, ] diff --git a/redisinsight/ui/src/utils/monaco/monacoRedisMonarchTokensProvider.ts b/redisinsight/ui/src/utils/monaco/monacoRedisMonarchTokensProvider.ts index 0c4b914333..ee62d07d51 100644 --- a/redisinsight/ui/src/utils/monaco/monacoRedisMonarchTokensProvider.ts +++ b/redisinsight/ui/src/utils/monaco/monacoRedisMonarchTokensProvider.ts @@ -8,13 +8,14 @@ const STRING_DOUBLE = 'string.double' export const getRedisMonarchTokensProvider = (commands: IRedisCommand[]): monacoEditor.languages.IMonarchLanguage => { const commandRedisCommands = [...commands] const searchCommands = remove(commandRedisCommands, ({ token }) => token?.startsWith(ModuleCommandPrefix.RediSearch)) - const COMMON_COMMANDS_REGEX = `(${commandRedisCommands.map(({ token }) => token).join('|')})\\b` - const SEARCH_COMMANDS_REGEX = `(${searchCommands.map(({ token }) => token).join('|')})\\b` + const COMMON_COMMANDS_REGEX = `^\\s*(${commandRedisCommands.map(({ token }) => token).join('|')})\\b` + const SEARCH_COMMANDS_REGEX = `^\\s*(${searchCommands.map(({ token }) => token).join('|')})\\b` return { defaultToken: '', tokenPostfix: '.redis', ignoreCase: true, + includeLF: true, brackets: [ { open: '[', close: ']', token: 'delimiter.square' }, { open: '(', close: ')', token: 'delimiter.parenthesis' }, @@ -23,6 +24,7 @@ export const getRedisMonarchTokensProvider = (commands: IRedisCommand[]): monaco operators: [], tokenizer: { root: [ + { include: '@startOfLine' }, { include: '@whitespace' }, { include: '@numbers' }, { include: '@strings' }, @@ -71,6 +73,9 @@ export const getRedisMonarchTokensProvider = (commands: IRedisCommand[]): monaco // TODO: can be tokens or functions the same - need to think how to avoid wrong ending endRedisearch: [ [`^\\s*${COMMON_COMMANDS_REGEX}`, { token: '@rematch', next: '@root', nextEmbedded: '@pop', log: 'end' }], + ], + startOfLine: [ + [/\n/, { next: '@root', token: '@pop' }], ] }, } diff --git a/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokensSubRedis.ts b/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokensSubRedis.ts index 2454443781..01e02f9680 100644 --- a/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokensSubRedis.ts +++ b/redisinsight/ui/src/utils/monaco/monarchTokens/redisearchTokensSubRedis.ts @@ -23,13 +23,17 @@ export const getRediSearchSubRedisMonarchTokensProvider = ( const generateTokensForCommands = () => { let commandTokens: any = {} - withNextIndexSuggestions.forEach((command) => { + commands.forEach((command) => { const isIndexAfterCommand = isIndexAfterKeyword(command) const argTokens = generateTokens(command) const tokenName = command.token?.replace(/(\.| )/g, '_') + const blockTokens = getBlockTokens(tokenName, argTokens?.pureTokens) + + if (blockTokens.length) { + commandTokens[`argument.block.${tokenName}`] = blockTokens + } if (isIndexAfterCommand) { - commandTokens[`argument.block.${tokenName}`] = getBlockTokens(tokenName, argTokens?.pureTokens) commandTokens = { ...commandTokens, ...generateTokensWithFunctions(tokenName, argTokens?.tokensWithQueryAfter) @@ -40,7 +44,6 @@ export const getRediSearchSubRedisMonarchTokensProvider = ( return commandTokens } - const keywords = generateKeywords(commands) const tokens = generateTokensForCommands() const includeTokens = () => { @@ -52,14 +55,16 @@ export const getRediSearchSubRedisMonarchTokensProvider = ( { defaultToken: '', tokenPostfix: '.redisearch', + includeLF: true, ignoreCase: true, brackets: [ { open: '[', close: ']', token: 'delimiter.square' }, { open: '(', close: ')', token: 'delimiter.parenthesis' }, ], - keywords, + keywords: [], tokenizer: { root: [ + { include: '@startOfLine' }, { include: '@keywords' }, ...includeTokens(), { include: '@fields' }, @@ -72,9 +77,9 @@ export const getRediSearchSubRedisMonarchTokensProvider = ( [/[\w@#$.]+/, 'identifier'] ], keywords: [ - [`(${generateKeywords(withNextQueryIndexSuggestions).join('|')})\\b`, { token: 'keyword', next: '@index.query' }], - [`(${generateKeywords(withNextIndexSuggestions).join('|')})\\b`, { token: 'keyword', next: '@index' }], - [`(${generateKeywords(withoutIndexSuggestions).join('|')})\\b`, { token: 'keyword', next: '@root' }], + [`^\\s*(${generateKeywords(withNextQueryIndexSuggestions).join('|')})\\b`, { token: 'keyword', next: '@index.query' }], + [`^\\s*(${generateKeywords(withNextIndexSuggestions).join('|')})\\b`, { token: 'keyword', next: '@index' }], + [`^\\s*(${generateKeywords(withoutIndexSuggestions).join('|')})\\b`, { token: 'keyword', next: '@root' }], ], ...tokens, ...generateQuery(), @@ -115,6 +120,9 @@ export const getRediSearchSubRedisMonarchTokensProvider = ( [/\\./, STRING_DOUBLE], [/"/, { token: STRING_DOUBLE, next: '@pop' }], [/[^\\"]+/, STRING_DOUBLE], + ], + startOfLine: [ + [/\n/, { next: '@keywords', token: '@pop' }] ] }, } From eebde4e78cdedbc71af8fbdb30bdb057cabde944 Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Thu, 24 Oct 2024 15:55:10 +0200 Subject: [PATCH 107/112] #RI-6226 - add sorting for list of commands #RI-6244 - fix finding of command --- redisinsight/ui/src/pages/workbench/constants.ts | 7 +++++++ .../pages/workbench/utils/searchSuggestions.ts | 4 ++-- .../ui/src/pages/workbench/utils/suggestions.ts | 12 +++++++++++- .../workbench/utils/tests/test-cases/ft-search.ts | 3 ++- redisinsight/ui/src/utils/monaco/monacoUtils.ts | 15 ++++++++------- 5 files changed, 30 insertions(+), 11 deletions(-) diff --git a/redisinsight/ui/src/pages/workbench/constants.ts b/redisinsight/ui/src/pages/workbench/constants.ts index 1c732c67ce..b894704f29 100644 --- a/redisinsight/ui/src/pages/workbench/constants.ts +++ b/redisinsight/ui/src/pages/workbench/constants.ts @@ -98,3 +98,10 @@ export const FIELD_START_SYMBOL = '@' export enum EmptySuggestionsIds { NoIndexes = 'no-indexes' } + +export const SORTED_SEARCH_COMMANDS = [ + 'FT.SEARCH', + 'FT.CREATE', + 'FT.EXPLAIN', + 'FT.PROFILE' +] diff --git a/redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts b/redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts index 3ef7caec10..0d408b170f 100644 --- a/redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts +++ b/redisinsight/ui/src/pages/workbench/utils/searchSuggestions.ts @@ -37,7 +37,7 @@ export const findSuggestionsByArg = ( const [beforeOffsetArgs, [currentOffsetArg]] = args const scopedList = command.name - ? listOfCommands.filter(({ name }) => name === command?.name) + ? listOfCommands.filter(({ token }) => token === command?.name) : listOfCommands const foundArg = findCurrentArgument(scopedList, beforeOffsetArgs) @@ -97,7 +97,7 @@ const handleIndexSuggestions = ( cursorContext: CursorContext ) => { const isIndex = indexes.length > 0 - const helpWidget = { isOpen: isIndex, data: { parent: command.info, currentArg: foundArg?.stopArg } } + const helpWidget = { isOpen: isIndex, data: { parent: foundArg.parent, currentArg: foundArg?.stopArg } } const currentCommand = command.info if (COMMANDS_WITHOUT_INDEX_PROPOSE.includes(command.name || '')) { diff --git a/redisinsight/ui/src/pages/workbench/utils/suggestions.ts b/redisinsight/ui/src/pages/workbench/utils/suggestions.ts index e7e1f5c1cc..9e443d3f06 100644 --- a/redisinsight/ui/src/pages/workbench/utils/suggestions.ts +++ b/redisinsight/ui/src/pages/workbench/utils/suggestions.ts @@ -104,6 +104,14 @@ export const getFunctionsSuggestions = (functions: IRedisCommand[], range: monac detail: summary })) +export const getSortingForCommand = (command: IRedisCommand) => { + if (!command.token?.startsWith(ModuleCommandPrefix.RediSearch)) return command.token + if (!SORTED_SEARCH_COMMANDS.includes(command.token)) return command.token + + const index = findIndex(SORTED_SEARCH_COMMANDS, (token) => token === command.token) + return `${ModuleCommandPrefix.RediSearch}_${index}` +} + export const getCommandsSuggestions = (commands: IRedisCommand[], range: monaco.IRange) => commands.map((command) => buildSuggestion(command, range, { detail: generateDetail(command), @@ -111,6 +119,7 @@ export const getCommandsSuggestions = (commands: IRedisCommand[], range: monaco. documentation: { value: getCommandMarkdown(command as any) }, + sortText: getSortingForCommand(command) })) export const getMandatoryArgumentSuggestions = ( @@ -176,7 +185,8 @@ export const getGeneralSuggestions = ( if (foundArg && !foundArg.isComplete) { return { suggestions: getMandatoryArgumentSuggestions(foundArg, fields, range), - helpWidgetData: { isOpen: !!foundArg?.stopArg, + helpWidgetData: { + isOpen: !!foundArg?.stopArg, data: { parent: foundArg?.parent, currentArg: foundArg?.stopArg, diff --git a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-search.ts b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-search.ts index 436308a7a3..a9d54d33e3 100644 --- a/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-search.ts +++ b/redisinsight/ui/src/pages/workbench/utils/tests/test-cases/ft-search.ts @@ -179,7 +179,8 @@ export const findArgumentftSearchTests = [ append: [], isBlocked: false, isComplete: true, - parent: expect.any(Object) + parent: expect.any(Object), + token: expect.any(Object) } }, { diff --git a/redisinsight/ui/src/utils/monaco/monacoUtils.ts b/redisinsight/ui/src/utils/monaco/monacoUtils.ts index db18fedb03..9a98e4dc5b 100644 --- a/redisinsight/ui/src/utils/monaco/monacoUtils.ts +++ b/redisinsight/ui/src/utils/monaco/monacoUtils.ts @@ -238,13 +238,6 @@ export const findCompleteQuery = ( fullQuery = `\n${fullQuery}` } - const matchedCommand = commandsArray - .find((command) => commandName?.trim().toUpperCase().startsWith(command.toUpperCase())) - - if (isUndefined(matchedCommand)) { - return null - } - const commandCursorPosition = fullQuery.length // find args in the next lines const linesCount = model.getLineCount() @@ -273,6 +266,14 @@ export const findCompleteQuery = ( compositeArgs, ) + const [[firstQueryArg]] = args + const matchedCommand = commandsArray + .find((command) => firstQueryArg?.toUpperCase() === command.toUpperCase()) + + if (isUndefined(matchedCommand)) { + return null + } + return { position, commandPosition, From d079e2e3b0e9979784438eea4fc8c3dcfa0b0704 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Thu, 24 Oct 2024 16:55:36 +0200 Subject: [PATCH 108/112] e2e/feature/RI-6226_change_sorting_for_ft --- .../pageObjects/components/monaco-editor.ts | 24 ++++++++++++++++--- .../workbench}/no-indexes-suggestions.e2e.ts | 6 ++--- .../search-and-query-autocomplete.e2e.ts} | 6 ++++- 3 files changed, 29 insertions(+), 7 deletions(-) rename tests/e2e/tests/web/{regression/search-and-query => critical-path/workbench}/no-indexes-suggestions.e2e.ts (89%) rename tests/e2e/tests/web/{regression/search-and-query/search-and-query-tab.e2e.ts => critical-path/workbench/search-and-query-autocomplete.e2e.ts} (98%) diff --git a/tests/e2e/pageObjects/components/monaco-editor.ts b/tests/e2e/pageObjects/components/monaco-editor.ts index 007ebaf859..5bfff1b56d 100644 --- a/tests/e2e/pageObjects/components/monaco-editor.ts +++ b/tests/e2e/pageObjects/components/monaco-editor.ts @@ -23,7 +23,7 @@ export class MonacoEditor { async sendTextToMonaco(input: Selector, command: string, clean = true): Promise { await t.click(input); - if(clean) { + if (clean) { await t // remove text since replace doesn't work here .pressKey('ctrl+a') @@ -39,10 +39,10 @@ export class MonacoEditor { * @param depth level of depth of the object */ async insertTextByLines(input: Selector, lines: string[], depth: number): Promise { - for(let i = 0; i < lines.length; i++) { + for (let i = 0; i < lines.length; i++) { const line = lines[i]; - for(let j = 0; j < depth; j++) { + for (let j = 0; j < depth; j++) { await t.pressKey('shift+tab'); } @@ -61,4 +61,22 @@ export class MonacoEditor { const textAreaMonaco = Selector('[class^=view-lines ]'); return (await textAreaMonaco.textContent).replace(/\s+/g, ' '); } + + /** + * Get suggestions as ordered array from monaco from the beginning + * @param suggestions number of elements to get + */ + async getSuggestionsArrayFromMonaco(suggestions: number): Promise { + const textArray: string[] = []; + const suggestionElements = this.monacoSuggestion; + + for (let i = 0; i < suggestions; i++) { + const suggestionItem = suggestionElements.nth(i); + if (await suggestionItem.exists) { + textArray.push(await suggestionItem.textContent); + } + } + + return textArray; + } } diff --git a/tests/e2e/tests/web/regression/search-and-query/no-indexes-suggestions.e2e.ts b/tests/e2e/tests/web/critical-path/workbench/no-indexes-suggestions.e2e.ts similarity index 89% rename from tests/e2e/tests/web/regression/search-and-query/no-indexes-suggestions.e2e.ts rename to tests/e2e/tests/web/critical-path/workbench/no-indexes-suggestions.e2e.ts index ac2a27038c..b0a8d1cb23 100644 --- a/tests/e2e/tests/web/regression/search-and-query/no-indexes-suggestions.e2e.ts +++ b/tests/e2e/tests/web/critical-path/workbench/no-indexes-suggestions.e2e.ts @@ -1,7 +1,7 @@ import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, WorkbenchPage } from '../../../../pageObjects'; import { rte } from '../../../../helpers/constants'; -import { commonUrl, ossClusterConfig, ossStandaloneConfig, ossStandaloneRedisearch } from '../../../../helpers/conf'; +import { commonUrl, ossClusterConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; const browserPage = new BrowserPage(); @@ -14,11 +14,11 @@ fixture `Search and Query Raw mode` .page(commonUrl); test - .before(async t => { + .before(async () => { await databaseHelper.acceptLicenseTermsAndAddOSSClusterDatabase(ossClusterConfig); await browserPage.Cli.sendCommandInCli('flushdb'); }) - .after(async t => { + .after(async () => { await databaseAPIRequests.deleteOSSClusterDatabaseApi(ossClusterConfig); })('Verify suggestions when there are no indexes', async t => { diff --git a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts b/tests/e2e/tests/web/critical-path/workbench/search-and-query-autocomplete.e2e.ts similarity index 98% rename from tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts rename to tests/e2e/tests/web/critical-path/workbench/search-and-query-autocomplete.e2e.ts index 2bbb08ab01..19012b5824 100644 --- a/tests/e2e/tests/web/regression/search-and-query/search-and-query-tab.e2e.ts +++ b/tests/e2e/tests/web/critical-path/workbench/search-and-query-autocomplete.e2e.ts @@ -18,7 +18,7 @@ let indexName2: string; let indexName3: string; fixture `Autocomplete for entered commands in search and query` - .meta({ type: 'regression', rte: rte.standalone }) + .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl) .beforeEach(async t => { await databaseHelper.acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig); @@ -84,8 +84,12 @@ test('Verify full commands suggestions with index and query for FT.AGGREGATE', a 'students', 'type' ]; + const ftSortedCommands = ['FT.SEARCH', 'FT.CREATE', 'FT.EXPLAIN', 'FT.PROFILE']; + // Verify basic commands suggestions FT.SEARCH and FT.AGGREGATE await t.typeText(workbenchPage.queryInput, 'FT', { replace: true }); + // Verify custom sorting for FT. commands + await t.expect(await workbenchPage.MonacoEditor.getSuggestionsArrayFromMonaco(4)).eql(ftSortedCommands, 'Wrong order of FT commands'); // Verify that the list with FT.SEARCH and FT.AGGREGATE auto-suggestions is displayed await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withText('FT._LIST').exists).ok('FT._LIST auto-suggestions are not displayed'); await t.expect(workbenchPage.MonacoEditor.monacoSuggestion.withText('FT.AGGREGATE').exists).ok('FT.AGGREGATE auto-suggestions are not displayed'); From 8cd844f758936e275452ff7963334e1db406c497 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Thu, 24 Oct 2024 17:45:27 +0200 Subject: [PATCH 109/112] fix ts issue --- tests/e2e/tests/web/regression/database/github.e2e.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e/tests/web/regression/database/github.e2e.ts b/tests/e2e/tests/web/regression/database/github.e2e.ts index ba2e4e4a91..a9bf329691 100644 --- a/tests/e2e/tests/web/regression/database/github.e2e.ts +++ b/tests/e2e/tests/web/regression/database/github.e2e.ts @@ -3,6 +3,7 @@ import { DatabaseHelper } from '../../../../helpers/database'; import { BrowserPage, MyRedisDatabasePage } from '../../../../pageObjects'; import { commonUrl, ossStandaloneConfig } from '../../../../helpers/conf'; import { DatabaseAPIRequests } from '../../../../helpers/api/api-database'; +import { Common } from '../../../../helpers/common'; const myRedisDatabasePage = new MyRedisDatabasePage(); const databaseHelper = new DatabaseHelper(); From 1d2d48e6acf01b315d934059cf25ad2fbe985b4c Mon Sep 17 00:00:00 2001 From: ArtemHoruzhenko Date: Fri, 25 Oct 2024 10:48:38 +0300 Subject: [PATCH 110/112] fix ITests --- .../repositories/local-command-execution.repository.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/redisinsight/api/src/modules/workbench/repositories/local-command-execution.repository.spec.ts b/redisinsight/api/src/modules/workbench/repositories/local-command-execution.repository.spec.ts index 3769d6bd0a..e5143d7f67 100644 --- a/redisinsight/api/src/modules/workbench/repositories/local-command-execution.repository.spec.ts +++ b/redisinsight/api/src/modules/workbench/repositories/local-command-execution.repository.spec.ts @@ -70,9 +70,9 @@ describe('LocalCommandExecutionRepository', () => { }); when(encryptionService.decrypt) - .calledWith(mockCommandExecutionEntity.command, jasmine.anything()) + .calledWith(mockCommandExecutionEntity.command, expect.anything()) .mockResolvedValue(mockCommandExecution.command) - .calledWith(mockCommandExecutionEntity.result, jasmine.anything()) + .calledWith(mockCommandExecutionEntity.result, expect.anything()) .mockResolvedValue(JSON.stringify(mockCommandExecution.result)); repository.save.mockReturnValue(mockCommandExecutionEntity); From f3698a8e5f027d73454a7e5ed53ba1f78cc529bb Mon Sep 17 00:00:00 2001 From: Roman Sergeenko Date: Fri, 25 Oct 2024 10:49:01 +0200 Subject: [PATCH 111/112] fix detail token --- redisinsight/ui/src/pages/workbench/utils/query.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/redisinsight/ui/src/pages/workbench/utils/query.ts b/redisinsight/ui/src/pages/workbench/utils/query.ts index d5489e8e98..c9eceb774b 100644 --- a/redisinsight/ui/src/pages/workbench/utils/query.ts +++ b/redisinsight/ui/src/pages/workbench/utils/query.ts @@ -379,7 +379,10 @@ export const findArgByToken = (list: IRedisCommand[], arg: string): Maybe) => { if (!command) return '' - if (command.arguments) return generateArgsNames(CommandProvider.Main, command.arguments).join(' ') + if (command.arguments) { + const args = generateArgsNames(CommandProvider.Main, command.arguments).join(' ') + return command.token ? `${command.token} ${args}` : args + } if (command.token) { if (command.type === ICommandTokenType.PureToken) return command.token return `${command.token}` From 8ae91a2c9a90dc7cfea9e0a9f32925dc94c628e0 Mon Sep 17 00:00:00 2001 From: ArtemHoruzhenko Date: Fri, 25 Oct 2024 11:54:27 +0300 Subject: [PATCH 112/112] fix ITests --- .../api/redisearch/POST-databases-id-redisearch-info.test.ts | 2 +- .../api/test/api/ws/bulk-actions/bulk-actions-create.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/redisinsight/api/test/api/redisearch/POST-databases-id-redisearch-info.test.ts b/redisinsight/api/test/api/redisearch/POST-databases-id-redisearch-info.test.ts index a70d92e566..e963782cce 100644 --- a/redisinsight/api/test/api/redisearch/POST-databases-id-redisearch-info.test.ts +++ b/redisinsight/api/test/api/redisearch/POST-databases-id-redisearch-info.test.ts @@ -115,7 +115,7 @@ describe('POST /databases/:id/redisearch/info', () => { }, statusCode: 500, responseBody: { - message: 'Unknown index name', + message: 'Unknown Index name', error: 'Internal Server Error', statusCode: 500, }, diff --git a/redisinsight/api/test/api/ws/bulk-actions/bulk-actions-create.test.ts b/redisinsight/api/test/api/ws/bulk-actions/bulk-actions-create.test.ts index e709c362b8..ab964dd605 100644 --- a/redisinsight/api/test/api/ws/bulk-actions/bulk-actions-create.test.ts +++ b/redisinsight/api/test/api/ws/bulk-actions/bulk-actions-create.test.ts @@ -23,7 +23,7 @@ const createDto = { let client; describe('bulk-actions', function () { - this.timeout(10000); + this.timeout(20000); beforeEach(async () => { client = await getClient(); await rte.data.generateKeys(true);