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 + }, []) +}