Skip to content
Merged
3 changes: 3 additions & 0 deletions redisinsight/ui/src/assets/img/icons/filter.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions redisinsight/ui/src/constants/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ enum ApiEndpoints {
DATABASES_TEST_CONNECTION = 'databases/test',
DATABASES_EXPORT = 'databases/export',

TAGS = 'tags',

BULK_ACTIONS_IMPORT = 'bulk-actions/import',
BULK_ACTIONS_IMPORT_DEFAULT_DATA = 'bulk-actions/import/default-data',
BULK_ACTIONS_IMPORT_TUTORIAL_DATA = 'bulk-actions/import/tutorial-data',
Expand Down
2 changes: 2 additions & 0 deletions redisinsight/ui/src/pages/home/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
resetImportInstances,
setEditedInstance
} from 'uiSrc/slices/instances/instances'
import { fetchTags } from 'uiSrc/slices/instances/tags'
import { localStorageService } from 'uiSrc/services'
import { resetDataSentinel, sentinelSelector } from 'uiSrc/slices/instances/sentinel'
import {
Expand Down Expand Up @@ -86,6 +87,7 @@ const HomePage = () => {
dispatch(resetInstancesRedisCluster())
dispatch(resetSubscriptionsRedisCloud())
dispatch(fetchCreateRedisButtonsAction())
dispatch(fetchTags())

return (() => {
dispatch(setEditedInstance(null))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,11 @@ import { appFeatureFlagsFeaturesSelector } from 'uiSrc/slices/app/features'
import { getUtmExternalLink } from 'uiSrc/utils/links'
import { CREATE_CLOUD_DB_ID, HELP_LINKS } from 'uiSrc/pages/home/constants'

import { Tag } from 'uiSrc/slices/interfaces/tag'
import DbStatus from '../db-status'

import { TagsCell } from '../tags-cell/TagsCell'
import { TagsCellHeader } from '../tags-cell/TagsCellHeader'
import styles from './styles.module.scss'

export interface Props {
Expand Down Expand Up @@ -280,7 +283,7 @@ const DatabasesListWrapper = (props: Props) => {
if (isCreateCloudDb(id)) return sortingRef.current.direction === 'asc' ? '' : false
return name?.toLowerCase()
},
width: '30%',
width: '200%',
render: function InstanceCell(name: string = '', instance: Instance) {
if (isCreateCloudDb(instance.id)) {
return (
Expand Down Expand Up @@ -326,7 +329,7 @@ const DatabasesListWrapper = (props: Props) => {
field: 'host',
className: 'column_host',
name: 'Host:Port',
width: '35%',
width: '200%',
dataType: 'string',
truncateText: true,
sortable: ({ host, port, id }) => {
Expand Down Expand Up @@ -361,7 +364,7 @@ const DatabasesListWrapper = (props: Props) => {
if (isCreateCloudDb(id)) return sortingRef.current.direction === 'asc' ? '' : false
return connectionType
},
width: '180px',
width: '150%',
truncateText: true,
hideForMobile: true,
render: (cellData: ConnectionType) => CONNECTION_TYPE_DISPLAY[cellData] || capitalize(cellData)
Expand All @@ -370,7 +373,7 @@ const DatabasesListWrapper = (props: Props) => {
field: 'modules',
className: styles.columnModules,
name: 'Capabilities',
width: '30%',
width: '100%',
dataType: 'string',
render: (_cellData, { modules = [], isRediStack }: Instance) => (
<div style={{ width: '100%', height: '100%', position: 'relative' }}>
Expand Down Expand Up @@ -415,7 +418,7 @@ const DatabasesListWrapper = (props: Props) => {
name: 'Last connection',
dataType: 'date',
align: 'right',
width: '170px',
width: '140%',
sortable: ({ lastConnection, id }) => {
if (isCreateCloudDb(id)) return sortingRef.current.direction === 'asc' ? -Infinity : +Infinity
return (lastConnection ? -new Date(`${lastConnection}`) : -Infinity)
Expand All @@ -425,10 +428,24 @@ const DatabasesListWrapper = (props: Props) => {
return lastConnectionFormat(date)
},
},
{
field: 'tags',
dataType: 'auto',
name: <TagsCellHeader />,
width: '150%',
sortable: ({ tags, id }) => {
if (isCreateCloudDb(id)) return sortingRef.current.direction === 'asc' ? '' : false
return tags?.[0] ? `${tags[0].key}:${tags[0].value}` : undefined
},
render: (tags: Tag[], { id }) => {
if (isCreateCloudDb(id) || !tags) return null
return <TagsCell tags={tags} />
},
},
{
field: 'controls',
className: 'column_controls',
width: '120px',
width: '60%',
name: '',
render: function Actions(_act: any, instance: Instance) {
if (isCreateCloudDb(instance?.id)) return null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ const instancesMock: Instance[] = [{
visible: true,
modules: [],
lastConnection: new Date(),
tags: [{
id: '1',
key: 'env',
value: 'prod',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}],
version: ''
}, {
id: '2',
Expand All @@ -30,6 +37,7 @@ const instancesMock: Instance[] = [{
visible: true,
modules: [],
lastConnection: new Date(),
tags: [],
version: ''
}]

Expand All @@ -46,6 +54,11 @@ beforeEach(() => {
...state.connections,
instances: {
...state.connections.instances,
data: instancesMock
},
tags: {
...state.connections.tags,
selectedTags: new Set(['env:prod'])
}
}
}))
Expand Down Expand Up @@ -87,4 +100,16 @@ describe('SearchDatabasesList', () => {
const expectedActions = [loadInstancesSuccess(newInstancesMock)]
expect(storeMock.getActions()).toEqual(expectedActions)
})

it('should call loadInstancesSuccess after selected tags state changes', async () => {
const newInstancesMock = [
{ ...instancesMock[0], visible: true },
{ ...instancesMock[1], visible: false }
]

render(<SearchDatabasesList />)

const expectedActions = [loadInstancesSuccess(newInstancesMock)]
expect(storeMock.getActions()).toEqual(expectedActions)
})
})
Original file line number Diff line number Diff line change
@@ -1,36 +1,43 @@
import React from 'react'
import React, { useState, useEffect } from 'react'
import { EuiFieldSearch } from '@elastic/eui'
import { useDispatch, useSelector } from 'react-redux'

import { instancesSelector, loadInstancesSuccess } from 'uiSrc/slices/instances/instances'
import { CONNECTION_TYPE_DISPLAY, Instance } from 'uiSrc/slices/interfaces'
import { tagsSelector } from 'uiSrc/slices/instances/tags'
import { lastConnectionFormat } from 'uiSrc/utils'
import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry'
import styles from './styles.module.scss'

export interface Props {
onAddInstance: () => void
direction: 'column' | 'row'
welcomePage?: boolean
}
export const instanceHasTags = (instance: Instance, selectedTags: Set<string>) =>
selectedTags.size === 0 || instance.tags?.some((tag) =>
selectedTags.has(`${tag.key}:${tag.value}`))

const SearchDatabasesList = () => {
const [ value, setValue ] = useState('')
const { data: instances } = useSelector(instancesSelector)
const { selectedTags } = useSelector(tagsSelector)

const dispatch = useDispatch()

const onQueryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e?.target?.value?.toLowerCase()
useEffect(() => {
const isInitialRender = value === '' && selectedTags.size === 0 && !instances.some(({ visible }) => visible === false )

if (isInitialRender) {
return
}

const itemsTemp = instances.map(
(item: Instance) => ({
...item,
visible: item.name?.toLowerCase().indexOf(value) !== -1
visible: instanceHasTags(item, selectedTags) && (item.name?.toLowerCase().indexOf(value) !== -1
|| item.host?.toString()?.indexOf(value) !== -1
|| item.port?.toString()?.indexOf(value) !== -1
|| (item.connectionType && CONNECTION_TYPE_DISPLAY[item.connectionType]?.toLowerCase()?.indexOf(value) !== -1)
|| item.modules?.map((m) => m.name?.toLowerCase()).join(',').indexOf(value) !== -1
|| lastConnectionFormat(item.lastConnection)?.indexOf(value) !== -1
|| item.tags?.some((tag) => `${tag.key.toLowerCase()}:${tag.value.toLowerCase()}`.indexOf(value) !== -1))
|| false // force boolean type
})
)

Expand All @@ -43,14 +50,15 @@ const SearchDatabasesList = () => {
})

dispatch(loadInstancesSuccess(itemsTemp))
}
}, [value, selectedTags])

return (
<EuiFieldSearch
isClearable
placeholder="Database List Search"
className={styles.search}
onChange={onQueryChange}
onChange={(e) => setValue(e.target.value.toLowerCase())}
value={value}
aria-label="Search database list"
data-testid="search-database-list"
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react'
import { render, screen } from 'uiSrc/utils/test-utils'
import { Tag } from 'uiSrc/slices/interfaces/tag'
import { TagsCell } from './TagsCell'

const tags: Tag[] = [
{
id: '1',
key: 'env',
value: 'prod',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
{
id: '2',
key: 'version',
value: '1.0',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
]

describe('TagsCell', () => {
it('should render the first tag and the count of remaining tags', () => {
render(<TagsCell tags={tags} />)
expect(screen.getByText('env : prod')).toBeInTheDocument()
expect(screen.getByText('+1')).toBeInTheDocument()
})

it('should render null if no tags are provided', () => {
const { container } = render(<TagsCell tags={[]} />)
expect(container.firstChild).toBeNull()
})
})
45 changes: 45 additions & 0 deletions redisinsight/ui/src/pages/home/components/tags-cell/TagsCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/* eslint-disable arrow-body-style */
import { EuiBadge, EuiToolTip } from '@elastic/eui'
import React from 'react'

import { Tag } from 'uiSrc/slices/interfaces/tag'
import styles from './styles.module.scss'

type TagsCellProps = {
tags: Tag[]
}

export const TagsCell = ({ tags }: TagsCellProps) => {
if (!tags[0]) {
return null
}

const firstTagText = `${tags[0].key} : ${tags[0].value}`
const remainingTagsCount = tags.length - 1

return (
<div className={styles.tagsCell}>
<EuiBadge className={`${styles.tagBadge} ${styles.tagBadgeOverflow}`}>
{firstTagText}
</EuiBadge>
{remainingTagsCount > 0 && (
<EuiToolTip
position="top"
content={
<div>
{tags.slice(1).map((tag) => (
<div key={tag.id}>
{tag.key} : {tag.value}
</div>
))}
</div>
}
>
<EuiBadge className={styles.tagBadge} title={undefined}>
+{remainingTagsCount}
</EuiBadge>
</EuiToolTip>
)}
</div>
)
}
Loading