Skip to content
Snippets Groups Projects
Commit fbb2f0f4 authored by Kyle Doherty's avatar Kyle Doherty
Browse files

Merge branch 'activity_feed' of github.com:metabase/metabase-init into activity_feed

parents 6281eee3 ccf36a53
No related branches found
No related tags found
No related merge requests found
Showing
with 235 additions and 504 deletions
...@@ -39,22 +39,6 @@ MetabaseControllers.controller('Metabase', ['$scope', '$location', 'MetabaseCore ...@@ -39,22 +39,6 @@ MetabaseControllers.controller('Metabase', ['$scope', '$location', 'MetabaseCore
}]); }]);
MetabaseControllers.controller('Homepage', ['$scope', '$location', 'ipCookie', 'AppState',
function($scope, $location, ipCookie, AppState) {
// At this point in time we don't actually have any kind of content to show for a homepage, so we just use this
// as a simple routing controller which sends users somewhere relevant
if (AppState.model.currentUser) {
$location.path('/dash/');
} else {
// User is not logged-in, so always send them to login page
$location.path('/auth/login');
}
}
]);
MetabaseControllers.controller('Unauthorized', ['$scope', '$location', function($scope, $location) { MetabaseControllers.controller('Unauthorized', ['$scope', '$location', function($scope, $location) {
}]); }]);
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
import _ from "underscore"; import _ from "underscore";
import { createAction } from "redux-actions"; import { createAction } from "redux-actions";
import { normalize, Schema, arrayOf } from "normalizr";
import moment from "moment"; import moment from "moment";
...@@ -38,18 +37,6 @@ const ActivityApi = new AngularResourceProxy("Activity", ["list", "recent_views" ...@@ -38,18 +37,6 @@ const ActivityApi = new AngularResourceProxy("Activity", ["list", "recent_views"
const CardApi = new AngularResourceProxy("Card", ["list"]); const CardApi = new AngularResourceProxy("Card", ["list"]);
const MetadataApi = new AngularResourceProxy("Metabase", ["db_list", "db_metadata"]); const MetadataApi = new AngularResourceProxy("Metabase", ["db_list", "db_metadata"]);
// 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 // action constants
export const SET_SELECTED_TAB = 'SET_SELECTED_TAB'; export const SET_SELECTED_TAB = 'SET_SELECTED_TAB';
...@@ -63,7 +50,6 @@ export const FETCH_RECENT_VIEWS = 'FETCH_RECENT_VIEWS'; ...@@ -63,7 +50,6 @@ export const FETCH_RECENT_VIEWS = 'FETCH_RECENT_VIEWS';
// action creators // action creators
export const setSelectedTab = createAction(SET_SELECTED_TAB); export const setSelectedTab = createAction(SET_SELECTED_TAB);
export const setCardsFilter = createThunkAction(SET_CARDS_FILTER, function(filterDef) { export const setCardsFilter = createThunkAction(SET_CARDS_FILTER, function(filterDef) {
...@@ -96,14 +82,14 @@ export const setCardsFilter = createThunkAction(SET_CARDS_FILTER, function(filte ...@@ -96,14 +82,14 @@ export const setCardsFilter = createThunkAction(SET_CARDS_FILTER, function(filte
export const fetchActivity = createThunkAction(FETCH_ACTIVITY, function() { export const fetchActivity = createThunkAction(FETCH_ACTIVITY, function() {
return async function(dispatch, getState) { return async function(dispatch, getState) {
let activityItems = await ActivityApi.list(); let activity = await ActivityApi.list();
for (var ai of activityItems) { for (var ai of activity) {
ai.timestamp = moment(ai.timestamp); ai.timestamp = moment(ai.timestamp);
ai.hasLinkableModel = function() { ai.hasLinkableModel = function() {
return (_.contains(["card", "dashboard"], this.model)); return (_.contains(["card", "dashboard"], this.model));
}; };
} }
return normalize(activityItems, arrayOf(activity)); return activity;
}; };
}); });
...@@ -117,6 +103,7 @@ export const fetchRecentViews = createThunkAction(FETCH_RECENT_VIEWS, function() ...@@ -117,6 +103,7 @@ export const fetchRecentViews = createThunkAction(FETCH_RECENT_VIEWS, function()
}; };
}); });
export const fetchCards = createThunkAction(FETCH_CARDS, function(filterMode, filterModelId) { export const fetchCards = createThunkAction(FETCH_CARDS, function(filterMode, filterModelId) {
return async function(dispatch, getState) { return async function(dispatch, getState) {
let cards = await CardApi.list({'filterMode' : filterMode, 'model_id' : filterModelId }); let cards = await CardApi.list({'filterMode' : filterMode, 'model_id' : filterModelId });
...@@ -125,26 +112,21 @@ export const fetchCards = createThunkAction(FETCH_CARDS, function(filterMode, fi ...@@ -125,26 +112,21 @@ export const fetchCards = createThunkAction(FETCH_CARDS, function(filterMode, fi
c.updated_at = moment(c.updated_at); c.updated_at = moment(c.updated_at);
c.icon = c.display ? 'illustration_visualization_' + c.display : null; c.icon = c.display ? 'illustration_visualization_' + c.display : null;
} }
return normalize(cards, arrayOf(card)); return cards;
}; };
}); });
export const fetchDatabases = createThunkAction(FETCH_DATABASES, function() { export const fetchDatabases = createThunkAction(FETCH_DATABASES, function() {
return async function(dispatch, getState) { return async function(dispatch, getState) {
let databases = await MetadataApi.db_list(); return await MetadataApi.db_list();
return databases;
}; };
}); });
export const clearDatabaseMetadata = createAction(CLEAR_DATABASE_METADATA);
export const fetchDatabaseMetadata = createThunkAction(FETCH_DATABASE_METADATA, function(database_id) { export const fetchDatabaseMetadata = createThunkAction(FETCH_DATABASE_METADATA, function(database_id) {
return async function(dispatch, getState) { return async function(dispatch, getState) {
let metadata = await MetadataApi.db_metadata({'dbId': database_id}); return await MetadataApi.db_metadata({'dbId': database_id});
return metadata;
}; };
}); });
export const clearDatabaseMetadata = createAction(CLEAR_DATABASE_METADATA);
// fetch recent items (user)
// fetch table list (database)
...@@ -12,7 +12,7 @@ export default class AccordianItem extends Component { ...@@ -12,7 +12,7 @@ export default class AccordianItem extends Component {
return ( return (
<div key={itemId}> <div key={itemId}>
<div className="p2 text-grey-4 text-brand-hover" onClick={() => (onClickFn(itemId))}> <div className="p2 text-grey-4 text-brand-hover border-bottom" onClick={() => (onClickFn(itemId))}>
<span className="float-left">{title}</span> <span className="float-left">{title}</span>
<div className="text-right text-grey-2 text-brand-hover"> <div className="text-right text-grey-2 text-brand-hover">
{ isOpen ? { isOpen ?
...@@ -23,7 +23,7 @@ export default class AccordianItem extends Component { ...@@ -23,7 +23,7 @@ export default class AccordianItem extends Component {
</div> </div>
</div> </div>
{ isOpen ? { isOpen ?
<div className="articlewrap"> <div className="pt1">
<div className="article"> <div className="article">
{children} {children}
</div> </div>
......
...@@ -22,113 +22,6 @@ export default class Activity extends Component { ...@@ -22,113 +22,6 @@ export default class Activity extends Component {
this.colorClasses = ['bg-brand', 'bg-purple', 'bg-error', 'bg-green', 'bg-gold', 'bg-grey-2']; this.colorClasses = ['bg-brand', 'bg-purple', 'bg-error', 'bg-green', 'bg-gold', 'bg-grey-2'];
} }
userName(user, currentUser) {
if (user && user.id === currentUser.id) {
return "You";
} else if (user) {
return user.first_name;
} else {
return "Metabase";
}
}
activityDescription(item, user) {
switch (item.topic) {
case "card-create":
return {
userName: this.userName(item.user, user),
subject: "saved a question about",
subjectRefLink: Urls.tableRowsQuery(item.database_id, item.table_id),
subjectRefName: item.table.display_name,
body: item.details.name,
bodyLink: Urls.modelToUrl(item.model, item.model_id),
timeSince: item.timestamp.fromNow()
};
case "card-update":
return {
userName: this.userName(item.user, user),
subject: "saved a question about",
subjectRefLink: Urls.tableRowsQuery(item.database_id, item.table_id),
subjectRefName: item.table.display_name,
body: item.details.name,
bodyLink: Urls.modelToUrl(item.model, item.model_id),
timeSince: item.timestamp.fromNow()
};
case "card-delete":
return {
userName: this.userName(item.user, user),
subject: "deleted a question",
subjectRefLink: null,
subjectRefName: null,
body: item.details.name,
bodyLink: Urls.modelToUrl(item.model, item.model_id),
timeSince: item.timestamp.fromNow()
};
case "dashboard-create":
return {
userName: this.userName(item.user, user),
subject: "created a dashboard",
subjectRefLink: null,
subjectRefName: null,
body: item.details.name,
bodyLink: Urls.modelToUrl(item.model, item.model_id),
timeSince: item.timestamp.fromNow()
};
case "dashboard-delete":
return {
userName: this.userName(item.user, user),
subject: "deleted a dashboard",
subjectRefLink: null,
subjectRefName: null,
body: item.details.name,
bodyLink: Urls.modelToUrl(item.model, item.model_id),
timeSince: item.timestamp.fromNow()
};
case "dashboard-add-cards":
return {
userName: this.userName(item.user, user),
subject: "added a question to the dashboard -",
subjectRefLink: Urls.dashboard(item.model_id),
subjectRefName: item.details.name,
body: item.details.dashcards[0].name,
bodyLink: Urls.card(item.details.dashcards[0].card_id),
timeSince: item.timestamp.fromNow()
};
case "dashboard-remove-cards":
return {
userName: this.userName(item.user, user),
subject: "removed a question from the dashboard -",
subjectRefLink: Urls.dashboard(item.model_id),
subjectRefName: item.details.name,
body: item.details.dashcards[0].name,
bodyLink: Urls.card(item.details.dashcards[0].card_id),
timeSince: item.timestamp.fromNow()
};
case "database-sync":
return {
userName: this.userName(item.user, user),
subject: "received the latest data from",
subjectRefLink: null,
subjectRefName: item.database.name,
body: null,
bodyLink: null,
timeSince: item.timestamp.fromNow()
};
case "user-joined":
return {
userName: this.userName(item.user, user),
subject: "joined the party!",
subjectRefLink: null,
subjectRefName: null,
body: null,
bodyLink: null,
timeSince: item.timestamp.fromNow()
};
default: return "did some super awesome stuff thats hard to describe";
};
}
async componentDidMount() { async componentDidMount() {
try { try {
await this.props.dispatch(fetchActivity()); await this.props.dispatch(fetchActivity());
...@@ -171,6 +64,76 @@ export default class Activity extends Component { ...@@ -171,6 +64,76 @@ export default class Activity extends Component {
}); });
} }
userName(user, currentUser) {
if (user && user.id === currentUser.id) {
return "You";
} else if (user) {
return user.first_name;
} else {
return "Metabase";
}
}
activityDescription(item, user) {
// this is a base to start with
const description = {
userName: this.userName(item.user, user),
subject: "did some super awesome stuff thats hard to describe",
subjectRefLink: null,
subjectRefName: null,
body: null,
bodyLink: null,
timeSince: item.timestamp.fromNow()
};
switch (item.topic) {
case "card-create":
case "card-update":
description.subject = "saved a question about";
description.subjectRefLink = Urls.tableRowsQuery(item.database_id, item.table_id);
description.subjectRefName = item.table.display_name;
description.body = item.details.name;
description.bodyLink = (item.model_exists) ? Urls.modelToUrl(item.model, item.model_id) : null;
break;
case "card-delete":
description.subject = "deleted a question";
description.body = item.details.name;
break;
case "dashboard-create":
description.subject = "created a dashboard";
description.body = item.details.name;
description.bodyLink = (item.model_exists) ? Urls.modelToUrl(item.model, item.model_id) : null;
break;
case "dashboard-delete":
description.subject = "deleted a dashboard";
description.body = item.details.name;
break;
case "dashboard-add-cards":
description.subject = "added a question to the dashboard -";
description.subjectRefLink = (item.model_exists) ? Urls.dashboard(item.model_id) : null;
description.subjectRefName = item.details.name;
description.body = item.details.dashcards[0].name;
description.bodyLink = Urls.card(item.details.dashcards[0].card_id);
break;
case "dashboard-remove-cards":
description.subject = "removed a question from the dashboard -";
description.subjectRefLink = (item.model_exists) ? Urls.dashboard(item.model_id) : null;
description.subjectRefName = item.details.name;
description.body = item.details.dashcards[0].name;
description.bodyLink = Urls.card(item.details.dashcards[0].card_id);
break;
case "database-sync":
description.subject = "received the latest data from";
description.subjectRefName = item.database.name;
break;
case "user-joined":
description.subject = "joined the party!";
break;
};
return description;
}
initialsCssClasses(user) { initialsCssClasses(user) {
let { userColors } = this.state; let { userColors } = this.state;
...@@ -193,26 +156,6 @@ export default class Activity extends Component { ...@@ -193,26 +156,6 @@ export default class Activity extends Component {
} }
} }
renderActivity(activity) {
return (
<ul className="pt2 pb4 relative">
{activity.map(item => {
const description = this.activityDescription(item, item.user);
return (
<li key={item.id} className="mt3">
<ActivityItem
item={item}
description={description}
userColors={this.initialsCssClasses(item.user)}
/>
{ description.body ? <ActivityStory story={description} /> : null }
</li>
)
})}
</ul>
);
}
render() { render() {
let { activity } = this.props; let { activity } = this.props;
let { error } = this.state; let { error } = this.state;
...@@ -229,7 +172,18 @@ export default class Activity extends Component { ...@@ -229,7 +172,18 @@ export default class Activity extends Component {
<div className="text-normal text-grey-2">Save a question and get this baby going!</div> <div className="text-normal text-grey-2">Save a question and get this baby going!</div>
</div> </div>
: :
this.renderActivity(activity) <ul className="pt2 pb4 relative">
{activity.map(item =>
<li key={item.id} className="mt3">
<ActivityItem
item={item}
description={this.activityDescription(item, item.user)}
userColors={this.initialsCssClasses(item.user)}
/>
<ActivityStory story={this.activityDescription(item, item.user)} />
</li>
)}
</ul>
} }
</div> </div>
</div> </div>
...@@ -238,10 +192,3 @@ export default class Activity extends Component { ...@@ -238,10 +192,3 @@ export default class Activity extends Component {
); );
} }
} }
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 ActivityDescription extends Component {
constructor(props) {
super(props);
}
render() {
let { description } = this.props;
return (
<div className="ml2 full flex align-center">
<div className="text-grey-4">
<span className="text-dark">{description.userName}</span>
&nbsp;{description.subject}&nbsp;
{ description.subjectRefName && description.subjectRefLink ?
<a className="link text-dark" href={description.subjectRefLink}>{description.subjectRefName}</a>
: null }
{ description.subjectRefName && !description.subjectRefLink ?
<span className="text-dark">{description.subjectRefName}</span>
: null }
</div>
<div className="flex-align-right text-right text-grey-2">
{description.timeSince}
</div>
</div>
);
}
}
'use strict'; 'use strict';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Icon from 'metabase/components/Icon.react'; import Icon from 'metabase/components/Icon.react';
import ActivityDescription from './ActivityDescription.react';
export default class ActivityItem extends Component { export default class ActivityItem extends Component {
constructor() {
super() constructor(props) {
super(props);
this.styles = { this.styles = {
initials: { initials: {
borderRadius: '0px', borderRadius: '0px',
} }
} };
} }
userInitials(user) { userInitials(user) {
...@@ -29,7 +32,8 @@ export default class ActivityItem extends Component { ...@@ -29,7 +32,8 @@ export default class ActivityItem extends Component {
} }
render() { render() {
const { item, description, userColors } = this.props const { item, description, userColors } = this.props;
return ( return (
<div className="flex align-center"> <div className="flex align-center">
{ item.user ? { item.user ?
...@@ -41,7 +45,25 @@ export default class ActivityItem extends Component { ...@@ -41,7 +45,25 @@ export default class ActivityItem extends Component {
<span className="UserInitials"><Icon name={'return'}></Icon></span> <span className="UserInitials"><Icon name={'return'}></Icon></span>
</span> </span>
} }
<ActivityDescription description={description} />
<div className="ml2 full flex align-center">
<div className="text-grey-4">
<span className="text-dark">{description.userName}</span>
&nbsp;{description.subject}&nbsp;
{ description.subjectRefName && description.subjectRefLink ?
<a className="link text-dark" href={description.subjectRefLink}>{description.subjectRefName}</a>
: null }
{ description.subjectRefName && !description.subjectRefLink ?
<span className="text-dark">{description.subjectRefName}</span>
: null }
</div>
<div className="flex-align-right text-right text-grey-2">
{description.timeSince}
</div>
</div>
</div> </div>
) )
} }
......
...@@ -2,21 +2,34 @@ ...@@ -2,21 +2,34 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
export default class ActivityStory extends Component { export default class ActivityStory extends Component {
constructor() {
super() constructor(props) {
super(props);
this.styles = { this.styles = {
modelLink: { modelLink: {
borderWidth: "2px" borderWidth: "2px"
}, },
} }
} }
render() { render() {
const { story } = this.props const { story } = this.props;
if (!story.body) {
return null;
}
return ( return (
<div className="ml2 mt1 border-left flex" style={{borderWidth: '3px'}}> <div className="ml2 mt1 border-left flex" style={{borderWidth: '3px'}}>
<div style={this.styles.modelLink} className="flex full ml4 bordered rounded p2"> <div style={this.styles.modelLink} className="flex full ml4 bordered rounded p2">
<a className="link" href={story.bodyLink}>{story.body}</a> { story.bodyLink ?
<a className="link" href={story.bodyLink}>{story.body}</a>
:
<span>{story.body}</span>
}
</div> </div>
</div> </div>
) )
......
...@@ -44,7 +44,7 @@ export default class CardFilters extends Component { ...@@ -44,7 +44,7 @@ export default class CardFilters extends Component {
<div className="h3">Filter saved questions</div> <div className="h3">Filter saved questions</div>
</div> </div>
<div className="bordered rounded bg-white"> <div className="bordered rounded bg-white">
<ul> <ul className="cursor-pointer">
{databases.map(item => {databases.map(item =>
<li key={item.id} className="border-row-divider"> <li key={item.id} className="border-row-divider">
<AccordianItem isOpen={cardsFilter.database === item.id} itemId={item.id} onClickFn={(id) => this.databaseClicked(id)} title={item.name}> <AccordianItem isOpen={cardsFilter.database === item.id} itemId={item.id} onClickFn={(id) => this.databaseClicked(id)} title={item.name}>
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
import React, { Component, PropTypes } from "react"; 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 HeaderTabs from "./HeaderTabs.react"; import HeaderTabs from "./HeaderTabs.react";
import Activity from "./Activity.react"; import Activity from "./Activity.react";
...@@ -10,6 +11,7 @@ import Cards from "./Cards.react"; ...@@ -10,6 +11,7 @@ import Cards from "./Cards.react";
import RecentViews from "./RecentViews.react"; import RecentViews from "./RecentViews.react";
import CardFilters from "./CardFilters.react"; import CardFilters from "./CardFilters.react";
import Smile from './Smile.react'; import Smile from './Smile.react';
import NewUserOnboardingModal from './NewUserOnboardingModal.react';
export default class Homepage extends Component { export default class Homepage extends Component {
...@@ -18,7 +20,8 @@ export default class Homepage extends Component { ...@@ -18,7 +20,8 @@ export default class Homepage extends Component {
super(props); super(props);
this.state = { this.state = {
greeting: Greeting.simpleGreeting() greeting: Greeting.simpleGreeting(),
onboarding: props.showOnboarding
}; };
this.styles = { this.styles = {
...@@ -41,11 +44,23 @@ export default class Homepage extends Component { ...@@ -41,11 +44,23 @@ export default class Homepage extends Component {
}; };
} }
completeOnboarding() {
this.setState({
'onboarding': false
});
}
render() { render() {
const { selectedTab, user } = this.props; const { selectedTab, user } = this.props;
return ( return (
<div> <div>
{ this.state.onboarding ?
<Modal>
<NewUserOnboardingModal user={user} closeFn={() => (this.completeOnboarding())}></NewUserOnboardingModal>
</Modal>
: null}
<div className="bg-brand text-white pl4"> <div className="bg-brand text-white pl4">
<div style={this.styles.main}> <div style={this.styles.main}>
<div style={this.styles.mainWrapper}> <div style={this.styles.mainWrapper}>
......
"use strict";
import React, { Component, PropTypes } from "react";
export default class NewUserOnboardingModal extends Component {
constructor(props) {
super(props);
this.state = {step: 1};
}
stepTwo() {
this.setState({step: 2});
}
closeModal() {
this.props.closeFn();
}
render() {
const { user } = this.props;
const { step } = this.state;
return (
<div>
{ step === 1 ?
<div className="bordered rounded shadowed">
<div className="pl4 pr4 pt4 pb1 border-bottom">
<h2>{user.first_name}, welcome to Metabase!</h2>
<h2>Analytics you can use by yourself.</h2>
<p>Metabase lets you find answers to your questions from data your company already has.</p>
<p>Its easy to use, because its designed so you dont need any analytics knowledge to get started.</p>
</div>
<div className="px4 py2 text-grey-2 flex align-center">
STEP 1 of 2
<button className="Button Button--primary flex-align-right" onClick={() => (this.stepTwo())}>Continue</button>
</div>
</div>
:
<div className="bordered rounded shadowed">
<div className="pl4 pr4 pt4 pb1 border-bottom">
<h2>Just 3 things worth knowing</h2>
<p className="clearfix pt1"><img className="float-left mr2" width="40" height="40" src="/app/home/partials/onboarding_illustration_tables.png" />All of your data is organized in Tables. Think of them in terms of Excel spreadsheets with columns and rows.</p>
<p className="clearfix"><img className="float-left mr2" width="40" height="40" src="/app/home/partials/onboarding_illustration_questions.png" />To get answers, you Ask Questions by picking a table and a few other parameters. You can visualize the answer in many ways, including cool charts.</p>
<p className="clearfix"><img className="float-left mr2" width="40" height="40" src="/app/home/partials/onboarding_illustration_dashboards.png" />You (and anyone on your team) can save answers in Dashboards, so you can check them often. It's a great way to quickly see a snapshot of your business.</p>
</div>
<div className="px4 py2 text-grey-2 flex align-center">
STEP 2 of 2
<a className="Button Button--primary flex-align-right" href="/" onClick={() => (this.closeModal())}>Continue</a>
</div>
</div>
}
</div>
);
}
}
'use strict'; 'use strict';
import Table from "metabase/lib/table";
import { createStore, applyMiddleware, combineReducers, compose } from 'redux'; import { createStore, applyMiddleware, combineReducers, compose } from 'redux';
import promiseMiddleware from 'redux-promise'; import promiseMiddleware from 'redux-promise';
import thunkMidleware from "redux-thunk"; import thunkMidleware from "redux-thunk";
...@@ -28,11 +26,7 @@ const finalCreateStore = compose( ...@@ -28,11 +26,7 @@ const finalCreateStore = compose(
const reducer = combineReducers(reducers); const reducer = combineReducers(reducers);
var HomeControllers = angular.module('metabase.home.controllers', [ var HomeControllers = angular.module('metabase.home.controllers', []);
'metabase.home.directives',
'metabase.metabase.services'
]);
HomeControllers.controller('Homepage', ['$scope', '$location', '$route', '$routeParams', function($scope, $location, $route, $routeParams) { HomeControllers.controller('Homepage', ['$scope', '$location', '$route', '$routeParams', function($scope, $location, $route, $routeParams) {
$scope.Component = HomepageApp; $scope.Component = HomepageApp;
$scope.props = { $scope.props = {
...@@ -43,7 +37,6 @@ HomeControllers.controller('Homepage', ['$scope', '$location', '$route', '$route ...@@ -43,7 +37,6 @@ HomeControllers.controller('Homepage', ['$scope', '$location', '$route', '$route
} }
}; };
$scope.store = finalCreateStore(reducer, { selectedTab: 'activity' }); $scope.store = finalCreateStore(reducer, { selectedTab: 'activity' });
// TODO: reflect onboarding state
// $scope.monitor = LogMonitor; // $scope.monitor = LogMonitor;
...@@ -76,30 +69,3 @@ HomeControllers.controller('Homepage', ['$scope', '$location', '$route', '$route ...@@ -76,30 +69,3 @@ HomeControllers.controller('Homepage', ['$scope', '$location', '$route', '$route
} }
}, true); }, true);
}]); }]);
HomeControllers.controller('HomeDatabaseList', ['$scope', 'Metabase', function($scope, Metabase) {
$scope.databases = [];
$scope.currentDB = {};
$scope.tables = [];
Metabase.db_list(function (databases) {
$scope.databases = databases;
$scope.selectCurrentDB(0)
}, function (error) {
console.log(error);
});
$scope.selectCurrentDB = function(index) {
$scope.currentDB = $scope.databases[index];
Metabase.db_tables({
'dbId': $scope.currentDB.id
}, function (tables) {
$scope.tables = tables.filter(Table.isQueryable);
}, function (error) {
console.log(error);
})
}
}]);
'use strict';
var HomeDirectives = angular.module('metabase.home.directives', []);
HomeDirectives.directive('mbNewUserOnboarding', ['$modal',
function($modal) {
function link(scope, element, attrs) {
function openModal() {
$modal.open({
templateUrl: '/app/home/partials/modal_user_onboarding.html',
controller: ['$scope', '$modalInstance',
function($scope, $modalInstance) {
$scope.firstStep = true;
$scope.user = scope.user;
$scope.next = function() {
$scope.firstStep = false;
};
$scope.close = function() {
$modalInstance.dismiss('cancel');
};
}
]
});
}
// always start with the modal open
openModal();
}
return {
restrict: 'E',
link: link
};
}
]);
<div class="Home" ng-controller="Home">
<div class="bg-brand text-white">
<div class="wrapper">
<div class="Grid Grid--full large-Grid--1of2 align-center">
<div class="Grid-cell" ng-controller="HomeGreeting">
<div class="Greeting">
<h1 class="text-light">{{greeting}}</h1>
<h2 class="text-light text-brand-light">{{subheading}}</h2>
</div>
</div>
</div>
</div>
<div class="wrapper">
<a class="HomeTab inline-block" ng-class="{'HomeTab--active text-dark' : currentView === 'data' }" ng-click="currentView = 'data'">A new question</a>
<a class="HomeTab inline-block" ng-class="{'HomeTab--active text-dark' : currentView === 'questions'}" ng-click="currentView = 'questions'">Start from a saved question</a>
</div>
</div>
<div ng-if="showOnboarding"><mb-new-user-onboarding></mb-new-user-onboarding></div>
<div ng-controller="CardList" ng-if="currentView != 'data'">
<div class="flex align-center py2 lg-py3 bg-white border-bottom">
<div class="wrapper flex">
<a href="#" class="flex text-grey-3 no-decoration transition-color align-center mr2" ng-click="filter('all')" ng-class="{'text-brand': filterMode === 'all', 'text-brand-hover': filterMode != 'all'}">
<mb-icon class="mr1" name="popular" width="32px" height="32px"></mb-icon>
Popular
</a>
<a href="#" class="flex text-grey-3 no-decoration transition-color align-center mr2" ng-click="filter('mine')" ng-class="{'text-brand': filterMode === 'mine', 'text-brand-hover': filterMode != 'mine' }">
<mb-icon class="mr1" name="mine" width="18px" height="18px"></mb-icon>
Mine
</a>
<a href="#" class="flex text-grey-3 no-decoration transition-color align-center mr2" ng-click="filter('fav')" ng-class="{'text-brand': filterMode === 'fav', 'text-brand-hover': filterMode != 'fav' }">
<mb-icon class="mr1" name="star" width="18px" height="18px"></mb-icon>
Favorites
</a>
</div>
</div>
<div class="wrapper">
<div ng-if="!cards" class="text-brand">
<mb-loading-icon></mb-loading-icon>
</div>
<div class="Grid">
<ul class="Grid-cell">
<li ng-repeat="card in cards | filter:searchFilter">
<div class="Entity flex align-center mb2 py2 lg-p2 border-bottom">
<a href="/card/{{card.id}}?clone" class="flex flex-column no-decoration">
<div class="flex align-center">
<h4 class="Entity-title break-word text-brand-hover">{{card.name}}</h4>
<mb-icon class="ml1 text-grey-4" name="lock" width="12px" height="12px" ng-if="card.public_perms === 0"></mb-icon>
</div>
<span class="Entity-attribution">
Asked by: {{card.creator.common_name}}
</span>
</a>
<a href="/card/{{card.id}}" class="flex-align-right IconCircle flex text-grey-1 text-grey-3-hover transition-color layout-centered">
<mb-icon name="pencil" width="18px" height="18px"></mb-icon>
</a>
</div>
</li>
</ul>
</div>
</div>
</div>
<div ng-if="currentView === 'data'" ng-controller="HomeDatabaseList">
<div class="flex py2 lg-py3 align-center border-bottom">
<div class="wrapper">
<span class="h2 text-grey-2">Data source:</span>
<div class="Dropdown inline-block" dropdown on-toggle="toggled(open)">
<div class="DataSourceDropdown-title text-brand" dropdown-toggle>
<span class="h2 ml1 text-brand-darken-hover transition-color cursor-pointer">
{{currentDB.name}}
<mb-icon name="chevrondown" width="8px" height="8px"></mb-icon>
</span>
<ul class="Dropdown-content">
<li ng-repeat="(id, db) in databases">
<a class="Dropdown-item block" ng-click="selectCurrentDB(id)">
{{db.name}}
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<div>
<div class="wrapper">
<h4 class="mt1 lg-mt3 text-grey-2">Tables:</h4>
</div>
<ul class="wrapper" ng-if="tables">
<li ng-repeat="table in tables" ng-if="table.rows > 0">
<a class="no-decoration py2 border-bottom flex align-center" href="/q?db={{currentDB.id}}&table={{table.id}}">
<div class="flex flex-column pr3 lg-pr0">
<h2 class="text-dark text-brand-hover break-word">{{table.display_name}}</h2>
<h4 class="TableDescription text-grey-4 text-normal mt1">{{table.description}}</h4>
</div>
<div class="flex flex-align-right align-center">
<div class="text-right text-brand text-brand-darken-hover mr2">
<h3>{{table.rows}}</h3>
<h6 class="text-uppercase ">total</h6>
</div>
<mb-icon class="text-grey-1" name="chevronright" width="18px" height="18px"></mb-icon>
</div>
</a>
</li>
</ul>
</div>
<div ng-if="tables.length === 0">
<div class="wrapper flex layout-centered">
<h2>No data is avaliable for this database</h2>
</div>
</div>
</div>
</div>
<div>
<div class="bordered rounded shadowed" ng-show="firstStep">
<div class="pl4 pr4 pt4 pb1 border-bottom">
<h2>{{user.first_name}}, welcome to Metabase!</h2>
<h2>Analytics you can use by yourself.</h2>
<p>Metabase lets you find answers to your questions from data your company already has.</p>
<p>It’s easy to use, because it’s designed so you don’t need any analytics knowledge to get started.</p>
</div>
<div class="px4 py2 text-grey-2 flex align-center">
STEP 1 of 2
<button class="Button Button--primary flex-align-right" ng-click="next()">Continue</button>
</div>
</div>
<div class="bordered rounded shadowed" ng-show="!firstStep">
<div class="pl4 pr4 pt4 pb1 border-bottom">
<h2>Just 3 things worth knowing</h2>
<p class="clearfix pt1"><img class="float-left mr2" width="40" height="40" src="/app/home/partials/onboarding_illustration_tables.png">All of your data is organized in Tables. Think of them in terms of Excel spreadsheets with columns and rows.</p>
<p class="clearfix"><img class="float-left mr2" width="40" height="40" src="/app/home/partials/onboarding_illustration_questions.png">To get answers, you Ask Questions by picking a table and a few other parameters. You can visualize the answer in many ways, including cool charts.</p>
<p class="clearfix"><img class="float-left mr2" width="40" height="40" src="/app/home/partials/onboarding_illustration_dashboards.png">You (and anyone on your team) can save answers in Dashboards, so you can check them often. It's a great way to quickly see a snapshot of your business.</p>
</div>
<div class="px4 py2 text-grey-2 flex align-center">
STEP 2 of 2
<button class="Button Button--primary flex-align-right" ng-click="close()">Continue</button>
</div>
</div>
</div>
...@@ -24,11 +24,7 @@ export const cardsFilter = handleActions({ ...@@ -24,11 +24,7 @@ export const cardsFilter = handleActions({
export const activity = handleActions({ export const activity = handleActions({
[FETCH_ACTIVITY]: { next: (state, { payload }) => ({ ...payload.entities.activity }) } [FETCH_ACTIVITY]: { next: (state, { payload }) => payload }
}, {});
export const activityIdList = handleActions({
[FETCH_ACTIVITY]: { next: (state, { payload }) => payload.result }
}, null); }, null);
export const recentViews = handleActions({ export const recentViews = handleActions({
...@@ -37,11 +33,7 @@ export const recentViews = handleActions({ ...@@ -37,11 +33,7 @@ export const recentViews = handleActions({
export const cards = handleActions({ export const cards = handleActions({
[FETCH_CARDS]: { next: (state, { payload }) => ({ ...payload.entities.card }) } [FETCH_CARDS]: { next: (state, { payload }) => payload }
}, {});
export const cardIdList = handleActions({
[FETCH_CARDS]: { next: (state, { payload }) => payload.result }
}, null); }, null);
......
...@@ -7,29 +7,16 @@ const selectedTabSelector = state => state.selectedTab; ...@@ -7,29 +7,16 @@ const selectedTabSelector = state => state.selectedTab;
const cardsFilterSelector = state => state.cardsFilter; const cardsFilterSelector = state => state.cardsFilter;
const activitySelector = state => state.activity; const activitySelector = state => state.activity;
const activityIdListSelector = state => state.activityIdList;
const recentViewsSelector = state => state.recentViews; const recentViewsSelector = state => state.recentViews;
const cardsSelector = state => state.cards; const cardsSelector = state => state.cards;
const cardIdListSelector = state => state.cardIdList;
const databasesSelector = state => state.databases; const databasesSelector = state => state.databases;
const databaseMetadataSelector = state => state.databaseMetadata; const databaseMetadataSelector = state => state.databaseMetadata;
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 // our master selector which combines all of our partial selectors above
export const homepageSelectors = createSelector( export const homepageSelectors = createSelector(
[selectedTabSelector, cardsFilterSelector, activityListSelector, recentViewsSelector, cardListSelector, databasesSelector, databaseMetadataSelector], [selectedTabSelector, cardsFilterSelector, activitySelector, recentViewsSelector, cardsSelector, databasesSelector, databaseMetadataSelector],
(selectedTab, cardsFilter, activity, recentViews, cards, databases, databaseMetadata) => ({selectedTab, cardsFilter, activity, recentViews, cards, databases, databaseMetadata}) (selectedTab, cardsFilter, activity, recentViews, cards, databases, databaseMetadata) => ({selectedTab, cardsFilter, activity, recentViews, cards, databases, databaseMetadata})
); );
\ No newline at end of file
'use strict';
import 'metabase/controllers';
describe('metabase.controllers', function() {
beforeEach(angular.mock.module('metabase.controllers'));
describe('Homepage', function() {
beforeEach(angular.mock.inject(function($location) {
spyOn($location, 'path').and.returnValue('Fake location');
}))
it('should redirect logged-out user to /auth/login', inject(function($controller, $location) {
$controller('Homepage', { $scope: {}, AppState: { model: { currentUser: null }} });
expect($location.path).toHaveBeenCalledWith('/auth/login');
}));
it('should redirect logged-in user to /dash/', inject(function($controller, $location) {
$controller('Homepage', { $scope: {}, AppState: { model: { currentUser: {} }} });
expect($location.path).toHaveBeenCalledWith('/dash/');
}));
});
});
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
"Get recent activity." "Get recent activity."
[] []
(-> (db/sel :many Activity (k/order :timestamp :DESC)) (-> (db/sel :many Activity (k/order :timestamp :DESC))
(hydrate :user :table :database))) (hydrate :user :table :database :model_exists)))
(defendpoint GET "/recent_views" (defendpoint GET "/recent_views"
"Get the list of 15 things the current user has been viewing most recently." "Get the list of 15 things the current user has been viewing most recently."
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
(k/limit 15)) (k/limit 15))
(map #(assoc % :model_object (delay (case (:model %) (map #(assoc % :model_object (delay (case (:model %)
"card" (-> (Card (:model_id %)) "card" (-> (Card (:model_id %))
(select-keys [:id :name :description])) (select-keys [:id :name :description :display]))
"dashboard" (-> (Dashboard (:model_id %)) "dashboard" (-> (Dashboard (:model_id %))
(select-keys [:id :name :description])) (select-keys [:id :name :description]))
nil))))) nil)))))
......
...@@ -8,7 +8,8 @@ ...@@ -8,7 +8,8 @@
(def view-counts-topics (def view-counts-topics
"The `Set` of event topics which we subscribe to for view counting." "The `Set` of event topics which we subscribe to for view counting."
#{:card-read #{:card-create
:card-read
:dashboard-read}) :dashboard-read})
(def ^:private view-counts-channel (def ^:private view-counts-channel
......
...@@ -3,7 +3,8 @@ ...@@ -3,7 +3,8 @@
[metabase.api.common :refer [*current-user-id*]] [metabase.api.common :refer [*current-user-id*]]
[metabase.db :refer :all] [metabase.db :refer :all]
[metabase.events :as events] [metabase.events :as events]
(metabase.models [dashboard :refer [Dashboard]] (metabase.models [card :refer [Card]]
[dashboard :refer [Dashboard]]
[database :refer [Database]] [database :refer [Database]]
[interface :refer :all] [interface :refer :all]
[table :refer [Table]] [table :refer [Table]]
...@@ -28,12 +29,16 @@ ...@@ -28,12 +29,16 @@
:details {}}] :details {}}]
(merge defaults activity))) (merge defaults activity)))
(post-select [_ {:keys [user_id database_id table_id] :as activity}] (post-select [_ {:keys [user_id database_id table_id model model_id] :as activity}]
(-> (map->ActivityFeedItemInstance activity) (-> (map->ActivityFeedItemInstance activity)
(assoc :user (delay (User user_id))) (assoc :user (delay (User user_id)))
(assoc :database (delay (-> (Database database_id) (assoc :database (delay (-> (Database database_id)
(select-keys [:id :name :description])))) (select-keys [:id :name :description]))))
(assoc :table (delay (-> (Table table_id) (assoc :table (delay (-> (Table table_id)
(select-keys [:id :name :display_name :description]))))))) (select-keys [:id :name :display_name :description]))))
(assoc :model_exists (delay (case model
"card" (exists? Card :id model_id)
"dashboard" (exists? Dashboard :id model_id)
nil))))))
(extend-ICanReadWrite ActivityEntity :read :public-perms, :write :public-perms) (extend-ICanReadWrite ActivityEntity :read :public-perms, :write :public-perms)
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