Просмотр исходного кода

- Adds Project Section (main menu item, dropdown, main list)
- Adds Project Store and Project Actions
- Fixes search on endpoints and projects

George Vrancianu 9 лет назад
Родитель
Сommit
a462d8cab8

+ 82 - 0
src/actions/ProjectActions/ProjectActions.js

@@ -0,0 +1,82 @@
+/*
+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 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/>.
+*/
+
+import Reflux from 'reflux';
+import Api from '../../components/ApiCaller';
+import {servicesUrl, defaultDomain} from '../../config';
+import Location from '../../core/Location';
+
+let ProjectActions = Reflux.createActions({
+  'loadProjects': { children: ["completed", "failed"] },
+  'setCurrentProject': {}
+})
+/*
+ProjectActions.login.listen((userData => {
+  let auth = {
+    "auth": {
+      "identity": {
+        "methods": [
+          "password"
+        ],
+        "password": {
+          "user": {
+            "name": userData.name,
+            "domain": {
+              "name": userData.domain ? userData.domain : defaultDomain
+            },
+            "password": userData.password
+          }
+        }
+      },
+      scope: {
+        project: {
+          domain: {
+            name: userData.domain ? userData.domain : defaultDomain
+          },
+          name: userData.name
+        }
+      }
+    }
+  }
+
+  Api.setDefaultHeader({ "X-Auth-Token": null })
+
+  Api.sendAjaxRequest({
+    url: servicesUrl.identity,
+    method: "POST",
+    data: auth
+  })
+    .then((response) => {
+      UserAction.login.success(response)
+      Location.push('/migrations')
+    }, UserAction.login.failed)
+    .catch(UserAction.login.failed)
+}))*/
+
+ProjectActions.loadProjects.listen(() => {
+  Api.sendAjaxRequest({
+      url: servicesUrl.projects,
+      method: "GET"
+    })
+    .then((response) => {
+      ProjectActions.loadProjects.completed(response)
+    }, ProjectActions.loadProjects.failed)
+    .catch((response) => {
+      ProjectActions.loadProjects.failed(response)
+    });
+})
+export default ProjectActions;

+ 6 - 0
src/actions/ProjectActions/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "ProjectActions",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./ProjectActions.js"
+}

+ 21 - 1
src/actions/UserActions/UserActions.js

