NodesListView and ListView components

* Introduces ListView components
* Adds view switcher to NodesToolbar, so user can switch between
  ListView and TableView
* Implements NodesListView which displays basic info about Nodes
  and allows for further implementation of detailed view toggle
* NodesListView defines form which handles Nodes operations,
  those are triggered by NodesToolbarActions which is remotely connected
  to the form.

Change-Id: I1832e6d7a488c91acbf63c652f51e5772817f4ed
This commit is contained in:
Jiri Tomasek 2017-04-26 13:04:25 +02:00
parent 4601a00ec5
commit 69cebd336f
14 changed files with 762 additions and 61 deletions

View File

@ -0,0 +1,8 @@
---
features:
- |
Nodes listing has been reworked, it now provides 2 separate views which
can be changed via toolbar content view switcher. Default nodes listing
is using ListView component instead of table. New nodes listing provides
information about node in more user friendly way. Actions on selected nodes
are triggered using action buttons in Nodes toolbar.

View File

@ -1,11 +1,16 @@
import { defineMessages, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Link } from 'react-router';
import PropTypes from 'prop-types';
import React from 'react';
import { getFilterByName } from '../../selectors/filters';
import { getFilteredNodes, nodesInProgress } from '../../selectors/nodes';
import NodesActions from '../../actions/NodesActions';
import NodesToolbar from './NodesToolbar';
import NodesListForm from './NodesListView/NodesListForm';
import NodesListView from './NodesListView/NodesListView';
import NodesToolbar from './NodesToolbar/NodesToolbar';
import NodesTableView from './NodesTableView';
import RolesActions from '../../actions/RolesActions';
@ -36,12 +41,26 @@ class Nodes extends React.Component {
this.props.fetchRoles(this.props.currentPlanName);
}
renderContentView() {
return this.props.contentView === 'table'
? <NodesTableView />
: <NodesListForm>
<NodesListView
nodes={this.props.nodes}
nodesInProgress={this.props.nodesInProgress}
/>
</NodesListForm>;
}
render() {
return (
<div>
<div className="page-header">
<div className="pull-right">
<a href="" onClick={this.refreshResults.bind(this)}>
<a
className="link btn btn-link"
onClick={this.refreshResults.bind(this)}
>
<span className="pficon pficon-refresh" />&nbsp;
<FormattedMessage {...messages.refreshResults} />
</a>
@ -54,7 +73,7 @@ class Nodes extends React.Component {
<h1><FormattedMessage {...messages.nodes} /></h1>
</div>
<NodesToolbar />
<NodesTableView />
{this.renderContentView()}
{this.props.children}
</div>
);
@ -62,13 +81,22 @@ class Nodes extends React.Component {
}
Nodes.propTypes = {
children: PropTypes.node,
contentView: PropTypes.string.isRequired,
currentPlanName: PropTypes.string.isRequired,
fetchNodes: PropTypes.func.isRequired,
fetchRoles: PropTypes.func.isRequired
fetchRoles: PropTypes.func.isRequired,
nodes: ImmutablePropTypes.map.isRequired,
nodesInProgress: ImmutablePropTypes.set.isRequired
};
const mapStateToProps = state => ({
currentPlanName: state.currentPlan.currentPlanName
contentView: getFilterByName(state, 'nodesToolbar').get(
'contentView',
'list'
),
currentPlanName: state.currentPlan.currentPlanName,
nodes: getFilteredNodes(state),
nodesInProgress: nodesInProgress(state)
});
const mapDispatchToProps = dispatch => ({

View File

@ -0,0 +1,117 @@
import ClassNames from 'classnames';
import { defineMessages, FormattedMessage } from 'react-intl';
import React, { PropTypes } from 'react';
const messages = defineMessages({
provisionState: {
id: 'NodeProvisionState.provisionStateLabel',
defaultMessage: 'Provision State:'
},
maintenance: {
id: 'NodeMaintenanceState.maintenanceWarning',
defaultMessage: 'Maintenance'
},
poweringOn: {
id: 'NodePowerState.poweringOn',
defaultMessage: 'Powering On'
},
poweringOff: {
id: 'NodePowerState.poweringOff',
defaultMessage: 'Powering Off'
},
rebooting: {
id: 'NodePowerState.rebooting',
defaultMessage: 'Rebooting'
},
powerOn: {
id: 'NodePowerState.powerOn',
defaultMessage: 'On'
},
powerOff: {
id: 'NodePowerState.powerOff',
defaultMessage: 'Off'
},
unknownPowerState: {
id: 'NodePowerState.unknownPowerState',
defaultMessage: 'Unknown'
}
});
export const NodeMaintenanceState = ({ maintenance }) => {
if (maintenance) {
return (
<span>
{' | '}
<span className="pficon pficon-warning-triangle-o" />
&nbsp;
<FormattedMessage {...messages.maintenance} />
</span>
);
}
return null;
};
NodeMaintenanceState.propTypes = {
maintenance: PropTypes.bool.isRequired
};
export const NodeProvisionState = ({
provisionState,
targetProvisionState
}) => (
<span>
<strong><FormattedMessage {...messages.provisionState} /></strong>&nbsp;
{targetProvisionState
? <span>
{provisionState}
{' '}
<span className="fa fa-long-arrow-right" />
{' '}
{targetProvisionState}
</span>
: provisionState}
</span>
);
NodeProvisionState.propTypes = {
provisionState: PropTypes.string.isRequired,
targetProvisionState: PropTypes.string
};
export class NodePowerState extends React.Component {
renderPowerState(message) {
const { powerState, targetPowerState } = this.props;
const iconClass = ClassNames({
'fa fa-power-off': true,
'text-warning': targetPowerState,
'text-success': powerState === 'power on',
'text-danger': powerState === 'power off'
});
return (
<span>
<span className={iconClass} title="Power state" />
&nbsp;
<FormattedMessage {...messages[message]} />
</span>
);
}
render() {
const { powerState, targetPowerState } = this.props;
if (targetPowerState === 'power on') {
return this.renderPowerState('poweringOn');
} else if (targetPowerState === 'power off') {
return this.renderPowerState('poweringOff');
} else if (targetPowerState === 'rebooting') {
return this.renderPowerState('rebooting');
} else if (powerState === 'power on') {
return this.renderPowerState('powerOn');
} else if (powerState === 'power off') {
return this.renderPowerState('powerOff');
} else {
return this.renderPowerState('unknownPowerState');
}
}
}
NodePowerState.propTypes = {
powerState: PropTypes.string,
targetPowerState: PropTypes.string
};

View File

@ -0,0 +1,97 @@
import { connect } from 'react-redux';
import { defineMessages, injectIntl } from 'react-intl';
import React, { PropTypes } from 'react';
import { Form, reduxForm } from 'redux-form';
import { pickBy } from 'lodash';
import NodesActions from '../../../actions/NodesActions';
import { nodesInProgress } from '../../../selectors/nodes';
const messages = defineMessages({
selectNodes: {
id: 'NodesListForm.selectNodesValidationMessage',
defaultMessage: 'Please select Nodes first'
},
operationInProgress: {
id: 'NodesListForm.operationInProgressValidationMessage',
defaultMessage: 'There is an operation in progress on some of the selected Nodes'
}
});
class NodesListForm extends React.Component {
handleFormSubmit(formData) {
const nodeIds = Object.keys(pickBy(formData.values, value => !!value));
switch (formData.submitAction) {
case 'introspect':
this.props.introspectNodes(nodeIds);
break;
case 'provide':
this.props.provideNodes(nodeIds);
break;
case 'tag':
this.props.tagNodes(nodeIds, formData.tag);
break;
case 'delete':
this.props.deleteNodes(nodeIds);
break;
default:
break;
}
this.props.reset();
}
render() {
return (
<Form
onSubmit={this.props.handleSubmit(this.handleFormSubmit.bind(this))}
>
{this.props.children}
</Form>
);
}
}
NodesListForm.propTypes = {
children: PropTypes.node,
deleteNodes: PropTypes.func.isRequired,
handleSubmit: PropTypes.func.isRequired,
intl: PropTypes.object,
introspectNodes: PropTypes.func.isRequired,
provideNodes: PropTypes.func.isRequired,
reset: PropTypes.func.isRequired,
tagNodes: PropTypes.func.isRequired
};
const validate = (formData, props) => {
const errors = {};
const nodeIds = Object.keys(pickBy(formData.values, value => !!value));
const selectedInProgress = props.nodesInProgress.intersect(nodeIds).size;
if (nodeIds.length === 0) {
errors._error = props.intl.formatMessage(messages.selectNodes);
} else if (selectedInProgress > 0) {
errors._error = props.intl.formatMessage(messages.operationInProgress);
}
return errors;
};
const form = reduxForm({
form: 'nodesListForm',
validate
});
const mapStateToProps = state => ({
nodesInProgress: nodesInProgress(state)
});
const mapDispatchToProps = dispatch => ({
deleteNodes: nodeIds => dispatch(NodesActions.deleteNodes(nodeIds)),
introspectNodes: nodeIds =>
dispatch(NodesActions.startNodesIntrospection(nodeIds)),
provideNodes: nodeIds => dispatch(NodesActions.startProvideNodes(nodeIds)),
tagNodes: (nodeIds, tag) => dispatch(NodesActions.tagNodes(nodeIds, tag))
});
export default injectIntl(
connect(mapStateToProps, mapDispatchToProps)(form(NodesListForm))
);

View File

@ -0,0 +1,143 @@
import ClassNames from 'classnames';
import { defineMessages, FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import React, { PropTypes } from 'react';
import {
ListView,
ListViewAdditionalInfo,
ListViewAdditionalInfoItem,
ListViewBody,
ListViewCheckbox,
ListViewDescription,
ListViewDescriptionHeading,
ListViewDescriptionText,
ListViewIcon,
ListViewItem,
ListViewLeft,
ListViewMainInfo
} from '../../ui/ListView';
import {
NodeMaintenanceState,
NodePowerState,
NodeProvisionState
} from './NodeStates';
import { parseNodeCapabilities } from '../../../utils/nodes';
const messages = defineMessages({
profile: {
id: 'NodeListItem.Profile',
defaultMessage: 'Profile:'
},
cpuCores: {
id: 'NodeListItem.cpuCores',
defaultMessage: `CPU {cpuCores, plural,
one {Core} other {Cores}}`
},
ram: {
id: 'NodeListItem.mbRam',
defaultMessage: 'MB RAM'
},
disk: {
id: 'NodeListItem.gbDisk',
defaultMessage: 'GB Disk'
}
});
export default class NodesListView extends React.Component {
render() {
return (
<ListView>
{this.props.nodes
.toList()
.toJS()
.map(node => (
<NodeListItem
node={node}
key={node.uuid}
inProgress={this.props.nodesInProgress.includes(node.uuid)}
/>
))}
</ListView>
);
}
}
NodesListView.propTypes = {
nodes: ImmutablePropTypes.map.isRequired,
nodesInProgress: ImmutablePropTypes.set.isRequired
};
export class NodeListItem extends React.Component {
render() {
const { node, inProgress } = this.props;
const iconClass = ClassNames({
'pficon pficon-server': true,
running: inProgress
});
return (
<ListViewItem stacked>
<ListViewCheckbox disabled={inProgress} name={`values.${node.uuid}`} />
<ListViewMainInfo>
<ListViewLeft>
<ListViewIcon size="sm" icon={iconClass} />
</ListViewLeft>
<ListViewBody>
<ListViewDescription>
<ListViewDescriptionHeading>
{node.name || node.uuid}
</ListViewDescriptionHeading>
<ListViewDescriptionText>
<NodePowerState
powerState={node.power_state}
targetPowerState={node.target_power_state}
/>
<NodeMaintenanceState maintenance={node.maintenance} />
{' | '}
<NodeProvisionState
provisionState={node.provision_state}
targetProvisionState={node.target_provision_state}
/>
</ListViewDescriptionText>
</ListViewDescription>
<ListViewAdditionalInfo>
<ListViewAdditionalInfoItem>
<span className="pficon pficon-flavor" />
<FormattedMessage {...messages.profile} />
&nbsp;
{parseNodeCapabilities(node.properties.capabilities).profile ||
'-'}
</ListViewAdditionalInfoItem>
<ListViewAdditionalInfoItem>
<span className="pficon pficon-cpu" />
<strong>{node.properties.cpus || '-'}</strong>
&nbsp;
<FormattedMessage
{...messages.cpuCores}
values={{ cpuCores: node.properties.cpus }}
/>
</ListViewAdditionalInfoItem>
<ListViewAdditionalInfoItem>
<span className="pficon pficon-memory" />
<strong>{node.properties.memory_mb || '-'}</strong>
&nbsp;
<FormattedMessage {...messages.ram} />
</ListViewAdditionalInfoItem>
<ListViewAdditionalInfoItem>
<span className="fa fa-database" />
<strong>{node.properties.local_gb || '-'}</strong>
&nbsp;
<FormattedMessage {...messages.disk} />
</ListViewAdditionalInfoItem>
</ListViewAdditionalInfo>
</ListViewBody>
</ListViewMainInfo>
</ListViewItem>
);
}
}
NodeListItem.propTypes = {
inProgress: PropTypes.bool.isRequired,
node: PropTypes.object.isRequired
};

View File

@ -6,19 +6,27 @@ import { submit } from 'redux-form';
import PropTypes from 'prop-types';
import React from 'react';
import { ActiveFilter, ActiveFiltersList } from '../ui/Toolbar/ActiveFilters';
import { getActiveFilters, getFilterByName } from '../../selectors/filters';
import { getFilteredNodes, getNodes } from '../../selectors/nodes';
import { nodeColumnMessages } from './messages';
import {
ActiveFilter,
ActiveFiltersList
} from '../../ui/Toolbar/ActiveFilters';
import { getActiveFilters, getFilterByName } from '../../../selectors/filters';
import { getFilteredNodes, getNodes } from '../../../selectors/nodes';
import { nodeColumnMessages } from '../messages';
import NodesToolbarForm from './NodesToolbarForm';
import ToolbarFiltersForm from '../ui/Toolbar/ToolbarFiltersForm';
import NodesToolbarActions from './NodesToolbarActions';
import ToolbarFiltersForm from '../../ui/Toolbar/ToolbarFiltersForm';
import {
clearActiveFilters,
deleteActiveFilter,
addActiveFilter,
updateFilter
} from '../../actions/FiltersActions';
import { Toolbar, ToolbarActions, ToolbarResults } from '../ui/Toolbar/Toolbar';
} from '../../../actions/FiltersActions';
import {
Toolbar,
ToolbarActions,
ToolbarResults
} from '../../ui/Toolbar/Toolbar';
const messages = defineMessages({
activeFilters: {
@ -38,27 +46,9 @@ const messages = defineMessages({
id: 'NodesToolbar.filterStringPlaceholder',
defaultMessage: 'Add filter'
},
introspectNodes: {
id: 'NodesToolbar.introspectNodes',
defaultMessage: 'Introspect Nodes'
},
nonFilteredToolbarResults: {
id: 'NodesToolbar.nonFilteredToolbarResults',
defaultMessage: '{totalCount, number} {totalCount, plural, one {Node} other {Nodes}}'
},
tagNodes: {
id: 'NodesToolbar.tagNodes',
defaultMessage: 'Tag Nodes'
},
provideNodes: {
id: 'NodesToolbar.provideNodes',
defaultMessage: 'Provide Nodes',
description: '"Providing" the nodes changes the provisioning state to "available" so that ' +
'they can be used in a deployment.'
},
removeNodes: {
id: 'NodesToolbar.removeNodes',
defaultMessage: 'Remove Nodes'
}
});
@ -102,16 +92,7 @@ class NodesToolbar extends React.Component {
onSubmit={updateFilter}
initialValues={initialValues}
/>
{/* TODO(jtomasek): Use these buttons to trigger Nodes actions once it is removed from
NodesTable */}
{/* <FormGroup>
<Button><FormattedMessage {...messages.introspectNodes} /></Button>
<Button><FormattedMessage {...messages.provideNodes} /></Button>
<DropdownKebab id="nodesActionsKebab">
<MenuItem><FormattedMessage {...messages.tagNodes} /></MenuItem>
<MenuItem><FormattedMessage {...messages.removeNodes} /></MenuItem>
</DropdownKebab>
</FormGroup> */}
<NodesToolbarActions />
</ToolbarActions>
<ToolbarResults>
<h5>

View File

@ -0,0 +1,159 @@
import { Button, FormGroup, MenuItem } from 'react-bootstrap';
import { connect } from 'react-redux';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import React, { PropTypes } from 'react';
import { change, submit, isInvalid } from 'redux-form';
import ConfirmationModal from '../../ui/ConfirmationModal';
import DropdownKebab from '../../ui/dropdown/DropdownKebab';
import { getAvailableNodeProfiles } from '../../../selectors/nodes';
import TagNodesModal from '../tag_nodes/TagNodesModal';
const messages = defineMessages({
deleteNodesModalTitle: {
id: 'NodesToolbarActions.deleteNodesModalTitle',
defaultMessage: 'Delete Nodes'
},
deleteNodesModalMessage: {
id: 'NodesToolbarActions.deleteNodesModalMessage',
defaultMessage: 'Are you sure you want to delete the selected nodes?'
},
disabledButtonsWarning: {
id: 'NodesToolbarActions.disabledButtonsWarning',
defaultMessage: `You need to select Nodes first or there is operation already in progress on
some of the selected Nodes`
},
introspectNodes: {
id: 'NodesToolbarActions.introspectNodes',
defaultMessage: 'Introspect Nodes'
},
tagNodes: {
id: 'NodesToolbarActions.tagNodes',
defaultMessage: 'Tag Nodes'
},
provideNodes: {
id: 'NodesToolbarActions.provideNodes',
defaultMessage: 'Provide Nodes',
description: '"Providing" the nodes changes the provisioning state to "available" so that ' +
'they can be used in a deployment.'
},
deleteNodes: {
id: 'NodesToolbarActions.deleteNodes',
defaultMessage: 'Delete Nodes'
}
});
class NodesToolbarActions extends React.Component {
constructor() {
super();
this.state = {
showDeleteModal: false,
showTagNodesModal: false
};
}
submitForm(action) {
this.props.setSubmitAction(action);
// TODO(jtomasek): hacky way to submit with updated values
// https://github.com/erikras/redux-form/issues/2818
setTimeout(() => this.props.submitForm());
}
deleteNodes(action) {
this.submitForm(action);
this.setState({ showDeleteModal: false });
}
tagNodes(tag) {
this.props.setTag(tag);
this.submitForm('tag');
this.setState({ showTagNodesModal: false });
}
render() {
const { disabled, intl } = this.props;
return (
// TODO(jtomasek): include proper error message from the form accessed via getFormSyncErrors
// selector once the 'error' is available via selector
// https://github.com/erikras/redux-form/issues/2872
(
<FormGroup
title={
disabled ? intl.formatMessage(messages.disabledButtonsWarning) : ''
}
>
<Button
disabled={this.props.disabled}
onClick={this.submitForm.bind(this, 'introspect')}
>
<FormattedMessage {...messages.introspectNodes} />
</Button>
<Button
disabled={this.props.disabled}
onClick={this.submitForm.bind(this, 'provide')}
>
<FormattedMessage {...messages.provideNodes} />
</Button>
<DropdownKebab id="nodesActionsKebab" pullRight>
<MenuItem
disabled={this.props.disabled}
onClick={() => this.setState({ showTagNodesModal: true })}
>
<FormattedMessage {...messages.tagNodes} />
</MenuItem>
<MenuItem
className="bg-danger"
disabled={this.props.disabled}
onClick={() => this.setState({ showDeleteModal: true })}
>
<FormattedMessage {...messages.deleteNodes} />
</MenuItem>
</DropdownKebab>
<ConfirmationModal
show={this.state.showDeleteModal}
title={this.props.intl.formatMessage(
messages.deleteNodesModalTitle
)}
question={this.props.intl.formatMessage(
messages.deleteNodesModalMessage
)}
iconClass="pficon pficon-delete"
onConfirm={() => this.deleteNodes('delete')}
onCancel={() => this.setState({ showDeleteModal: false })}
/>
<TagNodesModal
availableProfiles={this.props.availableProfiles.toArray()}
onProfileSelected={this.tagNodes.bind(this)}
onCancel={() => this.setState({ showTagNodesModal: false })}
show={this.state.showTagNodesModal}
/>
</FormGroup>
)
);
}
}
NodesToolbarActions.propTypes = {
availableProfiles: ImmutablePropTypes.list.isRequired,
disabled: PropTypes.bool.isRequired,
intl: PropTypes.object,
setSubmitAction: PropTypes.func.isRequired,
setTag: PropTypes.func.isRequired,
submitForm: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
availableProfiles: getAvailableNodeProfiles(state),
disabled: isInvalid('nodesListForm')(state)
});
const mapDispatchToProps = dispatch => ({
setSubmitAction: action =>
dispatch(change('nodesListForm', 'submitAction', action)),
setTag: tag => dispatch(change('nodesListForm', 'tag', tag)),
submitForm: () => dispatch(submit('nodesListForm'))
});
export default injectIntl(
connect(mapStateToProps, mapDispatchToProps)(NodesToolbarActions)
);

View File

@ -4,9 +4,12 @@ import { FormGroup, MenuItem } from 'react-bootstrap';
import PropTypes from 'prop-types';
import React from 'react';
import DropdownSelect from '../ui/reduxForm/DropdownSelect';
import { nodeColumnMessages } from './messages';
import { SortDirectionInput } from '../ui/Toolbar/ToolbarInputs';
import DropdownSelect from '../../ui/reduxForm/DropdownSelect';
import { nodeColumnMessages } from '../messages';
import {
ContentViewSelectorInput,
SortDirectionInput
} from '../../ui/Toolbar/ToolbarInputs';
const messages = defineMessages({
table: {
@ -59,17 +62,16 @@ const NodesToolbarForm = ({ handleSubmit, intl }) => (
component={SortDirectionInput}
/>
</FormGroup>
{/* TODO(jtomasek): Enable this once Nodes ListView is implemented
so it is possible to switch between list and table views */}
{/* <FormGroup className="toolbar-pf-view-selector">
<FormGroup className="pull-right">
<Field
name="contentView"
component={ContentViewSelectorInput}
options={{
table: intl.formatMessage(messages.table),
list: intl.formatMessage(messages.list)
}} />
</FormGroup> */}
list: intl.formatMessage(messages.list),
table: intl.formatMessage(messages.table)
}}
/>
</FormGroup>
</form>
);
NodesToolbarForm.propTypes = {

View File

@ -0,0 +1,154 @@
import ClassNames from 'classnames';
import { Field } from 'redux-form';
import React, { PropTypes } from 'react';
export const ListView = ({ children }) => (
<div className="list-group list-view-pf list-view-pf-view">
{children}
</div>
);
ListView.propTypes = {
children: PropTypes.node
};
export const ListViewItem = ({ children, stacked, expanded }) => {
const classes = ClassNames({
'list-group-item': true,
'list-view-pf-expand-active': expanded,
'list-view-pf-stacked': stacked
});
return (
<div className={classes}>
{children}
</div>
);
};
ListViewItem.propTypes = {
children: PropTypes.node,
expanded: PropTypes.bool.isRequired,
stacked: PropTypes.bool.isRequired
};
ListViewItem.defaultProps = {
expanded: false,
stacked: false
};
export const ListViewCheckbox = ({ disabled, name }) => (
<div className="list-view-pf-checkbox">
<Field name={name} type="checkbox" component="input" disabled={disabled} />
</div>
);
ListViewCheckbox.propTypes = {
disabled: PropTypes.bool.isRequired,
name: PropTypes.string.isRequired
};
ListViewCheckbox.defaultProps = {
disabled: false
};
export const ListViewExpand = ({ expanded }) => {
const classes = ClassNames({
'fa fa-angle-right': true,
'fa-angle-down': expanded
});
return (
<a className="list-view-pf-expand">
<span className={classes} />
</a>
);
};
ListViewExpand.propTypes = {
expanded: PropTypes.bool.isRequired
};
ListViewExpand.defaultProps = {
expanded: false
};
export const ListViewActions = ({ children }) => (
<div className="list-view-pf-actions">
{children}
</div>
);
ListViewActions.propTypes = {
children: PropTypes.node
};
export const ListViewMainInfo = ({ children }) => (
<div className="list-view-pf-main-info">
{children}
</div>
);
ListViewMainInfo.propTypes = {
children: PropTypes.node
};
export const ListViewLeft = ({ children }) => (
<div className="list-view-pf-left">
{children}
</div>
);
ListViewLeft.propTypes = {
children: PropTypes.node
};
export const ListViewIcon = ({ icon, size }) => {
return <span className={`list-view-pf-icon-${size} ${icon}`} />;
};
ListViewIcon.propTypes = {
icon: PropTypes.string.isRequired,
size: PropTypes.oneOf(['sm', 'md', 'lg'])
};
export const ListViewBody = ({ children }) => (
<div className="list-view-pf-body">
{children}
</div>
);
ListViewBody.propTypes = {
children: PropTypes.node
};
export const ListViewDescription = ({ children }) => (
<div className="list-view-pf-description">
{children}
</div>
);
ListViewDescription.propTypes = {
children: PropTypes.node
};
export const ListViewDescriptionHeading = ({ children }) => (
<div className="list-group-item-heading">
{children}
</div>
);
ListViewDescriptionHeading.propTypes = {
children: PropTypes.node
};
export const ListViewDescriptionText = ({ children }) => (
<div className="list-group-item-text">
{children}
</div>
);
ListViewDescriptionText.propTypes = {
children: PropTypes.node
};
export const ListViewAdditionalInfo = ({ children }) => (
<div className="list-view-pf-additional-info">
{children}
</div>
);
ListViewAdditionalInfo.propTypes = {
children: PropTypes.node
};
export const ListViewAdditionalInfoItem = ({ children }) => (
<div className="list-view-pf-additional-info-item">
{children}
</div>
);
ListViewAdditionalInfoItem.propTypes = {
children: PropTypes.node
};

View File

@ -27,7 +27,7 @@ DataTableHeaderCell.propTypes = {
export class DataTableCell extends React.Component {
render() {
return (
<td {...this.props}>
<td>
{this.props.children}
</td>
);

View File

@ -11,7 +11,7 @@ export const FiltersInitialState = Record({
activeFilters: Map(),
sortBy: 'name',
sortDir: 'asc',
contentView: 'table'
contentView: 'list'
}),
validationsToolbar: Map({
activeFilters: Map(),

View File

@ -117,6 +117,7 @@
@import "utils/patternflyOverrides";
@import "ui/Forms";
@import "ui/Grid";
@import "ui/ListView";
@import "ui/Modals";
@import "ui/Tables";
@import "ui/Navs";

View File

@ -73,15 +73,6 @@
}
}
.list-view-pf-icon-md.running {
position: relative;
&:after:extend(.spinner, .spinner.spinner-xl) {
content: "";
position: absolute;
left: 0;
}
}
/* https://davidwalsh.name/css-flip */
/* entire container, keeps perspective */
.flip-container {

20
src/less/ui/ListView.less Normal file
View File

@ -0,0 +1,20 @@
.list-view-pf-icon-md.running {
position: relative;
&:after:extend(.spinner, .spinner.spinner-xl) {
content: "";
position: absolute;
left: 0;
}
}
.list-view-pf-icon-sm.running {
position: relative;
&:after:extend(.spinner) {
content: "";
position: absolute;
left: -6px;
top: -6px;
height: 38px;
width: 38px;
}
}