Skip to content

Commit

Permalink
ItemsPanel component, Home page
Browse files Browse the repository at this point in the history
  • Loading branch information
rodichenko committed Nov 27, 2024
1 parent 4e33c6f commit e826594
Show file tree
Hide file tree
Showing 14 changed files with 351 additions and 154 deletions.
57 changes: 37 additions & 20 deletions portals-ui/packages/components/lib/components/list/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Key, ReactNode } from 'react';
import React, { useState } from 'react';
import type { ReactNode } from 'react';
import { useMemo, useCallback, useState } from 'react';
import type { VirtualListState } from '@epam/uui-core';
import classNames from 'classnames';
import { VirtualList } from '@epam/uui';
Expand All @@ -18,18 +18,45 @@ export default function List<Item>(props: ListProps<Item>): ReactNode {
style,
className,
itemKey,
sliced,
} = props;
const itemsToDisplayCount = useMemo(() => {
if (typeof sliced === 'boolean') {
return sliced ? MIN_VISIBLE_COUNT : Infinity;
}
if (typeof sliced === 'number') {
return sliced;
}
return Infinity;
}, [sliced]);
const [listState, setListState] = useState<VirtualListState>({
topIndex: 0,
visibleCount: MIN_VISIBLE_COUNT,
visibleCount: itemsToDisplayCount,
});
const visibleData = data.slice(
listState.topIndex,
(listState.topIndex ?? 0) + (listState.visibleCount ?? MIN_VISIBLE_COUNT),
const { topIndex = 0, visibleCount = itemsToDisplayCount } = listState;
const visibleData = useMemo(
() => data.slice(topIndex, topIndex + visibleCount),
[data, topIndex, visibleCount],
);
const getItemKey = useCallback(
(item: Item, index: number): string => {
if (itemKey && typeof itemKey === 'function') {
return String(itemKey(item, index));
}
if (itemKey && Object.hasOwnProperty.call(item, itemKey)) {
return String(item[itemKey]);
}
return `key_${index}`;
},
[itemKey],
);
const rows = useMemo(
() =>
visibleData.map((item, index) => (
<div key={getItemKey(item, index)}>{renderItem(item, index)}</div>
)),
[getItemKey, renderItem, visibleData],
);
const rows = visibleData.map((item, index) => (
<React.Fragment key={index}>{renderItem(item, index)}</React.Fragment>
));
const listComponent = virtualized ? (
<VirtualList
cx="max-h-full"
Expand All @@ -39,17 +66,7 @@ export default function List<Item>(props: ListProps<Item>): ReactNode {
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>
<div className="overflow-y-auto">{rows}</div>
);
return (
<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,21 @@ const ListHeader = (props: ListHeaderProps) => {
} = props;
return (
<div className={classNames(className, 'divide-y')} style={style}>
<b
className="flex no-wrap px-6 py-4"
<div
className="flex items-center no-wrap p-2"
style={{ color: 'var(--uui-text-secondary)' }}>
{title} {controls ? <div className="ml-auto">{controls}</div> : null}
</b>
<b>{title}</b>
{controls ? <div className="ml-auto">{controls}</div> : null}
</div>
{onSearch ? (
<div className="px-6 py-2">
<div className="p-0.5">
<SearchInput
value={search}
onValueChange={onSearch}
placeholder={searchPlaceholder ?? 'Search'}
debounceDelay={300}
disableDebounce
mode="inline"
size="30"
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type ListProps<Item> = CommonProps & {
footer?: ReactNode;
virtualized?: boolean;
itemKey?: keyof Item | ((item: Item, index: number) => string | number);
sliced?: number | boolean;
};

export type ListHeaderProps = CommonProps & {
Expand Down
1 change: 1 addition & 0 deletions portals-ui/packages/components/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ import './style.css';

export { DummyComponent, List, ListHeader };
export * from './components/common.types';
export * from './components/list/types';
165 changes: 64 additions & 101 deletions portals-ui/sites/ngs-portal/src/pages/home/home.tsx
Original file line number Diff line number Diff line change
@@ -1,117 +1,80 @@
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 { useEffect } from 'react';
import type { Run } from '@cloud-pipeline/core';
import { Button } from '@epam/uui';
import { useProjectsState } from '../../state/projects/hooks';
import { loadProjects } from '../../state/projects/load-projects';
import HighlightedText from '../../shared/highlight-text';
import { ItemsPanel } from '../../widgets/items-panel/items-panel.tsx';
import { usePipelinesState } from '../../state/pipelines/hooks.ts';
import { loadPipelines } from '../../state/pipelines/load-pipelines.ts';
import './style.css';

export const Home = () => {
const { projects } = useProjectsState();
const [projectSearch, setProjectSearch] = useState('');
const [pipelinesSearch, setPipelinesSearch] = useState('');
const { pipelines } = usePipelinesState();
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;
}
useEffect(() => {
loadPipelines()
.then(() => {})
.catch(() => {});
}, []);
return (
<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 className="flex h-full w-full gap-1 overflow-hidden flex-nowrap p-1">
<div className="flex-1 h-full overflow-auto p-2">
<ItemsPanel
className="max-h-full list-container overflow-auto"
title="Projects"
actions={
<Button caption="Create project" size="24" onClick={() => null} />
}
items={projects}
renderItem={(item, search) => (
<div className="p-2 border-b">
<HighlightedText search={search}>{item.name}</HighlightedText>
</div>
)}
sliced
search
itemKey="id"
viewAll={{ title: 'View all projects', link: '/projects' }}
/>
</div>
<div className="flex-1 h-full overflow-auto p-2">
<ItemsPanel
className="max-h-full list-container overflow-auto"
title="Pipelines"
items={pipelines}
renderItem={(item, search) => (
<div className="p-2 border-b">
<HighlightedText search={search}>{item.name}</HighlightedText>
</div>
)}
sliced
search
itemKey="id"
viewAll={{ title: 'View all pipelines', link: '/pipelines' }}
/>
</div>
<div className="flex-1 h-full overflow-auto p-2">
<ItemsPanel
className="max-h-full list-container overflow-auto"
title="Runs history"
renderItem={(run: Run) => (
<div className="p-2 border-b">
<span>
pipeline-{run.id}, status: {run.status}
</span>
</div>
)}
sliced
itemKey="id"
viewAll={{ title: 'View all runs', link: '/runs' }}
/>
</div>
</div>
);
};
13 changes: 1 addition & 12 deletions portals-ui/sites/ngs-portal/src/pages/home/style.css
Original file line number Diff line number Diff line change
@@ -1,16 +1,5 @@
@tailwind components;

.list-container {
box-shadow: var(--uui-shadow-level-1);
border-radius: var(--uui-border-radius);
@apply bg-slate-50 rounded shadow-md;
}

.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;
}
1 change: 0 additions & 1 deletion portals-ui/sites/ngs-portal/src/pages/layout/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export const Layout = () => {
<main>
<Outlet />
</main>
<footer>Footer</footer>
</div>
</Initialization>
);
Expand Down
2 changes: 1 addition & 1 deletion portals-ui/sites/ngs-portal/src/pages/layout/style.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.app-layout {
@apply w-full h-full overflow-auto flex flex-col;
@apply w-full h-full overflow-auto flex flex-col bg-slate-200;
}

.app-layout footer {
Expand Down
26 changes: 22 additions & 4 deletions portals-ui/sites/ngs-portal/src/pages/pipelines/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { useEffect } from 'react';
import { Spinner } from '@epam/uui';
import { loadPipelines } from '../../state/pipelines/load-pipelines';
import { usePipelinesState } from '../../state/pipelines/hooks';
import { List, ListHeader } from '@cloud-pipeline/components';
import HighlightedText from '../../shared/highlight-text';
import { useSearch } from '../../shared/hooks/use-search.ts';

export default function Pipelines() {
useEffect(() => {
Expand All @@ -10,6 +13,9 @@ export default function Pipelines() {
.catch(() => {});
}, []);
const { pipelines, error, pending } = usePipelinesState();
const { search, onSearchChange, filtered } = useSearch({
items: pipelines ?? [],
});
if (error) {
return <div>{error}</div>;
}
Expand All @@ -20,10 +26,22 @@ export default function Pipelines() {
return <div>No data</div>;
}
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
{pipelines.map((pipeline) => (
<span key={pipeline.id}>{pipeline.name}</span>
))}
<div className="flex flex-col overflow-auto">
<ListHeader
title="Pipelines"
className="shrink-0 border"
search={search}
onSearch={onSearchChange}
/>
<List
className="overflow-auto border-b border-l border-r"
data={filtered}
renderItem={(pipeline) => (
<HighlightedText search={search}>{pipeline.name}</HighlightedText>
)}
itemKey="id"
sliced={20}
/>
</div>
);
}
Loading

0 comments on commit e826594

Please sign in to comment.