@@ -24,7 +24,9 @@ let UserAction = Reflux.createActions({
   'login': { children: ["success", "failed"] },
   'logout': {},
   'tokenLogin': { children: ["failed"] },
-  'setCurrentUser': {}
+  'setCurrentUser': {},
+  'switchProject': {},
+  'getScopedProjects': { children: ["completed", "failed"] }
 })
 
 UserAction.login.listen((userData => {
@@ -44,6 +46,7 @@ UserAction.login.listen((userData => {
           }
         }
       },
+      /*scope: "unscoped"*/
       scope: {
         project: {
           domain: {
@@ -80,4 +83,21 @@ UserAction.tokenLogin.listen((token) => {
       UserAction.tokenLogin.failed(response)
     });
 })
+
+UserAction.getScopedProjects.listen((callback) => {
+  Api.sendAjaxRequest({
+    url: servicesUrl.projects,
+    method: "GET"
+  })
+    .then(
+      (response) => {
+        if (callback) {
+          callback(response)
+        }
+        UserAction.getScopedProjects.completed(response)
+      }, UserAction.getScopedProjects.failed)
+    .catch((response) => {
+      UserAction.getScopedProjects.failed(response)
+    });
+})
 export default UserAction;

Разница между файлами не показана из-за своего большого размера
+ 2 - 0
src/components/App/App.scss


+ 4 - 6
src/components/ConnectionsList/ConnectionsList.js

@@ -33,6 +33,7 @@ import FilteredTable from '../FilteredTable';
 import EndpointUsage from '../EndpointUsage';
 import NotificationIcon from '../NotificationIcon';
 import ConfirmationDialog from '../ConfirmationDialog'
+import ProjectsDropdown from '../ProjectsDropdown';
 
 
 const title = 'Cloud Endpoints';
@@ -123,7 +124,7 @@ class ConnectionsList extends Reflux.Component {
   }
 
   searchItem(queryText) {
-    this.setState({ queryText: queryText.target.value })
+    this.setState({ queryText: queryText })
   }
 
   filterType(e, type) {
@@ -200,11 +201,7 @@ class ConnectionsList extends Reflux.Component {
     }
     return output
   }
-
-  onProjectChange(project) {
-    this.setState({ currentProject: project.value })
-  }
-
+  
   currentInstance(migration) {
     let instance = "N/A"
     migration.vms.forEach((item) => {
@@ -248,6 +245,7 @@ class ConnectionsList extends Reflux.Component {
             <div className={s.top}>
               <h1>{title}</h1>
               <div className={s.topActions}>
+                <ProjectsDropdown />
                 <button onClick={(e) => this.showNewConnectionModal(e)}>New</button>
                 <UserIcon />
                 <NotificationIcon />

+ 1 - 0
src/components/Header/Header.js

@@ -96,6 +96,7 @@ class Header extends Component {
             <li><a onClick={(e) => this.goToMenu("/replicas")}>Replicas</a></li>
             <li><a onClick={(e) => this.goToMenu("/migrations")}>Migrations</a></li>
             <li><a onClick={(e) => this.goToMenu("/cloud-endpoints")}>Cloud Endpoints</a></li>
+            <li><a onClick={(e) => this.goToMenu("/projects")}>Projects</a></li>
           </ul>
         </div>
       </div>

+ 3 - 0
src/components/MigrationList/MigrationList.js

@@ -26,11 +26,13 @@ import SearchBox from '../SearchBox';
 import Moment from 'react-moment';
 import s from './MigrationList.scss';
 import MigrationStore from '../../stores/MigrationStore';
+import ProjectStore from '../../stores/MigrationStore';
 import MigrationActions from '../../actions/MigrationActions';
 import FilteredTable from '../FilteredTable';
 import TextTruncate from 'react-text-truncate';
 import LoadingIcon from "../LoadingIcon/LoadingIcon";
 import ConfirmationDialog from '../ConfirmationDialog'
+import ProjectsDropdown from '../ProjectsDropdown';
 
 const title = 'Migrations';
 const migrationTypes = [
@@ -365,6 +367,7 @@ class MigrationList extends Reflux.Component {
                   placeholder="Select"
                   value={this.state.currentProject}
                 />*/}
+                <ProjectsDropdown />
                 <button onClick={this.newMigration}>New</button>
                 <UserIcon />
                 <NotificationIcon />

+ 309 - 0
src/components/ProjectList/ProjectList.js

@@ -0,0 +1,309 @@
+/*
+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 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/>.
+*/
+
+import React, { PropTypes } from 'react';
+import Reflux from 'reflux';
+import withStyles from 'isomorphic-style-loader/lib/withStyles';
+import Location from '../../core/Location';
+import Dropdown from '../NewDropdown';
+import SearchBox from '../SearchBox';
+import s from './ProjectList.scss';
+import AddCloudConnection from '../AddCloudConnection';
+import Modal from 'react-modal';
+import ProjectStore from '../../stores/ProjectStore';
+import ProjectActions from '../../actions/ProjectActions';
+import TextTruncate from 'react-text-truncate';
+import UserIcon from '../UserIcon';
+import FilteredTable from '../FilteredTable';
+import NotificationIcon from '../NotificationIcon';
+import ConfirmationDialog from '../ConfirmationDialog'
+import ProjectsDropdown from '../ProjectsDropdown';
+
+
+const title = 'Projects';
+const connectionTypes = [
+  { label: "All", type: "all" }
+]
+
+const connectionActions = [
+  { label: "Delete", value: "delete" }
+]
+
+class ProjectList extends Reflux.Component {
+  constructor(props) {
+    super(props)
+    this.store = ProjectStore
+
+    this.state = {
+      showModal: false,
+      queryText: '',
+      filterType: 'all',
+      searchMin: true,
+      projects: null,
+      confirmationDialog: {
+        visible: false,
+        message: "Are you sure?",
+        onConfirm: null,
+        onCancel: null
+      }
+    }
+  }
+
+  static contextTypes = {
+    onSetTitle: PropTypes.func.isRequired,
+  };
+
+  componentWillMount() {
+    super.componentWillMount.call(this)
+
+    this.context.onSetTitle(title);
+    if (this.state.projects == null) {
+      ProjectActions.loadProjects()
+    }
+  }
+
+  projectsSelected() {
+    let count = this.projectsSelectedCount(),
+        total = 0
+    if (this.state.projects) {
+      total = this.state.projects.length
+    }
+
+    return `${count} of ${total} project(s) selected`;
+  }
+
+  projectsSelectedCount() {
+    let count = 0
+    if (this.state.projects) {
+      this.state.projects.forEach((item) => {
+        if (item.selected) count++
+      })
+    }
+    return count
+  }
+
+  projectDetail(e, item) {
+    //Location.push('/project/' + item.id + "/")
+  }
+
+  checkItem(e, itemRef) {
+    let items = this.state.projects
+    items.forEach((item) => {
+      if (item == itemRef) {
+        item.selected = !item.selected
+      }
+    })
+    this.setState({ projects: items })
+  }
+
+  filterFn(item, queryText, filterType) {
+    return (
+      item.name.toLowerCase().indexOf(queryText.toLowerCase()) != -1 &&
+      (filterType == "all" || filterType == item.type)
+    )
+  }
+
+  searchItem(queryText) {
+    this.setState({ queryText: queryText })
+  }
+
+  filterType(e, type) {
+    this.setState({ filterType: type })
+  }
+
+  closeModal() {
+    this.setState({ showModal: false })
+  }
+
+  bulkActions(action) {
+    switch (action.value) {
+      case "delete":
+        this.setState({
+          confirmationDialog: {
+            visible: true,
+            onConfirm: () => {
+              this.setState({ confirmationDialog: { visible: false }})
+              let selectedProjects = this.state.projects.filter((connection) => connection.selected)
+              selectedProjects.forEach(project => {
+                // TODO: Delete project action here
+              })
+            },
+            onCancel: () => {
+              this.setState({ confirmationDialog: { visible: false }})
+            }
+          }
+        })
+
+        break;
+    }
+  }
+
+  renderSearch(items) {
+    let output = null
+    if (items && items.length) {
+      output = items.map((item, index) => (
+        <div className={"item " + (item.selected ? " selected" : "")} 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>
+          </div>
+          <span className="cell cell-icon" onClick={(e) => this.projectDetail(e, item)}>
+            <div className={"icon project"}></div>
+            <span className="details">
+              {/*{item.name ? item.name : "N/A"}*/}
+              <TextTruncate line={1} truncateText="..." text={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}>Is Domain</span>
+              <span className={s.value}>
+                {item.is_domain ? "Yes" : "No"}
+              </span>
+            </span>
+          <span className={"cell " + s.composite}>
+              <span className={s.label}>Enabled</span>
+              <span className={s.value}>
+                {item.enabled ? "Yes" : "No"}
+              </span>
+            </span>
+        </div>
+      ), this)
+    }
+    return output
+  }
+  
+  currentInstance(migration) {
+    let instance = "N/A"
+    migration.vms.forEach((item) => {
+      if (item.selected) {
+        instance = item.name
+      }
+    })
+    return instance
+  }
+
+  showNewConnectionModal() {
+    this.setState({ showModal: true })
+  }
+
+  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"
+      }
+    }
+
+    return (
+      <div className={s.root}>
+        <div className={s.container}>
+          <div className={s.pageHeader}>
+            <div className={s.top}>
+              <h1>{title}</h1>
+              <div className={s.topActions}>
+                <ProjectsDropdown />
+                <button disabled onClick={(e) => this.showNewConnectionModal(e)}>New</button>
+                <UserIcon />
+                <NotificationIcon />
+              </div>
+            </div>
+            <div className="filters">
+              <div className="category-filter">
+                {itemStates}
+              </div>
+              <div className="name-filter">
+                <SearchBox
+                  placeholder="Search"
+                  value={this.state.queryText}
+                  onChange={(e) => this.searchItem(e)}
+                  minimize={true} // eslint-disable-line react/jsx-boolean-value
+                  onClick={(e) => this.toggleSearch(e)}
+                  className={"searchBox " + (this.state.searchMin ? "minimize" : "")}
+                />
+              </div>
+              <div className={s.bulkActions + (this.projectsSelectedCount() === 0 ? " invisible": "")}>
+                <div className={s.projectsCount}>
+                  {this.projectsSelected()}
+                </div>
+                <Dropdown
+                  options={connectionActions}
+                  placeholder="More Actions"
+                  onChange={(e) => this.bulkActions(e)}
+                />
+              </div>
+            </div>
+          </div>
+          <div className={s.pageContent}>
+            <FilteredTable
+              items={this.state.projects}
+              filterFn={this.filterFn}
+              queryText={this.state.queryText}
+              filterType={this.state.filterType}
+              renderSearch={(e) => this.renderSearch(e)}
+            ></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>
+    );
+  }
+
+}
+
+export default withStyles(ProjectList, s);

+ 173 - 0
src/components/ProjectList/ProjectList.scss

@@ -0,0 +1,173 @@
+/*
+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 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/>.
+*/
+
+@import '../variables.scss';
+
+.root {
+  height: 100%;
+}
+.pageHeader {
+  padding: 44px 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;
+      padding: 2px 8px 2px 0;
+      margin-top: 7px;
+      border-right: 1px solid $gray-dark;
+    }
+    :global(.name-filter) {
+      float: left;
+      padding-left: 24px;
+      margin-top: 2px;
+    }
+    &:after {
+      clear: both;
+      content: ' ';
+      display: block;
+      height: 0;
+    }
+  }
+}
+.pageContent {
+  flex: 10;
+  overflow-y: auto;
+  padding: 0 64px;
+}
+.pageFooter {
+  flex: 1;
+  padding: 16px 64px 44px;
+  :global(.Dropdown-root) {
+    width: 160px;
+    float: left;
+  }
+  .projectsCount {
+    float: left;
+    line-height: 32px;
+    margin-left: 16px;
+  }
+}
+.bulkActions {
+  float: right;
+  padding-top: 2px;
+  transition: opacity $animation-swift-out;
+  :global(.Dropdown-root) {
+    width: 160px;
+    float: left;
+  }
+  .projectsCount {
+    float: left;
+    line-height: 32px;
+    margin-right: 16px;
+  }
+}
+.container {
+  margin: 0 auto;
+  padding: 0;
+  max-width: $max-content-width;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  :global(.items-list) {
+    :global(.item) {
+      position: relative;
+      span:global(.cell):nth-child(2) {
+        flex: 3;
+        cursor: pointer;
+      }
+      span:global(.cell):nth-child(3) {
+        flex: 2;
+      }
+      span:global(.cell):nth-child(4) {
+        flex: 2;
+      }
+      span:global(.cell):nth-child(5) {
+        flex: 3;
+      }
+      :global(.checkbox-container) {
+        position: absolute;
+        left: -32px;
+        top: 21px;
+        opacity: 0;
+        transition: opacity $animation-swift-out;
+      }
+      &:hover, &:global(.selected) {
+        :global(.checkbox-container) {
+          opacity: 1;
+        }
+      }
+      .description {
+        display: block;
+        color: $gray-dark;
+        font-size: 14px;
+      }
+      :global(.icon.small-cloud) {
+        margin: 0 auto;
+      }
+    }
+
+    .composite {
+      flex-direction: column;
+      align-items: flex-start;
+      .label {
+        display: block;
+        color: $gray-dark;
+        margin-top: 4px;
+      }
+      .value {
+        color: $blue;
+        width: 100%;
+        display: inline-block;
+      }
+    }
+  }
+  .chevronRight {
+    background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOHB4IiBoZWlnaHQ9IjEzcHgiIHZpZXdCb3g9IjQgMiA4IDEzIiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPjxwb2x5Z29uIGlkPSJDb21iaW5lZC1TaGFwZSIgc3Ryb2tlPSJub25lIiBmaWxsPSIjNjE2NzcwIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDguMDAwMDAwLCA4LjM2Mzk2MSkgcm90YXRlKC05MC4wMDAwMDApIHRyYW5zbGF0ZSgtOC4wMDAwMDAsIC04LjM2Mzk2MSkgIiBwb2ludHM9IjggMTAuMzgyMzM3NiAyLjUwMjYwNzAyIDUgMS44MTU0MzI4OSA1LjY3Mjc5MjIxIDggMTEuNzI3OTIyMSAxNC4xODQ1NjcxIDUuNjcyNzkyMjEgMTMuNDk3MzkzIDUgOCAxMC4zODIzMzc2Ij48L3BvbHlnb24+PC9zdmc+');
+    width: 8px;
+    height: 13px;
+    display: inline-block;
+    margin: 0 16px;
+  }
+
+}

+ 6 - 0
src/components/ProjectList/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "ProjectList",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./ProjectList.js"
+}

+ 73 - 0
src/components/ProjectsDropdown/ProjectsDropdown.js

@@ -0,0 +1,73 @@
+/*
+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 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/>.
+*/
+
+import React, { Component, PropTypes } from 'react';
+import Reflux from 'reflux';
+import ProjectStore from '../../stores/ProjectStore';
+import UserActions from '../../actions/UserActions';
+import Dropdown from '../NewDropdown';
+import s from './ProjectsDropdown.scss';
+import withStyles from 'isomorphic-style-loader/lib/withStyles';
+
+class ProjectsDropdown extends Reflux.Component {
+
+  constructor(props) {
+    super(props)
+
+    this.store = ProjectStore
+  }
+
+  componentWillMount() {
+    super.componentWillMount.call(this)
+  }
+
+  switchProject(value) {
+    let project = null
+    this.state.projects.forEach(item => {
+      if (item.id == value.value) {
+        project = item
+      }
+    })
+    UserActions.switchProject(project)
+  }
+
+  render() {
+    let projects = this.state.projects.map(project => {
+      return { label: project.name, value: project.id }
+    })
+
+    let currentProject = null
+    if (Reflux.GlobalState.userStore.currentUser.project) {
+      currentProject = {
+        label: Reflux.GlobalState.userStore.currentUser.project.name,
+        value: Reflux.GlobalState.userStore.currentUser.project.id
+      }
+    }
+
+    return <div className={s.root}>
+      <Dropdown
+        options={projects}
+        placeholder="Switch Project"
+        onChange={(e) => this.switchProject(e)}
+        value={currentProject}
+      />
+    </div>
+  }
+
+}
+
+export default withStyles(ProjectsDropdown, s);

+ 23 - 0
src/components/ProjectsDropdown/ProjectsDropdown.scss

@@ -0,0 +1,23 @@
+/*
+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 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/>.
+*/
+
+@import '../variables.scss';
+
+.root {
+  float: left;
+  margin-left: 16px;
+}

+ 6 - 0
src/components/ProjectsDropdown/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "ProjectsDropdown",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./ProjectsDropdown.js"
+}

+ 4 - 0
src/components/WithSidebar/WithSidebar.js

@@ -41,6 +41,10 @@ class WithSidebar extends Component {
         {
           label: "Cloud Endpoints",
           route: "/cloud-endpoints"
+        },
+        {
+          label: "Projects",
+          route: "/projects"
         }
       ]
     }

+ 5 - 2
src/routes.js

@@ -20,20 +20,19 @@ import React from 'react';
 import Router from 'react-routing/src/Router';
 import fetch from './core/fetch';
 import App from './components/App';
-import ContentPage from './components/ContentPage';
 import MigrationWizard from './components/MigrationWizard';
 import WithSidebar from './components/WithSidebar';
 import MigrationList from './components/MigrationList';
 import MigrationView from './components/MigrationView';
 import MigrationDetail from './components/MigrationDetail';
 import MigrationTasks from './components/MigrationTasks';
-import MigrationNetworks from './components/MigrationNetworks';
 import MigrationSchedule from './components/MigrationSchedule';
 import CloudConnection from './components/CloudConnection';
 import CloudConnectionsView from './components/CloudConnectionsView';
 import CloudConnectionDetail from './components/CloudConnectionDetail';
 import CloudConnectionAuth from './components/CloudConnectionAuth';
 import ConnectionsList from './components/ConnectionsList';
+import ProjectList from './components/ProjectList';
 import ReplicaExecutions from './components/ReplicaExecutions';
 import UserView from './components/UserView';
 import UserOverview from './components/UserOverview';
@@ -105,6 +104,10 @@ const router = new Router(on => {
     </CloudConnection>
   )
 
+  on('/projects', async () =>
+    <WithSidebar route="/projects"><ProjectList /></WithSidebar>
+  )
+
   on('/user/profile/', async () => <UserView type="profile"><UserOverview /></UserView>)
 
   on('/user/billing/', async () =>

+ 55 - 0
src/stores/ProjectStore/ProjectStore.js

@@ -0,0 +1,55 @@
+/*
+ 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 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/>.
+ */
+
+
+import Reflux from 'reflux';
+import UserActions from '../../actions/UserActions';
+import ProjectActions from '../../actions/ProjectActions'
+import ConnectionsActions from '../../actions/ConnectionsActions'
+import MigrationActions from '../../actions/MigrationActions'
+import ConnectionsStore from '../../stores/ConnectionsStore'
+import Location from '../../core/Location';
+import Api from '../../components/ApiCaller';
+import cookie from 'react-cookie';
+import {servicesUrl} from '../../config'
+
+class ProjectStore extends Reflux.Store
+{
+
+  constructor() {
+    super()
+    this.listenables = ProjectActions
+
+    this.state = {
+      currentProject: null,
+      projects: []
+    }
+
+    ProjectActions.loadProjects()
+  }
+
+  onLoadProjectsCompleted(response) {
+    console.log("onLoadProjectsCompleted", response)
+    this.setState({
+      projects: response.data.projects
+    })
+  }
+}
+
+ProjectStore.id = "projectStore"
+
+export default ProjectStore;

+ 6 - 0
src/stores/ProjectStore/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "ProjectStore",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./ProjectStore.js"
+}

+ 15 - 0
src/stores/UserStore/UserStore.js

@@ -85,6 +85,8 @@ class UserStore extends Reflux.Store
     if (window.location.pathname == "/" || window.location.pathname == "/login") {
       Location.push('/migrations');
     }
+
+    UserActions.getScopedProjects()
   }
 
   onLoginFailed(response) {
@@ -131,6 +133,19 @@ class UserStore extends Reflux.Store
       }
     }, this)
   }
+
+  onSwitchProject(project) {
+    let currentUser = this.state.currentUser
+    currentUser.project = project
+    this.setState({
+      currentUser: currentUser
+    })
+    ConnectionsActions.loadConnections()
+  }
+
+  onGetScopedProjectsCompleted(response) {
+    console.log("onGetScopedProjectsCompleted", response)
+  }
 }
 
 UserStore.id = "userStore"

Некоторые файлы не были показаны из-за большого количества измененных файлов