Skip to content

Commit b1f7a4a

Browse files
#RI-4139 - Update visualizations when there is no data to visualize (#1735)
* #RI-4139 - add formatRedisReply to plugin sdk * #RI-4139 - add raw response to redisgraph when no data to visualize * add test for plugin text * selector added --------- Co-authored-by: vlad-dargel <[email protected]> Co-authored-by: vlad-dargel <[email protected]>
1 parent cbf4edf commit b1f7a4a

File tree

19 files changed

+389
-112
lines changed

19 files changed

+389
-112
lines changed

redisinsight/ui/src/components/query-card/QueryCardCliPlugin/QueryCardCliPlugin.spec.tsx

Lines changed: 115 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,24 @@ import { instance, mock } from 'ts-mockito'
44
import { PluginEvents } from 'uiSrc/plugins/pluginEvents'
55
import { pluginApi } from 'uiSrc/services/PluginAPI'
66
import { cleanup, mockedStore, render } from 'uiSrc/utils/test-utils'
7+
import { formatToText } from 'uiSrc/utils'
8+
import { sendPluginCommandAction, getPluginStateAction, setPluginStateAction } from 'uiSrc/slices/app/plugins'
79
import QueryCardCliPlugin, { Props } from './QueryCardCliPlugin'
810

911
const mockedProps = mock<Props>()
1012

11-
let store: typeof mockedStore
12-
beforeEach(() => {
13-
cleanup()
14-
store = cloneDeep(mockedStore)
15-
store.clearActions()
16-
})
17-
1813
jest.mock('uiSrc/services/PluginAPI', () => ({
1914
pluginApi: {
20-
onEvent: jest.fn()
15+
onEvent: jest.fn(),
16+
sendEvent: jest.fn(),
2117
}
2218
}))
2319

