Skip to content

Commit 4ad1640

Browse files
committed
#RI-3561 - Handle big output in Workbench
1 parent cba59c4 commit 4ad1640

File tree

17 files changed

+287
-48
lines changed

17 files changed

+287
-48
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ const QueryCard = (props: Props) => {
186186
resultsMode={resultsMode}
187187
result={result}
188188
isNotStored={isNotStored}
189+
isFullScreen={isFullScreen}
189190
data-testid="group-mode-card"
190191
/>
191192
)}
@@ -215,6 +216,7 @@ const QueryCard = (props: Props) => {
215216
resultsMode={resultsMode}
216217
result={result}
217218
isNotStored={isNotStored}
219+
isFullScreen={isFullScreen}
218220
/>
219221
)}
220222
</>
Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react'
22
import { instance, mock } from 'ts-mockito'
33
import { render } from 'uiSrc/utils/test-utils'
4-
import QueryCardCliDefaultResult, { Props } from './QueryCardCliDefaultResult'
4+
import QueryCardCliDefaultResult, { MAX_CARD_HEIGHT, MIN_ROWS_COUNT, Props } from './QueryCardCliDefaultResult'
55

66
const mockedProps = mock<Props>()
77

@@ -10,18 +10,33 @@ describe('QueryCardCliDefaultResult', () => {
1010
expect(render(<QueryCardCliDefaultResult {...instance(mockedProps)} />)).toBeTruthy()
1111
})
1212

13-
it('Result element should render (nil) result', () => {
14-
const mockResult = [{
15-
response: '',
16-
status: 'success'
17-
}]
13+
it(`container min-height should be ${MAX_CARD_HEIGHT} if items.length > ${MIN_ROWS_COUNT}`, () => {
14+
const mockResult: string[] = Array.from({ length: MIN_ROWS_COUNT + 1 }).fill('123')
1815

1916
const { queryByTestId } = render(
20-
<QueryCardCliDefaultResult {...instance(mockedProps)} result={mockResult} />
17+
<QueryCardCliDefaultResult {...instance(mockedProps)} items={mockResult} />
2118
)
2219

23-
const resultEl = queryByTestId('query-cli-group-result')
20+
const resultEl = queryByTestId('query-cli-card-result')
2421

25-
expect(resultEl).toHaveTextContent('(nil)')
22+
expect(resultEl).toHaveStyle(`min-height: ${MAX_CARD_HEIGHT}px`)
23+
expect(resultEl).toHaveTextContent(mockResult.join(''))
2624
})
25+
26+
it(
27+
`container min-height should be calculated and less than ${MAX_CARD_HEIGHT} if items.length < ${MIN_ROWS_COUNT} `,
28+
() => {
29+
const mockResult: string[] = Array.from({ length: 5 }).fill('123')
30+
31+
const { queryByTestId } = render(
32+
<QueryCardCliDefaultResult {...instance(mockedProps)} items={mockResult} />
33+
)
34+
35+
const resultEl = queryByTestId('query-cli-card-result')
36+
37+
expect(resultEl).not.toHaveStyle(`min-height: ${MAX_CARD_HEIGHT}px`)
38+
expect(resultEl).toHaveStyle('min-height: 90px')
39+
expect(resultEl).toHaveTextContent(mockResult.join(''))
40+
}
41+
)
2742
})

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

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,60 @@
1-
import React from 'react'
1+
import React, { useCallback, useEffect, useState } from 'react'
2+
import cx from 'classnames'
23

3-
import { CommandExecutionResult } from 'uiSrc/slices/interfaces'
4-
import { cliParseTextResponse, CliPrefix, Maybe } from 'uiSrc/utils'
4+
import VirtualList from 'uiSrc/components/virtual-list'
5+
import { getNodeText } from 'uiSrc/utils'
6+
7+
import styles from './styles.module.scss'
58

69
export interface Props {
7-
query: string
8-
result: Maybe<CommandExecutionResult[]>
10+
items: (string | JSX.Element)[]
11+
isFullScreen?: boolean
912
}
1013

