diff --git a/src/fireedge/package.json b/src/fireedge/package.json
index d96e7e54e2..0580076534 100644
--- a/src/fireedge/package.json
+++ b/src/fireedge/package.json
@@ -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"
}
-}
+}
\ No newline at end of file
diff --git a/src/fireedge/src/client/components/List/ListVirtualized.js b/src/fireedge/src/client/components/List/ListVirtualized.js
new file mode 100644
index 0000000000..51f4b0a9d9
--- /dev/null
+++ b/src/fireedge/src/client/components/List/ListVirtualized.js
@@ -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 (
+
+
+
+ {rowVirtualizer.virtualItems.map(virtualRow => {
+ console.log(virtualRow)
+ return (
+
+ Row {virtualRow.index}
+
+ )
+ })}
+
+
+
+ )
+}
+
+ListVirtualized.propTypes = {
+ list: PropTypes.arrayOf(PropTypes.any)
+}
+
+ListVirtualized.defaultProps = {
+ list: []
+}
+
+export default ListVirtualized
diff --git a/src/fireedge/src/client/components/List/index.js b/src/fireedge/src/client/components/List/index.js
index d0a421b2de..d4c2a0d485 100644
--- a/src/fireedge/src/client/components/List/index.js
+++ b/src/fireedge/src/client/components/List/index.js
@@ -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
}
diff --git a/src/fireedge/src/client/components/Table/EnhancedTable.js b/src/fireedge/src/client/components/Table/EnhancedTable.js
new file mode 100644
index 0000000000..1b784317ba
--- /dev/null
+++ b/src/fireedge/src/client/components/Table/EnhancedTable.js
@@ -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 (
+
+
+
+
+ {headerGroups.map(headerGroup => (
+
+ {headerGroup.headers.map(column => (
+
+ {column.render('Header')}
+ {column.id !== 'selection' ? (
+
+ ) : null}
+ {/* Render the columns filter UI */}
+ event.stopPropagation()}>
+ {column.canFilter ? column.render('Filter') : null}
+
+
+ ))}
+
+ ))}
+
+
+ {page.map((row, i) => {
+ prepareRow(row)
+ return (
+
+ {row.cells.map(cell => (
+
+ {cell.render('Cell')}
+
+ ))}
+
+ )
+ })}
+
+
+
+
+
+
+
+
+
+ )
+}
+
+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
diff --git a/src/fireedge/src/client/components/Table/Filters/DefaultFilter.js b/src/fireedge/src/client/components/Table/Filters/DefaultFilter.js
new file mode 100644
index 0000000000..74578d30c9
--- /dev/null
+++ b/src/fireedge/src/client/components/Table/Filters/DefaultFilter.js
@@ -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 (
+ {
+ 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
diff --git a/src/fireedge/src/client/components/Table/Filters/GlobalFilter.js b/src/fireedge/src/client/components/Table/Filters/GlobalFilter.js
new file mode 100644
index 0000000000..6031d0ccb6
--- /dev/null
+++ b/src/fireedge/src/client/components/Table/Filters/GlobalFilter.js
@@ -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 (
+
+
+
+
+
{
+ setValue(event.target.value)
+ handleChange(event.target.value)
+ }}
+ placeholder={`${count} records...`}
+ classes={{
+ root: classes.inputRoot,
+ input: classes.inputInput
+ }}
+ inputProps={{ 'aria-label': 'search' }}
+ />
+
+ )
+}
+
+GlobalFilter.propTypes = {
+ preGlobalFilteredRows: PropTypes.array.isRequired,
+ globalFilter: PropTypes.string,
+ setGlobalFilter: PropTypes.func.isRequired
+}
+
+export default GlobalFilter
diff --git a/src/fireedge/src/client/components/Table/Filters/SelectFilter.js b/src/fireedge/src/client/components/Table/Filters/SelectFilter.js
new file mode 100644
index 0000000000..ae960c54d8
--- /dev/null
+++ b/src/fireedge/src/client/components/Table/Filters/SelectFilter.js
@@ -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 (
+
+ )
+}
+
+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
diff --git a/src/fireedge/src/client/components/Table/Filters/index.js b/src/fireedge/src/client/components/Table/Filters/index.js
new file mode 100644
index 0000000000..415b8a2378
--- /dev/null
+++ b/src/fireedge/src/client/components/Table/Filters/index.js
@@ -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
+}
diff --git a/src/fireedge/src/client/components/Table/TablePaginationActions.js b/src/fireedge/src/client/components/Table/TablePaginationActions.js
new file mode 100644
index 0000000000..50ed44217c
--- /dev/null
+++ b/src/fireedge/src/client/components/Table/TablePaginationActions.js
@@ -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 (
+
+
+ {theme.direction === 'rtl' ? : }
+
+
+ {theme.direction === 'rtl' ? (
+
+ ) : (
+
+ )}
+
+
= Math.ceil(count / rowsPerPage) - 1}
+ aria-label="next page"
+ >
+ {theme.direction === 'rtl' ? (
+
+ ) : (
+
+ )}
+
+
= Math.ceil(count / rowsPerPage) - 1}
+ aria-label="last page"
+ >
+ {theme.direction === 'rtl' ? : }
+
+
+ )
+}
+
+TablePaginationActions.propTypes = {
+ count: PropTypes.number.isRequired,
+ onChangePage: PropTypes.func.isRequired,
+ page: PropTypes.number.isRequired,
+ rowsPerPage: PropTypes.number.isRequired
+}
+
+export default TablePaginationActions
diff --git a/src/fireedge/src/client/components/Table/TableToolbar.js b/src/fireedge/src/client/components/Table/TableToolbar.js
new file mode 100644
index 0000000000..43b5999e9f
--- /dev/null
+++ b/src/fireedge/src/client/components/Table/TableToolbar.js
@@ -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 (
+ 0
+ })}
+ >
+ {numSelected > 0 ? (
+
+ {numSelected} selected
+
+ ) : (
+
+ {title}
+
+ )}
+
+ {numSelected > 0 ? (
+ actions?.map(({ title, icon: Icon, handleClick }) => (
+
+
+
+
+
+ ))
+ ) : (
+
+ )}
+
+ )
+}
+
+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
diff --git a/src/fireedge/src/client/components/Table/index.js b/src/fireedge/src/client/components/Table/index.js
new file mode 100644
index 0000000000..83ae12fa6c
--- /dev/null
+++ b/src/fireedge/src/client/components/Table/index.js
@@ -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
+}
diff --git a/src/fireedge/src/client/components/Tables/VmTable/actions.js b/src/fireedge/src/client/components/Tables/VmTable/actions.js
new file mode 100644
index 0000000000..8579bd06cc
--- /dev/null
+++ b/src/fireedge/src/client/components/Tables/VmTable/actions.js
@@ -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 }
+]
diff --git a/src/fireedge/src/client/components/Tables/VmTable/body.js b/src/fireedge/src/client/components/Tables/VmTable/body.js
new file mode 100644
index 0000000000..64d57d0b44
--- /dev/null
+++ b/src/fireedge/src/client/components/Tables/VmTable/body.js
@@ -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 (
+
+
+
+
+ {headerGroups.map(headerGroup => (
+
+ {headerGroup.headers.map(column => (
+
+ {column.render('Header')}
+ {column.id !== 'selection' ? (
+
+ ) : null}
+ {/* Render the columns filter UI */}
+ event.stopPropagation()}>
+ {column.canFilter ? column.render('Filter') : null}
+
+
+ ))}
+
+ ))}
+
+
+ {page.map((row, i) => {
+ prepareRow(row)
+ const { onChange, checked } = row.getToggleRowSelectedProps()
+
+ return (
+
+ {row.cells.map(cell => {
+ console.log({ cell })
+ return (
+
+ {cell.render('Cell')}
+
+ )
+ })}
+
+ )
+ })}
+
+
+
+
+
+
+
+
+ )
+
+ /* return (
+
+ {rowVirtualizer.virtualItems.map(virtualRow => {
+ const row = rows[virtualRow.index]
+ prepareRow(row)
+
+ return
+ })}
+
+ ) */
+}
+
+TableBod.propTypes = {
+}
+
+TableBod.defaultProps = {
+}
+
+export default TableBod
diff --git a/src/fireedge/src/client/components/Tables/VmTable/columns.js b/src/fireedge/src/client/components/Tables/VmTable/columns.js
new file mode 100644
index 0000000000..1bb2c03f2a
--- /dev/null
+++ b/src/fireedge/src/client/components/Tables/VmTable/columns.js
@@ -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 }) => (
+
+ ),
+ // The cell can use the individual row's getToggleRowSelectedProps method
+ // to the render a checkbox
+ Cell: ({ row }) => (
+
+ )
+ }, */
+ {
+ Header: '#',
+ accessor: 'ID',
+ Cell: ({ 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 && (
+
+ ),
+ Filter: ({ column }) => (
+
+ ),
+ filter: (rows, id, filterValue) =>
+ rows.filter(row => row.values[id]?.name === filterValue)
+ },
+ {
+ Header: 'Ips',
+ accessor: row => VirtualMachineModel.getIps(row),
+ Cell: ({ value }) => value.map(nic => (
+
+ ))
+ }
+]
diff --git a/src/fireedge/src/client/components/Tables/VmTable/index.js b/src/fireedge/src/client/components/Tables/VmTable/index.js
new file mode 100644
index 0000000000..dcf1216cc6
--- /dev/null
+++ b/src/fireedge/src/client/components/Tables/VmTable/index.js
@@ -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 }) => (
+
+ {row.cells.map(cell => (
+
+ {cell.render('Cell')}
+
+ ))}
+
+ ), [prepareRow, rows])
+
+ return (
+
+
+
+ {headerGroups.map(headerGroup => (
+
+ {headerGroup.headers.map(column => (
+
+ {column.render('Header')}
+
+ ))}
+
+ ))}
+
+
+
+
+ {rowVirtualizer.virtualItems?.map(virtualRow => {
+ const row = rows[virtualRow.index]
+ prepareRow(row)
+
+ return
+ })}
+
+
+ {!finish && (
+
+ )}
+
+
+
+ Total loaded: {rows.length}
+ {isLoading && }
+
+
+
+
+ )
+}
+
+export default VmTable
diff --git a/src/fireedge/src/client/components/Tables/index.js b/src/fireedge/src/client/components/Tables/index.js
new file mode 100644
index 0000000000..84a50ec60f
--- /dev/null
+++ b/src/fireedge/src/client/components/Tables/index.js
@@ -0,0 +1,5 @@
+import VmTable from 'client/components/Tables/VmTable'
+
+export {
+ VmTable
+}
diff --git a/src/fireedge/src/client/containers/VirtualMachines/index.js b/src/fireedge/src/client/containers/VirtualMachines/index.js
index 67f67c421c..17974bd4f3 100644
--- a/src/fireedge/src/client/containers/VirtualMachines/index.js
+++ b/src/fireedge/src/client/containers/VirtualMachines/index.js
@@ -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 (
-
- fetchRequest(undefined, { reload: true }),
- isSubmitting: Boolean(loading || reloading)
- }}
- searchProps={{ handleChange }}
+
+
-
- ({
- actions: [
- {
- handleClick: () => terminateVm(ID)
- .then(() => enqueueSuccess(`VM terminate - ID: ${ID}`))
- .then(() => fetchRequest(undefined, { reload: true })),
- icon: ,
- cy: 'vm-delete'
- }
- ]
- })}
- />
-
- {/* {showDialog !== false && (
- getVm(showDialog.id)}
- dialogProps={{ handleCancel, ...showDialog }}
- >
- {({ data }) => }
-
- )} */}
)
}
diff --git a/src/fireedge/src/client/features/One/vm/hooks.js b/src/fireedge/src/client/features/One/vm/hooks.js
index d42587bda8..1d72b83bd8 100644
--- a/src/fireedge/src/client/features/One/vm/hooks.js
+++ b/src/fireedge/src/client/features/One/vm/hooks.js
@@ -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 }))
}
}
diff --git a/src/fireedge/src/client/models/VirtualMachine.js b/src/fireedge/src/client/models/VirtualMachine.js
index 8275299161..354788c6cb 100644
--- a/src/fireedge/src/client/models/VirtualMachine.js
+++ b/src/fireedge/src/client/models/VirtualMachine.js
@@ -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
+ }, [])
+}