Răsfoiți Sursa

New MainList component - provides abstractisation for main lists throughout the application

George Vrancianu 8 ani în urmă
părinte
comite
78fc89975b

+ 4 - 2
src/components/FilteredTable/FilteredTable.js

@@ -26,7 +26,8 @@ class FilteredTable extends Component {
   static defaultProps = {
     items: [],
     filterFn: null,
-    renderSearchItem: null
+    renderSearchItem: null,
+    customClassName: null
   }
 
   static propTypes = {
@@ -34,7 +35,8 @@ class FilteredTable extends Component {
     filterFn: PropTypes.func,
     queryText: PropTypes.string,
     filterType: PropTypes.string,
-    renderSearch: PropTypes.func
+    renderSearch: PropTypes.func,
+    customClassName: PropTypes.string
   }
 
   constructor(props) {

+ 185 - 167
src/components/MainList/MainList.js

@@ -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>
     );
   }

+ 29 - 35
src/components/MainList/MainList.scss

@@ -20,36 +20,14 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 .root {
   height: 100%;
 }
-.pageHeader {
-  padding: 44px 64px 0;
+.listHeader {
+  padding: 0px 64px 0;
   flex: 1;
 
   h1 {
     float: left;
     margin: 0;
   }
-  .top {
-    &:after {
-      content: " ";
-      clear: both;
-      display: block;
-      height: 0;
-    }
-    margin-bottom: 48px;
-  }
-  .topActions {
-    float: right;
-    margin-top: 3px;
-    :global(.Dropdown-root) {
-      margin-right: 16px;
-      float: left;
-      width: 160px;
-      :global(.Dropdown-control) {
-        text-align: left;
-      }
-    }
-
-  }
   :global(.filters) {
     :global(.category-filter) {
       float: left;
@@ -70,10 +48,14 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
     }
   }
 }
-.pageContent {
+.listContent {
   flex: 10;
   overflow-y: auto;
   padding: 0 64px;
+
+}
+.mainTable {
+  border-top: 1px solid $gray-light;
 }
 .pageFooter {
   flex: 1;
@@ -95,7 +77,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
     width: 160px;
     float: left;
   }
-  .connectionsCount {
+  .itemsCount {
     float: left;
     line-height: 32px;
     margin-right: 16px;
@@ -109,25 +91,37 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
   display: flex;
   flex-direction: column;
   :global(.items-list) {
+    .row {
+      position: relative;
+      :global(.checkbox-container) {
+        position: absolute;
+        left: -32px;
+        top: 21px;
+        opacity: 0;
+        transition: opacity $animation-swift-out;
+      }
+      &:hover, &:global(.selected) {
+        :global(.checkbox-container) {
+          opacity: 1;
+        }
+      }
+    }
+
     :global(.item) {
       position: relative;
-      span:global(.cell):nth-child(2) {
+      span:global(.cell):nth-child(1) {
         flex: 3;
       }
-      span:global(.cell):nth-child(3) {
+      span:global(.cell):nth-child(2) {
         flex: 2;
       }
-      span:global(.cell):nth-child(4) {
+      span:global(.cell):nth-child(3) {
         flex: 2;
       }
-      span:global(.cell):nth-child(5) {
+      span:global(.cell):nth-child(4) {
         flex: 3;
       }
-      :global(.checkbox-container) {
-        position: absolute;
-        left: -32px;
-        top: 21px;
-      }
+
       .description {
         display: block;
         color: $gray-dark;