Skip to content
Snippets Groups Projects
Commit 0f827fbc authored by Allen Gilliland's avatar Allen Gilliland
Browse files

remove tabs on the homepage and make Saved Questions its own top level nav...

remove tabs on the homepage and make Saved Questions its own top level nav button with its own page separate from the homepage.
parent cec332e0
No related branches found
No related tags found
No related merge requests found
Showing
with 243 additions and 280 deletions
"use strict";
import { createAction } from "redux-actions";
import moment from "moment";
// HACK: just use our Angular resources for now
function AngularResourceProxy(serviceName, methods) {
methods.forEach((methodName) => {
this[methodName] = function(...args) {
let service = angular.element(document.querySelector("body")).injector().get(serviceName);
return service[methodName](...args).$promise;
}
});
}
// similar to createAction but accepts a (redux-thunk style) thunk and dispatches based on whether
// the promise returned from the thunk resolves or rejects, similar to redux-promise
function createThunkAction(actionType, actionThunkCreator) {
return function(...actionArgs) {
var thunk = actionThunkCreator(...actionArgs);
return async function(dispatch, getState) {
try {
let payload = await thunk(dispatch, getState);
dispatch({ type: actionType, payload });
} catch (error) {
dispatch({ type: actionType, payload: error, error: true });
throw error;
}
}
}
}
// resource wrappers
const CardApi = new AngularResourceProxy("Card", ["list"]);
const MetadataApi = new AngularResourceProxy("Metabase", ["db_list", "db_metadata"]);
// action constants
export const SET_CARDS_FILTER = 'SET_CARDS_FILTER';
export const FETCH_CARDS = 'FETCH_CARDS';
export const FETCH_DATABASES = 'FETCH_DATABASES';
export const CLEAR_DATABASE_METADATA = 'CLEAR_DATABASE_METADATA';
export const FETCH_DATABASE_METADATA = 'FETCH_DATABASE_METADATA';
// action creators
export const setCardsFilter = createThunkAction(SET_CARDS_FILTER, function(filterDef) {
return function(dispatch, getState) {
let { cardsFilter } = getState();
let { database, table } = filterDef;
if (database && !table && database !== cardsFilter.database) {
// user has picked a database different from any previous choice
dispatch(clearDatabaseMetadata());
dispatch(fetchDatabaseMetadata(database));
dispatch(fetchCards('database', database));
} else if (database && !table && database === cardsFilter.database) {
// user is simply clearing the table selection
dispatch(fetchCards('database', database));
} else if (database && table) {
// user has chosen a specific table to filter on
dispatch(fetchCards('table', table));
} else if (!database && cardsFilter.database) {
// clearing out all filters
dispatch(fetchCards('all'));
}
return filterDef;
};
});
export const fetchCards = createThunkAction(FETCH_CARDS, function(filterMode, filterModelId) {
return async function(dispatch, getState) {
let cards = await CardApi.list({'filterMode' : filterMode, 'model_id' : filterModelId });
for (var c of cards) {
c.created_at = moment(c.created_at);
c.updated_at = moment(c.updated_at);
c.icon = c.display ? 'illustration_visualization_' + c.display : null;
}
return cards;
};
});
export const fetchDatabases = createThunkAction(FETCH_DATABASES, function() {
return async function(dispatch, getState) {
return await MetadataApi.db_list();
};
});
export const clearDatabaseMetadata = createAction(CLEAR_DATABASE_METADATA);
export const fetchDatabaseMetadata = createThunkAction(FETCH_DATABASE_METADATA, function(database_id) {
return async function(dispatch, getState) {
return await MetadataApi.db_metadata({'dbId': database_id});
};
});
'use strict'; 'use strict';
import { createStore, applyMiddleware, combineReducers, compose } from 'redux';
import promiseMiddleware from 'redux-promise';
import thunkMidleware from "redux-thunk";
import _ from "underscore"; import _ from "underscore";
import MetabaseAnalytics from '../lib/analytics'; import MetabaseAnalytics from '../lib/analytics';
...@@ -15,62 +18,31 @@ import QueryVisualization from '../query_builder/QueryVisualization.react'; ...@@ -15,62 +18,31 @@ import QueryVisualization from '../query_builder/QueryVisualization.react';
import Query from "metabase/lib/query"; import Query from "metabase/lib/query";
import { serializeCardForUrl, deserializeCardFromUrl, cleanCopyCard, urlForCardState } from './card.util'; import { serializeCardForUrl, deserializeCardFromUrl, cleanCopyCard, urlForCardState } from './card.util';
// Card Controllers import SavedQuestionsApp from './containers/SavedQuestionsApp.react';
var CardControllers = angular.module('metabase.card.controllers', []); import * as reducers from './reducers';
CardControllers.controller('CardList', ['$scope', '$location', 'Card', function($scope, $location, Card) {
// $scope.cards: the list of cards being displayed const finalCreateStore = compose(
applyMiddleware(
thunkMidleware,
promiseMiddleware
),
createStore
);
$scope.deleteCard = function(cardId) { const reducer = combineReducers(reducers);
Card.delete({
'cardId': cardId
}, function(result) {
$scope.cards = _.filter($scope.cards, function(card) {
return card.id != cardId;
});
$scope.searchFilter = undefined;
});
};
$scope.unfavorite = function(unfavIdx) { // Card Controllers
var cardToUnfav = $scope.cards[unfavIdx]; var CardControllers = angular.module('metabase.card.controllers', []);
Card.unfavorite({
'cardId': cardToUnfav.id
}, function(result) {
$scope.cards.splice(unfavIdx, 1);
});
};
$scope.filter = function(filterMode) {
$scope.filterMode = filterMode;
Card.list({
'filterMode': filterMode
}, function(cards) {
$scope.cards = cards;
}, function(error) {
console.log('error getting cards list', error);
});
};
$scope.inlineSave = function(card, idx) { CardControllers.controller('CardList', ['$scope', '$location', function($scope, $location) {
Card.update(card, function(result) { $scope.Component = SavedQuestionsApp;
if (result && !result.error) { $scope.props = {
$scope.cards[idx] = result; user: $scope.user,
} else { onChangeLocation: function(url) {
return "error"; $scope.$apply(() => $location.url(url));
} }
});
}; };
$scope.store = finalCreateStore(reducer, {});
// determine the appropriate filter to start with
if ($scope.hash && $scope.hash === 'fav') {
$scope.filter('fav');
} else {
$scope.filter('all');
}
}]); }]);
CardControllers.controller('CardDetail', [ CardControllers.controller('CardDetail', [
......
...@@ -29,4 +29,14 @@ Card.config(['$routeProvider', function($routeProvider) { ...@@ -29,4 +29,14 @@ Card.config(['$routeProvider', function($routeProvider) {
templateUrl: '/app/card/partials/card_detail.html', templateUrl: '/app/card/partials/card_detail.html',
controller: 'CardDetail' controller: 'CardDetail'
}); });
$routeProvider.when('/card/', {
template: '<div mb-redux-component class="flex flex-column flex-full" />',
controller: 'CardList',
resolve: {
appState: ["AppState", function(AppState) {
return AppState.init();
}]
}
});
}]); }]);
"use strict";
import React, { Component, PropTypes } from "react";
import Cards from "./Cards.react";
import CardFilters from "./CardFilters.react";
export default class SavedQuestions extends Component {
render() {
return (
<div className="flex flex-column flex-full">
<div className="relative felx flex-column flex-full md-pl4">
<div className="HomeLayout">
<div className="HomeLayout-mainColumn">
<Cards {...this.props} />
</div>
</div>
<div className="HomeLayout-sidebar">
<CardFilters {...this.props} />
</div>
</div>
</div>
);
}
}
SavedQuestions.propTypes = {
dispatch: PropTypes.func.isRequired
};
"use strict";
import React, { Component, PropTypes } from "react";
import { connect } from "react-redux";
import SavedQuestions from "../components/SavedQuestions.react";
import { savedQuestionsSelectors } from "../selectors";
@connect(savedQuestionsSelectors)
export default class SavedQuestionsApp extends Component {
render() {
return <SavedQuestions {...this.props} />;
}
}
"use strict";
import { handleActions } from 'redux-actions';
import {
SET_CARDS_FILTER,
FETCH_CARDS,
FETCH_DATABASES,
CLEAR_DATABASE_METADATA,
FETCH_DATABASE_METADATA
} from './actions';
export const cardsFilter = handleActions({
[SET_CARDS_FILTER]: { next: (state, { payload }) => payload }
}, {database: null, table: null});
export const cards = handleActions({
[FETCH_CARDS]: { next: (state, { payload }) => payload }
}, null);
export const databases = handleActions({
[FETCH_DATABASES]: { next: (state, { payload }) => payload }
}, []);
export const databaseMetadata = handleActions({
[FETCH_DATABASE_METADATA]: { next: (state, { payload }) => payload },
[CLEAR_DATABASE_METADATA]: { next: (state, { payload }) => null }
}, null);
"use strict";
import { createSelector } from 'reselect';
// our master selector which combines all of our partial selectors above
export const savedQuestionsSelectors = createSelector(
[state => state.cardsFilter,
state => state.cards,
state => state.databases,
state => state.databaseMetadata],
(cardsFilter, cards, databases, databaseMetadata) => ({cardsFilter, cards, databases, databaseMetadata})
);
\ No newline at end of file
...@@ -89,8 +89,13 @@ export default class Navbar extends Component { ...@@ -89,8 +89,13 @@ export default class Navbar extends Component {
<li> <li>
<DashboardsDropdown {...this.props}></DashboardsDropdown> <DashboardsDropdown {...this.props}></DashboardsDropdown>
</li> </li>
<li className="flex-align-right"> <li>
<a className="NavItem cursor-pointer text-white no-decoration flex align-center p2" href="/card/">Saved Questions</a>
</li>
<li className="ml2">
<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> <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>
</li>
<li className="flex-align-right">
<div className="inline-block text-white"><ProfileLink {...this.props}></ProfileLink></div> <div className="inline-block text-white"><ProfileLink {...this.props}></ProfileLink></div>
</li> </li>
</ul> </ul>
......
"use strict"; "use strict";
import _ from "underscore"; import _ from "underscore";
import { createAction } from "redux-actions";
import moment from "moment"; import moment from "moment";
...@@ -34,51 +33,14 @@ function createThunkAction(actionType, actionThunkCreator) { ...@@ -34,51 +33,14 @@ function createThunkAction(actionType, actionThunkCreator) {
// resource wrappers // resource wrappers
const ActivityApi = new AngularResourceProxy("Activity", ["list", "recent_views"]); const ActivityApi = new AngularResourceProxy("Activity", ["list", "recent_views"]);
const CardApi = new AngularResourceProxy("Card", ["list"]);
const MetadataApi = new AngularResourceProxy("Metabase", ["db_list", "db_metadata"]);
// action constants // action constants
export const SET_SELECTED_TAB = 'SET_SELECTED_TAB';
export const SET_CARDS_FILTER = 'SET_CARDS_FILTER';
export const FETCH_ACTIVITY = 'FETCH_ACTIVITY'; export const FETCH_ACTIVITY = 'FETCH_ACTIVITY';
export const FETCH_CARDS = 'FETCH_CARDS';
export const FETCH_DATABASES = 'FETCH_DATABASES';
export const CLEAR_DATABASE_METADATA = 'CLEAR_DATABASE_METADATA';
export const FETCH_DATABASE_METADATA = 'FETCH_DATABASE_METADATA';
export const FETCH_RECENT_VIEWS = 'FETCH_RECENT_VIEWS'; export const FETCH_RECENT_VIEWS = 'FETCH_RECENT_VIEWS';
// action creators // action creators
export const setSelectedTab = createAction(SET_SELECTED_TAB);
export const setCardsFilter = createThunkAction(SET_CARDS_FILTER, function(filterDef) {
return function(dispatch, getState) {
let { cardsFilter } = getState();
let { database, table } = filterDef;
if (database && !table && database !== cardsFilter.database) {
// user has picked a database different from any previous choice
dispatch(clearDatabaseMetadata());
dispatch(fetchDatabaseMetadata(database));
dispatch(fetchCards('database', database));
} else if (database && !table && database === cardsFilter.database) {
// user is simply clearing the table selection
dispatch(fetchCards('database', database));
} else if (database && table) {
// user has chosen a specific table to filter on
dispatch(fetchCards('table', table));
} else if (!database && cardsFilter.database) {
// clearing out all filters
dispatch(fetchCards('all'));
}
return filterDef;
};
});
export const fetchActivity = createThunkAction(FETCH_ACTIVITY, function() { export const fetchActivity = createThunkAction(FETCH_ACTIVITY, function() {
return async function(dispatch, getState) { return async function(dispatch, getState) {
...@@ -102,31 +64,3 @@ export const fetchRecentViews = createThunkAction(FETCH_RECENT_VIEWS, function() ...@@ -102,31 +64,3 @@ export const fetchRecentViews = createThunkAction(FETCH_RECENT_VIEWS, function()
return recentViews; return recentViews;
}; };
}); });
export const fetchCards = createThunkAction(FETCH_CARDS, function(filterMode, filterModelId) {
return async function(dispatch, getState) {
let cards = await CardApi.list({'filterMode' : filterMode, 'model_id' : filterModelId });
for (var c of cards) {
c.created_at = moment(c.created_at);
c.updated_at = moment(c.updated_at);
c.icon = c.display ? 'illustration_visualization_' + c.display : null;
}
return cards;
};
});
export const fetchDatabases = createThunkAction(FETCH_DATABASES, function() {
return async function(dispatch, getState) {
return await MetadataApi.db_list();
};
});
export const clearDatabaseMetadata = createAction(CLEAR_DATABASE_METADATA);
export const fetchDatabaseMetadata = createThunkAction(FETCH_DATABASE_METADATA, function(database_id) {
return async function(dispatch, getState) {
return await MetadataApi.db_metadata({'dbId': database_id});
};
});
"use strict";
import React, { Component, PropTypes } from "react";
import cx from "classnames";
import { setSelectedTab } from '../actions';
const ACTIVITY_TAB = 'activity';
const CARDS_TAB = 'cards';
export default class HeaderTabs extends Component {
onClickActivityTab() {
if (this.props.selectedTab !== ACTIVITY_TAB) {
this.props.dispatch(setSelectedTab(ACTIVITY_TAB));
}
}
onClickQuestionsTab() {
if (this.props.selectedTab !== CARDS_TAB) {
this.props.dispatch(setSelectedTab(CARDS_TAB));
}
}
render() {
const { selectedTab } = this.props;
const activityTab = cx({
'HomeTab': true,
'inline-block': true,
'HomeTab--active': (selectedTab === ACTIVITY_TAB),
'text-dark': (selectedTab === ACTIVITY_TAB)
});
const questionsTab = cx({
'HomeTab': true,
'inline-block': true,
'HomeTab--active': (selectedTab === CARDS_TAB),
'text-dark': (selectedTab === CARDS_TAB)
});
return (
<div className="text-white" style={{backgroundColor: 'transparent'}}>
<a className={activityTab} style={{marginLeft: '10px'}} href="/">Activity</a>
<a className={questionsTab} href="/?questions">Saved Questions</a>
</div>
);
}
}
HeaderTabs.propTypes = {
selectedTab: PropTypes.string.isRequired
}
...@@ -5,11 +5,8 @@ import React, { Component, PropTypes } from "react"; ...@@ -5,11 +5,8 @@ import React, { Component, PropTypes } from "react";
import Greeting from "metabase/lib/greeting"; import Greeting from "metabase/lib/greeting";
import Modal from "metabase/components/Modal.react"; import Modal from "metabase/components/Modal.react";
import HeaderTabs from "./HeaderTabs.react";
import Activity from "./Activity.react"; import Activity from "./Activity.react";
import Cards from "./Cards.react";
import RecentViews from "./RecentViews.react"; import RecentViews from "./RecentViews.react";
import CardFilters from "./CardFilters.react";
import Smile from './Smile.react'; import Smile from './Smile.react';
import NewUserOnboardingModal from './NewUserOnboardingModal.react'; import NewUserOnboardingModal from './NewUserOnboardingModal.react';
...@@ -38,7 +35,7 @@ export default class Homepage extends Component { ...@@ -38,7 +35,7 @@ export default class Homepage extends Component {
} }
render() { render() {
const { selectedTab, user } = this.props; const { user } = this.props;
return ( return (
<div className="flex flex-column flex-full"> <div className="flex flex-column flex-full">
...@@ -51,37 +48,23 @@ export default class Homepage extends Component { ...@@ -51,37 +48,23 @@ export default class Homepage extends Component {
<div className="CheckBg bg-brand text-white md-pl4"> <div className="CheckBg bg-brand text-white md-pl4">
<div className="HomeLayout"> <div className="HomeLayout">
<div className="HomeLayout-mainColumn"> <div className="HomeLayout-mainColumn">
<header style={this.styles.headerGreeting} className="flex align-center pb4"> <header style={this.styles.headerGreeting} className="flex align-center pb4 pt1">
<span className="float-left mr1"> <span className="float-left mr1">
<Smile /> <Smile />
</span> </span>
<span>{(user) ? this.state.greeting + ' ' + user.first_name : this.state.greeting}</span> <span>{(user) ? this.state.greeting + ' ' + user.first_name : this.state.greeting}</span>
</header> </header>
<div className="">
<span className="float-left UserNick bg-brand text-brand mr3">
<span className="UserInitials">MB</span>
</span>
<HeaderTabs {...this.props} />
</div>
</div> </div>
</div> </div>
</div> </div>
<div className="relative felx flex-column flex-full md-pl4"> <div className="relative felx flex-column flex-full md-pl4">
<div className="HomeLayout"> <div className="HomeLayout">
<div className="HomeLayout-mainColumn"> <div className="HomeLayout-mainColumn">
{ selectedTab === 'activity' ? <Activity {...this.props} />
<Activity {...this.props} />
:
<Cards {...this.props} />
}
</div> </div>
</div> </div>
<div className="HomeLayout-sidebar"> <div className="HomeLayout-sidebar">
{ selectedTab === 'activity' ? <RecentViews {...this.props} />
<RecentViews {...this.props} />
:
<CardFilters {...this.props} />
}
</div> </div>
</div> </div>
</div> </div>
...@@ -93,6 +76,5 @@ Homepage.propTypes = { ...@@ -93,6 +76,5 @@ Homepage.propTypes = {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
onChangeLocation: PropTypes.func.isRequired, onChangeLocation: PropTypes.func.isRequired,
showOnboarding: PropTypes.bool.isRequired, showOnboarding: PropTypes.bool.isRequired,
selectedTab: PropTypes.string.isRequired,
user: PropTypes.object.isRequired user: PropTypes.object.isRequired
}; };
...@@ -6,20 +6,12 @@ import thunkMidleware from "redux-thunk"; ...@@ -6,20 +6,12 @@ import thunkMidleware from "redux-thunk";
import HomepageApp from './containers/HomepageApp.react'; import HomepageApp from './containers/HomepageApp.react';
import * as reducers from './reducers'; import * as reducers from './reducers';
import { setSelectedTab } from './actions';
// import { devTools, persistState } from 'redux-devtools';
// import { LogMonitor } from 'redux-devtools/lib/react';
// import loggerMiddleware from 'redux-logger';
const finalCreateStore = compose( const finalCreateStore = compose(
applyMiddleware( applyMiddleware(
thunkMidleware, thunkMidleware,
promiseMiddleware promiseMiddleware
// ,loggerMiddleware
), ),
// devTools(),
// persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/)),
createStore createStore
); );
...@@ -36,36 +28,5 @@ HomeControllers.controller('Homepage', ['$scope', '$location', '$route', '$route ...@@ -36,36 +28,5 @@ HomeControllers.controller('Homepage', ['$scope', '$location', '$route', '$route
$scope.$apply(() => $location.url(url)); $scope.$apply(() => $location.url(url));
} }
}; };
$scope.store = finalCreateStore(reducer, { selectedTab: 'activity' }); $scope.store = finalCreateStore(reducer, { });
// $scope.monitor = LogMonitor;
// mildly hacky way to prevent reloading controllers as the URL changes
var route = $route.current;
$scope.$on('$locationChangeSuccess', function (event) {
var newParams = $route.current.params;
var oldParams = route.params;
if ($route.current.$$route.controller === 'Homepage') {
$route.current = route;
angular.forEach(oldParams, function(value, key) {
delete $route.current.params[key];
delete $routeParams[key];
});
angular.forEach(newParams, function(value, key) {
$route.current.params[key] = value;
$routeParams[key] = value;
});
}
});
$scope.routeParams = $routeParams;
$scope.$watch('routeParams', function() {
if ($scope.routeParams.questions === true) {
$scope.store.dispatch(setSelectedTab('cards'));
} else {
$scope.store.dispatch(setSelectedTab('activity'));
}
}, true);
}]); }]);
...@@ -3,25 +3,11 @@ ...@@ -3,25 +3,11 @@
import { handleActions } from 'redux-actions'; import { handleActions } from 'redux-actions';
import { import {
SET_SELECTED_TAB,
SET_CARDS_FILTER,
FETCH_ACTIVITY, FETCH_ACTIVITY,
FETCH_CARDS,
FETCH_DATABASES,
CLEAR_DATABASE_METADATA,
FETCH_DATABASE_METADATA,
FETCH_RECENT_VIEWS FETCH_RECENT_VIEWS
} from './actions'; } from './actions';
export const selectedTab = handleActions({
[SET_SELECTED_TAB]: { next: (state, { payload }) => payload }
}, 'activity');
export const cardsFilter = handleActions({
[SET_CARDS_FILTER]: { next: (state, { payload }) => payload }
}, {database: null, table: null});
export const activity = handleActions({ export const activity = handleActions({
[FETCH_ACTIVITY]: { next: (state, { payload }) => payload } [FETCH_ACTIVITY]: { next: (state, { payload }) => payload }
...@@ -30,20 +16,3 @@ export const activity = handleActions({ ...@@ -30,20 +16,3 @@ export const activity = handleActions({
export const recentViews = handleActions({ export const recentViews = handleActions({
[FETCH_RECENT_VIEWS]: { next: (state, { payload }) => payload } [FETCH_RECENT_VIEWS]: { next: (state, { payload }) => payload }
}, []); }, []);
export const cards = handleActions({
[FETCH_CARDS]: { next: (state, { payload }) => payload }
}, null);
export const databases = handleActions({
[FETCH_DATABASES]: { next: (state, { payload }) => payload }
}, []);
export const databaseMetadata = handleActions({
[FETCH_DATABASE_METADATA]: { next: (state, { payload }) => payload },
[CLEAR_DATABASE_METADATA]: { next: (state, { payload }) => null }
}, null);
...@@ -2,21 +2,9 @@ ...@@ -2,21 +2,9 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
const selectedTabSelector = state => state.selectedTab;
const cardsFilterSelector = state => state.cardsFilter;
const activitySelector = state => state.activity;
const recentViewsSelector = state => state.recentViews;
const cardsSelector = state => state.cards;
const databasesSelector = state => state.databases;
const databaseMetadataSelector = state => state.databaseMetadata;
// our master selector which combines all of our partial selectors above
export const homepageSelectors = createSelector( export const homepageSelectors = createSelector(
[selectedTabSelector, cardsFilterSelector, activitySelector, recentViewsSelector, cardsSelector, databasesSelector, databaseMetadataSelector], [state => state.activity,
(selectedTab, cardsFilter, activity, recentViews, cards, databases, databaseMetadata) => ({selectedTab, cardsFilter, activity, recentViews, cards, databases, databaseMetadata}) state => state.recentViews],
(activity, recentViews) => ({activity, recentViews})
); );
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment