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:
parent
4601a00ec5
commit
69cebd336f
|
@ -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.
|
|
@ -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" />
|
||||
<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 => ({
|
||||
|
|
|
@ -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" />
|
||||
|
||||
<FormattedMessage {...messages.maintenance} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
NodeMaintenanceState.propTypes = {
|
||||
maintenance: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export const NodeProvisionState = ({
|
||||
provisionState,
|
||||
targetProvisionState
|
||||
}) => (
|
||||
<span>
|
||||
<strong><FormattedMessage {...messages.provisionState} /></strong>
|
||||
{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" />
|
||||
|
||||
<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
|
||||
};
|
|
@ -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))
|
||||
);
|
|
@ -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} />
|
||||
|
||||
{parseNodeCapabilities(node.properties.capabilities).profile ||
|
||||
'-'}
|
||||
</ListViewAdditionalInfoItem>
|
||||
<ListViewAdditionalInfoItem>
|
||||
<span className="pficon pficon-cpu" />
|
||||
<strong>{node.properties.cpus || '-'}</strong>
|
||||
|
||||
<FormattedMessage
|
||||
{...messages.cpuCores}
|
||||
values={{ cpuCores: node.properties.cpus }}
|
||||
/>
|
||||
</ListViewAdditionalInfoItem>
|
||||
<ListViewAdditionalInfoItem>
|
||||
<span className="pficon pficon-memory" />
|
||||
<strong>{node.properties.memory_mb || '-'}</strong>
|
||||
|
||||
<FormattedMessage {...messages.ram} />
|
||||
</ListViewAdditionalInfoItem>
|
||||
<ListViewAdditionalInfoItem>
|
||||
<span className="fa fa-database" />
|
||||
<strong>{node.properties.local_gb || '-'}</strong>
|
||||
|
||||
<FormattedMessage {...messages.disk} />
|
||||
</ListViewAdditionalInfoItem>
|
||||
</ListViewAdditionalInfo>
|
||||
</ListViewBody>
|
||||
</ListViewMainInfo>
|
||||
</ListViewItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
NodeListItem.propTypes = {
|
||||
inProgress: PropTypes.bool.isRequired,
|
||||
node: PropTypes.object.isRequired
|
||||
};
|
|
@ -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>
|
|
@ -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)
|
||||
);
|
|
@ -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 = {
|
|
@ -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
|
||||
};
|
|
@ -27,7 +27,7 @@ DataTableHeaderCell.propTypes = {
|
|||
export class DataTableCell extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<td {...this.props}>
|
||||
<td>
|
||||
{this.props.children}
|
||||
</td>
|
||||
);
|
||||
|
|
|
@ -11,7 +11,7 @@ export const FiltersInitialState = Record({
|
|||
activeFilters: Map(),
|
||||
sortBy: 'name',
|
||||
sortDir: 'asc',
|
||||
contentView: 'table'
|
||||
contentView: 'list'
|
||||
}),
|
||||
validationsToolbar: Map({
|
||||
activeFilters: Map(),
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue