Skip to content

Commit e51cbb7

Browse files
authored
RI-7705: redesign RDI table (#5184)
* RI-7705: add redis-ui RdiInstancesList * RI-7705: remove RdiInstancesListWrapper * RI-7705: add columns filter for rdi * RI-7705: address comments
1 parent 23ada90 commit e51cbb7

33 files changed

+1429
-792
lines changed
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import React from 'react'
2+
import { cleanup, render, screen, fireEvent } from 'uiSrc/utils/test-utils'
3+
import ColumnsConfigPopover from 'uiSrc/components/columns-config/ColumnsConfigPopover'
4+
5+
// Simple test columns enum/union
6+
enum TestCol {
7+
A = 'a',
8+
B = 'b',
9+
C = 'c',
10+
}
11+
12+
const columnsMap = new Map<TestCol, string>([
13+
[TestCol.A, 'Column A'],
14+
[TestCol.B, 'Column B'],
15+
[TestCol.C, 'Column C'],
16+
])
17+
18+
describe('ColumnsConfigPopover', () => {
19+
beforeEach(() => cleanup())
20+
21+
const openPopover = async () => {
22+
fireEvent.click(screen.getByTestId('btn-columns-config'))
23+
const popover = await screen.findByTestId('columns-config-popover')
24+
expect(popover).toBeInTheDocument()
25+
return popover
26+
}
27+
28+
it('renders button and shows checkboxes with correct checked state', async () => {
29+
render(
30+
<ColumnsConfigPopover<TestCol>
31+
columnsMap={columnsMap}
32+
shownColumns={[TestCol.A, TestCol.B]}
33+
onChange={jest.fn()}
34+
/>,
35+
)
36+
37+
// Default label and button test id
38+
expect(screen.getByTestId('btn-columns-config')).toBeInTheDocument()
39+
expect(screen.getByText('Columns')).toBeInTheDocument()
40+
41+
await openPopover()
42+
43+
// Checked
44+
expect(screen.getByTestId('show-a')).toBeChecked()
45+
expect(screen.getByTestId('show-b')).toBeChecked()
46+
// Not checked
47+
expect(screen.getByTestId('show-c')).not.toBeChecked()
48+
})
49+
50+
it('calls onChange with hidden diff when unchecking a checked column', async () => {
51+
const onChange = jest.fn()
52+
53+
render(
54+
<ColumnsConfigPopover<TestCol>
55+
columnsMap={columnsMap}
56+
shownColumns={[TestCol.A, TestCol.B]}
57+
onChange={onChange}
58+
/>,
59+
)
60+
61+
await openPopover()
62+
63+
fireEvent.click(screen.getByTestId('show-a'))
64+
65+
expect(onChange).toHaveBeenCalledTimes(1)
66+
expect(onChange).toHaveBeenCalledWith([TestCol.B], {
67+
shown: [],
68+
hidden: [TestCol.A],
69+
})
70+
})
71+
72+
it('calls onChange with shown diff when checking a hidden column', async () => {
73+
const onChange = jest.fn()
74+
75+
render(
76+
<ColumnsConfigPopover<TestCol>
77+
columnsMap={columnsMap}
78+
shownColumns={[TestCol.A]}
79+
onChange={onChange}
80+
/>,
81+
)
82+
83+
await openPopover()
84+
85+
fireEvent.click(screen.getByTestId('show-b'))
86+
87+
expect(onChange).toHaveBeenCalledTimes(1)
88+
expect(onChange).toHaveBeenCalledWith([TestCol.A, TestCol.B], {
89+
shown: [TestCol.B],
90+
hidden: [],
91+
})
92+
})
93+
94+
it('prevents hiding the last remaining column (disabled and no onChange)', async () => {
95+
const onChange = jest.fn()
96+
97+
render(
98+
<ColumnsConfigPopover<TestCol>
99+
columnsMap={columnsMap}
100+
shownColumns={[TestCol.A]}
101+
onChange={onChange}
102+
/>,
103+
)
104+
105+
await openPopover()
106+
107+
const lastCheckbox = screen.getByTestId('show-a') as HTMLInputElement
108+
expect(lastCheckbox).toBeDisabled()
109+
110+
fireEvent.click(lastCheckbox)
111+
expect(onChange).not.toHaveBeenCalled()
112+
})
113+
})
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import React, { useState } from 'react'
2+
3+
import { RiPopover } from 'uiSrc/components/base'
4+
import { EmptyButton } from 'uiSrc/components/base/forms/buttons'
5+
import { ColumnsIcon } from 'uiSrc/components/base/icons'
6+
import { Checkbox } from 'uiSrc/components/base/forms/checkbox/Checkbox'
7+
import { Col } from 'uiSrc/components/base/layout/flex'
8+
9+
interface ColumnsConfigPopoverProps<T extends string = string> {
10+
columnsMap: Map<T, string>
11+
shownColumns: T[]
12+
onChange: (nextShownColumns: T[], diff: { shown: T[]; hidden: T[] }) => void
13+
buttonLabel?: React.ReactNode
14+
buttonTestId?: string
15+
popoverTestId?: string
16+
}
17+
18+
function ColumnsConfigPopover<T extends string = string>({
19+
columnsMap,
20+
shownColumns,
21+
onChange,
22+
buttonLabel = 'Columns',
23+
buttonTestId = 'btn-columns-config',
24+
popoverTestId = 'columns-config-popover',
25+
}: ColumnsConfigPopoverProps<T>) {
26+
const [isOpen, setIsOpen] = useState(false)
27+
28+
const toggle = () => setIsOpen((v) => !v)
29+
30+
const handleToggle = (checked: boolean, col: T) => {
31+
// prevent hiding the last remaining column
32+
if (!checked && shownColumns.length === 1 && shownColumns.includes(col)) {
33+
return
34+
}
35+
36+
const next = checked
37+
? [...shownColumns, col]
38+
: shownColumns.filter((c) => c !== col)
39+
40+
onChange(next, {
41+
shown: checked ? [col] : ([] as T[]),
42+
hidden: checked ? ([] as T[]) : [col],
43+
})
44+
}
45+
46+
return (
47+
<RiPopover
48+
ownFocus={false}
49+
anchorPosition="downLeft"
50+
isOpen={isOpen}
51+
closePopover={() => setIsOpen(false)}
52+
data-testid={popoverTestId}
53+
button={
54+
<EmptyButton
55+
icon={ColumnsIcon}
56+
onClick={toggle}
57+
data-testid={buttonTestId}
58+
aria-label="columns"
59+
>
60+
{buttonLabel}
61+
</EmptyButton>
62+
}
63+
>
64+
<Col gap="m">
65+
{Array.from(columnsMap.entries()).map(([field, name]) => (
66+
<Checkbox
67+
key={`show-${field}`}
68+
id={`show-${field}`}
69+
name={`show-${field}`}
70+
label={name}
71+
checked={shownColumns.includes(field)}
72+
disabled={shownColumns.includes(field) && shownColumns.length === 1}
73+
onChange={(e) => handleToggle(e.target.checked, field)}
74+
data-testid={`show-${field}`}
75+
/>
76+
))}
77+
</Col>
78+
</RiPopover>
79+
)
80+
}
81+
82+
export default ColumnsConfigPopover

redisinsight/ui/src/constants/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,5 @@ export * from './tutorials'
3535
export * from './datetime'
3636
export * from './sorting'
3737
export * from './databaseList'
38+
export * from './rdiList'
3839
export { ApiEndpoints, BrowserStorageItem, ApiStatusCode, apiErrors }
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export enum RdiListColumn {
2+
Name = 'name',
3+
Url = 'url',
4+
Version = 'version',
5+
LastConnection = 'lastConnection',
6+
Controls = 'controls',
7+
}
8+
9+
export const RDI_COLUMN_FIELD_NAME_MAP = new Map<RdiListColumn, string>([
10+
[RdiListColumn.Name, 'RDI alias'],
11+
[RdiListColumn.Url, 'URL'],
12+
[RdiListColumn.Version, 'RDI version'],
13+
[RdiListColumn.LastConnection, 'Last connection'],
14+
[RdiListColumn.Controls, 'Controls'],
15+
])
16+
17+
export const DEFAULT_RDI_SHOWN_COLUMNS = [
18+
RdiListColumn.Name,
19+
RdiListColumn.Url,
20+
RdiListColumn.Version,
21+
RdiListColumn.LastConnection,
22+
RdiListColumn.Controls,
23+
]

redisinsight/ui/src/pages/home/components/database-list-header/DatabaseListHeader.tsx

Lines changed: 16 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { OAuthSocialAction, OAuthSocialSource } from 'uiSrc/slices/interfaces'
1212
import PromoLink from 'uiSrc/components/promo-link/PromoLink'
1313

1414
import { FeatureFlagComponent, OAuthSsoHandlerDialog } from 'uiSrc/components'
15-
import { RiPopover } from 'uiSrc/components/base'
1615
import { getPathToResource } from 'uiSrc/services/resourcesService'
1716
import { ContentCreateRedis } from 'uiSrc/slices/interfaces/content'
1817
import { CREATE_CLOUD_DB_ID, HELP_LINKS } from 'uiSrc/pages/home/constants'
@@ -31,8 +30,8 @@ import {
3130
PrimaryButton,
3231
SecondaryButton,
3332
} from 'uiSrc/components/base/forms/buttons'
34-
import { ColumnsIcon, PlusIcon } from 'uiSrc/components/base/icons'
35-
import { Checkbox } from 'uiSrc/components/base/forms/checkbox/Checkbox'
33+
import { PlusIcon } from 'uiSrc/components/base/icons'
34+
import ColumnsConfigPopover from 'uiSrc/components/columns-config/ColumnsConfigPopover'
3635
import handleClickFreeCloudDb from '../database-list-component/methods/handleClickFreeCloudDb'
3736
import SearchDatabasesList from '../search-databases-list'
3837

@@ -48,7 +47,6 @@ const DatabaseListHeader = ({ onAddInstance }: Props) => {
4847
const { loading, data } = useSelector(contentSelector)
4948

5049
const [promoData, setPromoData] = useState<ContentCreateRedis>()
51-
const [columnsConfigShown, setColumnsConfigShown] = useState(false)
5250

5351
const { theme } = useContext(ThemeContext)
5452
const { [FeatureFlags.enhancedCloudUI]: enhancedCloudUIFeature } =
@@ -95,31 +93,14 @@ const DatabaseListHeader = ({ onAddInstance }: Props) => {
9593
handleClickLink(event, eventData)
9694
}
9795

98-
const toggleColumnsConfigVisibility = () =>
99-
setColumnsConfigShown(!columnsConfigShown)
100-
101-
const changeShownColumns = (status: boolean, column: DatabaseListColumn) => {
102-
const newColumns = status
103-
? [...shownColumns, column]
104-
: shownColumns.filter((col) => col !== column)
105-
106-
dispatch(setShownColumns(newColumns))
107-
108-
const shown: DatabaseListColumn[] = []
109-
const hidden: DatabaseListColumn[] = []
110-
111-
if (status) {
112-
shown.push(column)
113-
} else {
114-
hidden.push(column)
115-
}
116-
96+
const handleColumnsChange = (
97+
next: DatabaseListColumn[],
98+
diff: { shown: DatabaseListColumn[]; hidden: DatabaseListColumn[] },
99+
) => {
100+
dispatch(setShownColumns(next))
117101
sendEventTelemetry({
118102
event: TelemetryEvent.DATABASE_LIST_COLUMNS_CLICKED,
119-
eventData: {
120-
shown,
121-
hidden,
122-
},
103+
eventData: diff,
123104
})
124105
}
125106

@@ -185,21 +166,6 @@ const DatabaseListHeader = ({ onAddInstance }: Props) => {
185166
)
186167
}
187168

188-
const columnCheckboxes = Array.from(COLUMN_FIELD_NAME_MAP.entries()).map(
189-
([field, name]) => (
190-
<Checkbox
191-
key={`show-${field}`}
192-
id={`show-${field}`}
193-
name={`show-${field}`}
194-
label={name}
195-
checked={shownColumns.includes(field)}
196-
disabled={shownColumns.includes(field) && shownColumns.length === 1}
197-
onChange={(e) => changeShownColumns(e.target.checked, field)}
198-
data-testid={`show-${field}`}
199-
/>
200-
),
201-
)
202-
203169
return (
204170
<div className={styles.containerDl}>
205171
<Row
@@ -226,43 +192,14 @@ const DatabaseListHeader = ({ onAddInstance }: Props) => {
226192
</FlexItem>
227193
)}
228194
{instances.length > 0 && (
229-
<FlexItem grow>
230-
<Row justify="end" align="center" gap="s">
231-
<FlexItem className={styles.columnsButtonItem}>
232-
<RiPopover
233-
ownFocus={false}
234-
anchorPosition="downLeft"
235-
isOpen={columnsConfigShown}
236-
closePopover={() => setColumnsConfigShown(false)}
237-
data-testid="columns-config-popover"
238-
button={
239-
<SecondaryButton
240-
icon={ColumnsIcon}
241-
onClick={toggleColumnsConfigVisibility}
242-
className={styles.columnsButton}
243-
data-testid="btn-columns-config"
244-
aria-label="columns"
245-
>
246-
<span>Columns</span>
247-
</SecondaryButton>
248-
}
249-
>
250-
<div
251-
style={{
252-
display: 'flex',
253-
flexDirection: 'column',
254-
gap: '0.5rem',
255-
}}
256-
>
257-
{columnCheckboxes}
258-
</div>
259-
</RiPopover>
260-
</FlexItem>
261-
<FlexItem>
262-
<SearchDatabasesList />
263-
</FlexItem>
264-
</Row>
265-
</FlexItem>
195+
<Row justify="end" align="center" gap="l">
196+
<ColumnsConfigPopover
197+
columnsMap={COLUMN_FIELD_NAME_MAP}
198+
shownColumns={shownColumns}
199+
onChange={handleColumnsChange}
200+
/>
201+
<SearchDatabasesList />
202+
</Row>
266203
)}
267204
</Row>
268205
<Spacer className={styles.spacerDl} />

redisinsight/ui/src/pages/instance/InstancePage.spec.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ import { loadInstances as loadRdiInstances } from 'uiSrc/slices/rdi/instances'
5050

5151
import { clearExpertChatHistory } from 'uiSrc/slices/panels/aiAssistant'
5252
import { getAllPlugins } from 'uiSrc/slices/app/plugins'
53-
import { FeatureFlags } from 'uiSrc/constants'
53+
import { DEFAULT_RDI_SHOWN_COLUMNS, FeatureFlags } from 'uiSrc/constants'
5454
import { getDatabasesApiSpy } from 'uiSrc/mocks/handlers/instances/instancesHandlers'
5555
import { RdiInstance } from 'uiSrc/slices/interfaces'
5656
import InstancePage, { Props } from './InstancePage'
@@ -316,7 +316,7 @@ describe('InstancePage', () => {
316316
loadingChanging: false,
317317
errorChanging: '',
318318
changedSuccessfully: false,
319-
isPipelineLoaded: false,
319+
shownColumns: DEFAULT_RDI_SHOWN_COLUMNS,
320320
})
321321
const mockFetchInstancesAction = jest.fn()
322322
jest

0 commit comments

Comments
 (0)