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

get all of the card filtering by Table stuff working. formatting is pretty...

get all of the card filtering by Table stuff working.  formatting is pretty close now on Saved Questions tab.
parent a89b486c
No related branches found
No related tags found
No related merge requests found
......@@ -33,6 +33,11 @@ function createThunkAction(actionType, actionThunkCreator) {
}
}
// resource wrappers
const ActivityApi = new AngularResourceProxy("Activity", ["list"]);
const CardApi = new AngularResourceProxy("Card", ["list"]);
const MetadataApi = new AngularResourceProxy("Metabase", ["db_list", "db_metadata"]);
// normalizr schemas
const activity = new Schema('activity');
const card = new Schema('card');
......@@ -48,21 +53,36 @@ const card = new Schema('card');
// 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_CARDS = 'FETCH_CARDS';
export const FETCH_DATABASES = 'FETCH_DATABASES';
export const FETCH_DATABASE_METADATA = 'FETCH_DATABASE_METADATA';
// 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 setCardsFilter = createThunkAction(SET_CARDS_FILTER, function(filterDef) {
return function(dispatch, getState) {
let {database, table} = filterDef;
if (database && !table) {
// if we have a new database then fetch its metadata
dispatch(fetchDatabaseMetadata(database));
} else if (database && table) {
// if we have a new table then refetch the cards
dispatch(fetchCards('table', table));
}
return filterDef;
};
});
export const fetchActivity = createThunkAction(FETCH_ACTIVITY, function() {
return async function(dispatch, getState) {
let activityItems = await Activity.list();
let activityItems = await ActivityApi.list();
for (var ai of activityItems) {
ai.timestamp = moment(ai.timestamp);
ai.hasLinkableModel = function() {
......@@ -84,9 +104,9 @@ export const fetchActivity = createThunkAction(FETCH_ACTIVITY, function() {
};
});
export const fetchCards = createThunkAction(FETCH_CARDS, function(filterMode, filterEntityId) {
export const fetchCards = createThunkAction(FETCH_CARDS, function(filterMode, filterModelId) {
return async function(dispatch, getState) {
let cards = await Card.list({'filterMode' : filterMode});
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);
......@@ -96,7 +116,20 @@ export const fetchCards = createThunkAction(FETCH_CARDS, function(filterMode, fi
};
});
export const fetchDatabases = createThunkAction(FETCH_DATABASES, function() {
return async function(dispatch, getState) {
let databases = await MetadataApi.db_list();
return databases;
};
});
export const fetchDatabaseMetadata = createThunkAction(FETCH_DATABASE_METADATA, function(database_id) {
return async function(dispatch, getState) {
let metadata = await MetadataApi.db_metadata({'dbId': database_id});
return metadata;
};
});
// fetch recent items (user)
// fetch database list
// fetch table list (database)
"use strict";
import React, { Component, PropTypes } from "react";
import Icon from "metabase/components/Icon.react";
export default class AccordianItem extends Component {
constructor(props) {
super(props);
this.styles = {
heading: {
}
};
}
render() {
let { children, onClickFn, isOpen, itemId, title } = this.props;
return (
<div key={itemId}>
<div styles={this.styles.heading} className="p2 text-grey-4 text-brand-hover" onClick={() => (onClickFn(itemId))}>
<span className="float-left">{title}</span>
<div className="text-right text-grey-2 text-brand-hover">
{ isOpen ?
<Icon name="chevronup" width={12} height={12}></Icon>
:
<Icon name="chevrondown" width={12} height={12}></Icon>
}
</div>
</div>
{ isOpen ?
<div className="articlewrap">
<div className="article">
{children}
</div>
</div>
:
null
}
</div>
);
}
}
......@@ -4,14 +4,52 @@ import React, { Component, PropTypes } from "react";
import Icon from "metabase/components/Icon.react";
import { fetchDatabases, setCardsFilter } from "../actions";
import AccordianItem from "./AccordianItem.react";
import TableListing from "./TableListing.react";
export default class CardFilters extends Component {
constructor(props) {
super(props);
this.state = { error : null };
}
async componentDidMount() {
try {
await this.props.dispatch(fetchDatabases());
} catch (error) {
this.setState({ error });
}
}
async databaseClicked(id) {
if (this.props.cardsFilter.database !== id) {
this.props.dispatch(setCardsFilter({database: id, table: null}));
}
}
render() {
let { databases, cardsFilter } = this.props;
return (
<div className="p2">
<div className="text-brand">
<Icon className="inline-block" name={'filter'}></Icon> Filter saved questions
<div className="text-brand clearfix pt2">
<Icon className="float-left" name={'filter'} width={36} height={36}></Icon>
<div>Filter saved questions</div>
</div>
<div className="bordered rounded bg-white">
<ul>
{databases.map(item =>
<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}>
<TableListing {...this.props}></TableListing>
</AccordianItem>
</li>
)}
</ul>
</div>
</div>
);
......
......@@ -79,9 +79,9 @@ export default class Cards extends Component {
{() =>
<div>
{ cards.length === 0 ?
<div className="flex flex-column layout-centered">
<div className="flex flex-column layout-centered pt4">
<span className="QuestionCircle">?</span>
<div className="text-normal mt3 mb1">Hmmm, looks like nothing has happened yet.</div>
<div className="text-normal mt3 mb1">Hmmm, looks like you don't have any saved questions yet.</div>
<div className="text-normal text-grey-2">Save a question and get this baby going!</div>
</div>
:
......
"use strict";
import React, { Component, PropTypes } from "react";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.react";
import { fetchDatabaseMetadata, setCardsFilter } from "../actions";
export default class TableListing extends Component {
constructor(props) {
super(props);
this.state = { error: null };
}
async tableClicked(id) {
this.props.dispatch(setCardsFilter({database: this.props.cardsFilter.database, table: id}));
}
render() {
let { databaseMetadata } = this.props;
let { error } = this.state;
return (
<LoadingAndErrorWrapper loading={!databaseMetadata} error={error}>
{() =>
<ul className="pl4 pr2 pb1">
{databaseMetadata.tables.map(item =>
<li key={item.id} className="py1 text-brand-hover" onClick={() => (this.tableClicked(item.id))}>
{item.display_name}
</li>
)}
</ul>
}
</LoadingAndErrorWrapper>
);
}
}
......@@ -4,8 +4,11 @@ import { handleActions } from 'redux-actions';
import {
SET_SELECTED_TAB,
SET_CARDS_FILTER,
FETCH_ACTIVITY,
FETCH_CARDS
FETCH_CARDS,
FETCH_DATABASES,
FETCH_DATABASE_METADATA
} from './actions';
......@@ -13,6 +16,11 @@ 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({
[FETCH_ACTIVITY]: { next: (state, { payload }) => ({ ...payload.entities.activity }) }
}, {});
......@@ -21,10 +29,22 @@ 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
}, null);
export const databases = handleActions({
[FETCH_DATABASES]: { next: (state, { payload }) => payload }
}, []);
export const databaseMetadata = handleActions({
[FETCH_DATABASE_METADATA]: { next: (state, { payload }) => payload }
}, null);
"use strict";
// import _ from "underscore";
import { createSelector } from 'reselect';
const selectedTabSelector = state => state.selectedTab;
const cardsFilterSelector = state => state.cardsFilter;
const activitySelector = state => state.activity;
const activityIdListSelector = state => state.activityIdList;
const cardsSelector = state => state.cards;
const cardIdListSelector = state => state.cardIdList;
const databasesSelector = state => state.databases;
const databaseMetadataSelector = state => state.databaseMetadata;
const activityListSelector = createSelector(
[activityIdListSelector, activitySelector],
......@@ -23,6 +28,6 @@ const cardListSelector = createSelector(
// our master selector which combines all of our partial selectors above
export const homepageSelectors = createSelector(
[selectedTabSelector, activityListSelector, cardListSelector],
(selectedTab, activity, cards) => ({selectedTab, activity, cards})
[selectedTabSelector, cardsFilterSelector, activityListSelector, cardListSelector, databasesSelector, databaseMetadataSelector],
(selectedTab, cardsFilter, activity, cards, databases, databaseMetadata) => ({selectedTab, cardsFilter, activity, cards, databases, databaseMetadata})
);
\ No newline at end of file
......@@ -75,6 +75,13 @@ MetabaseServices.factory('Metabase', ['$resource', '$cookies', 'MetabaseCore', f
}
}
},
db_metadata: {
url: '/api/meta/db/:dbId/metadata',
method: 'GET',
params: {
dbId: '@dbId'
}
},
db_tables: {
url: '/api/meta/db/:dbId/tables',
method: 'GET',
......
......@@ -34,14 +34,14 @@
* `table` Return all `Cards` with `:table_id` equal to `id`
All returned cards must be either created by current user or are publicly visible."
[f id]
[f model_id]
{f CardFilterOption
id Integer}
model_id Integer}
(when (contains? #{:database :table} f)
(checkp (integer? id) "id" (format "id is required parameter when filter mode is '%s'" (name f)))
(checkp (integer? model_id) "id" (format "id is required parameter when filter mode is '%s'" (name f)))
(case f
:database (read-check Database id)
:table (read-check Database (:db_id (sel :one :fields [Table :db_id] :id id)))))
:database (read-check Database model_id)
:table (read-check Database (:db_id (sel :one :fields [Table :db_id] :id model_id)))))
(-> (case (or f :all) ; default value for `f` is `:all`
:all (sel :many Card (k/order :name :ASC) (k/where (or {:creator_id *current-user-id*}
{:public_perms [> common/perms-none]})))
......@@ -50,10 +50,10 @@
(hydrate :card))
(map :card)
(sort-by :name))
:database (sel :many Card (k/order :name :ASC) (k/where (and {:database_id id}
:database (sel :many Card (k/order :name :ASC) (k/where (and {:database_id model_id}
(or {:creator_id *current-user-id*}
{:public_perms [> common/perms-none]}))))
:table (sel :many Card (k/order :name :ASC) (k/where (and {:table_id id}
:table (sel :many Card (k/order :name :ASC) (k/where (and {:table_id model_id}
(or {:creator_id *current-user-id*}
{:public_perms [> common/perms-none]})))))
(hydrate :creator)))
......
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