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

M ~#: Add vms, datastores & hosts tables

This commit is contained in:
Sergio Betanzos 2021-06-16 15:33:49 +02:00
parent daadc63f20
commit fc7ca2830b
No known key found for this signature in database
GPG Key ID: E3E704F097737136
21 changed files with 519 additions and 180 deletions

View File

@ -37,7 +37,7 @@ const Group = () => {
}}
>
{NAME}
{isSelected && <SelectIcon size='1rem' />}
{isSelected && <SelectIcon size='1em' />}
</Button>
)
}

View File

@ -7,7 +7,8 @@ const useStyles = makeStyles(theme => ({
badge: {
backgroundColor: ({ stateColor }) => stateColor,
color: ({ stateColor }) => stateColor,
boxShadow: `0 0 0 2px ${theme.palette.background.paper}`,
transform: ({ customTransform }) => customTransform,
boxShadow: '0 0 0 2px transparent',
'&::after': {
position: 'absolute',
top: 0,
@ -32,26 +33,25 @@ const useStyles = makeStyles(theme => ({
}
}))
const StatusBadge = memo(({ stateColor, children, ...props }) => {
const classes = useStyles({ stateColor })
const StatusBadge = memo(({ stateColor, children, customTransform, ...props }) => {
const classes = useStyles({ stateColor, customTransform })
return (
<Badge
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
classes={{ badge: classes.badge }}
overlap="circle"
variant="dot"
overlap='circle'
variant='dot'
{...props}
>
{children}
</Badge>
)
},
(prev, next) => prev.stateColor === next.stateColor
)
}, (prev, next) => prev.stateColor === next.stateColor)
StatusBadge.propTypes = {
stateColor: PropTypes.string,
customTransform: PropTypes.string,
children: PropTypes.oneOfType([
PropTypes.string,
PropTypes.node
@ -60,6 +60,7 @@ StatusBadge.propTypes = {
StatusBadge.defaultProps = {
stateColor: undefined,
customTransform: undefined,
children: ''
}

View File

@ -10,17 +10,17 @@ const BorderLinearProgress = withStyles(({ palette }) => ({
borderRadius: 5
},
colorPrimary: {
backgroundColor: palette.grey[palette.type === 'light' ? 200 : 700]
backgroundColor: palette.grey[palette.type === 'light' ? 400 : 700]
},
bar: {
borderRadius: 5,
backgroundColor: palette.secondary.main
backgroundColor: palette.primary.main
}
}))(LinearProgress)
const LinearProgressWithLabel = memo(({ value, label }) => (
<div style={{ textAlign: 'end' }}>
<Typography component='span' variant='body2'>{label}</Typography>
<Typography component='span' variant='body2' noWrap>{label}</Typography>
<BorderLinearProgress variant='determinate' value={value} />
</div>
), (prev, next) => prev.value === next.value && prev.label === next.label)

View File

@ -156,7 +156,8 @@ EnhancedTable.propTypes = {
}
EnhancedTable.defaultProps = {
skipPageReset: false
skipPageReset: false,
filterTypes: []
}
export default EnhancedTable

View File

@ -1,6 +1,6 @@
import * as React from 'react'
import { LinearProgressWithLabel } from 'client/components/Status'
import { LinearProgressWithLabel, StatusBadge } from 'client/components/Status'
import * as DatastoreModel from 'client/models/Datastore'
export default [
@ -24,24 +24,39 @@ export default [
)
}, */
{
Header: '#', accessor: 'ID'
},
{ Header: 'Name', accessor: 'NAME' },
{
Header: 'State',
Header: '',
id: 'STATE',
accessor: row => DatastoreModel.getState(row)?.name
width: 50,
accessor: row => {
const state = DatastoreModel.getState(row)
return (
<StatusBadge
title={state?.name}
stateColor={state?.color}
customTransform='translate(150%, 50%)'
/>
)
}
},
{ Header: '#', accessor: 'ID', width: 45 },
{ Header: 'Name', accessor: 'NAME' },
{
Header: 'Type',
id: 'TYPE',
width: 100,
accessor: row => DatastoreModel.getType(row)?.name
},
{
Header: 'Owner/Group',
accessor: row => `${row.UNAME}/${row.GNAME}`
},
{ Header: 'Cluster', accessor: 'CLUSTER' },
{
Header: 'Clusters',
id: 'CLUSTERS',
width: 100,
accessor: row => [row.CLUSTERS?.ID ?? []].flat().join(',')
},
{
Header: 'Allocated CPU',
accessor: row => {

View File

@ -1,51 +1,30 @@
import React, { useEffect, useState } from 'react'
import React, { useEffect } 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 { EnhancedTable } from 'client/components/Tables'
import { DatastoreCard } from 'client/components/Cards'
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)
const { fetchRequest, loading, reloading } = 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
useEffect(() => { fetchRequest() }, [filterPool])
return (
<VirtualizedTable
<EnhancedTable
columns={columns}
data={datastores}
isLoading={loading || reloading}
canFetchMore={canFetchMore}
fetchMore={fetchMore}
MobileComponentRow={DatastoreCard}
/>
)
}

View File

@ -0,0 +1,228 @@
/* eslint-disable react/prop-types */
import * as React from 'react'
import { makeStyles, Box, CircularProgress, useMediaQuery } from '@material-ui/core'
import {
useGlobalFilter,
usePagination,
useRowSelect,
useTable
} from 'react-table'
import Toolbar from 'client/components/Tables/Virtualized/toolbar'
import Header from 'client/components/Tables/Virtualized/header'
import Row from 'client/components/Tables/Enhanced/row'
import Pagination from 'client/components/Tables/Enhanced/pagination'
import { addOpacityToColor } from 'client/utils'
const useStyles = makeStyles(({ palette, typography, breakpoints }) => ({
root: {
height: '100%',
display: 'flex',
flexDirection: 'column'
},
table: {
height: '100%',
overflow: 'auto',
[breakpoints.up('md')]: {
border: `1px solid ${palette.action.disabledBackground}`,
borderRadius: 6
},
// includes header row
'& *[role=row]': {
padding: '0.8em',
textAlign: 'start',
overflowWrap: 'break-word',
lineHeight: '1rem',
color: palette.text.primary,
[breakpoints.up('md')]: {
display: 'grid',
gridAutoFlow: 'column',
gridTemplateColumns: ({ columnsWidth }) => columnsWidth.join(' ')
}
}
},
header: {
position: 'sticky',
top: 0,
zIndex: 2,
textTransform: 'uppercase',
fontSize: '0.9em',
fontWeight: 700,
letterSpacing: '0.05em',
borderBottom: 'transparent',
backgroundColor: palette.grey[palette.type === 'light' ? 200 : 600],
[breakpoints.down('sm')]: {
display: 'none'
}
},
body: {
[breakpoints.only('sm')]: {
display: 'grid',
gap: '1em',
gridTemplateColumns: '1fr 1fr'
},
[breakpoints.only('xm')]: {
display: 'grid',
gap: '1em',
gridTemplateColumns: '1fr'
},
'& *[role=row]': {
fontSize: '1em',
fontWeight: typography.fontWeightMedium,
borderTop: `1px solid ${palette.action.disabledBackground}`,
backgroundColor: palette.background.paper,
'&:hover': {
backgroundColor: palette.action.hover
},
'&:first-of-type': {
borderTopColor: 'transparent'
},
'&.selected': {
backgroundColor: addOpacityToColor(palette.secondary.main, 0.7)
}
},
'& *[role=cell]': {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}
},
toolbar: {
...typography.body1,
color: 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 EnhancedTable = ({
data,
columns,
pageSize = 10,
isLoading,
showPageCount,
fetchMore,
canFetchMore,
MobileComponentRow
}) => {
const columnsWidth = React.useMemo(() =>
columns.map(({ width = '1fr' }) => {
return Number.isInteger(width) ? `${width}px` : width
}), [])
const isMobile = useMediaQuery(theme => theme.breakpoints.down('sm'))
const classes = useStyles({ columnsWidth })
const defaultColumn = React.useMemo(() => ({
// Filter: DefaultFilter,
Cell: DefaultCell
}), [])
const useTableProps = useTable(
{
columns,
data,
defaultColumn,
autoResetPage: false,
initialState: {
pageSize
}
},
useGlobalFilter,
usePagination,
useRowSelect
)
const {
getTableProps,
prepareRow,
rows,
page,
gotoPage,
pageCount,
state: { pageIndex }
} = useTableProps
const handleChangePage = newPage => {
gotoPage(newPage)
const canNextPage = pageCount === -1 ? page.length >= pageSize : newPage < pageCount - 1
newPage > pageIndex && canFetchMore && !canNextPage && fetchMore()
}
return (
<Box {...getTableProps()} className={classes.root}>
<div className={classes.toolbar}>
<Toolbar useTableProps={useTableProps} />
<div className={classes.total}>
{isLoading && <CircularProgress size='1em' color='secondary' />}
Total loaded: {rows.length}
</div>
</div>
<div className={classes.table}>
<div className={classes.header}>
<Header useTableProps={useTableProps} />
</div>
<div className={classes.body}>
{
page.map(row => {
prepareRow(row)
/** @type {import('react-table').UseRowSelectRowProps} */
const { getRowProps, original, toggleRowSelected, isSelected } = row
const key = getRowProps().key
const handleSelect = () => toggleRowSelected(!isSelected)
return isMobile && MobileComponentRow ? (
<MobileComponentRow
key={key}
value={original}
isSelected={isSelected}
handleClick={handleSelect}
/>
) : (
<Row
key={key}
row={row}
handleClick={handleSelect}
/>
)
})}
</div>
</div>
{page?.length > 0 && (
<Pagination
handleChangePage={handleChangePage}
useTableProps={useTableProps}
count={rows.length}
showPageCount={showPageCount}
/>
)}
</Box>
)
}
export default EnhancedTable

View File

@ -0,0 +1,94 @@
import React from 'react'
import PropTypes from 'prop-types'
import { makeStyles, 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: {
margin: '10px auto'
}
}))
const Pagination = ({
count = 0,
handleChangePage,
useTableProps,
showPageCount = true
}) => {
const classes = useStyles()
/** @type {import('react-table').UsePaginationState} */
const { pageIndex, pageSize } = useTableProps.state
const pageCount = React.useMemo(() => Math.ceil(count / pageSize), [count])
const handleFirstPageButtonClick = () => {
handleChangePage(0)
}
const handleBackButtonClick = () => {
handleChangePage(pageIndex - 1)
}
const handleNextButtonClick = () => {
handleChangePage(pageIndex + 1)
}
const handleLastPageButtonClick = () => {
handleChangePage(Math.max(0, pageCount - 1))
}
return (
<div className={classes.root}>
{/* <IconButton
onClick={handleFirstPageButtonClick}
disabled={pageIndex === 0}
aria-label="first page"
>
<FirstPageIcon />
</IconButton> */}
<IconButton
onClick={handleBackButtonClick}
disabled={pageIndex === 0}
aria-label="previous page"
>
<PreviousPageIcon />
</IconButton>
{showPageCount &&
<span style={{ marginInline: '1em' }}>
{`${pageIndex + 1} / ${pageCount}`}
</span>
}
<IconButton
onClick={handleNextButtonClick}
disabled={pageIndex >= Math.ceil(count / pageSize) - 1}
aria-label="next page"
>
<NextPageIcon />
</IconButton>
{/* <IconButton
onClick={handleLastPageButtonClick}
disabled={pageIndex >= Math.ceil(count / pageSize) - 1}
aria-label="last page"
>
<LastPageIcon />
</IconButton> */}
</div>
)
}
Pagination.propTypes = {
handleChangePage: PropTypes.func.isRequired,
useTableProps: PropTypes.object.isRequired,
count: PropTypes.number.isRequired,
showPageCount: PropTypes.bool
}
export default Pagination

View File

@ -0,0 +1,34 @@
import * as React from 'react'
import PropTypes from 'prop-types'
const Row = ({ row, handleClick }) => {
/** @type {import('react-table').Row} */
const { getRowProps, cells, isSelected } = row
const renderCell = React.useCallback(cell => (
<div {...cell.getCellProps()} data-header={cell.column.Header}>
{cell.render('Cell')}
</div>
), [])
return (
<div {...getRowProps()}
className={isSelected ? 'selected' : ''}
onClick={handleClick}
>
{cells?.map(renderCell)}
</div>
)
}
Row.propTypes = {
row: PropTypes.object,
handleClick: PropTypes.func
}
Row.defaultProps = {
row: {},
handleClick: undefined
}
export default Row

View File

@ -1,6 +1,6 @@
import * as React from 'react'
import { LinearProgressWithLabel } from 'client/components/Status'
import { LinearProgressWithLabel, StatusBadge } from 'client/components/Status'
import * as HostModel from 'client/models/Host'
export default [
@ -24,21 +24,31 @@ export default [
)
}, */
{
Header: '#', accessor: 'ID'
Header: '',
id: 'STATE',
width: 50,
accessor: row => {
const state = HostModel.getState(row)
return (
<StatusBadge
title={state?.name}
stateColor={state?.color}
customTransform='translate(150%, 50%)'
/>
)
}
},
{ Header: '#', accessor: 'ID', width: 45 },
{ Header: 'Name', accessor: 'NAME' },
{
Header: 'State',
id: 'STATE',
accessor: row => HostModel.getState(row)?.name
},
{
Header: 'IM/VM',
width: 100,
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: 'RVMs', accessor: 'HOST_SHARE.RUNNING_VMS', width: 100 },
{
Header: 'Allocated CPU',
accessor: row => {

View File

@ -1,51 +1,30 @@
import React, { useEffect, useState } from 'react'
import React, { useEffect } 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 { EnhancedTable } from 'client/components/Tables'
import { HostCard } from 'client/components/Cards'
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)
const { fetchRequest, loading, reloading } = 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
useEffect(() => { fetchRequest() }, [filterPool])
return (
<VirtualizedTable
<EnhancedTable
columns={columns}
data={hosts}
isLoading={loading || reloading}
canFetchMore={canFetchMore}
fetchMore={fetchMore}
MobileComponentRow={HostCard}
/>
)
}

View File

@ -1,55 +1,23 @@
import * as React from 'react'
import PropTypes from 'prop-types'
import { makeStyles, Box } from '@material-ui/core'
const useStyles = makeStyles(theme => ({
root: {
textTransform: 'uppercase',
fontSize: '0.9em',
fontWeight: 700,
lineHeight: '1rem',
letterSpacing: '0.05em',
overflowWrap: 'break-word',
textAlign: 'start',
padding: '1em',
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
}
}))
const Header = ({ useTableProps }) => {
const classes = useStyles()
/** @type {import('react-table').UseTableInstanceProps} */
const { headerGroups } = useTableProps
const renderHeaderColumn = React.useCallback(column => (
<Box {...column.getHeaderProps()}>
<div {...column.getHeaderProps()}>
{column.render('Header')}
</Box>
</div>
), [])
const renderHeaderGroup = React.useCallback(headerGroup => (
<Box {...headerGroup.getHeaderGroupProps()}>
<div {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(renderHeaderColumn)}
</Box>
</div>
), [])
return (
<Box className={classes.root}>
{headerGroups.map(renderHeaderGroup)}
</Box>
)
return headerGroups.map(renderHeaderGroup)
}
Header.propTypes = {

View File

@ -23,11 +23,11 @@ const useStyles = makeStyles(theme => ({
},
table: {
height: '100%',
overflow: 'hidden',
borderTop: 0,
overflow: 'auto',
border: `1px solid ${theme.palette.action.disabledBackground}`,
borderRadius: '0 0 6px 6px',
borderRadius: 6
},
body: {
'& *[role=row]': {
fontSize: '1em',
fontWeight: theme.typography.fontWeightMedium,
@ -48,7 +48,7 @@ const useStyles = makeStyles(theme => ({
}
}
},
header: {
toolbar: {
...theme.typography.body1,
color: theme.palette.text.hint,
marginBottom: 16,
@ -89,7 +89,7 @@ const VirtualizedTable = ({ data, columns, isLoading, canFetchMore, fetchMore })
return (
<Box {...getTableProps()} className={classes.root}>
<div className={classes.header}>
<div className={classes.toolbar}>
<Toolbar useTableProps={useTableProps} />
<div className={classes.total}>
{isLoading && <CircularProgress size='1em' color='secondary' />}
@ -97,23 +97,26 @@ const VirtualizedTable = ({ data, columns, isLoading, canFetchMore, fetchMore })
</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>
<Header useTableProps={useTableProps} />
<div className={classes.body}>
<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>
</div>
</Box>
)

View File

@ -33,7 +33,7 @@ const Toolbar = ({ useTableProps }) => {
<div className={classes.filterWrapper}>
<Button
variant='outlined'
startIcon={<FilterIcon size='1rem' />}
startIcon={<FilterIcon size='1em' />}
className={classes.filterButton}
>
Filters

View File

@ -1,3 +1,6 @@
import * as React from 'react'
import { StatusBadge } from 'client/components/Status'
import * as VirtualMachineModel from 'client/models/VirtualMachine'
export default [
@ -21,15 +24,20 @@ export default [
)
}, */
{
Header: '#', accessor: 'ID'
// Cell: ({ value }) =>
// <StatusChip stateColor={Colors.debug.light} text={`#${value}`} />
},
{ Header: 'Name', accessor: 'NAME' },
{
Header: 'State',
Header: '',
id: 'STATE',
accessor: row => VirtualMachineModel.getState(row)?.name
width: 50,
accessor: row => {
const state = VirtualMachineModel.getState(row)
return (
<StatusBadge
title={state?.name}
stateColor={state?.color}
customTransform='translate(150%, 50%)'
/>
)
}
// Cell: ({ value: { name, color } = {} }) => name && (
// <StatusChip stateColor={color} text={name} />
// ),
@ -39,6 +47,8 @@ export default [
// filter: (rows, id, filterValue) =>
// rows.filter(row => row.values[id]?.name === filterValue)
},
{ Header: '#', accessor: 'ID', width: 45 },
{ Header: 'Name', accessor: 'NAME' },
{
Header: 'Owner/Group',
accessor: row => `${row.UNAME}/${row.GNAME}`

View File

@ -1,20 +1,18 @@
import React, { useEffect, useState } from 'react'
import React, { useEffect, useState, useCallback } from 'react'
import { useAuth } from 'client/features/Auth'
import { useFetch } from 'client/hooks'
import { useVm, useVmApi } from 'client/features/One'
import { VirtualizedTable } from 'client/components/Tables'
import { EnhancedTable } from 'client/components/Tables'
import { VirtualMachineCard } from 'client/components/Cards'
import Columns from 'client/components/Tables/Vms/columns'
const INITIAL_ELEMENT = 0
const NUMBER_OF_INTERVAL = 20
const NUMBER_OF_INTERVAL = 6
const VmsTable = () => {
const [{ start, end }, setPage] = useState({
start: INITIAL_ELEMENT,
end: -NUMBER_OF_INTERVAL
})
const [[start, end], setPage] = useState([INITIAL_ELEMENT, -NUMBER_OF_INTERVAL])
const columns = React.useMemo(() => Columns, [])
@ -26,26 +24,29 @@ const VmsTable = () => {
useEffect(() => { fetchRequest({ start, end }) }, [filterPool])
const fetchMore = () => {
setPage(prevState => {
const newStart = prevState.start + NUMBER_OF_INTERVAL
const newEnd = prevState.end - NUMBER_OF_INTERVAL
const fetchMore = useCallback(() => {
setPage(([prevStart, prevEnd]) => {
const newStart = prevStart + NUMBER_OF_INTERVAL
const newEnd = prevEnd - NUMBER_OF_INTERVAL
fetchRequest({ start: newStart, end: newEnd })
return { start: newStart, end: newEnd }
return [newStart, newEnd]
})
}
}, [start, end])
const canFetchMore = error || data?.vms?.length < NUMBER_OF_INTERVAL
const canFetchMore = !error && data?.vms?.length % NUMBER_OF_INTERVAL === 0
return (
<VirtualizedTable
<EnhancedTable
columns={columns}
data={vms}
pageSize={NUMBER_OF_INTERVAL / 2}
isLoading={loading || reloading}
showPageCount={false}
canFetchMore={canFetchMore}
fetchMore={fetchMore}
MobileComponentRow={VirtualMachineCard}
/>
)
}

View File

@ -1,11 +1,13 @@
import DatastoresTable from 'client/components/Tables/Datastores'
import EnhancedTable from 'client/components/Tables/Enhanced'
import HostsTable from 'client/components/Tables/Hosts'
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 {
VirtualizedTable,
VmsTable,
DatastoresTable,
HostsTable
EnhancedTable,
HostsTable,
VirtualizedTable,
VmsTable
}

View File

@ -81,7 +81,7 @@ export const GROUP = {
const formatGroups = sortedGroupsById.map(({ ID, NAME }) => {
const markAsPrimary = user?.GID === ID ? (
<span style={{ marginLeft: 16 }}>
<SelectIcon size='1rem' />
<SelectIcon size='1em' />
</span>
) : null

View File

@ -47,6 +47,15 @@ const AntTab = withStyles(theme => ({
selected: {}
}))(props => <Tab disableRipple {...props} />)
const AntContainer = withStyles({
root: {
height: '100%',
display: 'flex',
flexDirection: 'column',
paddingInline: 0
}
})(Container)
const Newstone = () => {
const history = useHistory()
const { resource } = useParams()
@ -70,10 +79,10 @@ const Newstone = () => {
), [resource])
return (
<Container disableGutters>
<AntContainer>
{Object.values(TABS).includes(history.location.pathname) && renderTabs}
<Box py={2}>
<Box py={2} overflow='auto'>
<Switch>
<Route path={TABS.vms} component={VmsTable} />
<Route path={TABS.datastores} component={DatastoresTable} />
@ -82,7 +91,7 @@ const Newstone = () => {
<Route component={() => <Redirect to={TABS.vms} />} />
</Switch>
</Box>
</Container>
</AntContainer>
)
}

View File

@ -6,14 +6,14 @@ import { requestParams, RestClient } from 'client/utils'
export const createAction = (type, service, wrapResult) =>
createAsyncThunk(type, async (payload, { getState, rejectWithValue }) => {
try {
const { filterPool } = getState().auth
const { auth: { filterPool }, one } = getState()
const response = await service({
...payload,
filter: filterPool
})
return wrapResult ? wrapResult(response) : response
return wrapResult ? wrapResult(response, one) : response
} catch (err) {
return rejectWithValue(typeof err === 'string' ? err : err?.response?.data)
}

View File

@ -1,12 +1,17 @@
import { createAction } from 'client/features/One/utils'
import { vmService } from 'client/features/One/vm/services'
import { filterBy } from 'client/utils'
export const getVm = createAction('vm', vmService.getVm)
export const getVms = createAction(
'vm/pool',
vmService.getVms,
response => ({ vms: response })
(response, { vms: currentVms }) => {
const vms = filterBy([...currentVms, ...response], 'ID')
return { vms }
}
)
export const terminateVm = createAction(