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

basic scaffolding for new homepage implementation using redux + react.

parent 33f85b7d
No related branches found
No related tags found
No related merge requests found
Showing
with 548 additions and 39 deletions
'use strict';
var ActivityServices = angular.module('metabase.activity.services', ['ngResource', 'ngCookies']);
ActivityServices.factory('Activity', ['$resource', '$cookies', function($resource, $cookies) {
return $resource('/api/activity', {}, {
list: {
method: 'GET',
isArray: true
}
});
}]);
......@@ -5,6 +5,7 @@ var Metabase = angular.module('metabase', [
'ngRoute',
'ngCookies',
'ui.bootstrap', // bootstrap LIKE widgets via angular directives
'metabase.activity.services',
'metabase.auth',
'metabase.filters',
'metabase.directives',
......
......@@ -8,7 +8,7 @@ var CardServices = angular.module('metabase.card.services', ['ngResource', 'ngCo
CardServices.factory('Card', ['$resource', '$cookies', function($resource, $cookies) {
return $resource('/api/card/:cardId', {}, {
list: {
url: '/api/card/?org=:orgId&f=:filterMode',
url: '/api/card/?f=:filterMode',
method: 'GET',
isArray: true
},
......
"use strict";
import { createAction } from "redux-actions";
import { normalize, Schema, arrayOf } from "normalizr";
// 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;
}
}
}
}
// normalizr schemas
const activity = new Schema('activity');
const card = new Schema('card');
// const database = new Schema('database');
// const table = new Schema('table');
// const user = new Schema('user');
// activity.define({
// user: user,
// database: database,
// table: table
// })
// action constants
export const SET_SELECTED_TAB = 'SET_SELECTED_TAB';
export const FETCH_ACTIVITY = 'FETCH_ACTIVITY';
export const FETCH_CARDS = 'FETCH_CARDS';
// resource wrappers
const Activity = new AngularResourceProxy("Activity", ["list"]);
const Card = new AngularResourceProxy("Card", ["list"]);
// action creators
// select tab
export const setSelectedTab = createAction(SET_SELECTED_TAB);
export const fetchActivity = createThunkAction(FETCH_ACTIVITY, function() {
return async function(dispatch, getState) {
let activityItems = await Activity.list();
return normalize(activityItems, arrayOf(activity));
};
});
export const fetchCards = createThunkAction(FETCH_CARDS, function(filterMode) {
return async function(dispatch, getState) {
let cards = await Card.list({'filterMode' : filterMode});
return normalize(cards, arrayOf(card));
};
});
// fetch recent items (user)
// fetch database list
// fetch table list (database)
"use strict";
import React, { Component, PropTypes } from "react";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.react";
import { fetchActivity } from "../actions";
export default class Activity extends Component {
constructor() {
super();
this.state = { error: null };
}
async componentDidMount() {
try {
await this.props.dispatch(fetchActivity());
} catch (error) {
this.setState({ error });
}
}
renderActivity(activity) {
// colors for each user
// do we show user initials or the MB user icon
return (
<div>
{activity.map(item =>
<div key={item.id} className="ActivityItem">
user = {item.user}, {item.topic}
</div>
)}
</div>
);
}
render() {
let { activity } = this.props;
let { error } = this.state;
return (
<LoadingAndErrorWrapper className="" loading={!activity} error={error}>
{() =>
<div className="full flex flex-column">
<div className="">
{ activity.length === 0 ?
<div className="flex flex-column layout-centered">
<span className="QuestionCircle">?</span>
<div className="text-normal mt3 mb1">Hmmm, looks like nothing has happened yet.</div>
<div className="text-normal text-grey-2">Save a question and get this baby going!</div>
</div>
:
this.renderActivity(activity)
}
</div>
</div>
}
</LoadingAndErrorWrapper>
);
}
}
Activity.propTypes = {
dispatch: PropTypes.func.isRequired,
onChangeLocation: PropTypes.func.isRequired,
onDashboardDeleted: PropTypes.func.isRequired,
visualizationSettingsApi: PropTypes.object.isRequired
};
"use strict";
import React, { Component, PropTypes } from "react";
export default class CardFilters extends Component {
render() {
return (
<div>
Card Filters
</div>
);
}
}
"use strict";
import React, { Component, PropTypes } from "react";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.react";
import { fetchCards } from "../actions";
export default class Cards extends Component {
constructor() {
super();
this.state = { error: null };
}
async componentDidMount() {
try {
await this.props.dispatch(fetchCards('all'));
} catch (error) {
this.setState({ error });
}
}
renderCards(cards) {
// colors for each user
// do we show user initials or the MB user icon
return (
<div>
{cards.map(card =>
<div key={card.id} className="Card">
{card.name}, {card.display}
</div>
)}
</div>
);
}
render() {
let { cards } = this.props;
let { error } = this.state;
return (
<LoadingAndErrorWrapper className="" loading={!cards} error={error}>
{() =>
<div className="full flex flex-column">
<div className="">
{ cards.length === 0 ?
<div className="flex flex-column layout-centered">
<span className="QuestionCircle">?</span>
<div className="text-normal mt3 mb1">Hmmm, looks like nothing has happened yet.</div>
<div className="text-normal text-grey-2">Save a question and get this baby going!</div>
</div>
:
this.renderCards(cards)
}
</div>
</div>
}
</LoadingAndErrorWrapper>
);
}
}
"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;
// component = Tab (selected, label, action)
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="bg-brand text-white">
<a className={activityTab} onClick={() => this.onClickActivityTab()}>Activity</a>
<a className={questionsTab} onClick={() => this.onClickQuestionsTab()}>Saved Questions</a>
</div>
);
}
}
"use strict";
import React, { Component, PropTypes } from "react";
import Greeting from "metabase/lib/greeting";
import Icon from "metabase/components/Icon.react";
import HeaderTabs from "./HeaderTabs.react";
import Activity from "./Activity.react";
import Cards from "./Cards.react";
import RecentViews from "./RecentViews.react";
import CardFilters from "./CardFilters.react";
export default class Homepage extends Component {
constructor(props) {
super(props);
this.state = {
greeting: Greeting.simpleGreeting()
};
this.styles = {
main: {
marginRight: "346px"
},
mainWrapper: {
width: "100%",
margin: "0 auto",
paddingLeft: "12em",
paddingRight: "3em"
},
headerGreeting: {
fontSize: "x-large"
}
};
}
render() {
console.log('props=', this.props);
const { selectedTab, user } = this.props;
return (
<div>
<div className="bg-brand text-white">
<div style={this.styles.main}>
<div style={this.styles.mainWrapper}>
<header style={this.styles.headerGreeting} className="pb4">
<span className="float-left"><Icon name={'star'}></Icon></span>
<span className="pl1">{(user) ? this.state.greeting + ' ' + user.first_name : this.state.greeting}</span>
</header>
<div className="ml4">
<HeaderTabs {...this.props} />
</div>
</div>
</div>
</div>
<div className="">
<div style={this.styles.main}>
<div style={this.styles.mainWrapper}>
{ selectedTab === 'activity' ?
<Activity {...this.props} />
:
<Cards {...this.props} />
}
</div>
</div>
<div className="">
{ selectedTab === 'activity' ?
<RecentViews {...this.props} />
:
<CardFilters {...this.props} />
}
</div>
</div>
</div>
);
}
}
Homepage.propTypes = {
dispatch: PropTypes.func.isRequired,
onChangeLocation: PropTypes.func.isRequired
};
"use strict";
import React, { Component, PropTypes } from "react";
export default class RecentViews extends Component {
render() {
return (
<div>
Recent Views
</div>
);
}
}
"use strict";
import React, { Component, PropTypes } from "react";
import { connect } from "react-redux";
import Homepage from "../components/Homepage.react";
import { homepageSelectors } from "../selectors";
@connect(homepageSelectors)
export default class HomepageApp extends Component {
render() {
return <Homepage {...this.props} />;
}
}
......@@ -2,50 +2,52 @@
import Table from "metabase/lib/table";
import { createStore, applyMiddleware, combineReducers, compose } from 'redux';
import promiseMiddleware from 'redux-promise';
import thunkMidleware from "redux-thunk";
import HomepageApp from './containers/HomepageApp.react';
import * as reducers from './reducers';
// import { devTools, persistState } from 'redux-devtools';
// import { LogMonitor } from 'redux-devtools/lib/react';
// import loggerMiddleware from 'redux-logger';
const finalCreateStore = compose(
applyMiddleware(
thunkMidleware,
promiseMiddleware
// ,loggerMiddleware
),
// devTools(),
// persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/)),
createStore
);
const reducer = combineReducers(reducers);
var HomeControllers = angular.module('metabase.home.controllers', [
'metabase.home.directives',
'metabase.metabase.services'
]);
HomeControllers.controller('Home', ['$scope', '$location', function($scope, $location) {
$scope.currentView = 'data';
$scope.showOnboarding = false;
if('new' in $location.search()) {
$scope.showOnboarding = true;
}
}]);
HomeControllers.controller('HomeGreeting', ['$scope', '$location', function($scope, $location) {
var greetingPrefixes = [
'Hey there',
'How\'s it going',
'Howdy',
'Greetings',
'Good to see you',
];
var subheadPrefixes = [
'What do you want to know?',
'What\'s on your mind?',
'What do you want to find out?',
];
function buildGreeting (greetingOptions, personalization) {
// TODO - this can result in an undefined thing
var randomGreetingIndex = Math.floor(Math.random() * (greetingOptions.length - 1));
var greeting = greetingOptions[randomGreetingIndex];
if(personalization) {
greeting = greeting + ' ' + personalization;
HomeControllers.controller('Homepage', ['$scope', '$location', function($scope, $location) {
$scope.Component = HomepageApp;
$scope.props = {
user: $scope.user,
showOnboarding: ('new' in $location.search()),
onChangeLocation: function(url) {
$scope.$apply(() => $location.url(url));
}
return greeting;
}
};
$scope.store = finalCreateStore(reducer, { selectedTab: 'activity' });
// TODO: reflect onboarding state
$scope.greeting = buildGreeting(greetingPrefixes, $scope.user.first_name);
$scope.subheading = subheadPrefixes[Math.floor(Math.random() * (subheadPrefixes.length - 1))];
// $scope.monitor = LogMonitor;
}]);
HomeControllers.controller('HomeDatabaseList', ['$scope', 'Metabase', function($scope, Metabase) {
$scope.databases = [];
......
......@@ -4,10 +4,10 @@ var Home = angular.module('metabase.home', [
'metabase.home.controllers',
]);
Home.config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider) {
Home.config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/', {
templateUrl: '/app/home/home.html',
controller: 'Home',
template: '<div mb-redux-component class="flex flex-column flex-full" />',
controller: 'Homepage',
resolve: {
appState: ["AppState", function(AppState) {
return AppState.init();
......
"use strict";
import { handleActions } from 'redux-actions';
import {
SET_SELECTED_TAB,
FETCH_ACTIVITY,
FETCH_CARDS
} from './actions';
export const selectedTab = handleActions({
[SET_SELECTED_TAB]: { next: (state, { payload }) => payload }
}, 'activity');
export const activity = handleActions({
[FETCH_ACTIVITY]: { next: (state, { payload }) => ({ ...payload.entities.activity }) }
}, {});
export const activityIdList = handleActions({
[FETCH_ACTIVITY]: { next: (state, { payload }) => payload.result }
}, null);
export const cards = handleActions({
[FETCH_CARDS]: { next: (state, { payload }) => ({ ...payload.entities.card }) }
}, {});
export const cardIdList = handleActions({
[FETCH_CARDS]: { next: (state, { payload }) => payload.result }
}, null);
\ No newline at end of file
"use strict";
// import _ from "underscore";
import { createSelector } from 'reselect';
const selectedTabSelector = state => state.selectedTab;
const activitySelector = state => state.activity;
const activityIdListSelector = state => state.activityIdList;
const cardsSelector = state => state.cards;
const cardIdListSelector = state => state.cardIdList;
const activityListSelector = createSelector(
[activityIdListSelector, activitySelector],
(activityIdList, activity) => activityIdList && activityIdList.map(id => activity[id])
);
const cardListSelector = createSelector(
[cardIdListSelector, cardsSelector],
(cardIdList, cards) => cardIdList && cardIdList.map(id => cards[id])
);
// our master selector which combines all of our partial selectors above
export const homepageSelectors = createSelector(
[selectedTabSelector, activityListSelector, cardListSelector],
(selectedTab, activity, cards) => ({selectedTab, activity, cards})
);
\ No newline at end of file
"use strict";
const greetingPrefixes = [
'Hey there',
'How\'s it going',
'Howdy',
'Greetings',
'Good to see you',
];
const subheadPrefixes = [
'What do you want to know?',
'What\'s on your mind?',
'What do you want to find out?',
];
var Greeting = {
simpleGreeting: function() {
// TODO - this can result in an undefined thing
const randomIndex = Math.floor(Math.random() * (greetingPrefixes.length - 1));
return greetingPrefixes[randomIndex];
},
sayHello: function(personalization) {
if(personalization) {
return Greeting.simpleGreeting() + ' ' + personalization;
} else {
return Greeting.simpleGreeting();
}
},
encourageCuriosity: function() {
// TODO - this can result in an undefined thing
const randomIndex = Math.floor(Math.random() * (subheadPrefixes.length - 1));
return subheadPrefixes[randomIndex];
}
};
export default Greeting;
\ 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