1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-16 22:50:10 +03:00

F OpenNebula/one#5422: Add virtual list component

This commit is contained in:
Sergio Betanzos 2021-06-10 17:35:56 +02:00
parent ebdc1dc422
commit 694280f5f6
No known key found for this signature in database
GPG Key ID: E3E704F097737136
19 changed files with 1159 additions and 63 deletions

View File

@ -5,9 +5,9 @@
"main": "src/index.js",
"scripts": {
"dev": "rimraf dist/ && cross-env NODE_ENV=development babel-node ./src/server/index.js",
"build-front": "rimraf dist/client && webpack --config ./webpack.config.prod.client.js",
"build-client": "rimraf dist/client && webpack --config ./webpack.config.prod.client.js",
"build-server": "rimraf dist/index.js && webpack --config ./webpack.config.prod.server.js",
"build": "npm run build-front && npm run build-server",
"build": "npm run build-client && npm run build-server",
"start": "cross-env NODE_ENV=production babel-node ./dist/index.js",
"pot": "node potfile.js",
"po2json": "node po2json.js",
@ -108,7 +108,9 @@
"react-redux": "7.2.1",
"react-router": "5.2.0",
"react-router-dom": "5.2.0",
"react-table": "7.7.0",
"react-transition-group": "4.4.1",
"react-virtual": "2.7.1",
"redux": "4.1.0",
"redux-thunk": "2.3.0",
"rimraf": "3.0.2",
@ -129,4 +131,4 @@
"yup": "0.32.9",
"zeromq": "5.2.0"
}
}
}

View File

