diff --git a/frontend/src/metabase-lib/lib/Question.js b/frontend/src/metabase-lib/lib/Question.js index 48cdca5140953..59017e5cbbec2 100644 --- a/frontend/src/metabase-lib/lib/Question.js +++ b/frontend/src/metabase-lib/lib/Question.js @@ -40,6 +40,8 @@ import type { } from "metabase/meta/types/Card"; import { MetabaseApi, CardApi } from "metabase/services"; +import Questions from "metabase/entities/questions"; + import AtomicQuery from "metabase-lib/lib/queries/AtomicQuery"; import type { Dataset } from "metabase/meta/types/Dataset"; @@ -471,16 +473,30 @@ export default class Question { } } + // NOTE: prefer `reduxCreate` so the store is automatically updated async apiCreate() { - const createdCard = await CardApi.create(this.card()); + const createdCard = await Questions.api.create(this.card()); return this.setCard(createdCard); } + // NOTE: prefer `reduxUpdate` so the store is automatically updated async apiUpdate() { - const updatedCard = await CardApi.update(this.card()); + const updatedCard = await Questions.api.update(this.card()); return this.setCard(updatedCard); } + async reduxCreate(dispatch) { + const { payload } = await dispatch(Questions.actions.create(this.card())); + return this.setCard(payload.entities.questions[payload.result]); + } + + async reduxUpdate(dispatch) { + const { payload } = await dispatch( + Questions.actions.update({ id: this.id() }, this.card()), + ); + return this.setCard(payload.entities.questions[payload.result]); + } + // TODO: Fix incorrect Flow signature parameters(): ParameterObject[] { return getParametersWithExtras(this.card(), this._parameterValues); diff --git a/frontend/src/metabase-lib/lib/queries/StructuredQuery.js b/frontend/src/metabase-lib/lib/queries/StructuredQuery.js index 6e251a792633f..04f64ee530981 100644 --- a/frontend/src/metabase-lib/lib/queries/StructuredQuery.js +++ b/frontend/src/metabase-lib/lib/queries/StructuredQuery.js @@ -635,12 +635,24 @@ export default class StructuredQuery extends AtomicQuery { return this._updateQuery(Q.removeExpression, arguments); } - // FIELD OPTIONS + // FIELDS + /** + * Returns dimension options that can appear in the `fields` clause + */ + fieldsOptions(dimensionFilter = () => true): DimensionOptions { + if (this.isBareRows() && this.breakouts().length === 0) { + return this.dimensionOptions(dimensionFilter); + } + // TODO: allow adding fields connected by broken out PKs? + return { count: 0, dimensions: [], fks: [] }; + } + + // DIMENSION OPTIONS // TODO Atte Keinänen 6/18/17: Refactor to dimensionOptions which takes a dimensionFilter // See aggregationFieldOptions for an explanation why that covers more use cases - fieldOptions(fieldFilter = () => true): DimensionOptions { - const fieldOptions = { + dimensionOptions(dimensionFilter = () => true): DimensionOptions { + const dimensionOptions = { count: 0, fks: [], dimensions: [], @@ -648,11 +660,6 @@ export default class StructuredQuery extends AtomicQuery { const table = this.tableMetadata(); if (table) { - const dimensionFilter = dimension => { - const field = dimension.field && dimension.field(); - return !field || (field.isDimension() && fieldFilter(field)); - }; - const dimensionIsFKReference = dimension => dimension.field && dimension.field() && dimension.field().isFK(); @@ -660,8 +667,8 @@ export default class StructuredQuery extends AtomicQuery { // .filter(d => !dimensionIsFKReference(d)); for (const dimension of filteredNonFKDimensions) { - fieldOptions.count++; - fieldOptions.dimensions.push(dimension); + dimensionOptions.count++; + dimensionOptions.dimensions.push(dimension); } const fkDimensions = this.dimensions().filter(dimensionIsFKReference); @@ -671,8 +678,8 @@ export default class StructuredQuery extends AtomicQuery { .filter(dimensionFilter); if (fkDimensions.length > 0) { - fieldOptions.count += fkDimensions.length; - fieldOptions.fks.push({ + dimensionOptions.count += fkDimensions.length; + dimensionOptions.fks.push({ field: dimension.field(), dimension: dimension, dimensions: fkDimensions, @@ -681,7 +688,17 @@ export default class StructuredQuery extends AtomicQuery { } } - return fieldOptions; + return dimensionOptions; + } + + // FIELD OPTIONS + + fieldOptions(fieldFilter = () => true) { + const dimensionFilter = dimension => { + const field = dimension.field && dimension.field(); + return !field || (field.isDimension() && fieldFilter(field)); + }; + return this.dimensionOptions(dimensionFilter); } // DIMENSIONS diff --git a/frontend/src/metabase/App.jsx b/frontend/src/metabase/App.jsx index 768127b06f684..dc0bd741b94c9 100644 --- a/frontend/src/metabase/App.jsx +++ b/frontend/src/metabase/App.jsx @@ -2,7 +2,7 @@ import React, { Component } from "react"; import { connect } from "react-redux"; - +import ScrollToTop from "metabase/hoc/ScrollToTop"; import Navbar from "metabase/nav/containers/Navbar.jsx"; import UndoListing from "metabase/containers/UndoListing"; @@ -57,11 +57,13 @@ export default class App extends Component { } return ( -
- - {errorPage ? getErrorComponent(errorPage) : children} - -
+ +
+ + {errorPage ? getErrorComponent(errorPage) : children} + +
+
); } } diff --git a/frontend/src/metabase/admin/databases/containers/DatabaseEditApp.jsx b/frontend/src/metabase/admin/databases/containers/DatabaseEditApp.jsx index 1623ef4a8bbda..f2cf04d98736f 100644 --- a/frontend/src/metabase/admin/databases/containers/DatabaseEditApp.jsx +++ b/frontend/src/metabase/admin/databases/containers/DatabaseEditApp.jsx @@ -1,14 +1,16 @@ +/* @flow weak */ + import React, { Component } from "react"; import PropTypes from "prop-types"; import { connect } from "react-redux"; import title from "metabase/hoc/Title"; import cx from "classnames"; +import { t } from "c-3po"; import MetabaseSettings from "metabase/lib/settings"; import DeleteDatabaseModal from "../components/DeleteDatabaseModal.jsx"; import DatabaseEditForms from "../components/DatabaseEditForms.jsx"; import DatabaseSchedulingForm from "../components/DatabaseSchedulingForm"; -import { t } from "c-3po"; import ActionButton from "metabase/components/ActionButton.jsx"; import Breadcrumbs from "metabase/components/Breadcrumbs.jsx"; import ModalWithTrigger from "metabase/components/ModalWithTrigger.jsx"; @@ -85,6 +87,10 @@ const mapDispatchToProps = { @connect(mapStateToProps, mapDispatchToProps) @title(({ database }) => database && database.name) export default class DatabaseEditApp extends Component { + state: { + currentTab: "connection" | "scheduling", + }; + constructor(props, context) { super(props, context); diff --git a/frontend/src/metabase/admin/databases/containers/DatabaseListApp.jsx b/frontend/src/metabase/admin/databases/containers/DatabaseListApp.jsx index d3d49e0748f15..2c68cd70ca786 100644 --- a/frontend/src/metabase/admin/databases/containers/DatabaseListApp.jsx +++ b/frontend/src/metabase/admin/databases/containers/DatabaseListApp.jsx @@ -1,40 +1,46 @@ +/* @flow weak */ + import React, { Component } from "react"; import PropTypes from "prop-types"; import { connect } from "react-redux"; import { Link } from "react-router"; +import { t } from "c-3po"; import cx from "classnames"; import MetabaseSettings from "metabase/lib/settings"; + import ModalWithTrigger from "metabase/components/ModalWithTrigger.jsx"; import LoadingSpinner from "metabase/components/LoadingSpinner.jsx"; -import { t } from "c-3po"; +import FormMessage from "metabase/components/form/FormMessage"; + import CreatedDatabaseModal from "../components/CreatedDatabaseModal.jsx"; import DeleteDatabaseModal from "../components/DeleteDatabaseModal.jsx"; -import { - getDatabasesSorted, - hasSampleDataset, - getDeletes, - getDeletionError, -} from "../selectors"; -import * as databaseActions from "../database"; -import FormMessage from "metabase/components/form/FormMessage"; +import Databases from "metabase/entities/databases"; +import { entityListLoader } from "metabase/entities/containers/EntityListLoader"; -const mapStateToProps = (state, props) => { - return { - created: props.location.query.created, - databases: getDatabasesSorted(state), - hasSampleDataset: hasSampleDataset(state), - engines: MetabaseSettings.get("engines"), - deletes: getDeletes(state), - deletionError: getDeletionError(state), - }; -}; +import { getDeletes, getDeletionError } from "../selectors"; +import { deleteDatabase, addSampleDataset } from "../database"; + +const mapStateToProps = (state, props) => ({ + hasSampleDataset: Databases.selectors.getHasSampleDataset(state), + + created: props.location.query.created, + engines: MetabaseSettings.get("engines"), + + deletes: getDeletes(state), + deletionError: getDeletionError(state), +}); const mapDispatchToProps = { - ...databaseActions, + fetchDatabases: Databases.actions.fetchList, + // NOTE: still uses deleteDatabase from metabaseadmin/databases/databases.js + // rather than metabase/entities/databases since it updates deletes/deletionError + deleteDatabase: deleteDatabase, + addSampleDataset: addSampleDataset, }; +@entityListLoader({ entityType: "databases" }) @connect(mapStateToProps, mapDispatchToProps) export default class DatabaseList extends Component { static propTypes = { @@ -45,10 +51,6 @@ export default class DatabaseList extends Component { deletionError: PropTypes.object, }; - componentWillMount() { - this.props.fetchDatabases(); - } - componentWillReceiveProps(newProps) { if (!this.props.created && newProps.created) { this.refs.createdDatabaseModal.open(); diff --git a/frontend/src/metabase/admin/databases/database.js b/frontend/src/metabase/admin/databases/database.js index e8eefc1f413c9..89ab5de738567 100644 --- a/frontend/src/metabase/admin/databases/database.js +++ b/frontend/src/metabase/admin/databases/database.js @@ -1,4 +1,4 @@ -import _ from "underscore"; +/* @flow weak */ import { createAction } from "redux-actions"; import { @@ -12,6 +12,7 @@ import MetabaseAnalytics from "metabase/lib/analytics"; import MetabaseSettings from "metabase/lib/settings"; import { MetabaseApi } from "metabase/services"; +import Databases from "metabase/entities/databases"; // Default schedules for db sync and deep analysis export const DEFAULT_SCHEDULES = { @@ -69,22 +70,13 @@ export const CLEAR_FORM_STATE = "metabase/admin/databases/CLEAR_FORM_STATE"; export const MIGRATE_TO_NEW_SCHEDULING_SETTINGS = "metabase/admin/databases/MIGRATE_TO_NEW_SCHEDULING_SETTINGS"; +// NOTE: some but not all of these actions have been migrated to use metabase/entities/databases + export const reset = createAction(RESET); // selectEngine (uiControl) export const selectEngine = createAction(SELECT_ENGINE); -// fetchDatabases -export const fetchDatabases = createThunkAction(FETCH_DATABASES, function() { - return async function(dispatch, getState) { - try { - return await MetabaseApi.db_list(); - } catch (error) { - console.error("error fetching databases", error); - } - }; -}); - // Migrates old "Enable in-depth database analysis" option to new "Let me choose when Metabase syncs and scans" option // Migration is run as a separate action because that makes it easy to track in tests const migrateDatabaseToNewSchedulingSettings = database => { @@ -112,7 +104,10 @@ export const initializeDatabase = function(databaseId) { return async function(dispatch, getState) { if (databaseId) { try { - const database = await MetabaseApi.db_get({ dbId: databaseId }); + const { payload } = await dispatch( + Databases.actions.fetch({ id: databaseId }, { reload: true }), + ); + const database = payload.entities.databases[databaseId]; dispatch.action(INITIALIZE_DATABASE, database); // If the new scheduling toggle isn't set, run the migration @@ -196,13 +191,10 @@ export const createDatabase = function(database) { return async function(dispatch, getState) { try { dispatch.action(CREATE_DATABASE_STARTED, {}); - const createdDatabase = await MetabaseApi.db_create(database); + const { payload } = await dispatch(Databases.actions.create(database)); + const createdDatabase = payload.entities.databases[payload.result]; MetabaseAnalytics.trackEvent("Databases", "Create", database.engine); - // update the db metadata already here because otherwise there will be a gap between "Adding..." status - // and seeing the db that was just added - await dispatch(fetchDatabases()); - dispatch.action(CREATE_DATABASE); dispatch(push("/admin/databases?created=" + createdDatabase.id)); } catch (error) { @@ -221,7 +213,8 @@ export const updateDatabase = function(database) { return async function(dispatch, getState) { try { dispatch.action(UPDATE_DATABASE_STARTED, { database }); - const savedDatabase = await MetabaseApi.db_update(database); + const { payload } = await dispatch(Databases.actions.update(database)); + const savedDatabase = payload.entities.databases[payload.result]; MetabaseAnalytics.trackEvent("Databases", "Update", database.engine); dispatch.action(UPDATE_DATABASE, { database: savedDatabase }); @@ -270,7 +263,7 @@ export const deleteDatabase = function(databaseId, isDetailView = true) { try { dispatch.action(DELETE_DATABASE_STARTED, { databaseId }); dispatch(push("/admin/databases/")); - await MetabaseApi.db_delete({ dbId: databaseId }); + await dispatch(Databases.actions.delete({ id: databaseId })); MetabaseAnalytics.trackEvent( "Databases", "Delete", @@ -334,18 +327,6 @@ export const discardSavedFieldValues = createThunkAction( // reducers -const databases = handleActions( - { - [FETCH_DATABASES]: { next: (state, { payload }) => payload }, - [ADD_SAMPLE_DATASET]: { - next: (state, { payload }) => (payload ? [...state, payload] : state), - }, - [DELETE_DATABASE]: (state, { payload: { databaseId } }) => - databaseId ? _.reject(state, d => d.id === databaseId) : state, - }, - null, -); - const editingDatabase = handleActions( { [RESET]: () => null, @@ -420,7 +401,6 @@ const formState = handleActions( ); export default combineReducers({ - databases, editingDatabase, deletionError, databaseCreationStep, diff --git a/frontend/src/metabase/admin/databases/selectors.js b/frontend/src/metabase/admin/databases/selectors.js index 5582bccb4326a..acfc858f52fd6 100644 --- a/frontend/src/metabase/admin/databases/selectors.js +++ b/frontend/src/metabase/admin/databases/selectors.js @@ -1,19 +1,5 @@ /* @flow weak */ -import _ from "underscore"; -import { createSelector } from "reselect"; - -// Database List -export const databases = state => state.admin.databases.databases; - -export const getDatabasesSorted = createSelector([databases], databases => - _.sortBy(databases, "name"), -); - -export const hasSampleDataset = createSelector([databases], databases => - _.some(databases, d => d.is_sample), -); - // Database Edit export const getEditingDatabase = state => state.admin.databases.editingDatabase; @@ -21,5 +7,6 @@ export const getFormState = state => state.admin.databases.formState; export const getDatabaseCreationStep = state => state.admin.databases.databaseCreationStep; +// Database List export const getDeletes = state => state.admin.databases.deletes; export const getDeletionError = state => state.admin.databases.deletionError; diff --git a/frontend/src/metabase/admin/people/components/GroupDetail.jsx b/frontend/src/metabase/admin/people/components/GroupDetail.jsx index 2d8d5d647edc1..add1f3a65aec1 100644 --- a/frontend/src/metabase/admin/people/components/GroupDetail.jsx +++ b/frontend/src/metabase/admin/people/components/GroupDetail.jsx @@ -115,7 +115,7 @@ const AddUserRow = ({ onCancel={onCancel} > {selectedUsers.map(user => ( -
+
{user.common_name} props && props.collectionId; + const getSingleCollectionPermissionsMode = (state, props) => (props && props.singleCollectionMode) || false; @@ -675,10 +676,14 @@ const getCollections = createSelector( (collectionsById, collectionId, singleMode) => { if (collectionId && collectionsById[collectionId]) { if (singleMode) { + // pass the `singleCollectionMode=true` prop when we just want to show permissions for the provided collection, and not it's subcollections return [collectionsById[collectionId]]; } else { - return collectionsById[collectionId].children; + return collectionsById[collectionId].children.filter( + collection => !collection.is_personal, + ); } + // default to root collection } else if (collectionsById["root"]) { return [collectionsById["root"]]; } else { diff --git a/frontend/src/metabase/auth/AuthApp.jsx b/frontend/src/metabase/auth/AuthApp.jsx new file mode 100644 index 0000000000000..a329be379934a --- /dev/null +++ b/frontend/src/metabase/auth/AuthApp.jsx @@ -0,0 +1,6 @@ +import fitViewPort from "metabase/hoc/FitViewPort"; + +// Auth components expect a full viewport experience to center most of the pages +const AuthApp = ({ children }) => children; + +export default fitViewPort(AuthApp); diff --git a/frontend/src/metabase/auth/containers/LoginApp.jsx b/frontend/src/metabase/auth/containers/LoginApp.jsx index d47097dcd53fc..4c485df34be55 100644 --- a/frontend/src/metabase/auth/containers/LoginApp.jsx +++ b/frontend/src/metabase/auth/containers/LoginApp.jsx @@ -112,7 +112,7 @@ export default class LoginApp extends Component { const ldapEnabled = Settings.ldapEnabled(); return ( -
+
diff --git a/frontend/src/metabase/auth/containers/PasswordResetApp.jsx b/frontend/src/metabase/auth/containers/PasswordResetApp.jsx index f551e17961a4d..d7886045ef34d 100644 --- a/frontend/src/metabase/auth/containers/PasswordResetApp.jsx +++ b/frontend/src/metabase/auth/containers/PasswordResetApp.jsx @@ -93,7 +93,9 @@ export default class PasswordResetApp extends Component { render() { const { resetError, resetSuccess, newUserJoining } = this.props; - const passwordComplexity = MetabaseSettings.passwordComplexity(false); + const passwordComplexity = MetabaseSettings.passwordComplexityDescription( + false, + ); const requestLink = ( diff --git a/frontend/src/metabase/collections/containers/CollectionCreate.jsx b/frontend/src/metabase/collections/containers/CollectionCreate.jsx index fb84987622bbb..37f12efc9307d 100644 --- a/frontend/src/metabase/collections/containers/CollectionCreate.jsx +++ b/frontend/src/metabase/collections/containers/CollectionCreate.jsx @@ -9,16 +9,14 @@ export default class CollectionCreate extends Component { render() { const { params } = this.props; const collectionId = - params && params.collectionId && parseFloat(params.collectionId); + params && params.collectionId != null && params.collectionId !== "root" + ? parseInt(params.collectionId) + : null; return ( this.props.goBack()} onClose={this.props.goBack} /> diff --git a/frontend/src/metabase/components/ArchivedItem.jsx b/frontend/src/metabase/components/ArchivedItem.jsx index b4f7b4f527888..636be28e985ad 100644 --- a/frontend/src/metabase/components/ArchivedItem.jsx +++ b/frontend/src/metabase/components/ArchivedItem.jsx @@ -24,7 +24,7 @@ const ArchivedItem = ({ onToggleSelected, showSelect, }) => ( -
+
( ); +const PAGE_PADDING = [1, 2, 4]; +const ITEM_WIDTHS = [1, 1 / 2, 1 / 3]; + const SchemaListLoader = ({ dbId, ...props }) => ( ); @@ -63,7 +66,7 @@ export class SchemaBrowser extends React.Component { {schemas.map(schema => ( - + { return ( - + + {this.props.children}; + return {this.props.children}; } } @@ -199,7 +202,7 @@ export class DatabaseBrowser extends React.Component { return ( {databases.map(database => ( - + diff --git a/frontend/src/metabase/components/Card.jsx b/frontend/src/metabase/components/Card.jsx index e6072c9ac1379..13b9acedd4ba5 100644 --- a/frontend/src/metabase/components/Card.jsx +++ b/frontend/src/metabase/components/Card.jsx @@ -5,14 +5,17 @@ import colors, { alpha } from "metabase/lib/colors"; const Card = styled.div` ${space} background-color: ${props => props.dark ? colors["text-dark"] : "white"}; - border: 1px solid ${props => (props.dark ? "transparent" : colors["border"])}; + border: 1px solid + ${props => (props.dark ? "transparent" : colors["bg-medium"])}; ${props => props.dark && `color: white`}; border-radius: 6px; - box-shadow: 0 5px 22px ${props => colors["shadow"]}; + box-shadow: 0 7px 20px ${props => colors["shadow"]}; + transition: all 0.2s linear; + line-height: 24px; ${props => props.hoverable && `&:hover { - box-shadow: 0 5px 16px ${alpha(colors["shadow"], 0.1)}; + box-shadow: 0 10px 22px ${alpha(colors["shadow"], 0.09)}; }`}; `; diff --git a/frontend/src/metabase/components/CollectionItem.jsx b/frontend/src/metabase/components/CollectionItem.jsx new file mode 100644 index 0000000000000..b823b1ac2080d --- /dev/null +++ b/frontend/src/metabase/components/CollectionItem.jsx @@ -0,0 +1,28 @@ +import React from "react"; +import { Flex } from "grid-styled"; +import Ellipsified from "metabase/components/Ellipsified"; +import Icon from "metabase/components/Icon"; +import Link from "metabase/components/Link"; + +import colors from "metabase/lib/colors"; + +const CollectionItem = ({ collection, color, iconName = "all" }) => ( + + + +

+ {collection.name} +

+
+ +); + +export default CollectionItem; diff --git a/frontend/src/metabase/components/CollectionLanding.jsx b/frontend/src/metabase/components/CollectionLanding.jsx index 8612d0853c02a..764e62227181b 100644 --- a/frontend/src/metabase/components/CollectionLanding.jsx +++ b/frontend/src/metabase/components/CollectionLanding.jsx @@ -39,6 +39,55 @@ import PinDropTarget from "metabase/containers/dnd/PinDropTarget"; import ItemsDragLayer from "metabase/containers/dnd/ItemsDragLayer"; const ROW_HEIGHT = 72; +const PAGE_PADDING = [2, 3, 4]; + +const EmptyStateWrapper = ({ children }) => ( + + {children} + +); + +const DashboardEmptyState = () => ( + + + + +

{t`Dashboards let you collect and share data in one place.`}

+
+); + +const PulseEmptyState = () => ( + + + + +

{t`Pulses let you send out the latest data to your team on a schedule via email or slack.`}

+
+); + +const QuestionEmptyState = () => ( + + + + +

{t`Quesitons are a saved look at your data.`}

+
+); + +const EMPTY_STATES = { + dashboard: , + pulse: , + card: , +}; import { entityListLoader } from "metabase/entities/containers/EntityListLoader"; @@ -74,6 +123,7 @@ class DefaultLanding extends React.Component { render() { const { + ancestors, collection, collectionId, @@ -81,6 +131,7 @@ class DefaultLanding extends React.Component { pinned, unpinned, + isRoot, selected, selection, onToggleSelected, @@ -97,17 +148,64 @@ class DefaultLanding extends React.Component { onSelectNone(); }; - const collectionWidth = unpinned.length > 0 ? 1 / 3 : 1; - const itemWidth = unpinned.length > 0 ? 2 / 3 : 0; - const collectionGridSize = unpinned.length > 0 ? 1 : 1 / 4; + const collectionWidth = unpinned.length > 0 ? [1, 1 / 3] : 1; + const itemWidth = unpinned.length > 0 ? [1, 2 / 3] : 0; + const collectionGridSize = unpinned.length > 0 ? 1 : [1, 1 / 4]; + + let unpinnedItems = unpinned; + + if (location.query.type) { + unpinnedItems = unpinned.filter(u => u.model === location.query.type); + } return ( + + + + ({ + title: ( + + {name} + + ), + to: Urls.collection(id), + })), + ]} + /> + +

{collection.name}

+
+ + + {collection && + collection.can_write && + !collection.personal_owner_id && ( + + + + )} + + + + +
{pinned.length > 0 ? ( - + {t`Pins`} {pinned.map((item, index) => ( - + )} - + - + {t`Collections`} @@ -180,63 +282,70 @@ class DefaultLanding extends React.Component { /> - - {unpinned.length > 0 ? ( - - - - 0 ? 5 : 2} - style={{ - position: "relative", - height: ROW_HEIGHT * unpinned.length, - }} - > - u.model === location.query.type, - ) - : unpinned - } - rowHeight={ROW_HEIGHT} - renderItem={({ item, index }) => ( - - - this.setState({ moveItems }) - } - /> - - )} - /> - - - - ) : ( - - {({ hovered }) => ( -
- {t`Drag here to un-pin`} -
- )} -
- )} -
+ {unpinned.length > 0 && ( + + + + + {unpinnedItems.length > 0 ? ( + + + ( + + + + this.setState({ moveItems }) + } + /> + + + )} + /> + + + ) : ( + + {location.query.type && + EMPTY_STATES[location.query.type]} + + {({ hovered }) => ( +
+ {t`Drag here to un-pin`} +
+ )} +
+
+ )} +
+
+
+ )}
@@ -310,6 +419,7 @@ export const NormalItem = ({ }) => ( 0} selectable item={item} @@ -413,53 +523,16 @@ class CollectionLanding extends React.Component { return ( - - - - - ({ - title: ( - - {name} - - ), - to: Urls.collection(id), - })), - ]} - /> - -

{currentCollection.name}

-
- - - {currentCollection && - currentCollection.can_write && - !currentCollection.personal_owner_id && ( - - - - )} - - - - -
-
- - - { - // Need to have this here so the child modals will show up - this.props.children - } - + + { + // Need to have this here so the child modals will show up + this.props.children + }
); } diff --git a/frontend/src/metabase/components/CollectionList.jsx b/frontend/src/metabase/components/CollectionList.jsx index 48f3a54201857..587dea43b053e 100644 --- a/frontend/src/metabase/components/CollectionList.jsx +++ b/frontend/src/metabase/components/CollectionList.jsx @@ -3,10 +3,10 @@ import { t } from "c-3po"; import { Box, Flex } from "grid-styled"; import { connect } from "react-redux"; -import colors, { normal } from "metabase/lib/colors"; import * as Urls from "metabase/lib/urls"; -import Ellipsified from "metabase/components/Ellipsified"; +import CollectionItem from "metabase/components/CollectionItem"; +import { normal } from "metabase/lib/colors"; import { Grid, GridItem } from "metabase/components/Grid"; import Icon from "metabase/components/Icon"; import Link from "metabase/components/Link"; @@ -14,23 +14,6 @@ import Link from "metabase/components/Link"; import CollectionDropTarget from "metabase/containers/dnd/CollectionDropTarget"; import ItemDragSource from "metabase/containers/dnd/ItemDragSource"; -const CollectionItem = ({ collection, color, iconName = "all" }) => ( - - - - -

- {collection.name} -

-
-
- -); - @connect(({ currentUser }) => ({ currentUser }), null) class CollectionList extends React.Component { render() { @@ -42,12 +25,12 @@ class CollectionList extends React.Component { w, } = this.props; return ( - + {collections .filter(c => c.id !== currentUser.personal_collection_id) .map(collection => ( - + @@ -56,7 +39,7 @@ class CollectionList extends React.Component { ))} {isRoot && ( - + @@ -92,13 +75,12 @@ class CollectionList extends React.Component { to={Urls.newCollection(currentCollection.id)} color={normal.grey2} hover={{ color: normal.blue }} + p={w === 1 ? [1, 2] : 0} > - - - -

{t`New collection`}

-
-
+ + +

{t`New collection`}

+
)} diff --git a/frontend/src/metabase/components/CreateDashboardModal.jsx b/frontend/src/metabase/components/CreateDashboardModal.jsx index ef2f0b26e74c2..d1f353ad67215 100644 --- a/frontend/src/metabase/components/CreateDashboardModal.jsx +++ b/frontend/src/metabase/components/CreateDashboardModal.jsx @@ -25,17 +25,18 @@ const mapDispatchToProps = { export default class CreateDashboardModal extends Component { constructor(props, context) { super(props, context); - this.createNewDash = this.createNewDash.bind(this); - this.setDescription = this.setDescription.bind(this); - this.setName = this.setName.bind(this); this.state = { name: null, description: null, errors: null, - // collectionId in the url starts off as a string, but the select will - // compare it to the integer ID on colleciton objects - collection_id: parseInt(props.params.collectionId), + collection_id: + props.collectionId != null + ? props.collectionId + : props.params.collectionId != null && + props.params.collectionId !== "root" + ? parseInt(props.params.collectionId) + : null, }; } @@ -44,15 +45,15 @@ export default class CreateDashboardModal extends Component { onClose: PropTypes.func, }; - setName(event) { + setName = event => { this.setState({ name: event.target.value }); - } + }; - setDescription(event) { + setDescription = event => { this.setState({ description: event.target.value }); - } + }; - async createNewDash(event) { + createNewDash = async event => { event.preventDefault(); let name = this.state.name && this.state.name.trim(); @@ -68,7 +69,7 @@ export default class CreateDashboardModal extends Component { const { payload } = await this.props.createDashboard(newDash); this.props.push(Urls.dashboard(payload.result)); this.props.onClose(); - } + }; render() { let formError; diff --git a/frontend/src/metabase/components/EntityItem.jsx b/frontend/src/metabase/components/EntityItem.jsx index b77542e0675cd..e341992e3f659 100644 --- a/frontend/src/metabase/components/EntityItem.jsx +++ b/frontend/src/metabase/components/EntityItem.jsx @@ -1,7 +1,8 @@ import React from "react"; import { t } from "c-3po"; - +import cx from "classnames"; import { Flex } from "grid-styled"; + import EntityMenu from "metabase/components/EntityMenu"; import Swapper from "metabase/components/Swapper"; import IconWrapper from "metabase/components/IconWrapper"; @@ -12,7 +13,7 @@ import Icon from "metabase/components/Icon"; import colors from "metabase/lib/colors"; const EntityItemWrapper = Flex.extend` - border-bottom: 1px solid ${colors["bg-light"]}; + border-bottom: 1px solid ${colors["bg-medium"]}; /* TODO - figure out how to use the prop instead of this? */ align-items: center; &:hover { @@ -32,6 +33,7 @@ const EntityItem = ({ selected, onToggleSelected, selectable, + variant, }) => { const actions = [ onPin && { @@ -51,11 +53,32 @@ const EntityItem = ({ }, ].filter(action => action); + let spacing; + + switch (variant) { + case "list": + spacing = { + px: 2, + py: 2, + }; + break; + default: + spacing = { + py: 2, + }; + break; + } + return ( - + } - swappedElement={} + defaultElement={ + + } + swappedElement={} /> ) : ( - + )}

diff --git a/frontend/src/metabase/components/ErrorDetails.jsx b/frontend/src/metabase/components/ErrorDetails.jsx index e493754382da6..82a04ca2c19b4 100644 --- a/frontend/src/metabase/components/ErrorDetails.jsx +++ b/frontend/src/metabase/components/ErrorDetails.jsx @@ -26,7 +26,7 @@ export default class ErrorDetails extends React.Component {

{t`Here's the full error message`}

{/* ensure we don't try to render anything except a string */} {typeof details === "string" diff --git a/frontend/src/metabase/components/ExplorePane.jsx b/frontend/src/metabase/components/ExplorePane.jsx index f5f062d293485..4ee8aee50e161 100644 --- a/frontend/src/metabase/components/ExplorePane.jsx +++ b/frontend/src/metabase/components/ExplorePane.jsx @@ -158,7 +158,7 @@ export const ExploreList = ({ export const ExploreOption = ({ option }: { option: Candidate }) => ( ( - + {children} ); diff --git a/frontend/src/metabase/components/Header.jsx b/frontend/src/metabase/components/Header.jsx index c062d59fc9d77..92c2fbd991e4e 100644 --- a/frontend/src/metabase/components/Header.jsx +++ b/frontend/src/metabase/components/Header.jsx @@ -1,6 +1,7 @@ import React, { Component } from "react"; import ReactDOM from "react-dom"; +import CollectionBadge from "metabase/questions/components/CollectionBadge"; import InputBlurChange from "metabase/components/InputBlurChange.jsx"; import HeaderModal from "metabase/components/HeaderModal.jsx"; import TitleAndDescription from "metabase/components/TitleAndDescription.jsx"; @@ -77,6 +78,7 @@ export default class Header extends Component { } render() { + const { item } = this.props; let titleAndDescription; if (this.props.isEditingInfo) { titleAndDescription = ( @@ -154,9 +156,12 @@ export default class Header extends Component { } ref="header" > -
+
{titleAndDescription} {attribution} + {!this.props.isEditingInfo && ( + + )}
diff --git a/frontend/src/metabase/components/Icon.jsx b/frontend/src/metabase/components/Icon.jsx index cc1d98c3be865..5d3221a19e41a 100644 --- a/frontend/src/metabase/components/Icon.jsx +++ b/frontend/src/metabase/components/Icon.jsx @@ -7,6 +7,7 @@ import { color, space, hover } from "styled-system"; import cx from "classnames"; import { loadIcon } from "metabase/icon_paths"; +import { stripLayoutProps } from "metabase/lib/utils"; import Tooltipify from "metabase/hoc/Tooltipify"; @@ -56,7 +57,7 @@ class BaseIcon extends Component { return ; } else { return ( - + ); diff --git a/frontend/src/metabase/components/IconWrapper.jsx b/frontend/src/metabase/components/IconWrapper.jsx index 27120f2fbf966..5deb01190c325 100644 --- a/frontend/src/metabase/components/IconWrapper.jsx +++ b/frontend/src/metabase/components/IconWrapper.jsx @@ -2,7 +2,7 @@ import { Flex } from "grid-styled"; import colors from "metabase/lib/colors"; const IconWrapper = Flex.extend` - background: ${props => colors["bg-light"]}; + background: ${colors["bg-medium"]}; border-radius: 6px; `; diff --git a/frontend/src/metabase/components/ItemTypeFilterBar.jsx b/frontend/src/metabase/components/ItemTypeFilterBar.jsx index 9be9685b03b08..f3c51323fc28e 100644 --- a/frontend/src/metabase/components/ItemTypeFilterBar.jsx +++ b/frontend/src/metabase/components/ItemTypeFilterBar.jsx @@ -3,6 +3,7 @@ import { Flex } from "grid-styled"; import { t } from "c-3po"; import { withRouter } from "react-router"; +import Icon from "metabase/components/Icon"; import Link from "metabase/components/Link"; import colors from "metabase/lib/colors"; @@ -11,27 +12,31 @@ export const FILTERS = [ { name: t`Everything`, filter: null, + icon: "list", }, { name: t`Dashboards`, filter: "dashboard", + icon: "dashboard", }, { name: t`Questions`, filter: "card", + icon: "beaker", }, { name: t`Pulses`, filter: "pulse", + icon: "pulse", }, ]; const ItemTypeFilterBar = props => { const { location } = props; return ( - + {props.filters.map(f => { - let isActive = location.query.type === f.filter; + let isActive = location && location.query.type === f.filter; if (!location.query.type && !f.filter) { isActive = true; @@ -47,7 +52,9 @@ const ItemTypeFilterBar = props => { }} color={color} hover={{ color: colors.brand }} - mr={2} + className="flex-full flex align-center justify-center sm-block text-brand-hover text-medium" + mr={[0, 2]} + key={f.filter} py={1} style={{ borderBottom: `2px solid ${ @@ -55,10 +62,11 @@ const ItemTypeFilterBar = props => { }`, }} > +
diff --git a/frontend/src/metabase/components/Link.jsx b/frontend/src/metabase/components/Link.jsx index 34cf81f5c560d..15cafa2f180d8 100644 --- a/frontend/src/metabase/components/Link.jsx +++ b/frontend/src/metabase/components/Link.jsx @@ -2,9 +2,14 @@ import React from "react"; import { Link as ReactRouterLink } from "react-router"; import styled from "styled-components"; import { display, color, hover, space } from "styled-system"; +import { stripLayoutProps } from "metabase/lib/utils"; const BaseLink = ({ to, className, children, ...props }) => ( - + {children} ); diff --git a/frontend/src/metabase/components/QuestionSavedModal.jsx b/frontend/src/metabase/components/QuestionSavedModal.jsx index 4af499f8af042..3e256abdbafbe 100644 --- a/frontend/src/metabase/components/QuestionSavedModal.jsx +++ b/frontend/src/metabase/components/QuestionSavedModal.jsx @@ -18,7 +18,7 @@ export default class QuestionSavedModal extends Component { onClose={this.props.onClose} className="Modal-content Modal-content--small NewForm" > -
+
); } diff --git a/frontend/src/metabase/containers/CollectionMoveModal.jsx b/frontend/src/metabase/containers/CollectionMoveModal.jsx index 2ee3045f31e94..8792b8b6bbada 100644 --- a/frontend/src/metabase/containers/CollectionMoveModal.jsx +++ b/frontend/src/metabase/containers/CollectionMoveModal.jsx @@ -1,7 +1,6 @@ import React from "react"; import PropTypes from "prop-types"; -import _ from "underscore"; import { t } from "c-3po"; import { Flex, Box } from "grid-styled"; @@ -9,7 +8,6 @@ import Subhead from "metabase/components/Subhead"; import Button from "metabase/components/Button"; import Icon from "metabase/components/Icon"; -import CollectionListLoader from "metabase/containers/CollectionListLoader"; import CollectionPicker from "metabase/containers/CollectionPicker"; class CollectionMoveModal extends React.Component { @@ -24,10 +22,7 @@ class CollectionMoveModal extends React.Component { // null = root collection // number = non-root collection id // - selectedCollection: - props.initialCollectionId === undefined - ? undefined - : { id: props.initialCollectionId }, + selectedCollectionId: props.initialCollectionId, // whether the move action has started // TODO: use this loading and error state in the UI moving: false, @@ -43,7 +38,7 @@ class CollectionMoveModal extends React.Component { }; render() { - const { selectedCollection } = this.state; + const { selectedCollectionId } = this.state; return ( @@ -55,29 +50,24 @@ class CollectionMoveModal extends React.Component { onClick={() => this.props.onClose()} /> - - {({ collections, loading, error }) => ( - - this.setState({ - selectedCollection: - id == null ? null : _.find(collections, { id }), - }) - } - collections={collections} - /> - )} - + + this.setState({ selectedCollectionId }) + } + /> (
diff --git a/frontend/src/metabase/pulse/components/PulseEdit.jsx b/frontend/src/metabase/pulse/components/PulseEdit.jsx index 77772a29ab55f..baeba0a60d96f 100644 --- a/frontend/src/metabase/pulse/components/PulseEdit.jsx +++ b/frontend/src/metabase/pulse/components/PulseEdit.jsx @@ -155,7 +155,11 @@ export default class PulseEdit extends Component { ) : ( - this.addCard(index, questionId)} + className="flex-full" + // TODO: reimplement CardPicker's warnings for unsuitable cards + // attachmentsEnabled={this.props.attachmentsEnabled} /> )}
diff --git a/frontend/src/metabase/pulse/components/PulseEditChannels.jsx b/frontend/src/metabase/pulse/components/PulseEditChannels.jsx index 6dd43b1d1a6e1..cfb60fd8475ef 100644 --- a/frontend/src/metabase/pulse/components/PulseEditChannels.jsx +++ b/frontend/src/metabase/pulse/components/PulseEditChannels.jsx @@ -276,7 +276,7 @@ export default class PulseEditChannels extends Component { />
{channels.length > 0 && channelSpec.configured ? ( -
    {channels}
+
    {channels}
) : channels.length > 0 && !channelSpec.configured ? (

{t`${ diff --git a/frontend/src/metabase/pulse/components/PulseEditCollection.jsx b/frontend/src/metabase/pulse/components/PulseEditCollection.jsx index 9d6a16b9e3a9c..461801aa9dbec 100644 --- a/frontend/src/metabase/pulse/components/PulseEditCollection.jsx +++ b/frontend/src/metabase/pulse/components/PulseEditCollection.jsx @@ -6,6 +6,7 @@ import CollectionSelect from "metabase/containers/CollectionSelect"; export default class PulseEditCollection extends React.Component { render() { + const { pulse, setPulse, initialCollectionId } = this.props; return (

{t`Which collection should this pulse live in?`}

@@ -13,12 +14,13 @@ export default class PulseEditCollection extends React.Component { - this.props.setPulse({ - ...this.props.pulse, + setPulse({ + ...pulse, collection_id, }) } diff --git a/frontend/src/metabase/pulse/components/PulseListItem.jsx b/frontend/src/metabase/pulse/components/PulseListItem.jsx index db66a0ca628d6..245f15b3ae54c 100644 --- a/frontend/src/metabase/pulse/components/PulseListItem.jsx +++ b/frontend/src/metabase/pulse/components/PulseListItem.jsx @@ -68,7 +68,7 @@ export default class PulseListItem extends Component { ))} -