Skip to content

Commit 31d46aa

Browse files
authored
Merge pull request #1791 from RedisInsight/feature/RI-3726_profile_explain
Feature/ri 3726 profile explain
2 parents a1b0c54 + 095126b commit 31d46aa

File tree

31 files changed

+3511
-1050
lines changed

31 files changed

+3511
-1050
lines changed
Lines changed: 3 additions & 0 deletions
Loading

redisinsight/ui/src/components/query-card/QueryCard.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import cx from 'classnames'
44
import { EuiLoadingContent, keys } from '@elastic/eui'
55
import { useParams } from 'react-router-dom'
66

7-
import { WBQueryType } from 'uiSrc/pages/workbench/constants'
7+
import { WBQueryType, ProfileQueryType } from 'uiSrc/pages/workbench/constants'
88
import { RunQueryMode, ResultsMode, ResultsSummary } from 'uiSrc/slices/interfaces/workbench'
99
import {
1010
getWBQueryType,
@@ -44,6 +44,7 @@ export interface Props {
4444
onQueryDelete: () => void
4545
onQueryReRun: () => void
4646
onQueryOpen: () => void
47+
onQueryProfile: (type: ProfileQueryType) => void
4748
}
4849

4950
const getDefaultPlugin = (views: IPluginVisualization[], query: string) =>
@@ -75,6 +76,7 @@ const QueryCard = (props: Props) => {
7576
createdAt,
7677
onQueryOpen,
7778
onQueryDelete,
79+
onQueryProfile,
7880
onQueryReRun,
7981
loading,
8082
emptyCommand,
@@ -186,6 +188,7 @@ const QueryCard = (props: Props) => {
186188
setSelectedValue={changeViewTypeSelected}
187189
onQueryDelete={onQueryDelete}
188190
onQueryReRun={onQueryReRun}
191+
onQueryProfile={onQueryProfile}
189192
/>
190193
{isOpen && (
191194
<>

redisinsight/ui/src/components/query-card/QueryCardHeader/QueryCardHeader.tsx

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import { numberWithSpaces } from 'uiSrc/utils/numbers'
3232
import { ThemeContext } from 'uiSrc/contexts/themeContext'
3333
import { appPluginsSelector } from 'uiSrc/slices/app/plugins'
3434
import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
35-
import { getViewTypeOptions, WBQueryType } from 'uiSrc/pages/workbench/constants'
35+
import { getViewTypeOptions, WBQueryType, getProfileViewTypeOptions, ProfileQueryType, isCommandAllowedForProfile } from 'uiSrc/pages/workbench/constants'
3636
import { IPluginVisualization } from 'uiSrc/slices/interfaces'
3737
import { RunQueryMode, ResultsMode, ResultsSummary } from 'uiSrc/slices/interfaces/workbench'
3838
import { appRedisCommandsSelector } from 'uiSrc/slices/app/redis-commands'
@@ -70,6 +70,7 @@ export interface Props {
7070
setSelectedValue: (type: WBQueryType, value: string) => void
7171
onQueryDelete: () => void
7272
onQueryReRun: () => void
73+
onQueryProfile: (type: ProfileQueryType) => void
7374
}
7475

7576
const getExecutionTimeString = (value: number): string => {
@@ -109,6 +110,7 @@ const QueryCardHeader = (props: Props) => {
109110
setSelectedValue,
110111
onQueryDelete,
111112
onQueryReRun,
113+
onQueryProfile,
112114
db,
113115
} = props
114116

@@ -237,6 +239,30 @@ const QueryCardHeader = (props: Props) => {
237239
}
238240
})
239241

242+
const profileOptions: EuiSuperSelectOption<any>[] = (getProfileViewTypeOptions() as any[]).map((item) => {
243+
const { value, id, text } = item
244+
return {
245+
value: id ?? value,
246+
inputDisplay: (
247+
<div className={cx(styles.dropdownOption, styles.dropdownProfileOption)}>
248+
<EuiIcon
249+
className={styles.iconDropdownOption}
250+
type="visTagCloud"
251+
data-testid={`view-type-selected-${value}-${id}`}
252+
/>
253+
</div>
254+
),
255+
dropdownDisplay: (
256+
<div className={cx(styles.dropdownOption, styles.dropdownProfileOption)}>
257+
<span>{truncateText(text, 20)}</span>
258+
</div>
259+
),
260+
'data-test-subj': `profile-type-option-${value}-${id}`,
261+
}
262+
})
263+
264+
const canCommandProfile = isCommandAllowedForProfile(query)
265+
240266
const indexForSeparator = findIndex(pluginsOptions, (option) => !option.internal)
241267
if (indexForSeparator > -1) {
242268
modifiedOptions.splice(indexForSeparator + 1, 0, {
@@ -320,6 +346,26 @@ const QueryCardHeader = (props: Props) => {
320346
</EuiToolTip>
321347
)}
322348
</EuiFlexItem>
349+
{isOpen && canCommandProfile && !summaryText && (
350+
<EuiFlexItem
351+
grow={false}
352+
className={cx(styles.buttonIcon, styles.viewTypeIcon)}
353+
onClick={onDropDownViewClick}
354+
>
355+
<div className={styles.dropdownWrapper}>
356+
<div className={styles.dropdown}>
357+
<EuiSuperSelect
358+
options={profileOptions}
359+
itemClassName={cx(styles.changeViewItem, styles.dropdownProfileItem)}
360+
className={cx(styles.changeView, styles.dropdownProfileIcon)}
361+
valueOfSelected={ProfileQueryType.Profile}
362+
onChange={(value: ProfileQueryType) => onQueryProfile(value)}
363+
data-testid="run-profile-type"
364+
/>
365+
</div>
366+
</div>
367+
</EuiFlexItem>
368+
)}
323369
<EuiFlexItem
324370
grow={false}
325371
className={cx(styles.buttonIcon, styles.viewTypeIcon)}

redisinsight/ui/src/components/query-card/QueryCardHeader/styles.module.scss

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,27 @@ $marginIcon: 12px;
127127
height: 40px;
128128
}
129129

130+
.dropdownProfileIcon {
131+
padding: inherit !important;
132+
:global {
133+
.euiSuperSelectControl.euiFormControlLayoutIcons {
134+
display: none !important;
135+
}
136+
}
137+
}
138+
139+
.dropdownProfileOption {
140+
display: inherit !important;
141+
}
142+
143+
.dropdownProfileItem {
144+
:global {
145+
.euiContextMenu__icon {
146+
display: none !important;
147+
}
148+
}
149+
}
150+
130151
.dropdown {
131152
width: 168px;
132153
position: absolute;

redisinsight/ui/src/packages/redisearch/package.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,18 @@
4343
"iconLight": "./dist/table_view_icon_light.svg",
4444
"description": "RediSearch default plugin",
4545
"default": true
46+
},
47+
{
48+
"id": "redisearch-profile",
49+
"name": "Table",
50+
"activationMethod": "renderRediSearch",
51+
"matchCommands": [
52+
"FT.PROFILE"
53+
],
54+
"iconDark": "./dist/table_view_icon_dark.svg",
55+
"iconLight": "./dist/table_view_icon_light.svg",
56+
"description": "RediSearch default plugin",
57+
"default": false
4658
}
4759
],
4860
"devDependencies": {

redisinsight/ui/src/packages/redisearch/src/App.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
parseSearchRawResponse,
1010
parseAggregateRawResponse
1111
} from './utils'
12-
import { Command } from './constants'
12+
import { Command, ProfileType } from './constants'
1313
import { TableInfoResult, TableResult } from './components'
1414

1515
interface Props {
@@ -35,16 +35,25 @@ const App = (props: Props) => {
3535
return <TableInfoResult query={command} result={result} />
3636
}
3737

38-
if (commandUpper.startsWith(Command.Aggregate)) {
39-
const [matched, ...arrayResponse] = response
38+
const isProfileCommand = commandUpper.startsWith(Command.Profile)
39+
const profileQueryType = command?.split(' ')?.[2]
40+
41+
if (
42+
commandUpper.startsWith(Command.Aggregate)
43+
|| (isProfileCommand && profileQueryType.toUpperCase() === ProfileType.Aggregate)
44+
) {
45+
const [matched, ...arrayResponse] = isProfileCommand ? response[0] : response
4046
setHeaderText(`Matched:${matched}`)
4147

4248
const result = parseAggregateRawResponse(arrayResponse)
4349
return <TableResult query={command} result={result} matched={matched} />
4450
}
4551

46-
if (commandUpper.startsWith(Command.Search)) {
47-
const [matched, ...arrayResponse] = response
52+
if (
53+
commandUpper.startsWith(Command.Search)
54+
|| (isProfileCommand && profileQueryType.toUpperCase() === ProfileType.Search)
55+
) {
56+
const [matched, ...arrayResponse] = isProfileCommand ? response[0] : response
4857
setHeaderText(`Matched:${matched}`)
4958

5059
const result = parseSearchRawResponse(command, arrayResponse)

redisinsight/ui/src/packages/redisearch/src/constants/constants.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ export enum Command {
44
Search = 'FT.SEARCH',
55
Aggregate = 'FT.AGGREGATE',
66
Info = 'FT.INFO',
7+
Profile = 'FT.PROFILE',
8+
}
9+
10+
export enum ProfileType {
11+
Search = 'SEARCH',
12+
Aggregate = 'AGGREGATE',
713
}
814

915
export enum CommandArgument {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# RI-Explain plugin
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{
2+
"author": {
3+
"name": "Redis Ltd.",
4+
"email": "[email protected]",
5+
"url": "https://redis.com/redis-enterprise/redis-insight"
6+
},
7+
"bugs": {
8+
"url": "https://github.com/"
9+
},
10+
"description": "Show Profile/Explain Visualization",
11+
"source": "./src/main.tsx",
12+
"styles": "./dist/styles.css",
13+
"main": "./dist/index.js",
14+
"name": "explain-plugin",
15+
"version": "0.0.1",
16+
"scripts": {
17+
"start": "cross-env NODE_ENV=development parcel serve src/index.html",
18+
"build": "rimraf dist && cross-env NODE_ENV=production concurrently \"yarn build:js && yarn minify:js\" \"yarn build:css\" \"yarn build:assets\"",
19+
"build-lite": "rm dist/*.js && cross-env NODE_ENV=production concurrently \"yarn build:js && yarn minify:js\"",
20+
"build:js": "parcel build src/main.tsx --dist-dir dist",
21+
"build:css": "parcel build src/styles/styles.less --dist-dir dist",
22+
"build:assets": "parcel build src/assets/**/* --dist-dir dist",
23+
"minify:js": "terser -- dist/main.js > dist/index.js && rimraf dist/main.js"
24+
},
25+
"targets": {
26+
"main": false,
27+
"module": {
28+
"includeNodeModules": true
29+
}
30+
},
31+
"visualizations": [
32+
{
33+
"id": "profile-explain-viz",
34+
"name": "Visualization",
35+
"activationMethod": "renderCore",
36+
"matchCommands": [
37+
"FT.EXPLAIN",
38+
"FT.EXPLAINCLI",
39+
"FT.PROFILE",
40+
"GRAPH.EXPLAIN",
41+
"GRAPH.PROFILE"
42+
],
43+
"iconDark": "./dist/profile_icon_dark.svg",
44+
"iconLight": "./dist/profile_icon_light.svg",
45+
"description": "Profile/Explain plugin Visualization",
46+
"default": true
47+
}
48+
],
49+
"devDependencies": {
50+
"@parcel/compressor-brotli": "^2.0.0",
51+
"@parcel/compressor-gzip": "^2.0.0",
52+
"@parcel/transformer-less": "^2.3.2",
53+
"concurrently": "^6.3.0",
54+
"cross-env": "^7.0.3",
55+
"parcel": "^2.0.0",
56+
"rimraf": "^3.0.2",
57+
"terser": "^5.9.0"
58+
},
59+
"dependencies": {
60+
"@antv/hierarchy": "^0.6.8",
61+
"@antv/x6": "^2.1.3",
62+
"@antv/x6-react-shape": "^2.1.0",
63+
"@elastic/eui": "34.6.0",
64+
"@emotion/react": "^11.7.1",
65+
"classnames": "^2.3.1",
66+
"prop-types": "^15.8.1",
67+
"react": "^18.2.0",
68+
"react-dom": "^18.2.0",
69+
"redisinsight-plugin-sdk": "^1.1.0",
70+
"uuid": "^9.0.0"
71+
}
72+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import React from 'react'
2+
import Explain from './Explain'
3+
4+
export function App(props: { command?: string, data: any }) {
5+
6+
const ErrorResponse = HandleError(props)
7+
8+
if (ErrorResponse !== null) return ErrorResponse
9+
10+
return (
11+
<div id="mainApp" style={{ height: "100%", width: '100%', overflow: 'hidden'}}>
12+
<Explain command={props.command || ''} data={props.data}/>
13+
</div>
14+
)
15+
}
16+
17+
function HandleError(props: { command?: string, data: any }): JSX.Element | null {
18+
const { data: [{ response = '', status = '' } = {}] = [] } = props
19+
20+
if (status === 'fail') {
21+
return <div className="responseFail">{JSON.stringify(response)}</div>
22+
}
23+
24+
return null
25+
}

0 commit comments

Comments
 (0)