diff --git a/resources/frontend_client/app/components/DashboardsDropdown.react.js b/resources/frontend_client/app/components/DashboardsDropdown.react.js new file mode 100644 index 0000000000000000000000000000000000000000..2f7dd53a2fbabb5bc5d5334a185f16079deaadf4 --- /dev/null +++ b/resources/frontend_client/app/components/DashboardsDropdown.react.js @@ -0,0 +1,140 @@ +"use strict"; + +import React, { Component, PropTypes } from 'react'; +import cx from "classnames"; +import OnClickOut from 'react-onclickout'; + +import CreateDashboardModal from "metabase/components/CreateDashboardModal.react"; +import Icon from "metabase/components/Icon.react"; +import Modal from "metabase/components/Modal.react"; + + +export default class DashboardsDropdown extends Component { + + constructor(props) { + super(props); + + this.state = { + dropdownOpen: false, + modalOpen: false + }; + + this.toggleDropdown = this.toggleDropdown.bind(this); + this.closeDropdown = this.closeDropdown.bind(this); + this.toggleModal = this.toggleModal.bind(this); + this.closeModal = this.closeModal.bind(this); + } + + onCreateDashboard(newDashboard) { + let { createDashboardFn } = this.props; + + createDashboardFn(newDashboard).then(function() { + // close modal and add new dash to our dashboards list + this.setState({ + dropdownOpen: false, + modalOpen: false + }); + }.bind(this)); + } + + toggleModal() { + if (!this.state.modalOpen) { + // when we open our modal we always close the dropdown + this.setState({ + dropdownOpen: false, + modalOpen: !this.state.modalOpen + }); + } + } + + closeModal() { + this.setState({ modalOpen: false }); + } + + toggleDropdown() { + this.setState({ dropdownOpen: !this.state.dropdownOpen }); + } + + closeDropdown() { + this.setState({ dropdownOpen: false }); + } + + renderCreateDashboardModal() { + return ( + <Modal> + <CreateDashboardModal + createDashboardFn={this.onCreateDashboard.bind(this)} + closeFn={() => this.closeModal()} /> + </Modal> + ); + } + + render() { + let { dashboards } = this.props; + let { dropdownOpen, modalOpen } = this.state; + + let dropDownClasses = cx({ + 'NavDropdown': true, + 'inline-block': true, + 'cursor-pointer': true, + 'open': dropdownOpen, + }) + + return ( + <div> + { modalOpen ? this.renderCreateDashboardModal() : null } + + <OnClickOut onClickOut={this.closeDropdown}> + <div className={dropDownClasses}> + <a className="NavDropdown-button NavItem text-white cursor-pointer p2 flex align-center" onClick={this.toggleDropdown}> + <span className="NavDropdown-button-layer"> + Dashboards + <Icon className="ml1" name={'chevrondown'} width={8} height={8}></Icon> + </span> + </a> + + { dropdownOpen ? + <div className="NavDropdown-content DashboardList"> + { dashboards.length === 0 ? + <div className="NavDropdown-content-layer text-white text-centered"> + <div className="p2"><span className="QuestionCircle">?</span></div> + <div className="px2 py1 text-bold">You don’t have any dashboards yet.</div> + <div className="px2 pb2 text-light">Dashboards group visualizations for frequent questions in a single handy place.</div> + <div className="border-top border-light"> + <a className="Dropdown-item block text-white no-decoration" href="#" onClick={this.toggleModal}>Create a new dashboard</a> + </div> + </div> + : + <ul className="NavDropdown-content-layer"> + { dashboards.map(dash => + <li className="block"> + <a className="Dropdown-item block text-white no-decoration" href={"/dash/"+dash.id} onClick={this.closeDropdown}> + <div className="flex text-bold"> + {dash.name} + </div> + { dash.description ? + <div className="mt1 text-light text-brand-light"> + {dash.description} + </div> + : null } + </a> + </li> + )} + <li className="block border-top border-light"> + <a className="Dropdown-item block text-white no-decoration" href="#" onClick={this.toggleModal}>Create a new dashboard</a> + </li> + </ul> + } + </div> + : null } + </div> + </OnClickOut> + </div> + ); + } +} + +DashboardsDropdown.propTypes = { + createDashboardFn: PropTypes.func.isRequired, + dashboards: PropTypes.array.isRequired +}; diff --git a/resources/frontend_client/app/components/Navbar.react.js b/resources/frontend_client/app/components/Navbar.react.js new file mode 100644 index 0000000000000000000000000000000000000000..1785310f51fea1133fbd6689d357e09bf294a54c --- /dev/null +++ b/resources/frontend_client/app/components/Navbar.react.js @@ -0,0 +1,120 @@ +"use strict"; + +import React, { Component, PropTypes } from 'react'; +import cx from "classnames"; + +import DashboardsDropdown from "metabase/components/DashboardsDropdown.react"; +import Icon from "metabase/components/Icon.react"; +import LogoIcon from "metabase/components/LogoIcon.react"; +import ProfileLink from "metabase/components/ProfileLink.react"; + + +// TODO - this relies on props.location, which is angular's $location service + +export default class Navbar extends Component { + + isActive(path) { + return this.props.location.path().indexOf(path) >= 0; + } + + renderAdminNav() { + const classes = "NavItem py1 px2 no-decoration"; + + return ( + <nav className="AdminNav"> + <div className="wrapper flex align-center"> + <div className="NavTitle flex align-center"> + <Icon name={'gear'} className="AdminGear" width={22} height={22}></Icon> + <span className="NavItem-text ml1 hide sm-show">Site Administration</span> + </div> + + <ul className="sm-ml4 flex flex-full"> + <li> + <a className={cx(classes, {"is--selected": this.isActive("/admin/settings")})} href="/admin/settings/"> + Settings + </a> + </li> + <li> + <a className={cx(classes, {"is--selected": this.isActive("/admin/people")})} href="/admin/people/"> + People + </a> + </li> + <li> + <a className={cx(classes, {"is--selected": this.isActive("/admin/metadata")})} href="/admin/metadata/"> + Metadata + </a> + </li> + <li> + <a className={cx(classes, {"is--selected": this.isActive("/admin/databases")})} href="/admin/databases/"> + Databases + </a> + </li> + </ul> + + <ProfileLink {...this.props}></ProfileLink> + </div> + </nav> + ); + } + + renderAuthNav() { + return ( + <nav className="py2 sm-py1 xl-py3 relative"></nav> + ); + } + + renderEmptyNav() { + return ( + <nav className="py2 sm-py1 xl-py3 relative"> + <ul className="wrapper flex align-center"> + <li> + <a className="NavItem cursor-pointer flex align-center" href="/"> + <LogoIcon className="text-brand my2"></LogoIcon> + </a> + </li> + </ul> + </nav> + ); + } + + renderMainNav() { + return ( + <nav className="CheckBg CheckBg-offset sm-py2 sm-py1 xl-py3 relative bg-brand"> + <ul className="wrapper flex align-center"> + <li> + <a className="NavItem cursor-pointer text-white flex align-center my1" href="/"> + <LogoIcon className="text-white m1"></LogoIcon> + </a> + </li> + <li> + <DashboardsDropdown {...this.props}></DashboardsDropdown> + </li> + <li className="flex-align-right"> + <a className="rounded inline-block bg-white text-brand cursor-pointer p2 no-decoration" href="/q">New <span className="hide sm-show">Question</span></a> + <div className="inline-block text-white"><ProfileLink {...this.props}></ProfileLink></div> + </li> + </ul> + </nav> + ); + } + + render() { + let { context, user } = this.props; + + if (!user) return null; + + switch (context) { + case "admin": return this.renderAdminNav(); + case "auth": return this.renderAuthNav(); + case "none": return this.renderEmptyNav(); + case "setup": return null; + default: return this.renderMainNav(); + } + } +} + +Navbar.propTypes = { + context: PropTypes.string.isRequired, + location: PropTypes.string.isRequired, + user: PropTypes.object +}; diff --git a/resources/frontend_client/app/controllers.js b/resources/frontend_client/app/controllers.js index e6a80f52b167f690ad5b2246437512edeb9f0011..6227e326ff331da505cbac39ac8d2a2100173540 100644 --- a/resources/frontend_client/app/controllers.js +++ b/resources/frontend_client/app/controllers.js @@ -1,5 +1,8 @@ 'use strict'; +import Navbar from 'metabase/components/Navbar.react'; + + // Global Controllers var MetabaseControllers = angular.module('metabase.controllers', ['metabase.services', 'metabase.navbar.directives']); @@ -47,35 +50,71 @@ MetabaseControllers.controller('NotFound', ['AppState', function(AppState) { AppState.setAppContext('none'); }]); -MetabaseControllers.controller('Nav', ['$scope', '$routeParams', '$location', 'AppState', function($scope, $routeParams, $location, AppState) { - - $scope.isActive = function(location) { - return $location.path().indexOf(location) >= 0; - }; +MetabaseControllers.controller('Nav', ['$scope', '$routeParams', '$location', '$rootScope', 'AppState', 'Dashboard', + function($scope, $routeParams, $location, $rootScope, AppState, Dashboard) { + + function refreshDashboards() { + Dashboard.list({ + 'filterMode': 'all' + }, function (dashes) { + $scope.dashboards = dashes; + }, function (error) { + console.log('error getting dahsboards list', error); + }); + } - var setNavContext = function(context) { - switch (context) { - case "admin": - $scope.nav = 'admin'; - break; - case "setup": - $scope.nav = 'setup'; - break; - case "auth": - $scope.nav = 'auth'; - break; - case "none": - $scope.nav = 'none'; - break; - default: - $scope.nav = 'main'; + function setNavContext(context) { + switch (context) { + case "admin": + $scope.context = 'admin'; + break; + case "setup": + $scope.context = 'setup'; + break; + case "auth": + $scope.context = 'auth'; + break; + case "none": + $scope.context = 'none'; + break; + default: + $scope.context = 'main'; + } } - }; - $scope.$on('appstate:context-changed', function(event, newAppContext) { - setNavContext(newAppContext); - }); + $scope.Navbar = Navbar; + $scope.location = $location; - // initialize our state from the current AppState model, which we expect to have resolved already - setNavContext(AppState.model.appContext); -}]); + $scope.dashboards = []; + $scope.createDashboardFn = async function(newDashboard) { + var dashboard = await Dashboard.create(newDashboard).$promise; + $rootScope.$broadcast("dashboard:create", dashboard.id); + $location.path("/dash/" + dashboard.id); + + // this is important because it allows our caller to perform any of their own actions after the promis resolves + return dashboard; + }; + + $scope.$on('appstate:context-changed', function(event, newAppContext) { + setNavContext(newAppContext); + }); + + $scope.$on("dashboard:create", function(event, dashboardId) { + refreshDashboards(); + }); + + $scope.$on("dashboard:delete", function(event, dashboardId) { + refreshDashboards(); + }); + + $scope.$on("dashboard:update", function(event, dashboardId) { + refreshDashboards(); + }); + + // always initialize with a fresh listing + refreshDashboards(); + + // initialize our state from the current AppState model, which we expect to have resolved already + setNavContext(AppState.model.appContext); + } +]); diff --git a/resources/frontend_client/index_template.html b/resources/frontend_client/index_template.html index 14cd4a5538f3b8cb66957e22dd4bdcec07766a54..ae27cbb6f651f8a3fbb287eb824c7bf44ab9602d 100644 --- a/resources/frontend_client/index_template.html +++ b/resources/frontend_client/index_template.html @@ -16,110 +16,7 @@ </head> <body ng-controller="Metabase"> - <div class="Nav" ng-controller="Nav" ng-if="user" ng-cloak> - <!-- MAIN NAV --> - <nav class="CheckBg CheckBg-offset sm-py2 sm-py1 xl-py3 relative bg-brand" ng-show="nav === 'main'"> - <ul class="wrapper flex align-center"> - <li> - <a class="NavItem cursor-pointer text-white flex align-center my1" href="/"> - <mb-logo-icon class="text-white m1"></mb-logo-icon> - </a> - </li> - <li> - <div class="NavDropdown ml1 md-ml3" dropdown on-toggle="toggled(open)"> - <a class="NavDropdown-button NavItem text-white cursor-pointer p2 flex align-center" dropdown-toggle> - <span class="NavDropdown-button-layer"> - Dashboards - <mb-icon class="ml1" name="chevrondown" width="8px" height="8px"></mb-icon> - </span> - </a> - <div class="NavDropdown-content DashboardList" ng-controller="DashList"> - <div class="NavDropdown-content-layer text-white text-centered" ng-if="dashboards.length === 0"> - <div class="p2"><span class="QuestionCircle">?</span></div> - <div class="px2 py1 text-bold">You don’t have any dashboards yet.</div> - <div class="px2 pb2 text-light">Dashboards group visualizations for frequent questions in a single handy place.</div> - <div class="border-top border-light"> - <a class="Dropdown-item block text-white no-decoration" href="#" mb-dashboard-create>Create a new dashboard</a> - </div> - </div> - <ul class="NavDropdown-content-layer" ng-if="dashboards.length > 0"> - <li class="block" ng-repeat="dash in dashboards"> - <a class="Dropdown-item block text-white no-decoration" href="/dash/{{dash.id}}"> - <div class="flex text-bold"> - {{dash.name}} - <mb-icon class="ml1 flex flex-align-right align-center" name="lock" width="12px" height="12px" ng-if="dash.public_perms === 0"></mb-icon> - </div> - <div class="mt1 text-light text-brand-light" ng-if="dash.description"> - {{dash.description}} - </div> - </a> - </li> - <li class="block border-top border-light"> - <a class="Dropdown-item block text-white no-decoration" href="#" mb-dashboard-create>Create a new dashboard</a> - </li> - </ul> - </div> - </div> - </li> - <li class="flex-align-right"> - <a class="rounded inline-block bg-white text-brand cursor-pointer p2 no-decoration" href="/q">New <span class="hide sm-show">Question</span></a> - <div mb-profile-link class="inline-block text-white" user="user" context="nav"></div> - </li> - </ul> - </nav> - - <!-- ADMIN NAV --> - <nav class="AdminNav" ng-show="nav === 'admin'"> - <div class="wrapper flex align-center"> - <div class="NavTitle flex align-center"> - <mb-icon name="gear" class="AdminGear" width="22px" height="22px"></mb-icon> - <span class="NavItem-text ml1 hide sm-show">Site Administration</span> - </div> - - <!-- admin sections --> - <ul class="sm-ml4 flex flex-full"> - <li> - <a class="NavItem py1 px2 no-decoration" ng-class="{ 'is--selected' : isActive('/admin/settings')}" href="/admin/settings/"> - Settings - </a> - </li> - <li> - <a class="NavItem py1 px2 no-decoration" ng-class="{ 'is--selected' : isActive('/admin/people')}" href="/admin/people/"> - People - </a> - </li> - <li> - <a class="NavItem py1 px2 no-decoration" ng-class="{ 'is--selected' : isActive('/admin/metadata')}" href="/admin/metadata/"> - Metadata - </a> - </li> - <li> - <a class="NavItem py1 px2 no-decoration" ng-class="{ 'is--selected' : isActive('/admin/databases')}" href="/admin/databases/"> - Databases - </a> - </li> - </ul> - - <!-- user profile dropdown --> - <div mb-profile-link user="user" context="nav"></div> - </div> - </nav> - - <!-- AUTH NAV --> - <nav class="py2 sm-py1 xl-py3 relative" ng-show="nav === 'auth'"> - </nav> - - <!-- NO NAV --> - <nav class="py2 sm-py1 xl-py3 relative" ng-show="nav === 'none'"> - <ul class="wrapper flex align-center"> - <li> - <a class="NavItem cursor-pointer flex align-center" href="/"> - <mb-logo-icon class="text-brand my2"></mb-logo-icon> - </a> - </li> - </ul> - </nav> - </div> + <div class="Nav" ng-controller="Nav" mb-react-component="Navbar"></div> <main class="Main flex flex-column flex-full" ng-view></main> </body>