|
|
@@ -1,218 +1,241 @@
|
|
|
/*
|
|
|
-Copyright (C) 2017 Cloudbase Solutions SRL
|
|
|
+ Copyright (C) 2017 Cloudbase Solutions SRL
|
|
|
|
|
|
-This program is free software: you can redistribute it and/or modify
|
|
|
-it under the terms of the GNU Affero General Public License as
|
|
|
-published by the Free Software Foundation, either version 3 of the
|
|
|
-License, or (at your option) any later version.
|
|
|
+ This program is free software: you can redistribute it and/or modify
|
|
|
+ it under the terms of the GNU Affero General Public License as
|
|
|
+ published by the Free Software Foundation, either version 3 of the
|
|
|
+ License, or (at your option) any later version.
|
|
|
|
|
|
-This program is distributed in the hope that it will be useful,
|
|
|
-but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
-GNU Affero General Public License for more details.
|
|
|
+ This program is distributed in the hope that it will be useful,
|
|
|
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
+ GNU Affero General Public License for more details.
|
|
|
|
|
|
-You should have received a copy of the GNU Affero General Public License
|
|
|
-along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
-*/
|
|
|
+ You should have received a copy of the GNU Affero General Public License
|
|
|
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
+ */
|
|
|
|
|
|
-import React, { PropTypes } from 'react';
|
|
|
-import Reflux from 'reflux';
|
|
|
+import React, { Component, PropTypes } from 'react';
|
|
|
import withStyles from 'isomorphic-style-loader/lib/withStyles';
|
|
|
-import Location from '../../core/Location';
|
|
|
import Dropdown from '../NewDropdown';
|
|
|
import SearchBox from '../SearchBox';
|
|
|
-import Config from '../Config';
|
|
|
import Moment from 'react-moment';
|
|
|
-import s from './ConnectionsList.scss';
|
|
|
-import AddCloudConnection from '../AddCloudConnection';
|
|
|
-import Modal from 'react-modal';
|
|
|
-import ConnectionsStore from '../../stores/ConnectionsStore';
|
|
|
-import ConnectionsActions from '../../actions/ConnectionsActions';
|
|
|
+import s from './MainList.scss';
|
|
|
+import FilteredTable from '../FilteredTable';
|
|
|
import TextTruncate from 'react-text-truncate';
|
|
|
-import UserIcon from '../UserIcon';
|
|
|
-import FilteredTable from '../FilteredTable'
|
|
|
+import ConfirmationDialog from '../ConfirmationDialog'
|
|
|
|
|
|
|
|
|
-const title = 'Cloud Connections';
|
|
|
-const connectionTypes = [
|
|
|
- { label: "All", type: "all" },
|
|
|
- { label: "Amazon", type: "amazon" },
|
|
|
- { label: "Azure", type: "azure" },
|
|
|
- { label: "Openstack", type: "openstack" },
|
|
|
- { label: "VMware", type: "vmware_vsphere" }
|
|
|
-]
|
|
|
+class MainList extends Component {
|
|
|
+ static contextTypes = {
|
|
|
+ onSetTitle: PropTypes.func.isRequired
|
|
|
+ }
|
|
|
+
|
|
|
+ static propTypes = {
|
|
|
+ itemName: PropTypes.string,
|
|
|
+ items: PropTypes.array,
|
|
|
+ renderItem: PropTypes.func,
|
|
|
+ filters: PropTypes.array,
|
|
|
+ actions: PropTypes.object,
|
|
|
+ refresh: PropTypes.any,
|
|
|
+ detailAction: PropTypes.func
|
|
|
+ }
|
|
|
+
|
|
|
+ static defaultProps = {
|
|
|
+ itemName: "items",
|
|
|
+ items: [],
|
|
|
+ filters: [],
|
|
|
+ actions: [],
|
|
|
+ refresh: false
|
|
|
+ }
|
|
|
|
|
|
-class MainList extends Reflux.Component {
|
|
|
constructor(props) {
|
|
|
super(props)
|
|
|
- this.store = ConnectionsStore
|
|
|
|
|
|
this.state = {
|
|
|
- showModal: false,
|
|
|
queryText: '',
|
|
|
- filterType: 'all',
|
|
|
+ items: this.props.items,
|
|
|
+ filterType: "all",
|
|
|
+ filterStatus: "all",
|
|
|
searchMin: true,
|
|
|
- connections: null
|
|
|
+ selectedAll: false,
|
|
|
+ confirmationDialog: {
|
|
|
+ visible: false,
|
|
|
+ message: "Are you sure?",
|
|
|
+ onConfirm: null,
|
|
|
+ onCancel: null
|
|
|
+ }
|
|
|
+ }
|
|
|
+ this.listActions = []
|
|
|
+ for (let i in this.props.actions) {
|
|
|
+ this.listActions.push({ label: props.actions[i].label, value: i })
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- static contextTypes = {
|
|
|
- onSetTitle: PropTypes.func.isRequired,
|
|
|
- };
|
|
|
-
|
|
|
componentWillMount() {
|
|
|
- super.componentWillMount.call(this)
|
|
|
+ this.setState({ items: this.props.items }) // eslint-disable-line react/no-did-mount-set-state
|
|
|
+ }
|
|
|
+
|
|
|
+ componentWillReceiveProps(newProps, oldProps) {
|
|
|
+ this.setState({ items: newProps.items })
|
|
|
+ }
|
|
|
|
|
|
- this.context.onSetTitle(title);
|
|
|
- if (this.state.connections == null) {
|
|
|
- ConnectionsActions.loadConnections()
|
|
|
+ itemsSelected() {
|
|
|
+ let count = 0
|
|
|
+ let total = 0
|
|
|
+ if (this.state.items) {
|
|
|
+ count = this.selectedCount()
|
|
|
+ total = this.state.items.length
|
|
|
}
|
|
|
+
|
|
|
+ return `${count} of ${total} ${this.props.itemName}(s) selected`;
|
|
|
}
|
|
|
|
|
|
- connectionsSelected() {
|
|
|
- let count = 0,
|
|
|
- total = 0
|
|
|
- if (this.state.connections) {
|
|
|
- this.state.connections.forEach((item) => {
|
|
|
- if (item.selected) count++
|
|
|
+ selectedCount() {
|
|
|
+ let count = 0
|
|
|
+ if (this.state.items) {
|
|
|
+ this.state.items.forEach((item) => {
|
|
|
+ if (item.selected) {
|
|
|
+ count++
|
|
|
+ }
|
|
|
})
|
|
|
- total = this.state.connections.length
|
|
|
}
|
|
|
-
|
|
|
- return `${count} of ${total} connection(s) selected`;
|
|
|
+ return count
|
|
|
}
|
|
|
|
|
|
- connectionDetail(e, item) {
|
|
|
- Location.push('/cloud-endpoints/' + item.id + "/")
|
|
|
+ itemDetail(e, item) {
|
|
|
+ console.log(this.props.detailAction, typeof this.props.detailAction)
|
|
|
+ if (typeof this.props.detailAction == "function") {
|
|
|
+ this.props.detailAction(item)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
checkItem(e, itemRef) {
|
|
|
- let items = this.state.connections
|
|
|
+ let items = this.state.items
|
|
|
items.forEach((item) => {
|
|
|
if (item == itemRef) {
|
|
|
item.selected = !item.selected
|
|
|
}
|
|
|
})
|
|
|
- this.setState({ connections: items })
|
|
|
+ this.setState({ items: items, selectedAll: false })
|
|
|
}
|
|
|
|
|
|
- filterFn(item, queryText, filterType) {
|
|
|
- return (
|
|
|
- item.name.toLowerCase().indexOf(queryText.toLowerCase()) != -1 &&
|
|
|
- (filterType == "all" || filterType == item.type)
|
|
|
- )
|
|
|
+ checkAll() {
|
|
|
+ let items = this.state.items
|
|
|
+ let selectedAll = this.state.selectedAll
|
|
|
+
|
|
|
+ items.forEach((item) => {
|
|
|
+ item.selected = !selectedAll
|
|
|
+ })
|
|
|
+
|
|
|
+ this.setState({ items: items, selectedAll: !selectedAll })
|
|
|
}
|
|
|
|
|
|
searchItem(queryText) {
|
|
|
- this.setState({ queryText: queryText.target.value })
|
|
|
+ if (queryText.target) {
|
|
|
+ this.setState({ queryText: queryText.target.value })
|
|
|
+ } else {
|
|
|
+ this.setState({ queryText: queryText })
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
filterType(e, type) {
|
|
|
- this.setState({ filterType: type })
|
|
|
+ this.setState({ filterType: type }, () => {
|
|
|
+ this.searchItem({ target: { value: this.state.queryText } })
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
- closeModal() {
|
|
|
- this.setState({ showModal: false })
|
|
|
+ filterStatus(e, status) {
|
|
|
+ this.setState({ filterStatus: status }, () => {
|
|
|
+ this.searchItem({ target: { value: this.state.queryText } })
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
- bulkActions(action) {
|
|
|
- switch (action.value) {
|
|
|
- case "delete":
|
|
|
- let selectedConnections = this.state.connections.filter((connection) => connection.selected)
|
|
|
- selectedConnections.forEach(connection => {
|
|
|
- ConnectionsActions.deleteConnection(connection)
|
|
|
- })
|
|
|
- break;
|
|
|
- }
|
|
|
+ filterFn(item, queryText, filterType, filterStatus) {
|
|
|
+ return (
|
|
|
+ item.name.toLowerCase().indexOf(queryText.toLowerCase()) != -1 &&
|
|
|
+ (filterType == "all" || filterType == item.type) &&
|
|
|
+ (filterStatus == "all" || filterStatus == item.status)
|
|
|
+ )
|
|
|
}
|
|
|
|
|
|
renderSearch(items) {
|
|
|
- let output = null
|
|
|
- if (items && items.length) {
|
|
|
- output = items.map((item, index) => (
|
|
|
- <div className="item" key={"vm_" + index}>
|
|
|
- <div className="checkbox-container">
|
|
|
- <input
|
|
|
- id={"vm_check_" + index}
|
|
|
- type="checkbox"
|
|
|
- checked={item.selected}
|
|
|
- onChange={(e) => this.checkItem(e, item)}
|
|
|
- className="checkbox-normal"
|
|
|
- />
|
|
|
- <label htmlFor={"vm_check_" + index}></label>
|
|
|
+ if (items) {
|
|
|
+ let output = items.map((item) => {
|
|
|
+ return (
|
|
|
+ <div className={s.row + " " + (item.selected ? "selected" : "")}>
|
|
|
+ <div className="checkbox-container">
|
|
|
+ <input
|
|
|
+ id={"vm_check_" + item.id}
|
|
|
+ type="checkbox"
|
|
|
+ checked={item.selected}
|
|
|
+ onChange={(e) => this.checkItem(e, item)}
|
|
|
+ className="checkbox-normal"
|
|
|
+ />
|
|
|
+ <label htmlFor={"vm_check_" + item.id}></label>
|
|
|
+ </div>
|
|
|
+ {this.props.renderItem(item)}
|
|
|
</div>
|
|
|
- <span className="cell cell-icon" onClick={(e) => this.connectionDetail(e, item)}>
|
|
|
- <span className="details">
|
|
|
- {item.name}
|
|
|
- <span className={s.description}>{item.description == "" ? "N/A" : item.description}</span>
|
|
|
- </span>
|
|
|
- </span>
|
|
|
- <span className="cell">
|
|
|
- <div className={s.cloudImage + " icon small-cloud " + item.type}></div>
|
|
|
- </span>
|
|
|
- <span className={"cell " + s.composite}>
|
|
|
- <span className={s.label}>Created</span>
|
|
|
- <span className={s.value}>
|
|
|
- <Moment fromNow ago date={item.created_at}/> ago
|
|
|
- </span>
|
|
|
- </span>
|
|
|
- <span className={"cell " + s.composite}>
|
|
|
- <span className={s.label}>URL</span>
|
|
|
- <span className={s.value}>
|
|
|
- <TextTruncate line={1} truncateText="..." text={item.connection_info.secret_ref}/>
|
|
|
- </span>
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- ), this)
|
|
|
+ )
|
|
|
+ })
|
|
|
+ return output
|
|
|
+ } else {
|
|
|
+ return (<div className="no-results">Your search returned no results</div>)
|
|
|
}
|
|
|
- return output
|
|
|
}
|
|
|
|
|
|
- onProjectChange(project) {
|
|
|
- this.setState({ currentProject: project.value })
|
|
|
+ onActionChange(option) {
|
|
|
+ let items = this.state.items.forEach((item) => {
|
|
|
+ if (item.selected) {
|
|
|
+ return item
|
|
|
+ }
|
|
|
+ })
|
|
|
+ if (this.props.actions[option.value].action) {
|
|
|
+ items.forEach((item) => {
|
|
|
+ this.props.actions[option.value].action(item)
|
|
|
+ })
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- showNewConnectionModal() {
|
|
|
- this.setState({ showModal: true })
|
|
|
+ refreshList() {
|
|
|
+ this.props.refresh()
|
|
|
}
|
|
|
|
|
|
render() {
|
|
|
- let itemStates = connectionTypes.map((state, index) => (
|
|
|
- <a
|
|
|
- className={this.state.filterType == state.type || (this.state.filterType == null && state.type == "all") ?
|
|
|
- "selected" : ""}
|
|
|
- onClick={(e) => this.filterType(e, state.type)} key={"status_" + index}
|
|
|
- >{state.label}</a>
|
|
|
- ), this)
|
|
|
-
|
|
|
- let modalStyle = {
|
|
|
- content: {
|
|
|
- padding: "0px",
|
|
|
- borderRadius: "4px",
|
|
|
- bottom: "auto",
|
|
|
- width: "576px",
|
|
|
- height: "auto",
|
|
|
- left: "50%",
|
|
|
- top: "70px",
|
|
|
- marginLeft: "-288px"
|
|
|
- }
|
|
|
- }
|
|
|
+ let _this = this
|
|
|
+ let tableFilters = this.props.filters.map(filter => {
|
|
|
+ let filterTemplate = filter.options.map((state, index) => (
|
|
|
+ <a
|
|
|
+ className={_this.state.filterStatus == state.type || (_this.state.filterStatus == null && state.type == "all") ?
|
|
|
+ "selected" : ""}
|
|
|
+ onClick={(e) => _this.filterStatus(e, state.type)} key={"status_" + index}
|
|
|
+ >{state.label}</a>
|
|
|
+ )
|
|
|
+ )
|
|
|
+ return <div className="category-filter">{filterTemplate}</div>
|
|
|
+ })
|
|
|
|
|
|
return (
|
|
|
<div className={s.root}>
|
|
|
<div className={s.container}>
|
|
|
- <div className={s.pageHeader}>
|
|
|
- <div className={s.top}>
|
|
|
- <h1>{title}</h1>
|
|
|
- <div className={s.topActions}>
|
|
|
- <button onClick={(e) => this.showNewConnectionModal(e)}>New Connection</button>
|
|
|
- <UserIcon />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <div className={s.listHeader}>
|
|
|
<div className="filters">
|
|
|
- <div className="category-filter">
|
|
|
- {itemStates}
|
|
|
+ <div className="checkbox-container">
|
|
|
+ <input
|
|
|
+ id={"vm_check_all"}
|
|
|
+ type="checkbox"
|
|
|
+ checked={this.state.selectedAll[this.state.filterType]}
|
|
|
+ onChange={(e) => this.checkAll()}
|
|
|
+ className="checkbox-normal"
|
|
|
+ />
|
|
|
+ <label htmlFor={"vm_check_all"}></label>
|
|
|
</div>
|
|
|
+ {tableFilters}
|
|
|
+ {this.props.refresh && (
|
|
|
+ <div className={s.refreshBtn}>
|
|
|
+ <div className="icon refresh" onClick={(e) => this.refreshList(e)}></div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
<div className="name-filter">
|
|
|
<SearchBox
|
|
|
placeholder="Search"
|
|
|
@@ -223,41 +246,36 @@ class MainList extends Reflux.Component {
|
|
|
className={"searchBox " + (this.state.searchMin ? "minimize" : "")}
|
|
|
/>
|
|
|
</div>
|
|
|
- <div className={s.bulkActions}>
|
|
|
- <div className={s.connectionsCount}>
|
|
|
- {this.connectionsSelected()}
|
|
|
+ <div className={s.bulkActions + (this.selectedCount() === 0 ? " invisible" : "")}>
|
|
|
+ <div className={s.itemsCount}>
|
|
|
+ {this.itemsSelected()}
|
|
|
</div>
|
|
|
<Dropdown
|
|
|
- options={Config.connectionsActions}
|
|
|
- placeholder="Select"
|
|
|
- onChange={(e) => this.bulkActions(e)}
|
|
|
+ options={this.listActions}
|
|
|
+ onChange={(e) => this.onActionChange(e)}
|
|
|
+ placeholder="More Actions"
|
|
|
/>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div className={s.pageContent}>
|
|
|
+ <div className={s.listContent}>
|
|
|
<FilteredTable
|
|
|
- items={this.state.connections}
|
|
|
+ items={this.state.items}
|
|
|
filterFn={this.filterFn}
|
|
|
queryText={this.state.queryText}
|
|
|
filterType={this.state.filterType}
|
|
|
+ filterStatus={this.state.filterStatus}
|
|
|
renderSearch={(e) => this.renderSearch(e)}
|
|
|
+ customClassName={s.mainTable}
|
|
|
></FilteredTable>
|
|
|
</div>
|
|
|
- <div className={s.pageFooter}>
|
|
|
-
|
|
|
- </div>
|
|
|
</div>
|
|
|
- <Modal
|
|
|
- isOpen={this.state.showModal}
|
|
|
- contentLabel="Add new cloud connection"
|
|
|
- style={modalStyle}
|
|
|
- >
|
|
|
- <AddCloudConnection
|
|
|
- closeHandle={(e) => this.closeModal(e)}
|
|
|
- addHandle={(e) => this.closeModal(e)}
|
|
|
- />
|
|
|
- </Modal>
|
|
|
+ <ConfirmationDialog
|
|
|
+ visible={this.state.confirmationDialog.visible}
|
|
|
+ message={this.state.confirmationDialog.message}
|
|
|
+ onConfirm={(e) => this.state.confirmationDialog.onConfirm(e)}
|
|
|
+ onCancel={(e) => this.state.confirmationDialog.onCancel(e)}
|
|
|
+ />
|
|
|
</div>
|
|
|
);
|
|
|
}
|