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

M ~#: Add newwstone tab

This commit is contained in:
Sergio Betanzos 2021-06-14 18:09:29 +02:00
parent 8a65245d54
commit daadc63f20
No known key found for this signature in database
GPG Key ID: E3E704F097737136
35 changed files with 635 additions and 644 deletions

View File

@ -7,8 +7,7 @@ import { Folder as DatastoreIcon } from 'iconoir-react'
import SelectCard, { Action } from 'client/components/Cards/SelectCard'
import { StatusBadge, StatusChip, LinearProgressWithLabel } from 'client/components/Status'
import { prettyBytes } from 'client/utils'
import Datastore from 'client/constants/datastore'
import * as DatastoreModel from 'client/models/Datastore'
const useStyles = makeStyles(({
title: {
@ -27,14 +26,15 @@ const DatastoreCard = memo(
({ value, isSelected, handleClick, actions }) => {
const classes = useStyles()
const { ID, NAME, TYPE, STATE, TOTAL_MB, USED_MB } = value
const type = Datastore.TYPES[TYPE]
const state = Datastore.STATES[STATE]
const { ID, NAME } = value
const percentOfUsed = +USED_MB * 100 / +TOTAL_MB || 0
const usedBytes = prettyBytes(+USED_MB, 'MB')
const totalBytes = prettyBytes(+TOTAL_MB, 'MB')
const percentLabel = `${usedBytes} / ${totalBytes} (${Math.round(percentOfUsed)}%)`
const type = DatastoreModel.getType(value)
const state = DatastoreModel.getState(value)
const {
percentOfUsed,
percentLabel
} = DatastoreModel.getCapacityInfo(value)
return (
<SelectCard

View File

@ -7,8 +7,7 @@ import { HardDrive as HostIcon } from 'iconoir-react'
import SelectCard, { Action } from 'client/components/Cards/SelectCard'
import { StatusBadge, StatusChip, LinearProgressWithLabel } from 'client/components/Status'
import { prettyBytes } from 'client/utils'
import Host from 'client/constants/host'
import * as HostModel from 'client/models/Host'
const useStyles = makeStyles({
title: {
@ -27,18 +26,17 @@ const HostCard = memo(
({ value, isSelected, handleClick, actions }) => {
const classes = useStyles()
const { ID, NAME, STATE, IM_MAD, VM_MAD, HOST_SHARE } = value
const { CPU_USAGE, TOTAL_CPU, MEM_USAGE, TOTAL_MEM } = HOST_SHARE
const { ID, NAME, IM_MAD, VM_MAD } = value
const percentCpuUsed = +CPU_USAGE * 100 / +TOTAL_CPU || 0
const percentCpuLabel = `${CPU_USAGE} / ${TOTAL_CPU} (${Math.round(percentCpuUsed)}%)`
const {
percentCpuUsed,
percentCpuLabel,
percentMemUsed,
percentMemLabel
} = HostModel.getAllocatedInfo(value)
const percentMemUsed = +MEM_USAGE * 100 / +TOTAL_MEM || 0
const usedMemBytes = prettyBytes(+MEM_USAGE)
const totalMemBytes = prettyBytes(+TOTAL_MEM)
const percentMemLabel = `${usedMemBytes} / ${totalMemBytes} (${Math.round(percentMemUsed)}%)`
const state = HostModel.getState(value)
const state = Host.STATES[STATE]
const mad = IM_MAD === VM_MAD ? IM_MAD : `${IM_MAD}/${VM_MAD}`
return (

View File

@ -28,7 +28,7 @@ const debugLogStyles = makeStyles(theme => ({
border: '4px solid transparent',
borderRadius: 7,
boxShadow: 'inset 0 0 0 10px',
color: theme.palette.primary.light
color: theme.palette.secondary.light
}
}
}))

View File

@ -47,7 +47,7 @@ export default makeStyles(theme => ({
border: '4px solid transparent',
borderRadius: 7,
boxShadow: 'inset 0 0 0 10px',
color: theme.palette.primary.light
color: theme.palette.secondary.light
}
},
/* ROUTES TRANSITIONS */

View File

@ -2,10 +2,35 @@ import * as React from 'react'
import PropTypes from 'prop-types'
import { useVirtual } from 'react-virtual'
import { debounce, Box, LinearProgress } from '@material-ui/core'
import { debounce, makeStyles, Box, LinearProgress } from '@material-ui/core'
import { useNearScreen } from 'client/hooks'
const useStyles = makeStyles(theme => ({
root: {
height: '100%',
overflow: 'auto',
'&::-webkit-scrollbar': {
width: 14
},
'&::-webkit-scrollbar-thumb': {
backgroundClip: 'content-box',
border: '4px solid transparent',
borderRadius: 7,
boxShadow: 'inset 0 0 0 10px',
color: theme.palette.secondary.light
}
},
container: {
width: '100%',
position: 'relative'
},
loading: {
width: '100%',
marginTop: 10
}
}))
const ListVirtualized = ({
canFetchMore,
containerProps,
@ -14,6 +39,9 @@ const ListVirtualized = ({
fetchMore,
children
}) => {
// STYLES
const classes = useStyles()
// OBSERVER
const loaderRef = React.useRef()
const { isNearScreen } = useNearScreen({
@ -39,11 +67,10 @@ const ListVirtualized = ({
}, [isNearScreen, canFetchMore, debounceHandleNextPage])
return (
<Box ref={parentRef} height={1} overflow='auto'>
<Box ref={parentRef} className={classes.root}>
<Box {...containerProps}
height={`${rowVirtualizer.totalSize}px`}
width={1}
position='relative'
className={classes.container}
height={rowVirtualizer.totalSize}
>
{children(rowVirtualizer.virtualItems)}
</Box>
@ -52,7 +79,7 @@ const ListVirtualized = ({
<LinearProgress
ref={loaderRef}
color='secondary'
style={{ width: '100%', marginTop: 10 }}
className={classes.loading}
/>
)}
</Box>

View File

@ -107,15 +107,8 @@ export default makeStyles(theme => ({
backgroundClip: 'content-box',
border: '4px solid transparent',
borderRadius: 7,
boxShadow: 'inset 0 0 0 10px'
},
'&::-webkit-scrollbar-button': {
width: 0,
height: 0,
display: 'none'
},
'&::-webkit-scrollbar-corner': {
backgroundColor: 'transparent'
boxShadow: 'inset 0 0 0 10px',
color: theme.palette.secondary.light
}
},
list: {

View File

@ -14,7 +14,7 @@ const BorderLinearProgress = withStyles(({ palette }) => ({
},
bar: {
borderRadius: 5,
backgroundColor: palette.primary.main
backgroundColor: palette.secondary.main
}
}))(LinearProgress)

View File

@ -0,0 +1,57 @@
import * as React from 'react'
import { LinearProgressWithLabel } from 'client/components/Status'
import * as DatastoreModel from 'client/models/Datastore'
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'
},
{ Header: 'Name', accessor: 'NAME' },
{
Header: 'State',
id: 'STATE',
accessor: row => DatastoreModel.getState(row)?.name
},
{
Header: 'Type',
id: 'TYPE',
accessor: row => DatastoreModel.getType(row)?.name
},
{
Header: 'Owner/Group',
accessor: row => `${row.UNAME}/${row.GNAME}`
},
{ Header: 'Cluster', accessor: 'CLUSTER' },
{
Header: 'Allocated CPU',
accessor: row => {
const { percentOfUsed, percentLabel } = DatastoreModel.getCapacityInfo(row)
return (
<div style={{ paddingRight: '1em' }}>
<LinearProgressWithLabel value={percentOfUsed} label={percentLabel} />
</div>
)
}
}
]

View File

@ -0,0 +1,53 @@
import React, { useEffect, useState } from 'react'
import { useAuth } from 'client/features/Auth'
import { useFetch } from 'client/hooks'
import { useDatastore, useDatastoreApi } from 'client/features/One'
import { VirtualizedTable } from 'client/components/Tables'
import Columns from 'client/components/Tables/Datastores/columns'
const INITIAL_ELEMENT = 0
const NUMBER_OF_INTERVAL = 20
const DatastoresTable = () => {
const [{ start, end }, setPage] = useState({
start: INITIAL_ELEMENT,
end: -NUMBER_OF_INTERVAL
})
const columns = React.useMemo(() => Columns, [])
const datastores = useDatastore()
const { getDatastores } = useDatastoreApi()
const { filterPool } = useAuth()
const { data, fetchRequest, loading, reloading, error } = useFetch(getDatastores)
useEffect(() => { fetchRequest({ start, end }) }, [filterPool])
const fetchMore = () => {
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 canFetchMore = error || data?.datastores?.length < NUMBER_OF_INTERVAL
return (
<VirtualizedTable
columns={columns}
data={datastores}
isLoading={loading || reloading}
canFetchMore={canFetchMore}
fetchMore={fetchMore}
/>
)
}
export default DatastoresTable

View File

@ -0,0 +1,66 @@
import * as React from 'react'
import { LinearProgressWithLabel } from 'client/components/Status'
import * as HostModel from 'client/models/Host'
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'
},
{ Header: 'Name', accessor: 'NAME' },
{
Header: 'State',
id: 'STATE',
accessor: row => HostModel.getState(row)?.name
},
{
Header: 'IM/VM',
accessor: ({ IM_MAD, VM_MAD }) =>
IM_MAD === VM_MAD ? IM_MAD : `${IM_MAD}/${VM_MAD}`
},
{ Header: 'Cluster', accessor: 'CLUSTER' },
{ Header: 'RVMs', accessor: 'HOST_SHARE.RUNNING_VMS' },
{
Header: 'Allocated CPU',
accessor: row => {
const { percentCpuUsed, percentCpuLabel } = HostModel.getAllocatedInfo(row)
return (
<div style={{ paddingRight: '1em' }}>
<LinearProgressWithLabel value={percentCpuUsed} label={percentCpuLabel} />
</div>
)
}
},
{
Header: 'Allocated MEM',
accessor: row => {
const { percentMemUsed, percentMemLabel } = HostModel.getAllocatedInfo(row)
return (
<div style={{ paddingRight: '1em' }}>
<LinearProgressWithLabel value={percentMemUsed} label={percentMemLabel} />
</div>
)
}
}
]

View File

@ -0,0 +1,53 @@
import React, { useEffect, useState } from 'react'
import { useAuth } from 'client/features/Auth'
import { useFetch } from 'client/hooks'
import { useHost, useHostApi } from 'client/features/One'
import { VirtualizedTable } from 'client/components/Tables'
import Columns from 'client/components/Tables/Hosts/columns'
const INITIAL_ELEMENT = 0
const NUMBER_OF_INTERVAL = 20
const HostsTable = () => {
const [{ start, end }, setPage] = useState({
start: INITIAL_ELEMENT,
end: -NUMBER_OF_INTERVAL
})
const columns = React.useMemo(() => Columns, [])
const hosts = useHost()
const { getHosts } = useHostApi()
const { filterPool } = useAuth()
const { data, fetchRequest, loading, reloading, error } = useFetch(getHosts)
useEffect(() => { fetchRequest({ start, end }) }, [filterPool])
const fetchMore = () => {
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 canFetchMore = error || data?.hosts?.length < NUMBER_OF_INTERVAL
return (
<VirtualizedTable
columns={columns}
data={hosts}
isLoading={loading || reloading}
canFetchMore={canFetchMore}
fetchMore={fetchMore}
/>
)
}
export default HostsTable

View File

@ -15,9 +15,15 @@ const useStyles = makeStyles(theme => ({
textAlign: 'start',
padding: '1em',
color: '#4A5568',
backgroundColor: '#e6e8f7',
borderBlock: '0.5px solid #EDF2F7'
color: theme.palette.text.primary,
backgroundColor: theme.palette.action.hover,
border: `1px solid ${theme.palette.action.disabledBackground}`,
borderBottom: 'transparent',
borderLeftWidth: 1,
borderRightWidth: 1,
borderTopLeftRadius: 6,
borderTopRightRadius: 6
}
}))

View File

@ -0,0 +1,122 @@
/* eslint-disable react/prop-types */
/* eslint-disable react/jsx-key */
import * as React from 'react'
import { makeStyles, Box, CircularProgress } from '@material-ui/core'
import {
useTable,
useGlobalFilter,
useRowSelect,
useFlexLayout
} from 'react-table'
import { ListVirtualized } from 'client/components/List'
import Toolbar from 'client/components/Tables/Virtualized/toolbar'
import Header from 'client/components/Tables/Virtualized/header'
import Row from 'client/components/Tables/Virtualized/row'
const useStyles = makeStyles(theme => ({
root: {
height: '100%',
display: 'flex',
flexDirection: 'column'
},
table: {
height: '100%',
overflow: 'hidden',
borderTop: 0,
border: `1px solid ${theme.palette.action.disabledBackground}`,
borderRadius: '0 0 6px 6px',
'& *[role=row]': {
fontSize: '1em',
fontWeight: theme.typography.fontWeightMedium,
lineHeight: '1rem',
overflowWrap: 'break-word',
textAlign: 'start',
padding: '1em',
alignItems: 'center',
color: theme.palette.text.primary,
borderTop: `1px solid ${theme.palette.action.disabledBackground}`,
'&:hover': {
backgroundColor: theme.palette.action.hover
},
'&:first-of-type': {
borderTopColor: 'transparent'
}
}
},
header: {
...theme.typography.body1,
color: theme.palette.text.hint,
marginBottom: 16,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: '1em'
},
total: {
display: 'flex',
alignItems: 'center',
gap: '1em',
transition: 200
}
}))
const DefaultCell = React.memo(({ value }) => value ?? '--')
DefaultCell.displayName = 'DefaultCell'
const VirtualizedTable = ({ data, columns, isLoading, canFetchMore, fetchMore }) => {
const classes = useStyles()
const defaultColumn = React.useMemo(() => ({
// Filter: DefaultFilter,
Cell: DefaultCell
}), [])
const useTableProps = useTable(
{ columns, data, defaultColumn },
useRowSelect,
useFlexLayout,
useGlobalFilter
)
const { getTableProps, getTableBodyProps, rows } = useTableProps
return (
<Box {...getTableProps()} className={classes.root}>
<div className={classes.header}>
<Toolbar useTableProps={useTableProps} />
<div className={classes.total}>
{isLoading && <CircularProgress size='1em' color='secondary' />}
Total loaded: {useTableProps.rows.length}
</div>
</div>
<Header useTableProps={useTableProps} />
<div className={classes.table}>
<ListVirtualized
containerProps={{ ...getTableBodyProps() }}
canFetchMore={canFetchMore}
data={rows}
isLoading={isLoading}
fetchMore={fetchMore}
>
{virtualItems => virtualItems?.map(virtualRow => (
<Row key={virtualRow.index}
virtualRow={virtualRow}
useTableProps={useTableProps}
/>
))
}
</ListVirtualized>
</div>
</Box>
)
}
export default VirtualizedTable

View File

@ -4,24 +4,13 @@ import PropTypes from 'prop-types'
import { makeStyles, Box } from '@material-ui/core'
import clsx from 'clsx'
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles(() => ({
root: {
// <-- it's needed to virtualize -->
position: 'absolute',
top: 0,
left: 0,
width: '100%',
fontSize: '1em',
fontWeight: theme.typography.fontWeightMedium,
lineHeight: '1rem',
overflowWrap: 'break-word',
textAlign: 'start',
padding: '1em',
alignItems: 'center',
boxShadow: '0 0 0 0.5px #e6e8f7'
width: '100%'
},
virtual: ({ size, start }) => ({
height: size,

View File

@ -1,18 +1,15 @@
import * as React from 'react'
import PropTypes from 'prop-types'
import { makeStyles, Toolbar as MToolbar, Button } from '@material-ui/core'
import { makeStyles, Button } from '@material-ui/core'
import { Filter as FilterIcon } from 'iconoir-react'
import GlobalFilter from 'client/components/Table/Filters/GlobalFilter'
// import GlobalFilter from 'client/components/Table/Filters/GlobalFilter'
const useToolbarStyles = makeStyles(theme => ({
root: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(1)
},
filterWrapper: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
gap: '1em'
},
@ -20,10 +17,6 @@ const useToolbarStyles = makeStyles(theme => ({
...theme.typography.body1,
fontWeight: theme.typography.fontWeightBold,
textTransform: 'none'
},
filters: {
...theme.typography.body1,
color: theme.palette.grey[700]
}
}))
@ -31,39 +24,35 @@ const Toolbar = ({ useTableProps }) => {
const classes = useToolbarStyles()
/** @type {import('react-table').UseGlobalFiltersInstanceProps} */
const { preGlobalFilteredRows, setGlobalFilter, state } = useTableProps
// const { preGlobalFilteredRows, setGlobalFilter, state } = useTableProps
// const { selectedRowIds, globalFilter } = state
// const numSelected = Object.keys(selectedRowIds).length
return (
<MToolbar className={classes.root}>
<div className={classes.filterWrapper}>
<Button
variant='outlined'
startIcon={<FilterIcon size='1rem' />}
className={classes.filterButton}
>
<div className={classes.filterWrapper}>
<Button
variant='outlined'
startIcon={<FilterIcon size='1rem' />}
className={classes.filterButton}
>
Filters
</Button>
<span className={classes.filters}>
No filters selected
</span>
</div>
</Button>
<span>No filters selected</span>
</div>
)
{/* numSelected > 0 && (
/* numSelected > 0 && (
<Typography className={classes.title} color='inherit' variant='subtitle1'>
{numSelected} selected
</Typography>
) */}
) */
{/* <GlobalFilter
/* <GlobalFilter
preGlobalFilteredRows={preGlobalFilteredRows}
globalFilter={globalFilter}
setGlobalFilter={setGlobalFilter}
/> */}
</MToolbar>
)
/> */
}
Toolbar.propTypes = {

View File

@ -1,226 +0,0 @@
/* 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

@ -1,81 +0,0 @@
/* eslint-disable react/prop-types */
/* eslint-disable react/jsx-key */
import * as React from 'react'
import { Box, CircularProgress } from '@material-ui/core'
import {
useTable,
useGlobalFilter,
useRowSelect,
useFlexLayout
} from 'react-table'
import { ListVirtualized } from 'client/components/List'
import Toolbar from 'client/components/Tables/VmTable/toolbar'
import Header from 'client/components/Tables/VmTable/header'
import Row from 'client/components/Tables/VmTable/row'
import Columns from 'client/components/Tables/VmTable/columns'
const VmTable = ({ data, isLoading, canFetchMore, fetchMore }) => {
const columns = React.useMemo(() => Columns, [])
const defaultColumn = React.useMemo(() => ({
// Filter: DefaultFilter,
Cell: React.memo(({ value }) => value ?? '--')
}), [])
const useTableProps = useTable(
{ columns, data, defaultColumn },
useRowSelect,
useFlexLayout,
useGlobalFilter
)
const { getTableProps, getTableBodyProps, rows } = useTableProps
return (
<Box {...getTableProps()}
height={1}
display='flex'
flexDirection='column'
border='1px solid #d7dde3'
borderRadius={5}
>
<Toolbar useTableProps={useTableProps} />
<Header useTableProps={useTableProps} />
<ListVirtualized
containerProps={{ ...getTableBodyProps() }}
canFetchMore={canFetchMore}
data={rows}
isLoading={isLoading}
fetchMore={fetchMore}
>
{virtualItems => virtualItems?.map(virtualRow => (
<Row key={virtualRow.index}
virtualRow={virtualRow}
useTableProps={useTableProps}
/>
))
}
</ListVirtualized>
<div
style={{
padding: '1em',
display: 'flex',
alignItems: 'center',
gap: '1em',
color: '#4a5568',
backgroundColor: '#e6e8f7',
borderBlock: '0.5px solid #eef2f7'
}}
>
<span>Total loaded: {useTableProps.rows.length}</span>
{isLoading && <CircularProgress size='1em' />}
</div>
</Box>
)
}
export default VmTable

View File

@ -1,9 +1,3 @@
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 [
@ -27,31 +21,33 @@ export default [
)
}, */
{
Header: '#',
accessor: 'ID',
Cell: ({ value }) =>
<StatusChip stateColor={Colors.debug.light} text={`#${value}`} />
Header: '#', accessor: 'ID'
// Cell: ({ value }) =>
// <StatusChip stateColor={Colors.debug.light} text={`#${value}`} />
},
{ Header: 'Name', accessor: 'NAME' },
{
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)
accessor: row => VirtualMachineModel.getState(row)?.name
// 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: 'Owner/Group',
accessor: row => `${row.UNAME}/${row.GNAME}`
},
{ Header: 'Owner/Group', accessor: row => `${row.UNAME}/${row.GNAME}` },
{
Header: 'Ips',
accessor: row => VirtualMachineModel.getIps(row),
Cell: ({ value }) => value.map(nic => (
<StatusChip key={nic} stateColor={Colors.debug.light} text={nic} />
))
accessor: row => VirtualMachineModel.getIps(row).join(',')
// Cell: ({ value }) => value.map(nic => (
// <StatusChip key={nic} stateColor={Colors.debug.light} text={nic} />
// ))
}
]

View File

@ -1,22 +1,28 @@
import React, { useEffect, useState } from 'react'
import { useAuth } from 'client/features/Auth'
import { useVm, useVmApi } from 'client/features/One'
import { useFetch } from 'client/hooks'
import { useVm, useVmApi } from 'client/features/One'
import { VmTable } from 'client/components/Tables'
import { VirtualizedTable } from 'client/components/Tables'
import Columns from 'client/components/Tables/Vms/columns'
const INITIAL_ELEMENT = 0
const NUMBER_OF_INTERVAL = -100
const NUMBER_OF_INTERVAL = 20
function VirtualMachines () {
const [{ start, end }, setPage] = useState(({ start: INITIAL_ELEMENT, end: -NUMBER_OF_INTERVAL }))
const VmsTable = () => {
const [{ start, end }, setPage] = useState({
start: INITIAL_ELEMENT,
end: -NUMBER_OF_INTERVAL
})
const columns = React.useMemo(() => Columns, [])
const vms = useVm()
const { getVms } = useVmApi()
const { filterPool } = useAuth()
const { data, fetchRequest, loading, reloading } = useFetch(getVms)
const { data, fetchRequest, loading, reloading, error } = useFetch(getVms)
useEffect(() => { fetchRequest({ start, end }) }, [filterPool])
@ -31,16 +37,17 @@ function VirtualMachines () {
})
}
const finish = data?.length < NUMBER_OF_INTERVAL
const canFetchMore = error || data?.vms?.length < NUMBER_OF_INTERVAL
return (
<VmTable
<VirtualizedTable
columns={columns}
data={vms}
isLoading={loading || reloading}
finish={finish}
canFetchMore={canFetchMore}
fetchMore={fetchMore}
/>
)
}
export default VirtualMachines
export default VmsTable

View File

@ -1,5 +1,11 @@
import VmTable from 'client/components/Tables/VmTable'
import VirtualizedTable from 'client/components/Tables/Virtualized'
import VmsTable from 'client/components/Tables/Vms'
import DatastoresTable from 'client/components/Tables/Datastores'
import HostsTable from 'client/components/Tables/Hosts'
export {
VmTable
VirtualizedTable,
VmsTable,
DatastoresTable,
HostsTable
}

View File

@ -1,7 +1,7 @@
import * as STATES from 'client/constants/states'
import COLOR from 'client/constants/color'
const DATASTORE_TYPES = [
export const DATASTORE_TYPES = [
{
name: 'IMAGE',
shortName: 'img'
@ -16,7 +16,7 @@ const DATASTORE_TYPES = [
}
]
const DATASTORE_STATES = [
export const DATASTORE_STATES = [
{
name: STATES.READY,
shortName: 'on',
@ -28,8 +28,3 @@ const DATASTORE_STATES = [
color: COLOR.error.dark
}
]
export default {
TYPES: DATASTORE_TYPES,
STATES: DATASTORE_STATES
}

View File

@ -1,7 +1,7 @@
import * as STATES from 'client/constants/states'
import COLOR from 'client/constants/color'
const HOST_STATES = [
export const HOST_STATES = [
{
name: STATES.INIT,
shortName: 'init',
@ -48,7 +48,3 @@ const HOST_STATES = [
color: COLOR.error.dark
}
]
export default {
STATES: HOST_STATES
}

View File

@ -82,3 +82,5 @@ export * as STATES from 'client/constants/states'
export * from 'client/constants/flow'
export * from 'client/constants/provision'
export * from 'client/constants/vm'
export * from 'client/constants/host'
export * from 'client/constants/datastore'

View File

@ -0,0 +1,89 @@
import * as React from 'react'
import { useParams, useHistory } from 'react-router'
import { Redirect, Route, Switch, Link } from 'react-router-dom'
import { withStyles, Container, Tabs, Tab, Box } from '@material-ui/core'
import {
DatastoresTable,
HostsTable,
VmsTable
} from 'client/components/Tables'
import { PATH } from 'client/router/dev'
const TABS = {
vms: PATH.NEWSTONE.replace(':resource', 'vms'),
datastores: PATH.NEWSTONE.replace(':resource', 'datastores'),
hosts: PATH.NEWSTONE.replace(':resource', 'hosts')
}
const AntTabs = withStyles(theme => ({
root: {
borderBottom: '1px solid #e8e8e8'
},
indicator: {
backgroundColor: theme.palette.secondary.main
}
}))(Tabs)
const AntTab = withStyles(theme => ({
root: {
minWidth: 72,
fontWeight: theme.typography.fontWeightRegular,
marginRight: theme.spacing(4),
'&:hover': {
color: theme.palette.secondary.light,
opacity: 1
},
'&$selected': {
color: theme.palette.secondary.main,
fontWeight: theme.typography.fontWeightMedium
},
'&:focus': {
color: theme.palette.secondary.light
}
},
selected: {}
}))(props => <Tab disableRipple {...props} />)
const Newstone = () => {
const history = useHistory()
const { resource } = useParams()
const renderTabs = React.useMemo(() => (
<AntTabs
value={resource}
variant='scrollable'
scrollButtons='auto'
>
{Object.keys(TABS).map(tabName =>
<AntTab
key={`tab-${tabName}`}
label={tabName}
value={tabName}
component={Link}
to={tabName}
/>
)}
</AntTabs>
), [resource])
return (
<Container disableGutters>
{Object.values(TABS).includes(history.location.pathname) && renderTabs}
<Box py={2}>
<Switch>
<Route path={TABS.vms} component={VmsTable} />
<Route path={TABS.datastores} component={DatastoresTable} />
<Route path={TABS.hosts} component={HostsTable} />
<Route component={() => <Redirect to={TABS.vms} />} />
</Switch>
</Box>
</Container>
)
}
export default Newstone

View File

@ -1,159 +0,0 @@
import React, { memo, useState } from 'react'
import PropTypes from 'prop-types'
import { List, ListItem, Typography, Grid, Paper, Divider } from '@material-ui/core'
import { CheckBox, CheckBoxOutlineBlank, Visibility } from '@material-ui/icons'
import clsx from 'clsx'
import { useProviderApi } from 'client/features/One'
import { Action } from 'client/components/Cards/SelectCard'
import { Tr } from 'client/components/HOC'
import { T } from 'client/constants'
import useStyles from 'client/containers/Providers/Sections/styles'
const Info = memo(({ data }) => {
const classes = useStyles()
const { getProviderConnection } = useProviderApi()
const [showConnection, setShowConnection] = useState(undefined)
const { ID, NAME, GNAME, UNAME, PERMISSIONS, TEMPLATE } = data
const {
connection,
description,
provider: providerName,
registration_time: time
} = TEMPLATE?.PROVISION_BODY
const hasConnection = connection && Object.keys(connection).length > 0
const isChecked = checked =>
checked === '1' ? <CheckBox /> : <CheckBoxOutlineBlank />
const ConnectionButton = () => (
<Action
icon={<Visibility />}
cy='provider-connection'
handleClick={() => getProviderConnection(ID).then(setShowConnection)}
/>
)
return (
<Grid container spacing={1}>
<Grid item xs={12} md={6}>
<Paper variant="outlined" className={classes.marginBottom}>
<List className={clsx(classes.list, 'w-50')}>
<ListItem className={classes.title}>
<Typography>{Tr(T.Information)}</Typography>
</ListItem>
<Divider />
<ListItem>
<Typography>{'ID'}</Typography>
<Typography>{ID}</Typography>
</ListItem>
<ListItem>
<Typography>{Tr(T.Name)}</Typography>
<Typography data-cy="provider-name">{NAME}</Typography>
</ListItem>
<ListItem>
<Typography>{Tr(T.Description)}</Typography>
<Typography data-cy="provider-description" noWrap>{description}</Typography>
</ListItem>
<ListItem>
<Typography>{Tr(T.Provider)}</Typography>
<Typography data-cy="provider-type">{providerName}</Typography>
</ListItem>
<ListItem>
<Typography>{Tr(T.RegistrationTime)}</Typography>
<Typography>
{new Date(time * 1000).toLocaleString()}
</Typography>
</ListItem>
</List>
</Paper>
{hasConnection && (
<Paper variant="outlined">
<List className={clsx(classes.list, 'w-50')}>
<ListItem className={classes.title}>
<Typography>{Tr(T.Credentials)}</Typography>
<span className={classes.alignToRight}>
{!showConnection && <ConnectionButton />}
</span>
</ListItem>
<Divider />
{Object.entries(connection)?.map(([key, value]) =>
typeof value === 'string' && (
<ListItem key={key}>
<Typography>{key}</Typography>
<Typography data-cy={`provider-${key}`}>
{showConnection?.[key] ?? value}
</Typography>
</ListItem>
))}
</List>
</Paper>
)}
</Grid>
<Grid item xs={12} md={6}>
<Paper variant="outlined" className={classes.marginBottom}>
<List className={clsx(classes.list, 'w-25')}>
<ListItem className={classes.title}>
<Typography>{Tr(T.Permissions)}</Typography>
<Typography>{Tr(T.Use)}</Typography>
<Typography>{Tr(T.Manage)}</Typography>
<Typography>{Tr(T.Admin)}</Typography>
</ListItem>
<Divider />
<ListItem>
<Typography>{Tr(T.Owner)}</Typography>
<Typography>{isChecked(PERMISSIONS.OWNER_U)}</Typography>
<Typography>{isChecked(PERMISSIONS.OWNER_M)}</Typography>
<Typography>{isChecked(PERMISSIONS.OWNER_A)}</Typography>
</ListItem>
<ListItem>
<Typography>{Tr(T.Group)}</Typography>
<Typography>{isChecked(PERMISSIONS.GROUP_U)}</Typography>
<Typography>{isChecked(PERMISSIONS.GROUP_M)}</Typography>
<Typography>{isChecked(PERMISSIONS.GROUP_A)}</Typography>
</ListItem>
<ListItem>
<Typography>{Tr(T.Other)}</Typography>
<Typography>{isChecked(PERMISSIONS.OTHER_U)}</Typography>
<Typography>{isChecked(PERMISSIONS.OTHER_M)}</Typography>
<Typography>{isChecked(PERMISSIONS.OTHER_A)}</Typography>
</ListItem>
</List>
</Paper>
<Paper variant="outlined">
<List className={clsx(classes.list, 'w-50')}>
<ListItem className={classes.title}>
<Typography>{Tr(T.Ownership)}</Typography>
</ListItem>
<Divider />
<ListItem>
<Typography>{Tr(T.Owner)}</Typography>
<Typography>{UNAME}</Typography>
</ListItem>
<ListItem>
<Typography>{Tr(T.Group)}</Typography>
<Typography>{GNAME}</Typography>
</ListItem>
</List>
</Paper>
</Grid>
</Grid>
)
})
Info.propTypes = {
data: PropTypes.object.isRequired
}
Info.defaultProps = {
data: {}
}
Info.displayName = 'Info'
export default Info

View File

@ -1,32 +0,0 @@
import { makeStyles } from '@material-ui/core'
export default makeStyles(theme => ({
marginBottom: {
marginBottom: theme.spacing(2)
},
list: {
'& p': {
...theme.typography.body2,
overflow: 'hidden',
textOverflow: 'ellipsis'
},
'&.w-50 > *': {
'& > p, & > span': {
width: '50%'
}
},
'&.w-25 > *': {
'& > p, & > span': {
width: '25%'
}
}
},
title: {
'& p.bold': {
fontWeight: theme.typography.fontWeightBold
}
},
alignToRight: {
textAlign: 'right'
}
}))

View File

@ -4,7 +4,7 @@ import { datastoreService } from 'client/features/One/datastore/services'
export const getDatastore = createAction('datastore', datastoreService.getDatastore)
export const getDatastores = createAction(
'datastore',
'datastore/pool',
datastoreService.getDatastores,
response => ({ datastores: response })
)

View File

@ -18,6 +18,6 @@ export const useDatastoreApi = () => {
return {
getDatastore: id => unwrapDispatch(actions.getDatastore({ id })),
getDatastores: () => unwrapDispatch(actions.getDatastores())
getDatastores: options => unwrapDispatch(actions.getDatastores(options))
}
}

View File

@ -18,6 +18,6 @@ export const useHostApi = () => {
return {
getHost: id => unwrapDispatch(actions.getHost({ id })),
getHosts: () => unwrapDispatch(actions.getHosts())
getHosts: options => unwrapDispatch(actions.getHosts(options))
}
}

View File

@ -1,6 +1,7 @@
import { useState, useCallback, useEffect, useRef } from 'react'
import { debounce } from '@material-ui/core'
import { fakeDelay } from 'client/utils'
import { console } from 'window-or-global'
const useRequest = request => {
const [data, setData] = useState(undefined)
@ -22,7 +23,8 @@ const useRequest = request => {
} else setError(true)
}
})
.catch(() => {
.catch(e => {
console.log('error', e)
if (isMounted.current) {
setData(undefined)
setError(true)

View File

@ -0,0 +1,15 @@
import { prettyBytes } from 'client/utils'
import { DATASTORE_STATES, DATASTORE_TYPES } from 'client/constants'
export const getType = ({ TYPE } = {}) => DATASTORE_TYPES[TYPE]
export const getState = ({ STATE } = {}) => DATASTORE_STATES[STATE]
export const getCapacityInfo = ({ TOTAL_MB, USED_MB } = {}) => {
const percentOfUsed = +USED_MB * 100 / +TOTAL_MB || 0
const usedBytes = prettyBytes(+USED_MB, 'MB')
const totalBytes = prettyBytes(+TOTAL_MB, 'MB')
const percentLabel = `${usedBytes} / ${totalBytes} (${Math.round(percentOfUsed)}%)`
return { percentOfUsed, percentLabel }
}

View File

@ -0,0 +1,23 @@
import { prettyBytes } from 'client/utils'
import { HOST_STATES } from 'client/constants'
export const getState = ({ STATE } = {}) => HOST_STATES[STATE]
export const getAllocatedInfo = ({ HOST_SHARE } = {}) => {
const { CPU_USAGE, TOTAL_CPU, MEM_USAGE, TOTAL_MEM } = HOST_SHARE
const percentCpuUsed = +CPU_USAGE * 100 / +TOTAL_CPU || 0
const percentCpuLabel = `${CPU_USAGE} / ${TOTAL_CPU} (${Math.round(percentCpuUsed)}%)`
const percentMemUsed = +MEM_USAGE * 100 / +TOTAL_MEM || 0
const usedMemBytes = prettyBytes(+MEM_USAGE)
const totalMemBytes = prettyBytes(+TOTAL_MEM)
const percentMemLabel = `${usedMemBytes} / ${totalMemBytes} (${Math.round(percentMemUsed)}%)`
return {
percentCpuUsed,
percentCpuLabel,
percentMemUsed,
percentMemLabel
}
}

View File

@ -1,27 +1,26 @@
import loadable from '@loadable/component'
import {
Code as DevIcon,
ViewGrid as VmIcon
ViewGrid as NewstoneIcon
} from 'iconoir-react'
const VirtualMachines = loadable(() => import('client/containers/VirtualMachines'), { ssr: false })
const Newstone = loadable(() => import('client/containers/Newstone'), { ssr: false })
const TestApi = loadable(() => import('client/containers/TestApi'), { ssr: false })
const WebConsole = loadable(() => import('client/containers/WebConsole'), { ssr: false })
export const PATH = {
VIRTUAL_MACHINES: '/vms',
NEWSTONE: '/newstone/:resource',
TEST_API: '/test-api',
WEB_CONSOLE: '/webconsole'
}
export const ENDPOINTS = [
{
label: 'VMs',
path: PATH.VIRTUAL_MACHINES,
devMode: true,
label: 'Newstone',
path: PATH.NEWSTONE,
sidebar: true,
icon: VmIcon,
Component: VirtualMachines
icon: NewstoneIcon,
Component: Newstone
},
{
label: 'Test API',

View File

@ -34,11 +34,17 @@ export default {
},
typography: {
fontFamily: [
'"Ubuntu"',
'Ubuntu',
'-apple-system',
'BlinkMacSystemFont',
'"Segoe UI"',
'Roboto',
'Helvetica',
'"Helvetica Neue"',
'Arial',
'sans-serif'
'sans-serif',
'"Apple Color Emoji"',
'"Segoe UI Emoji"',
'"Segoe UI Symbol"'
].join(',')
},
mixins: {