From 3adcb2adb87281f51607fd785dfa71209f05d5b9 Mon Sep 17 00:00:00 2001 From: Sameer Al-Sakran Date: Wed, 18 Jul 2018 10:45:08 -0700 Subject: [PATCH 01/17] Revert "Fix _location CI failures" From 4e017c020b81bdf8a2968b99d681af5d68e03969 Mon Sep 17 00:00:00 2001 From: Cam Saul Date: Tue, 17 Jul 2018 14:44:26 -0700 Subject: [PATCH 02/17] Parse Postgres UUID fields correctly in Field Filters [ci drivers] --- .../middleware/parameters/sql.clj | 69 ++++++++++++++----- test/metabase/driver/postgres_test.clj | 16 ++++- .../middleware/parameters/sql_test.clj | 11 +-- 3 files changed, 74 insertions(+), 22 deletions(-) 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/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"}} From 395f8a61212df80ba3ea7a9e70369d52dfa3187e Mon Sep 17 00:00:00 2001 From: Kyle Doherty Date: Wed, 18 Jul 2018 12:06:12 -0700 Subject: [PATCH 03/17] Drag and drop cleanup (#8077) * use outline instead of border for collection dnd style * Pass highlight and hovered to CollectionItem for drag and drop * tweak hover style * unpin drop zone * fix show / hide logic * add new collection link --- .../components/CollectionEmptyState.jsx | 15 ++++- .../metabase/components/CollectionItem.jsx | 25 ++++++-- .../metabase/components/CollectionLanding.jsx | 58 ++++++++++++++----- .../metabase/components/CollectionList.jsx | 30 ++++++---- .../src/metabase/containers/dnd/DropArea.jsx | 3 - 5 files changed, 96 insertions(+), 35 deletions(-) diff --git a/frontend/src/metabase/components/CollectionEmptyState.jsx b/frontend/src/metabase/components/CollectionEmptyState.jsx index f8965abc07b62..29231f03dd649 100644 --- a/frontend/src/metabase/components/CollectionEmptyState.jsx +++ b/frontend/src/metabase/components/CollectionEmptyState.jsx @@ -2,11 +2,15 @@ import React from "react"; import { Box } from "grid-styled"; import { t } from "c-3po"; import RetinaImage from "react-retina-image"; +import { withRouter } from "react-router"; + import Subhead from "metabase/components/Subhead"; +import Link from "metabase/components/Link"; +import * as Urls from "metabase/lib/urls"; import { normal } from "metabase/lib/colors"; -const CollectionEmptyState = () => { +const CollectionEmptyState = ({ params }) => { return ( @@ -19,12 +23,17 @@ const CollectionEmptyState = () => { {t`This collection is empty, like a blank canvas`} -

+

{t`You can use collections to organize and group dashboards, questions and pulses for your team or yourself`}