14+
export const MIN_ROW_HEIGHT = 18
15+
export const MIN_ROWS_COUNT = 11
16+
export const MAX_CARD_HEIGHT = 210
17+
export const SYMBOL_WIDTH = 4.5
18+
1119
const QueryCardCliGroupResult = (props: Props) => {
12-
const { result = [], query } = props
20+
const { items = [], isFullScreen } = props
21+
22+
const [windowDimensions, setWindowDimensions] = useState({ width: 0, height: 0 })
23+
24+
const calculateHeight = useCallback((sum: number, item: string | JSX.Element) => {
25+
const itemLength = getNodeText(item)?.length || 0
26+
const symbolsPerLine = (windowDimensions.width - 400) / SYMBOL_WIDTH
27+
28+
return sum + Math.ceil((itemLength / symbolsPerLine)) * MIN_ROW_HEIGHT
29+
}, [windowDimensions.width])
30+
31+
const calculatedHeight = items.length > MIN_ROWS_COUNT && !isFullScreen
32+
? MAX_CARD_HEIGHT
33+
: items.reduce(calculateHeight, 0)
34+
35+
useEffect(() => {
36+
updateWindowDimensions()
37+
globalThis.addEventListener('resize', updateWindowDimensions)
38+
return () => {
39+
globalThis.removeEventListener('resize', updateWindowDimensions)
40+
}
41+
}, [])
42+
43+
const updateWindowDimensions = () => {
44+
setWindowDimensions({ height: globalThis.innerHeight, width: globalThis.innerWidth })
45+
}
1346

1447
return (
15-
<div data-testid="query-cli-group-result">
16-
{result?.map(({ response, status }) =>
17-
cliParseTextResponse(response || '(nil)', query, status, CliPrefix.QueryCard))}
48+
<div
49+
className={cx(styles.container, 'query-card-output-response-success')}
50+
style={{
51+
minHeight: isFullScreen
52+
? windowDimensions.height - 65
53+
: Math.min(calculatedHeight, MAX_CARD_HEIGHT)
54+
}}
55+
data-testid="query-cli-card-result"
56+
>
57+
<VirtualList items={items} />
1858
</div>
1959
)
2060
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
@import "@elastic/eui/src/global_styling/mixins/helpers";
2+
@import "@elastic/eui/src/components/table/mixins";
3+
@import "@elastic/eui/src/global_styling/index";
4+
5+
.container {
6+
position: relative;
7+
width: 100%;
8+
display: flex;
9+
flex-grow: 1;
10+
height: 100%;
11+
}
12+
13+
.listContent {
14+
@include euiScrollBar;
15+
}

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
1+
import { flatten } from 'lodash'
12
import React from 'react'
23

34
import { CommandExecutionResult } from 'uiSrc/slices/interfaces'
45
import { cliParseCommandsGroupResult, Maybe } from 'uiSrc/utils'
6+
import QueryCardCliDefaultResult from '../QueryCardCliDefaultResult'
57

68
export interface Props {
79
result?: Maybe<CommandExecutionResult[]>
10+
isFullScreen?: boolean
811
}
912

1013
const QueryCardCliGroupResult = (props: Props) => {
11-
const { result = [] } = props
14+
const { result = [], isFullScreen } = props
1215
return (
1316
<div data-testid="query-cli-default-result" className="query-card-output-response-success">
14-
{result[0]?.response.map((item: any, index: number) =>
15-
cliParseCommandsGroupResult(item, index))}
17+
<QueryCardCliDefaultResult
18+
isFullScreen={isFullScreen}
19+
items={flatten(result?.[0]?.response.map((item: any) =>
20+
flatten(cliParseCommandsGroupResult(item))))}
21+
/>
1622
</div>
1723
)
1824
}

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

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React from 'react'
33
import { instance, mock } from 'ts-mockito'
44
import { cleanup, mockedStore, render, screen } from 'uiSrc/utils/test-utils'
55
import { ResultsMode } from 'uiSrc/slices/interfaces/workbench'
6+
import { CommandExecutionStatus } from 'uiSrc/slices/interfaces/cli'
67
import QueryCardCliResultWrapper, { Props } from './QueryCardCliResultWrapper'
78
import QueryCardCliDefaultResult, { Props as QueryCardCliDefaultResultProps } from '../QueryCardCliDefaultResult'
89
import QueryCardCliGroupResult, { Props as QueryCardCliGroupResultProps } from '../QueryCardCliGroupResult'
@@ -28,13 +29,17 @@ jest.mock('uiSrc/services', () => ({
2829

2930
describe('QueryCardCliResultWrapper', () => {
3031
it('should render', () => {
31-
expect(render(<QueryCardCliResultWrapper {...instance(mockedProps)} />)).toBeTruthy()
32+
const mockResult = [{
33+
response: 'response',
34+
status: CommandExecutionStatus.Success
35+
}]
36+
expect(render(<QueryCardCliResultWrapper {...instance(mockedProps)} result={mockResult} />)).toBeTruthy()
3237
})
3338

3439
it('Result element should render with result prop', () => {
3540
const mockResult = [{
3641
response: 'response',
37-
status: 'success'
42+
status: CommandExecutionStatus.Success
3843
}]
3944

4045
render(
@@ -78,7 +83,7 @@ describe('QueryCardCliResultWrapper', () => {
7883
it('should render QueryCardCliDefaultResult when result.response is not array', () => {
7984
const mockResult = [{
8085
response: 'response',
81-
status: 'success'
86+
status: CommandExecutionStatus.Success
8287
}]
8388

8489
render(
@@ -97,10 +102,29 @@ describe('QueryCardCliResultWrapper', () => {
97102
})
98103

99104
it('should render warning', () => {
105+
const mockResult = [{
106+
response: 'response',
107+
status: CommandExecutionStatus.Success
108+
}]
100109
render(
101-
<QueryCardCliResultWrapper {...instance(mockedProps)} isNotStored />
110+
<QueryCardCliResultWrapper {...instance(mockedProps)} result={mockResult} isNotStored />
102111
)
103112

104113
expect(screen.queryByTestId('query-cli-warning')).toBeInTheDocument()
105114
})
115+
116+
it('Result element should render (nil) result', () => {
117+
const mockResult = [{
118+
response: '',
119+
status: CommandExecutionStatus.Success
120+
}]
121+
122+
const { queryByTestId } = render(
123+
<QueryCardCliResultWrapper {...instance(mockedProps)} result={mockResult} />
124+
)
125+
126+
const resultEl = queryByTestId('query-cli-card-result')
127+
128+
expect(resultEl).toHaveTextContent('(nil)')
129+
})
106130
})

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { isArray } from 'lodash'
55

66
import { CommandExecutionResult } from 'uiSrc/slices/interfaces'
77
import { ResultsMode } from 'uiSrc/slices/interfaces/workbench'
8-
import { Maybe } from 'uiSrc/utils'
8+
import { formatToText, Maybe } from 'uiSrc/utils'
99

1010
import QueryCardCliDefaultResult from '../QueryCardCliDefaultResult'
1111
import QueryCardCliGroupResult from '../QueryCardCliGroupResult'
@@ -18,10 +18,11 @@ export interface Props {
1818
status?: string
1919
resultsMode?: ResultsMode
2020
isNotStored?: boolean
21+
isFullScreen?: boolean
2122
}
2223

2324
const QueryCardCliResultWrapper = (props: Props) => {
24-
const { result = [], query, loading, resultsMode, isNotStored } = props
25+
const { result = [], query, loading, resultsMode, isNotStored, isFullScreen } = props
2526

2627
return (
2728
<div className={cx('queryResultsContainer', styles.container)}>
@@ -34,8 +35,13 @@ const QueryCardCliResultWrapper = (props: Props) => {
3435
</EuiText>
3536
)}
3637
{resultsMode === ResultsMode.GroupMode && isArray(result[0]?.response)
37-
? <QueryCardCliGroupResult result={result} />
38-
: <QueryCardCliDefaultResult query={query} result={result} />}
38+
? <QueryCardCliGroupResult result={result} isFullScreen={isFullScreen} />
39+
: (
40+
<QueryCardCliDefaultResult
41+
isFullScreen={isFullScreen}
42+
items={formatToText(result[0].response || '(nil)', query).split('\n')}
43+
/>
44+
)}
3945
</div>
4046
)}
4147
{loading && (

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
overflow: auto;
1010
white-space: pre-wrap;
1111
word-break: break-all;
12+
position: relative;
1213

1314
font: normal normal normal 14px/17px Inconsolata;
1415
text-align: left;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import React from 'react'
2+
import { instance, mock } from 'ts-mockito'
3+
import { render } from 'uiSrc/utils/test-utils'
4+
import VirtualGrid, { Props } from './VirtualList'
5+
6+
const mockedProps = mock<Props>()
7+
8+
describe('VirtualList', () => {
9+
it('should render', () => {
10+
expect(
11+
render(<VirtualGrid {...instance(mockedProps)} />)
12+
).toBeTruthy()
13+
})
14+
it('should render with empty rows', () => {
15+
expect(
16+
render(<VirtualGrid {...instance(mockedProps)} items={[]} />)
17+
).toBeTruthy()
18+
})
19+
})
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import React, { useEffect, useRef } from 'react'
2+
import { ListChildComponentProps, VariableSizeList as List } from 'react-window'
3+
import AutoSizer from 'react-virtualized-auto-sizer'
4+
5+
import styles from './styles.module.scss'
6+
7+
export interface Props {
8+
items: (string | JSX.Element)[]
9+
overscanCount?: number
10+
minRowHeight?: number
11+
}
12+
13+
const PROTRUDING_OFFSET = 2
14+
const MIN_ROW_HEIGHT = 18
15+
const OVERSCAN_COUNT = 20
16+
17+
const VirtualList = (props: Props) => {
18+
const {
19+
items = [],
20+
overscanCount = OVERSCAN_COUNT,
21+
minRowHeight = MIN_ROW_HEIGHT,
22+
} = props
23+
24+
const listRef = useRef<List>(null)
25+
const rowHeights = useRef<{ [key: number]: number }>({})
26+
const outerRef = useRef<HTMLDivElement>(null)
27+
28+
const getRowHeight = (index: number) => (
29+
rowHeights.current[index] > minRowHeight ? (rowHeights.current[index] + 2) : minRowHeight
30+
)
31+
32+
const setRowHeight = (index: number, size: number) => {
33+
listRef.current?.resetAfterIndex(0)
34+
rowHeights.current = { ...rowHeights.current, [index]: size }
35+
}
36+
37+
const Row = ({ index, style }: ListChildComponentProps) => {
38+
const rowRef = useRef<HTMLDivElement>(null)
39+
40+
useEffect(() => {
41+
if (rowRef.current) {
42+
setRowHeight(index, rowRef.current?.clientHeight)
43+
}
44+
}, [rowRef])
45+
46+
const rowContent = items[index]
47+
48+
return (
49+
<div style={style} className={styles.item} data-testid={`row-${index}`}>
50+
<div className={styles.message} ref={rowRef}>{rowContent}</div>
51+
</div>
52+
)
53+
}
54+
55+
return (
56+
<AutoSizer>
57+
{({ width, height }) => (
58+
<List
59+
itemCount={items.length}
60+
itemSize={getRowHeight}
61+
ref={listRef}
62+
className={styles.listContent}
63+
outerRef={outerRef}
64+
overscanCount={overscanCount}
65+
height={height}
66+
width={width - PROTRUDING_OFFSET}
67+
>
68+
{Row}
69+
</List>
70+
)}
71+
</AutoSizer>
72+
)
73+
}
74+
75+
export default VirtualList

0 commit comments

Comments
 (0)