20+
jest.mock('uiSrc/utils', () => ({
21+
...jest.requireActual('uiSrc/utils'),
22+
formatToText: jest.fn()
23+
}))
24+
2425
jest.mock('uiSrc/slices/app/plugins', () => ({
2526
...jest.requireActual('uiSrc/slices/app/plugins'),
2627
appPluginsSelector: jest.fn().mockReturnValue({
@@ -35,6 +36,9 @@ jest.mock('uiSrc/slices/app/plugins', () => ({
3536
}
3637
]
3738
}),
39+
sendPluginCommandAction: jest.fn(),
40+
getPluginStateAction: jest.fn(),
41+
setPluginStateAction: jest.fn(),
3842
}))
3943

4044
jest.mock('uiSrc/services', () => ({
@@ -45,6 +49,13 @@ jest.mock('uiSrc/services', () => ({
4549
},
4650
}))
4751

52+
let store: typeof mockedStore
53+
beforeEach(() => {
54+
cleanup()
55+
store = cloneDeep(mockedStore)
56+
store.clearActions()
57+
})
58+
4859
describe('QueryCardCliPlugin', () => {
4960
it('should render', () => {
5061
expect(render(<QueryCardCliPlugin {...instance(mockedProps)} />)).toBeTruthy()
@@ -64,5 +75,101 @@ describe('QueryCardCliPlugin', () => {
6475
expect(onEventMock).toBeCalledWith(expect.any(String), PluginEvents.executeRedisCommand, expect.any(Function))
6576
expect(onEventMock).toBeCalledWith(expect.any(String), PluginEvents.getState, expect.any(Function))
6677
expect(onEventMock).toBeCalledWith(expect.any(String), PluginEvents.setState, expect.any(Function))
78+
expect(onEventMock).toBeCalledWith(expect.any(String), PluginEvents.formatRedisReply, expect.any(Function))
79+
})
80+
81+
it('should subscribes and call sendPluginCommandAction', () => {
82+
const mockedSendPluginCommandAction = jest.fn().mockImplementation(() => jest.fn());
83+
(sendPluginCommandAction as jest.Mock).mockImplementation(mockedSendPluginCommandAction)
84+
85+
const onEventMock = jest.fn().mockImplementation(
86+
(_iframeId: string, event: string, callback: (data: any) => void) => {
87+
if (event === PluginEvents.executeRedisCommand) {
88+
callback({ command: 'info' })
89+
}
90+
}
91+
);
92+
93+
(pluginApi.onEvent as jest.Mock).mockImplementation(onEventMock)
94+
95+
render(<QueryCardCliPlugin {...instance(mockedProps)} id="1" />)
96+
97+
expect(mockedSendPluginCommandAction).toBeCalledWith(
98+
{
99+
command: 'info',
100+
onSuccessAction: expect.any(Function),
101+
onFailAction: expect.any(Function)
102+
}
103+
)
104+
})
105+
106+
it('should subscribes and call getPluginStateAction with proper data', () => {
107+
const mockedGetPluginStateAction = jest.fn().mockImplementation(() => jest.fn());
108+
(getPluginStateAction as jest.Mock).mockImplementation(mockedGetPluginStateAction)
109+
110+
const onEventMock = jest.fn().mockImplementation(
111+
(_iframeId: string, event: string, callback: (data: any) => void) => {
112+
if (event === PluginEvents.getState) {
113+
callback({ requestId: 5 })
114+
}
115+
}
116+
);
117+
118+
(pluginApi.onEvent as jest.Mock).mockImplementation(onEventMock)
119+
120+
render(<QueryCardCliPlugin {...instance(mockedProps)} id="1" commandId="100" />)
121+
122+
expect(mockedGetPluginStateAction).toBeCalledWith(
123+
{
124+
commandId: '100',
125+
onSuccessAction: expect.any(Function),
126+
onFailAction: expect.any(Function),
127+
visualizationId: '1'
128+
}
129+
)
130+
})
131+
132+
it('should subscribes and call setPluginStateAction with proper data', () => {
133+
const mockedSetPluginStateAction = jest.fn().mockImplementation(() => jest.fn());
134+
(setPluginStateAction as jest.Mock).mockImplementation(mockedSetPluginStateAction)
135+
136+
const onEventMock = jest.fn().mockImplementation(
137+
(_iframeId: string, event: string, callback: (data: any) => void) => {
138+
if (event === PluginEvents.setState) {
139+
callback({ requestId: 5 })
140+
}
141+
}
142+
);
143+
144+
(pluginApi.onEvent as jest.Mock).mockImplementation(onEventMock)
145+
146+
render(<QueryCardCliPlugin {...instance(mockedProps)} id="1" commandId="200" />)
147+
148+
expect(mockedSetPluginStateAction).toBeCalledWith(
149+
{
150+
commandId: '200',
151+
onSuccessAction: expect.any(Function),
152+
onFailAction: expect.any(Function),
153+
visualizationId: '1'
154+
}
155+
)
156+
})
157+
158+
it('should subscribes and call formatToText', () => {
159+
const formatToTextMock = jest.fn();
160+
(formatToText as jest.Mock).mockImplementation(formatToTextMock)
161+
const onEventMock = jest.fn().mockImplementation(
162+
(_iframeId: string, event: string, callback: (dat: any) => void) => {
163+
if (event === PluginEvents.formatRedisReply) {
164+
callback({ requestId: '1', data: { response: [], command: 'info' } })
165+
}
166+
}
167+
);
168+
169+
(pluginApi.onEvent as jest.Mock).mockImplementation(onEventMock)
170+
171+
render(<QueryCardCliPlugin {...instance(mockedProps)} id="1" />)
172+
173+
expect(formatToTextMock).toBeCalledWith([], 'info')
67174
})
68175
})

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

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { v4 as uuidv4 } from 'uuid'
55
import { EuiFlexItem, EuiIcon, EuiLoadingContent, EuiTextColor } from '@elastic/eui'
66
import { pluginApi } from 'uiSrc/services/PluginAPI'
77
import { ThemeContext } from 'uiSrc/contexts/themeContext'
8-
import { getBaseApiUrl, Nullable } from 'uiSrc/utils'
8+
import { getBaseApiUrl, Nullable, formatToText } from 'uiSrc/utils'
99
import { Theme } from 'uiSrc/constants'
1010
import { CommandExecutionResult, IPluginVisualization } from 'uiSrc/slices/interfaces'
1111
import { PluginEvents } from 'uiSrc/plugins/pluginEvents'
@@ -156,6 +156,28 @@ const QueryCardCliPlugin = (props: Props) => {
156156
)
157157
}
158158

159+
const formatRedisResponse = (
160+
{ requestId, data }: { requestId: string, data: { response: any, command: string } }
161+
) => {
162+
try {
163+
const reply = formatToText(data?.response || '(nil)', data.command)
164+
165+
sendMessageToPlugin({
166+
event: PluginEvents.formatRedisReply,
167+
requestId,
168+
actionType: ActionTypes.Resolve,
169+
data: reply
170+
})
171+
} catch (e) {
172+
sendMessageToPlugin({
173+
event: PluginEvents.formatRedisReply,
174+
requestId,
175+
actionType: ActionTypes.Reject,
176+
data: e
177+
})
178+
}
179+
}
180+
159181
useEffect(() => {
160182
if (currentView === null) return
161183
pluginApi.onEvent(generatedIframeNameRef.current, PluginEvents.heightChanged, (height: string) => {
@@ -183,6 +205,7 @@ const QueryCardCliPlugin = (props: Props) => {
183205
pluginApi.onEvent(generatedIframeNameRef.current, PluginEvents.executeRedisCommand, sendRedisCommand)
184206
pluginApi.onEvent(generatedIframeNameRef.current, PluginEvents.getState, getPluginState)
185207
pluginApi.onEvent(generatedIframeNameRef.current, PluginEvents.setState, setPluginState)
208+
pluginApi.onEvent(generatedIframeNameRef.current, PluginEvents.formatRedisReply, formatRedisResponse)
186209
}, [currentView])
187210

188211
const renderPluginIframe = (config: any) => {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,6 @@
6161
"react": "^17.0.2",
6262
"react-dom": "^17.0.2",
6363
"react-json-tree": "^0.16.1",
64-
"redisinsight-plugin-sdk": "^1.0.0"
64+
"redisinsight-plugin-sdk": "^1.1.0"
6565
}
6666
}

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

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,27 @@ import { COMPACT_FLAG } from './constants'
88
const isDarkTheme = document.body.classList.contains('theme_DARK')
99

1010
const json_tree_theme = {
11-
scheme: 'solarized',
12-
author: 'ethan schoonover (http://ethanschoonover.com/solarized)',
13-
base00: '#002b36',
14-
base01: '#073642',
15-
base02: '#586e75',
16-
base03: '#657b83',
17-
base04: '#839496',
18-
base05: '#93a1a1',
19-
base06: '#eee8d5',
20-
base07: '#fdf6e3',
21-
base08: '#dc322f',
22-
base09: '#098658',
23-
base0A: '#b58900',
24-
base0B: '#A31515',
25-
base0C: '#2aa198',
26-
base0D: '#0451A5',
27-
base0E: '#6c71c4',
28-
base0F: '#d33682',
11+
scheme: 'solarized',
12+
author: 'ethan schoonover (http://ethanschoonover.com/solarized)',
13+
base00: '#002b36',
14+
base01: '#073642',
15+
base02: '#586e75',
16+
base03: '#657b83',
17+
base04: '#839496',
18+
base05: '#93a1a1',
19+
base06: '#eee8d5',
20+
base07: '#fdf6e3',
21+
base08: '#dc322f',
22+
base09: '#098658',
23+
base0A: '#b58900',
24+
base0B: '#A31515',
25+
base0C: '#2aa198',
26+
base0D: '#0451A5',
27+
base0E: '#6c71c4',
28+
base0F: '#d33682',
2929
}
3030

3131
export function TableApp(props: { command?: string, data: any }) {
32-
3332
const ErrorResponse = HandleError(props)
3433

3534
if (ErrorResponse !== null) return ErrorResponse
@@ -40,10 +39,10 @@ export function TableApp(props: { command?: string, data: any }) {
4039
<div className="table-view">
4140
<Table
4241
data={tableData.results}
43-
columns={tableData.headers.map(h => ({
42+
columns={tableData.headers.map((h) => ({
4443
field: h,
4544
name: h,
46-
render: d => (
45+
render: (d) => (
4746
<JSONTree
4847
invertTheme={isDarkTheme}
4948
theme={{
@@ -52,7 +51,7 @@ export function TableApp(props: { command?: string, data: any }) {
5251
style: { ...style, backgroundColor: undefined }, // removing default background color from styles
5352
}),
5453
}}
55-
labelRenderer={(key) => key ? key : null}
54+
labelRenderer={(key) => (key || null)}
5655
hideRoot
5756
data={d}
5857
/>
@@ -63,16 +62,15 @@ export function TableApp(props: { command?: string, data: any }) {
6362
)
6463
}
6564

66-
6765
export function GraphApp(props: { command?: string, data: any }) {
68-
66+
const { data, command = '' } = props
6967
const ErrorResponse = HandleError(props)
7068

7169
if (ErrorResponse !== null) return ErrorResponse
7270

7371
return (
74-
<div style={{ height: "100%" }}>
75-
<Graph graphKey={props.command.split(' ')[1]} data={props.data[0].response} />
72+
<div style={{ height: '100%' }}>
73+
<Graph graphKey={command.split(' ')[1]} data={data[0].response} command={command} />
7674
</div>
7775
)
7876
}
@@ -84,7 +82,7 @@ function HandleError(props: { command?: string, data: any }): JSX.Element {
8482
return <div className="responseFail">{JSON.stringify(response)}</div>
8583
}
8684

87-
if (status === 'success' && typeof(response) === 'string') {
85+
if (status === 'success' && typeof (response) === 'string') {
8886
return <div className="responseFail">{JSON.stringify(response)}</div>
8987
}
9088

0 commit comments

Comments
 (0)