+ {t`Create a sub collection`}
); }; -export default CollectionEmptyState; +export default withRouter(CollectionEmptyState); diff --git a/frontend/src/metabase/components/CollectionItem.jsx b/frontend/src/metabase/components/CollectionItem.jsx index b823b1ac2080d..4ed2f712ee2ff 100644 --- a/frontend/src/metabase/components/CollectionItem.jsx +++ b/frontend/src/metabase/components/CollectionItem.jsx @@ -6,13 +6,28 @@ import Link from "metabase/components/Link"; import colors from "metabase/lib/colors"; -const CollectionItem = ({ collection, color, iconName = "all" }) => ( +const CollectionItem = ({ + collection, + color, + iconName = "all", + highlighted, + hovered, +}) => ( diff --git a/frontend/src/metabase/components/CollectionLanding.jsx b/frontend/src/metabase/components/CollectionLanding.jsx index 764e62227181b..463d2cadc277b 100644 --- a/frontend/src/metabase/components/CollectionLanding.jsx +++ b/frontend/src/metabase/components/CollectionLanding.jsx @@ -23,6 +23,7 @@ import EntityMenu from "metabase/components/EntityMenu"; import VirtualizedList from "metabase/components/VirtualizedList"; import BrowserCrumbs from "metabase/components/BrowserCrumbs"; import ItemTypeFilterBar from "metabase/components/ItemTypeFilterBar"; +import CollectionEmptyState from "metabase/components/CollectionEmptyState"; import CollectionMoveModal from "metabase/containers/CollectionMoveModal"; import { entityObjectLoader } from "metabase/entities/containers/EntityObjectLoader"; @@ -158,6 +159,10 @@ class DefaultLanding extends React.Component { unpinnedItems = unpinned.filter(u => u.model === location.query.type); } + const collectionIsEmpty = !unpinned.length > 0 && !collections.length > 0; + const collectionHasPins = pinned.length > 0; + const collectionHasItems = unpinned.length > 0; + return ( @@ -204,7 +209,7 @@ class DefaultLanding extends React.Component { - {pinned.length > 0 ? ( + {collectionHasPins ? ( {t`Pins`} - - - - {t`Collections`} - + {!collectionIsEmpty && ( + + + + {t`Collections`} + + + - - + )} - {unpinned.length > 0 && ( + {collectionHasItems && ( @@ -347,6 +354,29 @@ class DefaultLanding extends React.Component { )} + {unpinned.length === 0 && ( + + {({ hovered }) => ( + + {t`Drag here to un-pin`} + + )} + + )} + + {collectionIsEmpty && ( + + + + )} 0}> diff --git a/frontend/src/metabase/components/CollectionList.jsx b/frontend/src/metabase/components/CollectionList.jsx index 587dea43b053e..f7faabb255847 100644 --- a/frontend/src/metabase/components/CollectionList.jsx +++ b/frontend/src/metabase/components/CollectionList.jsx @@ -32,9 +32,15 @@ class CollectionList extends React.Component { .map(collection => ( - - - + {({ highlighted, hovered }) => ( + + + + )} ))} @@ -43,13 +49,17 @@ class CollectionList extends React.Component { - + {({ highlighted, hovered }) => ( + + )} )} diff --git a/frontend/src/metabase/containers/dnd/DropArea.jsx b/frontend/src/metabase/containers/dnd/DropArea.jsx index 5d0647582d3bf..cac0f1a23a05a 100644 --- a/frontend/src/metabase/containers/dnd/DropArea.jsx +++ b/frontend/src/metabase/containers/dnd/DropArea.jsx @@ -1,6 +1,5 @@ import React from "react"; import cx from "classnames"; -import { normal } from "metabase/lib/colors"; const DropTargetBackgroundAndBorder = ({ highlighted, @@ -24,8 +23,6 @@ const DropTargetBackgroundAndBorder = ({ right: -marginRight, zIndex: -1, boxSizing: "border-box", - border: "2px solid transparent", - borderColor: hovered & !noDrop ? normal.blue : "transparent", }} /> ); From d3b7843270c808576ec683051a9db266bb430300 Mon Sep 17 00:00:00 2001 From: Tom Robinson Date: Wed, 18 Jul 2018 12:09:25 -0700 Subject: [PATCH 04/17] Potential fix for _location CI failures --- frontend/test/__support__/integrated_tests.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/frontend/test/__support__/integrated_tests.js b/frontend/test/__support__/integrated_tests.js index b242270a8bfaf..e4eec58f8b6d7 100644 --- a/frontend/test/__support__/integrated_tests.js +++ b/frontend/test/__support__/integrated_tests.js @@ -668,6 +668,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 +718,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) { From 097168df96f52ba1ac9a48ddfd9888503177c399 Mon Sep 17 00:00:00 2001 From: Sameer Al-Sakran Date: Wed, 18 Jul 2018 12:39:02 -0700 Subject: [PATCH 05/17] fix_typos --- frontend/src/metabase/components/CollectionLanding.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/metabase/components/CollectionLanding.jsx b/frontend/src/metabase/components/CollectionLanding.jsx index 463d2cadc277b..d905685dbc73d 100644 --- a/frontend/src/metabase/components/CollectionLanding.jsx +++ b/frontend/src/metabase/components/CollectionLanding.jsx @@ -80,7 +80,7 @@ const QuestionEmptyState = () => ( -

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

+

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

); From 0189831c4d8eab261c6b290f23fd1f2906c67cf9 Mon Sep 17 00:00:00 2001 From: Tom Robinson Date: Wed, 18 Jul 2018 13:20:23 -0700 Subject: [PATCH 06/17] Generated passwords can be longer than the minimum complexity requirements. Use the previous default of 14 --- frontend/src/metabase/lib/utils.js | 5 +++-- frontend/test/lib/utils.unit.spec.js | 9 +++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) 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/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", () => { From 4968993e13575042de6cb7f204002673f63bd4cb Mon Sep 17 00:00:00 2001 From: Cam Saul Date: Wed, 18 Jul 2018 13:35:36 -0700 Subject: [PATCH 07/17] Fix duplicate SLF4J bindings by adding project.clj exclusion --- project.clj | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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" From 65344a05d8e8ae8dbb4968ca0734c0e225a4db81 Mon Sep 17 00:00:00 2001 From: Cam Saul Date: Wed, 18 Jul 2018 15:33:54 -0700 Subject: [PATCH 08/17] Include Root Collection in GET /api/collection response --- src/metabase/api/collection.clj | 34 ++++++++++++++++++--------- test/metabase/api/collection_test.clj | 15 ++++++++---- 2 files changed, 33 insertions(+), 16 deletions(-) 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/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"}]] From f607f2a6ea0cff5c969a1397438d873ff091e2c2 Mon Sep 17 00:00:00 2001 From: Kyle Doherty Date: Thu, 19 Jul 2018 11:57:49 -0700 Subject: [PATCH 09/17] fix archive redirect (#8108) --- .../components/ArchiveDashboardModal.jsx | 32 +++++++------------ .../dashboard/components/DashboardHeader.jsx | 25 ++++++++------- frontend/src/metabase/meta/types/Dashboard.js | 2 ++ 3 files changed, 27 insertions(+), 32 deletions(-) diff --git a/frontend/src/metabase/dashboard/components/ArchiveDashboardModal.jsx b/frontend/src/metabase/dashboard/components/ArchiveDashboardModal.jsx index 14eba601aa9cc..a1fe47cdd1620 100644 --- a/frontend/src/metabase/dashboard/components/ArchiveDashboardModal.jsx +++ b/frontend/src/metabase/dashboard/components/ArchiveDashboardModal.jsx @@ -1,16 +1,14 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; import { t } from "c-3po"; + +import Button from "metabase/components/Button"; import ModalContent from "metabase/components/ModalContent.jsx"; export default class ArchiveDashboardModal extends Component { - constructor(props, context) { - super(props, context); - - this.state = { - error: null, - }; - } + state = { + error: null, + }; static propTypes = { dashboard: PropTypes.object.isRequired, @@ -43,23 +41,15 @@ export default class ArchiveDashboardModal extends Component { return ( -
-

{t`Are you sure you want to do this?`}

-
+

{t`Are you sure you want to do this?`}

-
- - + + {formError}
diff --git a/frontend/src/metabase/dashboard/components/DashboardHeader.jsx b/frontend/src/metabase/dashboard/components/DashboardHeader.jsx index eab5200cc0241..383d9373baae9 100644 --- a/frontend/src/metabase/dashboard/components/DashboardHeader.jsx +++ b/frontend/src/metabase/dashboard/components/DashboardHeader.jsx @@ -3,20 +3,21 @@ import React, { Component } from "react"; import PropTypes from "prop-types"; import { t } from "c-3po"; -import ActionButton from "metabase/components/ActionButton.jsx"; -import AddToDashSelectQuestionModal from "./AddToDashSelectQuestionModal.jsx"; -import ArchiveDashboardModal from "./ArchiveDashboardModal.jsx"; -import Header from "metabase/components/Header.jsx"; -import Icon from "metabase/components/Icon.jsx"; -import ModalWithTrigger from "metabase/components/ModalWithTrigger.jsx"; -import Tooltip from "metabase/components/Tooltip.jsx"; +import ActionButton from "metabase/components/ActionButton"; +import AddToDashSelectQuestionModal from "./AddToDashSelectQuestionModal"; +import ArchiveDashboardModal from "./ArchiveDashboardModal"; +import Header from "metabase/components/Header"; +import Icon from "metabase/components/Icon"; +import ModalWithTrigger from "metabase/components/ModalWithTrigger"; +import Tooltip from "metabase/components/Tooltip"; import DashboardEmbedWidget from "../containers/DashboardEmbedWidget"; import { getDashboardActions } from "./DashboardActions"; -import ParametersPopover from "./ParametersPopover.jsx"; -import Popover from "metabase/components/Popover.jsx"; +import ParametersPopover from "./ParametersPopover"; +import Popover from "metabase/components/Popover"; +import * as Urls from "metabase/lib/urls"; import MetabaseSettings from "metabase/lib/settings"; import cx from "classnames"; @@ -144,8 +145,10 @@ export default class DashboardHeader extends Component { } async onArchive() { - await this.props.archiveDashboard(this.props.dashboard.id); - this.props.onChangeLocation("/dashboards"); + const { dashboard } = this.props; + // TODO - this should use entity action + await this.props.archiveDashboard(dashboard.id); + this.props.onChangeLocation(Urls.collection(dashboard.collection_id)); } getEditingButtons() { 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; From 74a780afd76263b62ccd2b2c948da5954b4af4c9 Mon Sep 17 00:00:00 2001 From: Tom Robinson Date: Thu, 19 Jul 2018 12:44:19 -0700 Subject: [PATCH 10/17] Add 'yarn ci' which runs all frontend and most backend tests (no drivers etc) --- package.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 28862e9d3dde2..56bcaf1ffa955 100644 --- a/package.json +++ b/package.json @@ -189,7 +189,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"] From 808984ec4616772f17332562ba8b104316f789ef Mon Sep 17 00:00:00 2001 From: Tom Robinson Date: Thu, 19 Jul 2018 16:32:20 -0700 Subject: [PATCH 11/17] Spaces polish (#8104) * Reset the save xray button when switching xrays * Fix duplicate xray save toast icon * Permissions grid wrap entity name * Extract bulk action methods * Line up bulk actions * Fix lint * remove favorite action on entity items * use and space button * fix padding on archive question modal --- .../components/PermissionsGrid.jsx | 9 +- .../src/metabase/components/BulkActionBar.jsx | 8 +- .../metabase/components/CollectionLanding.jsx | 150 ++++++++++-------- .../src/metabase/components/EntityItem.jsx | 8 - .../containers/AddToDashSelectDashModal.jsx | 8 +- .../containers/AutomaticDashboardApp.jsx | 14 +- .../metabase/home/containers/ArchiveApp.jsx | 8 +- .../containers/ArchiveQuestionModal.jsx | 3 +- 8 files changed, 113 insertions(+), 95 deletions(-) diff --git a/frontend/src/metabase/admin/permissions/components/PermissionsGrid.jsx b/frontend/src/metabase/admin/permissions/components/PermissionsGrid.jsx index 5e8d9a83d508a..40d639f47e6ab 100644 --- a/frontend/src/metabase/admin/permissions/components/PermissionsGrid.jsx +++ b/frontend/src/metabase/admin/permissions/components/PermissionsGrid.jsx @@ -117,9 +117,14 @@ const EntityHeader = ({ isLast, }) => (
-
+
-
+

{entity.name}

{entity.subtitle && (
diff --git a/frontend/src/metabase/components/BulkActionBar.jsx b/frontend/src/metabase/components/BulkActionBar.jsx index 9ebcbe6c302d2..335c642994d20 100644 --- a/frontend/src/metabase/components/BulkActionBar.jsx +++ b/frontend/src/metabase/components/BulkActionBar.jsx @@ -1,5 +1,5 @@ import React from "react"; -import { Box, Flex } from "grid-styled"; +import { Box } from "grid-styled"; import Card from "metabase/components/Card"; import { Motion, spring } from "react-motion"; @@ -29,11 +29,7 @@ const BulkActionBar = ({ children, showing }) => ( transform: `translateY(${translateY}px)`, }} > - - - {children} - - + {children} )} diff --git a/frontend/src/metabase/components/CollectionLanding.jsx b/frontend/src/metabase/components/CollectionLanding.jsx index d905685dbc73d..92ab6309c2971 100644 --- a/frontend/src/metabase/components/CollectionLanding.jsx +++ b/frontend/src/metabase/components/CollectionLanding.jsx @@ -1,12 +1,13 @@ import React from "react"; import { Box, Flex } from "grid-styled"; -import { t } from "c-3po"; +import { t, msgid, ngettext } from "c-3po"; import { connect } from "react-redux"; +import { withRouter } from "react-router"; import _ from "underscore"; +import cx from "classnames"; + import listSelect from "metabase/hoc/ListSelect"; import BulkActionBar from "metabase/components/BulkActionBar"; -import cx from "classnames"; -import { withRouter } from "react-router"; import * as Urls from "metabase/lib/urls"; import colors, { normal } from "metabase/lib/colors"; @@ -122,6 +123,38 @@ class DefaultLanding extends React.Component { moveItems: null, }; + handleBulkArchive = async () => { + try { + await Promise.all( + this.props.selected.map(item => item.setArchived(true)), + ); + } finally { + this.handleBulkActionSuccess(); + } + }; + + handleBulkMoveStart = () => { + this.setState({ moveItems: this.props.selected }); + }; + + handleBulkMove = async collection => { + try { + await Promise.all( + this.state.moveItems.map(item => item.setCollection(collection)), + ); + this.setState({ moveItems: null }); + } finally { + this.handleBulkActionSuccess(); + } + }; + + handleBulkActionSuccess = () => { + // Clear the selection in listSelect + // Fixes an issue where things were staying selected when moving between + // different collection pages + this.props.onSelectNone(); + }; + render() { const { ancestors, @@ -136,19 +169,10 @@ class DefaultLanding extends React.Component { selected, selection, onToggleSelected, - onSelectNone, location, } = this.props; const { moveItems } = this.state; - // Call this when finishing a bulk action - const onBulkActionSuccess = () => { - // Clear the selection in listSelect - // Fixes an issue where things were staying selected when moving between - // different collection pages - onSelectNone(); - }; - 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]; @@ -380,60 +404,53 @@ class DefaultLanding extends React.Component { 0}> - - - - item.setArchived) - ? async () => { - try { - await Promise.all( - selected.map(item => item.setArchived(true)), - ); - } finally { - onBulkActionSuccess(); - } - } - : null - } - onMove={ - _.all(selected, item => item.setCollection) - ? () => { - this.setState({ moveItems: selected }); - } - : null - } - /> - {t`${selected.length} items selected`} - - + {/* NOTE: these padding and grid sizes must be carefully matched + to the main content above to ensure the bulk checkbox lines up */} + + + + + + + item.setArchived) + ? this.handleBulkArchive + : null + } + onMove={ + _.all(selected, item => item.setCollection) + ? this.handleBulkMoveStart + : null + } + /> + + {ngettext( + msgid`${selected.length} item selected`, + `${selected.length} items selected`, + selected.length, + )} + + + + + - {moveItems && - moveItems.length > 0 && ( - - 1 - ? t`Move ${moveItems.length} items?` - : `Move "${moveItems[0].getName()}"?` - } - onClose={() => this.setState({ moveItems: null })} - onMove={async collection => { - try { - await Promise.all( - moveItems.map(item => item.setCollection(collection)), - ); - this.setState({ moveItems: null }); - } finally { - onBulkActionSuccess(); - } - }} - /> - - )} + {!_.isEmpty(moveItems) && ( + + 1 + ? t`Move ${moveItems.length} items?` + : t`Move "${moveItems[0].getName()}"?` + } + onClose={() => this.setState({ moveItems: null })} + onMove={this.handleBulkMove} + /> + + )} ); @@ -527,13 +544,14 @@ const SelectionControls = ({ deselected, onSelectAll, onSelectNone, + size = 18, }) => deselected.length === 0 ? ( - + ) : selected.length === 0 ? ( - + ) : ( - + ); @entityObjectLoader({ diff --git a/frontend/src/metabase/components/EntityItem.jsx b/frontend/src/metabase/components/EntityItem.jsx index e341992e3f659..a5c90897f7b27 100644 --- a/frontend/src/metabase/components/EntityItem.jsx +++ b/frontend/src/metabase/components/EntityItem.jsx @@ -107,14 +107,6 @@ const EntityItem = ({ e.preventDefault()}> - {(onFavorite || isFavorite) && ( - - )} {actions.length > 0 && ( - + {t`Create New Dashboard`} + ); } diff --git a/frontend/src/metabase/dashboard/containers/AutomaticDashboardApp.jsx b/frontend/src/metabase/dashboard/containers/AutomaticDashboardApp.jsx index 0e0356286440d..4e8abc47de530 100644 --- a/frontend/src/metabase/dashboard/containers/AutomaticDashboardApp.jsx +++ b/frontend/src/metabase/dashboard/containers/AutomaticDashboardApp.jsx @@ -70,12 +70,6 @@ class AutomaticDashboardApp extends React.Component { ); triggerToast(
- {t`Your dashboard was saved`}
, + { icon: "dashboard" }, ); this.setState({ savedDashboardId: newDashboard.id }); MetabaseAnalytics.trackEvent("AutoDashboard", "Save"); }; + componentWillReceiveProps(nextProps) { + // clear savedDashboardId if changing to a different dashboard + if (this.props.location.pathname !== nextProps.location.pathname) { + this.setState({ savedDashboardId: null }); + } + } + render() { const { dashboard, 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/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.`}
); } From 40f33d7de186604b88b80fe8900fd4754f7607f9 Mon Sep 17 00:00:00 2001 From: Kyle Doherty Date: Fri, 20 Jul 2018 08:37:24 -0700 Subject: [PATCH 12/17] Work on cleaning up empty states for the home page and collections (#8106) * clean up schema picker + add empty state for 'our analytics' * smaller illustration * more empty state space * more reasonable card content size * lint * copy tweak [ci skip] * admin based messaging * lighten image * lint * boo --- .../metabase/components/CollectionLanding.jsx | 2 +- .../src/metabase/components/ExplorePane.jsx | 6 +-- .../src/metabase/containers/Overworld.jsx | 35 ++++++++++++++---- .../containers/UserCollectionList.jsx | 4 +- resources/frontend_client/app/img/empty.png | Bin 0 -> 4790 bytes .../frontend_client/app/img/empty@2x.png | Bin 0 -> 10647 bytes 6 files changed, 33 insertions(+), 14 deletions(-) create mode 100644 resources/frontend_client/app/img/empty.png create mode 100644 resources/frontend_client/app/img/empty@2x.png diff --git a/frontend/src/metabase/components/CollectionLanding.jsx b/frontend/src/metabase/components/CollectionLanding.jsx index 92ab6309c2971..1822f1f04ba8b 100644 --- a/frontend/src/metabase/components/CollectionLanding.jsx +++ b/frontend/src/metabase/components/CollectionLanding.jsx @@ -47,7 +47,7 @@ const EmptyStateWrapper = ({ children }) => ( 1 && ( -
-
- Here's the schema I looked at: -
+
+
{t`Based on the schema`}
(

this.setState({ visibleItems: visibleItems + 4 })} > @@ -156,7 +156,7 @@ export const ExploreList = ({ export const ExploreOption = ({ option }: { option: Candidate }) => ( ( - - Favorites - - {({ favorites, loading, error }) => { - if (loading) { - return Loading...; - } - return favorites.map(favorite => {favorite.name}); - }} - - -); - -export default Favorites; diff --git a/frontend/src/metabase/components/FieldSet.jsx b/frontend/src/metabase/components/FieldSet.jsx index 1aa0b25174f84..e0accef80a857 100644 --- a/frontend/src/metabase/components/FieldSet.jsx +++ b/frontend/src/metabase/components/FieldSet.jsx @@ -20,7 +20,7 @@ export default function FieldSet({ return (
{legend && ( - + {legend} )} diff --git a/frontend/src/metabase/components/ListFilterWidget.jsx b/frontend/src/metabase/components/ListFilterWidget.jsx deleted file mode 100644 index ed6152a68455e..0000000000000 --- a/frontend/src/metabase/components/ListFilterWidget.jsx +++ /dev/null @@ -1,65 +0,0 @@ -/* @flow */ - -// An UI element that is normally right-aligned and showing a currently selected filter together with chevron. -// Clicking the element will trigger a popover showing all available filter options. - -import React, { Component } from "react"; -import Icon from "metabase/components/Icon"; -import PopoverWithTrigger from "./PopoverWithTrigger"; - -export type ListFilterWidgetItem = { - id: string, - name: string, - icon: string, -}; - -export default class ListFilterWidget extends Component { - props: { - items: ListFilterWidgetItem[], - activeItem: ListFilterWidgetItem, - onChange: ListFilterWidgetItem => void, - }; - - popoverRef: PopoverWithTrigger; - iconRef: Icon; - - render() { - const { items, activeItem, onChange } = this.props; - return ( - (this.popoverRef = p)} - triggerClasses="block ml-auto flex-no-shrink" - targetOffsetY={10} - triggerElement={ -
- {activeItem && activeItem.name} - (this.iconRef = i)} - className="ml1" - name="chevrondown" - width="12" - height="12" - /> -
- } - target={() => this.iconRef} - > -
    - {items.map((item, index) => ( -
  1. { - onChange(item); - this.popoverRef.close(); - }} - > - -

    {item.name}

    -
  2. - ))} -
-
- ); - } -} diff --git a/frontend/src/metabase/components/ListSearchField.jsx b/frontend/src/metabase/components/ListSearchField.jsx index a3dfd9d9f7288..42bd36d679153 100644 --- a/frontend/src/metabase/components/ListSearchField.jsx +++ b/frontend/src/metabase/components/ListSearchField.jsx @@ -13,7 +13,7 @@ export default class ListSearchField extends Component { }; static defaultProps = { - className: "bordered rounded text-grey-2 flex flex-full align-center", + className: "bordered rounded text-light flex flex-full align-center", inputClassName: "p1 h4 input--borderless text-default flex-full", placeholder: t`Find...`, searchText: "", diff --git a/frontend/src/metabase/components/LoadingAndErrorWrapper.jsx b/frontend/src/metabase/components/LoadingAndErrorWrapper.jsx index 5226f417b1f28..b20891c97f5e3 100644 --- a/frontend/src/metabase/components/LoadingAndErrorWrapper.jsx +++ b/frontend/src/metabase/components/LoadingAndErrorWrapper.jsx @@ -115,7 +115,7 @@ export default class LoadingAndErrorWrapper extends Component {
{error ? (
-

+

{this.getErrorMessage()}

@@ -123,7 +123,7 @@ export default class LoadingAndErrorWrapper extends Component {
{loadingScenes && loadingScenes[sceneIndex]} {!loadingScenes && showSpinner && } -

+

{loadingMessages[messageIndex]}

diff --git a/frontend/src/metabase/components/ModalContent.jsx b/frontend/src/metabase/components/ModalContent.jsx index 8f6abe52a2841..93598ba58402d 100644 --- a/frontend/src/metabase/components/ModalContent.jsx +++ b/frontend/src/metabase/components/ModalContent.jsx @@ -42,7 +42,7 @@ export default class ModalContent extends Component { > {onClose && ( -
+
{t`Get infrequent emails about new releases and feature updates.`}
diff --git a/frontend/src/metabase/components/PasswordReveal.jsx b/frontend/src/metabase/components/PasswordReveal.jsx index 956159a9b8bbb..b238467cd2a58 100644 --- a/frontend/src/metabase/components/PasswordReveal.jsx +++ b/frontend/src/metabase/components/PasswordReveal.jsx @@ -23,7 +23,7 @@ const styles = { const Label = () => (
- + {t`Temporary Password`}
@@ -48,7 +48,7 @@ export default class PasswordReveal extends Component { target.setSelectionRange(0, target.value.length) diff --git a/frontend/src/metabase/components/QuestionIcon.jsx b/frontend/src/metabase/components/QuestionIcon.jsx deleted file mode 100644 index 9856a926aabdc..0000000000000 --- a/frontend/src/metabase/components/QuestionIcon.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from "react"; - -import Icon from "metabase/components/Icon"; - -import Visualizations from "metabase/visualizations"; - -const QuestionIcon = ({ question, ...props }) => ( - -); - -export default QuestionIcon; diff --git a/frontend/src/metabase/components/SchedulePicker.jsx b/frontend/src/metabase/components/SchedulePicker.jsx index 62e61392ee545..80843cbaa72dd 100644 --- a/frontend/src/metabase/components/SchedulePicker.jsx +++ b/frontend/src/metabase/components/SchedulePicker.jsx @@ -205,7 +205,7 @@ export default class SchedulePicker extends Component { className="h4 text-bold bg-white" /> {textBeforeSendTime && ( -
+
{textBeforeSendTime} {hour === 0 ? 12 : hour}:00{" "} {amPm ? "PM" : "AM"} {timezone}, {t`your Metabase timezone`}.
diff --git a/frontend/src/metabase/components/SearchHeader.jsx b/frontend/src/metabase/components/SearchHeader.jsx index 902cb36a27310..e2191f793deb5 100644 --- a/frontend/src/metabase/components/SearchHeader.jsx +++ b/frontend/src/metabase/components/SearchHeader.jsx @@ -28,7 +28,7 @@ const SearchHeader = ({ searchText !== "" && ( diff --git a/frontend/src/metabase/components/Select.jsx b/frontend/src/metabase/components/Select.jsx index 1fb20041ab7e9..e481b6b426e5c 100644 --- a/frontend/src/metabase/components/Select.jsx +++ b/frontend/src/metabase/components/Select.jsx @@ -313,7 +313,7 @@ class LegacySelect extends Component {
{values && values.length !== 0 ? ( diff --git a/frontend/src/metabase/components/SelectButton.jsx b/frontend/src/metabase/components/SelectButton.jsx index 1a17421adf339..20dd188605b3e 100644 --- a/frontend/src/metabase/components/SelectButton.jsx +++ b/frontend/src/metabase/components/SelectButton.jsx @@ -10,7 +10,7 @@ const SelectButton = ({ className, style, children, hasValue = true }) => (
{children} diff --git a/frontend/src/metabase/components/SortableItemList.css b/frontend/src/metabase/components/SortableItemList.css deleted file mode 100644 index 5b0335d1ec6cc..0000000000000 --- a/frontend/src/metabase/components/SortableItemList.css +++ /dev/null @@ -1,3 +0,0 @@ -.SortableItemList-list { - overflow-y: auto; -} diff --git a/frontend/src/metabase/components/SortableItemList.jsx b/frontend/src/metabase/components/SortableItemList.jsx deleted file mode 100644 index 17149dc2bfc93..0000000000000 --- a/frontend/src/metabase/components/SortableItemList.jsx +++ /dev/null @@ -1,100 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; - -import "./SortableItemList.css"; -import { t } from "c-3po"; -import Icon from "metabase/components/Icon.jsx"; -import Radio from "metabase/components/Radio.jsx"; - -import moment from "moment"; - -export default class SortableItemList extends Component { - constructor(props, context) { - super(props, context); - this.state = { - sort: props.initialSort || "Last Modified", - }; - } - - static propTypes = { - items: PropTypes.array.isRequired, - clickItemFn: PropTypes.func, - showIcons: PropTypes.bool, - }; - - onClickItem(item) { - if (this.props.onClickItemFn) { - this.props.onClickItemFn(item); - } - } - - render() { - let items; - if (this.state.sort === "Last Modified") { - items = this.props.items - .slice() - .sort((a, b) => b.updated_at - a.updated_at); - } else if (this.state.sort === "Alphabetical Order") { - items = this.props.items - .slice() - .sort((a, b) => - a.name.toLowerCase().localeCompare(b.name.toLowerCase()), - ); - } - - return ( - - ); - } -} diff --git a/frontend/src/metabase/components/TermWithDefinition.jsx b/frontend/src/metabase/components/TermWithDefinition.jsx deleted file mode 100644 index 11ac2f2d9ee9a..0000000000000 --- a/frontend/src/metabase/components/TermWithDefinition.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from "react"; -import cxs from "cxs"; -import Tooltip from "metabase/components/Tooltip"; -import colors from "metabase/lib/colors"; - -const termStyles = cxs({ - textDecoration: "none", - borderBottom: `1px dotted ${colors["border"]}`, -}); -export const TermWithDefinition = ({ children, definition, link }) => ( - - {link ? ( - - {children} - - ) : ( - {children} - )} - -); diff --git a/frontend/src/metabase/components/TokenField.jsx b/frontend/src/metabase/components/TokenField.jsx index 587d80046a73c..0c0b598ab8f49 100644 --- a/frontend/src/metabase/components/TokenField.jsx +++ b/frontend/src/metabase/components/TokenField.jsx @@ -563,7 +563,7 @@ export default class TokenField extends Component { {valueRenderer(v)} {multi && ( { this.removeValue(v); e.preventDefault(); diff --git a/frontend/src/metabase/containers/EntitySearch.jsx b/frontend/src/metabase/containers/EntitySearch.jsx index 89af5acc90257..45f7d09c3251e 100644 --- a/frontend/src/metabase/containers/EntitySearch.jsx +++ b/frontend/src/metabase/containers/EntitySearch.jsx @@ -235,8 +235,8 @@ export default class EntitySearch extends Component { -

{t`No results found`}

-

{t`Try adjusting your filter to find what you’re looking for.`}

+

{t`No results found`}

+

{t`Try adjusting your filter to find what you’re looking for.`}

} image="app/img/empty_question" @@ -287,7 +287,7 @@ export class SearchGroupingOption extends Component {
  • @@ -511,7 +511,7 @@ class SearchResultsList extends Component { className={cx( "flex align-center justify-center rounded", { "cursor-pointer bg-medium text-white": !isInEnd }, - { "bg-light text-grey-2": isInEnd }, + { "bg-light text-light": isInEnd }, )} style={{ width: "22px", height: "22px" }} onClick={() => !isInEnd && setCurrentPage(entities, currentPage + 1)} diff --git a/frontend/src/metabase/containers/ItemName.jsx b/frontend/src/metabase/containers/ItemName.jsx deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/frontend/src/metabase/containers/ItemPicker.jsx b/frontend/src/metabase/containers/ItemPicker.jsx index 763c03fb05070..d2fa5719a6fcb 100644 --- a/frontend/src/metabase/containers/ItemPicker.jsx +++ b/frontend/src/metabase/containers/ItemPicker.jsx @@ -119,7 +119,7 @@ export default class ItemPicker extends React.Component { /> this.setState({ searchMode: null, searchString: null }) } @@ -130,7 +130,7 @@ export default class ItemPicker extends React.Component { this.setState({ searchMode: true })} /> @@ -245,7 +245,7 @@ const Item = ({ {Greeting.sayHello(user.first_name)} -

    {t`Don't tell anyone, but you're my favorite.`}

    +

    {t`Don't tell anyone, but you're my favorite.`}

    diff --git a/frontend/src/metabase/containers/QuestionListLoader.jsx b/frontend/src/metabase/containers/QuestionListLoader.jsx deleted file mode 100644 index 2b589c252aaa7..0000000000000 --- a/frontend/src/metabase/containers/QuestionListLoader.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from "react"; - -import EntityListLoader from "metabase/entities/containers/EntityListLoader"; - -const QuestionListLoader = props => ( - -); - -export default QuestionListLoader; diff --git a/frontend/src/metabase/css/admin.css b/frontend/src/metabase/css/admin.css index 17226a704a117..a886c098e58f2 100644 --- a/frontend/src/metabase/css/admin.css +++ b/frontend/src/metabase/css/admin.css @@ -309,3 +309,11 @@ .AdminTable tbody tr:first-child td { padding-top: var(--margin-1); } + +.AdminLink { + opacity: 0.435; +} + +.AdminLink:hover { + opacity: 1; +} diff --git a/frontend/src/metabase/css/components/dropdown.css b/frontend/src/metabase/css/components/dropdown.css deleted file mode 100644 index 4916a722f1935..0000000000000 --- a/frontend/src/metabase/css/components/dropdown.css +++ /dev/null @@ -1,65 +0,0 @@ -:root { - --dropdown-border-color: color(var(--color-accent2) alpha(-94%)); -} - -.Dropdown { - position: relative; -} - -.Dropdown-content { - opacity: 0; /* start invisible */ - pointer-events: none; /* and without any clicks */ - z-index: 20; - position: absolute; - top: 40px; - min-width: 200px; - margin-top: 18px; - border: 1px solid color(var(--color-accent2) alpha(-94%)); - background-color: var(--color-bg-white); - border-radius: 4px; - box-shadow: 0 0 2px var(--color-shadow); - background-clip: padding-box; - padding-top: 1em; - padding-bottom: 1em; -} - -.Dropdown-content:before { - position: absolute; - top: -20px; - right: 0; - border-left: 5px solid transparent; - border-right: 5px solid transparent; - border-right: 5px solid red; - content: ""; - display: block; -} - -/* switching from home rolled to BS logic for dropdowns so we still have both classes */ -.Dropdown.open .Dropdown-content, -.Dropdown--showing.Dropdown-content { - opacity: 1; - pointer-events: all; - transition: opacity 0.3s linear, margin 0.2s linear; - margin-top: 0; -} - -.Dropdown-item { - padding-top: 1rem; - padding-bottom: 1rem; - padding-left: 2rem; - padding-right: 2rem; - line-height: 1; -} - -.Dropdown .Dropdown-item .link:hover { - text-decoration: none; -} - -.Dropdown-item:hover { - color: var(--color-text-white); - background-color: var(--color-brand); -} - -.Dropdown .Dropdown-item:hover { - text-decoration: none; -} diff --git a/frontend/src/metabase/css/core/colors.css b/frontend/src/metabase/css/core/colors.css index 57831b8e92afa..48a9c2da42386 100644 --- a/frontend/src/metabase/css/core/colors.css +++ b/frontend/src/metabase/css/core/colors.css @@ -196,37 +196,21 @@ background-color: var(--color-bg-light); } -/* grey */ -.text-grey-1, -:local(.text-grey-1), -.text-grey-1-hover:hover { +.text-light, +:local(.text-light), +.text-light-hover:hover { color: var(--color-text-light); } -.text-grey-2, -:local(.text-grey-2), -.text-grey-2-hover:hover { - color: var(--color-text-light); -} - -.text-grey-3, -:local(.text-grey-3), -.text-grey-3-hover:hover { - color: var(--color-text-medium); -} - -.text-grey-4, -.text-grey-4-hover:hover { - color: var(--color-text-medium); -} - -.text-grey-5, -.text-grey-5-hover:hover { +.text-medium, +:local(.text-medium), +.text-medium-hover:hover { color: var(--color-text-medium); } .text-dark, -:local(.text-dark) { +:local(.text-dark), +.text-dark-hover { color: var(--color-text-dark); } diff --git a/frontend/src/metabase/css/home.css b/frontend/src/metabase/css/home.css index 169f6bb7b81cd..fdf4c43871576 100644 --- a/frontend/src/metabase/css/home.css +++ b/frontend/src/metabase/css/home.css @@ -1,158 +1,7 @@ -:root { - --search-bar-color: var(--color-brand); - --search-bar-active-color: var(--color-brand); - --search-bar-active-border-color: var(--color-brand); -} - .Nav { z-index: 4; } -/* temporary css for the navbar and search */ -.search-bar { - background-color: color(var(--color-bg-white) alpha(-90%)); - border-color: transparent; - color: white; -} - -.search-bar--active { - background-color: color(var(--color-bg-white) alpha(-75%)); - border-color: var(--color-brand); -} - -.NavItem.NavItem--selected { - background-color: color(var(--color-bg-black) alpha(-80%)); -} - -.NavItem { - justify-content: center; -} - -.NavItem > .Icon { - padding-left: 1em; - padding-right: 1em; - padding-top: 0.5em; - padding-bottom: 0.5em; -} - -@media screen and (--breakpoint-min-sm) { - .NavItem { - border-radius: 8px; - } - .NavItem:hover, - .NavItem.NavItem--selected { - background-color: color(var(--color-bg-white) alpha(-92%)); - } -} - -.NavNewQuestion { - box-shadow: 0px 2px 2px 0px var(--color-shadow); -} -.NavNewQuestion:hover { - box-shadow: 0px 3px 2px 2px var(--color-shadow); - color: var(--color-brand); -} - -.Greeting { - padding-top: 2rem; - padding-bottom: 3rem; -} - -@media screen and (--breakpoint-min-xl) { - .Greeting { - padding-top: 6em; - padding-bottom: 6em; - } -} - -.bullet { - position: relative; - margin-left: 1.2em; -} -.bullet:before { - content: "\2022"; - color: var(--color-brand); - position: absolute; - top: 0; - margin-top: 16px; - left: -0.85em; -} - -.NavDropdown { - position: relative; -} -.NavDropdown.open { - z-index: 100; -} -.NavDropdown .NavDropdown-content { - display: none; -} -.NavDropdown.open .NavDropdown-content { - display: inherit; -} -.NavDropdown .NavDropdown-button { - position: relative; - border-radius: 8px; -} -.NavDropdown .NavDropdown-content { - position: absolute; - border-radius: 4px; - top: 38px; - min-width: 200px; -} - -.NavDropdown .NavDropdown-content.NavDropdown-content--dashboards { - top: 33px; -} - -.NavDropdown .NavDropdown-button:before, -.NavDropdown .NavDropdown-content:before { - content: ""; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - box-shadow: 0 0 4px var(--color-shadow); - background-clip: padding-box; -} - -.NavDropdown .NavDropdown-content:before { - z-index: -2; - border-radius: 4px; -} -.NavDropdown .NavDropdown-button:before { - z-index: -1; - opacity: 0; - border-radius: 8px; -} -.NavDropdown.open .NavDropdown-button:before { - opacity: 1; -} -.NavDropdown .NavDropdown-content-layer { - position: relative; - z-index: 1; - overflow: hidden; -} -.NavDropdown .NavDropdown-button-layer { - position: relative; - z-index: 2; -} - -.NavDropdown.open .NavDropdown-button, -.NavDropdown .NavDropdown-content-layer { - background-color: var(--color-brand); -} - -.NavDropdown .NavDropdown-content-layer { - padding-top: 10px; - border-radius: 4px; -} - -.NavDropdown .DashboardList { - min-width: 332px; -} - .QuestionCircle { display: inline-block; font-size: 3.25rem; @@ -163,39 +12,6 @@ text-align: center; } -.IconCircle { - line-height: 0; - padding: var(--padding-1); - border-radius: 99px; - border: 1px solid currentcolor; -} - -@keyframes pop { - 0% { - transform: scale(0.75); - } - 75% { - transform: scale(1.0625); - } - 100% { - transform: scale(1); - } -} - -.animate-pop { - animation-name: popin; - animation-duration: 0.15s; - animation-timing-function: ease-out; -} - -.AdminLink { - opacity: 0.435; -} - -.AdminLink:hover { - opacity: 1; -} - .break-word { word-wrap: break-word; } @@ -208,11 +24,6 @@ color: var(--color-text-light); } -.TableDescription { - max-width: 42rem; - line-height: 1.4; -} - .Layout-sidebar { min-height: 100vh; width: 346px; @@ -231,38 +42,3 @@ line-height: 1; text-transform: uppercase; } - -@media screen and (--breakpoint-min-md) { - .HomepageGreeting { - margin-right: 346px; - } -} - -/* there are 5 mav items on mobile, so distribute the items evenly */ -.Nav ul li { - flex: 0 20%; -} - -/* on larger screens, things should just flow naturally */ -@media screen and (--breakpoint-min-md) { - .Nav ul li { - flex: unset; - } -} - -/* the logo nav item needs a little bit of additional padding so that it - * matches up with the other nav items which have 16px icons and 0.75em padding. - * Since the logo is 32px we cut the padding in half to get 0.375 - * */ -.LogoNavItem { - padding-top: 0.375em; - padding-bottom: 0.375em; -} - -/* we want to unset the above when we no longer need the nav item to fill the space */ -@media screen and (--breakpoint-min-md) { - .LogoNavItem { - padding-top: 0; - padding-bottom: 0; - } -} diff --git a/frontend/src/metabase/dashboard/components/AddSeriesModal.jsx b/frontend/src/metabase/dashboard/components/AddSeriesModal.jsx index 1dc6c4b08148f..399d06b955808 100644 --- a/frontend/src/metabase/dashboard/components/AddSeriesModal.jsx +++ b/frontend/src/metabase/dashboard/components/AddSeriesModal.jsx @@ -339,7 +339,7 @@ export default class AddSeriesModal extends Component { tooltip={t`We're not sure if this question is compatible`} > diff --git a/frontend/src/metabase/dashboard/components/DashCard.jsx b/frontend/src/metabase/dashboard/components/DashCard.jsx index ca7fbd0fa8c27..68b669448a759 100644 --- a/frontend/src/metabase/dashboard/components/DashCard.jsx +++ b/frontend/src/metabase/dashboard/components/DashCard.jsx @@ -138,7 +138,7 @@ export default class DashCard extends Component { > ) : isEmbed ? ( ( triggerElement={ } - triggerClasses="text-grey-2 text-grey-4-hover cursor-pointer flex align-center flex-no-shrink mr1" + triggerClasses="text-light text-medium-hover cursor-pointer flex align-center flex-no-shrink mr1" > ( const RemoveButton = ({ onRemove }) => (
    ( const AddSeriesButton = ({ series, onAddSeries }) => ( diff --git a/frontend/src/metabase/dashboard/components/Dashboard.jsx b/frontend/src/metabase/dashboard/components/Dashboard.jsx index b23786c59a397..eedd561e41bda 100644 --- a/frontend/src/metabase/dashboard/components/Dashboard.jsx +++ b/frontend/src/metabase/dashboard/components/Dashboard.jsx @@ -283,7 +283,7 @@ export default class Dashboard extends Component {
    {t`This dashboard is looking empty.`}
    -
    +
    {t`Add a question to start making it useful!`}
    diff --git a/frontend/src/metabase/dashboard/containers/AutomaticDashboardApp.jsx b/frontend/src/metabase/dashboard/containers/AutomaticDashboardApp.jsx index 4e8abc47de530..75a02b3545654 100644 --- a/frontend/src/metabase/dashboard/containers/AutomaticDashboardApp.jsx +++ b/frontend/src/metabase/dashboard/containers/AutomaticDashboardApp.jsx @@ -188,7 +188,7 @@ const TransientTitle = ({ dashboard }) => ) : null; const TransientFilters = ({ filter, metadata }) => ( -
    +
    {/* $FlowFixMe */} {Q.getFilters({ filter }).map((f, index) => ( @@ -242,11 +242,11 @@ const SuggestionsList = ({ suggestions, section }) => ( className="bg-light rounded flex align-center justify-center text-slate mr1 flex-no-shrink" style={{ width: 48, height: 48 }} > - +

    {s.title}

    -

    {s.description}

    +

    {s.description}

  • @@ -257,7 +257,7 @@ const SuggestionsList = ({ suggestions, section }) => ( const SuggestionsSidebar = ({ related }) => (
    -

    More X-rays

    +

    More X-rays

    {Object.entries(related).map(([section, suggestions]) => ( diff --git a/frontend/src/metabase/hoc/ListSearch.jsx b/frontend/src/metabase/hoc/ListSearch.jsx deleted file mode 100644 index 10778576416aa..0000000000000 --- a/frontend/src/metabase/hoc/ListSearch.jsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from "react"; - -import { caseInsensitiveSearch } from "metabase/lib/string"; -import _ from "underscore"; - -// Higher order component for filtering a list -// -// Injects searchText and onSetSearchText props, and filters down a list prop -// ("list" by default) -// -// Composes with EntityListLoader, ListSelect, etc -const listSearch = ({ - listProp = "list", - properties = ["name"], -} = {}) => ComposedComponent => - class ListSearch extends React.Component { - state = { - searchText: "", - }; - render() { - const { ...props } = this.props; - const { searchText } = this.state; - props[listProp] = - props[listProp] && - props[listProp].filter(item => - _.any(properties, p => caseInsensitiveSearch(item[p], searchText)), - ); - - return ( - this.setState({ searchText })} - /> - ); - } - }; - -export default listSearch; diff --git a/frontend/src/metabase/home/components/Activity.jsx b/frontend/src/metabase/home/components/Activity.jsx index 7a31be22737c3..b86041aee7829 100644 --- a/frontend/src/metabase/home/components/Activity.jsx +++ b/frontend/src/metabase/home/components/Activity.jsx @@ -528,7 +528,7 @@ export default class Activity extends Component {
    {t`Hmmm, looks like nothing has happened yet.`}
    -
    +
    {t`Save a question and get this baby going!`}
    diff --git a/frontend/src/metabase/home/components/ActivityItem.jsx b/frontend/src/metabase/home/components/ActivityItem.jsx index 194cc4039d15c..c67f278a7fcf9 100644 --- a/frontend/src/metabase/home/components/ActivityItem.jsx +++ b/frontend/src/metabase/home/components/ActivityItem.jsx @@ -32,11 +32,11 @@ export default class ActivityItem extends Component {
    -
    +
    {description.userName}  {description.summary}
    -
    +
    {description.timeSince}
    diff --git a/frontend/src/metabase/home/components/RecentViews.jsx b/frontend/src/metabase/home/components/RecentViews.jsx index 6696cf92a6e48..a705a6c774693 100644 --- a/frontend/src/metabase/home/components/RecentViews.jsx +++ b/frontend/src/metabase/home/components/RecentViews.jsx @@ -65,9 +65,9 @@ export default class RecentViews extends Component { })} ) : ( -
    +

    {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/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/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.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/PulseEditCards.jsx b/frontend/src/metabase/pulse/components/PulseEditCards.jsx index 86a5c99901475..8d36c48578971 100644 --- a/frontend/src/metabase/pulse/components/PulseEditCards.jsx +++ b/frontend/src/metabase/pulse/components/PulseEditCards.jsx @@ -159,7 +159,7 @@ export default class PulseEditCards extends Component { return (

    {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/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 0d54c04cda8b8..0ced7080bab49 100644 --- a/frontend/src/metabase/query_builder/components/QueryHeader.jsx +++ b/frontend/src/metabase/query_builder/components/QueryHeader.jsx @@ -193,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(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`} @@ -257,7 +257,7 @@ export default class QueryHeader extends Component { buttonSections.push([ {t`CANCEL`} 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/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/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/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 = ({ { .find(".Icon-staroutline"), ); await store.waitForActions([Dashboards.actionTypes.UPDATE]); - click(app.find(ListFilterWidget)); click(app.find(".TestPopover").find('h4[children="Favorites"]')); From c9c022cce260d0d895d2175ff23f52dce7721ce3 Mon Sep 17 00:00:00 2001 From: Tom Robinson Date: Fri, 20 Jul 2018 15:39:55 -0700 Subject: [PATCH 15/17] Fix collection breadcrumbs not updating after moving. Force the collection to reload when you visit it (#8120) --- frontend/src/metabase/components/CollectionLanding.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/metabase/components/CollectionLanding.jsx b/frontend/src/metabase/components/CollectionLanding.jsx index dd898bd4bc32c..cc17bc901c51d 100644 --- a/frontend/src/metabase/components/CollectionLanding.jsx +++ b/frontend/src/metabase/components/CollectionLanding.jsx @@ -557,6 +557,7 @@ const SelectionControls = ({ @entityObjectLoader({ entityType: "collections", entityId: (state, props) => props.params.collectionId, + reload: true, }) class CollectionLanding extends React.Component { render() { From c7fe642b677168c7c46d0b43df34bbdab3c405da Mon Sep 17 00:00:00 2001 From: Kyle Doherty Date: Fri, 20 Jul 2018 16:44:22 -0700 Subject: [PATCH 16/17] Clean up accidental color changes from color consolidation. (#8121) * more appropriate color * fix use of accent2 in embed --- frontend/src/metabase/public/components/EmbedFrame.css | 2 +- frontend/src/metabase/tutorial/Portal.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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", From 5c09139aad098aef49390ea355c8ee0364a7079f Mon Sep 17 00:00:00 2001 From: Maz Ameli Date: Mon, 23 Jul 2018 12:08:32 -0400 Subject: [PATCH 17/17] remove invisible bg shadow (#8123) --- .../app/img/collection-empty-state.png | Bin 10121 -> 5183 bytes .../app/img/collection-empty-state.svg | 13 ++++++------- .../app/img/collection-empty-state@2x.png | Bin 23276 -> 11578 bytes 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/resources/frontend_client/app/img/collection-empty-state.png b/resources/frontend_client/app/img/collection-empty-state.png index b712cc40eb54bd0fea36c61c98d7c8554286b172..41a19963a763ea81a8ea780db9fbe30122eeb27b 100644 GIT binary patch literal 5183 zcmdT|^;Z*K+#WcUkV%PjNGT|tDlo}O>f|dOqf=@FiBSTAbP0?O=~hOI4wzB{sZpCE zA>B3d-Fx1@;{D;?&pn^>oX@%EKF{Yo_uiNn`dV}}>@)xXfbKa&9ZHT^^600!L7p#7 zRD{TZ!Uw9Q3aG_#t^xo|r043-4Bt|0<_5Y6;w^p*XZ=8)G*~`q&pwpioiUM{E_K$Z zFa>X_gknT5itjkRtfIVLk<6=S@}k|DscjSOJYiJuNHy%^51z;`=~R?2KWM*=hCHIX z_u_#LA9|tlsA+v;#2M=fR)(6)IgS6UEijEFAQtt3}@V^{IpXFPQ z#*Biz5hD(j6YVnCwh}&O7Ny?5JO^f*!} zwke;BhwI$Y<0*Q9AADEj;d6$!70}N7z2J$hbB$kK8(O|U_=UtM={3zuJdzWK)Vr`I z#mEeLT|b}-kvq{d2*?|EV!VxeF46FWQPP2e~TJt z2$g{TV`NRcK`zDK0pNgY0WgqU9qSkDRIJVts)8Jg^a-o!>BJXq#5pnfCvL0XHO?@y zdfBFyTds*Jy2k6MlS;|ejzN-wHidysFHEMV2bMGJ zq?*IS!*RK8$eQlW&l?8-RT`>K+t6mvMk!9s&i*&j>hkEtceE=4-S+jDwRxbmH+*NE z;zRDb+DIDBpUbxLyfN9$o`$fms-fMl{2__3pRcpOq8oM<*r;Hfe*`DQkT;oyiwNc& zZN?Api_&Ch7g~F>zafruyM0MyKScM6kD-6QzE^wgLy`-wdUKQ1EUdqM3VVPO+Bl*LshA{Rqt1Wg*uo z^fspjUhPhlxEJt95CGeVb-c=LL?B8WuWUgxqOP}E58wzwBUWK0Xd^KS`~#DjpU+dH zxbetv{og7w3{Rj5NxmH4yNhLXNL>X4o{Vpt_g+ir8+@}?S*wUTs_9Ek9fm{86WUx? zWpz*AMTf}hsl5;%s+IE-W~+ifl=X@$G8swaUR{kgn&~%gX5Uy^y7N+m#l`vaBay%e ze)S=jpEQ4HnQYaz=}-n*33(NRSGGp2>qE#+LcqwaM$fsm8M?JWSap|p&Q0QT$z+Ui z8+uSvKM&F$p}yC|&XOMZA9K^_)X$&pKF{J-Rw}m@HuA!q7qyw6@&3((GfB`*&)s=F zL{w%PA5GYjEbqiVsk8|<4O;;7+EC`WdV@{>eBj*)W0;L!e8&49S$2l4*RUm#WZAJ8 zd`mF=U)e2zSW{WZvNL^3@dT;?z)<}Kt$irYNDb%ATC=rd8>wR*)S%Y{=pTeL#uqiE z<;%SQ1w0%HWHSZ{0km&Q+gL0yw(A!=S$##TzPr($Ra=EnN=_&E)=Y9z2e}offMdL< zA{cg5JZFFLd*?8;w*^?Bt0?o*^SbSrDys~O__pS61?U>|6*T}cR6m=nQ5(h~D(wvo z-`!@fR9}7*V0@wrGP-{!c4$USlAn3N_yJ5o##qOal+qT~p*VP6ZX*`e`kt@A(6)7c zs@lQi{R3efMOXsWeZgDrQ4(^Um(%rrNY_ursteZi3ellZOnt=z!HmZf>?Ut(J(GOu zRTL@vsk*&<1H$df=uw7!7tVW0K1K4sy?r}2XXGp{ZU+yj=|AH4_}V!Th3;KsmNstG z`c`l}AK-K&{oNJYuQVk|*iSrNU2U~o*?UQ6ROY)3-XEMfGzkzDifOs2sW}&-0k=}h z;CfFCb@6Kt8ON!X3v}2)APoyvAM6`nH`0Fe{83+%+Dc>6_2MHB?9EV_21KUpC>PE7te2p{%vN6 zZlzvYPGIjA3`2-eub_PQ&EhGP{ka?{7|&c>Gb6#K(}LeB2F3b#0(@kaRS|3v|D4D-@9)|5Kg*le~BWDy%og0C?6|<@Oo?gpB%aN z{IxP^BN&AAO74h;*%Z>71kd+Bl#))Cpz8Tw<}&2HUqz6ai!Qcy=pZxq)}LQqgNjV8 z0*wRq|0&>JcKhoQ=FJ59p?pu^7(AElF`Tt7FnJb69U&5D+KF3~r!C>_h|P&c!p?lMvPlF?$VDKCl$w4{0mW(BI=i8|MRb_RWzsXNAa`oX_llAH!pwqNy8 z7<5d)0%jc@50{Y|%cb}U}BZf zG>+=fTdbEiB>c_SC1m6r(e=SEyl3t2mha0f8RZpsDwU7>8Fy(IrfMfl8d`=UW&but zh}?3nI!_%i`|z&Kflke_`M2CR0YJ7(LKGpor5s`@NJ&^?lpY$n$_rzN$ou@Oqi*CH zn>Fi=^}_t~B+{hsqY*`g`&PgsXSh1eJw_PZr9o4EbIB$dIJ%lO&m%sIe%Q%uwF zWrq}^{Zz_&HK!PqD5#3#y}|+pzLc<|-O;?6oD?zzvr}r!?qF~B2np}C6PdeaM^MxF z+`vu&w@8VUf`q5Xg8wJ(BC@!T5DdOT&%nM}1A@=Jz^u>Ta{F)Td@C(YYlnDE9qCWS zCkDh@R+LloUnEekjEk0!b^k|VSAz~G@x~<(qL_+=UX+OKm)w>4lyU^vGY|~ZkNkBc z=O;1%EC&CX1TLP@Q)DdDGD)YeU4WPprM@R@`f)3rvlq@Y+T(U>*Mf8E#od96u5@Tl z`p0_1kfY$Pu+LTO3p{K*T&ID`O&xPTCYxCT+Xk+)OA(#vQsgM>ZvsJjp*;)L9&QsR z2BeZg_csY+`)N=sgW*D^o*3RF2FjWE!=V&?RKX;J=AeB~#eGc(utaDCPD3q}(#JwU z#cy`0?yPzb0E+-J_Swaf4f9b*kqjT%FjXmI>>9~F`SYX7OH>lM7x}`}rKZV9yMg@s zdJuAxCV}r|xsj2LO9p8|0%U82@Fwe`$;jzFVQnTxa&dtqh>#f>=|*DuL>&X@950bo z(f#$^EQHTK-VV%b0W_zB=y}qxj-{{Bwqf5dy*J-$qxxR46Ta*Q^L_(~C6egQgX^^i zouY(NmDNX2qwaPFfR~sNrovBz9cXyv*y%`!I?}6RwPRLbiR9-DkC$NQ5g1$~GM1`6ooiL>rG(#IJt8XQx~DBdC{)G{v@& zq5_9?pZ>#Yv|i%;tehtFM<3J$P7rED1DHvd_$&R~D!hx;QK>uoG6_G2#5;C)`4e#$ zLw-%Bo8E3`eB1e<4wy4d^P-()4*U_aO!yKra?;#MQcSoA3o$jt#9pa{rUb$sT6FDt zp`f3BU!;y?q^(7UUvkUmOgB|@Wh*NaP}*4EoUqHKbYUqoB8c{*D=etaR7vv{?y53_ zje5b=QCQgO0Bi4c)>LMoQpa&>)M4*xjC+#T^%OC~k%8Ydr?qu;UywF5gSX?#L{~ok zCpFwYUtLG5wDh~=@>I4tCp?BywjvMS?Qi+-HEV{W!`2t~v%4L*GSc$p;^D$VEV1RG zM?M#qMJacMf&ah?)tR25>v@j6SmJE!9qh8^h0j4)O3^wz-WfkI^X-h_X*qdJK;i{0 z@K)XfI>SvX(;qKPT74JxT5z8v#0>g;zxZ_F=MBw*wg$5n-FLTV-LSXzOHU?>PdfJaE2}nX(tXbN$fh#<2kK@ zHk{nkeO;&VgNq`S67 zwsv+rBgI(rceegu6jdy0i1YCS1HS9GQ{Gb=480D?!CIcj)m4XEByGA`nd@J>E_d)8 z>)-oG`Pl>Nf!#yX*0aX1xO538=G7mapikLD9{O{V=XxIIdCgi{q>s3%?YC@63Pd^N zr&A#hb8fT`f846*{_T-#)ja!(>8^<<7^?c+hWWCU@ZIXW*W(`tV`k3}wg%RG#Ul7} zwToI75%&_j@;NMIAKKyT@MhjZdx|?Hxs@I}nBLnw=19Gb$Vwh_ zf3u@SkeCdnB;V_YUk>I6*hcb;S+x6ZM$3y0%x2xaF~M^EdrqvSa0=ur+~w^_AW^~X zjM_;s6_wR-_$}3Cj!BebwzW0W&XRS^h=p6AjJVpCi+VAuQM1DIpW**;enp`_Za2(yQ1ZH$xAAZ-8I#up9{py4^5`G~N~hAKS?0x_*V7jLTe* zR;Nk%b4WPaMoRQnfc;^nXXbE4oO3=a*QOcnBW>}cp_8eRG59loY<6sS7AI0C@o8hb z-D_4e^!~Ge0{nVg!s+te0iiP``8b3prMdKP(ZD@ZM=CF0huTwX(VXw`x-_ap8{Q2r`a_aBBUOsx7u-(GKt9jiZVm9j`RH?1L-gAzlY|mFHLQ<*O zGa;bJa^bP6a-5Xj=CwDh>pRp=orJp;lLdK|*|db7L-%p6s{%E@vT)a>NWNLLsshg& z+pB+mw?JPgkMirB92eSnTfgP>^TW81q3x7lxsSo+&S!E~IpGwaA{$IRp6VX62>Z3{H#HWLJTQK~Hlmc2sr!vdStW^4nH7m~>}vAY+a(S!JcK z>L}_}@w=-nEP0o;4`g4}Sjmmi(omiJ?%rh|AcK8)(O z2u){-vQ^BWg0CZOrxTTxrB3YeY{tS`p8ZeTmS}nC@uoOwVU!1|&HBoSB{*VB zl(^Pi*`S32?P}ahVh-z@pLguz30Yk>n1yrAvehnN%aAo`61on9Oe^A}btD_*nk#o3 zyG4&NkfPra+d0xO)#KOpEUNTsWpS#$%S_1+Y$HP_!5XYZf!k`mc_#+$3{Ux!IPJhQb$2S#eaTqurQu`n0>5YKEHrg z;&5>kl$to4dlPgN6egIwl(?n`>OnS6lDVw!(oxyxE2XRUORhGdw!h)^K^OMsSajG} zX6Y+tXrQ!SH{4if7}z$^jDk=Xl~@rY8kNlaIFQK9gl!N>Yx*uF*o=&h42T6HGa0IV z8CZX`_~(!A=vaZbW$WE#M{H~hjqwKc;u*4|qaFI((JDg`!exPw6o=b*Ai;^IgCyX?@xKwbLSwT;OSdv)* zd$#PU*|NN^I>`BDrPdR&5zxeV-4U|qsWtVIg!Z(#vvB7&U*qj~iqhFaME-|>W#}lo zGLRYKs8AtlCrvw+GP2PjF?%IJd|4b@XHdM$ZyqG?VL%ihD!FreiLGh&o@6y^qMj^b z1T@_kms#Wq394Oa_}oql1`x->I5DXQ%fT&J2gAPvdwinq)$kE0SJ1n~mfB|c zq!>PV{_X=CF@9Wh^v^IA|6?{ey?*jEVyQa%23y^5C3xG*2M~vh#36awU>hBFpm0oQ z(or(yR*mhO3*rocls#^DU%F>BpmORZ2{svps;_DDwIK4S~@qNZBzq@ zI2Mc1C{K0H-jZ7^xb#w`r#uoXJ%vGIGhye5 zJVCpPPdcM{mBk3_gYED_(@c^$HBXZ=NEIr`xKSg7CDUp?Uu${d#ei^&>-gbr0EXeI z%m^RC_ii7o`;%ImU5&HVp#WtstbUz8jZb*o+cwo=9D(>8hfa61#+T|_{i1EKoy$VWT z{d+lX7XQnJfOx>*`scjak>d9CwSvs-699n7JAB(rjQ?t8qa%2 z-~l_U8(ZRDk=NuA8&FH`u}y#*k3c%+!#FkD8E>>>OFKb^XUU}lK9ZOAhD58*9yn+2 zU~s)4rh9>dN806gb@?%w2QBl`GLiEXL+NM~D*SQd#>4C#m)o6)^p|Pxi1A;j($m|O zLdP<>9_~n}JY+dcB~UaTyX>w1TAk!JgNYJFcoGqlOvCvW4{Y2MF~v=mcjr+}1Mw)> z6Wmm!?iT7LRs9q9Iq)Kf^9cNgXvz(R4FN99)eNeR6+3x4n%=}B6CXfBT%CN>jLb8u zY^5xN&%n682X;gL0YuJdR>C4_z~P9?S2IhU3|@4Up#@)X@h9h^_4@rK+1HF;HBIj1 z5`b4ZKT-P!{2ln9UZ9NJ=Cr68Jo<9nzy2D!#d(6Cj@Mkr*zVba_T{a=SR{lQVyEs} z@$pm!diR}Q*)ttT4jQbXdcuLHa^zhm6c^!y7I2jq41i-I8;XX+oU=FIf?Q-_5&n8M ztv}t}&0GvLp!{-~5-s-W`=VzWMqE}g&$+~knfauBT0 zFrXir@(A?}?=jNE2xvaFVJ{qi7#82_g`|0jt1TMefD?`KV28V%z#8F9k7|Tv+`U3X z>UgWBi*v#lfFk_epfa|{!#sOjz&REHOSU5ASG4#u=5ETm5Cc=ilnE5PRuBO?is zlBS_%eYJ*BjmUX=$&9dg7&Xw`FvYX=1&444UjicV1wG|2tqaR|1@n91QPgv`w?N+X z9~MioLH|Uu>Xen0c@rCSkvQB)Uo)?B{#{`VO!_%LEqwUBO5{4Zc(%rqwlSmefxuyf zZI+4~?Qdf8Ln@sl1Wh z*a~-(YR+b)$E2@8A#m8{gK15(n6;28JA_nApnUGK4P_LI9MVG{7y;%a?(xuWU;|pK zmWQGd&HAB5wTZwCaif5Aval3X-IqbUY>fyL&`Did^W`ax2^Sk-1RH8n=%;k7!EPOF z%u)+)$oyulKdJ>G2GI*Zt}1}O zH*U27c@}*XQrgLF_ z&&ZFU4WC`5VD8#v!o7t9F3lG z3rXkcN5I2hf9(C1@^aD{a$G9JPKb5PXy7r(d?(=Ep;fah=zBu7w8YKzXl=;wxXTJPWgEuX#-L%}2uJj*xRNJ8TlIBW>>DV(3n$&Qw0?h=pgk`6 z72|dceFrqx{c15>Mk@UtIvjh(Uqwfztu{sc+xQnVa5ivbK%O60IH z|K4-RdJ0ZZ7i}*eNG(h0r7-}QHKuDf#Ixv_AjQfb8%t#l6Wyt*i(WB+1yOYy&B^v= z<2yyRX9N20Sffr8JwoftPK4v+1Aw9^^!C`q-(VI@;;o&@6_n^?u{;w!A%1Js~A_DG6FUeW%Os0O`4rysxZkbs!y5FWHSsm5Y9u*V6yT&bl<@s*3kFimO zD8o`jND0J{I);WhLDu&p6QD*;8YAZY5RXCumFUk4_12F5#BVonB?EcOZhVj^q4d}+ zgm@I@e-bS?Ge?!(&D6HhX_J+oY)B!Iucu^IYzN4O%zw(wHzyn%o)x95A1G|p%DdVN zEKoxMyLx2%Z&vg8wT=auL#1U5CzJbVq5XwC20d15&u_FC<&(C=jb3W(XSiY#?^U17b&9-ASifQ)Tc)t6#lIyuXx=EB}UJSflA z;aX;Qz@+>dO`XFeR}FXiKgchLn%aC#+qSWo%Do(H+3YQ0nm|RMjJa;F$-rkF2@?GJ zMAHrwM{>T<$pyySj{PC>@CB!y9=Hvp<9f(jE>|C5V=*a)ggS5jBw>`jdH1`t$;~Ef zzN6CSPiP;)W^pUO^>Z?-*-iuCuu(0?#%<}KXv&u(li>jIL1)mrF;}sz8qxW7pgN7r z*^k+7EYwc1ut$6P6)|?`LCXjGA|j!-Qb)+UcMq%i?49WfWY*_4;F2W)-Ue6A(Yw=v ztBJNw9<>L+il5djaMcSf$00PC#FaVmUFS#BbB7NF1R;whN}E3j#uPS<#IdvefLuT`N~rwJu8wB((%l2sKNi^6pwE|X+M-%Gi^Z_;I?xq2NPI&6oZ zH;UXUSdlRAlT-JveBZ8B012-HObOZuy8c1yllv7b`hGFl0Z5G(CI?^uGoxyKcK~8% z1t!1%Sh+83pY?y)ymTmEtXqgjHV`8^z;fPhb|=>XSEtC>MLw4uRjZ>hn*v)_NTOXC zfF^@7n%tpi*U6`d+-A_ol@tZ7LCqo8hUJOC9~5=!o@%Vv#If&PW-8N3F=?z2{k~Xf zSxHu=2Ke?26R`oMAK62LTZ$>gKTuN*Gk-}#_x7`=O8i@^v%zWT#fsVSQt|t)(h_^- zYrzPvY+PYf+Q)aFiUmIvUuZPWc0XI#KWRyeDzdsbtAv}XndjLlmq;kBY9qO`hK#YF z`u;Re)#;$Wj2P8UZ;>Oz3JEG(?S|CLtd6fCjxEwJ59tvnLZ$xMN9cXM#4kWdgW3gB8tn7UKqq8H zc7Ix%h9;@)$swAfEI|lo1~4OF$MzD&<84Y0(fR1J=y#(Y3{P=|EYPAZV_^+=`tgiQ z`j!P%eKGM7CuqQQxoi5f{sYZ?tJqtA<~_*`2-SXwK0xKYY3P-S-w5q`VIPmzXC5L6 zBK(%DdFL~n>5V6iYh2h}1(CpkZ{C+}qIlPljpXT1{xjFswp}>{z7yFx5uU80S)s2X z-<`g8L-=v6I9FvTYlEy-v#*e72f@7{QnhwxF+pBLt$+JQ9B z!pglIaI#@CYL~p^1UU}8dI&Q@YZ{v6EE6{I9$hL7IBf2+v!9a%5+mgSAp@v$vwbmh zLnwoPKUP}T2O=irLIw!vyUAteKtbBI^W=894!nd&u4i(J{O54~CPETCe`7w|q7CmB z4t3TXrdC9sJ9q$pYx8X#jdC5DKRSE^%H^`4vOV$cbi$DTGe85N_gw;Mtq{iata11m zR*H9v@91nG_T&ppsD#!EC{wB|5ctqB11P>b49Lloji{leG^F{@FWB@&>&Ic+Qr^Da z#Alx$@7WK>gLyn(mx(YGjDVVwUtt6si722C%H1yCOf~x8!FSK2;GmTXg+(TmXP{gs z&p=^lfvwX376_ipzcZ6iY#{KO|6qHzT+1En^LuRmoumrQFh$A}(genWs^bdGG+pEX zXz*P76K(7P*cQ4>4p5LjV74Y@o-wrNfAJU(SZCh+v(Y_1oA&3$irXJ>|la$2~pv*uNF@% zsQ-nf7{C|nkl3%5bY_pk{2$zLRH*!FbP2CXANcn8LlDTKe;x)2Civk0kOHI$G~>ZU z14D0yoez)qUT!%UJ+oCRFd(XlCEX?JND~2%eiz}sc;|pE+YCZX2+`!0l#{v#42dyG zWZ9f&vHW^IHqR7v1!}?z9DWlZ#mhlZgj%_R0iNK@No45>>Ah>|B>xPQJn|UI546>1 zwkT6NIeyo7{LhYAjJWFm^g}QUaH`dW@8Pxi`B~Pu|2>U0w8jM`>hpyjbC=)O{(3%M z!E-rFOPymnk1G`D38)6b0%A`ny|Al3am`4^Uaf4Rq0+_*oFyYBqp`>Pf?$8k?*hEo zY-2MMz#D6$wna(;1<#ip2G9b4W2yNi#gW!Bp<8kRz*3+>)M>CKGc#ng`1o0T1Tb)Z z)0iH?@E^h<#+MJI_?M?4)_U{t_$-jrwN1r6IO> zoj4>sA8A$Y=pqqmFK3K`wL+iy!i^k`E&HQzI~GW&-2LX|(GhPHajdfZBqfL;Jpin- z?$na5Tp)+sP9k1RfCYh8dI*j7UV9P8+FMVugBTKFqGqHo+==+GWd-WDQ-FkWDB1Z& z1Kk{DnZ;_*);#GZhInBB;!X17=cdVg2N%WYLwOo;Ag|VgJ%48QA-}>ipvXxP5JLot zOw`i6q6;&mLv>ArAsz)F5n=Uf=Uk&hs;}u!jgFU+V@UC12t6@;mPGQ0^2JxV_Hf`m zO)l%O(dBN{4}{vAR1zyV=K{tMM`Whguy53V!~!j0{Fyp#&$V*ZYmX1;%{o2Sy!}ci zWvkDsT?t@9I1$?1HvMMv!1hcNdTg;bf>6w4k)Ap(N_*x$i~%dRkt^nJzrZcm=IPg^ z@XxJ!N7;r4v&Z#(zr$VIlYw$DLRibax>y6=j;ybIs@3fruXYSt7|V|47e72r35ZLE z6Mdj1(Ad4K$s^hv!SO`pW$afWQd1G$rb0lQhou&HQOR!+LK zM!&|xUuLE-@cYHzuIRCJAMAmfYV2;vRM4-7lV!zyj( z+vO9;dSylnOto*nfR2=j1NT*BVTPzOCI<#c}84v(%lCRzjIm6Dfe9Y;^1PF6{b9$=Rywnb&$Q( zmTG5H=H!@fjP1+&i{BneV6E+pH(u)z%BFciVV!?J$XX`yD%KFMjNj3#<4@0>LTtxj zriHEoM$l`_YDfCOWJm#z8H*47+}m_%n2WDH~`>Td0HFVed3 z0Q0-e4+MhF7M^9*vj-pX`Mc&tnfM4j1(3&pYkF6gIMrQ(vM!wo3< zj;jsD1o0#Mb50v&BQt0NB0Nd*-{SOe_5K8h-{8uGccMA!f7}=wvDr<) zdDWkU*j0KfWNxb9)5Hhnu*)-I0KW-T=zBR*CT%rvS@HDZ#ZWUFSVO$4f0RtimtW2! ztg*KC#hM+rS<8obB=>V1few|gt36+v&4$!DB_-e5xBy;kw?I;FEl0sQ|9))SbX?jj z=r|3SuZ+@=FRR}A=#QvQgeP(q^C%H+C!*wB{@CO@h`T*6+L~v9h!m{FFo4MeTfSC4 zReqBkp6`qPuLy+IW3dUPT(r@blJ90xW6RcaqgT)dz6Y`?E!=eE<>e7oK+qjy5ClMr zFmb>Gk%KL*XwU^;&hB#Aqi?Rm`FOl&e83U#IH69AO8Gn0rARJ5TCRf&R@9grjTFFW zxKQ1n!t^>7eN^5)l@0K`#4SJ>)VT783xDE95Op|g`KD%v4RDA7@j?9nwM85%r`&s-;n=u!Iu33 zTxTT23hYju{Bd#vMjX76!jVCm8^ojCKJ@gbg+4taA+;&hhrgnc(l)`mqKCcb=W)}kfz{cIZH?3=&q(4pzE|Xk3Z3O=EcZFF zh-6W^HQJ>^-wO@+KVcbfR5Ibh@0fva;xQAcxpb>)Z;_sm`>dQCDmUA^riI2LjZHBL z-j|D8U+cR*`nY&O6Vu%Xb`YUAWvmk0ht3OESpl|>KA?O@{VZuobutfRn!!*1If5}M z_Ae`uM(_EyUa=2E`mWBb{@(ZB8asF@rp<4#NipEEvK*r8IF9E%+V znCQfr?@!uzcxq3~?5a<@nbR4Mz&MPWRw)|1R6D=93gci)RvVQt!d-bq*P-+%lia|H)VVH zX>&r1eE{uaGCef@6{OAN1CttCevlV40;bWaK{dF;a{Oh5KhG<|@hNvpF=%<@1e06Q z0jKh~T?Ur|>GxL;QIz`Mx|4!08lf^N_<2=kT~D+FLcsJ(?iYjYoXq*O8n!DTlF*s( z_=&xf8N3JY*$*98QyrE@Y;<>8Y^|r?xX6$9I&KL+uz(+9k zNDEp1xzadQsL$Hm_BO%KDcy>6q%p$|!qm?f>T__P`=mWZ`jLI=L(v1xDJe6uTQ&Mc z8xdJ{;FVE>)z-tI^TYm{**lfBi+Q+?K634t$?fQXi)SvKbZEMR@6Ag^jlSt$MSjf; z+e@D5jodL7dQ8lQ2C^2$A#2-1JXJ(d9=epYT8W?Kgcr>Z&~a*o17;n&FygubmT$|H zRnSg{>X7>rx-O^FBGOxDq2m4`tW(E0+;EG(m->CeFnaexTDJ7d+!wRmV?;SQ)E->ncYq(HpECF|dczj-9;-K_#!YJdJ5 zR9(Xr&1;Ea#l16icovEnKBMgpKE3T^n^GuBR}xAOvwyij9nkb#Bu_g6`Bx z`*4A5yHs(|2ge$4&%(W?Z#=Si-2k!OY!rxoI60a8K?2t|n&|Imp1%pbpEj8zG*07& zo3o#=b61#Wpp&|l2Pyc(Jd>^Nvn-FrXLhY)29}}*m+bi8mFOpNKbs>y9la`2$@aoA z-0;EfB8&c=RKQmz#?#QrsSVz=CB>MDW{$ysG220_r)#iirJdC~nv?vIW=f(cQi&p6 zh9U?V`UW+rVBNm1uCC?2)D?)}(*44?RCtfNRM=kT2D$9o_kZVf`t!D%)m+bN_IG0! zXuh)HxS@_`>vQqk|%*-rCC zO%C$m{E`j(IQ0xZOC8}GbBx&GvJX~1F6-5q+AW$(jjeirS4CgdJ$MSUKP|j@xtua{ zp?{LHE)??-Tz4H}YHupkR?V=ILZVn+RpFRW>2^n=9X;Lg2{+WM48~;wwA-hcY;mBp zJnAH@bF2zIoA~w78tVMkfHQ|xH`%o&%}7s>m$&}npZAjya}C&=#3EtR8ULoJiTC*0 zBmR(4I7^Jg`w{6iWG{=8(levpLbY1$wzWgg}adsKCh{ZEpfxnnj@buhVgW|ruLN8NIG&`e?_ za$|bBqrm|?bYm+9*J|9l_0#m2^hy`d(HAhGgv&ucO_)~`Ri1n2v^A$t0mrG^iL`x!H==#9tAD?an;HL zc^N8YeXCR>Jv$YrdQ}wfrNv@5hpEn6G{p=< z_2T%us$Hy|_BM|$cdat-#(FNzo{KVOj(q+t$@p3Tou|RsbjlA|yWW}NsF+)IwRaix zIO?Awe6jM+-x=(=E3nN?nLYFNb0Vqz)iUeimmil@#Qg^nT1`k6Q4nVKI(i`fWY2RT zaJIs;Bx!hNCfU@H(3hq|^oCU~OV_9O8X{$nk?$|jaImIRbq4(vLz%%hL2!4(P$%_s zJ!Z57Ivl=RSvnki=caFhx-OjOQG&Ez?Ek6ry~t~RSo5m~F-SmTb42fCaM@FX z3rZ<+a^2p1f4F)!Ryvq+W6YPs^#+{zzD)wj^xq0~83}Fr*MfQtp{nz8Cbs|FQGDI; zKm1cuV3*v6q@r>YH(u`T1$?4xYWe)nui(!jUbk70w0?FZ(3Au6Y$12y8M$^0Co41d zsV?scZdR%QIg1aKBl;5gj%Zg&k#>EiI{u@Uk5@7@GYBpop2Se=h22;|?$f8{PoH)R z?;uwXv^1F^5a?sZT-(}dukM`v;%}9n z!G6IP=s`AHtH)x3wvUg$6mH;qaM_>9CFLoixbw@zn0Ft|+=1(=Yjbuw1!pRg!A~<8 zNRPjiZBAFeQHfgz{k?K6oe7^3-U`qeJeI!{D8!W6{K(OA8buj5Gj<|I1~i@3EWvkw zpCr(BQkLOGA>9LK$nyOu@tBog95*cy9$^ftI&-`+5*m0i2S;Q(KvT|8&3i94&eowp zzHM$kTx3H8M>?=}%AYm9vc7D+iYH!|se9um%!fczMu@C(&cmzi+3Op@?F=VHzMh%v zb#Wcw<WdNV5TJG|;dayYI%A~7Bp`!j7cU - - + + 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 0e98b3713def064c235fb9627b5a3cc0e9f825a7..3ef89ea1cf605a22da2f6d1c9f2200a0cfa0130c 100644 GIT binary patch literal 11578 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 literal 23276 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