@ -0,0 +1,66 @@
import * as React from 'react'
import PropTypes from 'prop-types'
import { useVirtual } from 'react-virtual'
import { Box } from '@material-ui/core'
const ListVirtualized = ({ list = [] }) => {
const parentRef = React.useRef()
const rowVirtualizer = useVirtual({
size: list.length,
parentRef,
overscan: 20,
estimateSize: React.useCallback(() => 35, []),
keyExtractor: index => list[index]?.ID
})
return (
<Box>
<div
ref={parentRef}
style={{
height: '150px',
overflow: 'auto'
}}
>
<div
style={{
height: `${rowVirtualizer.totalSize}px`,
width: '100%',
position: 'relative'
}}
>
{rowVirtualizer.virtualItems.map(virtualRow => {
console.log(virtualRow)
return (
<div
key={virtualRow.index}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`
}}
>
Row {virtualRow.index}
</div>
)
})}
</div>
</div>
</Box>
)
}
ListVirtualized.propTypes = {
list: PropTypes.arrayOf(PropTypes.any)
}
ListVirtualized.defaultProps = {
list: []
}
export default ListVirtualized

View File

@ -1,9 +1,11 @@
import ListHeader from 'client/components/List/ListHeader'
import ListCards from 'client/components/List/ListCards'
import ListHeader from 'client/components/List/ListHeader'
import ListInfiniteScroll from 'client/components/List/ListInfiniteScroll'
import ListVirtualized from 'client/components/List/ListVirtualized'
export {
ListHeader,
ListCards,
ListInfiniteScroll
ListHeader,
ListInfiniteScroll,
ListVirtualized
}

View File

@ -0,0 +1,162 @@
import React from 'react'
import PropTypes from 'prop-types'
import {
Table as MTable,
TableFooter,
TableContainer,
TablePagination,
TableSortLabel,
TableBody,
TableCell,
TableHead,
TableRow
} from '@material-ui/core'
import {
useFilters,
useGlobalFilter,
usePagination,
useRowSelect,
useSortBy,
useTable
} from 'react-table'
import TablePaginationActions from 'client/components/Table/TablePaginationActions'
import TableToolbar from 'client/components/Table/TableToolbar'
import * as TableFilters from 'client/components/Table/Filters'
const EnhancedTable = ({
title,
columns,
data,
actions,
skipPageReset,
filterTypes
}) => {
const defaultColumn = React.useMemo(() => ({
Filter: TableFilters.DefaultFilter
}), [])
const {
getTableProps,
headerGroups,
prepareRow,
page,
gotoPage,
setPageSize,
preGlobalFilteredRows,
setGlobalFilter,
state: { pageIndex, pageSize, selectedRowIds, globalFilter }
} = useTable(
{
columns,
data,
defaultColumn,
filterTypes,
autoResetPage: !skipPageReset
},
useFilters,
useGlobalFilter,
useSortBy,
usePagination,
useRowSelect
)
const handleChangePage = (_, newPage) => {
gotoPage(newPage)
}
const handleChangeRowsPerPage = event => {
setPageSize(parseInt(event.target.value, 10))
}
return (
<TableContainer>
<TableToolbar
title={title}
numSelected={Object.keys(selectedRowIds).length}
actions={actions}
preGlobalFilteredRows={preGlobalFilteredRows}
setGlobalFilter={setGlobalFilter}
globalFilter={globalFilter}
/>
<MTable size='small' {...getTableProps()}>
<TableHead>
{headerGroups.map(headerGroup => (
<TableRow {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<TableCell
{...(column.id === 'selection'
? column.getHeaderProps()
: column.getHeaderProps(column.getSortByToggleProps()))}
>
{column.render('Header')}
{column.id !== 'selection' ? (
<TableSortLabel
active={column.isSorted}
// react-table has a unsorted state which is not treated here
direction={column.isSortedDesc ? 'desc' : 'asc'}
/>
) : null}
{/* Render the columns filter UI */}
<div onClick={event => event.stopPropagation()}>
{column.canFilter ? column.render('Filter') : null}
</div>
</TableCell>
))}
</TableRow>
))}
</TableHead>
<TableBody>
{page.map((row, i) => {
prepareRow(row)
return (
<TableRow {...row.getRowProps()}>
{row.cells.map(cell => (
<TableCell {...cell.getCellProps()}>
{cell.render('Cell')}
</TableCell>
))}
</TableRow>
)
})}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25, { label: 'All', value: -1 }]}
count={data.length}
rowsPerPage={pageSize}
page={pageIndex}
SelectProps={{
inputProps: { 'aria-label': 'rows per page' },
native: true
}}
onChangePage={handleChangePage}
onChangeRowsPerPage={handleChangeRowsPerPage}
ActionsComponent={TablePaginationActions}
/>
</TableRow>
</TableFooter>
</MTable>
</TableContainer>
)
}
EnhancedTable.propTypes = {
title: PropTypes.string.isRequired,
columns: PropTypes.array.isRequired,
data: PropTypes.array.isRequired,
actions: PropTypes.array.isRequired,
skipPageReset: PropTypes.bool,
filterTypes: PropTypes.array
}
EnhancedTable.defaultProps = {
skipPageReset: false
}
export default EnhancedTable

View File

@ -0,0 +1,37 @@
import React from 'react'
import PropTypes from 'prop-types'
import { debounce } from '@material-ui/core'
const DefaultFilter = ({ column }) => {
/** @type {import('react-table').UseFiltersInstanceProps} */
const { filterValue, preFilteredRows, setFilter } = column
const count = preFilteredRows?.length
const [value, setValue] = React.useState(filterValue)
const handleChange = React.useCallback(
// Set undefined to remove the filter entirely
debounce(value => { setFilter(value || undefined) }, 200)
)
return (
<input
value={value || ''}
onChange={event => {
setValue(event.target.value)
handleChange(event.target.value)
}}
placeholder={`Search ${count} records...`}
/>
)
}
DefaultFilter.propTypes = {
column: PropTypes.shape({
filterValue: PropTypes.any,
preFilteredRows: PropTypes.array,
setFilter: PropTypes.func.isRequired
})
}
export default DefaultFilter

View File

@ -0,0 +1,90 @@
import React from 'react'
import PropTypes from 'prop-types'
import { makeStyles, fade, debounce, InputBase } from '@material-ui/core'
import { Search as SearchIcon } from 'iconoir-react'
const useStyles = makeStyles(theme => ({
search: {
position: 'relative',
borderRadius: theme.shape.borderRadius,
backgroundColor: fade(theme.palette.common.white, 0.15),
'&:hover': {
backgroundColor: fade(theme.palette.common.white, 0.25)
},
marginRight: theme.spacing(2),
marginLeft: 0,
width: '100%',
[theme.breakpoints.up('sm')]: {
marginLeft: theme.spacing(3),
width: 'auto'
}
},
searchIcon: {
width: theme.spacing(7),
height: '100%',
position: 'absolute',
pointerEvents: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
},
inputRoot: {
color: 'inherit'
},
inputInput: {
padding: theme.spacing(1, 1, 1, 7),
transition: theme.transitions.create('width'),
width: '100%',
[theme.breakpoints.up('md')]: {
width: 200
}
}
}))
const GlobalFilter = props => {
const { preGlobalFilteredRows, globalFilter, setGlobalFilter } = props
const classes = useStyles()
const count = preGlobalFilteredRows.length
const [value, setValue] = React.useState(globalFilter)
const handleChange = React.useCallback(
// Set undefined to remove the filter entirely
debounce(value => { setGlobalFilter(value || undefined) }, 200)
)
// Global filter only works with pagination from the first page.
// This may not be a problem for server side pagination when
// only the current page is downloaded.
return (
<div className={classes.search}>
<div className={classes.searchIcon}>
<SearchIcon />
</div>
<InputBase
value={value ?? ''}
onChange={event => {
setValue(event.target.value)
handleChange(event.target.value)
}}
placeholder={`${count} records...`}
classes={{
root: classes.inputRoot,
input: classes.inputInput
}}
inputProps={{ 'aria-label': 'search' }}
/>
</div>
)
}
GlobalFilter.propTypes = {
preGlobalFilteredRows: PropTypes.array.isRequired,
globalFilter: PropTypes.string,
setGlobalFilter: PropTypes.func.isRequired
}
export default GlobalFilter

View File

@ -0,0 +1,52 @@
import React from 'react'
import PropTypes from 'prop-types'
import { T } from 'client/constants'
const SelectFilter = ({ column, accessorOption }) => {
/** @type {import('react-table').UseFiltersInstanceProps} */
const { filterValue, setFilter, preFilteredRows, id } = column
// Calculate the options for filtering using the preFilteredRows
const options = React.useMemo(() => {
const options = new Set()
preFilteredRows.forEach(row => {
options.add(row.values[id])
})
return [...options.values()]
}, [id, preFilteredRows])
return (
<select
value={filterValue}
onChange={event => {
setFilter(event.target.value || undefined)
}}
>
<option value=''>{T.All}</option>
{options.map((option, i) => {
const value = option[accessorOption] ?? option
return (
<option key={i} value={value}>
{value}
</option>
)
})}
</select>
)
}
SelectFilter.propTypes = {
column: PropTypes.shape({
filterValue: PropTypes.any,
preFilteredRows: PropTypes.array,
setFilter: PropTypes.func.isRequired,
id: PropTypes.string.isRequired
}),
accessorOption: PropTypes.string
}
export default SelectFilter

View File

@ -0,0 +1,9 @@
import DefaultFilter from 'client/components/Table/Filters/DefaultFilter'
import GlobalFilter from 'client/components/Table/Filters/GlobalFilter'
import SelectFilter from 'client/components/Table/Filters/SelectFilter'
export {
DefaultFilter,
GlobalFilter,
SelectFilter
}

View File

@ -0,0 +1,89 @@
import React from 'react'
import PropTypes from 'prop-types'
import { makeStyles, useTheme, IconButton } from '@material-ui/core'
import {
FastArrowLeft as FirstPageIcon,
NavArrowLeft as PreviousPageIcon,
NavArrowRight as NextPageIcon,
FastArrowRight as LastPageIcon
} from 'iconoir-react'
const useStyles = makeStyles(theme => ({
root: {
flexShrink: 0,
marginLeft: theme.spacing(2.5)
}
}))
const TablePaginationActions = ({ count, page, rowsPerPage, onChangePage }) => {
const classes = useStyles()
const theme = useTheme()
const handleFirstPageButtonClick = event => {
onChangePage(event, 0)
}
const handleBackButtonClick = event => {
onChangePage(event, page - 1)
}
const handleNextButtonClick = event => {
onChangePage(event, page + 1)
}
const handleLastPageButtonClick = event => {
onChangePage(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1))
}
return (
<div className={classes.root}>
<IconButton
onClick={handleFirstPageButtonClick}
disabled={page === 0}
aria-label="first page"
>
{theme.direction === 'rtl' ? <LastPageIcon /> : <FirstPageIcon />}
</IconButton>
<IconButton
onClick={handleBackButtonClick}
disabled={page === 0}
aria-label="previous page"
>
{theme.direction === 'rtl' ? (
<NextPageIcon />
) : (
<PreviousPageIcon />
)}
</IconButton>
<IconButton
onClick={handleNextButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="next page"
>
{theme.direction === 'rtl' ? (
<PreviousPageIcon />
) : (
<NextPageIcon />
)}
</IconButton>
<IconButton
onClick={handleLastPageButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="last page"
>
{theme.direction === 'rtl' ? <FirstPageIcon /> : <LastPageIcon />}
</IconButton>
</div>
)
}
TablePaginationActions.propTypes = {
count: PropTypes.number.isRequired,
onChangePage: PropTypes.func.isRequired,
page: PropTypes.number.isRequired,
rowsPerPage: PropTypes.number.isRequired
}
export default TablePaginationActions

View File

@ -0,0 +1,92 @@
import * as React from 'react'
import PropTypes from 'prop-types'
import clsx from 'clsx'
import {
makeStyles,
lighten,
Toolbar,
Typography,
Tooltip,
IconButton
} from '@material-ui/core'
import GlobalFilter from 'client/components/Table/Filters/GlobalFilter'
const useToolbarStyles = makeStyles(theme => ({
root: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(1)
},
highlight:
theme.palette.type === 'light'
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85)
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark
},
title: {
flex: '1 1 100%'
}
}))
const TableToolbar = props => {
const classes = useToolbarStyles()
const {
title,
numSelected,
actions,
preGlobalFilteredRows,
setGlobalFilter,
globalFilter
} = props
return (
<Toolbar
className={clsx(classes.root, {
[classes.highlight]: numSelected > 0
})}
>
{numSelected > 0 ? (
<Typography className={classes.title} color='inherit' variant='subtitle1'>
{numSelected} selected
</Typography>
) : (
<Typography className={classes.title} variant='h6' id='tableTitle'>
{title}
</Typography>
)}
{numSelected > 0 ? (
actions?.map(({ title, icon: Icon, handleClick }) => (
<Tooltip key={title} title={title}>
<IconButton aria-label={title} onClick={handleClick}>
<Icon />
</IconButton>
</Tooltip>
))
) : (
<GlobalFilter
preGlobalFilteredRows={preGlobalFilteredRows}
globalFilter={globalFilter}
setGlobalFilter={setGlobalFilter}
/>
)}
</Toolbar>
)
}
TableToolbar.propTypes = {
title: PropTypes.string.isRequired,
numSelected: PropTypes.number.isRequired,
actions: PropTypes.array,
setGlobalFilter: PropTypes.func.isRequired,
preGlobalFilteredRows: PropTypes.array.isRequired,
globalFilter: PropTypes.string
}
export default TableToolbar

View File

@ -0,0 +1,11 @@
import EnhancedTable from 'client/components/Table/EnhancedTable'
import TablePaginationActions from 'client/components/Table/TablePaginationActions'
import TableToolbar from 'client/components/Table/TableToolbar'
export * from 'client/components/Table/Filters'
export {
EnhancedTable,
TablePaginationActions,
TableToolbar
}

View File

@ -0,0 +1,10 @@
import * as Icons from 'iconoir-react'
export default [
{ title: 'Delete', icon: Icons.Trash, handleClick: () => undefined },
{ title: 'Resume', icon: Icons.PlayOutline, handleClick: () => undefined },
{ title: 'Power Off', icon: Icons.OffRounded, handleClick: () => undefined },
{ title: 'Reboot', icon: Icons.Refresh, handleClick: () => undefined },
{ title: 'Lock', icon: Icons.Lock, handleClick: () => undefined },
{ title: 'Unlock', icon: Icons.NoLock, handleClick: () => undefined }
]

View File

@ -0,0 +1,226 @@
/* eslint-disable react/prop-types */
/* eslint-disable react/jsx-key */
import * as React from 'react'
import PropTypes from 'prop-types'
import clsx from 'clsx'
import {
makeStyles,
fade,
Table as MTable,
TableFooter,
TableContainer,
TablePagination,
TableSortLabel,
TableBody,
TableCell,
TableHead,
TableRow
} from '@material-ui/core'
import { TableToolbar, TablePaginationActions } from 'client/components/Table'
const useStyles = makeStyles(theme => ({
table: {
// borderCollapse: 'collapse',
[theme.breakpoints.down('sm')]: {
borderCollapse: 'separate',
borderSpacing: theme.spacing(0, 4)
}
},
head: {
[theme.breakpoints.down('sm')]: {
display: 'none'
}
},
headRow: {
[theme.breakpoints.down('sm')]: {
display: 'block',
marginBottom: 5
}
},
bodyRow: {
'&:nth-of-type(odd)': {
// backgroundColor: theme.palette.action.hover
},
'&$selected, &$selected:hover': {
backgroundColor: theme.palette.action.hover
// backgroundColor: fade(theme.palette.secondary.main, theme.palette.action.selectedOpacity)
}
},
cell: {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
'&:first-of-type': {
width: 45
},
[theme.breakpoints.down('sm')]: {
'&:first-of-type': {
display: 'none'
},
'&:last-of-type': {
borderBottom: `1px solid ${theme.palette.primary.contrastText}`
},
'&:nth-of-type(2)': {
borderTop: `1px solid ${theme.palette.primary.contrastText}`
},
borderInline: `1px solid ${theme.palette.primary.contrastText}`,
display: 'block',
position: 'relative',
textAlign: 'left',
borderBottom: 'none'
// paddingLeft: 130,
// '&::before': {
// content: 'attr(data-heading)',
// position: 'absolute',
// top: 0,
// left: 0,
// width: 120,
// height: '100%',
// display: 'flex',
// alignItems: 'center',
// backgroundColor: theme.palette.primary.main,
// color: theme.palette.primary.contrastText,
// fontSize: '0.75rem',
// padding: theme.spacing(0, 1),
// justifyContent: 'center'
// }
}
},
selected: {}
}))
const TableBod = ({
getTableProps,
headerGroups,
prepareRow,
page,
gotoPage,
setPageSize,
preGlobalFilteredRows,
setGlobalFilter,
state: { pageIndex, pageSize, selectedRowIds, globalFilter },
data
}) => {
const classes = useStyles()
const handleChangePage = (_, newPage) => {
gotoPage(newPage)
}
const handleChangeRowsPerPage = event => {
setPageSize(parseInt(event.target.value, 10))
}
return (
<TableContainer>
<TableToolbar
numSelected={Object.keys(selectedRowIds).length}
preGlobalFilteredRows={preGlobalFilteredRows}
setGlobalFilter={setGlobalFilter}
globalFilter={globalFilter}
/>
<MTable size='small' stickyHeader {...getTableProps()} className={classes.table}>
<TableHead className={classes.head}>
{headerGroups.map(headerGroup => (
<TableRow {...headerGroup.getHeaderGroupProps()} className={classes.headRow}>
{headerGroup.headers.map(column => (
<TableCell
{...(column.id === 'selection'
? column.getHeaderProps()
: column.getHeaderProps(column.getSortByToggleProps()))}
>
{column.render('Header')}
{column.id !== 'selection' ? (
<TableSortLabel
active={column.isSorted}
// react-table has a unsorted state which is not treated here
direction={column.isSortedDesc ? 'desc' : 'asc'}
/>
) : null}
{/* Render the columns filter UI */}
<div onClick={event => event.stopPropagation()}>
{column.canFilter ? column.render('Filter') : null}
</div>
</TableCell>
))}
</TableRow>
))}
</TableHead>
<TableBody>
{page.map((row, i) => {
prepareRow(row)
const { onChange, checked } = row.getToggleRowSelectedProps()
return (
<TableRow {...row.getRowProps()} hover onClick={onChange} selected={checked} className={classes.bodyRow}>
{row.cells.map(cell => {
console.log({ cell })
return (
<TableCell {...cell.getCellProps()} className={classes.cell} data-heading={cell.column.Header}>
{cell.render('Cell')}
</TableCell>
)
})}
</TableRow>
)
})}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
count={data.length}
rowsPerPage={pageSize}
page={pageIndex}
SelectProps={{
inputProps: { 'aria-label': 'rows per page' },
native: true
}}
onChangePage={handleChangePage}
onChangeRowsPerPage={handleChangeRowsPerPage}
ActionsComponent={TablePaginationActions}
/>
</TableRow>
</TableFooter>
</MTable>
</TableContainer>
)
/* return (
<Box
{...getTableBodyProps()}
height={`${rowVirtualizer.totalSize}px`}
position='relative'
style={{
display: 'flex',
flexWrap: 'wrap',
flexDirection: 'column',
gap: '1em',
margin: '0 0 3em 0',
padding: 0,
boxShadow: '0 0 40px rgba(0,0,0,0.2)'
}}
>
{rowVirtualizer.virtualItems.map(virtualRow => {
const row = rows[virtualRow.index]
prepareRow(row)
return <TableRow
key={row.getRowProps().key}
row={row}
virtualRow={virtualRow}
/>
})}
</Box>
) */
}
TableBod.propTypes = {
}
TableBod.defaultProps = {
}
export default TableBod

View File

@ -0,0 +1,57 @@
import * as React from 'react'
import { SelectFilter } from 'client/components/Table'
import { StatusChip } from 'client/components/Status'
import Colors from 'client/constants/color'
import * as VirtualMachineModel from 'client/models/VirtualMachine'
export default [
/* {
id: 'selection',
// The header can use the table's getToggleAllRowsSelectedProps method
// to render a checkbox.
// Pagination is a problem since this will select all rows even though
// not all rows are on the current page.
// The solution should be server side pagination.
// For one, the clients should not download all rows in most cases.
// The client should only download data for the current page.
// In that case, getToggleAllRowsSelectedProps works fine.
Header: ({ getToggleAllRowsSelectedProps }) => (
<CheckboxCell {...getToggleAllRowsSelectedProps()} />
),
// The cell can use the individual row's getToggleRowSelectedProps method
// to the render a checkbox
Cell: ({ row }) => (
<CheckboxCell {...row.getToggleRowSelectedProps()} />
)
}, */
{
Header: '#',
accessor: 'ID',
Cell: ({ value }) =>
<StatusChip stateColor={Colors.debug.light} text={`#${value}`} />
},
{ Header: 'Name', accessor: 'NAME' },
{ Header: 'Owner/Group', accessor: row => `${row.UNAME}/${row.GNAME}` },
{
Header: 'State',
id: 'STATE',
accessor: row => VirtualMachineModel.getState(row),
Cell: ({ value: { name, color } = {} }) => name && (
<StatusChip stateColor={color} text={name} />
),
Filter: ({ column }) => (
<SelectFilter column={column} accessorOption='name' />
),
filter: (rows, id, filterValue) =>
rows.filter(row => row.values[id]?.name === filterValue)
},
{
Header: 'Ips',
accessor: row => VirtualMachineModel.getIps(row),
Cell: ({ value }) => value.map(nic => (
<StatusChip key={nic} stateColor={Colors.debug.light} text={nic} />
))
}
]

View File

@ -0,0 +1,157 @@
/* eslint-disable react/prop-types */
/* eslint-disable react/jsx-key */
import * as React from 'react'
import { Paper, debounce, LinearProgress, CircularProgress } from '@material-ui/core'
import { useVirtual } from 'react-virtual'
import {
useTable,
useGlobalFilter,
useSortBy,
useRowSelect,
useFilters,
usePagination,
useFlexLayout
} from 'react-table'
import { useNearScreen } from 'client/hooks'
import { EnhancedTable, DefaultFilter } from 'client/components/Table'
import Columns from 'client/components/Tables/VmTable/columns'
import { console } from 'window-or-global'
const VmTable = ({ data, isLoading, finish, getNextData }) => {
const parentRef = React.useRef()
// <----------- USE TABLE ----------->
const columns = React.useMemo(() => Columns, [])
const defaultColumn = React.useMemo(() => ({
Filter: DefaultFilter
}), [])
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
totalColumnsWidth,
prepareRow
} = useTable(
{
columns,
data,
defaultColumn
},
useRowSelect,
useFlexLayout
)
// <----------- FINISH USE TABLE ----------->
// <----------- VIRTUALIZER ----------->
const rowVirtualizer = useVirtual({
size: rows.length,
parentRef,
overscan: 10,
estimateSize: React.useCallback(() => 50, []),
keyExtractor: index => rows[index]?.id
})
// <----------- FINISH VIRTUALIZER ----------->
// <----------- OBSERVER ----------->
const loaderRef = React.useRef()
const { isNearScreen } = useNearScreen({
distance: '100px',
externalRef: isLoading ? null : loaderRef,
once: false
})
const debounceHandleNextPage = React.useCallback(debounce(getNextData, 200), [])
React.useEffect(() => {
if (isNearScreen && !finish) debounceHandleNextPage()
}, [isNearScreen, finish, debounceHandleNextPage])
// <----------- FINISH OBSERVER ----------->
const RenderRow = React.useCallback(({ row, virtualRow }) => (
<div
{...row.getRowProps()}
ref={virtualRow.measureRef}
style={{
display: 'flex',
alignItems: 'center',
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`
}}
>
{row.cells.map(cell => (
<div {...cell.getCellProps()}>
{cell.render('Cell')}
</div>
))}
</div>
), [prepareRow, rows])
return (
<Paper style={{ height: '100%', overflow: 'hidden' }}>
<div
{...getTableProps()}
style={{ height: '100%', display: 'flex', flexFlow: 'column' }}
>
<div>
{headerGroups.map(headerGroup => (
<div {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<div {...column.getHeaderProps()}>
{column.render('Header')}
</div>
))}
</div>
))}
</div>
<div ref={parentRef} style={{ height: '100%', overflow: 'auto' }}>
<div
{...getTableBodyProps()}
style={{
height: `${rowVirtualizer.totalSize}px`,
width: '100%',
position: 'relative'
}}
>
{rowVirtualizer.virtualItems?.map(virtualRow => {
const row = rows[virtualRow.index]
prepareRow(row)
return <RenderRow
key={row.getRowProps().key}
row={row}
virtualRow={virtualRow}
/>
})}
</div>
{!finish && (
<LinearProgress
ref={loaderRef}
color='secondary'
style={{ width: '100%', marginTop: 10 }}
/>
)}
</div>
<p style={{ display: 'flex', alignItems: 'center', gap: '1em' }}>
<span>Total loaded: {rows.length}</span>
{isLoading && <CircularProgress size='1em' />}
</p>
</div>
</Paper>
)
}
export default VmTable

View File

@ -0,0 +1,5 @@
import VmTable from 'client/components/Tables/VmTable'
export {
VmTable
}

View File

@ -1,76 +1,51 @@
import React, { useEffect } from 'react'
import React, { useEffect, useState } from 'react'
import { Container, Box } from '@material-ui/core'
import { Trash as DeleteIcon } from 'iconoir-react'
import { Container } from '@material-ui/core'
import { useAuth } from 'client/features/Auth'
import { useVm, useVmApi } from 'client/features/One'
import { useGeneralApi } from 'client/features/General'
import { useFetch, useSearch } from 'client/hooks'
import { useFetch } from 'client/hooks'
import { ListHeader, ListCards } from 'client/components/List'
import { VirtualMachineCard } from 'client/components/Cards'
// import { DialogRequest } from 'client/components/Dialogs'
// import Information from 'client/containers/VirtualMachines/Sections/info'
import { T } from 'client/constants'
import { filterDoneVms } from 'client/models/VirtualMachine'
import { VmTable } from 'client/components/Tables'
const INITIAL_ELEMENT = -1
const NUMBER_OF_INTERVAL = -100
function VirtualMachines () {
// const [showDialog, setShowDialog] = useState(false)
const [{ start, end }, setPage] = useState(({ start: INITIAL_ELEMENT, end: -NUMBER_OF_INTERVAL }))
const vms = useVm()
const { getVm, getVms, terminateVm } = useVmApi()
const { getVms } = useVmApi()
const { filterPool } = useAuth()
const { enqueueSuccess } = useGeneralApi()
const { data, fetchRequest, loading, reloading } = useFetch(getVms)
const { fetchRequest, loading, reloading } = useFetch(getVms)
const { result, handleChange } = useSearch({
list: vms,
listOptions: { shouldSort: true, keys: ['ID', 'NAME'] }
})
useEffect(() => { fetchRequest({ start, end }) }, [filterPool])
useEffect(() => { fetchRequest() }, [filterPool])
const handleGetMoreData = () => {
console.log('FETCH MORE')
// const handleCancel = () => setShowDialog(false)
setPage(prevState => {
const newStart = prevState.start + NUMBER_OF_INTERVAL
const newEnd = prevState.end - NUMBER_OF_INTERVAL
fetchRequest({ start: newStart, end: newEnd })
return { start: newStart, end: newEnd }
})
}
const finish = data?.length < NUMBER_OF_INTERVAL
// console.log({ start, end, loading, finish, vms })
return (
<Container disableGutters>
<ListHeader
title={T.VMs}
reloadButtonProps={{
onClick: () => fetchRequest(undefined, { reload: true }),
isSubmitting: Boolean(loading || reloading)
}}
searchProps={{ handleChange }}
<Container disableGutters style={{ height: '100%' }}>
<VmTable
data={vms}
isLoading={(vms.length === 0 && (loading || reloading))}
finish={finish}
getNextData={handleGetMoreData}
/>
<Box p={3}>
<ListCards
list={result ?? filterDoneVms(vms)}
isLoading={vms.length === 0 && loading}
gridProps={{ 'data-cy': 'vms' }}
CardComponent={VirtualMachineCard}
cardsProps={({ value: { ID, NAME } }) => ({
actions: [
{
handleClick: () => terminateVm(ID)
.then(() => enqueueSuccess(`VM terminate - ID: ${ID}`))
.then(() => fetchRequest(undefined, { reload: true })),
icon: <DeleteIcon color='error' />,
cy: 'vm-delete'
}
]
})}
/>
</Box>
{/* {showDialog !== false && (
<DialogRequest
request={() => getVm(showDialog.id)}
dialogProps={{ handleCancel, ...showDialog }}
>
{({ data }) => <Information data={data} />}
</DialogRequest>
)} */}
</Container>
)
}

View File

@ -18,7 +18,7 @@ export const useVmApi = () => {
return {
getVm: id => unwrapDispatch(actions.getVm({ id })),
getVms: () => unwrapDispatch(actions.getVms()),
getVms: options => unwrapDispatch(actions.getVms(options)),
terminateVm: id => unwrapDispatch(actions.terminateVm({ id }))
}
}

View File

@ -1,5 +1,25 @@
import { STATES, VM_STATES, VM_LCM_STATES } from 'client/constants'
const EXTERNAL_IP_ATTRS = [
'GUEST_IP',
'GUEST_IP_ADDRESSES',
'AWS_IP_ADDRESS',
'AWS_PUBLIC_IP_ADDRESS',
'AWS_PRIVATE_IP_ADDRESS',
'AZ_IPADDRESS',
'SL_PRIMARYIPADDRESS'
]
const NIC_ALIAS_IP_ATTRS = [
'IP',
'IP6',
'IP6_GLOBAL',
'IP6_ULA',
'VROUTER_IP',
'VROUTER_IP6_GLOBAL',
'VROUTER_IP6_ULA'
]
export const filterDoneVms = (vms = []) =>
vms.filter(({ STATE }) => VM_STATES[STATE]?.name !== STATES.DONE)
@ -8,3 +28,37 @@ export const getState = ({ STATE, LCM_STATE } = {}) => {
return state?.name === STATES.ACTIVE ? VM_LCM_STATES[+LCM_STATE] : state
}
export const getIps = ({ TEMPLATE = {} } = {}) => {
const { NIC = [], PCI = [] } = TEMPLATE
// TODO: add monitoring ips
const nics = [NIC, PCI].flat()
return nics
.map(nic => NIC_ALIAS_IP_ATTRS.map(attr => nic[attr]).filter(Boolean))
.flat()
}
const getNicsFromMonitoring = ({ ID }) => {
const monitoringPool = {} // _getMonitoringPool()
const monitoringVM = monitoringPool[ID]
if (!monitoringPool || Object.keys(monitoringPool).length === 0 || !monitoringVM) return []
return EXTERNAL_IP_ATTRS.reduce(function (externalNics, attr) {
const monitoringValues = monitoringVM[attr]
if (monitoringValues) {
monitoringValues.split(',').forEach((_, ip) => {
const exists = externalNics.some(nic => nic.IP === ip)
if (!exists) {
externalNics.push({ NIC_ID: '_', IP: ip })
}
})
}
return externalNics
}, [])
}