-
Notifications
You must be signed in to change notification settings - Fork 59
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* GUI Components: add List component (#3780) * GUI Components: List component styling, add ListHeader (#3780)
- Loading branch information
1 parent
a550313
commit 4e33c6f
Showing
8 changed files
with
366 additions
and
4 deletions.
There are no files selected for viewing
65 changes: 65 additions & 0 deletions
65
portals-ui/packages/components/lib/components/list/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import type { Key, ReactNode } from 'react'; | ||
import React, { useState } from 'react'; | ||
import type { VirtualListState } from '@epam/uui-core'; | ||
import classNames from 'classnames'; | ||
import { VirtualList } from '@epam/uui'; | ||
import type { ListProps } from './types'; | ||
import ListHeader from './list-header'; | ||
|
||
const MIN_VISIBLE_COUNT = 20; | ||
|
||
export default function List<Item>(props: ListProps<Item>): ReactNode { | ||
const { | ||
header, | ||
footer, | ||
data, | ||
renderItem, | ||
virtualized = false, | ||
style, | ||
className, | ||
itemKey, | ||
} = props; | ||
const [listState, setListState] = useState<VirtualListState>({ | ||
topIndex: 0, | ||
visibleCount: MIN_VISIBLE_COUNT, | ||
}); | ||
const visibleData = data.slice( | ||
listState.topIndex, | ||
(listState.topIndex ?? 0) + (listState.visibleCount ?? MIN_VISIBLE_COUNT), | ||
); | ||
const rows = visibleData.map((item, index) => ( | ||
<React.Fragment key={index}>{renderItem(item, index)}</React.Fragment> | ||
)); | ||
const listComponent = virtualized ? ( | ||
<VirtualList | ||
cx="max-h-full" | ||
rows={rows} | ||
value={listState} | ||
onValueChange={setListState} | ||
rowsCount={data.length} | ||
/> | ||
) : ( | ||
<div className="overflow-y-auto"> | ||
{data.map((item, index) => { | ||
let key: Key = `key_${index}`; | ||
if (itemKey && typeof itemKey !== 'symbol') { | ||
key = typeof itemKey === 'function' ? itemKey(item, index) : itemKey; | ||
} | ||
return ( | ||
<React.Fragment key={key}>{renderItem(item, index)}</React.Fragment> | ||
); | ||
})} | ||
</div> | ||
); | ||
return ( | ||
<div | ||
style={style} | ||
className={classNames('overflow-hidden flex flex-col', className)}> | ||
{header ?? null} | ||
{listComponent} | ||
{footer ?? null} | ||
</div> | ||
); | ||
} | ||
|
||
export { ListHeader }; |
37 changes: 37 additions & 0 deletions
37
portals-ui/packages/components/lib/components/list/list-header.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import classNames from 'classnames'; | ||
import { SearchInput } from '@epam/uui'; | ||
import type { ListHeaderProps } from './types'; | ||
|
||
const ListHeader = (props: ListHeaderProps) => { | ||
const { | ||
className, | ||
style, | ||
title, | ||
controls, | ||
search, | ||
onSearch, | ||
searchPlaceholder, | ||
} = props; | ||
return ( | ||
<div className={classNames(className, 'divide-y')} style={style}> | ||
<b | ||
className="flex no-wrap px-6 py-4" | ||
style={{ color: 'var(--uui-text-secondary)' }}> | ||
{title} {controls ? <div className="ml-auto">{controls}</div> : null} | ||
</b> | ||
{onSearch ? ( | ||
<div className="px-6 py-2"> | ||
<SearchInput | ||
value={search} | ||
onValueChange={onSearch} | ||
placeholder={searchPlaceholder ?? 'Search'} | ||
debounceDelay={300} | ||
size="30" | ||
/> | ||
</div> | ||
) : null} | ||
</div> | ||
); | ||
}; | ||
|
||
export default ListHeader; |
20 changes: 20 additions & 0 deletions
20
portals-ui/packages/components/lib/components/list/types.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import type { ReactNode } from 'react'; | ||
import type { CommonProps } from '../common.types'; | ||
|
||
export type ListProps<Item> = CommonProps & { | ||
data: Item[]; | ||
renderItem: (item: Item, index: number) => ReactNode | string; | ||
header?: ReactNode; | ||
footer?: ReactNode; | ||
virtualized?: boolean; | ||
itemKey?: keyof Item | ((item: Item, index: number) => string | number); | ||
}; | ||
|
||
export type ListHeaderProps = CommonProps & { | ||
title: string | ReactNode; | ||
logo?: string; | ||
search?: string; | ||
searchPlaceholder?: string; | ||
onSearch?: (search: string) => void; | ||
controls?: ReactNode; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,8 @@ | ||
import DummyComponent from './components/dummy-component'; | ||
import List, { ListHeader } from './components/list'; | ||
import '@epam/uui-components/styles.css'; | ||
import '@epam/uui/styles.css'; | ||
import './style.css'; | ||
|
||
export { DummyComponent }; | ||
export { DummyComponent, List, ListHeader }; | ||
export * from './components/common.types'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,117 @@ | ||
import { DummyComponent } from '@cloud-pipeline/components'; | ||
import { useEffect, useMemo, useState } from 'react'; | ||
import type { Project } from '@cloud-pipeline/core'; | ||
import { Button, LinkButton } from '@epam/uui'; | ||
import { List, ListHeader } from '@cloud-pipeline/components'; | ||
import { useProjectsState } from '../../state/projects/hooks'; | ||
import { loadProjects } from '../../state/projects/load-projects'; | ||
import HighlightedText from '../../shared/highlight-text'; | ||
import './style.css'; | ||
|
||
export const Home = () => { | ||
const { projects } = useProjectsState(); | ||
const [projectSearch, setProjectSearch] = useState(''); | ||
const [pipelinesSearch, setPipelinesSearch] = useState(''); | ||
useEffect(() => { | ||
loadProjects() | ||
.then(() => {}) | ||
.catch(() => {}); | ||
}, []); | ||
const filteredProjects = useMemo(() => { | ||
if (!projects) { | ||
return []; | ||
} | ||
return projectSearch | ||
? projects.filter((project) => | ||
project.name.toLowerCase().includes(projectSearch.toLowerCase()), | ||
) | ||
: projects; | ||
}, [projectSearch, projects]); | ||
if (!projects) { | ||
return null; | ||
} | ||
return ( | ||
<div> | ||
<DummyComponent /> | ||
<div className="flex h-full gap-5 overflow-hidden flex-nowrap justify-around p-2"> | ||
<List | ||
className="list-container" | ||
header={ | ||
<ListHeader | ||
className="list-header-container" | ||
title="Projects" | ||
controls={ | ||
<Button caption="Add project" size="24" onClick={() => null} /> | ||
} | ||
search={projectSearch} | ||
onSearch={setProjectSearch} | ||
/> | ||
} | ||
footer={ | ||
<div className="list-footer-container"> | ||
<LinkButton | ||
caption="View all projects" | ||
link={{ pathname: '/projects' }} | ||
/> | ||
</div> | ||
} | ||
data={filteredProjects} | ||
itemKey={(item: Project) => item.id} | ||
virtualized | ||
renderItem={(item: Project) => ( | ||
<div className="p-2" style={{ height: 100 }}> | ||
<HighlightedText search={projectSearch}> | ||
{item.name} | ||
</HighlightedText> | ||
</div> | ||
)} | ||
style={{ flex: 1 }} | ||
/> | ||
<List | ||
className="list-container" | ||
header={ | ||
<ListHeader | ||
className="list-header-container" | ||
title="Pipelines" | ||
search={pipelinesSearch} | ||
onSearch={setPipelinesSearch} | ||
/> | ||
} | ||
footer={ | ||
<div className="list-footer-container"> | ||
<LinkButton | ||
caption="View all pipelines" | ||
link={{ pathname: '/pipelines' }} | ||
/> | ||
</div> | ||
} | ||
data={projects} | ||
virtualized | ||
itemKey={(item: Project) => item.id} | ||
renderItem={(item: Project) => ( | ||
<div className="p-2" style={{ height: 100 }}> | ||
{item.name} | ||
</div> | ||
)} | ||
style={{ flex: 1 }} | ||
/> | ||
<List | ||
className="list-container" | ||
header={ | ||
<ListHeader className="list-header-container" title="Run History" /> | ||
} | ||
footer={ | ||
<div className="list-footer-container"> | ||
<LinkButton caption="View all runs" link={{ pathname: '/runs' }} /> | ||
</div> | ||
} | ||
data={projects} | ||
virtualized | ||
itemKey={(item: Project) => item.id} | ||
renderItem={(item: Project) => ( | ||
<div className="p-2" style={{ height: 300 }}> | ||
{item.name} | ||
</div> | ||
)} | ||
style={{ flex: 1 }} | ||
/> | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
@tailwind components; | ||
|
||
.list-container { | ||
box-shadow: var(--uui-shadow-level-1); | ||
border-radius: var(--uui-border-radius); | ||
} | ||
|
||
.list-footer-container, | ||
.list-header-container { | ||
box-shadow: var(--uui-shadow-level-1); | ||
} | ||
|
||
.list-footer-container { | ||
box-shadow: var(--uui-shadow-level-1); | ||
@apply h-12 flex justify-center items-center; | ||
} |
110 changes: 110 additions & 0 deletions
110
portals-ui/sites/ngs-portal/src/shared/highlight-text/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import { useMemo } from 'react'; | ||
import type { CSSProperties } from 'react'; | ||
import classNames from 'classnames'; | ||
import type { CommonProps } from '@cloud-pipeline/components'; | ||
import './style.css'; | ||
|
||
export type HighlightedTextProps = CommonProps & { | ||
search?: string | undefined; | ||
children: string; | ||
highlightClassName?: string; | ||
highlightStyle?: CSSProperties; | ||
}; | ||
|
||
type HighlightPart = { | ||
part: string; | ||
highlight: boolean; | ||
}; | ||
|
||
const escapeRegExpCharacters = [ | ||
'.', | ||
'-', | ||
'+', | ||
'*', | ||
'?', | ||
'^', | ||
'$', | ||
'(', | ||
')', | ||
'[', | ||
']', | ||
'{', | ||
'}', | ||
]; | ||
|
||
function escapeRegExp( | ||
string: string, | ||
characters = escapeRegExpCharacters, | ||
): string { | ||
let result = string; | ||
characters.forEach((character) => { | ||
result = result.replace( | ||
new RegExp('\\' + character, 'g'), | ||
`\\${character}`, | ||
); | ||
}); | ||
return result; | ||
} | ||
|
||
export default function HighlightedText(props: HighlightedTextProps) { | ||
const { | ||
children: text, | ||
search = '', | ||
className, | ||
style, | ||
highlightClassName, | ||
highlightStyle, | ||
} = props; | ||
const parts = useMemo<HighlightPart[]>(() => { | ||
if (!search?.length) { | ||
return [ | ||
{ | ||
part: text, | ||
highlight: false, | ||
}, | ||
]; | ||
} | ||
const regExp = new RegExp(`${escapeRegExp(search)}`, 'ig'); | ||
let e = regExp.exec(text); | ||
let idx = 0; | ||
const result: HighlightPart[] = []; | ||
while (e) { | ||
result.push({ | ||
part: text.slice(idx, e.index), | ||
highlight: false, | ||
}); | ||
result.push({ | ||
part: e[0], | ||
highlight: true, | ||
}); | ||
idx = e.index + e[0].length; | ||
e = regExp.exec(text); | ||
} | ||
if (idx < text.length) { | ||
result.push({ | ||
part: text.slice(idx), | ||
highlight: false, | ||
}); | ||
} | ||
return result; | ||
}, [text, search]); | ||
return ( | ||
<span className={classNames(className, 'highlighted-text')} style={style}> | ||
{parts.map((part, idx) => ( | ||
<span | ||
key={`part-${idx}`} | ||
className={classNames( | ||
'm-0', | ||
'highlighted-text-part', | ||
highlightClassName, | ||
{ | ||
highlighted: part.highlight, | ||
}, | ||
)} | ||
style={part.highlight ? highlightStyle : undefined}> | ||
{part.part} | ||
</span> | ||
))} | ||
</span> | ||
); | ||
} |
5 changes: 5 additions & 0 deletions
5
portals-ui/sites/ngs-portal/src/shared/highlight-text/style.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
@tailwind components; | ||
|
||
.highlighted-text .highlighted-text-part.highlighted { | ||
@apply bg-amber-200 dark:bg-amber-700/75; | ||
} |