diff --git a/frontend/src/metabase/admin/databases/containers/DatabaseListApp.jsx b/frontend/src/metabase/admin/databases/containers/DatabaseListApp.jsx index 2c68cd70ca786..9f52b35ddeb85 100644 --- a/frontend/src/metabase/admin/databases/containers/DatabaseListApp.jsx +++ b/frontend/src/metabase/admin/databases/containers/DatabaseListApp.jsx @@ -158,7 +158,7 @@ export default class DatabaseList extends Component { })} > this.props.addSampleDataset()} >{t`Bring the sample dataset back`} diff --git a/frontend/src/metabase/admin/datamodel/components/ObjectActionSelect.jsx b/frontend/src/metabase/admin/datamodel/components/ObjectActionSelect.jsx index e8f857284c07f..d93fd995bb9a1 100644 --- a/frontend/src/metabase/admin/datamodel/components/ObjectActionSelect.jsx +++ b/frontend/src/metabase/admin/datamodel/components/ObjectActionSelect.jsx @@ -29,7 +29,7 @@ export default class ObjectActionsSelect extends Component { + } diff --git a/frontend/src/metabase/admin/datamodel/components/database/ColumnsList.jsx b/frontend/src/metabase/admin/datamodel/components/database/ColumnsList.jsx index bb1f42b6263fd..434f90b16d4d9 100644 --- a/frontend/src/metabase/admin/datamodel/components/database/ColumnsList.jsx +++ b/frontend/src/metabase/admin/datamodel/components/database/ColumnsList.jsx @@ -15,7 +15,7 @@ export default class ColumnsList extends Component { return (

{t`Columns`}

-
+
- {t`Current database:`}{" "} + {t`Current database:`}{" "} {this.renderDbSelector()}
diff --git a/frontend/src/metabase/admin/datamodel/components/database/MetadataSchema.jsx b/frontend/src/metabase/admin/datamodel/components/database/MetadataSchema.jsx index 0ef17b9b7e743..313e5b514d6e7 100644 --- a/frontend/src/metabase/admin/datamodel/components/database/MetadataSchema.jsx +++ b/frontend/src/metabase/admin/datamodel/components/database/MetadataSchema.jsx @@ -39,7 +39,7 @@ export default class MetadataSchema extends Component {
- + diff --git a/frontend/src/metabase/admin/datamodel/components/database/MetadataTable.jsx b/frontend/src/metabase/admin/datamodel/components/database/MetadataTable.jsx index def501db12b99..4e0dc0db7f36b 100644 --- a/frontend/src/metabase/admin/datamodel/components/database/MetadataTable.jsx +++ b/frontend/src/metabase/admin/datamodel/components/database/MetadataTable.jsx @@ -79,7 +79,7 @@ export default class MetadataTable extends Component { if (this.props.tableMetadata.visibility_type) { subTypes = ( - {t`Why Hide?`} + {t`Why Hide?`} {this.renderVisibilityType(t`Technical Data`, "technical")} {this.renderVisibilityType(t`Irrelevant/Cruft`, "cruft")} @@ -117,7 +117,7 @@ export default class MetadataTable extends Component { placeholder={t`No table description yet`} /> -
+
{t`Visibility`} {this.renderVisibilityWidget()} diff --git a/frontend/src/metabase/admin/datamodel/components/database/MetricsList.jsx b/frontend/src/metabase/admin/datamodel/components/database/MetricsList.jsx index 7b58dfbfa1724..bd48ca1ad4016 100644 --- a/frontend/src/metabase/admin/datamodel/components/database/MetricsList.jsx +++ b/frontend/src/metabase/admin/datamodel/components/database/MetricsList.jsx @@ -50,7 +50,7 @@ export default class MetricsList extends Component {
{t`Column`} {t`Data Type`}
{tableMetadata.metrics.length === 0 && ( -
+
{t`Create metrics to add them to the View dropdown in the query builder`}
)} diff --git a/frontend/src/metabase/admin/datamodel/components/database/SegmentsList.jsx b/frontend/src/metabase/admin/datamodel/components/database/SegmentsList.jsx index 3801e7abed03e..316e0353c29c9 100644 --- a/frontend/src/metabase/admin/datamodel/components/database/SegmentsList.jsx +++ b/frontend/src/metabase/admin/datamodel/components/database/SegmentsList.jsx @@ -50,7 +50,7 @@ export default class SegmentsList extends Component { {tableMetadata.segments.length === 0 && ( -
+
{t`Create segments to add them to the Filter dropdown in the query builder`}
)} diff --git a/frontend/src/metabase/admin/datamodel/components/revisions/Revision.jsx b/frontend/src/metabase/admin/datamodel/components/revisions/Revision.jsx index 688c901b64c01..c2f0b0638b601 100644 --- a/frontend/src/metabase/admin/datamodel/components/revisions/Revision.jsx +++ b/frontend/src/metabase/admin/datamodel/components/revisions/Revision.jsx @@ -72,7 +72,7 @@ export default class Revision extends Component { />
-
+
{this.getName()} {this.getAction()} diff --git a/frontend/src/metabase/admin/datamodel/containers/FieldApp.jsx b/frontend/src/metabase/admin/datamodel/containers/FieldApp.jsx index 2814efbc573ed..bdc10d3171b94 100644 --- a/frontend/src/metabase/admin/datamodel/containers/FieldApp.jsx +++ b/frontend/src/metabase/admin/datamodel/containers/FieldApp.jsx @@ -284,7 +284,7 @@ export const BackButton = ({ databaseId, tableId }) => ( ); const SelectSeparator = () => ( - + ); export class FieldHeader extends Component { @@ -469,7 +469,7 @@ export const SectionHeader = ({ title, description }) => (

{title}

{description && ( -

+

{description}

)} @@ -727,7 +727,7 @@ export class FieldRemapping extends Component { {fkMappingField ? ( fkMappingField.display_name ) : ( - {t`Choose a field`} + {t`Choose a field`} )} } diff --git a/frontend/src/metabase/admin/people/components/GroupDetail.jsx b/frontend/src/metabase/admin/people/components/GroupDetail.jsx index add1f3a65aec1..a0f7c6cb46efd 100644 --- a/frontend/src/metabase/admin/people/components/GroupDetail.jsx +++ b/frontend/src/metabase/admin/people/components/GroupDetail.jsx @@ -118,7 +118,7 @@ const AddUserRow = ({
{user.common_name} onRemoveUserFromSelection(user)} /> @@ -147,7 +147,7 @@ const UserRow = ({ user, showRemoveButton, onRemoveUserClicked }) => ( className="text-right cursor-pointer" onClick={onRemoveUserClicked.bind(null, user)} > - + ) : null} diff --git a/frontend/src/metabase/admin/people/components/GroupsListing.jsx b/frontend/src/metabase/admin/people/components/GroupsListing.jsx index d1858b0daf787..a3cd6c2605dd1 100644 --- a/frontend/src/metabase/admin/people/components/GroupsListing.jsx +++ b/frontend/src/metabase/admin/people/components/GroupsListing.jsx @@ -79,7 +79,7 @@ function ActionsPopover({ group, onEditGroupClicked, onDeleteGroupClicked }) { return ( } + triggerElement={} > ) : ( -
+

{t`You haven't looked at any dashboards or questions recently`} diff --git a/frontend/src/metabase/home/components/Smile.jsx b/frontend/src/metabase/home/components/Smile.jsx deleted file mode 100644 index 79c1c09127c4e..0000000000000 --- a/frontend/src/metabase/home/components/Smile.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import React, { Component } from "react"; - -export default class Smile extends Component { - render() { - const styles = { - width: "48px", - height: "48px", - backgroundImage: 'url("app/assets/img/smile.svg")', - }; - return

; - } -} diff --git a/frontend/src/metabase/home/containers/ArchiveApp.jsx b/frontend/src/metabase/home/containers/ArchiveApp.jsx index f228427d6c02f..2aba7ab44f169 100644 --- a/frontend/src/metabase/home/containers/ArchiveApp.jsx +++ b/frontend/src/metabase/home/containers/ArchiveApp.jsx @@ -76,9 +76,11 @@ export default class ArchiveApp extends Component { 0}> - - - {t`${selected.length} items selected`} + + + + {t`${selected.length} items selected`} + ); diff --git a/frontend/src/metabase/home/containers/SearchApp.jsx b/frontend/src/metabase/home/containers/SearchApp.jsx index 00607785497bc..0201ef6a807fc 100644 --- a/frontend/src/metabase/home/containers/SearchApp.jsx +++ b/frontend/src/metabase/home/containers/SearchApp.jsx @@ -65,7 +65,7 @@ export default class SearchApp extends React.Component { {types.dashboard && ( -
+
{t`Dashboards`}
@@ -83,7 +83,7 @@ export default class SearchApp extends React.Component { )} {types.collection && ( -
+
{t`Collections`}
@@ -101,7 +101,7 @@ export default class SearchApp extends React.Component { )} {types.card && ( -
+
{t`Questions`}
@@ -119,7 +119,7 @@ export default class SearchApp extends React.Component { )} {types.pulse && ( -
+
{t`Pulse`}
diff --git a/frontend/src/metabase/icon_paths.js b/frontend/src/metabase/icon_paths.js index ec21e42f56f66..6bf7d0429c295 100644 --- a/frontend/src/metabase/icon_paths.js +++ b/frontend/src/metabase/icon_paths.js @@ -236,8 +236,6 @@ export const ICON_PATHS = { "M7 12H5.546A3.548 3.548 0 0 0 2 15.553v12.894A3.547 3.547 0 0 0 5.546 32h20.908C28.414 32 30 30.41 30 28.447V15.553A3.547 3.547 0 0 0 26.454 12H25V8.99C25 4.029 20.97 0 16 0c-4.972 0-9 4.025-9 8.99V12zm4-3.766c0-2.338 1.89-4.413 4.219-4.634L16 3.525l.781.075C19.111 3.82 21 5.896 21 8.234V12H11V8.234zm-5 9.537C6 16.793 6.796 16 7.775 16h16.45c.98 0 1.775.787 1.775 1.77v8.46c0 .977-.796 1.77-1.775 1.77H7.775A1.77 1.77 0 0 1 6 26.23v-8.46zM16 25a3 3 0 1 0 0-6 3 3 0 0 0 0 6z", mail: "M1.503 6h28.994C31.327 6 32 6.673 32 7.503v16.06A3.436 3.436 0 0 1 28.564 27H3.436A3.436 3.436 0 0 1 0 23.564V7.504C0 6.673.673 6 1.503 6zm4.403 2.938l10.63 8.052 10.31-8.052H5.906zm-2.9 1.632v11.989c0 .83.674 1.503 1.504 1.503h23.087c.83 0 1.504-.673 1.504-1.503V11.005l-11.666 8.891a1.503 1.503 0 0 1-1.806.013l-12.622-9.34z", - mine: - "M28.4907419,50 C25.5584999,53.6578499 21.0527692,56 16,56 C10.9472308,56 6.44150015,53.6578499 3.50925809,50 L28.4907419,50 Z M29.8594823,31.9999955 C27.0930063,27.217587 21.922257,24 16,24 C10.077743,24 4.9069937,27.217587 2.1405177,31.9999955 L29.8594849,32 Z M16,21 C19.8659932,21 23,17.1944204 23,12.5 C23,7.80557963 22,3 16,3 C10,3 9,7.80557963 9,12.5 C9,17.1944204 12.1340068,21 16,21 Z", moon: "M11.6291702,1.84239429e-11 C19.1234093,1.22958025 24.8413559,7.73631246 24.8413559,15.5785426 C24.8413559,24.2977683 17.7730269,31.3660972 9.05380131,31.3660972 C7.28632096,31.3660972 5.58667863,31.0756481 4,30.5398754 C11.5007933,28.2096945 16.9475786,21.2145715 16.9475786,12.9472835 C16.9475786,7.90001143 14.9174312,3.32690564 11.6291702,1.70246039e-11 L11.6291702,1.84239429e-11 Z", move: @@ -340,8 +338,6 @@ export const ICON_PATHS = { "M12.3069589,4.52260192 C14.3462632,1.2440969 17.653446,1.24541073 19.691933,4.52260192 L31.2249413,23.0637415 C33.2642456,26.3422466 31.7889628,29 27.9115531,29 L4.08733885,29 C0.218100769,29 -1.26453645,26.3409327 0.77395061,23.0637415 L12.3069589,4.52260192 Z M18.0499318,23.0163223 C18.0499772,23.0222378 18.05,23.0281606 18.05,23.0340907 C18.05,23.3266209 17.9947172,23.6030345 17.8840476,23.8612637 C17.7737568,24.1186089 17.6195847,24.3426723 17.4224081,24.5316332 C17.2266259,24.7192578 16.998292,24.8660439 16.7389806,24.9713892 C16.4782454,25.0773129 16.1979962,25.1301134 15.9,25.1301134 C15.5950083,25.1301134 15.3111795,25.0774024 15.0502239,24.9713892 C14.7901813,24.8657469 14.5629613,24.7183609 14.3703047,24.5298034 C14.177545,24.3411449 14.0258626,24.1177208 13.9159524,23.8612637 C13.8052827,23.6030345 13.75,23.3266209 13.75,23.0340907 C13.75,22.7411889 13.8054281,22.4661013 13.9165299,22.2109786 C14.0264627,21.9585404 14.1779817,21.7374046 14.3703047,21.5491736 C14.5621821,21.3613786 14.7883231,21.2126553 15.047143,21.1034656 C15.3089445,20.9930181 15.593871,20.938068 15.9,20.938068 C16.1991423,20.938068 16.4804862,20.9931136 16.7420615,21.1034656 C17.0001525,21.2123478 17.2274115,21.360472 17.4224081,21.5473437 C17.6191428,21.7358811 17.7731504,21.957652 17.88347,22.2109786 C17.9124619,22.2775526 17.9376628,22.3454862 17.9590769,22.414741 C18.0181943,22.5998533 18.05,22.7963729 18.05,23 C18.05,23.0054459 18.0499772,23.0108867 18.0499318,23.0163223 L18.0499318,23.0163223 Z M17.7477272,14.1749999 L17.7477272,8.75 L14.1170454,8.75 L14.1170454,14.1749999 C14.1170454,14.8471841 14.1572355,15.5139742 14.2376219,16.1753351 C14.3174838,16.8323805 14.4227217,17.5019113 14.5533248,18.1839498 L14.5921937,18.3869317 L17.272579,18.3869317 L17.3114479,18.1839498 C17.442051,17.5019113 17.5472889,16.8323805 17.6271507,16.1753351 C17.7075371,15.5139742 17.7477272,14.8471841 17.7477272,14.1749999 Z", attrs: { fillRule: "evenodd" }, }, - x: - "m11.271709,16 l-3.19744231e-13,4.728291 l4.728291,0 l16,11.271709 l27.271709,2.39808173e-13 l32,4.728291 l20.728291,16 l31.1615012,26.4332102 l26.4332102,31.1615012 l16,20.728291 l5.56678976,31.1615012 l0.838498756,26.4332102 l11.271709,16 z", zoom: "M12.416 12.454V8.37h3.256v4.083h4.07v3.266h-4.07v4.083h-3.256V15.72h-4.07v-3.266h4.07zm10.389 13.28c-5.582 4.178-13.543 3.718-18.632-1.37-5.58-5.581-5.595-14.615-.031-20.179 5.563-5.563 14.597-5.55 20.178.031 5.068 5.068 5.545 12.985 1.422 18.563l5.661 5.661a2.08 2.08 0 0 1 .003 2.949 2.085 2.085 0 0 1-2.95-.003l-5.651-5.652zm-1.486-4.371c3.895-3.895 3.885-10.218-.021-14.125-3.906-3.906-10.23-3.916-14.125-.021-3.894 3.894-3.885 10.218.022 14.124 3.906 3.907 10.23 3.916 14.124.022z", slack: { diff --git a/frontend/src/metabase/lib/groups.js b/frontend/src/metabase/lib/groups.js index 1daa340310135..dfdebe013e684 100644 --- a/frontend/src/metabase/lib/groups.js +++ b/frontend/src/metabase/lib/groups.js @@ -21,5 +21,5 @@ export function canEditMembership(group) { export function getGroupColor(group) { return isAdminGroup(group) ? "text-purple" - : isDefaultGroup(group) ? "text-grey-4" : "text-brand"; + : isDefaultGroup(group) ? "text-medium" : "text-brand"; } diff --git a/frontend/src/metabase/lib/utils.js b/frontend/src/metabase/lib/utils.js index cc5d8ff6f8e46..e69b7d9f502b7 100644 --- a/frontend/src/metabase/lib/utils.js +++ b/frontend/src/metabase/lib/utils.js @@ -48,8 +48,9 @@ let MetabaseUtils = { generatePassword: function(complexity) { complexity = complexity || MetabaseSettings.passwordComplexityRequirements() || {}; - // fall back to length of 14 if the password_complexity Setting isn't set or total isn't passed in - const len = complexity.total || 14; + // generated password must be at least `complexity.total`, but can be longer + // so hard code a minimum of 14 + const len = Math.max(complexity.total || 0, 14); let password = ""; let tries = 0; diff --git a/frontend/src/metabase/meta/types/Dashboard.js b/frontend/src/metabase/meta/types/Dashboard.js index e48cd9358eacb..d09bed096de8e 100644 --- a/frontend/src/metabase/meta/types/Dashboard.js +++ b/frontend/src/metabase/meta/types/Dashboard.js @@ -18,6 +18,7 @@ export type Dashboard = { show_in_getting_started?: boolean, // incomplete parameters: Array, + collection_id: ?number, }; // TODO Atte Keinänen 4/5/16: After upgrading Flow, use spread operator `...Dashboard` @@ -28,6 +29,7 @@ export type DashboardWithCards = { ordered_cards: Array, // incomplete parameters: Array, + collection_id: ?number, }; export type DashCardId = number; diff --git a/frontend/src/metabase/nav/components/ProfileLink.jsx b/frontend/src/metabase/nav/components/ProfileLink.jsx index 81c71d25c45f5..29a84babb823b 100644 --- a/frontend/src/metabase/nav/components/ProfileLink.jsx +++ b/frontend/src/metabase/nav/components/ProfileLink.jsx @@ -89,13 +89,13 @@ export default class ProfileLink extends Component {

{t`You're on version`} {tag}

-

+

{t`Built on`} {date}

{!/^v\d+\.\d+\.\d+$/.test(tag) && (
{_.map(versionExtra, (value, key) => ( -

+

{capitalize(key)}: {value}

))} @@ -105,7 +105,7 @@ export default class ProfileLink extends Component {
Metabase{" "} diff --git a/frontend/src/metabase/nav/containers/Navbar.jsx b/frontend/src/metabase/nav/containers/Navbar.jsx index 32c748ccadf92..4c21e859c7673 100644 --- a/frontend/src/metabase/nav/containers/Navbar.jsx +++ b/frontend/src/metabase/nav/containers/Navbar.jsx @@ -248,15 +248,16 @@ export default class Navbar extends Component { py={1} pr={2} > - - - - - + + + {title} -

{description}

+

{description}

); diff --git a/frontend/src/metabase/public/components/EmbedFrame.css b/frontend/src/metabase/public/components/EmbedFrame.css index e431a2169bfbb..35865ff14f206 100644 --- a/frontend/src/metabase/public/components/EmbedFrame.css +++ b/frontend/src/metabase/public/components/EmbedFrame.css @@ -31,7 +31,7 @@ .Theme--night.EmbedFrame .DashCard .Card { background-color: var(--color-bg-black); - border: 1px solid var(--color-accent2); + border: 1px solid var(--color-bg-dark); } .Theme--night.EmbedFrame .enable-dots-onhover .dc-tooltip circle.dot:hover, diff --git a/frontend/src/metabase/public/components/EmbedFrame.jsx b/frontend/src/metabase/public/components/EmbedFrame.jsx index 96a9b1c80890f..93a4ae4587f2d 100644 --- a/frontend/src/metabase/public/components/EmbedFrame.jsx +++ b/frontend/src/metabase/public/components/EmbedFrame.jsx @@ -143,7 +143,7 @@ export default class EmbedFrame extends Component { )} {actionButtons && ( -
+
{actionButtons}
)} diff --git a/frontend/src/metabase/public/components/LogoBadge.jsx b/frontend/src/metabase/public/components/LogoBadge.jsx index 2ccdd5dd7f832..83eaa83e9455b 100644 --- a/frontend/src/metabase/public/components/LogoBadge.jsx +++ b/frontend/src/metabase/public/components/LogoBadge.jsx @@ -17,7 +17,7 @@ const LogoBadge = ({ dark }: Props) => ( > - Powered by{" "} + Powered by{" "} Metabase diff --git a/frontend/src/metabase/public/components/widgets/EmbedModalContent.jsx b/frontend/src/metabase/public/components/widgets/EmbedModalContent.jsx index d08380aa45250..94ef9c5974a38 100644 --- a/frontend/src/metabase/public/components/widgets/EmbedModalContent.jsx +++ b/frontend/src/metabase/public/components/widgets/EmbedModalContent.jsx @@ -188,7 +188,7 @@ export default class EmbedModalContent extends Component { /> { @@ -286,7 +286,7 @@ export const EmbedTitle = ({ }) => ( Sharing - {type && } + {type && } {type} ); diff --git a/frontend/src/metabase/public/components/widgets/SharingPane.jsx b/frontend/src/metabase/public/components/widgets/SharingPane.jsx index 420292342cadd..a588b9a312afa 100644 --- a/frontend/src/metabase/public/components/widgets/SharingPane.jsx +++ b/frontend/src/metabase/public/components/widgets/SharingPane.jsx @@ -126,7 +126,7 @@ export default class SharingPane extends Component { "cursor-pointer text-brand-hover text-bold text-uppercase", extension === this.state.extension ? "text-brand" - : "text-grey-2", + : "text-light", )} onClick={() => this.setState({ diff --git a/frontend/src/metabase/public/containers/PublicQuestion.jsx b/frontend/src/metabase/public/containers/PublicQuestion.jsx index fe5f9c9e28120..ee0cdd276118d 100644 --- a/frontend/src/metabase/public/containers/PublicQuestion.jsx +++ b/frontend/src/metabase/public/containers/PublicQuestion.jsx @@ -167,7 +167,7 @@ export default class PublicQuestion extends Component { const actionButtons = result && ( { - this.setState({ inputValue: target.value }); - }; - - onInputFocus = () => { - this.setState({ isOpen: true }); - }; - - onInputBlur = () => { - // Without a timeout here isOpen gets set to false when an item is clicked - // which causes the click handler to not fire. For some reason this even - // happens with a 100ms delay, but not 200ms? - clearTimeout(this._timer); - this._timer = setTimeout(() => { - if (!this.state.isClicking) { - this.setState({ isOpen: false }); - } else { - this.setState({ isClicking: false }); - } - }, 250); - }; - - onChange = id => { - this.props.onChange(id); - ReactDOM.findDOMNode(this.refs.input).blur(); - }; - - renderItem(card) { - const { attachmentsEnabled } = this.props; - let error; - try { - if (!attachmentsEnabled && Query.isBareRows(card.dataset_query.query)) { - error = t`Raw data cannot be included in pulses`; - } - } catch (e) {} - if ( - !attachmentsEnabled && - (card.display === "pin_map" || - card.display === "state" || - card.display === "country") - ) { - error = t`Maps cannot be included in pulses`; - } - - if (error) { - return ( -
  • -

    {card.name}

    -

    {error}

    -
  • - ); - } else { - return ( -
  • -

    {card.name}

    -
  • - ); - } - } - - // keep the modal width in sync with the input width :-/ - componentDidUpdate() { - let { scrollWidth } = ReactDOM.findDOMNode(this.refs.input); - if (this.state.inputWidth !== scrollWidth) { - this.setState({ inputWidth: scrollWidth }); - } - } - - render() { - let { cardList } = this.props; - - let { isOpen, inputValue, inputWidth, collectionId } = this.state; - - let cardByCollectionId = _.groupBy(cardList, "collection_id"); - let collectionIds = Object.keys(cardByCollectionId); - - const collections = _.chain(cardList) - .map(card => card.collection) - .uniq(c => c && c.id) - .filter(c => c) - .sortBy("name") - // add "Everything else" as the last option for cards without a - // collection - .concat([{ id: null, name: t`Everything else` }]) - .value(); - - let visibleCardList; - if (inputValue) { - let searchString = inputValue.toLowerCase(); - visibleCardList = cardList.filter( - card => - ~(card.name || "").toLowerCase().indexOf(searchString) || - ~(card.description || "").toLowerCase().indexOf(searchString), - ); - } else { - if (collectionId !== undefined) { - visibleCardList = cardByCollectionId[collectionId]; - } else if (collectionIds.length === 1) { - visibleCardList = cardByCollectionId[collectionIds[0]]; - } - } - - const collection = _.findWhere(collections, { id: collectionId }); - return ( -
    - - 0} - hasArrow={false} - tetherOptions={{ - attachment: "top left", - targetAttachment: "bottom left", - targetOffset: "0 0", - }} - > -
    - {visibleCardList && - collectionIds.length > 1 && ( -
    { - this.setState({ - collectionId: undefined, - isClicking: true, - }); - }} - > - -

    {collection && collection.name}

    -
    - )} - {visibleCardList ? ( -
      - {visibleCardList.map(card => this.renderItem(card))} -
    - ) : collections ? ( - - {collections.map(collection => ( - { - this.setState({ - collectionId: collection.id, - isClicking: true, - }); - }} - /> - ))} - - ) : null} -
    -
    -
    - ); - } -} - -const CollectionListItem = ({ collection, onClick }) => ( -
  • - -

    {collection.name}

    - -
  • -); - -CollectionListItem.propTypes = { - collection: PropTypes.object.isRequired, - onClick: PropTypes.func.isRequired, -}; - -const CollectionList = ({ children }) => ( -
      {children}
    -); - -CollectionList.propTypes = { - children: PropTypes.array.isRequired, -}; diff --git a/frontend/src/metabase/pulse/components/PulseCardPreview.jsx b/frontend/src/metabase/pulse/components/PulseCardPreview.jsx index e021fbde5553c..2b2c349876262 100644 --- a/frontend/src/metabase/pulse/components/PulseCardPreview.jsx +++ b/frontend/src/metabase/pulse/components/PulseCardPreview.jsx @@ -75,7 +75,7 @@ export default class PulseCardPreview extends Component { }} >
    ( -
    {children}
    +
    {children}
    ); RenderedPulseCardPreviewMessage.propTypes = { diff --git a/frontend/src/metabase/pulse/components/PulseEdit.jsx b/frontend/src/metabase/pulse/components/PulseEdit.jsx index baeba0a60d96f..25ecad73e11d8 100644 --- a/frontend/src/metabase/pulse/components/PulseEdit.jsx +++ b/frontend/src/metabase/pulse/components/PulseEdit.jsx @@ -26,7 +26,20 @@ import cx from "classnames"; import { connect } from "react-redux"; import { goBack } from "react-router-redux"; -@connect(null, { goBack }) +import Collections from "metabase/entities/collections"; + +const mapStateToProps = (state, props) => ({ + initialCollectionId: Collections.selectors.getInitialCollectionId( + state, + props, + ), +}); + +const mapDispatchToProps = { + goBack, +}; + +@connect(mapStateToProps, mapDispatchToProps) @withRouter export default class PulseEdit extends Component { constructor(props) { @@ -47,12 +60,15 @@ export default class PulseEdit extends Component { saveEditingPulse: PropTypes.func.isRequired, deletePulse: PropTypes.func.isRequired, onChangeLocation: PropTypes.func.isRequired, - location: PropTypes.object, goBack: PropTypes.func, + initialCollectionId: PropTypes.number, }; componentDidMount() { - this.props.setEditingPulse(this.props.pulseId); + this.props.setEditingPulse( + this.props.pulseId, + this.props.initialCollectionId, + ); this.props.fetchCards(); this.props.fetchUsers(); this.props.fetchPulseFormInput(); @@ -123,7 +139,7 @@ export default class PulseEdit extends Component { } render() { - const { pulse, formInput, location } = this.props; + const { pulse, formInput } = this.props; const isValid = pulseIsValid(pulse, formInput.channels); const attachmentsEnabled = emailIsEnabled(pulse); return ( @@ -152,15 +168,7 @@ export default class PulseEdit extends Component {
    - +

    {t`Pick your data`}

    -

    +

    {t`Choose questions you'd like to send in this pulse`}.

      diff --git a/frontend/src/metabase/pulse/components/PulseEditChannels.jsx b/frontend/src/metabase/pulse/components/PulseEditChannels.jsx index cfb60fd8475ef..00a0202ac6749 100644 --- a/frontend/src/metabase/pulse/components/PulseEditChannels.jsx +++ b/frontend/src/metabase/pulse/components/PulseEditChannels.jsx @@ -263,7 +263,7 @@ export default class PulseEditChannels extends Component {
      {CHANNEL_ICONS[channelSpec.type] && ( diff --git a/frontend/src/metabase/pulse/components/PulseEditCollection.jsx b/frontend/src/metabase/pulse/components/PulseEditCollection.jsx index 461801aa9dbec..34f4ae1cd567e 100644 --- a/frontend/src/metabase/pulse/components/PulseEditCollection.jsx +++ b/frontend/src/metabase/pulse/components/PulseEditCollection.jsx @@ -6,18 +6,14 @@ import CollectionSelect from "metabase/containers/CollectionSelect"; export default class PulseEditCollection extends React.Component { render() { - const { pulse, setPulse, initialCollectionId } = this.props; + const { pulse, setPulse } = this.props; return (

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

      setPulse({ ...pulse, diff --git a/frontend/src/metabase/pulse/components/PulseEditName.jsx b/frontend/src/metabase/pulse/components/PulseEditName.jsx index 3bf984dfd32e2..27d6c9f670cbb 100644 --- a/frontend/src/metabase/pulse/components/PulseEditName.jsx +++ b/frontend/src/metabase/pulse/components/PulseEditName.jsx @@ -33,7 +33,7 @@ export default class PulseEditName extends Component { return (

      {t`Name your pulse`}

      -

      +

      {t`Give your pulse a name to help others understand what it's about`}.

      diff --git a/frontend/src/metabase/pulse/components/PulseEditSkip.jsx b/frontend/src/metabase/pulse/components/PulseEditSkip.jsx index 32e29bf58225a..b178c2e5fa842 100644 --- a/frontend/src/metabase/pulse/components/PulseEditSkip.jsx +++ b/frontend/src/metabase/pulse/components/PulseEditSkip.jsx @@ -20,7 +20,7 @@ export default class PulseEditSkip extends Component { return (

      {t`Skip if no results`}

      -

      +

      {t`Skip a scheduled Pulse if none of its questions have any results`}.

      diff --git a/frontend/src/metabase/pulse/components/PulseListChannel.jsx b/frontend/src/metabase/pulse/components/PulseListChannel.jsx index 155a777ec45bf..29a0c21b69f8f 100644 --- a/frontend/src/metabase/pulse/components/PulseListChannel.jsx +++ b/frontend/src/metabase/pulse/components/PulseListChannel.jsx @@ -70,7 +70,7 @@ export default class PulseListChannel extends Component { } return ( -
      +
      {channelIcon && } {channelVerb + " "} @@ -105,7 +105,7 @@ export default class PulseListChannel extends Component { channel.channel_type }`}
      {t`Pulses let you send data from Metabase to email or Slack on the schedule of your choice.`} diff --git a/frontend/src/metabase/qb/components/actions/GenerateDashboardAction.jsx b/frontend/src/metabase/qb/components/actions/GenerateDashboardAction.jsx deleted file mode 100644 index b7ff628478a4a..0000000000000 --- a/frontend/src/metabase/qb/components/actions/GenerateDashboardAction.jsx +++ /dev/null @@ -1,40 +0,0 @@ -/* @flow */ - -import type { - ClickAction, - ClickActionProps, -} from "metabase/meta/types/Visualization"; -import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery"; -import { utf8_to_b64url } from "metabase/lib/card"; -import { t } from "c-3po"; - -export default ({ question, settings }: ClickActionProps): ClickAction[] => { - console.log(JSON.stringify(question.query().datasetQuery())); - let dashboard_url = "adhoc"; - - const query = question.query(); - if (!(query instanceof StructuredQuery)) { - return []; - } - - // aggregations - if (query.aggregations().length) { - return []; - } - if (question.card().id) { - dashboard_url = `/auto/dashboard/question/${question.card().id}`; - } else { - let encodedQueryDict = utf8_to_b64url( - JSON.stringify(question.query().datasetQuery()), - ); - dashboard_url = `/auto/dashboard/adhoc/${encodedQueryDict}`; - } - return [ - { - name: "generate-dashboard", - title: t`See an exploration of this question`, - icon: "bolt", - url: () => dashboard_url, - }, - ]; -}; diff --git a/frontend/src/metabase/query_builder/components/ActionsWidget.jsx b/frontend/src/metabase/query_builder/components/ActionsWidget.jsx index daec2564860d0..12215a83a2683 100644 --- a/frontend/src/metabase/query_builder/components/ActionsWidget.jsx +++ b/frontend/src/metabase/query_builder/components/ActionsWidget.jsx @@ -190,7 +190,7 @@ export default class ActionsWidget extends Component { > {PopoverComponent ? (
      -
      +
      (
      this.handleActionClick(index)} > {action.icon && ( diff --git a/frontend/src/metabase/query_builder/components/AddClauseButton.jsx b/frontend/src/metabase/query_builder/components/AddClauseButton.jsx index 866a2e4e2fc6b..fac84a957e902 100644 --- a/frontend/src/metabase/query_builder/components/AddClauseButton.jsx +++ b/frontend/src/metabase/query_builder/components/AddClauseButton.jsx @@ -22,7 +22,7 @@ export default class AddClauseButton extends Component { const { text, onClick } = this.props; const className = - "text-grey-2 text-bold flex align-center text-grey-4-hover cursor-pointer no-decoration transition-color"; + "text-light text-bold flex align-center text-medium-hover cursor-pointer no-decoration transition-color"; if (onClick) { return ( diff --git a/frontend/src/metabase/query_builder/components/AggregationPopover.jsx b/frontend/src/metabase/query_builder/components/AggregationPopover.jsx index 996e17c994391..6cf7e2e07d2a1 100644 --- a/frontend/src/metabase/query_builder/components/AggregationPopover.jsx +++ b/frontend/src/metabase/query_builder/components/AggregationPopover.jsx @@ -234,7 +234,7 @@ export default class AggregationPopover extends Component { if (editingAggregation) { return (
      -
      +
      (this._header = _)} - className="text-grey-3 p1 py2 border-bottom flex align-center" + className="text-medium p1 py2 border-bottom flex align-center" > } renderItemExtra={this.renderItemExtra.bind(this)} getItemClasses={item => - item.metric && item.metric.archived ? "text-grey-3" : null + item.metric && item.metric.archived ? "text-medium" : null } onChangeSection={index => { if (index === customExpressionIndex) { diff --git a/frontend/src/metabase/query_builder/components/AlertListPopoverContent.jsx b/frontend/src/metabase/query_builder/components/AlertListPopoverContent.jsx index 1417e6bcb8a39..59e0caff979f5 100644 --- a/frontend/src/metabase/query_builder/components/AlertListPopoverContent.jsx +++ b/frontend/src/metabase/query_builder/components/AlertListPopoverContent.jsx @@ -192,7 +192,7 @@ export class AlertListItem extends Component { return (
    1. diff --git a/frontend/src/metabase/query_builder/components/AlertModals.jsx b/frontend/src/metabase/query_builder/components/AlertModals.jsx index 5b067c24f741a..1cc0616d9c6bc 100644 --- a/frontend/src/metabase/query_builder/components/AlertModals.jsx +++ b/frontend/src/metabase/query_builder/components/AlertModals.jsx @@ -652,7 +652,7 @@ export class RawDataAlertTip extends Component { return (
      -
      +
      {showMultiSeriesGoalAlert ? ( diff --git a/frontend/src/metabase/query_builder/components/Clearable.jsx b/frontend/src/metabase/query_builder/components/Clearable.jsx index ff467956e97c2..12c205e56ad92 100644 --- a/frontend/src/metabase/query_builder/components/Clearable.jsx +++ b/frontend/src/metabase/query_builder/components/Clearable.jsx @@ -8,7 +8,7 @@ const Clearable = ({ onClear, children, className }) => ( {children} {onClear && (
      diff --git a/frontend/src/metabase/query_builder/components/DataSelector.jsx b/frontend/src/metabase/query_builder/components/DataSelector.jsx index 302c631e41372..11216c7554caa 100644 --- a/frontend/src/metabase/query_builder/components/DataSelector.jsx +++ b/frontend/src/metabase/query_builder/components/DataSelector.jsx @@ -54,7 +54,7 @@ export const SchemaAndSegmentTriggerContent = ({ ); } else { return ( - {t`Pick a segment or table`} + {t`Pick a segment or table`} ); } }; @@ -70,7 +70,7 @@ export const DatabaseTriggerContent = ({ selectedDatabase }) => selectedDatabase ? ( {selectedDatabase.name} ) : ( - {t`Select a database`} + {t`Select a database`} ); export const SchemaTableAndFieldDataSelector = props => ( @@ -85,7 +85,7 @@ export const SchemaTableAndFieldDataSelector = props => ( export const FieldTriggerContent = ({ selectedDatabase, selectedField }) => { if (!selectedField || !selectedField.table) { return ( - {t`Select...`} + {t`Select...`} ); } else { const hasMultipleSchemas = @@ -93,7 +93,7 @@ export const FieldTriggerContent = ({ selectedDatabase, selectedField }) => { _.uniq(selectedDatabase.tables, t => t.schema).length > 1; return (
      -
      +
      {hasMultipleSchemas && selectedField.table.schema + " > "} {selectedField.table.display_name}
      @@ -125,7 +125,7 @@ export const TableTriggerContent = ({ selectedTable }) => {selectedTable.display_name || selectedTable.name} ) : ( - {t`Select a table`} + {t`Select a table`} ); @connect(state => ({ metadata: getMetadata(state) }), { fetchTableMetadata }) diff --git a/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx b/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx index b307fe65f1b83..73d116f77b90c 100644 --- a/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx +++ b/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx @@ -121,7 +121,7 @@ export default class ExtendedOptions extends Component { if ((sortList && sortList.length > 0) || addSortButton) { return (
      -
      {t`Sort`}
      +
      {t`Sort`}
      {sortList} {addSortButton}
      @@ -190,7 +190,7 @@ export default class ExtendedOptions extends Component { {features.limit && (
      -
      {t`Row limit`}
      +
      {t`Row limit`}
      )} @@ -212,7 +212,7 @@ export default class ExtendedOptions extends Component { return (
      {item.dimension.tag} + {item.dimension.tag} )} {enableSubDimensions && item.dimension && diff --git a/frontend/src/metabase/query_builder/components/FieldName.jsx b/frontend/src/metabase/query_builder/components/FieldName.jsx index 7c178b68f162e..447375b202915 100644 --- a/frontend/src/metabase/query_builder/components/FieldName.jsx +++ b/frontend/src/metabase/query_builder/components/FieldName.jsx @@ -76,7 +76,7 @@ export default class FieldName extends Component { parts.push({t`Unknown Field`}); } } else { - parts.push({t`field`}); + parts.push({t`field`}); } const content = ( diff --git a/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx b/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx index 70f0e54048d20..33c30e1e74b0e 100644 --- a/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx +++ b/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx @@ -91,7 +91,7 @@ export default class GuiQueryEditor extends Component { renderAdd(text: ?string, onClick: ?() => void, targetRefName?: string) { let className = - "AddButton text-grey-2 text-bold flex align-center text-grey-4-hover cursor-pointer no-decoration transition-color"; + "AddButton text-light text-bold flex align-center text-medium-hover cursor-pointer no-decoration transition-color"; if (onClick) { return ( diff --git a/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx b/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx index f509501b973cc..0545a46c17db6 100644 --- a/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx +++ b/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx @@ -335,7 +335,7 @@ export default class NativeQueryEditor extends Component { } } else { dataSelectors = ( - {t`This question is written in ${query.nativeQueryLanguage()}.`} + {t`This question is written in ${query.nativeQueryLanguage()}.`} ); } diff --git a/frontend/src/metabase/query_builder/components/QueryHeader.jsx b/frontend/src/metabase/query_builder/components/QueryHeader.jsx index 1a29a8461cd55..0ced7080bab49 100644 --- a/frontend/src/metabase/query_builder/components/QueryHeader.jsx +++ b/frontend/src/metabase/query_builder/components/QueryHeader.jsx @@ -37,10 +37,16 @@ import { import { getUser } from "metabase/home/selectors"; import { fetchAlertsForQuestion } from "metabase/alert/alert"; +import Collections from "metabase/entities/collections"; + const mapStateToProps = (state, props) => ({ questionAlerts: getQuestionAlerts(state), visualizationSettings: getVisualizationSettings(state), user: getUser(state), + initialCollectionId: Collections.selectors.getInitialCollectionId( + state, + props, + ), }); const mapDispatchToProps = { @@ -187,7 +193,7 @@ export default class QueryHeader extends Component { form key="save" ref="saveModal" - triggerClasses="h4 text-grey-4 text-brand-hover text-uppercase" + triggerClasses="h4 text-medium text-brand-hover text-uppercase" triggerElement={t`Save`} > this.onSave(card, false)} createFn={this.onCreate} onClose={() => this.refs.saveModal && this.refs.saveModal.toggle()} + initialCollectionId={this.props.initialCollectionId} /> , ]); @@ -238,7 +245,7 @@ export default class QueryHeader extends Component { this.onSave(this.props.card, false)} - className="cursor-pointer text-brand-hover bg-white text-grey-4 text-uppercase" + className="cursor-pointer text-brand-hover bg-white text-medium text-uppercase" normalText={t`SAVE CHANGES`} activeText={t`Saving…`} failedText={t`Save failed`} @@ -250,7 +257,7 @@ export default class QueryHeader extends Component { buttonSections.push([ {t`CANCEL`} @@ -359,6 +366,7 @@ export default class QueryHeader extends Component { }} onClose={() => this.refs.addToDashSaveModal.toggle()} multiStep + initiCollectionId={this.props.initiCollectionId} /> , @@ -585,6 +593,7 @@ export default class QueryHeader extends Component { this.setState({ modal: null }) } multiStep + initiCollectionId={this.props.initiCollectionId} />
      diff --git a/frontend/src/metabase/query_builder/components/QueryModeButton.jsx b/frontend/src/metabase/query_builder/components/QueryModeButton.jsx index 315dc59bce972..b3f9d333e583f 100644 --- a/frontend/src/metabase/query_builder/components/QueryModeButton.jsx +++ b/frontend/src/metabase/query_builder/components/QueryModeButton.jsx @@ -69,7 +69,7 @@ export default class QueryModeButton extends Component { data-metabase-event={"QueryBuilder;Toggle Mode"} className={cx("cursor-pointer", { "text-brand-hover": onClick, - "text-grey-1": !onClick, + "text-light": !onClick, })} onClick={onClick} > diff --git a/frontend/src/metabase/query_builder/components/QueryVisualization.jsx b/frontend/src/metabase/query_builder/components/QueryVisualization.jsx index 23770d3d46590..2317bd7294154 100644 --- a/frontend/src/metabase/query_builder/components/QueryVisualization.jsx +++ b/frontend/src/metabase/query_builder/components/QueryVisualization.jsx @@ -182,7 +182,7 @@ export default class QueryVisualization extends Component { className="flex" items={messages} renderItem={item => ( -
      +
      {item.message}
      @@ -289,7 +289,7 @@ export default class QueryVisualization extends Component { } export const VisualizationEmptyState = ({ showTutorialLink }) => ( -
      +

      {t`If you give me some data I can show you something cool. Run a Query!`}

      {showTutorialLink && ( diff --git a/frontend/src/metabase/query_builder/components/RunButton.jsx b/frontend/src/metabase/query_builder/components/RunButton.jsx index 08409ac0cee29..2f7c0e3660a76 100644 --- a/frontend/src/metabase/query_builder/components/RunButton.jsx +++ b/frontend/src/metabase/query_builder/components/RunButton.jsx @@ -40,8 +40,8 @@ export default class RunButton extends Component { { "RunButton--hidden": !buttonText, "Button--primary": isDirty, - "text-grey-2": !isDirty, - "text-grey-4-hover": !isDirty, + "text-light": !isDirty, + "text-medium-hover": !isDirty, }, ); return ( diff --git a/frontend/src/metabase/query_builder/components/SavedQuestionIntroModal.jsx b/frontend/src/metabase/query_builder/components/SavedQuestionIntroModal.jsx index 32758c6a1a2b3..176e7b1dbb7d0 100644 --- a/frontend/src/metabase/query_builder/components/SavedQuestionIntroModal.jsx +++ b/frontend/src/metabase/query_builder/components/SavedQuestionIntroModal.jsx @@ -11,7 +11,7 @@ export default class SavedQuestionIntroModal extends Component {

      {t`It's okay to play around with saved questions`}

      -
      {t`You won't make any permanent changes to a saved question unless you click the edit icon in the top-right.`}
      +
      {t`You won't make any permanent changes to a saved question unless you click the edit icon in the top-right.`}
      diff --git a/frontend/src/metabase/query_builder/components/SelectionModule.jsx b/frontend/src/metabase/query_builder/components/SelectionModule.jsx index 78e509faac0f4..a3480a2aed029 100644 --- a/frontend/src/metabase/query_builder/components/SelectionModule.jsx +++ b/frontend/src/metabase/query_builder/components/SelectionModule.jsx @@ -244,7 +244,7 @@ export default class SelectionModule extends Component { if (this.props.remove) { remove = ( diff --git a/frontend/src/metabase/query_builder/components/dataref/DetailPane.jsx b/frontend/src/metabase/query_builder/components/dataref/DetailPane.jsx index 31d69223eddca..0244946ca5df6 100644 --- a/frontend/src/metabase/query_builder/components/dataref/DetailPane.jsx +++ b/frontend/src/metabase/query_builder/components/dataref/DetailPane.jsx @@ -13,7 +13,7 @@ const DetailPane = ({ }) => (

      {name}

      -

      +

      {description || t`No description set.`}

      {useForCurrentQuestion && useForCurrentQuestion.length > 0 ? ( diff --git a/frontend/src/metabase/query_builder/components/dataref/TablePane.jsx b/frontend/src/metabase/query_builder/components/dataref/TablePane.jsx index c2f7081d8dca5..5bcce76067233 100644 --- a/frontend/src/metabase/query_builder/components/dataref/TablePane.jsx +++ b/frontend/src/metabase/query_builder/components/dataref/TablePane.jsx @@ -121,7 +121,7 @@ export default class TablePane extends Component { > {fk.origin.table.display_name} {fkCountsByTable[fk.origin.table.id] > 1 ? ( - + {" "} via {fk.origin.display_name} @@ -145,7 +145,7 @@ export default class TablePane extends Component { ); } else { - const descriptionClasses = cx({ "text-grey-3": !table.description }); + const descriptionClasses = cx({ "text-medium": !table.description }); description = (

      {table.description || t`No description set.`} diff --git a/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.jsx b/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.jsx index fcdcf6026e760..3f3a0c7ef912e 100644 --- a/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.jsx +++ b/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.jsx @@ -314,7 +314,7 @@ export default class ExpressionEditorTextfield extends Component { (i === 0 || suggestion.type !== suggestions[i - 1].type) && (

    2. {suggestion.type}
    3. @@ -356,7 +356,7 @@ export default class ExpressionEditorTextfield extends Component {
    4. this.onShowMoreMouseDown(e)} - className="px2 text-italic text-grey-3 cursor-pointer text-brand-hover" + className="px2 text-italic text-medium cursor-pointer text-brand-hover" > and {suggestions.length - MAX_SUGGESTIONS} more
    5. diff --git a/frontend/src/metabase/query_builder/components/expressions/ExpressionWidget.jsx b/frontend/src/metabase/query_builder/components/expressions/ExpressionWidget.jsx index 567a0b8634ffc..9a0a9d381ffa0 100644 --- a/frontend/src/metabase/query_builder/components/expressions/ExpressionWidget.jsx +++ b/frontend/src/metabase/query_builder/components/expressions/ExpressionWidget.jsx @@ -43,7 +43,7 @@ export default class ExpressionWidget extends Component { return (
      -
      {t`Field formula`}
      +
      {t`Field formula`}
      -
      {t`Give it a name`}
      +
      {t`Give it a name`}
      -
      +
      Custom fields
      @@ -51,7 +51,7 @@ export default class Expressions extends Component {
      onAddExpression()} > diff --git a/frontend/src/metabase/query_builder/components/filters/FilterList.jsx b/frontend/src/metabase/query_builder/components/filters/FilterList.jsx deleted file mode 100644 index c92425f32e431..0000000000000 --- a/frontend/src/metabase/query_builder/components/filters/FilterList.jsx +++ /dev/null @@ -1,83 +0,0 @@ -/* @flow */ - -import React, { Component } from "react"; -import { findDOMNode } from "react-dom"; -import { t } from "c-3po"; -import FilterWidget from "./FilterWidget.jsx"; - -import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery"; -import type { Filter } from "metabase/meta/types/Query"; -import Dimension from "metabase-lib/lib/Dimension"; - -import type { TableMetadata } from "metabase/meta/types/Metadata"; - -type Props = { - query: StructuredQuery, - filters: Array, - removeFilter?: (index: number) => void, - updateFilter?: (index: number, filter: Filter) => void, - maxDisplayValues?: number, - tableMetadata?: TableMetadata, // legacy parameter -}; - -type State = { - shouldScroll: boolean, -}; - -export default class FilterList extends Component { - props: Props; - state: State; - - constructor(props: Props) { - super(props); - this.state = { - shouldScroll: false, - }; - } - - componentDidUpdate() { - this.state.shouldScroll - ? (findDOMNode(this).scrollLeft = findDOMNode(this).scrollWidth) - : null; - } - - componentWillReceiveProps(nextProps: Props) { - // only scroll when a filter is added - if (nextProps.filters.length > this.props.filters.length) { - this.setState({ shouldScroll: true }); - } else { - this.setState({ shouldScroll: false }); - } - } - - componentDidMount() { - this.componentDidUpdate(); - } - - render() { - const { query, filters, tableMetadata } = this.props; - return ( -
      - {filters.map((filter, index) => ( - tableMetadata, - parseFieldReference: fieldRef => - Dimension.parseMBQL(fieldRef, tableMetadata), - } - } - filter={filter} - index={index} - removeFilter={this.props.removeFilter} - updateFilter={this.props.updateFilter} - maxDisplayValues={this.props.maxDisplayValues} - /> - ))} -
      - ); - } -} diff --git a/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx b/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx index 83bdbfd1cd452..fd545ddeaf023 100644 --- a/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx +++ b/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx @@ -331,7 +331,7 @@ export default class FilterPopover extends Component { maxWidth: dimension.field().isDate() ? null : 500, }} > -
      +
      {onClear && ( diff --git a/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx b/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx index cc4b2455f0990..2f8a670662fe3 100644 --- a/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx +++ b/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx @@ -125,7 +125,7 @@ export default class TagEditorParam extends Component { this.setParameterAttribute("display_name", e.target.value) } @@ -216,7 +216,7 @@ export default class TagEditorParam extends Component { }} value={tag.default} setValue={value => this.setParameterAttribute("default", value)} - className="AdminSelect p1 text-bold text-grey-4 bordered border-med rounded bg-white" + className="AdminSelect p1 text-bold text-medium bordered border-med rounded bg-white" isEditing commitImmediately /> diff --git a/frontend/src/metabase/query_builder/containers/ArchiveQuestionModal.jsx b/frontend/src/metabase/query_builder/containers/ArchiveQuestionModal.jsx index 668ee6a45c833..6f5f69418a3a2 100644 --- a/frontend/src/metabase/query_builder/containers/ArchiveQuestionModal.jsx +++ b/frontend/src/metabase/query_builder/containers/ArchiveQuestionModal.jsx @@ -53,7 +53,8 @@ class ArchiveQuestionModal extends Component { >{t`Archive`}, ]} > -
      {t`This question will be removed from any dashboards or pulses using it.`}
      +
      {t`This question will be removed from any dashboards or pulses using it.`}
      ); } diff --git a/frontend/src/metabase/questions/components/CollectionActions.jsx b/frontend/src/metabase/questions/components/CollectionActions.jsx deleted file mode 100644 index 0a05aff400a3b..0000000000000 --- a/frontend/src/metabase/questions/components/CollectionActions.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; - -const CollectionActions = ({ children }) => ( -
      { - e.stopPropagation(); - e.preventDefault(); - }} - > - {React.Children.map(children, (child, index) => ( -
      - {child} -
      - ))} -
      -); - -export default CollectionActions; diff --git a/frontend/src/metabase/questions/components/CollectionButtons.jsx b/frontend/src/metabase/questions/components/CollectionButtons.jsx deleted file mode 100644 index d826ef70bdf9a..0000000000000 --- a/frontend/src/metabase/questions/components/CollectionButtons.jsx +++ /dev/null @@ -1,52 +0,0 @@ -import React from "react"; -import { Flex } from "grid-styled"; -import { Link } from "react-router"; -import { t } from "c-3po"; - -import Icon from "metabase/components/Icon"; -import colors from "metabase/lib/colors"; - -const COLLECTION_ICON_SIZE = 18; - -const CollectionButtons = ({ collections, isAdmin, push }) => ( -
        - {collections - .map(collection => ( - - )) - .concat(isAdmin ? [] : []) - .map((element, index) =>
      1. {element}
      2. )} -
      -); - -const CollectionLink = ({ name, slug }) => { - return ( - - - -

      {name}

      -
      - - ); -}; - -const NewCollectionButton = ({ push }) => ( -
      push(`/collections/create`)}> -
      -
      - -
      -
      -

      {t`New collection`}

      -
      -); - -export default CollectionButtons; diff --git a/frontend/src/metabase/questions/components/ExpandingSearchField.jsx b/frontend/src/metabase/questions/components/ExpandingSearchField.jsx deleted file mode 100644 index 56c989db6b56d..0000000000000 --- a/frontend/src/metabase/questions/components/ExpandingSearchField.jsx +++ /dev/null @@ -1,105 +0,0 @@ -/* eslint "react/prop-types": "warn" */ - -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import ReactDOM from "react-dom"; -import { t } from "c-3po"; -import cx from "classnames"; -import { Motion, spring } from "react-motion"; - -import Icon from "metabase/components/Icon"; - -import { - KEYCODE_FORWARD_SLASH, - KEYCODE_ENTER, - KEYCODE_ESCAPE, -} from "metabase/lib/keyboard"; - -export default class ExpandingSearchField extends Component { - constructor(props, context) { - super(props, context); - this.state = { - active: false, - }; - } - - static propTypes = { - onSearch: PropTypes.func.isRequired, - className: PropTypes.string, - defaultValue: PropTypes.string, - }; - - componentDidMount() { - this.listenToSearchKeyDown(); - } - - componentWillUnMount() { - this.stopListenToSearchKeyDown(); - } - - handleSearchKeydown = e => { - if (!this.state.active && e.keyCode === KEYCODE_FORWARD_SLASH) { - this.setActive(); - e.preventDefault(); - } - }; - - onKeyPress = e => { - if (e.keyCode === KEYCODE_ENTER) { - this.props.onSearch(e.target.value); - } else if (e.keyCode === KEYCODE_ESCAPE) { - this.setInactive(); - } - }; - - setActive = () => { - ReactDOM.findDOMNode(this.searchInput).focus(); - }; - - setInactive = () => { - ReactDOM.findDOMNode(this.searchInput).blur(); - }; - - listenToSearchKeyDown() { - window.addEventListener("keydown", this.handleSearchKeydown); - } - - stopListenToSearchKeyDown() { - window.removeEventListener("keydown", this.handleSearchKeydown); - } - - render() { - const { className } = this.props; - const { active } = this.state; - return ( -
      - - - {interpolatingStyle => ( - (this.searchInput = search)} - className="input borderless text-bold" - placeholder={t`Search for a question`} - style={Object.assign({}, interpolatingStyle, { fontSize: "1em" })} - onFocus={() => this.setState({ active: true })} - onBlur={() => this.setState({ active: false })} - onKeyUp={this.onKeyPress} - defaultValue={this.props.defaultValue} - /> - )} - -
      - ); - } -} diff --git a/frontend/src/metabase/reference/Reference.css b/frontend/src/metabase/reference/Reference.css index b5b0e57b89fcf..6001693df1492 100644 --- a/frontend/src/metabase/reference/Reference.css +++ b/frontend/src/metabase/reference/Reference.css @@ -31,7 +31,7 @@ } :local(.schemaSeparator) { - composes: text-grey-2 mt2 from "style"; + composes: text-light mt2 from "style"; margin-left: var(--icon-width); font-size: 18px; } diff --git a/frontend/src/metabase/reference/components/GuideDetail.jsx b/frontend/src/metabase/reference/components/GuideDetail.jsx index dc032bc3ec128..cc51d2c471872 100644 --- a/frontend/src/metabase/reference/components/GuideDetail.jsx +++ b/frontend/src/metabase/reference/components/GuideDetail.jsx @@ -150,13 +150,13 @@ const ItemTitle = ({ title, link, linkColorClass, linkHoverClass }) => ( ); const ContextHeading = ({ children }) => ( -

      {children}

      +

      {children}

      ); const ContextContent = ({ empty, children }) => (

      {children} diff --git a/frontend/src/metabase/reference/components/GuideDetailEditor.jsx b/frontend/src/metabase/reference/components/GuideDetailEditor.jsx index f6daefd5c3f17..daf53b92524cb 100644 --- a/frontend/src/metabase/reference/components/GuideDetailEditor.jsx +++ b/frontend/src/metabase/reference/components/GuideDetailEditor.jsx @@ -142,7 +142,7 @@ const GuideDetailEditor = ({ /> )}

      -
      +
      diff --git a/frontend/src/metabase/reference/components/GuideEditSection.css b/frontend/src/metabase/reference/components/GuideEditSection.css index 62c0d835be74e..8043cdcbe747c 100644 --- a/frontend/src/metabase/reference/components/GuideEditSection.css +++ b/frontend/src/metabase/reference/components/GuideEditSection.css @@ -4,7 +4,7 @@ } :local(.guideEditSectionDisabled) { - composes: text-grey-3 from "style"; + composes: text-medium from "style"; } :local(.guideEditSectionCollapsedIcon) { diff --git a/frontend/src/metabase/reference/components/ReferenceHeader.css b/frontend/src/metabase/reference/components/ReferenceHeader.css index e6ae9651d1ab2..c3173e09efd76 100644 --- a/frontend/src/metabase/reference/components/ReferenceHeader.css +++ b/frontend/src/metabase/reference/components/ReferenceHeader.css @@ -39,7 +39,7 @@ } :local(.headerSchema) { - composes: text-grey-2 absolute from "style"; + composes: text-light absolute from "style"; left: var(--icon-width); top: -10px; font-size: 12px; diff --git a/frontend/src/metabase/selectors/user.js b/frontend/src/metabase/selectors/user.js index b515503e99cc1..6fa15c0a161c3 100644 --- a/frontend/src/metabase/selectors/user.js +++ b/frontend/src/metabase/selectors/user.js @@ -1,4 +1,19 @@ +import { createSelector } from "reselect"; + export const getUser = state => state.currentUser; -export const getUserIsAdmin = state => - (getUser(state) || {}).is_superuser || false; +export const getUserIsAdmin = createSelector( + [getUser], + user => (user && user.is_superuser) || false, +); + +export const getUserPersonalCollectionId = createSelector( + [getUser], + user => (user && user.personal_collection_id) || null, +); + +export const getUserDefaultCollectionId = createSelector( + [getUser, getUserIsAdmin, getUserPersonalCollectionId], + (user, isAdmin, personalCollectionId) => + isAdmin ? null : personalCollectionId, +); diff --git a/frontend/src/metabase/setup/containers/PostSetupApp.jsx b/frontend/src/metabase/setup/containers/PostSetupApp.jsx index 742d1e694ba5a..e3d78fb91de46 100644 --- a/frontend/src/metabase/setup/containers/PostSetupApp.jsx +++ b/frontend/src/metabase/setup/containers/PostSetupApp.jsx @@ -93,7 +93,7 @@ export default class PostSetupApp extends Component {
      {t`I'm done exploring for now`} diff --git a/frontend/src/metabase/tutorial/Portal.jsx b/frontend/src/metabase/tutorial/Portal.jsx index 031bdaf89a3bd..cf3255a2660cf 100644 --- a/frontend/src/metabase/tutorial/Portal.jsx +++ b/frontend/src/metabase/tutorial/Portal.jsx @@ -70,7 +70,7 @@ export default class Portal extends Component { return { position: "absolute", boxSizing: "content-box", - border: `10000px solid ${colors["accent2"]}`, + border: `10000px solid ${colors["text-dark"]}`, boxShadow: `inset 0px 0px 8px ${colors["shadow"]}`, transform: "translate(-10000px, -10000px)", borderRadius: "10010px", diff --git a/frontend/src/metabase/tutorial/TutorialModal.jsx b/frontend/src/metabase/tutorial/TutorialModal.jsx index f0f08f8633545..3c0c8496c8aff 100644 --- a/frontend/src/metabase/tutorial/TutorialModal.jsx +++ b/frontend/src/metabase/tutorial/TutorialModal.jsx @@ -13,7 +13,7 @@ export default class TutorialModal extends Component {
      @@ -23,14 +23,14 @@ export default class TutorialModal extends Component {
      {showBackButton && ( back )} {showStepCount && ( - + {modalStepIndex + 1} {t`of`} {modalStepCount} )} diff --git a/frontend/src/metabase/user/components/UpdateUserDetails.jsx b/frontend/src/metabase/user/components/UpdateUserDetails.jsx index d398fa9393fc9..0f1e3e0f5872a 100644 --- a/frontend/src/metabase/user/components/UpdateUserDetails.jsx +++ b/frontend/src/metabase/user/components/UpdateUserDetails.jsx @@ -144,7 +144,7 @@ export default class UpdateUserDetails extends Component { ref="email" className={cx("Form-offset full", { "Form-input": !managed, - "text-grey-2 h1 borderless mt1": managed, + "text-light h1 borderless mt1": managed, })} name="email" defaultValue={user ? user.email : null} diff --git a/frontend/src/metabase/user/components/UserSettings.jsx b/frontend/src/metabase/user/components/UserSettings.jsx index 443b02fb749e6..68d49b524e6e8 100644 --- a/frontend/src/metabase/user/components/UserSettings.jsx +++ b/frontend/src/metabase/user/components/UserSettings.jsx @@ -51,7 +51,7 @@ export default class UserSettings extends Component {
      -

      {t`Account settings`}

      +

      {t`Account settings`}

      diff --git a/frontend/src/metabase/visualizations/components/ChartClickActions.jsx b/frontend/src/metabase/visualizations/components/ChartClickActions.jsx index 70c6475b5e792..af152826f283a 100644 --- a/frontend/src/metabase/visualizations/components/ChartClickActions.jsx +++ b/frontend/src/metabase/visualizations/components/ChartClickActions.jsx @@ -164,7 +164,7 @@ export default class ChartClickActions extends Component { {popover ? ( popover ) : ( -
      +
      {sections.map(([key, actions]) => (
      0 && ( onRemoveSeries(s.card)} diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldPicker.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldPicker.jsx index f503df8cd92fe..d1601c329e584 100644 --- a/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldPicker.jsx +++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldPicker.jsx @@ -17,7 +17,7 @@ const ChartSettingFieldPicker = ({ value, options, onChange, onRemove }) => ( /> ) : ( -
      +
      {t`Add fields from the list below`}
      )} @@ -140,7 +140,7 @@ export default class ChartSettingOrderedColumns extends Component { ))} {additionalFieldOptions.fks.map(fk => (
      -
      +
      {fk.field.target.table.display_name}
      {fk.dimensions.map((dimension, index) => ( @@ -170,7 +170,7 @@ const ColumnItem = ({ title, onAdd, onRemove }) => ( {onAdd && ( { e.stopPropagation(); onAdd(); @@ -180,7 +180,7 @@ const ColumnItem = ({ title, onAdd, onRemove }) => ( {onRemove && ( { e.stopPropagation(); onRemove(); diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingsTableFormatting.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingsTableFormatting.jsx index 5fc6542547384..f1f5829d9b9a2 100644 --- a/frontend/src/metabase/visualizations/components/settings/ChartSettingsTableFormatting.jsx +++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingsTableFormatting.jsx @@ -206,7 +206,7 @@ const RulePreview = ({ rule, cols, onClick, onRemove }) => ( { e.stopPropagation(); onRemove(); diff --git a/frontend/src/metabase/visualizations/visualizations/ObjectDetail.jsx b/frontend/src/metabase/visualizations/visualizations/ObjectDetail.jsx index 2daa03af2f7f6..e62b13759d791 100644 --- a/frontend/src/metabase/visualizations/visualizations/ObjectDetail.jsx +++ b/frontend/src/metabase/visualizations/visualizations/ObjectDetail.jsx @@ -97,7 +97,7 @@ export class ObjectDetail extends Component { isLink = false; } else { if (value === null || value === undefined || value === "") { - cellValue = {t`Empty`}; + cellValue = {t`Empty`}; } else if (isa(column.special_type, TYPE.SerializedJSON)) { let formattedJson = JSON.stringify(JSON.parse(value), null, 2); cellValue =
      {formattedJson}
      ; @@ -208,7 +208,7 @@ export class ObjectDetail extends Component { ); const via = fkCountsByTable[fk.origin.table.id] > 1 ? ( - + {" "} {t`via ${fk.origin.display_name}`} @@ -226,7 +226,7 @@ export class ObjectDetail extends Component { let fkReference; const referenceClasses = cx("flex align-center my2 pb2 border-bottom", { "text-brand-hover cursor-pointer text-dark": fkClickable, - "text-grey-3": !fkClickable, + "text-medium": !fkClickable, }); if (fkClickable) { @@ -284,7 +284,7 @@ export class ObjectDetail extends Component {
      -
      +
      {jt`This ${( diff --git a/frontend/src/metabase/visualizations/visualizations/Progress.jsx b/frontend/src/metabase/visualizations/visualizations/Progress.jsx index ee0a439cd4144..759ffe27a61fd 100644 --- a/frontend/src/metabase/visualizations/visualizations/Progress.jsx +++ b/frontend/src/metabase/visualizations/visualizations/Progress.jsx @@ -152,7 +152,7 @@ export default class Progress extends Component { >
      diff --git a/frontend/src/metabase/visualizations/visualizations/Text.jsx b/frontend/src/metabase/visualizations/visualizations/Text.jsx index d465dfbcae98d..f338d848f5033 100644 --- a/frontend/src/metabase/visualizations/visualizations/Text.jsx +++ b/frontend/src/metabase/visualizations/visualizations/Text.jsx @@ -212,7 +212,7 @@ const TextActionButtons = ({ cleanup.actions.push(action); cleanup.metric = metric => cleanup.fn(() => deleteMetric(metric)); cleanup.segment = segment => cleanup.fn(() => deleteSegment(segment)); cleanup.question = question => cleanup.fn(() => deleteQuestion(question)); +cleanup.collection = c => cleanup.fn(() => deleteCollection(c)); export const deleteQuestion = question => CardApi.delete({ cardId: getId(question) }); @@ -625,6 +626,8 @@ export const deleteSegment = segment => SegmentApi.delete({ segmentId: getId(segment), revision_message: "Please" }); export const deleteMetric = metric => MetricApi.delete({ metricId: getId(metric), revision_message: "Please" }); +export const deleteCollection = collection => + CollectionsApi.update({ id: getId(collection), archived: true }); const getId = o => typeof o === "object" && o != null @@ -668,6 +671,14 @@ api._makeRequest = async (method, url, headers, requestBody, data, options) => { ? { status: 0, responseText: "" } : await fetch(api.basename + url, fetchOptions); + if (!window.document) { + console.warn( + "API request completed after test ended. Ignoring result.", + url, + ); + return; + } + if (isCancelled) { throw { status: 0, data: "", isCancelled: true }; } @@ -710,6 +721,16 @@ api._makeRequest = async (method, url, headers, requestBody, data, options) => { throw error; } + } catch (e) { + if (!window.document) { + console.warn( + "API request failed after test ended. Ignoring result.", + url, + e, + ); + return; + } + throw e; } finally { pendingRequests--; if (pendingRequests === 0 && pendingRequestsDeferred) { diff --git a/frontend/test/collection/initial_collection.integ.spec.js b/frontend/test/collection/initial_collection.integ.spec.js new file mode 100644 index 0000000000000..b296b7a459215 --- /dev/null +++ b/frontend/test/collection/initial_collection.integ.spec.js @@ -0,0 +1,95 @@ +import { + createTestStore, + createAllUsersWritableCollection, + useSharedAdminLogin, + useSharedNormalLogin, + eventually, + cleanup, +} from "__support__/integrated_tests"; + +import { mount } from "enzyme"; + +const ROOT_COLLECTION_NAME = "Our analytics"; +const NORMAL_USER_COLLECTION_NAME = "Robert Charts's Personal Collection"; + +describe("initial collection id", () => { + let collection; + let app, store; + + beforeAll(async () => { + useSharedAdminLogin(); + collection = await createAllUsersWritableCollection(); + cleanup.collection(collection); + }); + afterAll(cleanup); + + describe("for admins", () => { + beforeEach(async () => { + useSharedAdminLogin(); + store = await createTestStore(); + app = mount(store.getAppContainer()); + }); + + describe("a new collection", () => { + it("should be the parent collection", async () => { + store.pushPath(`/collection/${collection.id}/new_collection`); + await assertInitialCollection(app, collection.name); + }); + }); + describe("a new pulse", () => { + it("should be the root collection", async () => { + store.pushPath("/pulse/create"); + await assertInitialCollection(app, ROOT_COLLECTION_NAME); + }); + }); + describe("a new dashboard", () => { + it("should be the root collection", async () => { + store.pushPath("/"); + await clickNewDashboard(app); + await assertInitialCollection(app, ROOT_COLLECTION_NAME); + }); + }); + }); + + describe("for non-admins", () => { + beforeEach(async () => { + useSharedNormalLogin(); + store = await createTestStore(); + app = mount(store.getAppContainer()); + }); + + describe("a new pulse", () => { + it("should be the personal collection", async () => { + store.pushPath("/pulse/create"); + await assertInitialCollection(app, NORMAL_USER_COLLECTION_NAME); + }); + }); + describe("a new dashboard", () => { + it("should be the personal collection", async () => { + store.pushPath("/"); + await clickNewDashboard(app); + await assertInitialCollection(app, NORMAL_USER_COLLECTION_NAME); + }); + }); + }); +}); + +const clickNewDashboard = app => + eventually(() => { + app + .find("Navbar") + .find("EntityMenu") + .first() + .props() + .items[0].action(); + }); + +const assertInitialCollection = (app, collectionName) => + eventually(() => { + expect( + app + .find(".AdminSelect") + .first() + .text(), + ).toBe(collectionName); + }); diff --git a/frontend/test/dashboards/dashboards.integ.spec.js b/frontend/test/dashboards/dashboards.integ.spec.js index ca7e2e0685c6a..c50fea4cfae93 100644 --- a/frontend/test/dashboards/dashboards.integ.spec.js +++ b/frontend/test/dashboards/dashboards.integ.spec.js @@ -14,7 +14,6 @@ import { DashboardApi } from "metabase/services"; import SearchHeader from "metabase/components/SearchHeader"; import EmptyState from "metabase/components/EmptyState"; import Dashboard from "metabase/dashboard/components/Dashboard"; -import ListFilterWidget from "metabase/components/ListFilterWidget"; import ArchivedItem from "metabase/components/ArchivedItem"; /* @@ -121,7 +120,6 @@ xdescribe("dashboards list", () => { .find(".Icon-staroutline"), ); await store.waitForActions([Dashboards.actionTypes.UPDATE]); - click(app.find(ListFilterWidget)); click(app.find(".TestPopover").find('h4[children="Favorites"]')); diff --git a/frontend/test/lib/utils.unit.spec.js b/frontend/test/lib/utils.unit.spec.js index 987a2eb7ebef5..a1fe21415eeae 100644 --- a/frontend/test/lib/utils.unit.spec.js +++ b/frontend/test/lib/utils.unit.spec.js @@ -3,9 +3,14 @@ import MetabaseSettings from "metabase/lib/settings"; describe("utils", () => { describe("generatePassword", () => { - it("defaults to complexity requirements from Settings", () => { + it("defaults to at least 14 characters even if password_complexity requirements are lower", () => { MetabaseSettings.set("password_complexity", { total: 10 }); - expect(MetabaseUtils.generatePassword().length).toBe(10); + expect(MetabaseUtils.generatePassword().length).toBe(14); + }); + + it("defaults to complexity requirements if greater than 14", () => { + MetabaseSettings.set("password_complexity", { total: 20 }); + expect(MetabaseUtils.generatePassword().length).toBe(20); }); it("falls back to length 14 passwords", () => { diff --git a/package.json b/package.json index 6c99a31bd21db..d9a454add342f 100644 --- a/package.json +++ b/package.json @@ -190,7 +190,11 @@ "echo $npm_execpath | grep -q yarn || echo '\\033[0;33mSorry, npm is not supported. Please use Yarn (https://yarnpkg.com/).\\033[0m'", "prettier": "prettier --write 'frontend/**/*.{js,jsx,css}'", "docs": - "documentation build -f html -o frontend/docs frontend/src/metabase-lib/lib/**" + "documentation build -f html -o frontend/docs frontend/src/metabase-lib/lib/**", + "ci": "yarn ci-frontend && yarn ci-backend", + "ci-frontend": "yarn lint && yarn flow && yarn test", + "ci-backend": + "lein docstring-checker && lein bikeshed && lein eastwood && lein test" }, "lint-staged": { "frontend/**/*.{js,jsx,css}": ["prettier --write", "git add"] diff --git a/project.clj b/project.clj index 55620b3cbf128..36eeef8686113 100644 --- a/project.clj +++ b/project.clj @@ -20,13 +20,14 @@ [org.clojure/java.jdbc "0.7.6"] ; basic JDBC access from Clojure [org.clojure/math.combinatorics "0.1.4"] ; combinatorics functions [org.clojure/math.numeric-tower "0.0.4"] ; math functions like `ceil` - [org.clojure/tools.logging "0.3.1"] ; logging framework + [org.clojure/tools.logging "0.4.1"] ; logging framework [org.clojure/tools.namespace "0.2.10"] [amalloy/ring-buffer "1.2.1" :exclusions [org.clojure/clojure org.clojure/clojurescript]] ; fixed length queue implementation, used in log buffering [amalloy/ring-gzip-middleware "0.1.3"] ; Ring middleware to GZIP responses if client can handle it - [aleph "0.4.5-alpha2"] ; Async HTTP library; WebSockets + [aleph "0.4.5-alpha2" ; Async HTTP library; WebSockets + :exclusions [org.clojure/tools.logging]] [buddy/buddy-core "1.2.0"] ; various cryptograhpic functions [buddy/buddy-sign "1.5.0"] ; JSON Web Tokens; High-Level message signing library [cheshire "5.7.0"] ; fast JSON encoding (used by Ring JSON middleware) @@ -83,7 +84,8 @@ [net.sf.cssbox/cssbox "4.12" ; HTML / CSS rendering :exclusions [org.slf4j/slf4j-api]] [org.clojars.pntblnk/clj-ldap "0.0.12"] ; LDAP client - [org.liquibase/liquibase-core "3.6.2"] ; migration management (Java lib) + [org.liquibase/liquibase-core "3.6.2" ; migration management (Java lib) + :exclusions [ch.qos.logback/logback-classic]] [org.postgresql/postgresql "42.2.2"] ; Postgres driver [org.slf4j/slf4j-log4j12 "1.7.25"] ; abstraction for logging frameworks -- allows end user to plug in desired logging framework at deployment time [org.tcrawley/dynapath "0.2.5"] ; Dynamically add Jars (e.g. Oracle or Vertica) to classpath @@ -134,7 +136,7 @@ :docstring-checker {:include [#"^metabase"] :exclude [#"test" #"^metabase\.http-client$"]} - :profiles {:dev {:dependencies [[expectations "2.2.0-beta2"] ; unit tests + :profiles {:dev {:dependencies [[expectations "2.2.0-beta2"] ; unit tests [ring/ring-mock "0.3.0"]] ; Library to create mock Ring requests for unit tests :plugins [[docstring-checker "1.0.2"] ; Check that all public vars have docstrings. Run with 'lein docstring-checker' [jonase/eastwood "0.2.6" diff --git a/resources/frontend_client/app/img/collection-empty-state.png b/resources/frontend_client/app/img/collection-empty-state.png index b712cc40eb54b..41a19963a763e 100644 Binary files a/resources/frontend_client/app/img/collection-empty-state.png and b/resources/frontend_client/app/img/collection-empty-state.png differ diff --git a/resources/frontend_client/app/img/collection-empty-state.svg b/resources/frontend_client/app/img/collection-empty-state.svg index 209f77a3efedf..5982fd752703a 100644 --- a/resources/frontend_client/app/img/collection-empty-state.svg +++ b/resources/frontend_client/app/img/collection-empty-state.svg @@ -1,13 +1,12 @@ - - + + Artboard Created with Sketch. - - + @@ -27,9 +26,9 @@ - - - + + + \ No newline at end of file diff --git a/resources/frontend_client/app/img/collection-empty-state@2x.png b/resources/frontend_client/app/img/collection-empty-state@2x.png index 0e98b3713def0..3ef89ea1cf605 100644 Binary files a/resources/frontend_client/app/img/collection-empty-state@2x.png and b/resources/frontend_client/app/img/collection-empty-state@2x.png differ diff --git a/resources/frontend_client/app/img/empty.png b/resources/frontend_client/app/img/empty.png new file mode 100644 index 0000000000000..bf66147252b06 Binary files /dev/null and b/resources/frontend_client/app/img/empty.png differ diff --git a/resources/frontend_client/app/img/empty@2x.png b/resources/frontend_client/app/img/empty@2x.png new file mode 100644 index 0000000000000..98d2a85018f77 Binary files /dev/null and b/resources/frontend_client/app/img/empty@2x.png differ diff --git a/src/metabase/api/collection.clj b/src/metabase/api/collection.clj index f6b19eb527a4c..494094451c06e 100644 --- a/src/metabase/api/collection.clj +++ b/src/metabase/api/collection.clj @@ -20,6 +20,8 @@ [db :as db] [hydrate :refer [hydrate]]])) +(declare root-collection) + (api/defendpoint GET "/" "Fetch a list of all Collections that the current user has read permissions for (`:can_write` is returned as an additional property of each Collection so you can tell which of these you have write permissions for.) @@ -28,10 +30,18 @@ `?archived=true`." [archived] {archived (s/maybe su/BooleanString)} - (as-> (db/select Collection :archived (Boolean/parseBoolean archived) - {:order-by [[:%lower.name :asc]]}) collections - (filter mi/can-read? collections) - (hydrate collections :can_write))) + (let [archived? (Boolean/parseBoolean archived)] + (as-> (db/select Collection :archived archived? + {:order-by [[:%lower.name :asc]]}) collections + (filter mi/can-read? collections) + ;; include Root Collection at beginning or results if archived isn't `true` + (if archived? + collections + (cons (root-collection) collections)) + (hydrate collections :can_write) + ;; remove the :metabase.models.collection/is-root? tag since FE doesn't need it + (for [collection collections] + (dissoc collection ::collection/is-root?))))) ;;; --------------------------------- Fetching a single Collection & its 'children' ---------------------------------- @@ -121,18 +131,20 @@ {:model (keyword model) :archived? (Boolean/parseBoolean archived)})) + ;;; -------------------------------------------- GET /api/collection/root -------------------------------------------- +(defn- root-collection [] + ;; add in some things for the FE to display since the 'Root' Collection isn't real and wouldn't normally have + ;; these things + (assoc (collection-detail collection/root-collection) + :name (tru "Our analytics") + :id "root")) + (api/defendpoint GET "/root" "Return the 'Root' Collection object with standard details added" [] - (-> (collection-detail collection/root-collection) - ;; add in some things for the FE to display since the 'Root' Collection isn't real and wouldn't normally have - ;; these things - (assoc - :name (tru "Our analytics") - :id "root") - (dissoc ::collection/is-root?))) + (dissoc (root-collection) ::collection/is-root?)) (api/defendpoint GET "/root/items" "Fetch objects that the current user should see at their root level. As mentioned elsewhere, the 'Root' Collection diff --git a/src/metabase/query_processor/middleware/parameters/sql.clj b/src/metabase/query_processor/middleware/parameters/sql.clj index bc61f4c701bfe..bfcc36768c311 100644 --- a/src/metabase/query_processor/middleware/parameters/sql.clj +++ b/src/metabase/query_processor/middleware/parameters/sql.clj @@ -7,8 +7,8 @@ [honeysql.core :as hsql] [medley.core :as m] [metabase.driver :as driver] - [metabase.models.field :as field :refer [Field]] [metabase.driver.generic-sql :as sql] + [metabase.models.field :as field :refer [Field]] [metabase.query-processor.middleware.expand :as ql] [metabase.query-processor.middleware.parameters.dates :as date-params] [metabase.util @@ -21,7 +21,7 @@ honeysql.types.SqlCall java.text.NumberFormat java.util.regex.Pattern - java.util.TimeZone + java.util.UUID metabase.models.field.FieldInstance)) ;; The Basics: @@ -68,6 +68,9 @@ (defn- no-value? [x] (instance? NoValue x)) +(def ^:private ParamType + (s/enum "number" "dimension" "text" "date")) + ;; various schemas are used to check that various functions return things in expected formats ;; TAGS in this case are simple params like {{x}} that get replaced with a single value ("ABC" or 1) as opposed to a @@ -80,7 +83,7 @@ {(s/optional-key :id) su/NonBlankString ; this is used internally by the frontend :name su/NonBlankString :display_name su/NonBlankString - :type (s/enum "number" "dimension" "text" "date") + :type ParamType (s/optional-key :dimension) [s/Any] (s/optional-key :widget_type) su/NonBlankString ; type of the [default] value if `:type` itself is `dimension` (s/optional-key :required) s/Bool @@ -171,8 +174,10 @@ Filter\" in the Native Query Editor." [tag :- TagParam, params :- (s/maybe [DimensionValue])] (when-let [dimension (:dimension tag)] - (map->Dimension {:field (or (db/select-one [Field :name :parent_id :table_id], :id (dimension->field-id dimension)) - (throw (Exception. (str "Can't find field with ID: " (dimension->field-id dimension))))) + (map->Dimension {:field (or (db/select-one [Field :name :parent_id :table_id :base_type], + :id (dimension->field-id dimension)) + (throw (Exception. (str (tru "Can't find field with ID: {0}" + (dimension->field-id dimension)))))) :param (or ;; look in the sequence of params we were passed to see if there's anything that matches (param-with-target params ["dimension" ["template-tag" (:name tag)]]) @@ -193,7 +198,7 @@ [{:keys [default display_name required]} :- TagParam] (or default (when required - (throw (Exception. (format "'%s' is a required param." display_name)))))) + (throw (Exception. (str (tru "''{0}'' is a required param." display_name))))))) ;;; Parsing Values @@ -228,18 +233,47 @@ ;; otherwise just return the single number (first parts))))) +(s/defn ^:private parse-value-for-field-base-type :- s/Any + "Do special parsing for value for a (presumably textual) FieldFilter 'dimension' param (i.e., attempt to parse it as + appropriate based on the base-type of the Field associated with it). These are special cases for handling types that + do not have an associated parameter type (such as `date` or `number`), such as UUID fields." + [base-type :- su/FieldType, value] + (cond + (isa? base-type :type/UUID) (UUID/fromString value) + :else value)) + (s/defn ^:private parse-value-for-type :- ParamValue - [param-type value] + "Parse a `value` based on the type chosen for the param, such as `text` or `number`. (Depending on the type of param + created, `value` here might be a raw value or a map including information about the Field it references as well as a + value.) For numbers, dates, and the like, this will parse the string appropriately; for `text` parameters, this will + additionally attempt handle special cases based on the base type of the Field, for example, parsing params for UUID + base type Fields as UUIDs." + [param-type :- ParamType, value] (cond - (no-value? value) value - (= param-type "number") (value->number value) - (= param-type "date") (map->Date {:s value}) + (no-value? value) + value + + (= param-type "number") + (value->number value) + + (= param-type "date") + (map->Date {:s value}) + + (and (= param-type "dimension") + (= (get-in value [:param :type]) "number")) + (update-in value [:param :value] value->number) + + (sequential? value) + (map->MultipleValues {:values (for [v value] + (parse-value-for-type param-type v))}) + (and (= param-type "dimension") - (= (get-in value [:param :type]) "number")) (update-in value [:param :value] value->number) - (sequential? value) (map->MultipleValues - {:values (for [v value] - (parse-value-for-type param-type v))}) - :else value)) + (get-in value [:field :base_type]) + (string? (get-in value [:param :value]))) + (update-in value [:param :value] (partial parse-value-for-field-base-type (get-in value [:field :base_type]))) + + :else + value)) (s/defn ^:private value-for-tag :- ParamValue "Given a map TAG (a value in the `:template_tags` dictionary) return the corresponding value from the PARAMS @@ -352,12 +386,12 @@ (defn- create-replacement-snippet [nil-or-obj] (let [{:keys [sql-string param-values]} (sql/->prepared-substitution *driver* nil-or-obj)] - {:replacement-snippet sql-string + {:replacement-snippet sql-string :prepared-statement-args param-values})) (defn- prepared-ts-subs [operator date-str] (let [{:keys [sql-string param-values]} (sql/->prepared-substitution *driver* (du/->Timestamp date-str))] - {:replacement-snippet (str operator " " sql-string) + {:replacement-snippet (str operator " " sql-string) :prepared-statement-args param-values})) (extend-protocol ISQLParamSubstituion @@ -367,6 +401,7 @@ Boolean (->replacement-snippet-info [this] (create-replacement-snippet this)) Keyword (->replacement-snippet-info [this] (create-replacement-snippet this)) SqlCall (->replacement-snippet-info [this] (create-replacement-snippet this)) + UUID (->replacement-snippet-info [this] {:replacement-snippet (format "CAST('%s' AS uuid)" (str this))}) NoValue (->replacement-snippet-info [_] {:replacement-snippet ""}) CommaSeparatedNumbers diff --git a/test/metabase/api/collection_test.clj b/test/metabase/api/collection_test.clj index 8614ce3faef1f..1d37635a8eb62 100644 --- a/test/metabase/api/collection_test.clj +++ b/test/metabase/api/collection_test.clj @@ -28,14 +28,16 @@ ;; check that we can get a basic list of collections ;; (for the purposes of test purposes remove the personal collections) (tt/expect-with-temp [Collection [collection]] - [(assoc (into {} collection) :can_write true)] + [{:parent_id nil, :effective_location nil, :effective_ancestors (), :can_write true, :name "Our analytics", :id "root"} + (assoc (into {} collection) :can_write true)] (for [collection ((user->client :crowberto) :get 200 "collection") :when (not (:personal_owner_id collection))] collection)) ;; We should only see our own Personal Collections! (expect - ["Lucky Pigeon's Personal Collection"] + ["Our analytics" + "Lucky Pigeon's Personal Collection"] (do (collection-test/force-create-personal-collections!) ;; now fetch those Collections as the Lucky bird @@ -43,7 +45,8 @@ ;; ...unless we are *admins* (expect - ["Crowberto Corv's Personal Collection" + ["Our analytics" + "Crowberto Corv's Personal Collection" "Lucky Pigeon's Personal Collection" "Rasta Toucan's Personal Collection" "Trash Bird's Personal Collection"] @@ -54,7 +57,8 @@ ;; check that we don't see collections if we don't have permissions for them (expect - ["Collection 1" + ["Our analytics" + "Collection 1" "Rasta Toucan's Personal Collection"] (tt/with-temp* [Collection [collection-1 {:name "Collection 1"}] Collection [collection-2 {:name "Collection 2"}]] @@ -64,7 +68,8 @@ ;; check that we don't see collections if they're archived (expect - ["Rasta Toucan's Personal Collection" + ["Our analytics" + "Rasta Toucan's Personal Collection" "Regular Collection"] (tt/with-temp* [Collection [collection-1 {:name "Archived Collection", :archived true}] Collection [collection-2 {:name "Regular Collection"}]] diff --git a/test/metabase/driver/postgres_test.clj b/test/metabase/driver/postgres_test.clj index f4f5c4ba33eb6..d8d472df653e6 100644 --- a/test/metabase/driver/postgres_test.clj +++ b/test/metabase/driver/postgres_test.clj @@ -83,7 +83,6 @@ [#uuid "7a5ce4a2-0958-46e7-9685-1a4eaa3bd08a"] [#uuid "84ed434e-80b4-41cf-9c88-e334427104ae"]]]]) - ;; Check that we can load a Postgres Database with a :type/UUID (expect-with-engine :postgres [{:name "id", :base_type :type/Integer} @@ -109,6 +108,21 @@ (data/run-query users (ql/filter (ql/= $user_id nil)))))) +;; Check that we can filter by a UUID for SQL Field filters (#7955) +(expect-with-engine :postgres + [[#uuid "4f01dcfd-13f7-430c-8e6f-e505c0851027" 1]] + (data/dataset metabase.driver.postgres-test/with-uuid + (rows (qp/process-query {:database (data/id) + :type :native + :native {:query "SELECT * FROM users WHERE {{user}}" + :template_tags {:user {:name "user" + :display_name "User ID" + :type "dimension" + :dimension ["field-id" (data/id :users :user_id)]}}} + :parameters [{:type "text" + :target ["dimension" ["template-tag" "user"]] + :value "4f01dcfd-13f7-430c-8e6f-e505c0851027"}]})))) + ;; Make sure that Tables / Fields with dots in their names get escaped properly (i/def-database-definition ^:private dots-in-names diff --git a/test/metabase/query_processor/middleware/parameters/sql_test.clj b/test/metabase/query_processor/middleware/parameters/sql_test.clj index bb775402c848c..d89aeabe584d1 100644 --- a/test/metabase/query_processor/middleware/parameters/sql_test.clj +++ b/test/metabase/query_processor/middleware/parameters/sql_test.clj @@ -298,7 +298,8 @@ (expect {:field {:name "DATE" :parent_id nil - :table_id (data/id :checkins)} + :table_id (data/id :checkins) + :base_type :type/Date} :param {:type "date/range" :target ["dimension" ["template-tag" "checkin_date"]] :value "2015-04-01~2015-05-01"}} @@ -309,7 +310,8 @@ (expect {:field {:name "DATE" :parent_id nil - :table_id (data/id :checkins)} + :table_id (data/id :checkins) + :base_type :type/Date} :param nil} (into {} (#'sql/value-for-tag {:name "checkin_date", :display_name "Checkin Date", :type "dimension", :dimension ["field-id" (data/id :checkins :date)]} nil))) @@ -318,7 +320,8 @@ (expect {:field {:name "DATE" :parent_id nil - :table_id (data/id :checkins)} + :table_id (data/id :checkins) + :base_type :type/Date} :param [{:type "date/range" :target ["dimension" ["template-tag" "checkin_date"]] :value "2015-01-01~2016-09-01"} @@ -701,7 +704,7 @@ ;; Make sure defaults values get picked up for field filter clauses (expect - {:field {:name "DATE", :parent_id nil, :table_id (data/id :checkins)} + {:field {:name "DATE", :parent_id nil, :table_id (data/id :checkins), :base_type :type/Date} :param {:type "date/all-options" :target ["dimension" ["template-tag" "checkin_date"]] :value "past5days"}}