diff --git a/.eslintrc b/.eslintrc index f522a534d6aba867c232b5e9876a9b7e0583b416..9dbe198988b824953d24c71806c75c80a096765f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -2,7 +2,7 @@ "rules": { "strict": [2, "never"], "no-undef": 2, - "no-unused-vars": [1, {"vars": "all", "args": "none", "varsIgnorePattern": "^(React|PropTypes|Component)$"}], + "no-unused-vars": [1, {"vars": "all", "args": "none"}], "import/no-commonjs": 1, "quotes": 0, "camelcase": 0, diff --git a/OSX/Metabase/Backend/TaskHealthChecker.m b/OSX/Metabase/Backend/TaskHealthChecker.m index 6a4b9d8a3009dcfc0aaf4b2708d99b3dc7436beb..cfb2221e83b6598f917674465d80161801dd8822 100644 --- a/OSX/Metabase/Backend/TaskHealthChecker.m +++ b/OSX/Metabase/Backend/TaskHealthChecker.m @@ -9,10 +9,10 @@ #import "TaskHealthChecker.h" /// Check out health every this many seconds -static const CGFloat HealthCheckIntervalSeconds = 1.2f; +static const CGFloat HealthCheckIntervalSeconds = 2.0f; /// This number should be lower than HealthCheckIntervalSeconds so requests don't end up piling up -static const CGFloat HealthCheckRequestTimeout = 0.25f; +static const CGFloat HealthCheckRequestTimeout = 1.75f; /// After this many seconds of being unhealthy, consider the task timed out so it can be killed static const CFTimeInterval TimeoutIntervalSeconds = 60.0f; @@ -50,7 +50,7 @@ static const CFTimeInterval TimeoutIntervalSeconds = 60.0f; [self resetTimeout]; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ self.healthCheckTimer = [NSTimer timerWithTimeInterval:HealthCheckIntervalSeconds target:self selector:@selector(checkHealth) userInfo:nil repeats:YES]; self.healthCheckTimer.tolerance = HealthCheckIntervalSeconds / 2.0f; [[NSRunLoop mainRunLoop] addTimer:self.healthCheckTimer forMode:NSRunLoopCommonModes]; @@ -91,8 +91,9 @@ static const CFTimeInterval TimeoutIntervalSeconds = 60.0f; } - (void)checkHealth { + // run the health check on the high-priorty GCD queue because it's imperative that it complete so we can get an accurate picture of Mac App health __weak TaskHealthChecker *weakSelf = self; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ [weakSelf checkHealth:^(BOOL healthy) { if (!healthy) NSLog(@"😷"); if (healthy && !weakSelf.healthy) NSLog(@"✅"); diff --git a/README.md b/README.md index 6c3dff4579d816e6ca3c12071988c2bd5f01c904..98437a7abbf4fa5609eaa0552d874e3acbd85d7e 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ For more information check out [metabase.com](http://www.metabase.com) - CrateDB - Oracle - Vertica +- Presto Don't see your favorite database? File an issue to let us know. diff --git a/bin/ci b/bin/ci index 461fdffa284967ba745f08edadf10ba88f525c38..4869c1cd4ae31e520fe065a3278870a386de2183 100755 --- a/bin/ci +++ b/bin/ci @@ -19,11 +19,15 @@ node-1() { run_step lein-test } node-2() { - is_enabled "drivers" && export ENGINES="h2,postgres,sqlite" || export ENGINES="h2" + is_enabled "drivers" && export ENGINES="h2,postgres,sqlite,presto" || export ENGINES="h2" if is_engine_enabled "crate"; then run_step install-crate fi + if is_engine_enabled "presto"; then + run_step install-presto + fi MB_ENCRYPTION_SECRET_KEY='Orw0AAyzkO/kPTLJRxiyKoBHXa/d6ZcO+p+gpZO/wSQ=' MB_DB_TYPE=mysql MB_DB_DBNAME=circle_test MB_DB_PORT=3306 MB_DB_USER=ubuntu MB_DB_HOST=localhost \ + MB_PRESTO_HOST=localhost MB_PRESTO_PORT=8080 \ run_step lein-test } node-3() { @@ -91,6 +95,11 @@ install-vertica() { sleep 60 } +install-presto() { + docker run --detach --publish 8080:8080 wiill/presto-mb-ci + sleep 10 +} + lein-test() { lein test } diff --git a/docs/administration-guide/databases/cratedb.md b/docs/administration-guide/databases/cratedb.md index dcc6f5da89561b51876db3810b8a325069ad55d1..10864759412c4e9b516080acb1db35ec1e27278a 100644 --- a/docs/administration-guide/databases/cratedb.md +++ b/docs/administration-guide/databases/cratedb.md @@ -15,4 +15,8 @@ Starting in v0.18.0 Metabase provides a driver for connecting to CrateDB directl 3. Click the `Save` button. Done. -Metabase will now begin inspecting your CrateDB Dataset and finding any tables and fields to build up a sense for the schema. Give it a little bit of time to do its work and then you're all set to start querying. \ No newline at end of file +Metabase will now begin inspecting your CrateDB Dataset and finding any tables and fields to build up a sense for the schema. Give it a little bit of time to do its work and then you're all set to start querying. + +### Known limitations + +* Columns/Fields of type `object_array` are deactivated and not exposed. However, their nested fields are listed and also supported for queries. \ No newline at end of file diff --git a/docs/api-documentation.md b/docs/api-documentation.md index 53bd42dfedfb3aecab2292023250be8315415ad3..72bf4b3df84005d9816d8b543b1d751d18f28eec 100644 --- a/docs/api-documentation.md +++ b/docs/api-documentation.md @@ -1,4 +1,4 @@ -# API Documentation for Metabase v0.23.0-snapshot +# API Documentation for Metabase v0.24.0-snapshot ## `GET /api/activity/` @@ -150,6 +150,8 @@ Run the query associated with a Card. * **`parameters`** +* **`ignore_cache`** value may be nil, or if non-nil, value must be a boolean. + ## `POST /api/card/:card-id/query/csv` @@ -193,7 +195,7 @@ Update a `Card`. * **`visualization_settings`** value may be nil, or if non-nil, value must be a map. -* **`description`** value may be nil, or if non-nil, value must be a non-blank string. +* **`description`** value may be nil, or if non-nil, value must be a string. * **`archived`** value may be nil, or if non-nil, value must be a boolean. @@ -420,7 +422,7 @@ Update a `Dashboard`. * **`points_of_interest`** value may be nil, or if non-nil, value must be a non-blank string. -* **`description`** value may be nil, or if non-nil, value must be a non-blank string. +* **`description`** value may be nil, or if non-nil, value must be a string. * **`show_in_getting_started`** value may be nil, or if non-nil, value must be a non-blank string. @@ -587,7 +589,7 @@ You must be a superuser to do this. ## `POST /api/dataset/` -Execute an MQL query and retrieve the results as JSON. +Execute a query and retrieve the results in the usual format. ##### PARAMS: @@ -1286,6 +1288,8 @@ Create a new `Pulse`. * **`channels`** value must be an array. Each value must be a map. The array cannot be empty. +* **`skip_if_empty`** value must be a boolean. + ## `POST /api/pulse/test` @@ -1299,6 +1303,8 @@ Test send an unsaved pulse. * **`channels`** value must be an array. Each value must be a map. The array cannot be empty. +* **`skip_if_empty`** value must be a boolean. + ## `PUT /api/pulse/:id` @@ -1314,6 +1320,8 @@ Update a `Pulse` with ID. * **`channels`** value must be an array. Each value must be a map. The array cannot be empty. +* **`skip_if_empty`** value must be a boolean. + ## `GET /api/revision/` @@ -1482,8 +1490,6 @@ Send a reset email when user has forgotten their password. * **`remote-address`** -* **`request`** - ## `POST /api/session/google_auth` @@ -1561,8 +1567,6 @@ Special endpoint for creating the first user during setup. * **`first_name`** value must be a non-blank string. -* **`request`** - * **`password`** Insufficient password strength * **`name`** @@ -1786,7 +1790,7 @@ Update a user's password. ## `PUT /api/user/:id/qbnewb` -Indicate that a user has been informed about the vast intricacies of 'the' QueryBuilder. +Indicate that a user has been informed about the vast intricacies of 'the' Query Builder. ##### PARAMS: diff --git a/docs/operations-guide/start.md b/docs/operations-guide/start.md index e405a26d087690332e9ea1fb216a4df1467c929a..ab59cd165887477e196a5525b880e944bfdedad3 100644 --- a/docs/operations-guide/start.md +++ b/docs/operations-guide/start.md @@ -98,6 +98,21 @@ If this happens, setting a few JVM options should fix your issue: Alternatively, you can upgrade to Java 8 instead, which will fix the issue as well. +### Metabase fails to connect to H2 Database on Windows 10 + +In some situations the Metabase JAR needs to be unblocked so it has permissions to create local files for the application database. + +On Windows 10, if you see an error message like + + Exception in thread "main" java.lang.AssertionError: Assert failed: Unable to connect to Metabase DB. + +when running the JAR, you can unblock the file by right-clicking, clicking "Properties", and then clicking "Unblock". +See Microsoft's documentation [here](https://blogs.msdn.microsoft.com/delay/p/unblockingdownloadedfile/) for more details on unblocking downloaded files. + +There are a few other reasons why Metabase might not be able to connect to your H2 DB. Metabase connects to the DB over a TCP port, and it's possible +that something in your `ipconfig` configuration is blocking the H2 port. See the discussion [here](https://github.com/metabase/metabase/issues/1871) for +details on how to resolve this issue. + # Configuring the Metabase Application Database diff --git a/frontend/src/metabase/App.jsx b/frontend/src/metabase/App.jsx index 73954a11290ef435c4a124c800d21a31bea3a1b0..89922c39f14769a232e61c36a5a64bdbe913df61 100644 --- a/frontend/src/metabase/App.jsx +++ b/frontend/src/metabase/App.jsx @@ -1,6 +1,6 @@ /* @flow weak */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { connect } from "react-redux"; import Navbar from "metabase/nav/containers/Navbar.jsx"; diff --git a/frontend/src/metabase/admin/databases/components/CreatedDatabaseModal.jsx b/frontend/src/metabase/admin/databases/components/CreatedDatabaseModal.jsx index 29c31dbd99275f2afc8c53df334af36ec1f78098..7db4fd4a3abfc7c97c6313860c715d6893a1af5c 100644 --- a/frontend/src/metabase/admin/databases/components/CreatedDatabaseModal.jsx +++ b/frontend/src/metabase/admin/databases/components/CreatedDatabaseModal.jsx @@ -1,8 +1,11 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { Link } from "react-router"; import ModalContent from "metabase/components/ModalContent.jsx"; +import * as Urls from "metabase/lib/urls"; + export default class CreatedDatabaseModal extends Component { static propTypes = { databaseId: PropTypes.number.isRequired, @@ -22,7 +25,7 @@ export default class CreatedDatabaseModal extends Component { We're analyzing its schema now to make some educated guesses about its metadata. <Link to={"/admin/datamodel/database/"+databaseId}>View this database</Link> in the Data Model section to see what we've found and to - make edits, or <Link to={"/q#?db="+databaseId}>ask a question</Link> about + make edits, or <Link to={Urls.question(null, `?db=${databaseId}`)}>ask a question</Link> about this database. </p> </div> diff --git a/frontend/src/metabase/admin/databases/components/DatabaseEditForms.jsx b/frontend/src/metabase/admin/databases/components/DatabaseEditForms.jsx index feeb38e6dbe951e6ad8a590180343e25e7972e56..ffdcb6bcdf7364f73406a100946a65240559d15d 100644 --- a/frontend/src/metabase/admin/databases/components/DatabaseEditForms.jsx +++ b/frontend/src/metabase/admin/databases/components/DatabaseEditForms.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import cx from "classnames"; import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx"; import DatabaseDetailsForm from "metabase/components/DatabaseDetailsForm.jsx"; diff --git a/frontend/src/metabase/admin/databases/components/DeleteDatabaseModal.jsx b/frontend/src/metabase/admin/databases/components/DeleteDatabaseModal.jsx index 920c65e77fabeef9271b229c54644441f55002f4..1498d90312e97eaae8acec2407b2ca8b070dd528 100644 --- a/frontend/src/metabase/admin/databases/components/DeleteDatabaseModal.jsx +++ b/frontend/src/metabase/admin/databases/components/DeleteDatabaseModal.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ModalContent from "metabase/components/ModalContent.jsx"; diff --git a/frontend/src/metabase/admin/databases/containers/DatabaseEditApp.jsx b/frontend/src/metabase/admin/databases/containers/DatabaseEditApp.jsx index ad8c03784e8bf4f337c21d653f247f5281ece429..b3e122cbc78e59a1dedd976dc1c45c2e8ab5cbe2 100644 --- a/frontend/src/metabase/admin/databases/containers/DatabaseEditApp.jsx +++ b/frontend/src/metabase/admin/databases/containers/DatabaseEditApp.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { connect } from "react-redux"; import MetabaseSettings from "metabase/lib/settings"; diff --git a/frontend/src/metabase/admin/databases/containers/DatabaseListApp.jsx b/frontend/src/metabase/admin/databases/containers/DatabaseListApp.jsx index 439a2c7246ee42152036e345a49f37340dbdea80..ab7241838a81395ecb91c0bcdb54cf1e90b276fd 100644 --- a/frontend/src/metabase/admin/databases/containers/DatabaseListApp.jsx +++ b/frontend/src/metabase/admin/databases/containers/DatabaseListApp.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { connect } from "react-redux"; import { Link } from "react-router"; diff --git a/frontend/src/metabase/admin/datamodel/components/FormInput.jsx b/frontend/src/metabase/admin/datamodel/components/FormInput.jsx index 0cb6812714174fec17835e381f7893e0079f50a6..bfce1383e6f27116d91c5ada25e3af8a1d47a4a6 100644 --- a/frontend/src/metabase/admin/datamodel/components/FormInput.jsx +++ b/frontend/src/metabase/admin/datamodel/components/FormInput.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import cx from "classnames"; import { formDomOnlyProps } from "metabase/lib/redux"; diff --git a/frontend/src/metabase/admin/datamodel/components/FormLabel.jsx b/frontend/src/metabase/admin/datamodel/components/FormLabel.jsx index de2e6fdb440de7938ae07b3e6a4e168bad06de63..c45ee4b1208e5601dab8a96356e20eed93ca81c7 100644 --- a/frontend/src/metabase/admin/datamodel/components/FormLabel.jsx +++ b/frontend/src/metabase/admin/datamodel/components/FormLabel.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; export default class FormLabel extends Component { static propTypes = { diff --git a/frontend/src/metabase/admin/datamodel/components/FormTextArea.jsx b/frontend/src/metabase/admin/datamodel/components/FormTextArea.jsx index e440768bc3e53ce118678d438daba3ba15b9d6b2..a2119223ab145de400721a0a35d488909cbcebfa 100644 --- a/frontend/src/metabase/admin/datamodel/components/FormTextArea.jsx +++ b/frontend/src/metabase/admin/datamodel/components/FormTextArea.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import cx from "classnames"; diff --git a/frontend/src/metabase/admin/datamodel/components/ObjectActionSelect.jsx b/frontend/src/metabase/admin/datamodel/components/ObjectActionSelect.jsx index 9910e1a11a7a4915d4884a6c09ba60046ec5da5c..7e03f702055f9f6ba417f7be4007b864a4baa319 100644 --- a/frontend/src/metabase/admin/datamodel/components/ObjectActionSelect.jsx +++ b/frontend/src/metabase/admin/datamodel/components/ObjectActionSelect.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { Link } from "react-router"; import Icon from "metabase/components/Icon.jsx"; diff --git a/frontend/src/metabase/admin/datamodel/components/ObjectRetireModal.jsx b/frontend/src/metabase/admin/datamodel/components/ObjectRetireModal.jsx index 4393e9d0b1621ed908e63510ceebb3926c89cc85..7f4b8b32638d6d23f88cd47b689e2cc9fdaaab5d 100644 --- a/frontend/src/metabase/admin/datamodel/components/ObjectRetireModal.jsx +++ b/frontend/src/metabase/admin/datamodel/components/ObjectRetireModal.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import ReactDOM from "react-dom"; import ActionButton from "metabase/components/ActionButton.jsx"; diff --git a/frontend/src/metabase/admin/datamodel/components/PartialQueryBuilder.jsx b/frontend/src/metabase/admin/datamodel/components/PartialQueryBuilder.jsx index 133fc5c27f05bcdf1b76aa478e0e8056e9cb8067..ac46b9c868c425a7e745338ba50f53746b3221bc 100644 --- a/frontend/src/metabase/admin/datamodel/components/PartialQueryBuilder.jsx +++ b/frontend/src/metabase/admin/datamodel/components/PartialQueryBuilder.jsx @@ -1,8 +1,9 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import GuiQueryEditor from "metabase/query_builder/components/GuiQueryEditor.jsx"; -import { serializeCardForUrl } from "metabase/lib/card"; +import * as Urls from "metabase/lib/urls"; import cx from "classnames"; @@ -56,7 +57,7 @@ export default class PartialQueryBuilder extends Component { } } }; - let previewUrl = "/q#" + serializeCardForUrl(previewCard); + let previewUrl = Urls.question(null, previewCard); const onChange = (query) => { this.props.onChange(query); diff --git a/frontend/src/metabase/admin/datamodel/components/database/ColumnItem.jsx b/frontend/src/metabase/admin/datamodel/components/database/ColumnItem.jsx index 752e30bdca0a33e3813c6978d6653a1e32e4a9a9..a1283fd11080ced28ad6dc43f862172320ec0e77 100644 --- a/frontend/src/metabase/admin/datamodel/components/database/ColumnItem.jsx +++ b/frontend/src/metabase/admin/datamodel/components/database/ColumnItem.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import Input from "metabase/components/Input.jsx"; import Select from "metabase/components/Select.jsx"; diff --git a/frontend/src/metabase/admin/datamodel/components/database/ColumnsList.jsx b/frontend/src/metabase/admin/datamodel/components/database/ColumnsList.jsx index 7fce80642ffc7c0fd4a59755bd00d33cf3639053..306b46a5cc5c22347e90394618fa397a8f21c9ae 100644 --- a/frontend/src/metabase/admin/datamodel/components/database/ColumnsList.jsx +++ b/frontend/src/metabase/admin/datamodel/components/database/ColumnsList.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ColumnItem from "./ColumnItem.jsx"; diff --git a/frontend/src/metabase/admin/datamodel/components/database/MetadataHeader.jsx b/frontend/src/metabase/admin/datamodel/components/database/MetadataHeader.jsx index 7ce8cf02a275d396c60dea167b65759e7bbaad2a..24900337e631605128b91d715b474b9a869bd6ce 100644 --- a/frontend/src/metabase/admin/datamodel/components/database/MetadataHeader.jsx +++ b/frontend/src/metabase/admin/datamodel/components/database/MetadataHeader.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import SaveStatus from "metabase/components/SaveStatus.jsx"; import Toggle from "metabase/components/Toggle.jsx"; diff --git a/frontend/src/metabase/admin/datamodel/components/database/MetadataSchema.jsx b/frontend/src/metabase/admin/datamodel/components/database/MetadataSchema.jsx index 55c4a4e88ff43d029ebafe510823b3c9c30cfbd5..571cbaed564a98a8d413e0a65ff2095ea089d240 100644 --- a/frontend/src/metabase/admin/datamodel/components/database/MetadataSchema.jsx +++ b/frontend/src/metabase/admin/datamodel/components/database/MetadataSchema.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; export default class MetadataSchema extends Component { static propTypes = { diff --git a/frontend/src/metabase/admin/datamodel/components/database/MetadataSchemaList.jsx b/frontend/src/metabase/admin/datamodel/components/database/MetadataSchemaList.jsx index ef5ecba11ba5a4f7f0ef223e6aa8b710388862c2..f5a1220ef3aaa7d3afa8d9e68c0452366d6e1f03 100644 --- a/frontend/src/metabase/admin/datamodel/components/database/MetadataSchemaList.jsx +++ b/frontend/src/metabase/admin/datamodel/components/database/MetadataSchemaList.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import Icon from "metabase/components/Icon.jsx"; diff --git a/frontend/src/metabase/admin/datamodel/components/database/MetadataTable.jsx b/frontend/src/metabase/admin/datamodel/components/database/MetadataTable.jsx index 92b1a392780814fca12e6ab9b74e10cc2e44c702..190f29442097dabc5623bc672a3d99a65301b12f 100644 --- a/frontend/src/metabase/admin/datamodel/components/database/MetadataTable.jsx +++ b/frontend/src/metabase/admin/datamodel/components/database/MetadataTable.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import MetricsList from "./MetricsList.jsx"; import ColumnsList from "./ColumnsList.jsx"; diff --git a/frontend/src/metabase/admin/datamodel/components/database/MetadataTableList.jsx b/frontend/src/metabase/admin/datamodel/components/database/MetadataTableList.jsx index eeea47c7c4d2e306e66512813119b715030bcc67..5c0897a543e8d4deae1f443b8c71862f07880d68 100644 --- a/frontend/src/metabase/admin/datamodel/components/database/MetadataTableList.jsx +++ b/frontend/src/metabase/admin/datamodel/components/database/MetadataTableList.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ProgressBar from "metabase/components/ProgressBar.jsx"; import Icon from "metabase/components/Icon.jsx"; diff --git a/frontend/src/metabase/admin/datamodel/components/database/MetadataTablePicker.jsx b/frontend/src/metabase/admin/datamodel/components/database/MetadataTablePicker.jsx index fd9a65a18250cc0e264b1407b435dd24620bb3c9..6c6df516e693a1195e1280cfd8d562821eb2c456 100644 --- a/frontend/src/metabase/admin/datamodel/components/database/MetadataTablePicker.jsx +++ b/frontend/src/metabase/admin/datamodel/components/database/MetadataTablePicker.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import MetadataTableList from "./MetadataTableList.jsx"; import MetadataSchemaList from "./MetadataSchemaList.jsx"; diff --git a/frontend/src/metabase/admin/datamodel/components/database/MetricItem.jsx b/frontend/src/metabase/admin/datamodel/components/database/MetricItem.jsx index c3149b62a0b07039cc9835d003d4fbfc64dfdcdf..fa8ce0053fcab6d515d178cad42bd285d34eb43e 100644 --- a/frontend/src/metabase/admin/datamodel/components/database/MetricItem.jsx +++ b/frontend/src/metabase/admin/datamodel/components/database/MetricItem.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ObjectActionSelect from "../ObjectActionSelect.jsx"; diff --git a/frontend/src/metabase/admin/datamodel/components/database/MetricsList.jsx b/frontend/src/metabase/admin/datamodel/components/database/MetricsList.jsx index 15197ce4b1abe022cfb6aae92163e672bf90f908..b2b0452db84b39e864ac32be2baee3615a13893e 100644 --- a/frontend/src/metabase/admin/datamodel/components/database/MetricsList.jsx +++ b/frontend/src/metabase/admin/datamodel/components/database/MetricsList.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { Link } from "react-router"; import MetricItem from "./MetricItem.jsx"; diff --git a/frontend/src/metabase/admin/datamodel/components/database/SegmentItem.jsx b/frontend/src/metabase/admin/datamodel/components/database/SegmentItem.jsx index 85afc4e7c26d9d073748bc3f5c2228bfb0b0377c..529d3b5e9377d526ab7e348cd3abfb913dc02e0e 100644 --- a/frontend/src/metabase/admin/datamodel/components/database/SegmentItem.jsx +++ b/frontend/src/metabase/admin/datamodel/components/database/SegmentItem.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ObjectActionSelect from "../ObjectActionSelect.jsx"; diff --git a/frontend/src/metabase/admin/datamodel/components/database/SegmentsList.jsx b/frontend/src/metabase/admin/datamodel/components/database/SegmentsList.jsx index f6c7e032a3969a49d7a12739ab4704aa09a651a6..4035b6bd5fac66e54c689f27434adb15dace9ecc 100644 --- a/frontend/src/metabase/admin/datamodel/components/database/SegmentsList.jsx +++ b/frontend/src/metabase/admin/datamodel/components/database/SegmentsList.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { Link } from "react-router"; import SegmentItem from "./SegmentItem.jsx"; diff --git a/frontend/src/metabase/admin/datamodel/components/revisions/QueryDiff.jsx b/frontend/src/metabase/admin/datamodel/components/revisions/QueryDiff.jsx index fecf6b6ff1530875af886e2af55268b138402503..9d6e29ef3e947177df5ec253b4604fa7e0543568 100644 --- a/frontend/src/metabase/admin/datamodel/components/revisions/QueryDiff.jsx +++ b/frontend/src/metabase/admin/datamodel/components/revisions/QueryDiff.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx"; diff --git a/frontend/src/metabase/admin/datamodel/components/revisions/Revision.jsx b/frontend/src/metabase/admin/datamodel/components/revisions/Revision.jsx index 9a2e79460fa089607ff78faae67cb34199bbd578..7a5149b28ac10d51ccc14854ace086261c4df028 100644 --- a/frontend/src/metabase/admin/datamodel/components/revisions/Revision.jsx +++ b/frontend/src/metabase/admin/datamodel/components/revisions/Revision.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import RevisionDiff from "./RevisionDiff.jsx"; diff --git a/frontend/src/metabase/admin/datamodel/components/revisions/RevisionDiff.jsx b/frontend/src/metabase/admin/datamodel/components/revisions/RevisionDiff.jsx index 6d0a0fef8da69e575c7f78b3469828da23d333c8..410bab3eab61013192e072fc21890bc8848f3813 100644 --- a/frontend/src/metabase/admin/datamodel/components/revisions/RevisionDiff.jsx +++ b/frontend/src/metabase/admin/datamodel/components/revisions/RevisionDiff.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import TextDiff from "./TextDiff.jsx"; import QueryDiff from "./QueryDiff.jsx"; diff --git a/frontend/src/metabase/admin/datamodel/components/revisions/RevisionHistory.jsx b/frontend/src/metabase/admin/datamodel/components/revisions/RevisionHistory.jsx index 08dafb3bd6a231978af2cf9b2ad5ebe29e7464ea..69535b8e52765be0b97c45660f4904a62cb167bc 100644 --- a/frontend/src/metabase/admin/datamodel/components/revisions/RevisionHistory.jsx +++ b/frontend/src/metabase/admin/datamodel/components/revisions/RevisionHistory.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import Revision from "./Revision.jsx"; diff --git a/frontend/src/metabase/admin/datamodel/components/revisions/TextDiff.jsx b/frontend/src/metabase/admin/datamodel/components/revisions/TextDiff.jsx index 922a0ad0fe7b372bc4cf18158b6f2099d9251c80..507eb67c54bbfc3c2a8ea3f390fd845cc16a5d3a 100644 --- a/frontend/src/metabase/admin/datamodel/components/revisions/TextDiff.jsx +++ b/frontend/src/metabase/admin/datamodel/components/revisions/TextDiff.jsx @@ -1,6 +1,7 @@ /*eslint-env node */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { diffWords } from 'diff'; diff --git a/frontend/src/metabase/admin/datamodel/containers/MetadataEditorApp.jsx b/frontend/src/metabase/admin/datamodel/containers/MetadataEditorApp.jsx index 5c96a1335d54fbfaa1dfaa3dce172d5a71d5194f..cfba3ebbf3a95950eed261532a2437ad8cab0914 100644 --- a/frontend/src/metabase/admin/datamodel/containers/MetadataEditorApp.jsx +++ b/frontend/src/metabase/admin/datamodel/containers/MetadataEditorApp.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { connect } from "react-redux"; import _ from "underscore"; diff --git a/frontend/src/metabase/admin/datamodel/containers/MetricApp.jsx b/frontend/src/metabase/admin/datamodel/containers/MetricApp.jsx index a8f4cf5ad2230b39a140eb214cda0dc81e5df19a..19707a34cde6dcc1dc5957c6b4377cae1a0624a8 100644 --- a/frontend/src/metabase/admin/datamodel/containers/MetricApp.jsx +++ b/frontend/src/metabase/admin/datamodel/containers/MetricApp.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { connect } from "react-redux"; import { push } from "react-router-redux"; diff --git a/frontend/src/metabase/admin/datamodel/containers/MetricForm.jsx b/frontend/src/metabase/admin/datamodel/containers/MetricForm.jsx index 5ad0b08922d20b80bc018b316d59c9e614e54d6e..2029c47fa6eb6cbc00556513d591da5a5c6db990 100644 --- a/frontend/src/metabase/admin/datamodel/containers/MetricForm.jsx +++ b/frontend/src/metabase/admin/datamodel/containers/MetricForm.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { Link } from "react-router"; import FormLabel from "../components/FormLabel.jsx"; diff --git a/frontend/src/metabase/admin/datamodel/containers/RevisionHistoryApp.jsx b/frontend/src/metabase/admin/datamodel/containers/RevisionHistoryApp.jsx index d5e2f1d2aeb00498fca0f6c29a01117ad0b92b09..bf4043b2a553838b40ebf49b8466f27d64e5664f 100644 --- a/frontend/src/metabase/admin/datamodel/containers/RevisionHistoryApp.jsx +++ b/frontend/src/metabase/admin/datamodel/containers/RevisionHistoryApp.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { connect } from "react-redux"; import RevisionHistory from "../components/revisions/RevisionHistory.jsx"; diff --git a/frontend/src/metabase/admin/datamodel/containers/SegmentApp.jsx b/frontend/src/metabase/admin/datamodel/containers/SegmentApp.jsx index 735eca3e2d7a1b8dca6893ae483e8f74c1f49a1f..a4fbba0163f443aa6640a30aa9fa8c4edfc1d1f6 100644 --- a/frontend/src/metabase/admin/datamodel/containers/SegmentApp.jsx +++ b/frontend/src/metabase/admin/datamodel/containers/SegmentApp.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { connect } from "react-redux"; import { push } from "react-router-redux"; diff --git a/frontend/src/metabase/admin/datamodel/containers/SegmentForm.jsx b/frontend/src/metabase/admin/datamodel/containers/SegmentForm.jsx index cd99d32ff2475fa2f0fc75353fed4ee827892ea7..9f5bf96c06559e8241ebdc6d39d9284aeed11e54 100644 --- a/frontend/src/metabase/admin/datamodel/containers/SegmentForm.jsx +++ b/frontend/src/metabase/admin/datamodel/containers/SegmentForm.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { Link } from "react-router"; import FormLabel from "../components/FormLabel.jsx"; diff --git a/frontend/src/metabase/admin/people/components/AddRow.jsx b/frontend/src/metabase/admin/people/components/AddRow.jsx index a33568fae6a93673cada34e8449f010c331855fe..e0a6a58ba289d4af3488199664ead59866a35334 100644 --- a/frontend/src/metabase/admin/people/components/AddRow.jsx +++ b/frontend/src/metabase/admin/people/components/AddRow.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; import cx from "classnames"; diff --git a/frontend/src/metabase/admin/people/components/EditUserForm.jsx b/frontend/src/metabase/admin/people/components/EditUserForm.jsx index 6ae4e222ce3c41560fb725b13666bb59d8a65f09..b5280a9980cb029908a7cdc911e29f7aacaaa364 100644 --- a/frontend/src/metabase/admin/people/components/EditUserForm.jsx +++ b/frontend/src/metabase/admin/people/components/EditUserForm.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ReactDOM from "react-dom"; import FormField from "metabase/components/form/FormField.jsx"; diff --git a/frontend/src/metabase/admin/people/components/GroupSelect.jsx b/frontend/src/metabase/admin/people/components/GroupSelect.jsx index 69815705a72fe7ba2c761c568ffa1de19b1e8970..e92435ef8fdabb1962dd60072e1d38c5cdde7e36 100644 --- a/frontend/src/metabase/admin/people/components/GroupSelect.jsx +++ b/frontend/src/metabase/admin/people/components/GroupSelect.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; import CheckBox from "metabase/components/CheckBox.jsx"; diff --git a/frontend/src/metabase/admin/people/components/GroupSummary.jsx b/frontend/src/metabase/admin/people/components/GroupSummary.jsx index 1ed9e3d643297607e38f50a07cf0428d5b1bc661..13e9a22e024d1db7dc57e3d293815056c4a208f6 100644 --- a/frontend/src/metabase/admin/people/components/GroupSummary.jsx +++ b/frontend/src/metabase/admin/people/components/GroupSummary.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; import _ from "underscore"; diff --git a/frontend/src/metabase/admin/people/components/UserActionsSelect.jsx b/frontend/src/metabase/admin/people/components/UserActionsSelect.jsx index 7d6108a84987472e51b0e11d5024cea1354a1eb2..ce88849accf75385b4c829d38f18185acbd48d3e 100644 --- a/frontend/src/metabase/admin/people/components/UserActionsSelect.jsx +++ b/frontend/src/metabase/admin/people/components/UserActionsSelect.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import Icon from "metabase/components/Icon.jsx"; import MetabaseSettings from "metabase/lib/settings"; diff --git a/frontend/src/metabase/admin/people/components/UserGroupSelect.jsx b/frontend/src/metabase/admin/people/components/UserGroupSelect.jsx index cffd89efa0c5eba3570398618284fdbf4941b01e..b8e835c5b9426b60a90a541d2ae380c7b2063d86 100644 --- a/frontend/src/metabase/admin/people/components/UserGroupSelect.jsx +++ b/frontend/src/metabase/admin/people/components/UserGroupSelect.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import cx from "classnames"; import Icon from "metabase/components/Icon.jsx"; diff --git a/frontend/src/metabase/admin/people/containers/AdminPeopleApp.jsx b/frontend/src/metabase/admin/people/containers/AdminPeopleApp.jsx index 1ea8e2fc98dc151b9a1e4a87a0cb41b4eda08cb3..c08dd3678f592853a466233639d2a949813b3af2 100644 --- a/frontend/src/metabase/admin/people/containers/AdminPeopleApp.jsx +++ b/frontend/src/metabase/admin/people/containers/AdminPeopleApp.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { LeftNavPane, LeftNavPaneItem } from "metabase/components/LeftNavPane.jsx"; diff --git a/frontend/src/metabase/admin/people/containers/PeopleListingApp.jsx b/frontend/src/metabase/admin/people/containers/PeopleListingApp.jsx index c306b66c19fb0e0f3bea3f54a890a94e2199f618..408f9c77a93b6fc7f8c1f0696d44813d9f1a327a 100644 --- a/frontend/src/metabase/admin/people/containers/PeopleListingApp.jsx +++ b/frontend/src/metabase/admin/people/containers/PeopleListingApp.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { Link } from "react-router"; import _ from "underscore"; import { connect } from "react-redux"; diff --git a/frontend/src/metabase/admin/permissions/components/FixedHeaderGrid.jsx b/frontend/src/metabase/admin/permissions/components/FixedHeaderGrid.jsx index 4de3c687ea7ff17a5409dc65e245637aea597133..205a6e09307b50a56cac6eca799970919bbe509a 100644 --- a/frontend/src/metabase/admin/permissions/components/FixedHeaderGrid.jsx +++ b/frontend/src/metabase/admin/permissions/components/FixedHeaderGrid.jsx @@ -1,6 +1,6 @@ /* eslint-disable react/display-name */ -import React, { Component, PropTypes } from "react"; +import React from "react"; import { Grid, ScrollSync } from 'react-virtualized' import 'react-virtualized/styles.css'; diff --git a/frontend/src/metabase/admin/permissions/components/PermissionsConfirm.jsx b/frontend/src/metabase/admin/permissions/components/PermissionsConfirm.jsx index 92abdcaa9d67acb0070b8a0b327f8919b0f9c377..782974f644d889eedac58d2e2e99e8964be21b04 100644 --- a/frontend/src/metabase/admin/permissions/components/PermissionsConfirm.jsx +++ b/frontend/src/metabase/admin/permissions/components/PermissionsConfirm.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; import { inflect } from "metabase/lib/formatting"; diff --git a/frontend/src/metabase/admin/permissions/components/PermissionsEditor.jsx b/frontend/src/metabase/admin/permissions/components/PermissionsEditor.jsx index b9ad99a77e491846b8c54f50b04935720093326c..a35dcf3310070bee770ce208458f2152e35fc334 100644 --- a/frontend/src/metabase/admin/permissions/components/PermissionsEditor.jsx +++ b/frontend/src/metabase/admin/permissions/components/PermissionsEditor.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx"; import Confirm from "metabase/components/Confirm.jsx"; diff --git a/frontend/src/metabase/admin/permissions/components/PermissionsGrid.jsx b/frontend/src/metabase/admin/permissions/components/PermissionsGrid.jsx index cad0652cc54cdfe12f3b0daa55d499184f5170f1..5e9dfe6bf15f38afaba310f621b71212412d480a 100644 --- a/frontend/src/metabase/admin/permissions/components/PermissionsGrid.jsx +++ b/frontend/src/metabase/admin/permissions/components/PermissionsGrid.jsx @@ -1,6 +1,6 @@ /* eslint-disable react/display-name */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { Link } from "react-router"; diff --git a/frontend/src/metabase/admin/permissions/containers/CollectionsPermissionsApp.jsx b/frontend/src/metabase/admin/permissions/containers/CollectionsPermissionsApp.jsx index 7e182b4de13545392ae959fe239ba70bbae08d33..585d002b56e20fe08ef0296bb77d8f18f191b215 100644 --- a/frontend/src/metabase/admin/permissions/containers/CollectionsPermissionsApp.jsx +++ b/frontend/src/metabase/admin/permissions/containers/CollectionsPermissionsApp.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { connect } from "react-redux"; import PermissionsEditor from "../components/PermissionsEditor.jsx"; diff --git a/frontend/src/metabase/admin/permissions/containers/DataPermissionsApp.jsx b/frontend/src/metabase/admin/permissions/containers/DataPermissionsApp.jsx index 823d6a46f979650ed70e1d3fe77bfd92de05dd3e..9cc8f3a18c0dce61951dfaea543326867ff59f81 100644 --- a/frontend/src/metabase/admin/permissions/containers/DataPermissionsApp.jsx +++ b/frontend/src/metabase/admin/permissions/containers/DataPermissionsApp.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { connect } from "react-redux" import PermissionsApp from "./PermissionsApp.jsx"; diff --git a/frontend/src/metabase/admin/permissions/containers/DatabasesPermissionsApp.jsx b/frontend/src/metabase/admin/permissions/containers/DatabasesPermissionsApp.jsx index 3614f63a52fd6e57563889c068888c011983a19a..6a58c07a5fe1d73effdc9789f4074c11db4a59a6 100644 --- a/frontend/src/metabase/admin/permissions/containers/DatabasesPermissionsApp.jsx +++ b/frontend/src/metabase/admin/permissions/containers/DatabasesPermissionsApp.jsx @@ -1,4 +1,3 @@ -import React, { Component, PropTypes } from "react"; import { connect } from "react-redux"; import PermissionsEditor from "../components/PermissionsEditor.jsx"; diff --git a/frontend/src/metabase/admin/permissions/containers/PermissionsApp.jsx b/frontend/src/metabase/admin/permissions/containers/PermissionsApp.jsx index f4afd2bb54eab5cc306ef6240d03de9faeec367c..47bb661672719eec9fc4ef0c9ce99e1f0903daac 100644 --- a/frontend/src/metabase/admin/permissions/containers/PermissionsApp.jsx +++ b/frontend/src/metabase/admin/permissions/containers/PermissionsApp.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { withRouter } from "react-router"; import { connect } from "react-redux" import { push } from "react-router-redux"; diff --git a/frontend/src/metabase/admin/permissions/containers/SchemasPermissionsApp.jsx b/frontend/src/metabase/admin/permissions/containers/SchemasPermissionsApp.jsx index fdcbed3a8ebed55bb3a809f82a1147838779a6d0..744ed2d9e55e3d6af448a665565c8b15c07af414 100644 --- a/frontend/src/metabase/admin/permissions/containers/SchemasPermissionsApp.jsx +++ b/frontend/src/metabase/admin/permissions/containers/SchemasPermissionsApp.jsx @@ -1,4 +1,3 @@ -import React, { Component, PropTypes } from "react"; import { connect } from "react-redux"; import PermissionsEditor from "../components/PermissionsEditor.jsx"; diff --git a/frontend/src/metabase/admin/permissions/containers/TablesPermissionsApp.jsx b/frontend/src/metabase/admin/permissions/containers/TablesPermissionsApp.jsx index 43216ff56a16ccb9502d0c4abdb33be51c707905..31ad4e730a4f722437a4e0979d83cb16e64499e6 100644 --- a/frontend/src/metabase/admin/permissions/containers/TablesPermissionsApp.jsx +++ b/frontend/src/metabase/admin/permissions/containers/TablesPermissionsApp.jsx @@ -1,4 +1,3 @@ -import React, { Component, PropTypes } from "react"; import { connect } from "react-redux"; import PermissionsEditor from "../components/PermissionsEditor.jsx"; diff --git a/frontend/src/metabase/admin/permissions/routes.jsx b/frontend/src/metabase/admin/permissions/routes.jsx index 2b976fe0dcb8495870b2d628f7e8ce956dfd51cb..47308bb5699934df97207a1de4e079202407ddd8 100644 --- a/frontend/src/metabase/admin/permissions/routes.jsx +++ b/frontend/src/metabase/admin/permissions/routes.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; import { Route, IndexRedirect } from 'react-router'; import DataPermissionsApp from "./containers/DataPermissionsApp.jsx"; diff --git a/frontend/src/metabase/admin/permissions/selectors.js b/frontend/src/metabase/admin/permissions/selectors.js index 5d188ddb0dac335ee6fbd87be4257bb3b0db0901..85510bee59995fd0d021deae11d1dfd3e9201eef 100644 --- a/frontend/src/metabase/admin/permissions/selectors.js +++ b/frontend/src/metabase/admin/permissions/selectors.js @@ -43,7 +43,7 @@ const SPECIAL_GROUP_FILTERS = [isAdminGroup, isDefaultGroup, isMetaBotGroup].rev function getTooltipForGroup(group) { if (isAdminGroup(group)) { - return "Administrators always have the highest level of acess to everything in Metabase." + return "Administrators always have the highest level of access to everything in Metabase." } else if (isDefaultGroup(group)) { return "Every Metabase user belongs to the All Users group. If you want to limit or restrict a group's access to something, make sure the All Users group has an equal or lower level of access."; } else if (isMetaBotGroup(group)) { diff --git a/frontend/src/metabase/admin/settings/components/SettingsEmailForm.jsx b/frontend/src/metabase/admin/settings/components/SettingsEmailForm.jsx index f3f4451b123ead08a4747a32f6ec992f3b5f63db..de3639693cb4af2697575517e404cb079693df46 100644 --- a/frontend/src/metabase/admin/settings/components/SettingsEmailForm.jsx +++ b/frontend/src/metabase/admin/settings/components/SettingsEmailForm.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import cx from "classnames"; import _ from "underscore"; diff --git a/frontend/src/metabase/admin/settings/components/SettingsSetting.jsx b/frontend/src/metabase/admin/settings/components/SettingsSetting.jsx index a1827f2f27765897d54d3485191b0f78d44f3fbf..8e5498b759b80f4d52b3b19588a7084c33d1ccd3 100644 --- a/frontend/src/metabase/admin/settings/components/SettingsSetting.jsx +++ b/frontend/src/metabase/admin/settings/components/SettingsSetting.jsx @@ -1,8 +1,10 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import SettingHeader from "./SettingHeader.jsx"; import SettingInput from "./widgets/SettingInput.jsx"; +import SettingNumber from "./widgets/SettingNumber.jsx"; import SettingPassword from "./widgets/SettingPassword.jsx"; import SettingRadio from "./widgets/SettingRadio.jsx"; import SettingToggle from "./widgets/SettingToggle.jsx"; @@ -10,6 +12,7 @@ import SettingSelect from "./widgets/SettingSelect.jsx"; const SETTING_WIDGET_MAP = { "string": SettingInput, + "number": SettingNumber, "password": SettingPassword, "select": SettingSelect, "radio": SettingRadio, diff --git a/frontend/src/metabase/admin/settings/components/SettingsSetupList.jsx b/frontend/src/metabase/admin/settings/components/SettingsSetupList.jsx index 111727d889a93f047e8b7861256fe1d3967b3c8a..b6b58ff722f001194d5e491afccd265dc3065511 100644 --- a/frontend/src/metabase/admin/settings/components/SettingsSetupList.jsx +++ b/frontend/src/metabase/admin/settings/components/SettingsSetupList.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { Link } from "react-router"; import Icon from "metabase/components/Icon.jsx"; import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx"; diff --git a/frontend/src/metabase/admin/settings/components/SettingsSingleSignOnForm.jsx b/frontend/src/metabase/admin/settings/components/SettingsSingleSignOnForm.jsx index c6606d9d0f325ff97e2f4c5290255b44de98bfd6..722f5e81bc205c08ffaca4f30672101cae902c66 100644 --- a/frontend/src/metabase/admin/settings/components/SettingsSingleSignOnForm.jsx +++ b/frontend/src/metabase/admin/settings/components/SettingsSingleSignOnForm.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import cx from "classnames"; import _ from "underscore"; diff --git a/frontend/src/metabase/admin/settings/components/SettingsSlackForm.jsx b/frontend/src/metabase/admin/settings/components/SettingsSlackForm.jsx index ba5fe3c053b3f9f6fe54abe5ea33048a2e0c41b4..fb111c719574dc4977fa6a391735cc6b18ef4e56 100644 --- a/frontend/src/metabase/admin/settings/components/SettingsSlackForm.jsx +++ b/frontend/src/metabase/admin/settings/components/SettingsSlackForm.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import MetabaseAnalytics from "metabase/lib/analytics"; import MetabaseUtils from "metabase/lib/utils"; diff --git a/frontend/src/metabase/admin/settings/components/SettingsUpdatesForm.jsx b/frontend/src/metabase/admin/settings/components/SettingsUpdatesForm.jsx index bb27567c535793e72027d9e4c57beb5da0d28ce3..1b137d567f2aaa9a04b20395e11a6ff6d883ec87 100644 --- a/frontend/src/metabase/admin/settings/components/SettingsUpdatesForm.jsx +++ b/frontend/src/metabase/admin/settings/components/SettingsUpdatesForm.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import MetabaseSettings from "metabase/lib/settings"; import MetabaseUtils from "metabase/lib/utils"; diff --git a/frontend/src/metabase/admin/settings/components/widgets/CustomGeoJSONWidget.jsx b/frontend/src/metabase/admin/settings/components/widgets/CustomGeoJSONWidget.jsx index c36cda10147a25d18ea928c93a7f4d1a30226093..5308fcd1cd408de0d28ea7ed4a9d872750b7403c 100644 --- a/frontend/src/metabase/admin/settings/components/widgets/CustomGeoJSONWidget.jsx +++ b/frontend/src/metabase/admin/settings/components/widgets/CustomGeoJSONWidget.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import Utils from "metabase/lib/utils"; diff --git a/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx b/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx index 109b043fa9f1bfe3e9a8295e6c5472340a6a0366..892ba549ca6aeb38110a651b9fa711153f6b7e2c 100644 --- a/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx +++ b/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import Icon from "metabase/components/Icon"; import Link from "metabase/components/Link"; @@ -9,7 +9,7 @@ import Confirm from "metabase/components/Confirm"; import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper"; import { CardApi, DashboardApi } from "metabase/services"; -import Urls from "metabase/lib/urls"; +import * as Urls from "metabase/lib/urls"; import MetabaseAnalytics from "metabase/lib/analytics"; @@ -161,7 +161,7 @@ export const PublicLinksQuestionListing = () => load={CardApi.listPublic} revoke={CardApi.deletePublicLink} type='Public Card Listing' - getUrl={({ id }) => Urls.card(id)} + getUrl={({ id }) => Urls.question(id)} getPublicUrl={({ public_uuid }) => window.location.origin + Urls.publicCard(public_uuid)} noLinksMessage="No questions have been publicly shared yet." />; @@ -177,7 +177,7 @@ export const EmbeddedDashboardListing = () => export const EmbeddedQuestionListing = () => <PublicLinksListing load={CardApi.listEmbeddable} - getUrl={({ id }) => Urls.card(id)} + getUrl={({ id }) => Urls.question(id)} type='Embedded Card Listing' noLinksMessage="No questions have been embedded yet." />; diff --git a/frontend/src/metabase/admin/settings/components/widgets/SecretKeyWidget.jsx b/frontend/src/metabase/admin/settings/components/widgets/SecretKeyWidget.jsx index c4b066484f8f74a17a5a754d994146b8e81686c1..192e2b305c0ac13f82a81cd5db29eaf93175c8ad 100644 --- a/frontend/src/metabase/admin/settings/components/widgets/SecretKeyWidget.jsx +++ b/frontend/src/metabase/admin/settings/components/widgets/SecretKeyWidget.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import SettingInput from "./SettingInput"; import Button from "metabase/components/Button"; diff --git a/frontend/src/metabase/admin/settings/components/widgets/SettingNumber.jsx b/frontend/src/metabase/admin/settings/components/widgets/SettingNumber.jsx new file mode 100644 index 0000000000000000000000000000000000000000..05fd4da0445808f4c7febc128deb46da6df01bd0 --- /dev/null +++ b/frontend/src/metabase/admin/settings/components/widgets/SettingNumber.jsx @@ -0,0 +1,8 @@ +import React from "react"; + +import SettingInput from "./SettingInput"; + +const SettingNumber = ({ type = "number", ...props }) => + <SettingInput {...props} type="number" /> + +export default SettingNumber; diff --git a/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx b/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx index 90d818639cda26de2b30555145fa6625199a9307..0928a1420d60e83ec6c58c92eec2ed837016f83b 100644 --- a/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx +++ b/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { Link } from "react-router"; import { connect } from "react-redux"; import MetabaseAnalytics from "metabase/lib/analytics"; diff --git a/frontend/src/metabase/admin/settings/selectors.js b/frontend/src/metabase/admin/settings/selectors.js index 1f620e2ba88a90be895262ebfe397f88c5861871..ae501e697ee409d3393db53d75673754fa40a973 100644 --- a/frontend/src/metabase/admin/settings/selectors.js +++ b/frontend/src/metabase/admin/settings/selectors.js @@ -86,7 +86,7 @@ const SECTIONS = [ key: "email-smtp-port", display_name: "SMTP Port", placeholder: "587", - type: "string", + type: "number", required: true, validations: [["integer", "That's not a valid port number"]] }, @@ -236,6 +236,34 @@ const SECTIONS = [ getHidden: (settings) => !settings["enable-embedding"] } ] + }, + { + name: "Caching", + settings: [ + { + key: "enable-query-caching", + display_name: "Enable Caching", + type: "boolean" + }, + { + key: "query-caching-min-ttl", + display_name: "Minimum Query Duration", + type: "number", + getHidden: (settings) => !settings["enable-query-caching"] + }, + { + key: "query-caching-ttl-ratio", + display_name: "Cache Time-To-Live (TTL)", + type: "number", + getHidden: (settings) => !settings["enable-query-caching"] + }, + { + key: "query-caching-max-kb", + display_name: "Max Cache Entry Size", + type: "number", + getHidden: (settings) => !settings["enable-query-caching"] + } + ] } ]; for (const section of SECTIONS) { diff --git a/frontend/src/metabase/auth/components/AuthScene.jsx b/frontend/src/metabase/auth/components/AuthScene.jsx index 21f84fb783316a65c43091650b8b189574f48c33..5a00cebce7863e68a0aa865cab3eea906ad2dea8 100644 --- a/frontend/src/metabase/auth/components/AuthScene.jsx +++ b/frontend/src/metabase/auth/components/AuthScene.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import ReactDOM from "react-dom"; export default class AuthScene extends Component { diff --git a/frontend/src/metabase/auth/components/SSOLoginButton.jsx b/frontend/src/metabase/auth/components/SSOLoginButton.jsx index 91cb3b72fd66e0756302b241495280754fb3c10a..7af959a1a02d070b58eff7922fd55ba64da5dbe7 100644 --- a/frontend/src/metabase/auth/components/SSOLoginButton.jsx +++ b/frontend/src/metabase/auth/components/SSOLoginButton.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import Icon from "metabase/components/Icon"; import { capitalize } from 'humanize' diff --git a/frontend/src/metabase/auth/containers/ForgotPasswordApp.jsx b/frontend/src/metabase/auth/containers/ForgotPasswordApp.jsx index f9bce741aa031cbd4314e70fe1de1c7424ff8e1e..0c51a8a90fd448a31f906868fdfd9e6400f60e94 100644 --- a/frontend/src/metabase/auth/containers/ForgotPasswordApp.jsx +++ b/frontend/src/metabase/auth/containers/ForgotPasswordApp.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import _ from "underscore"; import cx from "classnames"; diff --git a/frontend/src/metabase/auth/containers/LoginApp.jsx b/frontend/src/metabase/auth/containers/LoginApp.jsx index 1223d3c25c1fbbb75ea1454a7770344a21bdb3eb..229febd771c0a21a04db540746c7fe5a041aeb16 100644 --- a/frontend/src/metabase/auth/containers/LoginApp.jsx +++ b/frontend/src/metabase/auth/containers/LoginApp.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { findDOMNode } from "react-dom"; import { Link } from "react-router"; import { connect } from "react-redux"; diff --git a/frontend/src/metabase/auth/containers/LogoutApp.jsx b/frontend/src/metabase/auth/containers/LogoutApp.jsx index c7ac8276c83d8f19243087ed6910444156fc8b10..5c42f34868d43fecd1eaa9194ffba54d7e023899 100644 --- a/frontend/src/metabase/auth/containers/LogoutApp.jsx +++ b/frontend/src/metabase/auth/containers/LogoutApp.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import { Component } from "react"; import { connect } from "react-redux"; import { logout } from "../auth"; diff --git a/frontend/src/metabase/auth/containers/PasswordResetApp.jsx b/frontend/src/metabase/auth/containers/PasswordResetApp.jsx index 2db47592cacd1d7328c563f8123e158a8f0005d5..cc2aeb724fcd101d0d0801fc741633b02d011e2f 100644 --- a/frontend/src/metabase/auth/containers/PasswordResetApp.jsx +++ b/frontend/src/metabase/auth/containers/PasswordResetApp.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { connect } from "react-redux"; import { Link } from "react-router"; diff --git a/frontend/src/metabase/components/AccordianList.jsx b/frontend/src/metabase/components/AccordianList.jsx index 953be139c289362f20d0024ae4d7698ed6e4abd6..64fb9752e73736eab212b5571c88838179782517 100644 --- a/frontend/src/metabase/components/AccordianList.jsx +++ b/frontend/src/metabase/components/AccordianList.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ReactDOM from "react-dom"; import cx from "classnames"; diff --git a/frontend/src/metabase/components/ActionButton.jsx b/frontend/src/metabase/components/ActionButton.jsx index 465fd693c6b50dca5656caac3899d8587748f731..168d2c71a6ee66d8c4c2f64f4258cf2f1b677033 100644 --- a/frontend/src/metabase/components/ActionButton.jsx +++ b/frontend/src/metabase/components/ActionButton.jsx @@ -1,6 +1,7 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import Icon from "metabase/components/Icon"; import Button from "metabase/components/Button"; diff --git a/frontend/src/metabase/components/AddToDashSelectDashModal.jsx b/frontend/src/metabase/components/AddToDashSelectDashModal.jsx index 408cb5777523ee39bdb95e3a7bce27f553055a8e..64d6ef600c945e7326c547cbe7e48b15ff454663 100644 --- a/frontend/src/metabase/components/AddToDashSelectDashModal.jsx +++ b/frontend/src/metabase/components/AddToDashSelectDashModal.jsx @@ -1,11 +1,12 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import CreateDashboardModal from 'metabase/components/CreateDashboardModal.jsx'; import Icon from 'metabase/components/Icon.jsx'; import ModalContent from "metabase/components/ModalContent.jsx"; import SortableItemList from 'metabase/components/SortableItemList.jsx'; -import Urls from "metabase/lib/urls"; +import * as Urls from "metabase/lib/urls"; import { DashboardApi } from "metabase/services"; import moment from 'moment'; diff --git a/frontend/src/metabase/components/AdminEmptyText.jsx b/frontend/src/metabase/components/AdminEmptyText.jsx index 727b17f71e833f5d6aa322cf0c7a71c71d2e2095..a9aec2383bc565e9efa277e5aa95749c71accc1d 100644 --- a/frontend/src/metabase/components/AdminEmptyText.jsx +++ b/frontend/src/metabase/components/AdminEmptyText.jsx @@ -1,4 +1,5 @@ -import React, { PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; const AdminEmptyText = ({ message }) => <h2 className="text-grey-3">{message}</h2> diff --git a/frontend/src/metabase/components/AdminHeader.jsx b/frontend/src/metabase/components/AdminHeader.jsx index ea413d439a4022d02ea7047e91c934b65986804d..c2507bb5f740839c0ac767b9ecd1605944281748 100644 --- a/frontend/src/metabase/components/AdminHeader.jsx +++ b/frontend/src/metabase/components/AdminHeader.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import SaveStatus from "metabase/components/SaveStatus.jsx"; diff --git a/frontend/src/metabase/components/AdminLayout.jsx b/frontend/src/metabase/components/AdminLayout.jsx index 697281025d930647096ed184393ac795511608f8..8dff1b7fe84be3c8604837ec22c58ac6682e4cb2 100644 --- a/frontend/src/metabase/components/AdminLayout.jsx +++ b/frontend/src/metabase/components/AdminLayout.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import AdminHeader from "./AdminHeader.jsx"; diff --git a/frontend/src/metabase/components/AdminPaneLayout.jsx b/frontend/src/metabase/components/AdminPaneLayout.jsx index 5709b31eaa3c50ceb943ff3c75bb2b6cabc3ae88..11cae74b5d1275776ec5685679cf9012c76b9e9e 100644 --- a/frontend/src/metabase/components/AdminPaneLayout.jsx +++ b/frontend/src/metabase/components/AdminPaneLayout.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; import cx from "classnames"; diff --git a/frontend/src/metabase/components/Alert.jsx b/frontend/src/metabase/components/Alert.jsx index c9ee25c3bb51f163be88e7b620402ddbaa5d4932..5fc912ca84b1d3c5953395b223ccdc4240f79942 100644 --- a/frontend/src/metabase/components/Alert.jsx +++ b/frontend/src/metabase/components/Alert.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; import Modal from "metabase/components/Modal.jsx"; diff --git a/frontend/src/metabase/components/BodyComponent.jsx b/frontend/src/metabase/components/BodyComponent.jsx index 11de27256f1648df07a49c7c7877c9939f7bb463..b14e7b8e15f19e8ed062c510fdea0d621340068a 100644 --- a/frontend/src/metabase/components/BodyComponent.jsx +++ b/frontend/src/metabase/components/BodyComponent.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import ReactDOM from "react-dom"; export default ComposedComponent => class extends Component { diff --git a/frontend/src/metabase/components/Breadcrumbs.jsx b/frontend/src/metabase/components/Breadcrumbs.jsx index 9931219ba4dfdf8165eda1bf1df3d655f1706597..c4b10537b02b798f920b970f3d8396baaea053e4 100644 --- a/frontend/src/metabase/components/Breadcrumbs.jsx +++ b/frontend/src/metabase/components/Breadcrumbs.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { Link } from "react-router"; import S from "./Breadcrumbs.css"; diff --git a/frontend/src/metabase/components/Button.jsx b/frontend/src/metabase/components/Button.jsx index bb8eb9f5a383b4de62596efba06a1cdd2cd49c6d..0bea4d0c9481528eaf488483b25c141736e0e98b 100644 --- a/frontend/src/metabase/components/Button.jsx +++ b/frontend/src/metabase/components/Button.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import Icon from "metabase/components/Icon.jsx"; diff --git a/frontend/src/metabase/components/ButtonBar.jsx b/frontend/src/metabase/components/ButtonBar.jsx index 851689df07061ce19a98534a511009e6d6dfad69..24381f8eac5e5cba064f43e3386aa5ebf0a3924d 100644 --- a/frontend/src/metabase/components/ButtonBar.jsx +++ b/frontend/src/metabase/components/ButtonBar.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; export default class ButtonBar extends Component { diff --git a/frontend/src/metabase/components/Calendar.jsx b/frontend/src/metabase/components/Calendar.jsx index d3b16c32f139ff32148120ca6c12c32ab8d95f2b..6715dcb129dcaeca413aceed6cd914540f0a5fcd 100644 --- a/frontend/src/metabase/components/Calendar.jsx +++ b/frontend/src/metabase/components/Calendar.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from "prop-types"; import "./Calendar.css"; diff --git a/frontend/src/metabase/components/CheckBox.jsx b/frontend/src/metabase/components/CheckBox.jsx index c22ec977a378c7354ef58064e2071c292efe8bdd..d215052027bd764feb8d3a3ab2b567a258e71330 100644 --- a/frontend/src/metabase/components/CheckBox.jsx +++ b/frontend/src/metabase/components/CheckBox.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import Icon from "metabase/components/Icon.jsx"; import cx from "classnames"; diff --git a/frontend/src/metabase/components/Code.jsx b/frontend/src/metabase/components/Code.jsx index 53364f56b49edca86783e67d4688cb398e78819f..b262c0683aa68006f2d21e5ae860c850000db4da 100644 --- a/frontend/src/metabase/components/Code.jsx +++ b/frontend/src/metabase/components/Code.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; const Code = ({ children, block }) => { if (block) { diff --git a/frontend/src/metabase/components/ColorPicker.jsx b/frontend/src/metabase/components/ColorPicker.jsx index 076c097e7c50f88d74a744f6c1a070fb9d6b571f..272ed65b5a832e4d058e540e9b4e0292e8f4fc41 100644 --- a/frontend/src/metabase/components/ColorPicker.jsx +++ b/frontend/src/metabase/components/ColorPicker.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import PopoverWithTrigger from "metabase/components/PopoverWithTrigger"; diff --git a/frontend/src/metabase/components/ColumnarSelector.jsx b/frontend/src/metabase/components/ColumnarSelector.jsx index 13512c863b4e221ed152963b8a4af8e0828e2167..e456a839cc886f7154ce7456763c69b23fc22361 100644 --- a/frontend/src/metabase/components/ColumnarSelector.jsx +++ b/frontend/src/metabase/components/ColumnarSelector.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import "./ColumnarSelector.css"; diff --git a/frontend/src/metabase/components/Confirm.jsx b/frontend/src/metabase/components/Confirm.jsx index 79e9733e599721befca7d101dd0b1a8ea8314480..ca10c75a0508f6dcbb9561392518f92f113737e5 100644 --- a/frontend/src/metabase/components/Confirm.jsx +++ b/frontend/src/metabase/components/Confirm.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ModalWithTrigger from "metabase/components/ModalWithTrigger.jsx"; import ConfirmContent from "./ConfirmContent.jsx"; diff --git a/frontend/src/metabase/components/ConfirmContent.jsx b/frontend/src/metabase/components/ConfirmContent.jsx index 984d420db23c298abd2da2dc6f37e965ceb11dea..64234914d058b63f3099b71969c68dffbd572f19 100644 --- a/frontend/src/metabase/components/ConfirmContent.jsx +++ b/frontend/src/metabase/components/ConfirmContent.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; import ModalContent from "metabase/components/ModalContent.jsx"; diff --git a/frontend/src/metabase/components/ConstrainToScreen.jsx b/frontend/src/metabase/components/ConstrainToScreen.jsx index 52b6956b27c9b6fa61041d17a42b8f0b656db450..354878bbc8ffb0e94c35b38c10e2e4e029facd70 100644 --- a/frontend/src/metabase/components/ConstrainToScreen.jsx +++ b/frontend/src/metabase/components/ConstrainToScreen.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import ReactDOM from "react-dom"; import { constrainToScreen } from "metabase/lib/dom"; diff --git a/frontend/src/metabase/components/CopyButton.jsx b/frontend/src/metabase/components/CopyButton.jsx index 7d2ef27b5f6d13f120d5a3870638a470d1c55365..76a51cbf5387694ebe4c529773908a2c75ff229a 100644 --- a/frontend/src/metabase/components/CopyButton.jsx +++ b/frontend/src/metabase/components/CopyButton.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import Icon from "metabase/components/Icon"; import Tooltip from "metabase/components/Tooltip"; diff --git a/frontend/src/metabase/components/CopyWidget.jsx b/frontend/src/metabase/components/CopyWidget.jsx index 7006b591f9bd5dde63acd68e9fae73a5c9e2e2bf..01a3b4854073bfe0fb3e9482b543fbd84020f392 100644 --- a/frontend/src/metabase/components/CopyWidget.jsx +++ b/frontend/src/metabase/components/CopyWidget.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import CopyButton from "./CopyButton"; diff --git a/frontend/src/metabase/components/CreateDashboardModal.jsx b/frontend/src/metabase/components/CreateDashboardModal.jsx index e39425df144470eefa1301fc06f55246d34c542f..58a9141cf05ef3c22005be481589442a72ed90c4 100644 --- a/frontend/src/metabase/components/CreateDashboardModal.jsx +++ b/frontend/src/metabase/components/CreateDashboardModal.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import FormField from "metabase/components/FormField.jsx"; import ModalContent from "metabase/components/ModalContent.jsx"; diff --git a/frontend/src/metabase/components/DatabaseDetailsForm.jsx b/frontend/src/metabase/components/DatabaseDetailsForm.jsx index 8a8e26de7835b6fdb06a8f1fb372d5e48fcf6306..65bf744eafab904554bfd95945ef94c3b32ba8ee 100644 --- a/frontend/src/metabase/components/DatabaseDetailsForm.jsx +++ b/frontend/src/metabase/components/DatabaseDetailsForm.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import cx from "classnames"; import FormField from "metabase/components/form/FormField.jsx"; diff --git a/frontend/src/metabase/components/DeleteModalWithConfirm.jsx b/frontend/src/metabase/components/DeleteModalWithConfirm.jsx index d4380609900deb313087c0b3b025221dff9d78dc..a0009f137748a89d48364b794b62928056de4639 100644 --- a/frontend/src/metabase/components/DeleteModalWithConfirm.jsx +++ b/frontend/src/metabase/components/DeleteModalWithConfirm.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ModalContent from "metabase/components/ModalContent.jsx"; import CheckBox from "metabase/components/CheckBox.jsx"; diff --git a/frontend/src/metabase/components/DeleteQuestionModal.jsx b/frontend/src/metabase/components/DeleteQuestionModal.jsx index 9f9977ba2ae972c05681d032d5adbf020c4478cf..510693b1fa80724a1a6b673a548934652c1737d1 100644 --- a/frontend/src/metabase/components/DeleteQuestionModal.jsx +++ b/frontend/src/metabase/components/DeleteQuestionModal.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ModalContent from "metabase/components/ModalContent.jsx"; diff --git a/frontend/src/metabase/components/DisclosureTriangle.jsx b/frontend/src/metabase/components/DisclosureTriangle.jsx index 25eba51de80fc7e33e620b9218bc1378b0e8f7c8..908286bc03a9af78187d1df2f414b59439cbee56 100644 --- a/frontend/src/metabase/components/DisclosureTriangle.jsx +++ b/frontend/src/metabase/components/DisclosureTriangle.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; import { Motion, spring, presets } from "react-motion"; import Icon from "metabase/components/Icon"; diff --git a/frontend/src/metabase/components/DownloadButton.jsx b/frontend/src/metabase/components/DownloadButton.jsx index 2095f254563529a1ecf8d76f3a6b300885a7edc1..e6d45c3871ef037dde4163b0de33369eb3dca6f5 100644 --- a/frontend/src/metabase/components/DownloadButton.jsx +++ b/frontend/src/metabase/components/DownloadButton.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import Button from "metabase/components/Button.jsx"; diff --git a/frontend/src/metabase/components/EditBar.jsx b/frontend/src/metabase/components/EditBar.jsx index 3069c0d8d88a771d6dc1a083cad165640a57e4c7..9f18730d62548d02446ed26b5456dcd0dd36c0ff 100644 --- a/frontend/src/metabase/components/EditBar.jsx +++ b/frontend/src/metabase/components/EditBar.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from "prop-types"; import cx from "classnames"; class EditBar extends Component { diff --git a/frontend/src/metabase/components/Ellipsified.jsx b/frontend/src/metabase/components/Ellipsified.jsx index a6c2718dd2e97ca0f5bdd6fcfc315238b47af4c6..5162a76a644084013b8193d39edb1667c89096a1 100644 --- a/frontend/src/metabase/components/Ellipsified.jsx +++ b/frontend/src/metabase/components/Ellipsified.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import ReactDOM from "react-dom"; import Tooltip from "metabase/components/Tooltip.jsx"; diff --git a/frontend/src/metabase/components/EmojiIcon.jsx b/frontend/src/metabase/components/EmojiIcon.jsx index 3256aeec840336c7d33df5b5d4fc09607ee8cc1d..fd1a0d0e23ed726da158a59de4f8a24fbeefed8a 100644 --- a/frontend/src/metabase/components/EmojiIcon.jsx +++ b/frontend/src/metabase/components/EmojiIcon.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import { emoji } from "metabase/lib/emoji"; diff --git a/frontend/src/metabase/components/EmptyState.jsx b/frontend/src/metabase/components/EmptyState.jsx index a2e16089ca21859f261bd2ee5637c9893b257bdc..323cdb10afc4b0c79251838e7ca1237144d6d3b8 100644 --- a/frontend/src/metabase/components/EmptyState.jsx +++ b/frontend/src/metabase/components/EmptyState.jsx @@ -1,4 +1,5 @@ -import React, { PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import { Link } from "react-router"; import Icon from "metabase/components/Icon.jsx"; diff --git a/frontend/src/metabase/components/Expandable.jsx b/frontend/src/metabase/components/Expandable.jsx index f921bde99352074b612009e367948fc4d638999b..256eeb20c99e0c29ae90bacfc7bd2833170d1c31 100644 --- a/frontend/src/metabase/components/Expandable.jsx +++ b/frontend/src/metabase/components/Expandable.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; const Expandable = (ComposedComponent) => class extends Component { static displayName = "Expandable["+(ComposedComponent.displayName || ComposedComponent.name)+"]"; diff --git a/frontend/src/metabase/components/ExplicitSize.jsx b/frontend/src/metabase/components/ExplicitSize.jsx index be7c380b4457ddf6107cabe9086ac709d0fc1969..9d10e95f74f14cef9c10ce22b77ebd63446d3ac1 100644 --- a/frontend/src/metabase/components/ExplicitSize.jsx +++ b/frontend/src/metabase/components/ExplicitSize.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import ReactDOM from "react-dom"; import ResizeObserver from "resize-observer-polyfill"; diff --git a/frontend/src/metabase/components/FieldSet.jsx b/frontend/src/metabase/components/FieldSet.jsx index d73e60dc5af0e35fab8494a6de0275e4173a75f7..2e787403e5aefdefb2385bb11b820107bf942af4 100644 --- a/frontend/src/metabase/components/FieldSet.jsx +++ b/frontend/src/metabase/components/FieldSet.jsx @@ -1,4 +1,4 @@ -import React, {Component, PropTypes} from "react"; +import React from "react"; import cx from "classnames"; diff --git a/frontend/src/metabase/components/FormField.jsx b/frontend/src/metabase/components/FormField.jsx index 3000b231d68f6f245a8dfec10905c3b07fd3d1e2..a615e91d64afa863976f1ff822dc717acb07dd41 100644 --- a/frontend/src/metabase/components/FormField.jsx +++ b/frontend/src/metabase/components/FormField.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import cx from "classnames"; diff --git a/frontend/src/metabase/components/Header.jsx b/frontend/src/metabase/components/Header.jsx index 2f16bfbb78da0113f897fd0c794fb3cb16e3c68f..f8948586f987b91aaefe5fd4697a4b147d6bd136 100644 --- a/frontend/src/metabase/components/Header.jsx +++ b/frontend/src/metabase/components/Header.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import ReactDOM from "react-dom"; import Input from "metabase/components/Input.jsx"; diff --git a/frontend/src/metabase/components/HeaderBar.jsx b/frontend/src/metabase/components/HeaderBar.jsx index 5a67451b39f67b4906b083486018f5df22e6f04e..c82d1a23020f8c26cf0150d3c62ad054842d7171 100644 --- a/frontend/src/metabase/components/HeaderBar.jsx +++ b/frontend/src/metabase/components/HeaderBar.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import Input from "metabase/components/Input.jsx"; import TitleAndDescription from "metabase/components/TitleAndDescription.jsx"; diff --git a/frontend/src/metabase/components/HeaderModal.jsx b/frontend/src/metabase/components/HeaderModal.jsx index 16b18c3ea40ff87d6cc0d34bcf76b5d9b3b24a7e..6de586575247a16b3a28f54f4c1a39441bf8afcc 100644 --- a/frontend/src/metabase/components/HeaderModal.jsx +++ b/frontend/src/metabase/components/HeaderModal.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import BodyComponent from "metabase/components/BodyComponent"; import cx from "classnames"; diff --git a/frontend/src/metabase/components/HeaderWithBack.jsx b/frontend/src/metabase/components/HeaderWithBack.jsx index 6f5761adfc0bf1f90ca26cf8d70ab74d321e8fb6..b3fbfbf2b889d6c4a7533028f04f1dfa70025eee 100644 --- a/frontend/src/metabase/components/HeaderWithBack.jsx +++ b/frontend/src/metabase/components/HeaderWithBack.jsx @@ -1,4 +1,5 @@ -import React, { PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import Icon from "metabase/components/Icon"; import TitleAndDescription from "metabase/components/TitleAndDescription"; diff --git a/frontend/src/metabase/components/HistoryModal.jsx b/frontend/src/metabase/components/HistoryModal.jsx index 82fcb3bf48e2ca99e38dbb6bd71f88fc8144402a..3b545724611c3afbb110586d732256c215b38da5 100644 --- a/frontend/src/metabase/components/HistoryModal.jsx +++ b/frontend/src/metabase/components/HistoryModal.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ActionButton from "metabase/components/ActionButton.jsx"; import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx"; @@ -72,7 +73,7 @@ export default class HistoryModal extends Component { var { revisions } = this.props; return ( <ModalContent - title="Change History" + title="Revision history" onClose={() => this.props.onClose()} > <LoadingAndErrorWrapper loading={!revisions} error={this.state.error}> diff --git a/frontend/src/metabase/components/Icon.jsx b/frontend/src/metabase/components/Icon.jsx index 847e1cc131e6d55aa169f0db8eb9a0dddd9e92f9..ff6078af9f0ffc2f3e62a34d85a5298e40984fa9 100644 --- a/frontend/src/metabase/components/Icon.jsx +++ b/frontend/src/metabase/components/Icon.jsx @@ -1,6 +1,7 @@ /*eslint-disable react/no-danger */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import RetinaImage from "react-retina-image"; import { loadIcon } from 'metabase/icon_paths'; diff --git a/frontend/src/metabase/components/IconBorder.jsx b/frontend/src/metabase/components/IconBorder.jsx index b23a29bf1655bd73df445e5ac7962f405d20abab..f249b505b2a49de2c2b6e63d5acd9c940d4cf783 100644 --- a/frontend/src/metabase/components/IconBorder.jsx +++ b/frontend/src/metabase/components/IconBorder.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import cx from "classnames"; /* diff --git a/frontend/src/metabase/components/Input.jsx b/frontend/src/metabase/components/Input.jsx index bfef3a9a2988d52d906adb5dd318cd0eaa403e5d..9d754421d076894331c8f61fc03504560bc56667 100644 --- a/frontend/src/metabase/components/Input.jsx +++ b/frontend/src/metabase/components/Input.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import _ from "underscore"; diff --git a/frontend/src/metabase/components/LabelIcon.jsx b/frontend/src/metabase/components/LabelIcon.jsx index 51ce497119276171807674ab91a8b8cd4e0e589d..b62b0752479e5f49ecc6358b426a04dc51fc9390 100644 --- a/frontend/src/metabase/components/LabelIcon.jsx +++ b/frontend/src/metabase/components/LabelIcon.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import S from "./LabelIcon.css"; diff --git a/frontend/src/metabase/components/List.jsx b/frontend/src/metabase/components/List.jsx index 407cafe8b88ad608d7073307690824cf6808a8a4..047518a04c3ab389e81dad992853b198fbf3b430 100644 --- a/frontend/src/metabase/components/List.jsx +++ b/frontend/src/metabase/components/List.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import S from "./List.css"; import pure from "recompose/pure"; diff --git a/frontend/src/metabase/components/ListItem.jsx b/frontend/src/metabase/components/ListItem.jsx index f7e15914a6b7db38f112aa2d87a191652e950f45..6082dc558bd8b6e04829c5ec77f816d83ace479a 100644 --- a/frontend/src/metabase/components/ListItem.jsx +++ b/frontend/src/metabase/components/ListItem.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import { Link } from "react-router"; import S from "./List.css"; diff --git a/frontend/src/metabase/components/ListSearchField.jsx b/frontend/src/metabase/components/ListSearchField.jsx index 0783744264ab81252a98f5f08f8582d70dddd429..6025da72aacd978db8625c02fc901897b9cf288c 100644 --- a/frontend/src/metabase/components/ListSearchField.jsx +++ b/frontend/src/metabase/components/ListSearchField.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import Icon from "metabase/components/Icon.jsx"; diff --git a/frontend/src/metabase/components/LoadingAndErrorWrapper.jsx b/frontend/src/metabase/components/LoadingAndErrorWrapper.jsx index fd4c7df84662c17f003b7903ae6a853ea28ab591..a0fc95eae3c38bc1107a25b7cd74f5569dfd811c 100644 --- a/frontend/src/metabase/components/LoadingAndErrorWrapper.jsx +++ b/frontend/src/metabase/components/LoadingAndErrorWrapper.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import LoadingSpinner from "metabase/components/LoadingSpinner.jsx"; diff --git a/frontend/src/metabase/components/LoadingSpinner.jsx b/frontend/src/metabase/components/LoadingSpinner.jsx index 8356d3d71216aea07723bef88c5951164d95d159..38a5d0a8bae99f415cf5d3b87d25d320bba36d0f 100644 --- a/frontend/src/metabase/components/LoadingSpinner.jsx +++ b/frontend/src/metabase/components/LoadingSpinner.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import "./LoadingSpinner.css"; diff --git a/frontend/src/metabase/components/LogoIcon.jsx b/frontend/src/metabase/components/LogoIcon.jsx index 15443a7e57805c18307f15997323a15551fe6435..b3134321a4f48d3a4e07a543be1d308818579b9d 100644 --- a/frontend/src/metabase/components/LogoIcon.jsx +++ b/frontend/src/metabase/components/LogoIcon.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import cx from "classnames"; export default class LogoIcon extends Component { diff --git a/frontend/src/metabase/components/Logs.jsx b/frontend/src/metabase/components/Logs.jsx index aa3e31572ad1b3b6c5a4c9f682cf1ad315e9e1b6..eb1521b1b217a7412375917ad9f1f5a9b0fc110d 100644 --- a/frontend/src/metabase/components/Logs.jsx +++ b/frontend/src/metabase/components/Logs.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import ReactDOM from "react-dom"; import fetch from 'isomorphic-fetch'; diff --git a/frontend/src/metabase/components/Modal.jsx b/frontend/src/metabase/components/Modal.jsx index 592a39fc1bfb60bb20965daf9381f69c06eab7ba..04d97740acfd632b116169f0f7c4626b492199f8 100644 --- a/frontend/src/metabase/components/Modal.jsx +++ b/frontend/src/metabase/components/Modal.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ReactDOM from "react-dom"; import cx from "classnames"; diff --git a/frontend/src/metabase/components/ModalContent.jsx b/frontend/src/metabase/components/ModalContent.jsx index 560aca89e9de922aea4715d6943fecd6d2637c8b..51909fcdea445bb91926073cd2fb473f8aa564d8 100644 --- a/frontend/src/metabase/components/ModalContent.jsx +++ b/frontend/src/metabase/components/ModalContent.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { MODAL_CHILD_CONTEXT_TYPES } from "./Modal"; import Icon from "metabase/components/Icon.jsx"; diff --git a/frontend/src/metabase/components/NewsletterForm.jsx b/frontend/src/metabase/components/NewsletterForm.jsx index 18955f11386584ca14d33e4a359165071b29fee0..55a52d563c6d2f7d04b6fb03c8cdf8531e15254f 100644 --- a/frontend/src/metabase/components/NewsletterForm.jsx +++ b/frontend/src/metabase/components/NewsletterForm.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ReactDOM from "react-dom"; import Icon from 'metabase/components/Icon.jsx'; diff --git a/frontend/src/metabase/components/NotFound.jsx b/frontend/src/metabase/components/NotFound.jsx index 0eb16a19945a2dbe89ac36d3a778b913b0820d96..08c54655ccf0cf68cb99631ced3add58f3c2b897 100644 --- a/frontend/src/metabase/components/NotFound.jsx +++ b/frontend/src/metabase/components/NotFound.jsx @@ -1,6 +1,8 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { Link } from "react-router"; +import * as Urls from "metabase/lib/urls"; + export default class NotFound extends Component { render() { return ( @@ -11,7 +13,7 @@ export default class NotFound extends Component { <p className="h4">You might've been tricked by a ninja, but in all likelihood, you were just given a bad link.</p> <p className="h4 my4">You can always:</p> <div className="flex align-center"> - <Link to="/q" className="Button Button--primary"> + <Link to={Urls.question()} className="Button Button--primary"> <div className="p1">Ask a new question.</div> </Link> <span className="mx2">or</span> diff --git a/frontend/src/metabase/components/OnClickOutsideWrapper.jsx b/frontend/src/metabase/components/OnClickOutsideWrapper.jsx index 7a972d73ec8c0bee92630b34eb954b946b2291bf..961382729f9324e365e1351207531c8ec5ae8c96 100644 --- a/frontend/src/metabase/components/OnClickOutsideWrapper.jsx +++ b/frontend/src/metabase/components/OnClickOutsideWrapper.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ReactDOM from "react-dom"; // keep track of the order popovers were opened so we only close the last one when clicked outside diff --git a/frontend/src/metabase/components/PasswordReveal.jsx b/frontend/src/metabase/components/PasswordReveal.jsx index eba44e2cc2d075b759376c8d308fe5e1c8d86a25..be1387260a99f4fff186c6a430d9f4179c3646ef 100644 --- a/frontend/src/metabase/components/PasswordReveal.jsx +++ b/frontend/src/metabase/components/PasswordReveal.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; export default class PasswordReveal extends Component { diff --git a/frontend/src/metabase/components/Popover.jsx b/frontend/src/metabase/components/Popover.jsx index f4d255e88c3aea8ad4c6a7c5d362e331f5b45c7d..cd72579ac1558cecd4cf550494cb984c06043548 100644 --- a/frontend/src/metabase/components/Popover.jsx +++ b/frontend/src/metabase/components/Popover.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ReactDOM from "react-dom"; import ReactCSSTransitionGroup from "react-addons-css-transition-group"; diff --git a/frontend/src/metabase/components/ProgressBar.jsx b/frontend/src/metabase/components/ProgressBar.jsx index cdc79c62f7d4d58acace1ff048b5b3a0c4b009d7..a10bf6de3685c4257ba41b8bedc1036a245bb689 100644 --- a/frontend/src/metabase/components/ProgressBar.jsx +++ b/frontend/src/metabase/components/ProgressBar.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; export default class ProgressBar extends Component { static propTypes = { diff --git a/frontend/src/metabase/components/QueryButton.jsx b/frontend/src/metabase/components/QueryButton.jsx index 83fdb22244dfeda38d2097d0a7860a3b54309119..58985a6ae905c2ee55a6e00e4f5e7d0735d44d40 100644 --- a/frontend/src/metabase/components/QueryButton.jsx +++ b/frontend/src/metabase/components/QueryButton.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import { Link } from "react-router"; import pure from "recompose/pure"; import cx from "classnames"; @@ -39,4 +40,4 @@ QueryButton.propTypes = { link: PropTypes.string }; -export default pure(QueryButton); \ No newline at end of file +export default pure(QueryButton); diff --git a/frontend/src/metabase/components/QuestionSavedModal.jsx b/frontend/src/metabase/components/QuestionSavedModal.jsx index 6623904d06014358400fdffe83b2d3862acc4e00..6bcbe8486e6ea8ed97bcbb95bd57f570835b04cd 100644 --- a/frontend/src/metabase/components/QuestionSavedModal.jsx +++ b/frontend/src/metabase/components/QuestionSavedModal.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ModalContent from "metabase/components/ModalContent.jsx"; diff --git a/frontend/src/metabase/components/Radio.jsx b/frontend/src/metabase/components/Radio.jsx index 3707ed77871e26d369809fcc23ae83ab5f60d925..0a5c7eb9c80429d334215e67eead3f01ce46a996 100644 --- a/frontend/src/metabase/components/Radio.jsx +++ b/frontend/src/metabase/components/Radio.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import cx from "classnames"; import _ from "underscore"; diff --git a/frontend/src/metabase/components/SaveStatus.jsx b/frontend/src/metabase/components/SaveStatus.jsx index 21719c6ae7af6ad8c5b08e3bf67e7f001f7c52d2..2b6405f0c96bb8c8e37138156ac7569d5bebc5de 100644 --- a/frontend/src/metabase/components/SaveStatus.jsx +++ b/frontend/src/metabase/components/SaveStatus.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import Icon from "metabase/components/Icon.jsx"; import LoadingSpinner from "metabase/components/LoadingSpinner.jsx"; diff --git a/frontend/src/metabase/components/Select.jsx b/frontend/src/metabase/components/Select.jsx index 20adc9c358a24b0216b040e7acf630be9878d727..46452638ca8020475919afe1daa823cbaed7b6a0 100644 --- a/frontend/src/metabase/components/Select.jsx +++ b/frontend/src/metabase/components/Select.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ColumnarSelector from "metabase/components/ColumnarSelector.jsx"; import Icon from "metabase/components/Icon.jsx"; diff --git a/frontend/src/metabase/components/SelectButton.jsx b/frontend/src/metabase/components/SelectButton.jsx index f0156b5c45269b55c13734514e16aedde32ba986..e14ed5577c0c8171bd9faf7758b5058e085a07be 100644 --- a/frontend/src/metabase/components/SelectButton.jsx +++ b/frontend/src/metabase/components/SelectButton.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import Icon from "metabase/components/Icon.jsx"; diff --git a/frontend/src/metabase/components/ShrinkableList.jsx b/frontend/src/metabase/components/ShrinkableList.jsx new file mode 100644 index 0000000000000000000000000000000000000000..457841fd15c62d49a9945102ec96c4e89e03ade5 --- /dev/null +++ b/frontend/src/metabase/components/ShrinkableList.jsx @@ -0,0 +1,59 @@ +/* @flow */ + +import React, { Component } from "react"; +import ReactDOM from "react-dom"; + +import ExplicitSize from "metabase/components/ExplicitSize"; + +type Props = { + className?: string, + items: any[], + renderItem: (item: any) => any, + renderItemSmall: (item: any) => any +}; + +type State = { + isShrunk: ?boolean +}; + +@ExplicitSize +export default class ShrinkableList extends Component<*, Props, State> { + state: State = { + isShrunk: null + } + + componentWillReceiveProps() { + this.setState({ + isShrunk: null + }) + } + + componentDidMount() { + this.componentDidUpdate(); + } + + componentDidUpdate() { + const container = ReactDOM.findDOMNode(this) + const { isShrunk } = this.state; + if (container && isShrunk === null) { + this.setState({ + isShrunk: container.scrollWidth !== container.offsetWidth + }) + } + } + + render() { + const { items, className, renderItemSmall, renderItem } = this.props; + const { isShrunk } = this.state; + return ( + <div className={className}> + { items.map(item => + isShrunk ? + renderItemSmall(item) + : + renderItem(item) + )} + </div> + ); + } +} diff --git a/frontend/src/metabase/components/Sidebar.jsx b/frontend/src/metabase/components/Sidebar.jsx index ff8983019307843cdc49e9e6fafb8f38a37a783b..2d5d89794c76150df5bde3fa377a28d7e3fbec56 100644 --- a/frontend/src/metabase/components/Sidebar.jsx +++ b/frontend/src/metabase/components/Sidebar.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import { Link } from "react-router"; import S from "./Sidebar.css"; diff --git a/frontend/src/metabase/components/SidebarLayout.jsx b/frontend/src/metabase/components/SidebarLayout.jsx index 3ad943653ccfb66c89dcd344d68e6a2f2a8872ee..6a72feeadcc938d2c852b430f77693b77c41fada 100644 --- a/frontend/src/metabase/components/SidebarLayout.jsx +++ b/frontend/src/metabase/components/SidebarLayout.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; const SidebarLayout = ({ className, style, sidebar, children }) => <div className={className} style={{ ...style, display: "flex", flexDirection: "row"}}> diff --git a/frontend/src/metabase/components/SortableItemList.jsx b/frontend/src/metabase/components/SortableItemList.jsx index 00495171c6ff8fe3a56c83e9f629225452409b57..417eaacacff8a95e844e15f1308af066b58a7fdc 100644 --- a/frontend/src/metabase/components/SortableItemList.jsx +++ b/frontend/src/metabase/components/SortableItemList.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import "./SortableItemList.css"; diff --git a/frontend/src/metabase/components/StackedCheckBox.jsx b/frontend/src/metabase/components/StackedCheckBox.jsx index 6b44a0dbaa1a0f2ef38a20525074a46075f61d6a..0332500ed3045a0edf069d2f80b68b3d51d1fd82 100644 --- a/frontend/src/metabase/components/StackedCheckBox.jsx +++ b/frontend/src/metabase/components/StackedCheckBox.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; import CheckBox from "metabase/components/CheckBox.jsx"; diff --git a/frontend/src/metabase/components/TextEditor.jsx b/frontend/src/metabase/components/TextEditor.jsx index db65a48337fb5c7b84f4b18fc373f944fae4ba68..d23fc3505aec4e4ef33e88f50cba0692e54751b6 100644 --- a/frontend/src/metabase/components/TextEditor.jsx +++ b/frontend/src/metabase/components/TextEditor.jsx @@ -1,6 +1,7 @@ /*global ace*/ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ReactDOM from "react-dom"; import "ace/ace"; diff --git a/frontend/src/metabase/components/TitleAndDescription.jsx b/frontend/src/metabase/components/TitleAndDescription.jsx index 81dee583aad5a0617bdc0d3ece93aee999ba9728..899de18e87ff7f913f8486f7bfd1b24df7e6f346 100644 --- a/frontend/src/metabase/components/TitleAndDescription.jsx +++ b/frontend/src/metabase/components/TitleAndDescription.jsx @@ -1,4 +1,5 @@ -import React, { PropTypes } from 'react'; +import React from 'react'; +import PropTypes from "prop-types"; import pure from "recompose/pure"; import Icon from "metabase/components/Icon.jsx"; diff --git a/frontend/src/metabase/components/Toggle.jsx b/frontend/src/metabase/components/Toggle.jsx index 79f80c4d1069f86815f9297cbbd7f69a0970d941..7617f8723622bf3ba192ef9175a87a4b0756ebd9 100644 --- a/frontend/src/metabase/components/Toggle.jsx +++ b/frontend/src/metabase/components/Toggle.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import styles from "./Toggle.css"; diff --git a/frontend/src/metabase/components/Tooltip.jsx b/frontend/src/metabase/components/Tooltip.jsx index 68a84a2c6a3343882c046e3ee9bfab27ffbc4395..7b8d6f5fbb4c1adf7ba7f639195a044462f45704 100644 --- a/frontend/src/metabase/components/Tooltip.jsx +++ b/frontend/src/metabase/components/Tooltip.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ReactDOM from "react-dom"; import TooltipPopover from "./TooltipPopover.jsx"; @@ -14,7 +15,7 @@ export default class Tooltip extends Component { } static propTypes = { - tooltip: PropTypes.node.isRequired, + tooltip: PropTypes.node, children: PropTypes.element.isRequired, isEnabled: PropTypes.bool, verticalAttachments: PropTypes.array, diff --git a/frontend/src/metabase/components/TooltipPopover.jsx b/frontend/src/metabase/components/TooltipPopover.jsx index 5bbf6c15ab6a31bbe3602e67ed8403ffafe687cc..5fceead08de609725c46ed812fc92d966d24c337 100644 --- a/frontend/src/metabase/components/TooltipPopover.jsx +++ b/frontend/src/metabase/components/TooltipPopover.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; import cx from "classnames"; import pure from "recompose/pure"; diff --git a/frontend/src/metabase/components/Triggerable.jsx b/frontend/src/metabase/components/Triggerable.jsx index 9a4fbc67d51d8f2f13fedf23204bac63819b703d..b30d90daca10f29915e5e069866fd0caed8b5ec3 100644 --- a/frontend/src/metabase/components/Triggerable.jsx +++ b/frontend/src/metabase/components/Triggerable.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import ReactDOM from "react-dom"; import { isObscured } from "metabase/lib/dom"; diff --git a/frontend/src/metabase/components/Unauthorized.jsx b/frontend/src/metabase/components/Unauthorized.jsx index 7c29bac0f7f9ed425226851485e01b4e88cbdd8e..d43c1eb98a9c87e8a86a601b2e6ce5016b87c407 100644 --- a/frontend/src/metabase/components/Unauthorized.jsx +++ b/frontend/src/metabase/components/Unauthorized.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import Icon from "metabase/components/Icon.jsx"; diff --git a/frontend/src/metabase/components/UserAvatar.jsx b/frontend/src/metabase/components/UserAvatar.jsx index af24b4bcf6bc6986d78330e2d1f48d0d5cf1f21b..0c002cd4d32931636033ea4ab727e69c0b27de9b 100644 --- a/frontend/src/metabase/components/UserAvatar.jsx +++ b/frontend/src/metabase/components/UserAvatar.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from "prop-types"; import cx from 'classnames'; export default class UserAvatar extends Component { diff --git a/frontend/src/metabase/components/form/FormField.jsx b/frontend/src/metabase/components/form/FormField.jsx index 90cf12e242f7edc03ddb73146e28fb296c4b1ae2..4e4066d20969dddfbf9b7808754712fd0b4c15cf 100644 --- a/frontend/src/metabase/components/form/FormField.jsx +++ b/frontend/src/metabase/components/form/FormField.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import cx from "classnames"; diff --git a/frontend/src/metabase/components/form/FormLabel.jsx b/frontend/src/metabase/components/form/FormLabel.jsx index 5e79a4bb8a50ed7730b33c525f7c6b925c1fb505..5a14bed3229ad96489b1a4040543389a9e1a9149 100644 --- a/frontend/src/metabase/components/form/FormLabel.jsx +++ b/frontend/src/metabase/components/form/FormLabel.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import cx from "classnames"; diff --git a/frontend/src/metabase/components/form/FormMessage.jsx b/frontend/src/metabase/components/form/FormMessage.jsx index 1fead88a8a2142d2c17ba58ad76614f7cc2e8bf4..09f2c4ca15ea682dd34c66a7f62f2484c2d55637 100644 --- a/frontend/src/metabase/components/form/FormMessage.jsx +++ b/frontend/src/metabase/components/form/FormMessage.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import cx from "classnames"; diff --git a/frontend/src/metabase/containers/SaveQuestionModal.jsx b/frontend/src/metabase/containers/SaveQuestionModal.jsx index 6e4ed2b26b0d9220254fdf51900bb54eabdbf361..7e7dae69a8a5745ba16408ecfc3c2d09c38e79ed 100644 --- a/frontend/src/metabase/containers/SaveQuestionModal.jsx +++ b/frontend/src/metabase/containers/SaveQuestionModal.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ReactCSSTransitionGroup from "react-addons-css-transition-group"; diff --git a/frontend/src/metabase/containers/UndoListing.jsx b/frontend/src/metabase/containers/UndoListing.jsx index 8d1e7785a39cb51b33b74d790bdb01d20421a02a..672fcd1d86906f66496fcda45e0851e480ac17e0 100644 --- a/frontend/src/metabase/containers/UndoListing.jsx +++ b/frontend/src/metabase/containers/UndoListing.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { connect } from "react-redux"; import S from "./UndoListing.css"; diff --git a/frontend/src/metabase/css/core/flex.css b/frontend/src/metabase/css/core/flex.css index 9ad2d5853c9fea87147af5128d3264e0e8de72e1..f88fcf8efb97e6ba990655bf4b5468b492994915 100644 --- a/frontend/src/metabase/css/core/flex.css +++ b/frontend/src/metabase/css/core/flex.css @@ -32,6 +32,10 @@ justify-content: space-between; } +.justify-end { + justify-content: flex-end; +} + .align-start { align-items: flex-start; } diff --git a/frontend/src/metabase/css/query_builder.css b/frontend/src/metabase/css/query_builder.css index a8cb318deca637f3a5104b37fdbd098911dc7753..f298eff4caf824a7d90164dc5ec9cfb288b2c88e 100644 --- a/frontend/src/metabase/css/query_builder.css +++ b/frontend/src/metabase/css/query_builder.css @@ -518,12 +518,13 @@ z-index: 1; opacity: 1; box-shadow: 0 1px 2px rgba(0, 0, 0, .22); - transition: margin-top 0.5s, opacity 0.5s; + transition: transform 0.5s, opacity 0.5s; min-width: 8em; + position: relative; } .RunButton.RunButton--hidden { - margin-top: -110px; + transform: translateY(-65px); opacity: 0; } diff --git a/frontend/src/metabase/dashboard/components/AddSeriesModal.jsx b/frontend/src/metabase/dashboard/components/AddSeriesModal.jsx index 4dfbefdcadeb031a842a90661be62a8390e730f5..d373afa4b74a12f312be6c6b394e58abdd15d661 100644 --- a/frontend/src/metabase/dashboard/components/AddSeriesModal.jsx +++ b/frontend/src/metabase/dashboard/components/AddSeriesModal.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import Visualization from "metabase/visualizations/components/Visualization.jsx"; import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx"; diff --git a/frontend/src/metabase/dashboard/components/AddToDashSelectQuestionModal.jsx b/frontend/src/metabase/dashboard/components/AddToDashSelectQuestionModal.jsx index 1d513501c53df8ecf1b277ac0d76114c2a542fd3..0ab0ea01ddc693a8d45b094189192f875c624201 100644 --- a/frontend/src/metabase/dashboard/components/AddToDashSelectQuestionModal.jsx +++ b/frontend/src/metabase/dashboard/components/AddToDashSelectQuestionModal.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import MetabaseAnalytics from "metabase/lib/analytics"; import AddToDashboard from "metabase/questions/containers/AddToDashboard.jsx"; diff --git a/frontend/src/metabase/dashboard/components/DashCard.jsx b/frontend/src/metabase/dashboard/components/DashCard.jsx index 0ef43f0234fb8a89309a3c11d006dbd81499c650..01bb76aeff49686d006fdb854fd5b5c96630a0f5 100644 --- a/frontend/src/metabase/dashboard/components/DashCard.jsx +++ b/frontend/src/metabase/dashboard/components/DashCard.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ReactDOM from "react-dom"; import visualizations, { getVisualizationRaw } from "metabase/visualizations"; diff --git a/frontend/src/metabase/dashboard/components/Dashboard.jsx b/frontend/src/metabase/dashboard/components/Dashboard.jsx index 931f3d9969ea0b7ef2366c8923f40bc3c2876481..5325abcbdd86492c2bb0a301d9bede809b99ecc6 100644 --- a/frontend/src/metabase/dashboard/components/Dashboard.jsx +++ b/frontend/src/metabase/dashboard/components/Dashboard.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import DashboardHeader from "../components/DashboardHeader.jsx"; import DashboardGrid from "../components/DashboardGrid.jsx"; diff --git a/frontend/src/metabase/dashboard/components/DashboardGrid.jsx b/frontend/src/metabase/dashboard/components/DashboardGrid.jsx index a037198fc1b42e9a037b36d1dabe345f93f1cebe..4441f0249d1d3900b451f32908881a42ef70f5ec 100644 --- a/frontend/src/metabase/dashboard/components/DashboardGrid.jsx +++ b/frontend/src/metabase/dashboard/components/DashboardGrid.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import GridLayout from "./grid/GridLayout.jsx"; import DashCard from "./DashCard.jsx"; diff --git a/frontend/src/metabase/dashboard/components/DashboardHeader.jsx b/frontend/src/metabase/dashboard/components/DashboardHeader.jsx index 9debed742065740077330b83776c326a7994b3f5..db1f7d977f87d32f2150aab9882267d9d519caee 100644 --- a/frontend/src/metabase/dashboard/components/DashboardHeader.jsx +++ b/frontend/src/metabase/dashboard/components/DashboardHeader.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ActionButton from "metabase/components/ActionButton.jsx"; import AddToDashSelectQuestionModal from "./AddToDashSelectQuestionModal.jsx"; @@ -146,7 +147,7 @@ export default class DashboardHeader extends Component { // Parameters buttons.push( <span> - <Tooltip tooltip="Add a Filter"> + <Tooltip tooltip="Add a filter"> <a key="parameters" className={cx("text-brand-hover", { "text-brand": this.state.modal == "parameters" })} @@ -173,7 +174,7 @@ export default class DashboardHeader extends Component { key="history" ref="dashboardHistory" triggerElement={ - <Tooltip tooltip="Revision History"> + <Tooltip tooltip="Revision history"> <span data-metabase-event={"Dashboard;Revisions"}> <Icon className="text-brand-hover" name="history" size={16} /> </span> @@ -195,7 +196,7 @@ export default class DashboardHeader extends Component { if (!isFullscreen && !isEditing && canEdit) { buttons.push( - <Tooltip tooltip="Edit Dashboard"> + <Tooltip tooltip="Edit dashboard"> <a data-metabase-event="Dashboard;Edit" key="edit" title="Edit Dashboard Layout" className="text-brand-hover cursor-pointer" onClick={() => this.onEdit()}> <Icon name="pencil" size={16} /> </a> @@ -210,7 +211,7 @@ export default class DashboardHeader extends Component { key="add" ref="addQuestionModal" triggerElement={ - <Tooltip tooltip="Add Card"> + <Tooltip tooltip="Add a question"> <span data-metabase-event="Dashboard;Add Card Modal" title="Add a question to this dashboard"> <Icon className={cx("text-brand-hover cursor-pointer", { "Icon--pulse": isEmpty })} name="add" size={16} /> </span> diff --git a/frontend/src/metabase/dashboard/components/DeleteDashboardModal.jsx b/frontend/src/metabase/dashboard/components/DeleteDashboardModal.jsx index 653449a0899930a454017e99479d208f00af4ded..f75223b38abe756115200ad4dfa9029dfe857496 100644 --- a/frontend/src/metabase/dashboard/components/DeleteDashboardModal.jsx +++ b/frontend/src/metabase/dashboard/components/DeleteDashboardModal.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ModalContent from "metabase/components/ModalContent.jsx"; diff --git a/frontend/src/metabase/dashboard/components/RefreshWidget.jsx b/frontend/src/metabase/dashboard/components/RefreshWidget.jsx index 7558419887f11b33d70c2e54c16cfcd4a3090f19..afcf9cf84e6f4e4ec7579af80ab11c8b68bf42d2 100644 --- a/frontend/src/metabase/dashboard/components/RefreshWidget.jsx +++ b/frontend/src/metabase/dashboard/components/RefreshWidget.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import styles from "./RefreshWidget.css"; import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx"; diff --git a/frontend/src/metabase/dashboard/components/RemoveFromDashboardModal.jsx b/frontend/src/metabase/dashboard/components/RemoveFromDashboardModal.jsx index e6d17b5fa6122f15c26c49701d9315898adf9418..b1d85a855b075c0f6649eb53458fbda733f9df95 100644 --- a/frontend/src/metabase/dashboard/components/RemoveFromDashboardModal.jsx +++ b/frontend/src/metabase/dashboard/components/RemoveFromDashboardModal.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import MetabaseAnalytics from "metabase/lib/analytics"; import ModalContent from "metabase/components/ModalContent.jsx"; diff --git a/frontend/src/metabase/dashboard/components/grid/GridItem.jsx b/frontend/src/metabase/dashboard/components/grid/GridItem.jsx index 481cda2928d94c1408cdd3fa6cbc94ca4060149d..6a0b745f8dce13f7cfb09a5633e50daa4c4294c9 100644 --- a/frontend/src/metabase/dashboard/components/grid/GridItem.jsx +++ b/frontend/src/metabase/dashboard/components/grid/GridItem.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { DraggableCore } from "react-draggable"; import { Resizable } from "react-resizable"; diff --git a/frontend/src/metabase/dashboard/components/grid/GridLayout.jsx b/frontend/src/metabase/dashboard/components/grid/GridLayout.jsx index 57f9d0e365ed11d2559785d873bef99b24066232..701934e1ca19e6cbd361019f7c15b6bbff8917e5 100644 --- a/frontend/src/metabase/dashboard/components/grid/GridLayout.jsx +++ b/frontend/src/metabase/dashboard/components/grid/GridLayout.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import ReactDOM from "react-dom"; import GridItem from "./GridItem.jsx"; diff --git a/frontend/src/metabase/dashboard/components/parameters/DashCardParameterMapper.jsx b/frontend/src/metabase/dashboard/components/parameters/DashCardParameterMapper.jsx index 24d92c79fc7c0b1cd72f91354d5af46ee0614e5f..00a5bc7476a71085a655458bb948e8add226d23b 100644 --- a/frontend/src/metabase/dashboard/components/parameters/DashCardParameterMapper.jsx +++ b/frontend/src/metabase/dashboard/components/parameters/DashCardParameterMapper.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; import DashCardCardParameterMapper from "../../containers/DashCardCardParameterMapper.jsx"; @@ -6,7 +6,7 @@ const DashCardParameterMapper = ({ dashcard }) => <div className="relative flex-full flex flex-column layout-centered"> { dashcard.series && dashcard.series.length > 0 && <div className="mx4 my1 p1 rounded" style={{ backgroundColor: "#F5F5F5", color: "#8691AC", marginTop: -10 }}> - Make sure to make a selection for each series, or the filter won't work for the card + Make sure to make a selection for each series, or the filter won't work on this card. </div> } <div className="flex mx4 z1" style={{ justifyContent: "space-around" }}> diff --git a/frontend/src/metabase/dashboard/components/parameters/ParameterValueWidget.jsx b/frontend/src/metabase/dashboard/components/parameters/ParameterValueWidget.jsx index 4aacaf8dff48740d3752a72e4df363ff07088f3f..c7d44bf601f85dfca12787a1fff1c65bc5977dec 100644 --- a/frontend/src/metabase/dashboard/components/parameters/ParameterValueWidget.jsx +++ b/frontend/src/metabase/dashboard/components/parameters/ParameterValueWidget.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, {Component, PropTypes} from "react" +import React, {Component} from "react" +import PropTypes from "prop-types"; import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx"; import Icon from "metabase/components/Icon.jsx"; diff --git a/frontend/src/metabase/dashboard/components/parameters/ParametersPopover.jsx b/frontend/src/metabase/dashboard/components/parameters/ParametersPopover.jsx index d1788e09a6685d0dd315d0ecf388d9198a4db1db..636c2f92323db6b95b9b5d1fa3852171a407df27 100644 --- a/frontend/src/metabase/dashboard/components/parameters/ParametersPopover.jsx +++ b/frontend/src/metabase/dashboard/components/parameters/ParametersPopover.jsx @@ -1,5 +1,5 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { PARAMETER_SECTIONS } from "metabase/meta/Dashboard"; import type { ParameterOption } from "metabase/meta/types/Dashboard"; diff --git a/frontend/src/metabase/dashboard/components/parameters/widgets/CategoryWidget.jsx b/frontend/src/metabase/dashboard/components/parameters/widgets/CategoryWidget.jsx index 553f579c6c9635cd2ab99ed6cafcc4d614fa933c..1d19376aee2b7e29ddeea6c7263bb000ceb2ade5 100644 --- a/frontend/src/metabase/dashboard/components/parameters/widgets/CategoryWidget.jsx +++ b/frontend/src/metabase/dashboard/components/parameters/widgets/CategoryWidget.jsx @@ -1,7 +1,8 @@ /* @flow */ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { createMultiwordSearchRegex } from "metabase/lib/string"; diff --git a/frontend/src/metabase/dashboard/components/parameters/widgets/DateAllOptionsWidget.jsx b/frontend/src/metabase/dashboard/components/parameters/widgets/DateAllOptionsWidget.jsx index 8c6bf8188c4d73492833bf63dbbc0eb6db3a8189..52b252b6de994e5a428cf3e21e8511fedd30dc3d 100644 --- a/frontend/src/metabase/dashboard/components/parameters/widgets/DateAllOptionsWidget.jsx +++ b/frontend/src/metabase/dashboard/components/parameters/widgets/DateAllOptionsWidget.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, {Component, PropTypes} from "react"; +import React, {Component} from "react"; import cx from "classnames"; import DatePicker, {DATE_OPERATORS} from "metabase/query_builder/components/filters/pickers/DatePicker.jsx"; diff --git a/frontend/src/metabase/dashboard/components/parameters/widgets/DateMonthYearWidget.jsx b/frontend/src/metabase/dashboard/components/parameters/widgets/DateMonthYearWidget.jsx index 64c904c407ada9e7b2ed7eb4d548e5c16b7a69b7..a037e3576f3cc4316021a3377d7b81304c62bc00 100644 --- a/frontend/src/metabase/dashboard/components/parameters/widgets/DateMonthYearWidget.jsx +++ b/frontend/src/metabase/dashboard/components/parameters/widgets/DateMonthYearWidget.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import YearPicker from "./YearPicker.jsx"; diff --git a/frontend/src/metabase/dashboard/components/parameters/widgets/DateQuarterYearWidget.jsx b/frontend/src/metabase/dashboard/components/parameters/widgets/DateQuarterYearWidget.jsx index 47e3ece51266cc889a6e42258a83e61bf0f9e2be..7fb3c4549b39968ed6fb819ff59099776cdddeab 100644 --- a/frontend/src/metabase/dashboard/components/parameters/widgets/DateQuarterYearWidget.jsx +++ b/frontend/src/metabase/dashboard/components/parameters/widgets/DateQuarterYearWidget.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import YearPicker from "./YearPicker.jsx"; diff --git a/frontend/src/metabase/dashboard/components/parameters/widgets/DateRangeWidget.jsx b/frontend/src/metabase/dashboard/components/parameters/widgets/DateRangeWidget.jsx index fd664cc474b772bef6b5c47dbab6079cc72c2d31..b3c777f50653f537749d8182cbdaebbc40775e6a 100644 --- a/frontend/src/metabase/dashboard/components/parameters/widgets/DateRangeWidget.jsx +++ b/frontend/src/metabase/dashboard/components/parameters/widgets/DateRangeWidget.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import Calendar from "metabase/components/Calendar.jsx"; import moment from "moment"; diff --git a/frontend/src/metabase/dashboard/components/parameters/widgets/DateRelativeWidget.jsx b/frontend/src/metabase/dashboard/components/parameters/widgets/DateRelativeWidget.jsx index dfc1f98bece7aadd1c101229c11e7a373de99818..e5d865a86d0d6888a73aad46e281b64778440948 100644 --- a/frontend/src/metabase/dashboard/components/parameters/widgets/DateRelativeWidget.jsx +++ b/frontend/src/metabase/dashboard/components/parameters/widgets/DateRelativeWidget.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import cx from "classnames"; import _ from "underscore"; diff --git a/frontend/src/metabase/dashboard/components/parameters/widgets/DateSingleWidget.jsx b/frontend/src/metabase/dashboard/components/parameters/widgets/DateSingleWidget.jsx index 1035cf795d40e785e0575a45426cd923f24f547e..09141e8f4e137bb4fe7b1dee6174021f97ac0265 100644 --- a/frontend/src/metabase/dashboard/components/parameters/widgets/DateSingleWidget.jsx +++ b/frontend/src/metabase/dashboard/components/parameters/widgets/DateSingleWidget.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; import Calendar from "metabase/components/Calendar.jsx"; import moment from "moment"; diff --git a/frontend/src/metabase/dashboard/components/parameters/widgets/TextWidget.jsx b/frontend/src/metabase/dashboard/components/parameters/widgets/TextWidget.jsx index b26c6f929ca16caf9ba7b2ea7ec6907898b1411e..553c01eaee5ac465b771c4a5dacafac797d7b4ab 100644 --- a/frontend/src/metabase/dashboard/components/parameters/widgets/TextWidget.jsx +++ b/frontend/src/metabase/dashboard/components/parameters/widgets/TextWidget.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; export default class TextWidget extends Component { constructor(props, context) { diff --git a/frontend/src/metabase/dashboard/components/parameters/widgets/YearPicker.jsx b/frontend/src/metabase/dashboard/components/parameters/widgets/YearPicker.jsx index df2a080279da2fd39aa388051fd91de22761fbbb..455eeb5fc466b9623184922623e87cb497db7140 100644 --- a/frontend/src/metabase/dashboard/components/parameters/widgets/YearPicker.jsx +++ b/frontend/src/metabase/dashboard/components/parameters/widgets/YearPicker.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; import Select from "metabase/components/Select.jsx"; import _ from "underscore"; diff --git a/frontend/src/metabase/dashboard/containers/DashCardCardParameterMapper.jsx b/frontend/src/metabase/dashboard/containers/DashCardCardParameterMapper.jsx index f23e115582c97416696af72db8e352b1ed14809e..f2049bfce17699c3f815077a5854b736e3dd2239 100644 --- a/frontend/src/metabase/dashboard/containers/DashCardCardParameterMapper.jsx +++ b/frontend/src/metabase/dashboard/containers/DashCardCardParameterMapper.jsx @@ -1,6 +1,7 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { connect } from "react-redux"; import S from "./DashCardCardParameterMapper.css"; diff --git a/frontend/src/metabase/dashboard/containers/DashboardApp.jsx b/frontend/src/metabase/dashboard/containers/DashboardApp.jsx index 6ec9f224767665e4b7c468e664a5dea6c20e9d85..8e704cde2cdc3225d7c6a29d626cc344f587e87d 100644 --- a/frontend/src/metabase/dashboard/containers/DashboardApp.jsx +++ b/frontend/src/metabase/dashboard/containers/DashboardApp.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { connect } from "react-redux"; import { push } from "react-router-redux"; diff --git a/frontend/src/metabase/dashboard/containers/DashboardEmbedWidget.jsx b/frontend/src/metabase/dashboard/containers/DashboardEmbedWidget.jsx index 6ff0deeb50520db60ecf107be57925c318123142..cd76a516159bb1b31fe14cd12ee93e2eaf917491 100644 --- a/frontend/src/metabase/dashboard/containers/DashboardEmbedWidget.jsx +++ b/frontend/src/metabase/dashboard/containers/DashboardEmbedWidget.jsx @@ -1,11 +1,11 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { connect } from "react-redux"; import EmbedWidget from "metabase/public/components/widgets/EmbedWidget"; -import Urls from "metabase/lib/urls"; +import * as Urls from "metabase/lib/urls"; import { createPublicLink, deletePublicLink, updateEnableEmbedding, updateEmbeddingParams } from "../dashboard"; diff --git a/frontend/src/metabase/dashboard/containers/ParameterWidget.jsx b/frontend/src/metabase/dashboard/containers/ParameterWidget.jsx index e352c644b130e2fc9c67233e6b8ec30905713116..cb81c3845169edfde21e6791528d0063d352d951 100644 --- a/frontend/src/metabase/dashboard/containers/ParameterWidget.jsx +++ b/frontend/src/metabase/dashboard/containers/ParameterWidget.jsx @@ -1,4 +1,5 @@ -import React, {Component, PropTypes} from 'react'; +import React, {Component} from 'react'; +import PropTypes from "prop-types"; import {connect} from "react-redux"; import ParameterValueWidget from "../components/parameters/ParameterValueWidget.jsx"; diff --git a/frontend/src/metabase/dashboard/containers/Parameters.jsx b/frontend/src/metabase/dashboard/containers/Parameters.jsx index c0d14c887c0ed80efbc85f2352b7f6b9c1cf45c2..27922677a5f5ab5172ed0918ac7f465698bb0861 100644 --- a/frontend/src/metabase/dashboard/containers/Parameters.jsx +++ b/frontend/src/metabase/dashboard/containers/Parameters.jsx @@ -1,6 +1,6 @@ /* @flow weak */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import ParameterWidget from "./ParameterWidget.jsx"; diff --git a/frontend/src/metabase/hoc/Routeless.jsx b/frontend/src/metabase/hoc/Routeless.jsx index 1e2b22de0357f296d9e44a23c6aa6921efc90bb1..f231e6c2798bfb476cc904b66b09f6c7b2e1b072 100644 --- a/frontend/src/metabase/hoc/Routeless.jsx +++ b/frontend/src/metabase/hoc/Routeless.jsx @@ -1,6 +1,6 @@ /* @flow weak */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { connect } from "react-redux"; import { push, goBack } from "react-router-redux"; diff --git a/frontend/src/metabase/hoc/Tooltipify.jsx b/frontend/src/metabase/hoc/Tooltipify.jsx index e2c8a2e45f0820cb544e25fc66e7569e47adc0ba..5aaae09c1eb677ea25508976425a156de05df041 100644 --- a/frontend/src/metabase/hoc/Tooltipify.jsx +++ b/frontend/src/metabase/hoc/Tooltipify.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import Tooltip from "metabase/components/Tooltip"; diff --git a/frontend/src/metabase/hoc/Typeahead.jsx b/frontend/src/metabase/hoc/Typeahead.jsx index 2fa2e872eb7b21a5a3fc1c6401a126d7475c95d6..bbe356554a3f1f7e201288a67e8cf26aa99e3b34 100644 --- a/frontend/src/metabase/hoc/Typeahead.jsx +++ b/frontend/src/metabase/hoc/Typeahead.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import _ from "underscore"; diff --git a/frontend/src/metabase/home/components/Activity.jsx b/frontend/src/metabase/home/components/Activity.jsx index 69dcce269099d519495e83ac6a2ff91787aca92f..fe3a77789457ede273e3b0255c4a84b1e9f83061 100644 --- a/frontend/src/metabase/home/components/Activity.jsx +++ b/frontend/src/metabase/home/components/Activity.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from "prop-types"; import { Link } from "react-router"; import _ from 'underscore'; @@ -6,7 +7,7 @@ import LoadingAndErrorWrapper from 'metabase/components/LoadingAndErrorWrapper.j import ActivityItem from './ActivityItem.jsx'; import ActivityStory from './ActivityStory.jsx'; -import Urls from 'metabase/lib/urls'; +import * as Urls from "metabase/lib/urls"; export default class Activity extends Component { @@ -213,7 +214,7 @@ export default class Activity extends Component { case "dashboard-remove-cards": description.body = item.details.dashcards[0].name; if (item.details.dashcards[0].exists) { - description.bodyLink = Urls.card(item.details.dashcards[0].card_id); + description.bodyLink = Urls.question(item.details.dashcards[0].card_id); } break; case "metric-create": diff --git a/frontend/src/metabase/home/components/ActivityItem.jsx b/frontend/src/metabase/home/components/ActivityItem.jsx index b453b1eb4b8707b228b6bb08c90bb1d67f238621..8d7ba0966acc930656e531126128437b1cfd1792 100644 --- a/frontend/src/metabase/home/components/ActivityItem.jsx +++ b/frontend/src/metabase/home/components/ActivityItem.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from "prop-types"; import Icon from 'metabase/components/Icon.jsx'; import IconBorder from 'metabase/components/IconBorder.jsx'; import UserAvatar from 'metabase/components/UserAvatar.jsx'; diff --git a/frontend/src/metabase/home/components/ActivityStory.jsx b/frontend/src/metabase/home/components/ActivityStory.jsx index 93446a019a5984604c536cb5b04c356c696836d2..5c520197a9f9e1c631059ae2e4c6695de42a483f 100644 --- a/frontend/src/metabase/home/components/ActivityStory.jsx +++ b/frontend/src/metabase/home/components/ActivityStory.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from "prop-types"; import { Link } from "react-router"; export default class ActivityStory extends Component { diff --git a/frontend/src/metabase/home/components/NewUserOnboardingModal.jsx b/frontend/src/metabase/home/components/NewUserOnboardingModal.jsx index 1de3e68b33880d05c0de16500768e7c36579329b..12e9b1c812fda901b8595a2b0df77ce893a4150f 100644 --- a/frontend/src/metabase/home/components/NewUserOnboardingModal.jsx +++ b/frontend/src/metabase/home/components/NewUserOnboardingModal.jsx @@ -1,7 +1,9 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { Link } from "react-router"; import MetabaseSettings from "metabase/lib/settings"; +import * as Urls from "metabase/lib/urls"; export default class NewUserOnboardingModal extends Component { constructor(props, context) { @@ -84,7 +86,7 @@ export default class NewUserOnboardingModal extends Component { {this.renderStep()} <span className="flex-align-right"> <a className="text-underline-hover cursor-pointer mr3" onClick={() => (this.closeModal())}>skip for now</a> - <Link to="/q#?tutorial" className="Button Button--primary">Let's do it!</Link> + <Link to={Urls.question(null, "?tutorial")} className="Button Button--primary">Let's do it!</Link> </span> </div> </div> diff --git a/frontend/src/metabase/home/components/NextStep.jsx b/frontend/src/metabase/home/components/NextStep.jsx index a3d1ef4a52fb6f8e118000b5b16e2db8f3f2c726..95424080c7806ae205ac52eedfcc2c450582d26b 100644 --- a/frontend/src/metabase/home/components/NextStep.jsx +++ b/frontend/src/metabase/home/components/NextStep.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { Link } from "react-router"; import fetch from 'isomorphic-fetch'; diff --git a/frontend/src/metabase/home/components/RecentViews.jsx b/frontend/src/metabase/home/components/RecentViews.jsx index f1146966a807b999db69613662d91df7f7d40d4f..a1b0a36ed9e6dc4c3725d04a0e7a98f7493fffce 100644 --- a/frontend/src/metabase/home/components/RecentViews.jsx +++ b/frontend/src/metabase/home/components/RecentViews.jsx @@ -1,9 +1,10 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { Link } from "react-router"; import Icon from "metabase/components/Icon.jsx"; import SidebarSection from "./SidebarSection.jsx"; -import Urls from "metabase/lib/urls"; +import * as Urls from "metabase/lib/urls"; import { normal } from "metabase/lib/colors"; diff --git a/frontend/src/metabase/home/components/SidebarSection.jsx b/frontend/src/metabase/home/components/SidebarSection.jsx index a83469d754b758be168f3cb03b5f7df2560471cb..cfbd4537bf6a9a9e1473e5c7eb08418f0addc884 100644 --- a/frontend/src/metabase/home/components/SidebarSection.jsx +++ b/frontend/src/metabase/home/components/SidebarSection.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; import Icon from "metabase/components/Icon.jsx"; diff --git a/frontend/src/metabase/home/containers/HomepageApp.jsx b/frontend/src/metabase/home/containers/HomepageApp.jsx index 1644539b7c056abbebd2c43b7a06f0a82f7fe275..cd5be9797e6a557cfd5b8b7b29a07a2df8e6a9fa 100644 --- a/frontend/src/metabase/home/containers/HomepageApp.jsx +++ b/frontend/src/metabase/home/containers/HomepageApp.jsx @@ -1,5 +1,6 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { connect } from "react-redux"; import Greeting from "metabase/lib/greeting"; diff --git a/frontend/src/metabase/icon_paths.js b/frontend/src/metabase/icon_paths.js index a4c0902142b699967314a7f7bd6e42040f8f51b0..d2686f2f7fa438d72e6e382b4d06198c4959804d 100644 --- a/frontend/src/metabase/icon_paths.js +++ b/frontend/src/metabase/icon_paths.js @@ -213,6 +213,9 @@ ICON_PATHS["horizontal_bar"] = { } }; +// $FlowFixMe +ICON_PATHS["scalar"] = ICON_PATHS["number"]; + export function loadIcon(name) { var def = ICON_PATHS[name]; if (!def) { diff --git a/frontend/src/metabase/lib/card.js b/frontend/src/metabase/lib/card.js index b53d09d1732ae751d2dcddc8f34f73d8986ac573..2b0ac9090b755dc86b2a981f6fdd1cb00de4faa3 100644 --- a/frontend/src/metabase/lib/card.js +++ b/frontend/src/metabase/lib/card.js @@ -1,6 +1,7 @@ import _ from "underscore"; import Query, { createQuery } from "metabase/lib/query"; import Utils from "metabase/lib/utils"; +import * as Urls from "metabase/lib/urls"; import { CardApi } from "metabase/services"; @@ -24,10 +25,10 @@ export function startNewCard(type, databaseId, tableId) { } // load a card either by ID or from a base64 serialization. if both are present then they are merged, which the serialized version taking precedence +// TODO: move to redux export async function loadCard(cardId) { try { - let card = await CardApi.get({ "cardId": cardId }); - return card && cleanCopyCard(card); + return await CardApi.get({ "cardId": cardId }); } catch (error) { console.log("error loading card", error); throw error; @@ -111,16 +112,10 @@ export function b64url_to_utf8(b64url) { } export function urlForCardState(state, dirty) { - var url; - if (state.cardId) { - url = "/card/" + state.cardId; - } else { - url = "/q"; - } - if (state.serializedCard && dirty) { - url += "#" + state.serializedCard; - } - return url; + return Urls.question( + state.cardId, + (state.serializedCard && dirty) ? state.serializedCard : "" + ); } export function cleanCopyCard(card) { diff --git a/frontend/src/metabase/lib/dataset.js b/frontend/src/metabase/lib/dataset.js new file mode 100644 index 0000000000000000000000000000000000000000..c5e56bf62088ae03f3cf17a4f883669ad49a9fef --- /dev/null +++ b/frontend/src/metabase/lib/dataset.js @@ -0,0 +1,4 @@ +import _ from "underscore"; + +// Many aggregations result in [[null]] if there are no rows to aggregate after filters +export const datasetContainsNoResults = (data) => data.rows.length === 0 || _.isEqual(data.rows, [[null]]) diff --git a/frontend/src/metabase/lib/urls.js b/frontend/src/metabase/lib/urls.js index ed11450f3b0fd4c68e1de077a1e29dc25ee491ea..7c2eba73166aeef3535ac9f4ee9e9f3b995fba8d 100644 --- a/frontend/src/metabase/lib/urls.js +++ b/frontend/src/metabase/lib/urls.js @@ -1,66 +1,71 @@ import { serializeCardForUrl } from "metabase/lib/card"; // provides functions for building urls to things we care about -var Urls = { - q: function(card) { - return "/q#" + serializeCardForUrl(card); - }, - card: function(card_id) { - // NOTE that this is for an ephemeral card link, not an editable card - return "/card/"+card_id; - }, - - dashboard: function(dashboard_id) { - return "/dash/"+dashboard_id; - }, +export function question(cardId, cardOrHash = "") { + if (cardOrHash && typeof cardOrHash === "object") { + cardOrHash = serializeCardForUrl(cardOrHash); + } + if (cardOrHash && cardOrHash.charAt(0) !== "#") { + cardOrHash = "#" + cardOrHash; + } + // NOTE that this is for an ephemeral card link, not an editable card + return cardId != null + ? `/question/${cardId}${cardOrHash}` + : `/question${cardOrHash}`; +} - modelToUrl: function(model, model_id) { - switch (model) { - case "card": return Urls.card(model_id); - case "dashboard": return Urls.dashboard(model_id); - case "pulse": return Urls.pulse(model_id); - default: return null; - } - }, +export function dashboard(dashboardId) { + return `/dashboard/${dashboardId}`; +} - pulse: function(pulse_id) { - return "/pulse/#"+pulse_id; - }, +export function modelToUrl(model, modelId) { + switch (model) { + case "card": + return question(modelId); + case "dashboard": + return dashboard(modelId); + case "pulse": + return pulse(modelId); + default: + return null; + } +} - tableRowsQuery: function(database_id, table_id, metric_id, segment_id) { - let url = "/q#?db="+database_id+"&table="+table_id; +export function pulse(pulseId) { + return `/pulse/#${pulseId}`; +} - if (metric_id) { - url = url + "&metric="+metric_id; - } +export function tableRowsQuery(databaseId, tableId, metricId, segmentId) { + let query = `?db=${databaseId}&table=${tableId}`; - if (segment_id) { - url = url + "&segment="+segment_id; - } + if (metricId) { + query += `&metric=${metricId}`; + } - return url; - }, + if (segmentId) { + query += `&segment=${segmentId}`; + } - collection(collection) { - return `/questions/collections/${encodeURIComponent(collection.slug)}`; - }, + return question(null, query); +} - label(label) { - return `/questions/search?label=${encodeURIComponent(label.slug)}`; - }, +export function collection(collection) { + return `/questions/collections/${encodeURIComponent(collection.slug)}`; +} - publicCard(uuid, type = null) { - return `/public/question/${uuid}` + (type ? `.${type}` : ``); - }, +export function label(label) { + return `/questions/search?label=${encodeURIComponent(label.slug)}`; +} - publicDashboard(uuid) { - return `/public/dashboard/${uuid}`; - }, +export function publicCard(uuid, type = null) { + return `/public/question/${uuid}` + (type ? `.${type}` : ``); +} - embedCard(token, type = null) { - return `/embed/question/${token}` + (type ? `.${type}` : ``); - }, +export function publicDashboard(uuid) { + return `/public/dashboard/${uuid}`; } -export default Urls; +export function embedCard(token, type = null) { + return `/embed/question/${token}` + (type ? `.${type}` : ``); +} diff --git a/frontend/src/metabase/nav/components/ProfileLink.jsx b/frontend/src/metabase/nav/components/ProfileLink.jsx index a244dc74eeef0f54d59301c51cc099b9a1f0bac3..d9b3ad48a7087e8c4656980fdad314394150a644 100644 --- a/frontend/src/metabase/nav/components/ProfileLink.jsx +++ b/frontend/src/metabase/nav/components/ProfileLink.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from "prop-types"; import { Link } from "react-router"; import OnClickOutsideWrapper from 'metabase/components/OnClickOutsideWrapper'; diff --git a/frontend/src/metabase/nav/containers/DashboardsDropdown.jsx b/frontend/src/metabase/nav/containers/DashboardsDropdown.jsx index b902a25096e2c25ff4cdac9d6854c6b69643a88c..a4f1cd7c1e93f45ab2f5fdcde2a7838cda786f0f 100644 --- a/frontend/src/metabase/nav/containers/DashboardsDropdown.jsx +++ b/frontend/src/metabase/nav/containers/DashboardsDropdown.jsx @@ -1,14 +1,16 @@ -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from "prop-types"; import { connect } from "react-redux"; import { Link } from "react-router"; import OnClickOutsideWrapper from 'metabase/components/OnClickOutsideWrapper.jsx'; - -import MetabaseAnalytics from "metabase/lib/analytics"; import CreateDashboardModal from "metabase/components/CreateDashboardModal.jsx"; import Modal from "metabase/components/Modal.jsx"; import ConstrainToScreen from "metabase/components/ConstrainToScreen"; +import MetabaseAnalytics from "metabase/lib/analytics"; +import * as Urls from "metabase/lib/urls"; + import _ from "underscore"; import cx from "classnames"; @@ -62,7 +64,7 @@ export default class DashboardsDropdown extends Component { try { let action = await createDashboard(newDashboard, true); // FIXME: this doesn't feel right... - this.props.onChangeLocation(`/dash/${action.payload.id}`); + this.props.onChangeLocation(Urls.dashboard(action.payload.id)); } catch (e) { console.log("createDashboard failed", e); } @@ -137,7 +139,7 @@ export default class DashboardsDropdown extends Component { <ul className="NavDropdown-content-layer"> { dashboards.map(dash => <li key={dash.id} className="block"> - <Link to={"/dash/"+dash.id} data-metabase-event={"Navbar;Dashboard Dropdown;Open Dashboard;"+dash.id} className="Dropdown-item block text-white no-decoration" onClick={this.closeDropdown}> + <Link to={Urls.dashboard(dash.id)} data-metabase-event={"Navbar;Dashboard Dropdown;Open Dashboard;"+dash.id} className="Dropdown-item block text-white no-decoration" onClick={this.closeDropdown}> <div className="flex text-bold"> {dash.name} </div> diff --git a/frontend/src/metabase/nav/containers/Navbar.jsx b/frontend/src/metabase/nav/containers/Navbar.jsx index 74520d03ff4d49a44a9bc1a91e2df4f3bf0ee24c..3c286bcbfaa7606cafcb486063df0cab68c61d68 100644 --- a/frontend/src/metabase/nav/containers/Navbar.jsx +++ b/frontend/src/metabase/nav/containers/Navbar.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from "prop-types"; import cx from "classnames"; import { connect } from "react-redux"; @@ -11,6 +12,8 @@ import LogoIcon from "metabase/components/LogoIcon.jsx"; import DashboardsDropdown from "metabase/nav/containers/DashboardsDropdown.jsx"; import ProfileLink from "metabase/nav/components/ProfileLink.jsx"; +import * as Urls from "metabase/lib/urls"; + import { getPath, getContext, getUser } from "../selectors"; const mapStateToProps = (state, props) => ({ @@ -115,7 +118,13 @@ export default class Navbar extends Component { </li> <li className="pl3"> <DashboardsDropdown {...this.props}> - <a data-metabase-event={"Navbar;Dashboard Dropdown;Toggle"} style={this.styles.navButton} className={cx("NavDropdown-button NavItem text-white text-bold cursor-pointer px2 flex align-center transition-background", {"NavItem--selected": this.isActive("/dash/")})}> + <a + data-metabase-event={"Navbar;Dashboard Dropdown;Toggle"} + style={this.styles.navButton} + className={cx("NavDropdown-button NavItem text-white text-bold cursor-pointer px2 flex align-center transition-background", { + "NavItem--selected": this.isActive("/dashboard/") + })} + > <span className="NavDropdown-button-layer"> Dashboards <Icon className="ml1" name={'chevrondown'} size={8}></Icon> @@ -133,7 +142,7 @@ export default class Navbar extends Component { <Link to="/reference/guide" data-metabase-event={"Navbar;DataReference"} style={this.styles.navButton} className={cx("NavItem cursor-pointer text-white text-bold no-decoration flex align-center px2 transition-background")} activeClassName="NavItem--selected">Data Reference</Link> </li> <li className="pl3"> - <Link to="/q" data-metabase-event={"Navbar;New Question"} style={this.styles.newQuestion} className="NavNewQuestion rounded inline-block bg-white text-brand text-bold cursor-pointer px2 no-decoration transition-all">New <span className="hide sm-show">Question</span></Link> + <Link to={Urls.question()} data-metabase-event={"Navbar;New Question"} style={this.styles.newQuestion} className="NavNewQuestion rounded inline-block bg-white text-brand text-bold cursor-pointer px2 no-decoration transition-all">New <span className="hide sm-show">Question</span></Link> </li> <li className="flex-align-right transition-background"> <div className="inline-block text-white"><ProfileLink {...this.props}></ProfileLink></div> diff --git a/frontend/src/metabase/public/components/EmbedFrame.jsx b/frontend/src/metabase/public/components/EmbedFrame.jsx index 531adf83a66cf13a2693a7b469403048ab198ca0..cc4b7fc0512e8ffc6aaeda9ab2078e94970044b2 100644 --- a/frontend/src/metabase/public/components/EmbedFrame.jsx +++ b/frontend/src/metabase/public/components/EmbedFrame.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { withRouter } from "react-router"; import { IFRAMED } from "metabase/lib/dom"; import Parameters from "metabase/dashboard/containers/Parameters"; diff --git a/frontend/src/metabase/public/components/MetabaseEmbed.jsx b/frontend/src/metabase/public/components/MetabaseEmbed.jsx index 841d3fa3f257ccfe8df84a3e18189c58de81b8f4..80635a96ca845345af476a2c217bd796ba424517 100644 --- a/frontend/src/metabase/public/components/MetabaseEmbed.jsx +++ b/frontend/src/metabase/public/components/MetabaseEmbed.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import querystring from "querystring"; import _ from "underscore"; diff --git a/frontend/src/metabase/public/components/PublicError.jsx b/frontend/src/metabase/public/components/PublicError.jsx index 01e429cfccb78ec6abee1f68eb53a84e6eb06c87..d0ce8d1dffd6c61af60674731ff13ea48793e439 100644 --- a/frontend/src/metabase/public/components/PublicError.jsx +++ b/frontend/src/metabase/public/components/PublicError.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React from "react"; import { connect } from "react-redux"; import { getErrorMessage } from "metabase/selectors/app"; diff --git a/frontend/src/metabase/public/components/PublicNotFound.jsx b/frontend/src/metabase/public/components/PublicNotFound.jsx index 1c775ff4a478a514c52d88d3af66ac0171215db0..0ae0e771ad8a1a2bfb0e7414ce6a0cebd3c534ac 100644 --- a/frontend/src/metabase/public/components/PublicNotFound.jsx +++ b/frontend/src/metabase/public/components/PublicNotFound.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React from "react"; import EmbedFrame from "./EmbedFrame"; diff --git a/frontend/src/metabase/public/components/widgets/AdvancedEmbedPane.jsx b/frontend/src/metabase/public/components/widgets/AdvancedEmbedPane.jsx index 1462d528d9c0157b4dce35cbebee57c654ef7198..f5f4d111e1c43a7e643ebcb2035e79ebb0b7e91b 100644 --- a/frontend/src/metabase/public/components/widgets/AdvancedEmbedPane.jsx +++ b/frontend/src/metabase/public/components/widgets/AdvancedEmbedPane.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React from "react"; import ToggleLarge from "metabase/components/ToggleLarge"; import Button from "metabase/components/Button"; diff --git a/frontend/src/metabase/public/components/widgets/CodeSample.jsx b/frontend/src/metabase/public/components/widgets/CodeSample.jsx index 0db42c7e1b0280b3e535739603be425ddf7665f5..f236fd875b1a1c91c9f6fa38f28bda6e6aa0e217 100644 --- a/frontend/src/metabase/public/components/widgets/CodeSample.jsx +++ b/frontend/src/metabase/public/components/widgets/CodeSample.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import Select, { Option } from "metabase/components/Select"; import CopyButton from "metabase/components/CopyButton"; diff --git a/frontend/src/metabase/public/components/widgets/DisplayOptionsPane.jsx b/frontend/src/metabase/public/components/widgets/DisplayOptionsPane.jsx index 57aa1fdca1d39a54a0e0509f2320be1c86ff20f6..748840682947e4497e769589277ecbda7ca447de 100644 --- a/frontend/src/metabase/public/components/widgets/DisplayOptionsPane.jsx +++ b/frontend/src/metabase/public/components/widgets/DisplayOptionsPane.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React from "react"; import EmbedSelect from "./EmbedSelect"; import CheckBox from "metabase/components/CheckBox"; diff --git a/frontend/src/metabase/public/components/widgets/EmbedCodePane.jsx b/frontend/src/metabase/public/components/widgets/EmbedCodePane.jsx index 5671264be8f8465f13e0f5c600c0bbdd27275f43..3958f551e00d797aed0d33242f430fbd199dcc3e 100644 --- a/frontend/src/metabase/public/components/widgets/EmbedCodePane.jsx +++ b/frontend/src/metabase/public/components/widgets/EmbedCodePane.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import ExternalLink from "metabase/components/ExternalLink"; import CodeSample from "./CodeSample"; diff --git a/frontend/src/metabase/public/components/widgets/EmbedModalContent.jsx b/frontend/src/metabase/public/components/widgets/EmbedModalContent.jsx index d5796ef95c8f5894a472e77a841f311290e54c6b..e46b9fb677fc6188dca638e23fd76bf77498dd4d 100644 --- a/frontend/src/metabase/public/components/widgets/EmbedModalContent.jsx +++ b/frontend/src/metabase/public/components/widgets/EmbedModalContent.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { connect } from "react-redux"; import { titleize } from "inflection"; diff --git a/frontend/src/metabase/public/components/widgets/EmbedSelect.jsx b/frontend/src/metabase/public/components/widgets/EmbedSelect.jsx index 3ef3b3d0867ad1b54db2019d6b5c5f6b8cd6d0ea..5e569c9b2859318c112f50fe8922d316f6149246 100644 --- a/frontend/src/metabase/public/components/widgets/EmbedSelect.jsx +++ b/frontend/src/metabase/public/components/widgets/EmbedSelect.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React from "react"; import Icon from "metabase/components/Icon"; diff --git a/frontend/src/metabase/public/components/widgets/EmbedWidget.jsx b/frontend/src/metabase/public/components/widgets/EmbedWidget.jsx index a890aedafeb465b284b43564ff4fb37dc801a709..05a759a4ba821eb7b8f3f1e96513bd805879ccfb 100644 --- a/frontend/src/metabase/public/components/widgets/EmbedWidget.jsx +++ b/frontend/src/metabase/public/components/widgets/EmbedWidget.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import ModalWithTrigger from "metabase/components/ModalWithTrigger"; import Tooltip from "metabase/components/Tooltip"; @@ -29,7 +29,7 @@ export default class EmbedWidget extends Component<*, Props, *> { ref={m => this._modal = m} full triggerElement={ - <Tooltip tooltip={`Sharing and Embedding`}> + <Tooltip tooltip={`Sharing and embedding`}> <Icon name="share" onClick={() => MetabaseAnalytics.trackEvent("Sharing / Embedding", resourceType, "Sharing Link Clicked") } /> </Tooltip> } diff --git a/frontend/src/metabase/public/components/widgets/PreviewPane.jsx b/frontend/src/metabase/public/components/widgets/PreviewPane.jsx index 975f527b869fe77bd6d2a18e9281f311a239540b..28fa3d17766654dbf287d70d37febbf6eff3f4f7 100644 --- a/frontend/src/metabase/public/components/widgets/PreviewPane.jsx +++ b/frontend/src/metabase/public/components/widgets/PreviewPane.jsx @@ -1,5 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import cx from "classnames"; diff --git a/frontend/src/metabase/public/components/widgets/SharingPane.jsx b/frontend/src/metabase/public/components/widgets/SharingPane.jsx index 624896fa8151cea1102f8fa283eb314a0f5bd788..6d9a8aa8ad4775775f04cd79cd4301ba855c0150 100644 --- a/frontend/src/metabase/public/components/widgets/SharingPane.jsx +++ b/frontend/src/metabase/public/components/widgets/SharingPane.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import RetinaImage from "react-retina-image"; import Icon from "metabase/components/Icon"; diff --git a/frontend/src/metabase/public/containers/PublicApp.jsx b/frontend/src/metabase/public/containers/PublicApp.jsx index 8155831aa9872f72309f2931d8194831d8ce74e3..9405c6015ae048f78ea2c1ade05614d4034f5761 100644 --- a/frontend/src/metabase/public/containers/PublicApp.jsx +++ b/frontend/src/metabase/public/containers/PublicApp.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { connect } from "react-redux"; import PublicNotFound from "metabase/public/components/PublicNotFound"; diff --git a/frontend/src/metabase/public/containers/PublicDashboard.jsx b/frontend/src/metabase/public/containers/PublicDashboard.jsx index c7c83caeee2fbc3bf1abc303f519c2a8fddb4e6b..8e4e688aa1fd990217ac551530c536d83e945fba 100644 --- a/frontend/src/metabase/public/containers/PublicDashboard.jsx +++ b/frontend/src/metabase/public/containers/PublicDashboard.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { connect } from "react-redux"; import { push } from "react-router-redux"; diff --git a/frontend/src/metabase/public/containers/PublicQuestion.jsx b/frontend/src/metabase/public/containers/PublicQuestion.jsx index cccf1b1cbc96ce0ba40574c4276410182829edba..cdbba6e144db90b25de5ea9b6502939acc18f19b 100644 --- a/frontend/src/metabase/public/containers/PublicQuestion.jsx +++ b/frontend/src/metabase/public/containers/PublicQuestion.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { connect } from "react-redux"; import Visualization from "metabase/visualizations/components/Visualization"; diff --git a/frontend/src/metabase/pulse/components/CardPicker.jsx b/frontend/src/metabase/pulse/components/CardPicker.jsx index 9a0069980853c17f5fe10fbcd283f4eea6af0f9d..7d5d883862ed863853644fa679a9ca4893824539 100644 --- a/frontend/src/metabase/pulse/components/CardPicker.jsx +++ b/frontend/src/metabase/pulse/components/CardPicker.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ReactDOM from "react-dom"; import Icon from "metabase/components/Icon.jsx"; diff --git a/frontend/src/metabase/pulse/components/PulseCardPreview.jsx b/frontend/src/metabase/pulse/components/PulseCardPreview.jsx index 2afafd3dbf8116adf9fdcca8acbcb85f3a41f56e..fd6a1aac9ac37b26fd8d5e9013bbaa62fab54204 100644 --- a/frontend/src/metabase/pulse/components/PulseCardPreview.jsx +++ b/frontend/src/metabase/pulse/components/PulseCardPreview.jsx @@ -1,6 +1,7 @@ /* eslint "react/prop-types": "warn" */ /*eslint-disable react/no-danger */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import Icon from "metabase/components/Icon.jsx"; import LoadingSpinner from "metabase/components/LoadingSpinner.jsx"; diff --git a/frontend/src/metabase/pulse/components/PulseEdit.jsx b/frontend/src/metabase/pulse/components/PulseEdit.jsx index 7d50c9bae6f5d01a32bb07201cf5dd07a071fc45..a212847eebcf3b8f43d9e1ca9c25245d5212b4cb 100644 --- a/frontend/src/metabase/pulse/components/PulseEdit.jsx +++ b/frontend/src/metabase/pulse/components/PulseEdit.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { Link } from "react-router"; import PulseEditName from "./PulseEditName.jsx"; diff --git a/frontend/src/metabase/pulse/components/PulseEditCards.jsx b/frontend/src/metabase/pulse/components/PulseEditCards.jsx index 2f631f9459aefdf43028074c2b58252b0a4ec5a1..b05bd4e20befa1c9e058b77094bf179a6a40425b 100644 --- a/frontend/src/metabase/pulse/components/PulseEditCards.jsx +++ b/frontend/src/metabase/pulse/components/PulseEditCards.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import CardPicker from "./CardPicker.jsx"; import PulseCardPreview from "./PulseCardPreview.jsx"; diff --git a/frontend/src/metabase/pulse/components/PulseEditChannels.jsx b/frontend/src/metabase/pulse/components/PulseEditChannels.jsx index c92a3215312688b996aea6152064d99f87ea7cc5..56363d46a080273da1a2387371a4a5c073569ebc 100644 --- a/frontend/src/metabase/pulse/components/PulseEditChannels.jsx +++ b/frontend/src/metabase/pulse/components/PulseEditChannels.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import _ from "underscore"; import { assoc, assocIn } from "icepick"; diff --git a/frontend/src/metabase/pulse/components/PulseEditName.jsx b/frontend/src/metabase/pulse/components/PulseEditName.jsx index c3c0cbacecf4af47b203cea549a8df20981d0c98..f725b04685b775ec4f8868ef305f9c86c6bc45cb 100644 --- a/frontend/src/metabase/pulse/components/PulseEditName.jsx +++ b/frontend/src/metabase/pulse/components/PulseEditName.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import ReactDOM from "react-dom"; import _ from "underscore"; diff --git a/frontend/src/metabase/pulse/components/PulseEditSkip.jsx b/frontend/src/metabase/pulse/components/PulseEditSkip.jsx index 4c17054314ebac9831fd5b0dfc43c0f5a5b6ac10..21b99a6cd4578a4486325be134be7991cd65d17d 100644 --- a/frontend/src/metabase/pulse/components/PulseEditSkip.jsx +++ b/frontend/src/metabase/pulse/components/PulseEditSkip.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import Toggle from "metabase/components/Toggle.jsx"; diff --git a/frontend/src/metabase/pulse/components/PulseList.jsx b/frontend/src/metabase/pulse/components/PulseList.jsx index 4df6d170d0f5ecbb4d599f26a507957f93488eb8..e560516d847ae2e1a43acedc46b8d41f7736b698 100644 --- a/frontend/src/metabase/pulse/components/PulseList.jsx +++ b/frontend/src/metabase/pulse/components/PulseList.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import PulseListItem from "./PulseListItem.jsx"; import WhatsAPulse from "./WhatsAPulse.jsx"; diff --git a/frontend/src/metabase/pulse/components/PulseListChannel.jsx b/frontend/src/metabase/pulse/components/PulseListChannel.jsx index 3c53647b0540b7cfaddd5df7274f9fb34cb160cd..e7775c1bc330d5df1c9ec22295e2121d10bae5b0 100644 --- a/frontend/src/metabase/pulse/components/PulseListChannel.jsx +++ b/frontend/src/metabase/pulse/components/PulseListChannel.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import Icon from "metabase/components/Icon.jsx"; diff --git a/frontend/src/metabase/pulse/components/PulseListItem.jsx b/frontend/src/metabase/pulse/components/PulseListItem.jsx index 7429928e757872a2c685fb3d3549fe073191833a..116ab53f43942d34a8824f12755f3d95cd4d2ce7 100644 --- a/frontend/src/metabase/pulse/components/PulseListItem.jsx +++ b/frontend/src/metabase/pulse/components/PulseListItem.jsx @@ -1,11 +1,12 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ReactDOM from "react-dom"; import { Link } from "react-router"; import cx from "classnames"; -import Urls from "metabase/lib/urls"; +import * as Urls from "metabase/lib/urls"; import PulseListChannel from "./PulseListChannel.jsx"; export default class PulseListItem extends Component { @@ -43,7 +44,7 @@ export default class PulseListItem extends Component { <ol className="mb2 px4 flex flex-wrap"> { pulse.cards.map((card, index) => <li key={index} className="mr1 mb1"> - <Link to={Urls.card(card.id)} className="Button"> + <Link to={Urls.question(card.id)} className="Button"> {card.name} </Link> </li> diff --git a/frontend/src/metabase/pulse/components/RecipientPicker.jsx b/frontend/src/metabase/pulse/components/RecipientPicker.jsx index 67db8b2cd3cca1a4e5706a7d378f8ce417933624..b53fba16aa3e68fbe5efedfbf9615c4a867a2405 100644 --- a/frontend/src/metabase/pulse/components/RecipientPicker.jsx +++ b/frontend/src/metabase/pulse/components/RecipientPicker.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ReactDOM from "react-dom"; import Icon from "metabase/components/Icon.jsx"; diff --git a/frontend/src/metabase/pulse/components/SchedulePicker.jsx b/frontend/src/metabase/pulse/components/SchedulePicker.jsx index 99e70181c37960d4ed5a2a5839b88653f4bcd024..69ed785abcc22b32d6be6e61fcdc2ed50fbbf1f5 100644 --- a/frontend/src/metabase/pulse/components/SchedulePicker.jsx +++ b/frontend/src/metabase/pulse/components/SchedulePicker.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import Select from "metabase/components/Select.jsx"; diff --git a/frontend/src/metabase/pulse/components/SetupMessage.jsx b/frontend/src/metabase/pulse/components/SetupMessage.jsx index 1efa3a30e05bfc5e982548cd0289ae35ed46d0bf..416321fdfc6459670743075f335b6d0320e7c847 100644 --- a/frontend/src/metabase/pulse/components/SetupMessage.jsx +++ b/frontend/src/metabase/pulse/components/SetupMessage.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { Link } from "react-router"; import Settings from "metabase/lib/settings"; diff --git a/frontend/src/metabase/pulse/components/SetupModal.jsx b/frontend/src/metabase/pulse/components/SetupModal.jsx index e4996fa8e276b8177dfb9a138019174b9850eaba..c0a7edbf93a14aa7291ec26b38ed63968979d008 100644 --- a/frontend/src/metabase/pulse/components/SetupModal.jsx +++ b/frontend/src/metabase/pulse/components/SetupModal.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import SetupMessage from "./SetupMessage.jsx"; import ModalContent from "metabase/components/ModalContent.jsx"; diff --git a/frontend/src/metabase/pulse/components/WhatsAPulse.jsx b/frontend/src/metabase/pulse/components/WhatsAPulse.jsx index 639fc20d4d31ba45b8af9786078a60a81f95157a..0d44604a99758b1ed9ba13be70b6a39cde1619f4 100644 --- a/frontend/src/metabase/pulse/components/WhatsAPulse.jsx +++ b/frontend/src/metabase/pulse/components/WhatsAPulse.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import RetinaImage from "react-retina-image"; diff --git a/frontend/src/metabase/pulse/containers/PulseEditApp.jsx b/frontend/src/metabase/pulse/containers/PulseEditApp.jsx index fc7510993e1fe53817dcceb2f9abea3a30e77964..60c68b4aa41af0ee53acb4adb1de150d9ef6a8b8 100644 --- a/frontend/src/metabase/pulse/containers/PulseEditApp.jsx +++ b/frontend/src/metabase/pulse/containers/PulseEditApp.jsx @@ -1,5 +1,5 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { connect } from "react-redux"; import { push } from "react-router-redux"; diff --git a/frontend/src/metabase/pulse/containers/PulseListApp.jsx b/frontend/src/metabase/pulse/containers/PulseListApp.jsx index bc01b350b7c702cf68433deae0bf273124ff7677..239841391e1608daede3a89807d13a8d14806af7 100644 --- a/frontend/src/metabase/pulse/containers/PulseListApp.jsx +++ b/frontend/src/metabase/pulse/containers/PulseListApp.jsx @@ -1,5 +1,5 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { connect } from "react-redux"; import { push } from "react-router-redux"; diff --git a/frontend/src/metabase/qb/components/TimeseriesFilterWidget.jsx b/frontend/src/metabase/qb/components/TimeseriesFilterWidget.jsx index bafa6394ae0c9402c97694f6ffd4e60f7c25a0f9..285e528400e100284e6c370c8d317912019f1290 100644 --- a/frontend/src/metabase/qb/components/TimeseriesFilterWidget.jsx +++ b/frontend/src/metabase/qb/components/TimeseriesFilterWidget.jsx @@ -1,6 +1,6 @@ /* @flow weak */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import DatePicker from "metabase/query_builder/components/filters/pickers/DatePicker"; @@ -34,7 +34,7 @@ type Props = { card: CardObject, tableMetadata: TableMetadata, setDatasetQuery: (datasetQuery: DatasetQuery) => void, - runQueryFn: () => void + runQuery: () => void }; type State = { @@ -90,7 +90,7 @@ export default class TimeseriesFilterWidget extends Component<*, Props, State> { card, tableMetadata, setDatasetQuery, - runQueryFn + runQuery } = this.props; const { filter, filterIndex, currentFilter } = this.state; let currentDescription; @@ -148,7 +148,7 @@ export default class TimeseriesFilterWidget extends Component<*, Props, State> { ...card.dataset_query, query }); - runQueryFn(); + runQuery(); } if (this._popover) { this._popover.close(); diff --git a/frontend/src/metabase/qb/components/TimeseriesGroupingWidget.jsx b/frontend/src/metabase/qb/components/TimeseriesGroupingWidget.jsx index 4681fdbe7a139b647831b85cdeb18b9ec02bcb36..f548331efd29641dc6a6fe19063a0313b71bcd5b 100644 --- a/frontend/src/metabase/qb/components/TimeseriesGroupingWidget.jsx +++ b/frontend/src/metabase/qb/components/TimeseriesGroupingWidget.jsx @@ -1,6 +1,6 @@ /* @flow weak */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import TimeGroupingPopover from "metabase/query_builder/components/TimeGroupingPopover"; @@ -20,14 +20,14 @@ import type { type Props = { card: CardObject, setDatasetQuery: (datasetQuery: DatasetQuery) => void, - runQueryFn: () => void + runQuery: () => void }; export default class TimeseriesGroupingWidget extends Component<*, Props, *> { _popover: ?any; render() { - const { card, setDatasetQuery, runQueryFn } = this.props; + const { card, setDatasetQuery, runQuery } = this.props; if (Card.isStructured(card)) { const query = Card.getQuery(card); const breakouts = query && Query.getBreakouts(query); @@ -62,7 +62,7 @@ export default class TimeseriesGroupingWidget extends Component<*, Props, *> { ...card.dataset_query, query }); - runQueryFn(); + runQuery(); if (this._popover) { this._popover.close(); } diff --git a/frontend/src/metabase/qb/components/actions/PivotByCategoryAction.jsx b/frontend/src/metabase/qb/components/actions/PivotByCategoryAction.jsx index 1165f67ffc6972bb4a4f4d8658fa29c53b6d2e2b..a7b811801fd68a06dab6d27736107fb536417d21 100644 --- a/frontend/src/metabase/qb/components/actions/PivotByCategoryAction.jsx +++ b/frontend/src/metabase/qb/components/actions/PivotByCategoryAction.jsx @@ -1,7 +1,5 @@ /* @flow */ -import React from "react"; - import { isCategory, isAddress } from "metabase/lib/schema_metadata"; import PivotByAction from "./PivotByAction"; diff --git a/frontend/src/metabase/qb/components/actions/PivotByLocationAction.jsx b/frontend/src/metabase/qb/components/actions/PivotByLocationAction.jsx index 218ee7f08200ec2cf15853ae6efc9d7bb36b9ddf..0b4f241ba1da0478052bbd065c13e90f81d93751 100644 --- a/frontend/src/metabase/qb/components/actions/PivotByLocationAction.jsx +++ b/frontend/src/metabase/qb/components/actions/PivotByLocationAction.jsx @@ -1,7 +1,5 @@ /* @flow */ -import React from "react"; - import { isAddress } from "metabase/lib/schema_metadata"; import PivotByAction from "./PivotByAction"; diff --git a/frontend/src/metabase/qb/components/actions/PivotByTimeAction.jsx b/frontend/src/metabase/qb/components/actions/PivotByTimeAction.jsx index b80c6bdc549b020ae6bfc10110594c1e0a151966..c70764e3f0468469ee2420b014d57842076c2941 100644 --- a/frontend/src/metabase/qb/components/actions/PivotByTimeAction.jsx +++ b/frontend/src/metabase/qb/components/actions/PivotByTimeAction.jsx @@ -1,7 +1,5 @@ /* @flow */ -import React from "react"; - import { isDate } from "metabase/lib/schema_metadata"; import PivotByAction from "./PivotByAction"; diff --git a/frontend/src/metabase/qb/components/actions/SummarizeBySegmentMetricAction.jsx b/frontend/src/metabase/qb/components/actions/SummarizeBySegmentMetricAction.jsx index 8d05d6502b7926dc22ebae4c412c6d8e6f7d08b9..97a53e6d458dc884197c81465d838a3498496b13 100644 --- a/frontend/src/metabase/qb/components/actions/SummarizeBySegmentMetricAction.jsx +++ b/frontend/src/metabase/qb/components/actions/SummarizeBySegmentMetricAction.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React from "react"; import AggregationPopover from "metabase/qb/components/gui/AggregationPopover"; diff --git a/frontend/src/metabase/qb/components/drill/PivotByCategoryDrill.jsx b/frontend/src/metabase/qb/components/drill/PivotByCategoryDrill.jsx index f10a4d8aa883711cd2835519fce48fa98f2feb5b..fdc7580a417f1ff565f6ccb87b3959b616b0e03e 100644 --- a/frontend/src/metabase/qb/components/drill/PivotByCategoryDrill.jsx +++ b/frontend/src/metabase/qb/components/drill/PivotByCategoryDrill.jsx @@ -1,7 +1,5 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; - import PivotByCategoryAction from "../actions/PivotByCategoryAction"; import type { diff --git a/frontend/src/metabase/qb/components/drill/PivotByLocationDrill.jsx b/frontend/src/metabase/qb/components/drill/PivotByLocationDrill.jsx index 385a54969550c2c2a75eb29c69c5d10f7e8872fb..b365e0b955e53f41b3c5b57a09136a8419e3128f 100644 --- a/frontend/src/metabase/qb/components/drill/PivotByLocationDrill.jsx +++ b/frontend/src/metabase/qb/components/drill/PivotByLocationDrill.jsx @@ -1,7 +1,5 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; - import PivotByLocationAction from "../actions/PivotByLocationAction"; import type { diff --git a/frontend/src/metabase/qb/components/drill/PivotByTimeDrill.jsx b/frontend/src/metabase/qb/components/drill/PivotByTimeDrill.jsx index 110d5c12336756e9c662a951576c13c3f9636d3c..9d2c7969d1bb443e5540abfbdafab20323422031 100644 --- a/frontend/src/metabase/qb/components/drill/PivotByTimeDrill.jsx +++ b/frontend/src/metabase/qb/components/drill/PivotByTimeDrill.jsx @@ -1,7 +1,5 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; - import PivotByTimeAction from "../actions/PivotByTimeAction"; import type { diff --git a/frontend/src/metabase/qb/components/gui/AggregationPopover.jsx b/frontend/src/metabase/qb/components/gui/AggregationPopover.jsx index e5ff6ff997038f31d6aaf371033c6f3f9dceded0..7e5e0bf1b9fd81dd59c7189db6736f7c7cb25191 100644 --- a/frontend/src/metabase/qb/components/gui/AggregationPopover.jsx +++ b/frontend/src/metabase/qb/components/gui/AggregationPopover.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React from "react"; import AggPopover from "metabase/query_builder/components/AggregationPopover"; diff --git a/frontend/src/metabase/qb/components/gui/BreakoutPopover.jsx b/frontend/src/metabase/qb/components/gui/BreakoutPopover.jsx index 285fe299bf3f88eab7707ce725e8c95121da2897..2a985b1f39e39d68b7158d0852102b8bf08b5afb 100644 --- a/frontend/src/metabase/qb/components/gui/BreakoutPopover.jsx +++ b/frontend/src/metabase/qb/components/gui/BreakoutPopover.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React from "react"; import FieldList from "metabase/query_builder/components/FieldList.jsx"; diff --git a/frontend/src/metabase/qb/components/modes/NativeMode.jsx b/frontend/src/metabase/qb/components/modes/NativeMode.jsx index abaf45d3587774a6bc70c4f867973e8868c17a1f..debf0cea4f81a0313d1a391be1afe77d97096249 100644 --- a/frontend/src/metabase/qb/components/modes/NativeMode.jsx +++ b/frontend/src/metabase/qb/components/modes/NativeMode.jsx @@ -1,7 +1,5 @@ /* @flow */ -import React from "react"; - import type { QueryMode } from "metabase/meta/types/Visualization"; const NativeMode: QueryMode = { diff --git a/frontend/src/metabase/qb/components/modes/SegmentMode.jsx b/frontend/src/metabase/qb/components/modes/SegmentMode.jsx index 974154f82bb70f8acefd6f529d8b7ceb0f38bca0..c302cc83f025dfa3031cd437b39e38c25b463dd3 100644 --- a/frontend/src/metabase/qb/components/modes/SegmentMode.jsx +++ b/frontend/src/metabase/qb/components/modes/SegmentMode.jsx @@ -1,7 +1,5 @@ /* @flow */ -import React from "react"; - import { DEFAULT_ACTIONS } from "../actions"; import { DEFAULT_DRILLS } from "../drill"; diff --git a/frontend/src/metabase/qb/components/modes/TimeseriesMode.jsx b/frontend/src/metabase/qb/components/modes/TimeseriesMode.jsx index e60cbf8232ba836cec1afc73637919db6915aa07..8f77ec576b22fa63acda94a7586008ba404321b0 100644 --- a/frontend/src/metabase/qb/components/modes/TimeseriesMode.jsx +++ b/frontend/src/metabase/qb/components/modes/TimeseriesMode.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React from "react"; import TimeseriesGroupingWidget from "metabase/qb/components/TimeseriesGroupingWidget"; @@ -29,7 +29,7 @@ type Props = { lastRunCard: CardObject, tableMetadata: TableMetadata, setDatasetQuery: (datasetQuery: DatasetQuery) => void, - runQueryFn: () => void + runQuery: () => void }; export const TimeseriesModeFooter = (props: Props) => { diff --git a/frontend/src/metabase/query_builder/actions.js b/frontend/src/metabase/query_builder/actions.js index 3916b647c42d54ae30d8c43ceadef1f7f42897b8..4baf7bc67caf8d82235f288e7c414acf955ba8cb 100644 --- a/frontend/src/metabase/query_builder/actions.js +++ b/frontend/src/metabase/query_builder/actions.js @@ -223,7 +223,7 @@ export const initializeQB = createThunkAction(INITIALIZE_QB, (location, params) if (card && card.dataset_query && (Query.canRun(card.dataset_query.query) || card.dataset_query.type === "native")) { // NOTE: timeout to allow Parameters widget to set parameterValues setTimeout(() => - dispatch(runQuery(card, false)) + dispatch(runQuery(card, { shouldUpdateUrl: false })) , 0); } @@ -285,7 +285,7 @@ export const cancelEditing = createThunkAction(CANCEL_EDITING, () => { dispatch(loadMetadataForCard(card)); // we do this to force the indication of the fact that the card should not be considered dirty when the url is updated - dispatch(runQuery(card, false)); + dispatch(runQuery(card, { shouldUpdateUrl: false })); dispatch(updateUrl(card, { dirty: false })); MetabaseAnalytics.trackEvent("QueryBuilder", "Edit Cancel"); @@ -466,7 +466,7 @@ export const reloadCard = createThunkAction(RELOAD_CARD, () => { dispatch(loadMetadataForCard(card)); // we do this to force the indication of the fact that the card should not be considered dirty when the url is updated - dispatch(runQuery(card, false)); + dispatch(runQuery(card, { shouldUpdateUrl: false })); dispatch(updateUrl(card, { dirty: false })); return card; @@ -482,7 +482,7 @@ export const setCardAndRun = createThunkAction(SET_CARD_AND_RUN, (runCard, shoul dispatch(loadMetadataForCard(card)); - dispatch(runQuery(card, shouldUpdateUrl)); + dispatch(runQuery(card, { shouldUpdateUrl: shouldUpdateUrl })); return card; }; @@ -850,7 +850,11 @@ export const removeQueryExpression = createQueryAction( // runQuery export const RUN_QUERY = "metabase/qb/RUN_QUERY"; -export const runQuery = createThunkAction(RUN_QUERY, (card, shouldUpdateUrl = true, parameterValues, dirty) => { +export const runQuery = createThunkAction(RUN_QUERY, (card, { + shouldUpdateUrl = true, + ignoreCache = false, // currently only implemented for saved cards + parameterValues +} = {}) => { return async (dispatch, getState) => { const state = getState(); const parameters = getParameters(state); @@ -880,8 +884,12 @@ export const runQuery = createThunkAction(RUN_QUERY, (card, shouldUpdateUrl = tr const datasetQuery = applyParameters(card, parameters, parameterValues); // use the CardApi.query if the query is saved and not dirty so users with view but not create permissions can see it. - if (card.id && !cardIsDirty) { - CardApi.query({ cardId: card.id, parameters: datasetQuery.parameters }, { cancelled: cancelQueryDeferred.promise }).then(onQuerySuccess, onQueryError); + if (card.id != null && !cardIsDirty) { + CardApi.query({ + cardId: card.id, + parameters: datasetQuery.parameters, + ignore_cache: ignoreCache + }, { cancelled: cancelQueryDeferred.promise }).then(onQuerySuccess, onQueryError); } else { MetabaseApi.dataset(datasetQuery, { cancelled: cancelQueryDeferred.promise }).then(onQuerySuccess, onQueryError); } @@ -1108,8 +1116,6 @@ export const toggleDataReferenceFn = toggleDataReference; export const onBeginEditing = beginEditing; export const onCancelEditing = cancelEditing; export const setQueryModeFn = setQueryMode; -export const runQueryFn = runQuery; -export const cancelQueryFn = cancelQuery; export const setDatabaseFn = setQueryDatabase; export const setSourceTableFn = setQuerySourceTable; export const setDisplayFn = setCardVisualization; diff --git a/frontend/src/metabase/query_builder/components/ActionsWidget.jsx b/frontend/src/metabase/query_builder/components/ActionsWidget.jsx index fccb165d952d715624ed713e5424272bf87efe36..1cc9e5473c74ad2ea4b657243ef9e581310b6541 100644 --- a/frontend/src/metabase/query_builder/components/ActionsWidget.jsx +++ b/frontend/src/metabase/query_builder/components/ActionsWidget.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import Icon from "metabase/components/Icon"; import OnClickOutsideWrapper from "metabase/components/OnClickOutsideWrapper"; diff --git a/frontend/src/metabase/query_builder/components/AddClauseButton.jsx b/frontend/src/metabase/query_builder/components/AddClauseButton.jsx index 27bbc17d244484ad8102908955119be6a202fe9a..fe49f92665908f8f6dcb51ded6dd25725b2aca70 100644 --- a/frontend/src/metabase/query_builder/components/AddClauseButton.jsx +++ b/frontend/src/metabase/query_builder/components/AddClauseButton.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import Icon from "metabase/components/Icon.jsx"; import IconBorder from "metabase/components/IconBorder.jsx"; diff --git a/frontend/src/metabase/query_builder/components/AggregationPopover.jsx b/frontend/src/metabase/query_builder/components/AggregationPopover.jsx index d02705b9ebcca688e4dcee8dd40e0b7561e9c894..d4e53eef082ac454dba7c90356a38e78a1f48382 100644 --- a/frontend/src/metabase/query_builder/components/AggregationPopover.jsx +++ b/frontend/src/metabase/query_builder/components/AggregationPopover.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import AccordianList from "metabase/components/AccordianList.jsx"; import FieldList from './FieldList.jsx'; diff --git a/frontend/src/metabase/query_builder/components/AggregationWidget.jsx b/frontend/src/metabase/query_builder/components/AggregationWidget.jsx index d833d2c8d72f55115a1c26c316dfe407b4f4daad..a36a9b8e50a36607ca2b0afd41c0954125fcde88 100644 --- a/frontend/src/metabase/query_builder/components/AggregationWidget.jsx +++ b/frontend/src/metabase/query_builder/components/AggregationWidget.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import AggregationPopover from "./AggregationPopover.jsx"; import FieldName from './FieldName.jsx'; diff --git a/frontend/src/metabase/query_builder/components/BreakoutWidget.jsx b/frontend/src/metabase/query_builder/components/BreakoutWidget.jsx index 4719bfb7cdfa6374797256a4ca0b1c312a7481c5..285e31223afbbc099236b38f918956b36c667998 100644 --- a/frontend/src/metabase/query_builder/components/BreakoutWidget.jsx +++ b/frontend/src/metabase/query_builder/components/BreakoutWidget.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import FieldList from "./FieldList.jsx"; import FieldName from "./FieldName.jsx"; diff --git a/frontend/src/metabase/query_builder/components/DataSelector.jsx b/frontend/src/metabase/query_builder/components/DataSelector.jsx index 24504e412ba6229f7f328998edaede93e7f73663..1b8a48983a2c2636121fdced07ad8a41abc90b8d 100644 --- a/frontend/src/metabase/query_builder/components/DataSelector.jsx +++ b/frontend/src/metabase/query_builder/components/DataSelector.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import Icon from "metabase/components/Icon.jsx"; import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx"; diff --git a/frontend/src/metabase/query_builder/components/ExpandableString.jsx b/frontend/src/metabase/query_builder/components/ExpandableString.jsx index 90127363e4ba354f4d10e59ee0997bfd852a37a0..002cefdf5cd9ef05943077c7a27ae69fcbdd3568 100644 --- a/frontend/src/metabase/query_builder/components/ExpandableString.jsx +++ b/frontend/src/metabase/query_builder/components/ExpandableString.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import Humanize from 'humanize'; diff --git a/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx b/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx index b5c13ee78ad4490114ded991d31ec7818e5ee9d3..c6faaf9dfe4a0911ef2908d475ffb367d471e120 100644 --- a/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx +++ b/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import _ from "underscore"; import cx from "classnames"; diff --git a/frontend/src/metabase/query_builder/components/FieldList.jsx b/frontend/src/metabase/query_builder/components/FieldList.jsx index 143ad547f31595ec1e518f5f7789ddfdc3c12728..9ecf0e7efea297727035833ee0f7a4cd50a38b78 100644 --- a/frontend/src/metabase/query_builder/components/FieldList.jsx +++ b/frontend/src/metabase/query_builder/components/FieldList.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import AccordianList from "metabase/components/AccordianList.jsx"; import Icon from "metabase/components/Icon.jsx"; diff --git a/frontend/src/metabase/query_builder/components/FieldName.jsx b/frontend/src/metabase/query_builder/components/FieldName.jsx index d21e0f44cf5f2d4b39df63adcf0f633e3ffef4aa..d5aa48c5d6219ec811d8c9255e2c8482bb73bdac 100644 --- a/frontend/src/metabase/query_builder/components/FieldName.jsx +++ b/frontend/src/metabase/query_builder/components/FieldName.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import Icon from "metabase/components/Icon.jsx"; import Clearable from "./Clearable.jsx"; diff --git a/frontend/src/metabase/query_builder/components/FieldWidget.jsx b/frontend/src/metabase/query_builder/components/FieldWidget.jsx index 08232b33d1713c3622af3545349baf599f97716b..659157a747cfe053ecac30fdcd50a974ebe3c09f 100644 --- a/frontend/src/metabase/query_builder/components/FieldWidget.jsx +++ b/frontend/src/metabase/query_builder/components/FieldWidget.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import FieldList from "./FieldList.jsx"; import FieldName from "./FieldName.jsx"; diff --git a/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx b/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx index d6d99f521255d3c2346f567e42fcf0597c5dd8d8..9dee1e629a51b25e64a3275516e9f95a6f222f7d 100644 --- a/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx +++ b/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ReactDOM from "react-dom"; import AggregationWidget from './AggregationWidget.jsx'; diff --git a/frontend/src/metabase/query_builder/components/LimitWidget.jsx b/frontend/src/metabase/query_builder/components/LimitWidget.jsx index ab72989a4eb133649ec466138006b59a55305d04..a85f0069c96e2ef60abe9556295a36dea70c8b21 100644 --- a/frontend/src/metabase/query_builder/components/LimitWidget.jsx +++ b/frontend/src/metabase/query_builder/components/LimitWidget.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import cx from "classnames"; diff --git a/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx b/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx index a68ac9cb3ec9b4be627ff43cf2fda37e950c0809..645519774829e6067f5ae4a151203d72d76bf3da 100644 --- a/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx +++ b/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx @@ -1,7 +1,8 @@ /*global ace*/ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ReactDOM from "react-dom"; import "./NativeQueryEditor.css"; @@ -90,7 +91,7 @@ export default class NativeQueryEditor extends Component { nativeDatabases: PropTypes.array.isRequired, datasetQuery: PropTypes.object.isRequired, setDatasetQuery: PropTypes.func.isRequired, - runQueryFn: PropTypes.func.isRequired, + runQuery: PropTypes.func.isRequired, setDatabaseFn: PropTypes.func.isRequired, autocompleteResultsFn: PropTypes.func.isRequired, isOpen: PropTypes.bool, @@ -163,10 +164,10 @@ export default class NativeQueryEditor extends Component { const selectedText = this._editor.getSelectedText(); if (selectedText) { const temporaryCard = assocIn(card, ["dataset_query", "native", "query"], selectedText); - this.props.runQueryFn(temporaryCard, false, null, true); + this.props.runQuery(temporaryCard, { shouldUpdateUrl: false }); } } else { - this.props.runQueryFn(); + this.props.runQuery(); } } } diff --git a/frontend/src/metabase/query_builder/components/QueryDefinitionTooltip.jsx b/frontend/src/metabase/query_builder/components/QueryDefinitionTooltip.jsx index d86211846e3dc53b74ec7653ade9deee383bb5fd..d7fa933a7dfee7e152f3e9c82340cab0264f0329 100644 --- a/frontend/src/metabase/query_builder/components/QueryDefinitionTooltip.jsx +++ b/frontend/src/metabase/query_builder/components/QueryDefinitionTooltip.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import FilterList from "./filters/FilterList.jsx"; import AggregationWidget from "./AggregationWidget.jsx"; diff --git a/frontend/src/metabase/query_builder/components/QueryDownloadWidget.jsx b/frontend/src/metabase/query_builder/components/QueryDownloadWidget.jsx index 58ead4423d3d5bfc1e7931c0bb4b6f11bb1d2664..0726bbdc7a7ab439a2c3ae241e583881b85743a9 100644 --- a/frontend/src/metabase/query_builder/components/QueryDownloadWidget.jsx +++ b/frontend/src/metabase/query_builder/components/QueryDownloadWidget.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx"; import Icon from "metabase/components/Icon.jsx"; @@ -7,7 +8,7 @@ import Tooltip from "metabase/components/Tooltip.jsx"; import FieldSet from "metabase/components/FieldSet.jsx"; -import Urls from "metabase/lib/urls"; +import * as Urls from "metabase/lib/urls"; import _ from "underscore"; import cx from "classnames"; diff --git a/frontend/src/metabase/query_builder/components/QueryHeader.jsx b/frontend/src/metabase/query_builder/components/QueryHeader.jsx index 2e61da66556b74a6600bced942a4b3457a78ee0d..046d39df20344c0c18fe931d4b1d872140ad83ca 100644 --- a/frontend/src/metabase/query_builder/components/QueryHeader.jsx +++ b/frontend/src/metabase/query_builder/components/QueryHeader.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { Link } from "react-router"; import QueryModeButton from "./QueryModeButton.jsx"; @@ -23,7 +24,7 @@ import { CardApi, RevisionApi } from "metabase/services"; import MetabaseAnalytics from "metabase/lib/analytics"; import Query from "metabase/lib/query"; import { cancelable } from "metabase/lib/promise"; -import Urls from "metabase/lib/urls"; +import * as Urls from "metabase/lib/urls"; import cx from "classnames"; import _ from "underscore"; diff --git a/frontend/src/metabase/query_builder/components/QueryModeButton.jsx b/frontend/src/metabase/query_builder/components/QueryModeButton.jsx index 6e0fd438dc233057f7d54f366a3254da773b9b47..47f0890a29fe8f2927f0665df26a90e63c5f80c5 100644 --- a/frontend/src/metabase/query_builder/components/QueryModeButton.jsx +++ b/frontend/src/metabase/query_builder/components/QueryModeButton.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import cx from "classnames"; import { formatSQL, capitalize } from "metabase/lib/formatting"; diff --git a/frontend/src/metabase/query_builder/components/QueryVisualization.jsx b/frontend/src/metabase/query_builder/components/QueryVisualization.jsx index c236ebd05c6a7eca95310cfbf2ae8f178ae4a85a..5cdc602c9e48f5dcd5576eb669e6c44c0b2c5a93 100644 --- a/frontend/src/metabase/query_builder/components/QueryVisualization.jsx +++ b/frontend/src/metabase/query_builder/components/QueryVisualization.jsx @@ -1,7 +1,12 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { Link } from "react-router"; import LoadingSpinner from 'metabase/components/LoadingSpinner.jsx'; +import Tooltip from "metabase/components/Tooltip"; +import Icon from "metabase/components/Icon"; +import ShrinkableList from "metabase/components/ShrinkableList"; + import RunButton from './RunButton.jsx'; import VisualizationSettings from './VisualizationSettings.jsx'; @@ -12,12 +17,16 @@ import Warnings from "./Warnings.jsx"; import QueryDownloadWidget from "./QueryDownloadWidget.jsx"; import QuestionEmbedWidget from "../containers/QuestionEmbedWidget"; -import { formatNumber, inflect } from "metabase/lib/formatting"; +import { formatNumber, inflect, duration } from "metabase/lib/formatting"; import Utils from "metabase/lib/utils"; import MetabaseSettings from "metabase/lib/settings"; +import * as Urls from "metabase/lib/urls"; import cx from "classnames"; import _ from "underscore"; +import moment from "moment"; + +const REFRESH_TOOLTIP_THRESHOLD = 30 * 1000; // 30 seconds export default class QueryVisualization extends Component { constructor(props, context) { @@ -39,8 +48,8 @@ export default class QueryVisualization extends Component { cellClickedFn: PropTypes.func, isRunning: PropTypes.bool.isRequired, isRunnable: PropTypes.bool.isRequired, - runQueryFn: PropTypes.func.isRequired, - cancelQueryFn: PropTypes.func + runQuery: PropTypes.func.isRequired, + cancelQuery: PropTypes.func }; static defaultProps = { @@ -62,44 +71,85 @@ export default class QueryVisualization extends Component { } } - queryIsDirty() { - // a query is considered dirty if ANY part of it has been changed - return ( - !Utils.equals(this.props.card.dataset_query, this.state.lastRunDatasetQuery) || - !Utils.equals(this.props.parameterValues, this.state.lastRunParameterValues) - ); - } - isChartDisplay(display) { return (display !== "table" && display !== "scalar"); } + runQuery = () => { + this.props.runQuery(null, { ignoreCache: true }); + } + renderHeader() { - const { isObjectDetail, isRunnable, isRunning, isAdmin, card, result, runQueryFn, cancelQueryFn } = this.props; - const isDirty = this.queryIsDirty(); + const { isObjectDetail, isRunnable, isRunning, isResultDirty, isAdmin, card, result, cancelQuery } = this.props; const isSaved = card.id != null; + + let runButtonTooltip; + if (!isResultDirty && result && result.cached && result.average_execution_time > REFRESH_TOOLTIP_THRESHOLD) { + runButtonTooltip = `This question will take approximately ${duration(result.average_execution_time)} to refresh`; + } + + const messages = []; + if (result && result.cached) { + messages.push({ + icon: "clock", + message: ( + <div> + Updated {moment(result.updated_at).fromNow()} + </div> + ) + }) + } + if (result && result.data && !isObjectDetail && card.display === "table") { + messages.push({ + icon: "table2", + message: ( + <div> + { result.data.rows_truncated != null ? ("Showing first ") : ("Showing ")} + <strong>{formatNumber(result.row_count)}</strong> + { " " + inflect("row", result.data.rows.length) } + </div> + ) + }) + } + const isPublicLinksEnabled = MetabaseSettings.get("public_sharing"); const isEmbeddingEnabled = MetabaseSettings.get("embedding"); return ( - <div className="relative flex flex-no-shrink mt3 mb1" style={{ minHeight: "2em" }}> - <span className="relative z4"> + <div className="relative flex align-center flex-no-shrink mt2 mb1" style={{ minHeight: "2em" }}> + <div className="z4 flex-full"> { !isObjectDetail && <VisualizationSettings ref="settings" {...this.props} /> } - </span> - <div className="absolute flex layout-centered left right z3"> - <RunButton - isRunnable={isRunnable} - isDirty={isDirty} - isRunning={isRunning} - onRun={runQueryFn} - onCancel={cancelQueryFn} - /> </div> - <div className="absolute right z4 flex align-center" style={{ lineHeight: 0 /* needed to align icons :-/ */ }}> - { !isDirty && this.renderCount() } + <div className="z3"> + <Tooltip tooltip={runButtonTooltip}> + <RunButton + isRunnable={isRunnable} + isDirty={isResultDirty} + isRunning={isRunning} + onRun={this.runQuery} + onCancel={cancelQuery} + /> + </Tooltip> + </div> + <div className="z4 flex-full flex align-center justify-end" style={{ lineHeight: 0 /* needed to align icons :-/ */ }}> + <ShrinkableList + className="flex" + items={messages} + renderItem={(item) => + <div className="flex-no-shrink flex align-center mx2 h5 text-grey-4"> + <Icon className="mr1" name={item.icon} size={12} /> + {item.message} + </div> + } + renderItemSmall={(item) => + <Tooltip tooltip={<div className="p1">{item.message}</div>}> + <Icon className="mx1" name={item.icon} size={16} /> + </Tooltip> + } + /> { !isObjectDetail && - <Warnings warnings={this.state.warnings} className="mx2" size={18} /> + <Warnings warnings={this.state.warnings} className="mx1" size={18} /> } - { !isDirty && result && !result.error ? + { !isResultDirty && result && !result.error ? <QueryDownloadWidget className="mx1" card={card} @@ -120,19 +170,6 @@ export default class QueryVisualization extends Component { ); } - renderCount() { - let { result, isObjectDetail, card } = this.props; - if (result && result.data && !isObjectDetail && card.display === "table") { - return ( - <div> - { result.data.rows_truncated != null ? ("Showing first ") : ("Showing ")} - <b>{formatNumber(result.row_count)}</b> - { " " + inflect("row", result.data.rows.length) }. - </div> - ); - } - } - render() { const { className, card, databases, isObjectDetail, isRunning, result } = this.props let viz; @@ -188,5 +225,5 @@ export default class QueryVisualization extends Component { const VisualizationEmptyState = ({showTutorialLink}) => <div className="flex full layout-centered text-grey-1 flex-column"> <h1>If you give me some data I can show you something cool. Run a Query!</h1> - { showTutorialLink && <Link to="/q#?tutorial" className="link cursor-pointer my2">How do I use this thing?</Link> } + { showTutorialLink && <Link to={Urls.question(null, "?tutorial")} className="link cursor-pointer my2">How do I use this thing?</Link> } </div> diff --git a/frontend/src/metabase/query_builder/components/QueryVisualizationObjectDetailTable.jsx b/frontend/src/metabase/query_builder/components/QueryVisualizationObjectDetailTable.jsx index df548770d3f4ad88d7c218894ce7787d4f619d4d..de4b791b3fa3fd5a58184e4e1566958f0e08dd02 100644 --- a/frontend/src/metabase/query_builder/components/QueryVisualizationObjectDetailTable.jsx +++ b/frontend/src/metabase/query_builder/components/QueryVisualizationObjectDetailTable.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ExpandableString from './ExpandableString.jsx'; import Icon from 'metabase/components/Icon.jsx'; diff --git a/frontend/src/metabase/query_builder/components/RunButton.jsx b/frontend/src/metabase/query_builder/components/RunButton.jsx index 64763834590a41f8a9f0417bb436ffa28ab4793f..91ecceca4a1fa6e6aa8bbe22c18e1ce605246ff7 100644 --- a/frontend/src/metabase/query_builder/components/RunButton.jsx +++ b/frontend/src/metabase/query_builder/components/RunButton.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import Icon from "metabase/components/Icon.jsx"; diff --git a/frontend/src/metabase/query_builder/components/SavedQuestionIntroModal.jsx b/frontend/src/metabase/query_builder/components/SavedQuestionIntroModal.jsx index c4a08ab92732f8b9166fac9bfbc44c0bd36bf13b..a96780e8789ef7856e65e810d87337ff5cc024b6 100644 --- a/frontend/src/metabase/query_builder/components/SavedQuestionIntroModal.jsx +++ b/frontend/src/metabase/query_builder/components/SavedQuestionIntroModal.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import Modal from "metabase/components/Modal.jsx"; diff --git a/frontend/src/metabase/query_builder/components/SearchBar.jsx b/frontend/src/metabase/query_builder/components/SearchBar.jsx index 02467fea4c21df781abc2082ac1298a97798dbc6..abca8cf77a553d7451d85c4abab220305fa03c55 100644 --- a/frontend/src/metabase/query_builder/components/SearchBar.jsx +++ b/frontend/src/metabase/query_builder/components/SearchBar.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import ReactDOM from "react-dom"; export default class SearchBar extends React.Component { diff --git a/frontend/src/metabase/query_builder/components/SelectionModule.jsx b/frontend/src/metabase/query_builder/components/SelectionModule.jsx index 2bd4a20d9741551b5c557a10c4b307ee32f235ca..3224055dd7097aabaa793cbe254834b892fb1e8a 100644 --- a/frontend/src/metabase/query_builder/components/SelectionModule.jsx +++ b/frontend/src/metabase/query_builder/components/SelectionModule.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import Popover from "metabase/components/Popover.jsx"; import Icon from "metabase/components/Icon.jsx"; diff --git a/frontend/src/metabase/query_builder/components/SortWidget.jsx b/frontend/src/metabase/query_builder/components/SortWidget.jsx index f499138daa38ccfb725ebcd1412282e5de01081b..dccce59acc8d7911bda53236b306e2127b8c2ca6 100644 --- a/frontend/src/metabase/query_builder/components/SortWidget.jsx +++ b/frontend/src/metabase/query_builder/components/SortWidget.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import Icon from "metabase/components/Icon.jsx"; import FieldWidget from './FieldWidget.jsx'; diff --git a/frontend/src/metabase/query_builder/components/TimeGroupingPopover.jsx b/frontend/src/metabase/query_builder/components/TimeGroupingPopover.jsx index 029ebb2228be2814afc33f55c52bb78ecf0c7d2e..462c94134b36f94079b7323aa464cdc66a2ecabd 100644 --- a/frontend/src/metabase/query_builder/components/TimeGroupingPopover.jsx +++ b/frontend/src/metabase/query_builder/components/TimeGroupingPopover.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { parseFieldBucketing, formatBucketing } from "metabase/lib/query_time"; diff --git a/frontend/src/metabase/query_builder/components/VisualizationError.jsx b/frontend/src/metabase/query_builder/components/VisualizationError.jsx index a43c2e264b09ef83f4a278b2226703b46fd85030..80b795c90fe77b0a274bedb252da92363cd94ca8 100644 --- a/frontend/src/metabase/query_builder/components/VisualizationError.jsx +++ b/frontend/src/metabase/query_builder/components/VisualizationError.jsx @@ -1,6 +1,7 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from "prop-types"; import MetabaseSettings from "metabase/lib/settings"; import VisualizationErrorMessage from './VisualizationErrorMessage'; diff --git a/frontend/src/metabase/query_builder/components/VisualizationErrorMessage.jsx b/frontend/src/metabase/query_builder/components/VisualizationErrorMessage.jsx index 0b2908a87dad377141cfc4b4de2a488271b9fdfa..47fb7cdcc6c120e08df7f108b272ec21fc30df60 100644 --- a/frontend/src/metabase/query_builder/components/VisualizationErrorMessage.jsx +++ b/frontend/src/metabase/query_builder/components/VisualizationErrorMessage.jsx @@ -1,6 +1,7 @@ /* eslint "react/prop-types": "warn" */ -import React, { PropTypes } from 'react'; +import React from 'react'; +import PropTypes from "prop-types"; const VisualizationErrorMessage = ({title, type, message, action}) => { return ( diff --git a/frontend/src/metabase/query_builder/components/VisualizationResult.jsx b/frontend/src/metabase/query_builder/components/VisualizationResult.jsx index 3a6c8a94d96f2c99336e86039b8ba1d8bab8e29a..d959a936de3aa3bd747248fb0e3213abc5c18bb3 100644 --- a/frontend/src/metabase/query_builder/components/VisualizationResult.jsx +++ b/frontend/src/metabase/query_builder/components/VisualizationResult.jsx @@ -1,14 +1,18 @@ /* eslint "react/prop-types": "warn" */ -import React, { PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import QueryVisualizationObjectDetailTable from './QueryVisualizationObjectDetailTable.jsx'; import VisualizationErrorMessage from './VisualizationErrorMessage'; import Visualization from "metabase/visualizations/components/Visualization.jsx"; +import { datasetContainsNoResults } from "metabase/lib/dataset"; const VisualizationResult = ({card, isObjectDetail, lastRunDatasetQuery, result, ...props}) => { + const noResults = datasetContainsNoResults(result.data); + if (isObjectDetail) { return <QueryVisualizationObjectDetailTable data={result.data} {...props} /> - } else if (result.data.rows.length === 0) { + } else if (noResults) { // successful query but there were 0 rows returned with the result return <VisualizationErrorMessage type='noRows' diff --git a/frontend/src/metabase/query_builder/components/VisualizationSettings.jsx b/frontend/src/metabase/query_builder/components/VisualizationSettings.jsx index f414f0f5435ca7789ebed3188d4df8a0e11451e4..8e6b07a5f14b754d79a49eec5ed14eb205550d6c 100644 --- a/frontend/src/metabase/query_builder/components/VisualizationSettings.jsx +++ b/frontend/src/metabase/query_builder/components/VisualizationSettings.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import Icon from "metabase/components/Icon.jsx"; import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx"; diff --git a/frontend/src/metabase/query_builder/components/dataref/DataReference.jsx b/frontend/src/metabase/query_builder/components/dataref/DataReference.jsx index d706aa202c86dff9be9f120485a698f850a5cc94..13664ab7e8761d31ca1cec679c4c5abed84933c7 100644 --- a/frontend/src/metabase/query_builder/components/dataref/DataReference.jsx +++ b/frontend/src/metabase/query_builder/components/dataref/DataReference.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import MainPane from './MainPane.jsx'; import TablePane from './TablePane.jsx'; @@ -26,7 +27,7 @@ export default class DataReference extends Component { static propTypes = { query: PropTypes.object.isRequired, onClose: PropTypes.func.isRequired, - runQueryFn: PropTypes.func.isRequired, + runQuery: PropTypes.func.isRequired, setDatasetQuery: PropTypes.func.isRequired, setDatabaseFn: PropTypes.func.isRequired, setSourceTableFn: PropTypes.func.isRequired, diff --git a/frontend/src/metabase/query_builder/components/dataref/DetailPane.jsx b/frontend/src/metabase/query_builder/components/dataref/DetailPane.jsx index f1705a42973259d9435d1f3c21a06bf7c4649b91..cd8203745c1096b91d0e0537ad6453e916822f29 100644 --- a/frontend/src/metabase/query_builder/components/dataref/DetailPane.jsx +++ b/frontend/src/metabase/query_builder/components/dataref/DetailPane.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import cx from "classnames"; diff --git a/frontend/src/metabase/query_builder/components/dataref/FieldPane.jsx b/frontend/src/metabase/query_builder/components/dataref/FieldPane.jsx index 4cdde08fdb9cbf36e103bc8b713dd75058dea42d..baa074fe261acdd2b819467a73414f92baa51f66 100644 --- a/frontend/src/metabase/query_builder/components/dataref/FieldPane.jsx +++ b/frontend/src/metabase/query_builder/components/dataref/FieldPane.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import DetailPane from "./DetailPane.jsx"; import QueryButton from "metabase/components/QueryButton.jsx"; @@ -28,7 +29,7 @@ export default class FieldPane extends Component { field: PropTypes.object.isRequired, datasetQuery: PropTypes.object, loadTableAndForeignKeysFn: PropTypes.func.isRequired, - runQueryFn: PropTypes.func.isRequired, + runQuery: PropTypes.func.isRequired, setDatasetQuery: PropTypes.func.isRequired, setCardAndRun: PropTypes.func.isRequired }; @@ -63,7 +64,7 @@ export default class FieldPane extends Component { } Query.addBreakout(datasetQuery.query, this.props.field.id); this.props.setDatasetQuery(datasetQuery); - this.props.runQueryFn(); + this.props.runQuery(); } newCard() { diff --git a/frontend/src/metabase/query_builder/components/dataref/MainPane.jsx b/frontend/src/metabase/query_builder/components/dataref/MainPane.jsx index 29841da129e10a1a8166b5e8e994e8ce97f2bfb2..7963e969fe1f3420b53b59ea02643890de9c43dd 100644 --- a/frontend/src/metabase/query_builder/components/dataref/MainPane.jsx +++ b/frontend/src/metabase/query_builder/components/dataref/MainPane.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import { isQueryable } from "metabase/lib/table"; diff --git a/frontend/src/metabase/query_builder/components/dataref/MetricPane.jsx b/frontend/src/metabase/query_builder/components/dataref/MetricPane.jsx index 9c64ed132d6340e1da8a613f2c39be54e49a4a33..527782119e3268005aaa5cee37038a7687852823 100644 --- a/frontend/src/metabase/query_builder/components/dataref/MetricPane.jsx +++ b/frontend/src/metabase/query_builder/components/dataref/MetricPane.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import DetailPane from "./DetailPane.jsx"; import QueryButton from "metabase/components/QueryButton.jsx"; @@ -26,7 +27,7 @@ export default class MetricPane extends Component { metric: PropTypes.object.isRequired, query: PropTypes.object, loadTableAndForeignKeysFn: PropTypes.func.isRequired, - runQueryFn: PropTypes.func.isRequired, + runQuery: PropTypes.func.isRequired, setDatasetQuery: PropTypes.func.isRequired, setCardAndRun: PropTypes.func.isRequired }; diff --git a/frontend/src/metabase/query_builder/components/dataref/QueryDefinition.jsx b/frontend/src/metabase/query_builder/components/dataref/QueryDefinition.jsx index 7b2c49b6f0953929249a211098636a7878f1768c..3cc284aa23362feba9ac66eeb4f56a16c261c9af 100644 --- a/frontend/src/metabase/query_builder/components/dataref/QueryDefinition.jsx +++ b/frontend/src/metabase/query_builder/components/dataref/QueryDefinition.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; import FilterList from "../filters/FilterList.jsx"; import AggregationWidget from "../AggregationWidget.jsx"; diff --git a/frontend/src/metabase/query_builder/components/dataref/SegmentPane.jsx b/frontend/src/metabase/query_builder/components/dataref/SegmentPane.jsx index 665f1a04008872e4f2ffa428bf4d7de22d686ba6..59878068c3cb032152626501c37b743ed25525f4 100644 --- a/frontend/src/metabase/query_builder/components/dataref/SegmentPane.jsx +++ b/frontend/src/metabase/query_builder/components/dataref/SegmentPane.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import DetailPane from "./DetailPane.jsx"; import QueryButton from "metabase/components/QueryButton.jsx"; @@ -27,7 +28,7 @@ export default class SegmentPane extends Component { segment: PropTypes.object.isRequired, datasetQuery: PropTypes.object, loadTableAndForeignKeysFn: PropTypes.func.isRequired, - runQueryFn: PropTypes.func.isRequired, + runQuery: PropTypes.func.isRequired, setDatasetQuery: PropTypes.func.isRequired, setCardAndRun: PropTypes.func.isRequired }; @@ -53,7 +54,7 @@ export default class SegmentPane extends Component { } Query.addFilter(datasetQuery.query, ["SEGMENT", this.props.segment.id]); this.props.setDatasetQuery(datasetQuery); - this.props.runQueryFn(); + this.props.runQuery(); } newCard() { diff --git a/frontend/src/metabase/query_builder/components/dataref/TablePane.jsx b/frontend/src/metabase/query_builder/components/dataref/TablePane.jsx index 99ff5c7e1072fb8619bb814c9ad424383a8cdc9e..206a720240a4325a4fa0aafe00b1bd9ec2860d57 100644 --- a/frontend/src/metabase/query_builder/components/dataref/TablePane.jsx +++ b/frontend/src/metabase/query_builder/components/dataref/TablePane.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import QueryButton from "metabase/components/QueryButton.jsx"; import { createCard } from "metabase/lib/card"; diff --git a/frontend/src/metabase/query_builder/components/dataref/UseForButton.jsx b/frontend/src/metabase/query_builder/components/dataref/UseForButton.jsx index 8e4254e413147e92b50c2597b5dbddfd73997282..ae3853de049591928f3167fb07426b14fa57507e 100644 --- a/frontend/src/metabase/query_builder/components/dataref/UseForButton.jsx +++ b/frontend/src/metabase/query_builder/components/dataref/UseForButton.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; import Icon from "metabase/components/Icon.jsx"; diff --git a/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.jsx b/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.jsx index d7099a9d5f57a4590873b245350c6cf43b5e5169..3c3c73f632db068d7dd0bdd69490ee3a1f325172 100644 --- a/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.jsx +++ b/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ReactDOM from "react-dom"; import S from "./ExpressionEditorTextfield.css"; diff --git a/frontend/src/metabase/query_builder/components/expressions/ExpressionWidget.jsx b/frontend/src/metabase/query_builder/components/expressions/ExpressionWidget.jsx index 19ebf2437291eb2ad60e2d23939d8b3b0b788c7f..4f592ba64c357abdaa519082a16e44cae9033d3f 100644 --- a/frontend/src/metabase/query_builder/components/expressions/ExpressionWidget.jsx +++ b/frontend/src/metabase/query_builder/components/expressions/ExpressionWidget.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from "prop-types"; import cx from "classnames"; import _ from 'underscore'; diff --git a/frontend/src/metabase/query_builder/components/expressions/Expressions.jsx b/frontend/src/metabase/query_builder/components/expressions/Expressions.jsx index f2a1360e225c7b68c4a95b8d613c12d3eb6135f7..a65797a093eb9964c20136ebb8d773b71fe3b586 100644 --- a/frontend/src/metabase/query_builder/components/expressions/Expressions.jsx +++ b/frontend/src/metabase/query_builder/components/expressions/Expressions.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import _ from "underscore"; import Icon from "metabase/components/Icon.jsx"; diff --git a/frontend/src/metabase/query_builder/components/expressions/TokenizedExpression.jsx b/frontend/src/metabase/query_builder/components/expressions/TokenizedExpression.jsx index 66163c0abab7079247c339def3aced5bc98549d7..6d56e2b263b0e8d80d4a0d3be703303085023a95 100644 --- a/frontend/src/metabase/query_builder/components/expressions/TokenizedExpression.jsx +++ b/frontend/src/metabase/query_builder/components/expressions/TokenizedExpression.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; import "./TokenizedExpression.css"; diff --git a/frontend/src/metabase/query_builder/components/expressions/TokenizedInput.jsx b/frontend/src/metabase/query_builder/components/expressions/TokenizedInput.jsx index d02dfdcc2bb84b18cafe7d176a3a52b2a3ac7371..f431a6c542f00b146a34e34a33deb270b5ef7584 100644 --- a/frontend/src/metabase/query_builder/components/expressions/TokenizedInput.jsx +++ b/frontend/src/metabase/query_builder/components/expressions/TokenizedInput.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import ReactDOM from "react-dom"; import TokenizedExpression from "./TokenizedExpression.jsx"; diff --git a/frontend/src/metabase/query_builder/components/filters/DateOperatorSelector.jsx b/frontend/src/metabase/query_builder/components/filters/DateOperatorSelector.jsx index 16eb3999fac7140f2e4c81a173ca548a267082a9..5aca73f9400d560f68e4dd0b1dddf7b17cf4fbfe 100644 --- a/frontend/src/metabase/query_builder/components/filters/DateOperatorSelector.jsx +++ b/frontend/src/metabase/query_builder/components/filters/DateOperatorSelector.jsx @@ -1,6 +1,7 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import cx from "classnames"; import { titleCase } from "humanize-plus"; diff --git a/frontend/src/metabase/query_builder/components/filters/FilterList.jsx b/frontend/src/metabase/query_builder/components/filters/FilterList.jsx index 08ebdef0e11d57cfc797da78d92f138f2451a677..b83df09ce9e0deab83d87be6c1c39caeb5a2413e 100644 --- a/frontend/src/metabase/query_builder/components/filters/FilterList.jsx +++ b/frontend/src/metabase/query_builder/components/filters/FilterList.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { findDOMNode } from 'react-dom'; import FilterWidget from './FilterWidget.jsx'; diff --git a/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx b/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx index 1f3460474b824cbce14b121188d7b1c930c2a0bd..8239053f68f3d5ed6f4e3ee143aedef348b746c6 100644 --- a/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx +++ b/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx @@ -1,6 +1,7 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import FieldList from "../FieldList.jsx"; import OperatorSelector from "./OperatorSelector.jsx"; diff --git a/frontend/src/metabase/query_builder/components/filters/FilterWidget.jsx b/frontend/src/metabase/query_builder/components/filters/FilterWidget.jsx index 495ea0ccc5d0c72f7f608dc6cc67e51f0b5cb296..274faf29b88153bb0edfc9f561974b1780d1c30c 100644 --- a/frontend/src/metabase/query_builder/components/filters/FilterWidget.jsx +++ b/frontend/src/metabase/query_builder/components/filters/FilterWidget.jsx @@ -1,6 +1,7 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import Icon from "metabase/components/Icon.jsx"; import FieldName from '../FieldName.jsx'; diff --git a/frontend/src/metabase/query_builder/components/filters/OperatorSelector.jsx b/frontend/src/metabase/query_builder/components/filters/OperatorSelector.jsx index 4d5ce7a240fc237ba19ecda0b730b99a71392bbd..b6acdcbde667b1da3b90b8b1f7d05738d88f64cc 100644 --- a/frontend/src/metabase/query_builder/components/filters/OperatorSelector.jsx +++ b/frontend/src/metabase/query_builder/components/filters/OperatorSelector.jsx @@ -1,6 +1,7 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import Icon from "metabase/components/Icon.jsx"; diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker.jsx index bfb8e9cd2d56f47d3a05674c6a7435c0a4401392..c0152c54148cbdc7f2afc64b6e93fb346ef4cbce 100644 --- a/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker.jsx +++ b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker.jsx @@ -1,6 +1,7 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import SpecificDatePicker from "./SpecificDatePicker"; import RelativeDatePicker, { DATE_PERIODS, UnitPicker } from "./RelativeDatePicker"; diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/NumberPicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/NumberPicker.jsx index 204594f76215c21ba2319f6c856d8f58fbd6a7a6..8fa6e7098ee5d13911898bdcb72c77c92139c8c0 100644 --- a/frontend/src/metabase/query_builder/components/filters/pickers/NumberPicker.jsx +++ b/frontend/src/metabase/query_builder/components/filters/pickers/NumberPicker.jsx @@ -1,6 +1,7 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import TextPicker from "./TextPicker.jsx"; @@ -23,7 +24,13 @@ export default class NumberPicker extends Component<*, Props, State> { constructor(props: Props) { super(props); this.state = { - stringValues: props.values.map(v => String(v || "")), + stringValues: props.values.map(v => { + if(typeof v === 'number') { + return String(v) + } else { + return String(v || "") + } + }), validations: this._validate(props.values) } } diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/RelativeDatePicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/RelativeDatePicker.jsx index 589a529dc9c10ad0a879c10f09ca1b6ac84d7399..50be208d1497dc3402dfa607edd617d965950537 100644 --- a/frontend/src/metabase/query_builder/components/filters/pickers/RelativeDatePicker.jsx +++ b/frontend/src/metabase/query_builder/components/filters/pickers/RelativeDatePicker.jsx @@ -1,6 +1,7 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { pluralize, titleCase, capitalize } from "humanize-plus"; import cx from "classnames"; diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/SelectPicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/SelectPicker.jsx index a91fdd0633f186000e974962a043fdc110c55e92..040d960c5489e5592ed54c597d01b8bc44275693 100644 --- a/frontend/src/metabase/query_builder/components/filters/pickers/SelectPicker.jsx +++ b/frontend/src/metabase/query_builder/components/filters/pickers/SelectPicker.jsx @@ -1,6 +1,7 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import CheckBox from 'metabase/components/CheckBox.jsx'; import ListSearchField from "metabase/components/ListSearchField.jsx"; diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/SpecificDatePicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/SpecificDatePicker.jsx index 8b5096629906e92327da0907e198583a51227298..83d9ab5ae3a18deb2a86f772c2a31395a86a3b2a 100644 --- a/frontend/src/metabase/query_builder/components/filters/pickers/SpecificDatePicker.jsx +++ b/frontend/src/metabase/query_builder/components/filters/pickers/SpecificDatePicker.jsx @@ -1,6 +1,7 @@ /* @flow */ -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from "prop-types"; import Calendar from "metabase/components/Calendar"; import Input from "metabase/components/Input"; diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/TextPicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/TextPicker.jsx index 90755d02ea841a28eba067041e3d21da0be57ede..29e58cb2e139783c01d7327e1500acf8287d4ae8 100644 --- a/frontend/src/metabase/query_builder/components/filters/pickers/TextPicker.jsx +++ b/frontend/src/metabase/query_builder/components/filters/pickers/TextPicker.jsx @@ -1,6 +1,7 @@ /* @flow */ -import React, {Component, PropTypes} from "react"; +import React, {Component} from "react"; +import PropTypes from "prop-types"; import AutosizeTextarea from 'react-textarea-autosize'; import cx from "classnames"; diff --git a/frontend/src/metabase/query_builder/components/template_tags/TagEditorHelp.jsx b/frontend/src/metabase/query_builder/components/template_tags/TagEditorHelp.jsx index 4a43f13bf97862f1d7cadfc605e03e010a09064e..a8ecf63af1d755f9970901c6a616347137ce0197 100644 --- a/frontend/src/metabase/query_builder/components/template_tags/TagEditorHelp.jsx +++ b/frontend/src/metabase/query_builder/components/template_tags/TagEditorHelp.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; import Code from "metabase/components/Code.jsx"; 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 fc385c58253f903783ba7c8a12e63dfe919e7577..75cc35c13acf85a134fcbb12305779dc4782f7c4 100644 --- a/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx +++ b/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import Toggle from "metabase/components/Toggle.jsx"; import Select, { Option } from "metabase/components/Select.jsx"; diff --git a/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx b/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx index b1dadd198f9b755d50034d3747934360d678a21f..b969fcbd9e985811a1c885a84915c00a0f5a6802 100644 --- a/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx +++ b/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import Icon from "metabase/components/Icon.jsx"; import TagEditorParam from "./TagEditorParam.jsx"; import TagEditorHelp from "./TagEditorHelp.jsx"; diff --git a/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx b/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx index 95f367df9fa450f25cb94ddc88ed670c35a0138b..d934891be8cb6a19b532b1d3611cda766ee24d69 100644 --- a/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx +++ b/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import ReactDOM from "react-dom"; import { connect } from "react-redux"; import cx from "classnames"; @@ -154,9 +154,9 @@ export default class QueryBuilder extends Component { if (nextProps.location.action === "POP" && getURL(nextProps.location) !== getURL(this.props.location)) { this.props.popState(nextProps.location); - } else if (this.props.location.query.tutorial === undefined && nextProps.location.query.tutorial !== undefined) { + } else if (this.props.location.hash !== "#?tutorial" && nextProps.location.hash === "#?tutorial") { this.props.initializeQB(nextProps.location, nextProps.params); - } else if (getURL(nextProps.location) === "/q" && getURL(this.props.location) !== "/q") { + } else if (getURL(nextProps.location) === "/question" && getURL(this.props.location) !== "/question") { this.props.initializeQB(nextProps.location, nextProps.params); } } diff --git a/frontend/src/metabase/query_builder/containers/QuestionEmbedWidget.jsx b/frontend/src/metabase/query_builder/containers/QuestionEmbedWidget.jsx index d285d4c94d8dc3c1d6b1402e76bbc91ca5a3f36c..5ad4562fd5611ea0d4e2b96a4bacbfec9eab98fe 100644 --- a/frontend/src/metabase/query_builder/containers/QuestionEmbedWidget.jsx +++ b/frontend/src/metabase/query_builder/containers/QuestionEmbedWidget.jsx @@ -1,11 +1,11 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { connect } from "react-redux"; import EmbedWidget from "metabase/public/components/widgets/EmbedWidget"; -import Urls from "metabase/lib/urls"; +import * as Urls from "metabase/lib/urls"; import { getParameters } from "metabase/meta/Card"; import { createPublicLink, deletePublicLink, updateEnableEmbedding, updateEmbeddingParams, } from "../actions"; diff --git a/frontend/src/metabase/questions/collections.js b/frontend/src/metabase/questions/collections.js index 98ffd30aa0967674657997ae18af6a66f19d9069..592417105a325b7ba5ad1890a68ff19bc568216d 100644 --- a/frontend/src/metabase/questions/collections.js +++ b/frontend/src/metabase/questions/collections.js @@ -2,7 +2,7 @@ import { createAction, createThunkAction, handleActions, combineReducers } from "metabase/lib/redux"; import { reset } from 'redux-form'; import { replace } from "react-router-redux"; -import Urls from "metabase/lib/urls"; +import * as Urls from "metabase/lib/urls"; import _ from "underscore"; diff --git a/frontend/src/metabase/questions/components/ActionHeader.jsx b/frontend/src/metabase/questions/components/ActionHeader.jsx index 041e8cf186daea51db034576691c5504c0b40ad9..a0eb0cea3e13c5b3c2f265158ebbaab2374e0411 100644 --- a/frontend/src/metabase/questions/components/ActionHeader.jsx +++ b/frontend/src/metabase/questions/components/ActionHeader.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import S from "./ActionHeader.css"; import StackedCheckBox from "metabase/components/StackedCheckBox.jsx"; diff --git a/frontend/src/metabase/questions/components/ArchivedItem.jsx b/frontend/src/metabase/questions/components/ArchivedItem.jsx index bcfe9200e77f7525f589ed616cafb90fa572aeda..babb93cfedfe2d20fc31bbee6677666baaca7bc4 100644 --- a/frontend/src/metabase/questions/components/ArchivedItem.jsx +++ b/frontend/src/metabase/questions/components/ArchivedItem.jsx @@ -1,6 +1,7 @@ /* eslint "react/prop-types": "warn" */ -import React, { PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import Icon from "metabase/components/Icon"; import Tooltip from "metabase/components/Tooltip"; diff --git a/frontend/src/metabase/questions/components/CollectionBadge.jsx b/frontend/src/metabase/questions/components/CollectionBadge.jsx index 143520b64333f5aa63cb28db9bc50eef5d7dfa91..42711f9f18a02d4dde90ea27c39e2d8c22467fbe 100644 --- a/frontend/src/metabase/questions/components/CollectionBadge.jsx +++ b/frontend/src/metabase/questions/components/CollectionBadge.jsx @@ -1,7 +1,7 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; import { Link } from "react-router"; -import Urls from "metabase/lib/urls"; +import * as Urls from "metabase/lib/urls"; import Color from "color"; import cx from "classnames"; diff --git a/frontend/src/metabase/questions/components/CollectionButtons.jsx b/frontend/src/metabase/questions/components/CollectionButtons.jsx index a27c6f4a61b0b6af4d53f2ad3a2d6dabfaa58d6b..ce2b246ec29085b3738c6aaa597d7c423232a2a5 100644 --- a/frontend/src/metabase/questions/components/CollectionButtons.jsx +++ b/frontend/src/metabase/questions/components/CollectionButtons.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { Link } from "react-router"; import cx from "classnames"; diff --git a/frontend/src/metabase/questions/components/ExpandingSearchField.jsx b/frontend/src/metabase/questions/components/ExpandingSearchField.jsx index 51ae5c303dad0d1ddefafb41b9fb0155addf4aac..b2583c2c48d7af90b15fbb8df24049270b9a3825 100644 --- a/frontend/src/metabase/questions/components/ExpandingSearchField.jsx +++ b/frontend/src/metabase/questions/components/ExpandingSearchField.jsx @@ -1,6 +1,7 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ReactDOM from "react-dom"; import cx from "classnames"; diff --git a/frontend/src/metabase/questions/components/Item.jsx b/frontend/src/metabase/questions/components/Item.jsx index b8f5003d4fd7c20809de6f5504c96cc032442fd3..533016e71cbe52afa983ed79b5bdc7734c6bea67 100644 --- a/frontend/src/metabase/questions/components/Item.jsx +++ b/frontend/src/metabase/questions/components/Item.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import { Link } from "react-router"; import cx from "classnames"; import pure from "recompose/pure"; @@ -14,7 +15,7 @@ import MoveToCollection from "../containers/MoveToCollection.jsx"; import Labels from "./Labels.jsx"; import CollectionBadge from "./CollectionBadge.jsx"; -import Urls from "metabase/lib/urls"; +import * as Urls from "metabase/lib/urls"; const ITEM_ICON_SIZE = 20; @@ -122,7 +123,7 @@ Item.propTypes = { const ItemBody = pure(({ entity, id, name, description, labels, favorite, collection, setFavorited, onEntityClick }) => <div className={S.itemBody}> <div className={cx('flex', S.itemTitle)}> - <Link to={Urls.card(id)} className={cx(S.itemName)} onClick={onEntityClick && ((e) => { e.preventDefault(); onEntityClick(entity); })}> + <Link to={Urls.question(id)} className={cx(S.itemName)} onClick={onEntityClick && ((e) => { e.preventDefault(); onEntityClick(entity); })}> {name} </Link> { collection && diff --git a/frontend/src/metabase/questions/components/LabelIconPicker.jsx b/frontend/src/metabase/questions/components/LabelIconPicker.jsx index c78d4898592af89e481c592eecbfc266d96e32d8..fe58e7058ac4253ac63d81465fc181aa7cecd813 100644 --- a/frontend/src/metabase/questions/components/LabelIconPicker.jsx +++ b/frontend/src/metabase/questions/components/LabelIconPicker.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import S from "./LabelIconPicker.css"; diff --git a/frontend/src/metabase/questions/components/LabelPicker.jsx b/frontend/src/metabase/questions/components/LabelPicker.jsx index 36d6645ea2c20a57a53ff8e9ae1113deb4d6c671..f835089581eaac5ac9a132f28971fca534f79f68 100644 --- a/frontend/src/metabase/questions/components/LabelPicker.jsx +++ b/frontend/src/metabase/questions/components/LabelPicker.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import { Link } from "react-router"; import S from "./LabelPicker.css"; diff --git a/frontend/src/metabase/questions/components/Labels.jsx b/frontend/src/metabase/questions/components/Labels.jsx index cadf7680243fc2acd3dc436d3c45eac04b524f1f..87f822aa96cbe4833aa66ce63519a43dd93568df 100644 --- a/frontend/src/metabase/questions/components/Labels.jsx +++ b/frontend/src/metabase/questions/components/Labels.jsx @@ -1,9 +1,10 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { Link } from "react-router"; import S from "./Labels.css"; import color from 'color' -import Urls from "metabase/lib/urls"; +import * as Urls from "metabase/lib/urls"; import EmojiIcon from "metabase/components/EmojiIcon.jsx" diff --git a/frontend/src/metabase/questions/components/List.jsx b/frontend/src/metabase/questions/components/List.jsx index a02ba7d094451095e2a219924479c5f576d1c3e6..cc49ac1cf0ab94d1ff5773371f55b8ae442c9533 100644 --- a/frontend/src/metabase/questions/components/List.jsx +++ b/frontend/src/metabase/questions/components/List.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import S from "./List.css"; import pure from "recompose/pure"; diff --git a/frontend/src/metabase/questions/components/SearchHeader.jsx b/frontend/src/metabase/questions/components/SearchHeader.jsx index abf67231b4c3e7d5af25570b0c216f2146a5172d..2014b6603b9dfb2ade2691eb12ff324370280245 100644 --- a/frontend/src/metabase/questions/components/SearchHeader.jsx +++ b/frontend/src/metabase/questions/components/SearchHeader.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import S from "./SearchHeader.css"; import Icon from "metabase/components/Icon.jsx"; diff --git a/frontend/src/metabase/questions/containers/ArchiveCollectionWidget.jsx b/frontend/src/metabase/questions/containers/ArchiveCollectionWidget.jsx index 63e94607239152cadd83a7460f73312604814767..3fc8b913ec6a795af113601f8e55110d7fb76786 100644 --- a/frontend/src/metabase/questions/containers/ArchiveCollectionWidget.jsx +++ b/frontend/src/metabase/questions/containers/ArchiveCollectionWidget.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { connect } from "react-redux"; import ModalWithTrigger from "metabase/components/ModalWithTrigger"; diff --git a/frontend/src/metabase/questions/containers/CollectionCreate.jsx b/frontend/src/metabase/questions/containers/CollectionCreate.jsx index d9da2e02dd9fc1cf2e17af946b2677a1e71a36a5..edb1e6a93591ac30b71f849e12987d92034fa286 100644 --- a/frontend/src/metabase/questions/containers/CollectionCreate.jsx +++ b/frontend/src/metabase/questions/containers/CollectionCreate.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { connect } from "react-redux"; import { push } from "react-router-redux"; diff --git a/frontend/src/metabase/questions/containers/CollectionEdit.jsx b/frontend/src/metabase/questions/containers/CollectionEdit.jsx index bf87420bc608b58715e444bf74e6c55ba9559260..b297607024bd6d59d9fe4f7aaa00af56314bc42d 100644 --- a/frontend/src/metabase/questions/containers/CollectionEdit.jsx +++ b/frontend/src/metabase/questions/containers/CollectionEdit.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { connect } from "react-redux"; import { goBack } from "react-router-redux"; diff --git a/frontend/src/metabase/questions/containers/CollectionPage.jsx b/frontend/src/metabase/questions/containers/CollectionPage.jsx index 45f73d2de67caf8169bab7415611dd44dd58f398..c69b15be2244a3fd6a3394eeaf68a405e4f03789 100644 --- a/frontend/src/metabase/questions/containers/CollectionPage.jsx +++ b/frontend/src/metabase/questions/containers/CollectionPage.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { connect } from "react-redux"; import { push, replace, goBack } from "react-router-redux"; diff --git a/frontend/src/metabase/questions/containers/EditLabels.jsx b/frontend/src/metabase/questions/containers/EditLabels.jsx index 45b3aaf0bce084634ae167659f656d9890b068a3..065e38a06d2a68848f67b4a4c361d48faf58d395 100644 --- a/frontend/src/metabase/questions/containers/EditLabels.jsx +++ b/frontend/src/metabase/questions/containers/EditLabels.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { connect } from "react-redux"; import S from "./EditLabels.css"; diff --git a/frontend/src/metabase/questions/containers/EntityItem.jsx b/frontend/src/metabase/questions/containers/EntityItem.jsx index 42882525c8b39a4ed1e948b8e3ef6e9e22cf5c07..12b839966c9e02d383383c0620486ac611535880 100644 --- a/frontend/src/metabase/questions/containers/EntityItem.jsx +++ b/frontend/src/metabase/questions/containers/EntityItem.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { connect } from "react-redux"; import Item from "../components/Item.jsx"; diff --git a/frontend/src/metabase/questions/containers/EntityList.jsx b/frontend/src/metabase/questions/containers/EntityList.jsx index 67cc28dfb991d4f02eb83880a22a790ce9453edd..b06cc645fec8a4b0926a0e50f3543ea828433655 100644 --- a/frontend/src/metabase/questions/containers/EntityList.jsx +++ b/frontend/src/metabase/questions/containers/EntityList.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ReactDOM from "react-dom"; import { connect } from "react-redux"; diff --git a/frontend/src/metabase/questions/containers/LabelEditorForm.jsx b/frontend/src/metabase/questions/containers/LabelEditorForm.jsx index 82f34a8191c59471f96615c8cc553765e80ce992..b1954d1ab91a3ab528fc895323fa28aeaf398f58 100644 --- a/frontend/src/metabase/questions/containers/LabelEditorForm.jsx +++ b/frontend/src/metabase/questions/containers/LabelEditorForm.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import S from "./LabelEditorForm.css"; import LabelIconPicker from "../components/LabelIconPicker.jsx"; diff --git a/frontend/src/metabase/questions/containers/LabelPopover.jsx b/frontend/src/metabase/questions/containers/LabelPopover.jsx index b1c67461ba9e8c6c31141daab2f15298ffd3b019..7530929de177fda583887c2bbbf593e44e74c364 100644 --- a/frontend/src/metabase/questions/containers/LabelPopover.jsx +++ b/frontend/src/metabase/questions/containers/LabelPopover.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { connect } from "react-redux"; import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx"; diff --git a/frontend/src/metabase/questions/containers/QuestionIndex.jsx b/frontend/src/metabase/questions/containers/QuestionIndex.jsx index e65f45470f0db9abf3497fc9ed8f43afe4161d92..e0b95acbc50cd9ce174d5086c5c9b789af5a5d2f 100644 --- a/frontend/src/metabase/questions/containers/QuestionIndex.jsx +++ b/frontend/src/metabase/questions/containers/QuestionIndex.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { connect } from "react-redux"; import { Link } from "react-router"; import Collapse from "react-collapse"; diff --git a/frontend/src/metabase/questions/questions.js b/frontend/src/metabase/questions/questions.js index 9d1970c246abe09a32a35cda55ae91100af7ecfe..056815d94ee2c1828401aafc862f112d7af16502 100644 --- a/frontend/src/metabase/questions/questions.js +++ b/frontend/src/metabase/questions/questions.js @@ -7,7 +7,7 @@ import _ from "underscore"; import { inflect } from "metabase/lib/formatting"; import MetabaseAnalytics from "metabase/lib/analytics"; -import Urls from "metabase/lib/urls"; +import * as Urls from "metabase/lib/urls"; import { push, replace } from "react-router-redux"; import { setRequestState } from "metabase/redux/requests"; diff --git a/frontend/src/metabase/reference/components/Detail.jsx b/frontend/src/metabase/reference/components/Detail.jsx index c6d76d55c70bd1ab5071e1f9cf371e00e069eca0..210d4f11167d328deb124402eb486cc10a46dff5 100644 --- a/frontend/src/metabase/reference/components/Detail.jsx +++ b/frontend/src/metabase/reference/components/Detail.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import { Link } from "react-router"; import S from "./Detail.css"; diff --git a/frontend/src/metabase/reference/components/EditButton.jsx b/frontend/src/metabase/reference/components/EditButton.jsx index fe3a34779fcd078bd9b067f3beb973177fcdca79..81d2513fef3b9ce4addac03620a50da0c6924aba 100644 --- a/frontend/src/metabase/reference/components/EditButton.jsx +++ b/frontend/src/metabase/reference/components/EditButton.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import cx from "classnames"; import pure from "recompose/pure"; diff --git a/frontend/src/metabase/reference/components/EditHeader.jsx b/frontend/src/metabase/reference/components/EditHeader.jsx index 116e82143a8300db3619a6d93b04d01bfbe5b422..5434e4844195886fc8cf6c158f24515f9ccc0e21 100644 --- a/frontend/src/metabase/reference/components/EditHeader.jsx +++ b/frontend/src/metabase/reference/components/EditHeader.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import cx from "classnames"; import pure from "recompose/pure"; diff --git a/frontend/src/metabase/reference/components/Field.jsx b/frontend/src/metabase/reference/components/Field.jsx index 7c2770a99ba320b87db62876f550c63594098fb5..c5c020605f3b18cd830e4b1bcb0fc0377da4e6d0 100644 --- a/frontend/src/metabase/reference/components/Field.jsx +++ b/frontend/src/metabase/reference/components/Field.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import { Link } from "react-router"; import * as MetabaseCore from "metabase/lib/core"; diff --git a/frontend/src/metabase/reference/components/FieldToGroupBy.jsx b/frontend/src/metabase/reference/components/FieldToGroupBy.jsx index 33fb42cbb1bc9a86eedc6d03cdcd435d8a58f3df..87852ec59defb46f0a006b2f2f0d58ef2af136c4 100644 --- a/frontend/src/metabase/reference/components/FieldToGroupBy.jsx +++ b/frontend/src/metabase/reference/components/FieldToGroupBy.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import pure from "recompose/pure"; import S from "./FieldToGroupBy.css"; @@ -42,4 +43,4 @@ FieldToGroupBy.propTypes = { secondaryOnClick: PropTypes.func }; -export default pure(FieldToGroupBy); \ No newline at end of file +export default pure(FieldToGroupBy); diff --git a/frontend/src/metabase/reference/components/FieldTypeDetail.jsx b/frontend/src/metabase/reference/components/FieldTypeDetail.jsx index ce5cda7e62e02966928188a31b4f8c6e1c722082..c42ab2572e4fcd38901e1f44b86a80d73b4be7bd 100644 --- a/frontend/src/metabase/reference/components/FieldTypeDetail.jsx +++ b/frontend/src/metabase/reference/components/FieldTypeDetail.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import cx from "classnames"; import { getIn } from "icepick"; import pure from "recompose/pure"; diff --git a/frontend/src/metabase/reference/components/FieldsToGroupBy.jsx b/frontend/src/metabase/reference/components/FieldsToGroupBy.jsx index e5b77572f10ff70486b11e751e53d45baf64bd48..48cd1535bfbd2e533b04ade920f101820355a8f3 100644 --- a/frontend/src/metabase/reference/components/FieldsToGroupBy.jsx +++ b/frontend/src/metabase/reference/components/FieldsToGroupBy.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import cx from "classnames"; import pure from "recompose/pure"; diff --git a/frontend/src/metabase/reference/components/Formula.jsx b/frontend/src/metabase/reference/components/Formula.jsx index 04819cf39f8425f0150268be20c896268802c961..4882a4d1b741312af2c100be4e5c21803db372f2 100644 --- a/frontend/src/metabase/reference/components/Formula.jsx +++ b/frontend/src/metabase/reference/components/Formula.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import pure from "recompose/pure"; import cx from "classnames"; diff --git a/frontend/src/metabase/reference/components/GuideDetail.jsx b/frontend/src/metabase/reference/components/GuideDetail.jsx index bbb27cc09b74ceff2c85ff22c45cd4925aa6add1..45b547e95adb026958df62b0c2283c582c1375d2 100644 --- a/frontend/src/metabase/reference/components/GuideDetail.jsx +++ b/frontend/src/metabase/reference/components/GuideDetail.jsx @@ -1,8 +1,11 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import { Link } from "react-router"; import pure from "recompose/pure"; import cx from "classnames"; + import Icon from "metabase/components/Icon" +import * as Urls from "metabase/lib/urls"; import { getQuestionUrl, @@ -21,7 +24,7 @@ const GuideDetail = ({ const title = entity.display_name || entity.name; const { caveats, points_of_interest } = entity; const typeToLink = { - dashboard: `/dash/${entity.id}`, + dashboard: Urls.dashboard(entity.id), metric: getQuestionUrl({ dbId: tables[entity.table_id] && tables[entity.table_id].db_id, tableId: entity.table_id, diff --git a/frontend/src/metabase/reference/components/GuideDetailEditor.jsx b/frontend/src/metabase/reference/components/GuideDetailEditor.jsx index cc3eb93c86938cd50f407ac933ca224d2971f84e..cd82f2e410b31dd39156855fb6714f02a20b4b88 100644 --- a/frontend/src/metabase/reference/components/GuideDetailEditor.jsx +++ b/frontend/src/metabase/reference/components/GuideDetailEditor.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; // FIXME: using pure seems to mess with redux form updates // import pure from "recompose/pure"; import cx from "classnames"; diff --git a/frontend/src/metabase/reference/components/GuideEditSection.jsx b/frontend/src/metabase/reference/components/GuideEditSection.jsx index f8e416a928dd48be5191f6fa7068da770931c65e..f3b30093b2448bb13c0d610ae384f176fe22eee5 100644 --- a/frontend/src/metabase/reference/components/GuideEditSection.jsx +++ b/frontend/src/metabase/reference/components/GuideEditSection.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import { Link } from "react-router"; import pure from "recompose/pure"; import cx from "classnames"; diff --git a/frontend/src/metabase/reference/components/GuideHeader.jsx b/frontend/src/metabase/reference/components/GuideHeader.jsx index 28a37f070506960c1adeef68496bc3646248381f..863e7d4f08305f80434755a1079647d5037327a1 100644 --- a/frontend/src/metabase/reference/components/GuideHeader.jsx +++ b/frontend/src/metabase/reference/components/GuideHeader.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import pure from "recompose/pure"; import EditButton from "metabase/reference/components/EditButton.jsx"; diff --git a/frontend/src/metabase/reference/components/MetricImportantFieldsDetail.jsx b/frontend/src/metabase/reference/components/MetricImportantFieldsDetail.jsx index 1fbf5afb469053cacad2a40fade6fabefaa5e3cd..f21e13d63269a23865f5366b2976204a8079c85a 100644 --- a/frontend/src/metabase/reference/components/MetricImportantFieldsDetail.jsx +++ b/frontend/src/metabase/reference/components/MetricImportantFieldsDetail.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import cx from "classnames"; import pure from "recompose/pure"; diff --git a/frontend/src/metabase/reference/components/ReferenceHeader.jsx b/frontend/src/metabase/reference/components/ReferenceHeader.jsx index 21a718a30e020e3748fb145455e567b326cf6a4e..9a5fcfedde558e2b70d27cc1fa772aa510297e15 100644 --- a/frontend/src/metabase/reference/components/ReferenceHeader.jsx +++ b/frontend/src/metabase/reference/components/ReferenceHeader.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import { Link } from "react-router"; import cx from "classnames"; import pure from "recompose/pure"; diff --git a/frontend/src/metabase/reference/components/RevisionMessageModal.jsx b/frontend/src/metabase/reference/components/RevisionMessageModal.jsx index 5e052780d7d29b164ac7bb1f3815bb4e44e6d770..282e4f54b0a8ddde81a6a8409f9831d71b9caf64 100644 --- a/frontend/src/metabase/reference/components/RevisionMessageModal.jsx +++ b/frontend/src/metabase/reference/components/RevisionMessageModal.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ModalWithTrigger from "metabase/components/ModalWithTrigger.jsx"; import ModalContent from "metabase/components/ModalContent.jsx"; diff --git a/frontend/src/metabase/reference/components/UsefulQuestions.jsx b/frontend/src/metabase/reference/components/UsefulQuestions.jsx index 0c7e49a463a77860fca9fb1122de35f00f27b1b2..bac4d8e2bcbd5ed42f67312ba03cd5ba7160784d 100644 --- a/frontend/src/metabase/reference/components/UsefulQuestions.jsx +++ b/frontend/src/metabase/reference/components/UsefulQuestions.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; +import PropTypes from "prop-types"; import cx from "classnames"; import pure from "recompose/pure"; diff --git a/frontend/src/metabase/reference/containers/ReferenceApp.jsx b/frontend/src/metabase/reference/containers/ReferenceApp.jsx index f89d6b700e4d931c4d466fabb74049a8a085986e..dbb904d0a65fc2e7d2e8b5a85799b7aef51a349a 100644 --- a/frontend/src/metabase/reference/containers/ReferenceApp.jsx +++ b/frontend/src/metabase/reference/containers/ReferenceApp.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from 'react'; +import React, { Component } from 'react'; +import PropTypes from "prop-types"; import { connect } from 'react-redux'; import Sidebar from 'metabase/components/Sidebar.jsx'; diff --git a/frontend/src/metabase/reference/containers/ReferenceEntity.jsx b/frontend/src/metabase/reference/containers/ReferenceEntity.jsx index b5d43d664d6c7a0c585bf59b9e4fe373ba92883b..45a0b6ec4ac7f76d968d2fa79cae07b745639607 100644 --- a/frontend/src/metabase/reference/containers/ReferenceEntity.jsx +++ b/frontend/src/metabase/reference/containers/ReferenceEntity.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { connect } from "react-redux"; import { reduxForm } from "redux-form"; import { push } from "react-router-redux"; diff --git a/frontend/src/metabase/reference/containers/ReferenceEntityList.jsx b/frontend/src/metabase/reference/containers/ReferenceEntityList.jsx index daf3818bd173b97955d390ef587e863b4299d9a7..3773c59cf77c34e39bcdaf1bda60ddd36d2ec68e 100644 --- a/frontend/src/metabase/reference/containers/ReferenceEntityList.jsx +++ b/frontend/src/metabase/reference/containers/ReferenceEntityList.jsx @@ -1,10 +1,12 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { connect } from "react-redux"; import moment from "moment"; import visualizations from "metabase/visualizations"; import { isQueryable } from "metabase/lib/table"; +import * as Urls from "metabase/lib/urls"; import S from "metabase/components/List.css"; import R from "metabase/reference/Reference.css"; @@ -57,7 +59,7 @@ const createListItem = (entity, index, section) => } url={section.type !== 'questions' ? `${section.id}/${entity.id}` : - `/card/${entity.id}` + Urls.question(entity.id) } icon={section.type === 'questions' ? visualizations.get(entity.display).iconName : diff --git a/frontend/src/metabase/reference/containers/ReferenceFieldsList.jsx b/frontend/src/metabase/reference/containers/ReferenceFieldsList.jsx index b680dba9f4a54aa3a360db4bd9ad12c881bd8e75..a5aebda8df7c32abe0e925d300af48dd7f7493f4 100644 --- a/frontend/src/metabase/reference/containers/ReferenceFieldsList.jsx +++ b/frontend/src/metabase/reference/containers/ReferenceFieldsList.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { connect } from "react-redux"; import { reduxForm } from "redux-form"; diff --git a/frontend/src/metabase/reference/containers/ReferenceGettingStartedGuide.jsx b/frontend/src/metabase/reference/containers/ReferenceGettingStartedGuide.jsx index a5640fb8d7295fb1b3d616207ce54a3a38edbcef..9cdedc4cc97b2a64fb9b4029c58181ca174149c2 100644 --- a/frontend/src/metabase/reference/containers/ReferenceGettingStartedGuide.jsx +++ b/frontend/src/metabase/reference/containers/ReferenceGettingStartedGuide.jsx @@ -1,13 +1,16 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { Link } from "react-router"; import { connect } from 'react-redux'; import { push } from 'react-router-redux'; import { reduxForm } from "redux-form"; + import { assoc } from "icepick"; import cx from "classnames"; import MetabaseAnalytics from "metabase/lib/analytics"; +import * as Urls from "metabase/lib/urls"; import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx"; import CreateDashboardModal from 'metabase/components/CreateDashboardModal.jsx'; @@ -225,7 +228,7 @@ export default class ReferenceGettingStartedGuide extends Component { createDashboardFn={async (newDashboard) => { try { const action = await createDashboard(newDashboard, true); - push(`/dash/${action.payload.id}`); + push(Urls.dashboard(action.payload.id)); } catch(error) { console.error(error); @@ -632,4 +635,4 @@ const AdminInstructions = ({ children }) => // eslint-disable-line react/prop-ty </div> const SectionHeader = ({ trim, children }) => // eslint-disable-line react/prop-types - <h2 className={cx('text-dark text-measure', { "mb0" : trim }, { "mb4" : !trim })}>{children}</h2> + <h2 className={cx('text-dark text-measure', { "mb0" : trim }, { "mb4" : !trim })}>{children}</h2> diff --git a/frontend/src/metabase/reference/containers/ReferenceRevisionsList.jsx b/frontend/src/metabase/reference/containers/ReferenceRevisionsList.jsx index 192b6b45e583467d2866db37a41a1228a9e17642..05465289ba7289c2adf13f311541b27a6c790217 100644 --- a/frontend/src/metabase/reference/containers/ReferenceRevisionsList.jsx +++ b/frontend/src/metabase/reference/containers/ReferenceRevisionsList.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { connect } from "react-redux"; import { getIn } from "icepick"; diff --git a/frontend/src/metabase/reference/utils.js b/frontend/src/metabase/reference/utils.js index e22840da4f48bf99e05eb563c75c6951927bee43..0a85e12cc89dcc5ec8ef40b417179cc031cae011 100644 --- a/frontend/src/metabase/reference/utils.js +++ b/frontend/src/metabase/reference/utils.js @@ -2,8 +2,9 @@ import { assoc, assocIn, chain } from "icepick"; import _ from "underscore"; import { titleize, humanize } from "metabase/lib/formatting"; -import { startNewCard, serializeCardForUrl } from "metabase/lib/card"; +import { startNewCard } from "metabase/lib/card"; import { isPK } from "metabase/lib/types"; +import * as Urls from "metabase/lib/urls"; export const idsToObjectMap = (ids, objects) => ids .map(id => objects[id]) @@ -76,11 +77,10 @@ export const tryUpdateData = async (fields, props) => { const importantFieldIds = fields.important_fields.map(field => field.id); const existingImportantFieldIds = guide.metric_important_fields && guide.metric_important_fields[entity.id]; - const areFieldIdsIdentitical = existingImportantFieldIds && + const areFieldIdsIdentitical = existingImportantFieldIds && existingImportantFieldIds.length === importantFieldIds.length && existingImportantFieldIds.every(id => importantFieldIds.includes(id)); - - console.log(areFieldIdsIdentitical); + if (!areFieldIdsIdentitical) { await updateMetricImportantFields(entity.id, importantFieldIds); tryFetchData(props); @@ -156,8 +156,8 @@ export const tryUpdateGuide = async (formFields, props) => { startLoading(); try { const updateNewEntities = ({ - entities, - formFields, + entities, + formFields, updateEntity }) => formFields.map(formField => { if (!formField.id) { @@ -175,7 +175,7 @@ export const tryUpdateGuide = async (formFields, props) => { const newEntity = entities[formField.id]; const updatedNewEntity = { - ...newEntity, + ...newEntity, ...editedEntity }; @@ -185,9 +185,9 @@ export const tryUpdateGuide = async (formFields, props) => { }); const updateOldEntities = ({ - newEntityIds, - oldEntityIds, - entities, + newEntityIds, + oldEntityIds, + entities, updateEntity }) => oldEntityIds .filter(oldEntityId => !newEntityIds.includes(oldEntityId)) @@ -201,14 +201,14 @@ export const tryUpdateGuide = async (formFields, props) => { ); const updatingOldEntity = updateEntity(updatedOldEntity); - + return [updatingOldEntity]; }); //FIXME: necessary because revision_message is a mandatory field // even though we don't actually keep track of changes to caveats/points_of_interest yet const updateWithRevisionMessage = updateEntity => entity => updateEntity(assoc( entity, - 'revision_message', + 'revision_message', 'Updated in Getting Started guide.' )); @@ -218,9 +218,9 @@ export const tryUpdateGuide = async (formFields, props) => { updateEntity: updateDashboard }) .concat(updateOldEntities({ - newEntityIds: formFields.most_important_dashboard ? + newEntityIds: formFields.most_important_dashboard ? [formFields.most_important_dashboard.id] : [], - oldEntityIds: guide.most_important_dashboard ? + oldEntityIds: guide.most_important_dashboard ? [guide.most_important_dashboard] : [], entities: dashboards, @@ -239,7 +239,7 @@ export const tryUpdateGuide = async (formFields, props) => { entities: metrics, updateEntity: updateWithRevisionMessage(updateMetric) })); - + const updatingMetricImportantFields = formFields.important_metrics .map(metricFormField => { if (!metricFormField.id || !metricFormField.important_fields) { @@ -248,17 +248,17 @@ export const tryUpdateGuide = async (formFields, props) => { const importantFieldIds = metricFormField.important_fields .map(field => field.id); const existingImportantFieldIds = guide.metric_important_fields[metricFormField.id]; - - const areFieldIdsIdentitical = existingImportantFieldIds && + + const areFieldIdsIdentitical = existingImportantFieldIds && existingImportantFieldIds.length === importantFieldIds.length && existingImportantFieldIds.every(id => importantFieldIds.includes(id)); if (areFieldIdsIdentitical) { return []; } - + return [updateMetricImportantFields(metricFormField.id, importantFieldIds)]; }); - + const segmentFields = formFields.important_segments_and_tables .filter(field => field.type === 'segment'); @@ -299,7 +299,7 @@ export const tryUpdateGuide = async (formFields, props) => { guide.contact.name !== formFields.contact.name ? [updateSetting({key: 'getting-started-contact-name', value: formFields.contact.name })] : []; - + const updatingContactEmail = guide.contact && formFields.contact && guide.contact.email !== formFields.contact.email ? [updateSetting({key: 'getting-started-contact-email', value: formFields.contact.email })] : @@ -318,7 +318,7 @@ export const tryUpdateGuide = async (formFields, props) => { if (updatingData.length > 0) { await Promise.all(updatingData); - + clearRequestState({statePath: ['reference', 'guide']}); await fetchGuide(); @@ -431,7 +431,7 @@ export const getQuestion = ({dbId, tableId, fieldId, metricId, segmentId, getCou return question; }; -export const getQuestionUrl = getQuestionArgs => `/q#${serializeCardForUrl(getQuestion(getQuestionArgs))}`; +export const getQuestionUrl = getQuestionArgs => Urls.question(null, getQuestion(getQuestionArgs)); export const isGuideEmpty = ({ things_to_know, @@ -446,7 +446,7 @@ export const isGuideEmpty = ({ most_important_dashboard ? false : important_metrics && important_metrics.length !== 0 ? false : important_segments && important_segments.length !== 0 ? false : - important_tables && important_tables.length !== 0 ? false : + important_tables && important_tables.length !== 0 ? false : true; export const typeToLinkClass = { diff --git a/frontend/src/metabase/routes-embed.jsx b/frontend/src/metabase/routes-embed.jsx index 96e5de25976a2d133116f2ebab42766940c6d3e0..6697785f74855e4906bdd4925979c69bc472c387 100644 --- a/frontend/src/metabase/routes-embed.jsx +++ b/frontend/src/metabase/routes-embed.jsx @@ -1,6 +1,6 @@ /* @flow weak */ -import React, { Component, PropTypes } from "react"; +import React from "react"; import { Route } from 'react-router'; diff --git a/frontend/src/metabase/routes-public.jsx b/frontend/src/metabase/routes-public.jsx index 0ed8e87dd228c8d5350d3f55f7e50527e9b56c68..f40e59cec01b8a3ca84cb667d5d49f9526a57be4 100644 --- a/frontend/src/metabase/routes-public.jsx +++ b/frontend/src/metabase/routes-public.jsx @@ -1,6 +1,6 @@ /* @flow weak */ -import React, { Component, PropTypes } from "react"; +import React from "react"; import { Route } from 'react-router'; diff --git a/frontend/src/metabase/routes.jsx b/frontend/src/metabase/routes.jsx index 26285e188981bda6c8cadddadaa8053905382c08..f52854442201ee62eae1d9693c4d623e074c5099 100644 --- a/frontend/src/metabase/routes.jsx +++ b/frontend/src/metabase/routes.jsx @@ -1,6 +1,6 @@ /* @flow weak */ -import React, { Component, PropTypes } from "react"; +import React from "react"; import { Route, Redirect, IndexRedirect, IndexRoute } from 'react-router'; import { routerActions } from 'react-router-redux'; @@ -151,11 +151,11 @@ export const getRoutes = (store) => <Route path="/" component={HomepageApp} /> {/* DASHBOARD */} - <Route path="/dash/:dashboardId" component={DashboardApp} /> + <Route path="/dashboard/:dashboardId" component={DashboardApp} /> {/* QUERY BUILDER */} - <Route path="/card/:cardId" component={QueryBuilder} /> - <Route path="/q" component={QueryBuilder} /> + <Route path="/question" component={QueryBuilder} /> + <Route path="/question/:cardId" component={QueryBuilder} /> {/* QUESTIONS */} <Route path="/questions"> @@ -262,13 +262,13 @@ export const getRoutes = (store) => <IndexRedirect to="/_internal/list" /> </Route> + {/* DEPRECATED */} + <Redirect from="/q" to="/question" /> + <Redirect from="/card/:cardId" to="/question/:cardId" /> + <Redirect from="/dash/:dashboardId" to="/dashboard/:dashboardId" /> + {/* MISC */} <Route path="/unauthorized" component={Unauthorized} /> <Route path="/*" component={NotFound} /> - - {/* LEGACY */} - <Redirect from="/card" to="/questions" /> - <Redirect from="/card/:cardId/:serializedCard" to="/questions/:cardId#:serializedCard" /> - <Redirect from="/q/:serializedCard" to="/q#:serializedCard" /> </Route> </Route> diff --git a/frontend/src/metabase/setup/components/CollapsedStep.jsx b/frontend/src/metabase/setup/components/CollapsedStep.jsx index 90ecad4374d914cc10adc81ceab900dec7a0a9e3..34501e614341736a446b9199f99fcd0550d8196f 100644 --- a/frontend/src/metabase/setup/components/CollapsedStep.jsx +++ b/frontend/src/metabase/setup/components/CollapsedStep.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import cx from "classnames"; import Icon from "metabase/components/Icon.jsx"; diff --git a/frontend/src/metabase/setup/components/DatabaseStep.jsx b/frontend/src/metabase/setup/components/DatabaseStep.jsx index 5bf60b9c2340522889f31d8ed4dcae340403884b..9ec39c01afd0de087400196c1d138cb70e9317f8 100644 --- a/frontend/src/metabase/setup/components/DatabaseStep.jsx +++ b/frontend/src/metabase/setup/components/DatabaseStep.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ReactDOM from "react-dom"; import StepTitle from './StepTitle.jsx' diff --git a/frontend/src/metabase/setup/components/PreferencesStep.jsx b/frontend/src/metabase/setup/components/PreferencesStep.jsx index 70a21fe00bca237c6ff46e3d2c2abdae6a931c00..bac1d80bcdcceb24ac04b1402bbdb4d6f42bfe0a 100644 --- a/frontend/src/metabase/setup/components/PreferencesStep.jsx +++ b/frontend/src/metabase/setup/components/PreferencesStep.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import MetabaseAnalytics from "metabase/lib/analytics"; import MetabaseSettings from "metabase/lib/settings"; diff --git a/frontend/src/metabase/setup/components/Setup.jsx b/frontend/src/metabase/setup/components/Setup.jsx index 0437ae223d8f347d212824f1b20ecdf9441c0c09..4c99d49bd281a92bb099203516d1052c5354c5b4 100644 --- a/frontend/src/metabase/setup/components/Setup.jsx +++ b/frontend/src/metabase/setup/components/Setup.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import { Link } from "react-router"; import LogoIcon from 'metabase/components/LogoIcon.jsx'; diff --git a/frontend/src/metabase/setup/components/StepTitle.jsx b/frontend/src/metabase/setup/components/StepTitle.jsx index 3f1cf52ba8cc5521987b58e5bbc109ab0ace4e53..48531185c97e616b2f6f8e9d1d36a805c4e18eea 100644 --- a/frontend/src/metabase/setup/components/StepTitle.jsx +++ b/frontend/src/metabase/setup/components/StepTitle.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from 'react' +import React, { Component } from 'react' +import PropTypes from "prop-types"; import Icon from "metabase/components/Icon.jsx"; export default class StepTitle extends Component { diff --git a/frontend/src/metabase/setup/components/UserStep.jsx b/frontend/src/metabase/setup/components/UserStep.jsx index 43d5f2258d2d480a6fce0cf3b0aa86c557f244b3..24c31440ee1b0042abe5af605efca2e3c5a0c7f0 100644 --- a/frontend/src/metabase/setup/components/UserStep.jsx +++ b/frontend/src/metabase/setup/components/UserStep.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ReactDOM from "react-dom"; import FormField from "metabase/components/form/FormField.jsx"; diff --git a/frontend/src/metabase/setup/containers/SetupApp.jsx b/frontend/src/metabase/setup/containers/SetupApp.jsx index bd9a7dcba0e481452e87fb0e1977b1af71340859..aa2f5617c9af0dc43021b64b5621a47d1d5b2968 100644 --- a/frontend/src/metabase/setup/containers/SetupApp.jsx +++ b/frontend/src/metabase/setup/containers/SetupApp.jsx @@ -1,5 +1,5 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { connect } from "react-redux"; import Setup from "../components/Setup.jsx"; diff --git a/frontend/src/metabase/tutorial/PageFlag.jsx b/frontend/src/metabase/tutorial/PageFlag.jsx index 2f9e7e328dfaac869f67053de5e65d61fbfe85e9..56c1ca977cf9a24f90a1cc2926f01393a6df9cca 100644 --- a/frontend/src/metabase/tutorial/PageFlag.jsx +++ b/frontend/src/metabase/tutorial/PageFlag.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import "./PageFlag.css"; diff --git a/frontend/src/metabase/tutorial/Portal.jsx b/frontend/src/metabase/tutorial/Portal.jsx index 052d4d7e092e1adf7f2b4c896600c35de0d38859..150e19b27f15a8ab6e8bffa342f710c5b551636d 100644 --- a/frontend/src/metabase/tutorial/Portal.jsx +++ b/frontend/src/metabase/tutorial/Portal.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import BodyComponent from "metabase/components/BodyComponent.jsx"; diff --git a/frontend/src/metabase/tutorial/QueryBuilderTutorial.jsx b/frontend/src/metabase/tutorial/QueryBuilderTutorial.jsx index ad9d0c1817b5c136d80f3e8c094ea6b849622aaf..24638a3850d7ec602f9cc23d1af9a0047c4473cc 100644 --- a/frontend/src/metabase/tutorial/QueryBuilderTutorial.jsx +++ b/frontend/src/metabase/tutorial/QueryBuilderTutorial.jsx @@ -1,6 +1,6 @@ /* eslint-disable react/display-name */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import Tutorial, { qs, qsWithContent } from "./Tutorial.jsx"; diff --git a/frontend/src/metabase/tutorial/Tutorial.jsx b/frontend/src/metabase/tutorial/Tutorial.jsx index 059b17e514f9c22eaf683a44b10d997d433ae2c8..81066e500a9f72c96f350a131ba899d48c484177 100644 --- a/frontend/src/metabase/tutorial/Tutorial.jsx +++ b/frontend/src/metabase/tutorial/Tutorial.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import Modal from "metabase/components/Modal.jsx"; import Popover from "metabase/components/Popover.jsx"; diff --git a/frontend/src/metabase/tutorial/TutorialModal.jsx b/frontend/src/metabase/tutorial/TutorialModal.jsx index 46037b83d126c47be500a9826d64531c896fac50..b1c2c73c5db9ebd689034bc2530fe5f450df5e5a 100644 --- a/frontend/src/metabase/tutorial/TutorialModal.jsx +++ b/frontend/src/metabase/tutorial/TutorialModal.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import Icon from "metabase/components/Icon.jsx"; diff --git a/frontend/src/metabase/user/components/SetUserPassword.jsx b/frontend/src/metabase/user/components/SetUserPassword.jsx index bcb2c03c3771116bd84d3edf69d467412398288b..85af677fc4ede5f7dbd02e211fb6086c2aa76831 100644 --- a/frontend/src/metabase/user/components/SetUserPassword.jsx +++ b/frontend/src/metabase/user/components/SetUserPassword.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ReactDOM from "react-dom"; import FormField from "metabase/components/form/FormField.jsx"; diff --git a/frontend/src/metabase/user/components/UpdateUserDetails.jsx b/frontend/src/metabase/user/components/UpdateUserDetails.jsx index c5950c24fb0bb5608d98946f78b3bdeb4eb9d01c..acff1daecbc5e3fabb3eb72368626272fd42ce13 100644 --- a/frontend/src/metabase/user/components/UpdateUserDetails.jsx +++ b/frontend/src/metabase/user/components/UpdateUserDetails.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ReactDOM from "react-dom"; import FormField from "metabase/components/form/FormField.jsx"; diff --git a/frontend/src/metabase/user/components/UserSettings.jsx b/frontend/src/metabase/user/components/UserSettings.jsx index a06bc0807cf2e1d45ca535b1f27c8aa9ae6323da..036ff8898b190217e7a2ae22610ef2bd5533cada 100644 --- a/frontend/src/metabase/user/components/UserSettings.jsx +++ b/frontend/src/metabase/user/components/UserSettings.jsx @@ -1,5 +1,6 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import cx from "classnames"; import SetUserPassword from "./SetUserPassword.jsx"; diff --git a/frontend/src/metabase/user/containers/UserSettingsApp.jsx b/frontend/src/metabase/user/containers/UserSettingsApp.jsx index 971d547953f06267eb1f79694deb960dda97b736..839412015a88510ecb1a1e01e21df1eadea18d58 100644 --- a/frontend/src/metabase/user/containers/UserSettingsApp.jsx +++ b/frontend/src/metabase/user/containers/UserSettingsApp.jsx @@ -1,5 +1,5 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { connect } from "react-redux"; import UserSettings from "../components/UserSettings.jsx"; diff --git a/frontend/src/metabase/visualizations/components/CardRenderer.jsx b/frontend/src/metabase/visualizations/components/CardRenderer.jsx index 223184ce7592bed7cae9c4abcdd023dd6c35d80f..545881c62ee9fd4451bd97e1d2533339b0e1b11e 100644 --- a/frontend/src/metabase/visualizations/components/CardRenderer.jsx +++ b/frontend/src/metabase/visualizations/components/CardRenderer.jsx @@ -1,6 +1,7 @@ /* eslint "react/prop-types": "warn" */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ReactDOM from "react-dom"; import ExplicitSize from "metabase/components/ExplicitSize.jsx"; diff --git a/frontend/src/metabase/visualizations/components/ChartClickActions.jsx b/frontend/src/metabase/visualizations/components/ChartClickActions.jsx index 0e3563eb51714831c71312168f6b487cb245f326..87c0c1d05c003ab1f4ba629c261c6df0d34e7ddf 100644 --- a/frontend/src/metabase/visualizations/components/ChartClickActions.jsx +++ b/frontend/src/metabase/visualizations/components/ChartClickActions.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import Button from "metabase/components/Button"; import Popover from "metabase/components/Popover"; diff --git a/frontend/src/metabase/visualizations/components/ChartSettings.jsx b/frontend/src/metabase/visualizations/components/ChartSettings.jsx index a7dce968035f0f5dedfa84e521392746a52d53b3..15e4f01eb7756e53560dce726c4a7eccbee05e5d 100644 --- a/frontend/src/metabase/visualizations/components/ChartSettings.jsx +++ b/frontend/src/metabase/visualizations/components/ChartSettings.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import cx from "classnames"; import { assocIn } from "icepick"; import _ from "underscore"; diff --git a/frontend/src/metabase/visualizations/components/ChartTooltip.jsx b/frontend/src/metabase/visualizations/components/ChartTooltip.jsx index 501b403030d05f48af9fe7fc1b59bb937f742384..71d2383a483ad50f68bd92d8f7fa1c2d9975f003 100644 --- a/frontend/src/metabase/visualizations/components/ChartTooltip.jsx +++ b/frontend/src/metabase/visualizations/components/ChartTooltip.jsx @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import TooltipPopover from "metabase/components/TooltipPopover.jsx" import Value from "metabase/components/Value.jsx"; diff --git a/frontend/src/metabase/visualizations/components/ChartWithLegend.jsx b/frontend/src/metabase/visualizations/components/ChartWithLegend.jsx index 9218edfcb3009c2e5de766cbaa5f26a76b66ecfb..4cb2efaf16edf22f8c1dfce215957276335f149d 100644 --- a/frontend/src/metabase/visualizations/components/ChartWithLegend.jsx +++ b/frontend/src/metabase/visualizations/components/ChartWithLegend.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import styles from "./ChartWithLegend.css"; import LegendVertical from "./LegendVertical.jsx"; diff --git a/frontend/src/metabase/visualizations/components/ChoroplethMap.jsx b/frontend/src/metabase/visualizations/components/ChoroplethMap.jsx index d390fa5d61203b5a6877e1c46ec9e2596bac3563..d576bd738b37e7a266a64dfc710bb79bca200037 100644 --- a/frontend/src/metabase/visualizations/components/ChoroplethMap.jsx +++ b/frontend/src/metabase/visualizations/components/ChoroplethMap.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import LoadingSpinner from "metabase/components/LoadingSpinner.jsx"; diff --git a/frontend/src/metabase/visualizations/components/FunnelBar.jsx b/frontend/src/metabase/visualizations/components/FunnelBar.jsx index 79454994387531948a582ba368aa0fb591bb7687..1028592b93793e6032836dd598f2569d8dab1b0e 100644 --- a/frontend/src/metabase/visualizations/components/FunnelBar.jsx +++ b/frontend/src/metabase/visualizations/components/FunnelBar.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import BarChart from "metabase/visualizations/visualizations/BarChart.jsx"; diff --git a/frontend/src/metabase/visualizations/components/FunnelNormal.jsx b/frontend/src/metabase/visualizations/components/FunnelNormal.jsx index 2f793f8d1949ac20073e631854455fb7cb360d85..104b44a799aeec7554a0e098aaf5393e78b03361 100644 --- a/frontend/src/metabase/visualizations/components/FunnelNormal.jsx +++ b/frontend/src/metabase/visualizations/components/FunnelNormal.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import cx from "classnames"; import styles from "./FunnelNormal.css"; diff --git a/frontend/src/metabase/visualizations/components/LeafletChoropleth.jsx b/frontend/src/metabase/visualizations/components/LeafletChoropleth.jsx index b958b5b4cd7890e6cb7bbd17cea5b557622fecf9..6099b9eeeec27cd5841fe10dda0396ca91dec170 100644 --- a/frontend/src/metabase/visualizations/components/LeafletChoropleth.jsx +++ b/frontend/src/metabase/visualizations/components/LeafletChoropleth.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; import { normal } from "metabase/lib/colors"; diff --git a/frontend/src/metabase/visualizations/components/LeafletMap.jsx b/frontend/src/metabase/visualizations/components/LeafletMap.jsx index e6e3d1bf109dfda4e0199a64e311acc6eaee701d..380d750eca5caa3411cc3277e909541e77ea92c1 100644 --- a/frontend/src/metabase/visualizations/components/LeafletMap.jsx +++ b/frontend/src/metabase/visualizations/components/LeafletMap.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import ReactDOM from "react-dom"; import MetabaseSettings from "metabase/lib/settings"; diff --git a/frontend/src/metabase/visualizations/components/LeafletMarkerPinMap.jsx b/frontend/src/metabase/visualizations/components/LeafletMarkerPinMap.jsx index 7daf75bba13cadbd967286b64ddcf978cb495896..fabd69a39fdf6b55a855ab6ea5593271ca5a7028 100644 --- a/frontend/src/metabase/visualizations/components/LeafletMarkerPinMap.jsx +++ b/frontend/src/metabase/visualizations/components/LeafletMarkerPinMap.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; import ReactDOM from "react-dom"; import LeafletMap from "./LeafletMap.jsx"; diff --git a/frontend/src/metabase/visualizations/components/LeafletTilePinMap.jsx b/frontend/src/metabase/visualizations/components/LeafletTilePinMap.jsx index da7b688e340f2a4792ad9d6143b5d620750030f3..9bcf98d86b3422035472c58b3a7969285904b916 100644 --- a/frontend/src/metabase/visualizations/components/LeafletTilePinMap.jsx +++ b/frontend/src/metabase/visualizations/components/LeafletTilePinMap.jsx @@ -1,4 +1,3 @@ -import React, { Component, PropTypes } from "react"; import LeafletMap from "./LeafletMap.jsx"; import L from "leaflet"; diff --git a/frontend/src/metabase/visualizations/components/LegacyChoropleth.jsx b/frontend/src/metabase/visualizations/components/LegacyChoropleth.jsx index 8059a0b659cdf9f63ca053f339304d5777e0177d..b2f9b507242659772b8da09b59248494e46ec2c2 100644 --- a/frontend/src/metabase/visualizations/components/LegacyChoropleth.jsx +++ b/frontend/src/metabase/visualizations/components/LegacyChoropleth.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { isSameSeries } from "metabase/visualizations/lib/utils"; import d3 from "d3"; diff --git a/frontend/src/metabase/visualizations/components/LegendHeader.jsx b/frontend/src/metabase/visualizations/components/LegendHeader.jsx index 0d29843b595d4362aef8c5523adc295b86978fbe..d625cbd316b0b8a9d5b43be19681b68a66d02b8c 100644 --- a/frontend/src/metabase/visualizations/components/LegendHeader.jsx +++ b/frontend/src/metabase/visualizations/components/LegendHeader.jsx @@ -1,11 +1,12 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ReactDOM from "react-dom"; import styles from "./Legend.css"; import Icon from "metabase/components/Icon.jsx"; import LegendItem from "./LegendItem.jsx"; -import Urls from "metabase/lib/urls"; +import * as Urls from "metabase/lib/urls"; import cx from "classnames"; @@ -65,7 +66,7 @@ export default class LegendHeader extends Component { key={index} title={s.card.name} description={description} - href={linkToCard && s.card.id && Urls.card(s.card.id)} + href={linkToCard && s.card.id && Urls.question(s.card.id)} color={colors[index % colors.length]} showDot={showDots} showTitle={showTitles} diff --git a/frontend/src/metabase/visualizations/components/LegendHorizontal.jsx b/frontend/src/metabase/visualizations/components/LegendHorizontal.jsx index 2e1d85296e25d47270225fe85245ca818b86c283..10600a508438ebd76db5a0329210b1ce1d1cb77b 100644 --- a/frontend/src/metabase/visualizations/components/LegendHorizontal.jsx +++ b/frontend/src/metabase/visualizations/components/LegendHorizontal.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import ReactDOM from "react-dom"; import styles from "./Legend.css"; diff --git a/frontend/src/metabase/visualizations/components/LegendItem.jsx b/frontend/src/metabase/visualizations/components/LegendItem.jsx index 9a594e4317aee5a9370cfdea9afccbbe16dae67d..f7a59666cd9906591a723ed40cd910c3a571ca7a 100644 --- a/frontend/src/metabase/visualizations/components/LegendItem.jsx +++ b/frontend/src/metabase/visualizations/components/LegendItem.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import Icon from "metabase/components/Icon.jsx"; import Tooltip from "metabase/components/Tooltip.jsx"; diff --git a/frontend/src/metabase/visualizations/components/LegendVertical.jsx b/frontend/src/metabase/visualizations/components/LegendVertical.jsx index 1f78062295f24d22472abf0520a756eb2941b127..4d0cf771e2a966aa3f541546e10a1a69fd88aaf8 100644 --- a/frontend/src/metabase/visualizations/components/LegendVertical.jsx +++ b/frontend/src/metabase/visualizations/components/LegendVertical.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import ReactDOM from "react-dom"; import styles from "./Legend.css"; diff --git a/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx b/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx index e2716e6f90dd953c56e7bee892c527d16aa3366b..07bcaa255b0b28fc887723c29a5db3a7077bbdb2 100644 --- a/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx +++ b/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx @@ -1,6 +1,7 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import CardRenderer from "./CardRenderer.jsx"; import LegendHeader from "./LegendHeader.jsx"; diff --git a/frontend/src/metabase/visualizations/components/PinMap.jsx b/frontend/src/metabase/visualizations/components/PinMap.jsx index 596864c978aec3be5776947a4373acafef1068cc..ce57b2a3263bdbc60a9ca75797ea19f57bce7d23 100644 --- a/frontend/src/metabase/visualizations/components/PinMap.jsx +++ b/frontend/src/metabase/visualizations/components/PinMap.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { hasLatitudeAndLongitudeColumns } from "metabase/lib/schema_metadata"; import { LatitudeLongitudeError } from "metabase/visualizations/lib/errors"; diff --git a/frontend/src/metabase/visualizations/components/TableInteractive.jsx b/frontend/src/metabase/visualizations/components/TableInteractive.jsx index 4e33e5d28b67367582839eca374a08741290b841..af8a77121c4f6440c91aef961e29ac1fc542d087 100644 --- a/frontend/src/metabase/visualizations/components/TableInteractive.jsx +++ b/frontend/src/metabase/visualizations/components/TableInteractive.jsx @@ -1,6 +1,7 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ReactDOM from "react-dom"; import "./TableInteractive.css"; diff --git a/frontend/src/metabase/visualizations/components/TableSimple.jsx b/frontend/src/metabase/visualizations/components/TableSimple.jsx index d2f5081be36f05185dd1b424540606134d3bdc47..db4d176f501c73b5a477d8ee7c35ce728e885c94 100644 --- a/frontend/src/metabase/visualizations/components/TableSimple.jsx +++ b/frontend/src/metabase/visualizations/components/TableSimple.jsx @@ -1,6 +1,7 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; +import PropTypes from "prop-types"; import ReactDOM from "react-dom"; import styles from "./Table.css"; diff --git a/frontend/src/metabase/visualizations/components/Visualization.jsx b/frontend/src/metabase/visualizations/components/Visualization.jsx index 404daf44cd669f772dc44af176485e21fee053fe..bfc935a93c8a4d2eb64c4c7244af5d42c5f434a1 100644 --- a/frontend/src/metabase/visualizations/components/Visualization.jsx +++ b/frontend/src/metabase/visualizations/components/Visualization.jsx @@ -1,6 +1,6 @@ /* @flow weak */ -import React, { Component, PropTypes, Element } from "react"; +import React, { Component, Element } from "react"; import ExplicitSize from "metabase/components/ExplicitSize.jsx"; import LegendHeader from "metabase/visualizations/components/LegendHeader.jsx"; @@ -15,13 +15,14 @@ import { duration, formatNumber } from "metabase/lib/formatting"; import { getVisualizationTransformed } from "metabase/visualizations"; import { getSettings } from "metabase/visualizations/lib/settings"; import { isSameSeries } from "metabase/visualizations/lib/utils"; -import Utils from "metabase/lib/utils"; +import Utils from "metabase/lib/utils"; +import { datasetContainsNoResults } from "metabase/lib/dataset"; import { getModeDrills } from "metabase/qb/lib/modes" import { MinRowsError, ChartSettingsError } from "metabase/visualizations/lib/errors"; -import { assoc, getIn, setIn } from "icepick"; +import { assoc, setIn } from "icepick"; import _ from "underscore"; import cx from "classnames"; @@ -273,7 +274,8 @@ export default class Visualization extends Component<*, Props, State> { } if (!error) { - noResults = getIn(series, [0, "data", "rows", "length"]) === 0; + // $FlowFixMe + noResults = series[0] && series[0].data && datasetContainsNoResults(series[0].data); } let extra = ( diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingColorPicker.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingColorPicker.jsx index 2a2731d01a5f75c9acaac6b3a1efc1eab116c7f4..1cbd1d953792156272f9fecf783c70e6be3bba76 100644 --- a/frontend/src/metabase/visualizations/components/settings/ChartSettingColorPicker.jsx +++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingColorPicker.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import ColorPicker from "metabase/components/ColorPicker"; diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingColorsPicker.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingColorsPicker.jsx index 740e52a670fe48a8c43a12c4283f0398e0c03696..ff7d45d05ea2087d6ec0cc17df17deaf4f04e347 100644 --- a/frontend/src/metabase/visualizations/components/settings/ChartSettingColorsPicker.jsx +++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingColorsPicker.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import ChartSettingColorPicker from "./ChartSettingColorPicker.jsx"; diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldPicker.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldPicker.jsx index 2c7e9896d4e543749099ae6338367e71e13db6a1..32b6a0c6cc4a339df7a9bd5294da23d70e28756d 100644 --- a/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldPicker.jsx +++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldPicker.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; import Icon from "metabase/components/Icon"; import cx from "classnames"; diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldsPicker.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldsPicker.jsx index 7e6ea5cc982dc8d9d68ae9788636d9212a71a23a..09463240b7e5ad01fb497df7bda19b2833540a93 100644 --- a/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldsPicker.jsx +++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldsPicker.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; import ChartSettingFieldPicker from "./ChartSettingFieldPicker.jsx"; diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingInput.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingInput.jsx index ecd0cbacdc632be6d37d292eb8b070305f5a8dfa..1f1fcfae14e1ea092a185c3a732f58334a948cfa 100644 --- a/frontend/src/metabase/visualizations/components/settings/ChartSettingInput.jsx +++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingInput.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; const ChartSettingInput = ({ value, onChange }) => <input diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingInputNumeric.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingInputNumeric.jsx index 76a3dc4f57eecf26146d066d450b08864bb4988a..9341f85c8423a90722840fda1523a7d2485b815f 100644 --- a/frontend/src/metabase/visualizations/components/settings/ChartSettingInputNumeric.jsx +++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingInputNumeric.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import cx from "classnames"; diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedFields.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedFields.jsx index f72f67e7aff88627b6862e70930b12414e4238d2..4c9df5db752a42dd18ccd311e3c0ad5f014318c7 100644 --- a/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedFields.jsx +++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedFields.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import CheckBox from "metabase/components/CheckBox.jsx"; import Icon from "metabase/components/Icon.jsx"; diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingRadio.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingRadio.jsx index 0f137687605dafb25c609a88ca4fb7e5d03c0ab6..f935c2c623e5f95537d5270efe4c99209e342d79 100644 --- a/frontend/src/metabase/visualizations/components/settings/ChartSettingRadio.jsx +++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingRadio.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; import Radio from "metabase/components/Radio.jsx"; diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingSelect.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingSelect.jsx index b8a2875d4fcf9d591e1caf2860f7cb5fad5d1378..cabe7746d865de7154e347e15eaa6efd6c173063 100644 --- a/frontend/src/metabase/visualizations/components/settings/ChartSettingSelect.jsx +++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingSelect.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; import Select from "metabase/components/Select.jsx"; diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingToggle.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingToggle.jsx index b3c5c073842e2d137d526bbdb79c2dcdc80b15c8..d0d7f6680d6a26c08a511bb162dfd75e8c229d69 100644 --- a/frontend/src/metabase/visualizations/components/settings/ChartSettingToggle.jsx +++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingToggle.jsx @@ -1,4 +1,4 @@ -import React, { Component, PropTypes } from "react"; +import React from "react"; import Toggle from "metabase/components/Toggle.jsx"; diff --git a/frontend/src/metabase/visualizations/index.js b/frontend/src/metabase/visualizations/index.js index 2e9780978050ce64be8c86797c60c999dcffb99b..0297c20a1cc69a0b886380efc8d5fdb3d7967ea5 100644 --- a/frontend/src/metabase/visualizations/index.js +++ b/frontend/src/metabase/visualizations/index.js @@ -1,7 +1,5 @@ /* @flow weak */ -import { Component } from "react"; - import Scalar from "./visualizations/Scalar.jsx"; import Progress from "./visualizations/Progress.jsx"; import Table from "./visualizations/Table.jsx"; diff --git a/frontend/src/metabase/visualizations/visualizations/AreaChart.jsx b/frontend/src/metabase/visualizations/visualizations/AreaChart.jsx index 4732041c65bd3c9ba989cca1b9192bce244fe030..6043514557e20b730e858285f8f5da3c933b0ebc 100644 --- a/frontend/src/metabase/visualizations/visualizations/AreaChart.jsx +++ b/frontend/src/metabase/visualizations/visualizations/AreaChart.jsx @@ -1,6 +1,5 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; import LineAreaBarChart from "../components/LineAreaBarChart.jsx"; import { areaRenderer } from "../lib/LineAreaBarRenderer"; diff --git a/frontend/src/metabase/visualizations/visualizations/BarChart.jsx b/frontend/src/metabase/visualizations/visualizations/BarChart.jsx index 3df88ea438cfc3d5a3e159916e1dfa8347a08637..aa2b8bf74d93325e5929a4b2484331c37277dc61 100644 --- a/frontend/src/metabase/visualizations/visualizations/BarChart.jsx +++ b/frontend/src/metabase/visualizations/visualizations/BarChart.jsx @@ -1,6 +1,5 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; import LineAreaBarChart from "../components/LineAreaBarChart.jsx"; import { barRenderer } from "../lib/LineAreaBarRenderer"; diff --git a/frontend/src/metabase/visualizations/visualizations/Funnel.jsx b/frontend/src/metabase/visualizations/visualizations/Funnel.jsx index 4fd4fb8ef028bf4a1527415d56701387613a9fb5..ab15739464b8828ff35ce45aeef0c7671bc0a3c5 100644 --- a/frontend/src/metabase/visualizations/visualizations/Funnel.jsx +++ b/frontend/src/metabase/visualizations/visualizations/Funnel.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { MinRowsError, ChartSettingsError } from "metabase/visualizations/lib/errors"; @@ -51,14 +51,14 @@ export default class Funnel extends Component<*, VisualizationProps, *> { "funnel.dimension": { section: "Data", title: "Step", - ...dimensionSetting("pie.dimension"), + ...dimensionSetting("funnel.dimension"), dashboard: false, useRawSeries: true, }, "funnel.metric": { section: "Data", title: "Measure", - ...metricSetting("pie.metric"), + ...metricSetting("funnel.metric"), dashboard: false, useRawSeries: true, }, diff --git a/frontend/src/metabase/visualizations/visualizations/LineChart.jsx b/frontend/src/metabase/visualizations/visualizations/LineChart.jsx index 922dcb00dd9ef48b723075b5b614436250f5c8c9..9b981abc43242dd86c0e7172f779db69e9e62f63 100644 --- a/frontend/src/metabase/visualizations/visualizations/LineChart.jsx +++ b/frontend/src/metabase/visualizations/visualizations/LineChart.jsx @@ -1,6 +1,5 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; import LineAreaBarChart from "../components/LineAreaBarChart.jsx"; import { lineRenderer } from "../lib/LineAreaBarRenderer"; diff --git a/frontend/src/metabase/visualizations/visualizations/Map.jsx b/frontend/src/metabase/visualizations/visualizations/Map.jsx index d552cc87a7f73032b44ff851b3bf9b78b6ae5d9e..cd65132915a7cb0863a0df028997e4994fc99110 100644 --- a/frontend/src/metabase/visualizations/visualizations/Map.jsx +++ b/frontend/src/metabase/visualizations/visualizations/Map.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import ChoroplethMap from "../components/ChoroplethMap.jsx"; import PinMap from "../components/PinMap.jsx"; diff --git a/frontend/src/metabase/visualizations/visualizations/PieChart.jsx b/frontend/src/metabase/visualizations/visualizations/PieChart.jsx index b922e95cb76714c4bd966056a2e65ff4b5922392..56e92be176230a0cbf49f72af1508f25d0397c57 100644 --- a/frontend/src/metabase/visualizations/visualizations/PieChart.jsx +++ b/frontend/src/metabase/visualizations/visualizations/PieChart.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import ReactDOM from "react-dom"; import styles from "./PieChart.css"; @@ -184,8 +184,8 @@ export default class PieChart extends Component<*, Props, *> { let value, title; if (hovered && hovered.index != null && slices[hovered.index] !== otherSlice) { - title = slices[hovered.index].key; - value = slices[hovered.index].value; + title = formatDimension(slices[hovered.index].key); + value = formatMetric(slices[hovered.index].value); } else { title = "Total"; value = formatMetric(total); diff --git a/frontend/src/metabase/visualizations/visualizations/Progress.jsx b/frontend/src/metabase/visualizations/visualizations/Progress.jsx index 1189f5d1afcf797b275fcd31bcb3d34e8f33c9cf..1cf8fa459e3724b283bb64da1805121b0af692ab 100644 --- a/frontend/src/metabase/visualizations/visualizations/Progress.jsx +++ b/frontend/src/metabase/visualizations/visualizations/Progress.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import ReactDOM from "react-dom"; import { formatValue } from "metabase/lib/formatting"; diff --git a/frontend/src/metabase/visualizations/visualizations/RowChart.jsx b/frontend/src/metabase/visualizations/visualizations/RowChart.jsx index c35274410df90e46718d856692f82e903fabd0e0..819123485d8e62f20621cdaa63a437a188991b4c 100644 --- a/frontend/src/metabase/visualizations/visualizations/RowChart.jsx +++ b/frontend/src/metabase/visualizations/visualizations/RowChart.jsx @@ -1,6 +1,5 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; import LineAreaBarChart from "../components/LineAreaBarChart.jsx"; import { rowRenderer } from "../lib/LineAreaBarRenderer"; diff --git a/frontend/src/metabase/visualizations/visualizations/Scalar.jsx b/frontend/src/metabase/visualizations/visualizations/Scalar.jsx index 33dda37c911ae1e6c651788909cefd9e5ad26151..f55135518c34f78701d54cab1e5d7b273b2b036f 100644 --- a/frontend/src/metabase/visualizations/visualizations/Scalar.jsx +++ b/frontend/src/metabase/visualizations/visualizations/Scalar.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import { Link } from "react-router"; import styles from "./Scalar.css"; @@ -8,7 +8,7 @@ import Icon from "metabase/components/Icon.jsx"; import Tooltip from "metabase/components/Tooltip.jsx"; import Ellipsified from "metabase/components/Ellipsified.jsx"; -import Urls from "metabase/lib/urls"; +import * as Urls from "metabase/lib/urls"; import { formatValue } from "metabase/lib/formatting"; import { TYPE } from "metabase/lib/types"; import { isNumber } from "metabase/lib/schema_metadata"; @@ -195,7 +195,7 @@ export default class Scalar extends Component<*, VisualizationProps, *> { <div className={styles.Title + " flex align-center"}> <Ellipsified tooltip={card.name}> { linkToCard ? - <Link to={Urls.card(card.id)} className="no-decoration fullscreen-normal-text fullscreen-night-text">{settings["card.title"]}</Link> + <Link to={Urls.question(card.id)} className="no-decoration fullscreen-normal-text fullscreen-night-text">{settings["card.title"]}</Link> : <span className="fullscreen-normal-text fullscreen-night-text">{settings["card.title"]}</span> } diff --git a/frontend/src/metabase/visualizations/visualizations/ScatterPlot.jsx b/frontend/src/metabase/visualizations/visualizations/ScatterPlot.jsx index 8a5c9ac2dc1eb713966092f6a3319c9a6f5daaea..034929f09cd561d47692bebb9ab2d877d4bcbc3f 100644 --- a/frontend/src/metabase/visualizations/visualizations/ScatterPlot.jsx +++ b/frontend/src/metabase/visualizations/visualizations/ScatterPlot.jsx @@ -1,6 +1,5 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; import LineAreaBarChart from "../components/LineAreaBarChart.jsx"; import { scatterRenderer } from "../lib/LineAreaBarRenderer"; diff --git a/frontend/src/metabase/visualizations/visualizations/Table.jsx b/frontend/src/metabase/visualizations/visualizations/Table.jsx index bd9477488b93f34145278e3f5ddb0e23f4e34bfc..d45ac9c86111bb8a0529872b0f51f694db5501af 100644 --- a/frontend/src/metabase/visualizations/visualizations/Table.jsx +++ b/frontend/src/metabase/visualizations/visualizations/Table.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, PropTypes } from "react"; +import React, { Component } from "react"; import TableInteractive from "../components/TableInteractive.jsx"; import TableSimple from "../components/TableSimple.jsx"; diff --git a/frontend/test/e2e/auth/login.spec.js b/frontend/test/e2e/auth/login.spec.js index d67f0476d4aff0f55317f1b3a5c71743fda8e77e..fe191cebf9c5107762286b3346a5890b20aed86a 100644 --- a/frontend/test/e2e/auth/login.spec.js +++ b/frontend/test/e2e/auth/login.spec.js @@ -55,8 +55,8 @@ describeE2E("auth/login", () => { }); it("loads the qb", async () => { - await driver.get(`${server.host}/q#eyJuYW1lIjpudWxsLCJkYXRhc2V0X3F1ZXJ5Ijp7ImRhdGFiYXNlIjoxLCJ0eXBlIjoibmF0aXZlIiwibmF0aXZlIjp7InF1ZXJ5Ijoic2VsZWN0ICdvaCBoYWkgZ3Vpc2Ug8J-QsScifSwicGFyYW1ldGVycyI6W119LCJkaXNwbGF5Ijoic2NhbGFyIiwidmlzdWFsaXphdGlvbl9zZXR0aW5ncyI6e319`); - await waitForUrl(driver, `${server.host}/q#eyJuYW1lIjpudWxsLCJkYXRhc2V0X3F1ZXJ5Ijp7ImRhdGFiYXNlIjoxLCJ0eXBlIjoibmF0aXZlIiwibmF0aXZlIjp7InF1ZXJ5Ijoic2VsZWN0ICdvaCBoYWkgZ3Vpc2Ug8J-QsScifSwicGFyYW1ldGVycyI6W119LCJkaXNwbGF5Ijoic2NhbGFyIiwidmlzdWFsaXphdGlvbl9zZXR0aW5ncyI6e319`); + await driver.get(`${server.host}/question#eyJuYW1lIjpudWxsLCJkYXRhc2V0X3F1ZXJ5Ijp7ImRhdGFiYXNlIjoxLCJ0eXBlIjoibmF0aXZlIiwibmF0aXZlIjp7InF1ZXJ5Ijoic2VsZWN0ICdvaCBoYWkgZ3Vpc2Ug8J-QsScifSwicGFyYW1ldGVycyI6W119LCJkaXNwbGF5Ijoic2NhbGFyIiwidmlzdWFsaXphdGlvbl9zZXR0aW5ncyI6e319`); + await waitForUrl(driver, `${server.host}/question#eyJuYW1lIjpudWxsLCJkYXRhc2V0X3F1ZXJ5Ijp7ImRhdGFiYXNlIjoxLCJ0eXBlIjoibmF0aXZlIiwibmF0aXZlIjp7InF1ZXJ5Ijoic2VsZWN0ICdvaCBoYWkgZ3Vpc2Ug8J-QsScifSwicGFyYW1ldGVycyI6W119LCJkaXNwbGF5Ijoic2NhbGFyIiwidmlzdWFsaXphdGlvbl9zZXR0aW5ncyI6e319`); await screenshot(driver, "screenshots/qb.png"); }); }); diff --git a/frontend/test/e2e/query_builder/query_builder.spec.js b/frontend/test/e2e/query_builder/query_builder.spec.js index 6e4106bd8722b4ed842020f59468a80d715e3e2a..9eb6aa8d69d39a6dc0d9d572ca0f2113db7db8ee 100644 --- a/frontend/test/e2e/query_builder/query_builder.spec.js +++ b/frontend/test/e2e/query_builder/query_builder.spec.js @@ -15,7 +15,7 @@ describeE2E("query_builder", () => { describe("tables", () => { it("should allow users to create pivot tables", async () => { // load the query builder and screenshot blank - await d.get("/q"); + await d.get("/question"); await d.screenshot("screenshots/qb-initial.png"); // pick the orders table (assumes database is already selected, i.e. there's only 1 database) @@ -56,7 +56,7 @@ describeE2E("query_builder", () => { describe("charts", () => { xit("should allow users to create line charts", async () => { - await d.get("/q"); + await d.get("/question"); // select orders table await d.select("#TablePicker .List-item:first-child>a").wait().click(); @@ -106,7 +106,7 @@ describeE2E("query_builder", () => { xit("should allow users to create bar charts", async () => { // load line chart - await d.get("/card/2"); + await d.get("/question/2"); // dismiss saved questions modal await d.select(".Modal .Button.Button--primary").wait().click(); diff --git a/frontend/test/e2e/query_builder/tutorial.spec.js b/frontend/test/e2e/query_builder/tutorial.spec.js index 8b6b31f589a2becb8170a0bac563025c3ad08501..9298adaf0a18211a64e7647e82286bf14ddf5504 100644 --- a/frontend/test/e2e/query_builder/tutorial.spec.js +++ b/frontend/test/e2e/query_builder/tutorial.spec.js @@ -24,7 +24,7 @@ describeE2E("tutorial", () => { await waitForElementAndClick(driver, ".Modal .Button.Button--primary"); await waitForElementAndClick(driver, ".Modal .Button.Button--primary"); - await waitForUrl(driver, `${server.host}/q`); + await waitForUrl(driver, `${server.host}/question`); await waitForElement(driver, ".Modal .Button.Button--primary"); await screenshot(driver, "screenshots/setup-tutorial-qb.png"); await waitForElementAndClick(driver, ".Modal .Button.Button--primary"); diff --git a/package.json b/package.json index 4473174e5dfd9341a9a811ba925c23e4df144749..b850a4d0ebecad6003d4242671a34a0a3eab43ef 100644 --- a/package.json +++ b/package.json @@ -34,14 +34,15 @@ "normalizr": "^3.0.2", "number-to-locale-string": "^1.0.1", "password-generator": "^2.0.1", - "react": "^15.2.1", + "prop-types": "^15.5.4", + "react": "15.5.0", "react-addons-css-transition-group": "^15.2.1", "react-addons-perf": "^15.2.1", "react-addons-shallow-compare": "^15.2.1", "react-ansi-style": "^1.0.0", "react-collapse": "^2.3.3", "react-copy-to-clipboard": "^4.2.3", - "react-dom": "^15.2.1", + "react-dom": "15.5.0", "react-draggable": "^2.2.3", "react-height": "^2.1.1", "react-motion": "^0.4.5", @@ -103,6 +104,7 @@ "flow-bin": "^0.37.4", "fs-promise": "^1.0.0", "glob": "^7.1.1", + "html-webpack-harddisk-plugin": "^0.1.0", "html-webpack-plugin": "^2.14.0", "husky": "^0.13.2", "image-diff": "^1.6.3", @@ -154,7 +156,7 @@ "test-e2e-sauce": "USE_SAUCE=true yarn run test-e2e", "build": "webpack --bail", "build-watch": "webpack --watch", - "build-hot": "NODE_ENV=hot webpack --bail && NODE_ENV=hot webpack-dev-server --progress", + "build-hot": "NODE_ENV=hot webpack-dev-server --progress", "start": "yarn run build && lein ring server", "storybook": "start-storybook -p 9001", "precommit": "lint-staged", diff --git a/project.clj b/project.clj index 9177e9125445f387d2b1e67943478345bd0c1026..f972daf60dc0175ebe9ae6ca40f91f24c996eab1 100644 --- a/project.clj +++ b/project.clj @@ -12,7 +12,7 @@ "profile" ["with-profile" "+profile" "run" "profile"] "h2" ["with-profile" "+h2-shell" "run" "-url" "jdbc:h2:./metabase.db" "-user" "" "-password" "" "-driver" "org.h2.Driver"]} :dependencies [[org.clojure/clojure "1.8.0"] - [org.clojure/core.async "0.2.395"] + [org.clojure/core.async "0.3.442"] [org.clojure/core.match "0.3.0-alpha4"] ; optimized pattern matching library for Clojure [org.clojure/core.memoize "0.5.9"] ; needed by core.match; has useful FIFO, LRU, etc. caching mechanisms [org.clojure/data.csv "0.1.3"] ; CSV parsing / generation @@ -25,7 +25,7 @@ :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.1"] ; Async HTTP library; WebSockets + [aleph "0.4.3"] ; Async HTTP library; WebSockets [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) @@ -43,13 +43,14 @@ ring/ring-core]] [com.draines/postal "2.0.2"] ; SMTP library [com.google.apis/google-api-services-analytics ; Google Analytics Java Client Library - "v3-rev136-1.22.0"] + "v3-rev139-1.22.0"] [com.google.apis/google-api-services-bigquery ; Google BigQuery Java Client Library - "v2-rev334-1.22.0"] - [com.h2database/h2 "1.4.193"] ; embedded SQL database + "v2-rev342-1.22.0"] + [com.h2database/h2 "1.4.194"] ; embedded SQL database [com.mattbertolini/liquibase-slf4j "2.0.0"] ; Java Migrations lib [com.mchange/c3p0 "0.9.5.2"] ; connection pooling library [com.novemberain/monger "3.1.0"] ; MongoDB Driver + [com.taoensso/nippy "2.13.0"] ; Fast serialization (i.e., GZIP) library for Clojure [compojure "1.5.2"] ; HTTP Routing library built on Ring [crypto-random "1.2.0"] ; library for generating cryptographically secure random bytes and strings [environ "1.1.0"] ; easy environment management @@ -67,20 +68,20 @@ :exclusions [org.slf4j/slf4j-api]] [net.sourceforge.jtds/jtds "1.3.1"] ; Open Source SQL Server driver [org.liquibase/liquibase-core "3.5.3"] ; migration management (Java lib) - [org.slf4j/slf4j-log4j12 "1.7.22"] ; abstraction for logging frameworks -- allows end user to plug in desired logging framework at deployment time - [org.yaml/snakeyaml "1.17"] ; YAML parser (required by liquibase) - [org.xerial/sqlite-jdbc "3.8.11.2"] ; SQLite driver !!! DO NOT UPGRADE THIS UNTIL UPSTREAM BUG IS FIXED -- SEE https://github.com/metabase/metabase/issues/3753 !!! + [org.slf4j/slf4j-log4j12 "1.7.25"] ; abstraction for logging frameworks -- allows end user to plug in desired logging framework at deployment time + [org.yaml/snakeyaml "1.18"] ; YAML parser (required by liquibase) + [org.xerial/sqlite-jdbc "3.16.1"] ; SQLite driver [postgresql "9.3-1102.jdbc41"] ; Postgres driver [io.crate/crate-jdbc "2.1.6"] ; Crate JDBC driver - [prismatic/schema "1.1.3"] ; Data schema declaration and validation library + [prismatic/schema "1.1.5"] ; Data schema declaration and validation library [ring/ring-jetty-adapter "1.5.1"] ; Ring adapter using Jetty webserver (used to run a Ring server for unit tests) [ring/ring-json "0.4.0"] ; Ring middleware for reading/writing JSON automatically [stencil "0.5.0"] ; Mustache templates for Clojure [toucan "1.0.2" ; Model layer, hydration, and DB utilities :exclusions [honeysql]]] :repositories [["bintray" "https://dl.bintray.com/crate/crate"]] ; Repo for Crate JDBC driver - :plugins [[lein-environ "1.0.3"] ; easy access to environment variables - [lein-ring "0.9.7" ; start the HTTP server with 'lein ring server' + :plugins [[lein-environ "1.1.0"] ; easy access to environment variables + [lein-ring "0.11.0" ; start the HTTP server with 'lein ring server' :exclusions [org.clojure/clojure]]] ; TODO - should this be a dev dependency ? :main ^:skip-aot metabase.core :manifest {"Liquibase-Package" "liquibase.change,liquibase.changelog,liquibase.database,liquibase.parser,liquibase.precondition,liquibase.datatype,liquibase.serializer,liquibase.sqlgenerator,liquibase.executor,liquibase.snapshot,liquibase.logging,liquibase.diff,liquibase.structure,liquibase.structurecompare,liquibase.lockservice,liquibase.sdk,liquibase.ext"} @@ -106,13 +107,12 @@ :docstring-checker {:include [#"^metabase"] :exclude [#"test" #"^metabase\.http-client$"]} - :profiles {:dev {:dependencies [[org.clojure/tools.nrepl "0.2.12"] ; REPL <3 - [expectations "2.1.9"] ; unit tests + :profiles {:dev {:dependencies [[expectations "2.1.9"] ; unit tests [ring/ring-mock "0.3.0"]] ; Library to create mock Ring requests for unit tests :plugins [[docstring-checker "1.0.0"] ; Check that all public vars have docstrings. Run with 'lein docstring-checker' [jonase/eastwood "0.2.3" :exclusions [org.clojure/clojure]] ; Linting - [lein-bikeshed "0.3.0"] ; Linting + [lein-bikeshed "0.4.1"] ; Linting [lein-expectations "0.0.8"] ; run unit tests with 'lein expectations' [lein-instant-cheatsheet "2.2.1" ; use awesome instant cheatsheet created by yours truly w/ 'lein instant-cheatsheet' :exclusions [org.clojure/clojure diff --git a/reset_password/metabase/reset_password/core.clj b/reset_password/metabase/reset_password/core.clj index f963a49e8dc88550ed3788842ea355119e7d5a75..510adf2a331102750cd1a5a8ccc3f54782d3249a 100644 --- a/reset_password/metabase/reset_password/core.clj +++ b/reset_password/metabase/reset_password/core.clj @@ -14,10 +14,10 @@ (defn -main [email-address] (mdb/setup-db!) - (println (format "Resetting password for %s..." email-address)) + (printf "Resetting password for %s...\n" email-address) (try - (println (format "OK [[[%s]]]" (set-reset-token! email-address))) + (printf "OK [[[%s]]]\n" (set-reset-token! email-address)) (System/exit 0) (catch Throwable e - (println (format "FAIL [[[%s]]]" (.getMessage e))) + (printf "FAIL [[[%s]]]\n" (.getMessage e)) (System/exit -1)))) diff --git a/resources/frontend_client/index_template.html b/resources/frontend_client/index_template.html index 5086fc555cda70e69ef449c369df68184726e109..573608b6bd9945517b89250107362f6c3607076a 100644 --- a/resources/frontend_client/index_template.html +++ b/resources/frontend_client/index_template.html @@ -1,5 +1,5 @@ <!DOCTYPE html> -<html lang="en" ng-app="metabase" class="no-js"> +<html lang="en"> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> @@ -12,50 +12,48 @@ <title>Metabase</title> <script type="text/javascript"> - window.MetabaseBootstrap = {{{bootstrap_json}}}; + window.MetabaseBootstrap = {{{bootstrap_json}}}; </script> </head> <body> - <div id="root" /> - <div style="display: none;" ng-controller="Metabase" ng-view /> - </body> + <div id="root"></div> + + <script type="text/javascript"> + // Load scripts asyncronously after the page has finished loading + (function () { + function loadScript(src, onload) { + var script = document.createElement('script'); + script.type = "text/javascript"; + script.async = true; + script.src = src; + if (onload) script.onload = onload; + document.body.appendChild(script); + } + loadScript('https://ajax.googleapis.com/ajax/libs/webfont/1.4.7/webfont.js', function () { + WebFont.load({ google: { families: ["Lato:n3,n4,n9"] } }); + }); + var googleAuthClientID = window.MetabaseBootstrap.google_auth_client_id; + if (googleAuthClientID) { + loadScript('https://apis.google.com/js/api:client.js'); + } + })(); + </script> - <script type="text/javascript"> - // Load scripts asyncronously after the page has finished loading - (function () { - function loadScript(src, onload) { - var script = document.createElement('script'); - script.type = "text/javascript"; - script.async = true; - script.src = src; - if (onload) script.onload = onload; - document.body.appendChild(script); - } - loadScript('https://ajax.googleapis.com/ajax/libs/webfont/1.4.7/webfont.js', function () { - WebFont.load({ google: { families: ["Lato:n3,n4,n9"] } }); - }); - - var googleAuthClientID = window.MetabaseBootstrap.google_auth_client_id; - if (googleAuthClientID) { - loadScript('https://apis.google.com/js/api:client.js'); - } - })(); - </script> - - <script> - (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ - (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), - m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) - })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); - - // if we are not doing tracking then go ahead and disable GA now so we never even track the initial pageview - const tracking = window.MetabaseBootstrap.anon_tracking_enabled; - const ga_code = window.MetabaseBootstrap.ga_code; - if (!tracking) { - window['ga-disable-'+ga_code] = true; - } - - ga('create', ga_code, 'auto'); - </script> + <script type="text/javascript"> + (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ + (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), + m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) + })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); + + // if we are not doing tracking then go ahead and disable GA now so we never even track the initial pageview + const tracking = window.MetabaseBootstrap.anon_tracking_enabled; + const ga_code = window.MetabaseBootstrap.ga_code; + if (!tracking) { + window['ga-disable-'+ga_code] = true; + } + + ga('create', ga_code, 'auto'); + </script> + </body> </html> diff --git a/resources/migrations/052_add_query_cache_table.yaml b/resources/migrations/052_add_query_cache_table.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e21e19f5047f4a544217a8faf4d822d624cae3b0 --- /dev/null +++ b/resources/migrations/052_add_query_cache_table.yaml @@ -0,0 +1,49 @@ +databaseChangeLog: + - property: + name: blob.type + value: blob + dbms: mysql,h2 + - property: + name: blob.type + value: bytea + dbms: postgresql + - changeSet: + id: 52 + author: camsaul + changes: + - createTable: + tableName: query_cache + remarks: 'Cached results of queries are stored here when using the DB-based query cache.' + columns: + - column: + name: query_hash + type: binary(32) + remarks: 'The hash of the query dictionary. (This is a 256-bit SHA3 hash of the query dict).' + constraints: + primaryKey: true + nullable: false + - column: + name: updated_at + type: datetime + remarks: 'The timestamp of when these query results were last refreshed.' + constraints: + nullable: false + - column: + name: results + type: ${blob.type} + remarks: 'Cached, compressed results of running the query with the given hash.' + constraints: + nullable: false + - createIndex: + tableName: query_cache + indexName: idx_query_cache_updated_at + columns: + column: + name: updated_at + - addColumn: + tableName: report_card + columns: + - column: + name: cache_ttl + type: int + remarks: 'The maximum time, in seconds, to return cached results for this Card rather than running a new query.' diff --git a/resources/migrations/053_add_query_table.yaml b/resources/migrations/053_add_query_table.yaml new file mode 100644 index 0000000000000000000000000000000000000000..772de999cb50a8a43200f4f043ff3034ed93d425 --- /dev/null +++ b/resources/migrations/053_add_query_table.yaml @@ -0,0 +1,22 @@ +databaseChangeLog: + - changeSet: + id: 53 + author: camsaul + changes: + - createTable: + tableName: query + remarks: 'Information (such as average execution time) for different queries that have been previously ran.' + columns: + - column: + name: query_hash + type: binary(32) + remarks: 'The hash of the query dictionary. (This is a 256-bit SHA3 hash of the query dict.)' + constraints: + primaryKey: true + nullable: false + - column: + name: average_execution_time + type: int + remarks: 'Average execution time for the query, round to nearest number of milliseconds. This is updated as a rolling average.' + constraints: + nullable: false diff --git a/sample_dataset/metabase/sample_dataset/generate.clj b/sample_dataset/metabase/sample_dataset/generate.clj index 3807dbcc1427aa73d7334a964379adae8c0a4a86..50bd431ce21ea4db945264236063c992fae29c91 100644 --- a/sample_dataset/metabase/sample_dataset/generate.clj +++ b/sample_dataset/metabase/sample_dataset/generate.clj @@ -253,7 +253,7 @@ (= (count (:products %)) products) (every? keyword? (keys %)) (every? sequential? (vals %))]} - (println (format "Generating random data: %d people, %d products..." people products)) + (printf "Generating random data: %d people, %d products...\n" people products) (let [products (mapv product-add-reviews (create-randoms products random-product)) people (mapv (partial person-add-orders products) (create-randoms people random-person))] {:people (mapv #(dissoc % :orders) people) @@ -416,6 +416,6 @@ (defn -main [& [filename]] (let [filename (or filename sample-dataset-filename)] - (println (format "Writing sample dataset to %s..." filename)) + (printf "Writing sample dataset to %s...\n" filename) (create-h2-db filename) (System/exit 0))) diff --git a/src/metabase/api/card.clj b/src/metabase/api/card.clj index 3c3b09c511463c7a0e5d6d112de9f85b850a723f..e27c8c855ce6dcc1e2143a40bf4663287d34e84f 100644 --- a/src/metabase/api/card.clj +++ b/src/metabase/api/card.clj @@ -1,7 +1,9 @@ (ns metabase.api.card (:require [clojure.data :as data] + [clojure.tools.logging :as log] [cheshire.core :as json] [compojure.core :refer [GET POST DELETE PUT]] + [ring.util.codec :as codec] [schema.core :as s] (toucan [db :as db] [hydrate :refer [hydrate]]) @@ -18,10 +20,14 @@ [interface :as mi] [label :refer [Label]] [permissions :as perms] + [query :as query] [table :refer [Table]] [view-log :refer [ViewLog]]) - (metabase [query-processor :as qp] - [util :as u]) + [metabase.public-settings :as public-settings] + [metabase.query-processor :as qp] + [metabase.query-processor.middleware.cache :as cache] + [metabase.query-processor.util :as qputil] + [metabase.util :as u] [metabase.util.schema :as su]) (:import java.util.UUID)) @@ -128,8 +134,18 @@ (defn- ^:deprecated card-has-label? [label-slug card] (contains? (set (map :slug (:labels card))) label-slug)) +(defn- collection-slug->id [collection-slug] + (when (seq collection-slug) + ;; special characters in the slugs are always URL-encoded when stored in the DB, e.g. + ;; "Obsługa klienta" becomes "obs%C5%82uga_klienta". But for some weird reason sometimes the slug is passed in like + ;; "obsługa_klientaa" (not URL-encoded) so go ahead and URL-encode the input as well so we can match either case + (check-404 (db/select-one-id Collection + {:where [:or [:= :slug collection-slug] + [:= :slug (codec/url-encode collection-slug)]]})))) + ;; TODO - do we need to hydrate the cards' collections as well? -(defn- cards-for-filter-option [filter-option model-id label collection] +(defn- cards-for-filter-option [filter-option model-id label collection-slug] + (println "collection-slug:" collection-slug) ; NOCOMMIT (let [cards (-> ((filter-option->fn (or filter-option :all)) model-id) (hydrate :creator :collection) hydrate-labels @@ -137,11 +153,10 @@ ;; Since labels and collections are hydrated in Clojure-land we need to wait until this point to apply label/collection filtering if applicable ;; COLLECTION can optionally be an empty string which is used to repre (filter (cond - collection (let [collection-id (when (seq collection) - (check-404 (db/select-one-id Collection :slug collection)))] - (comp (partial = collection-id) :collection_id)) - (seq label) (partial card-has-label? label) - :else identity) + collection-slug (let [collection-id (collection-slug->id collection-slug)] + (comp (partial = collection-id) :collection_id)) + (seq label) (partial card-has-label? label) + :else identity) cards))) @@ -227,7 +242,7 @@ {name (s/maybe su/NonBlankString) dataset_query (s/maybe su/Map) display (s/maybe su/NonBlankString) - description (s/maybe su/NonBlankString) + description (s/maybe s/Str) visualization_settings (s/maybe su/Map) archived (s/maybe s/Bool) enable_embedding (s/maybe s/Bool) @@ -254,12 +269,17 @@ (check-superuser)) ;; ok, now save the Card (db/update! Card id - (merge (when (contains? body :collection_id) - {:collection_id collection_id}) - (into {} (for [k [:dataset_query :description :display :name :visualization_settings :archived :enable_embedding :embedding_params] - :let [v (k body)] - :when (not (nil? v))] - {k v})))) + (merge + ;; `collection_id` and `description` can be `nil` (in order to unset them) + (when (contains? body :collection_id) + {:collection_id collection_id}) + (when (contains? body :description) + {:description description}) + ;; other values should only be modified if they're passed in as non-nil + (into {} (for [k [:dataset_query :display :name :visualization_settings :archived :enable_embedding :embedding_params] + :let [v (k body)] + :when (not (nil? v))] + {k v})))) (let [event (cond ;; card was archived (and archived @@ -354,6 +374,26 @@ ;;; ------------------------------------------------------------ Running a Query ------------------------------------------------------------ +(defn- query-magic-ttl + "Compute a 'magic' cache TTL time (in seconds) for QUERY by multipling its historic average execution times by the `query-caching-ttl-ratio`. + If the TTL is less than a second, this returns `nil` (i.e., the cache should not be utilized.)" + [query] + (when-let [average-duration (query/average-execution-time-ms (qputil/query-hash query))] + (let [ttl-seconds (Math/round (float (/ (* average-duration (public-settings/query-caching-ttl-ratio)) + 1000.0)))] + (when-not (zero? ttl-seconds) + (log/info (format "Question's average execution duration is %d ms; using 'magic' TTL of %d seconds" average-duration ttl-seconds) (u/emoji "💾")) + ttl-seconds)))) + +(defn- query-for-card [card parameters constraints] + (let [query (assoc (:dataset_query card) + :constraints constraints + :parameters parameters) + ttl (when (public-settings/enable-query-caching) + (or (:cache_ttl card) + (query-magic-ttl query)))] + (assoc query :cache_ttl ttl))) + (defn run-query-for-card "Run the query for Card with PARAMETERS and CONSTRAINTS, and return results in the usual format." {:style/indent 1} @@ -362,9 +402,7 @@ context :question}}] {:pre [(u/maybe? sequential? parameters)]} (let [card (read-check Card card-id) - query (assoc (:dataset_query card) - :parameters parameters - :constraints constraints) + query (query-for-card card parameters constraints) options {:executed-by *current-user-id* :context context :card-id card-id @@ -374,26 +412,30 @@ (defendpoint POST "/:card-id/query" "Run the query associated with a Card." - [card-id, :as {{:keys [parameters]} :body}] - (run-query-for-card card-id, :parameters parameters)) + [card-id :as {{:keys [parameters ignore_cache], :or {ignore_cache false}} :body}] + {ignore_cache (s/maybe s/Bool)} + (binding [cache/*ignore-cached-results* ignore_cache] + (run-query-for-card card-id, :parameters parameters))) (defendpoint POST "/:card-id/query/csv" "Run the query associated with a Card, and return its results as CSV. Note that this expects the parameters as serialized JSON in the 'parameters' parameter" [card-id parameters] {parameters (s/maybe su/JSONString)} - (dataset-api/as-csv (run-query-for-card card-id - :parameters (json/parse-string parameters keyword) - :constraints nil - :context :csv-download))) + (binding [cache/*ignore-cached-results* true] + (dataset-api/as-csv (run-query-for-card card-id + :parameters (json/parse-string parameters keyword) + :constraints nil + :context :csv-download)))) (defendpoint POST "/:card-id/query/json" "Run the query associated with a Card, and return its results as JSON. Note that this expects the parameters as serialized JSON in the 'parameters' parameter" [card-id parameters] {parameters (s/maybe su/JSONString)} - (dataset-api/as-json (run-query-for-card card-id - :parameters (json/parse-string parameters keyword) - :constraints nil - :context :json-download))) + (binding [cache/*ignore-cached-results* true] + (dataset-api/as-json (run-query-for-card card-id + :parameters (json/parse-string parameters keyword) + :constraints nil + :context :json-download)))) ;;; ------------------------------------------------------------ Sharing is Caring ------------------------------------------------------------ diff --git a/src/metabase/api/dashboard.clj b/src/metabase/api/dashboard.clj index d252c6542321c5023ded495ffe5342a03c1b2d5a..e965621b68f40904f7e45b873a7811c5828fbc40 100644 --- a/src/metabase/api/dashboard.clj +++ b/src/metabase/api/dashboard.clj @@ -73,7 +73,7 @@ but to change the value of `enable_embedding` you must be a superuser." [id :as {{:keys [description name parameters caveats points_of_interest show_in_getting_started enable_embedding embedding_params], :as dashboard} :body}] {name (s/maybe su/NonBlankString) - description (s/maybe su/NonBlankString) + description (s/maybe s/Str) caveats (s/maybe su/NonBlankString) points_of_interest (s/maybe su/NonBlankString) show_in_getting_started (s/maybe su/NonBlankString) diff --git a/src/metabase/api/dataset.clj b/src/metabase/api/dataset.clj index ddeaf0e370cf7040ede72a001eb480c11d3e61ca..0dae851c67f8b593f82f55715825106e9029bb05 100644 --- a/src/metabase/api/dataset.clj +++ b/src/metabase/api/dataset.clj @@ -8,6 +8,7 @@ [hydrate :refer [hydrate]]) (metabase.models [card :refer [Card]] [database :refer [Database]] + [query :as query] [query-execution :refer [QueryExecution]]) [metabase.query-processor :as qp] [metabase.query-processor.util :as qputil] @@ -41,8 +42,8 @@ (read-check Database database) ;; try calculating the average for the query as it was given to us, otherwise with the default constraints if there's no data there. ;; if we still can't find relevant info, just default to 0 - {:average (or (qputil/query-average-duration query) - (qputil/query-average-duration (assoc query :constraints default-query-constraints)) + {:average (or (query/average-execution-time-ms (qputil/query-hash query)) + (query/average-execution-time-ms (qputil/query-hash (assoc query :constraints default-query-constraints))) 0)}) (defn as-csv diff --git a/src/metabase/cmd/load_from_h2.clj b/src/metabase/cmd/load_from_h2.clj index cd73db016935c026d12edb1c7664b014656df981..879b4e35ac043a33068fcadc2387b8b9c38370ec 100644 --- a/src/metabase/cmd/load_from_h2.clj +++ b/src/metabase/cmd/load_from_h2.clj @@ -46,7 +46,6 @@ [pulse-card :refer [PulseCard]] [pulse-channel :refer [PulseChannel]] [pulse-channel-recipient :refer [PulseChannelRecipient]] - [query-execution :refer [QueryExecution]] [raw-column :refer [RawColumn]] [raw-table :refer [RawTable]] [revision :refer [Revision]] @@ -89,7 +88,6 @@ DashboardCard DashboardCardSeries Activity - QueryExecution Pulse PulseCard PulseChannel diff --git a/src/metabase/config.clj b/src/metabase/config.clj index e5d45b1b6c7dab6b361c24b6554237862ea7bc29..ac7ff3fd221372021a4a8776af0136b0b9ad5fa1 100644 --- a/src/metabase/config.clj +++ b/src/metabase/config.clj @@ -29,7 +29,8 @@ :mb-version-info-url "http://static.metabase.com/version-info.json" :max-session-age "20160" ; session length in minutes (14 days) :mb-colorize-logs "true" - :mb-emoji-in-logs "true"}) + :mb-emoji-in-logs "true" + :mb-qp-cache-backend "db"}) (defn config-str diff --git a/src/metabase/driver/bigquery.clj b/src/metabase/driver/bigquery.clj index 8e4c41e194baa3c014da31a0f84141ef7332734b..6e9b011b647f2770f88ba82ed3b44dbb639201bb 100644 --- a/src/metabase/driver/bigquery.clj +++ b/src/metabase/driver/bigquery.clj @@ -77,7 +77,8 @@ (defn- can-connect? [details-map] {:pre [(map? details-map)]} - (boolean (describe-database {:details details-map}))) + ;; check whether we can connect by just fetching the first page of tables for the database. If that succeeds we're g2g + (boolean (list-tables {:details details-map}))) (defn- ^Table get-table @@ -358,13 +359,71 @@ ;; ORDER BY [sad_toucan_incidents.incidents.timestamp] ASC ;; LIMIT 10 -(defn- field->identitfier [field] +(defn- deduplicate-aliases + "Given a sequence of aliases, return a sequence where duplicate aliases have been appropriately suffixed. + + (deduplicate-aliases [\"sum\" \"count\" \"sum\" \"avg\" \"sum\" \"min\"]) + ;; -> [\"sum\" \"count\" \"sum_2\" \"avg\" \"sum_3\" \"min\"]" + [aliases] + (loop [acc [], alias->use-count {}, [alias & more, :as aliases] aliases] + (let [use-count (get alias->use-count alias)] + (cond + (empty? aliases) acc + (not alias) (recur (conj acc alias) alias->use-count more) + (not use-count) (recur (conj acc alias) (assoc alias->use-count alias 1) more) + :else (let [new-count (inc use-count) + new-alias (str alias "_" new-count)] + (recur (conj acc new-alias) (assoc alias->use-count alias new-count, new-alias 1) more)))))) + +(defn- select-subclauses->aliases + "Return a vector of aliases used in HoneySQL SELECT-SUBCLAUSES. + (For clauses that aren't aliased, `nil` is returned as a placeholder)." + [select-subclauses] + (for [subclause select-subclauses] + (when (and (vector? subclause) + (= 2 (count subclause))) + (second subclause)))) + +(defn update-select-subclause-aliases + "Given a vector of HoneySQL SELECT-SUBCLAUSES and a vector of equal length of NEW-ALIASES, + return a new vector with combining the original `SELECT` subclauses with the new aliases. + + Subclauses that are not aliased are not modified; they are given a placeholder of `nil` in the NEW-ALIASES vector. + + (update-select-subclause-aliases [[:user_id \"user_id\"] :venue_id] + [\"user_id_2\" nil]) + ;; -> [[:user_id \"user_id_2\"] :venue_id]" + [select-subclauses new-aliases] + (for [[subclause new-alias] (partition 2 (interleave select-subclauses new-aliases))] + (if-not new-alias + subclause + [(first subclause) new-alias]))) + +(defn- deduplicate-select-aliases + "Replace duplicate aliases in SELECT-SUBCLAUSES with appropriately suffixed aliases. + + BigQuery doesn't allow duplicate aliases in `SELECT` statements; a statement like `SELECT sum(x) AS sum, sum(y) AS sum` is invalid. (See #4089) + To work around this, we'll modify the HoneySQL aliases to make sure the same one isn't used twice by suffixing duplicates appropriately. + (We'll generate SQL like `SELECT sum(x) AS sum, sum(y) AS sum_2` instead.)" + [select-subclauses] + (let [aliases (select-subclauses->aliases select-subclauses) + deduped (deduplicate-aliases aliases)] + (update-select-subclause-aliases select-subclauses deduped))) + +(defn- apply-aggregation + "BigQuery's implementation of `apply-aggregation` just hands off to the normal Generic SQL implementation, but calls `deduplicate-select-aliases` on the results." + [driver honeysql-form query] + (-> (sqlqp/apply-aggregation driver honeysql-form query) + (update :select deduplicate-select-aliases))) + + +(defn- field->breakout-identifier [field] (hsql/raw (str \[ (field->alias field) \]))) (defn- apply-breakout [honeysql-form {breakout-fields :breakout, fields-fields :fields}] (-> honeysql-form ;; Group by all the breakout fields - ((partial apply h/group) (map field->identitfier breakout-fields)) + ((partial apply h/group) (map field->breakout-identifier breakout-fields)) ;; Add fields form only for fields that weren't specified in :fields clause -- we don't want to include it twice, or HoneySQL will barf ((partial apply h/merge-select) (for [field breakout-fields :when (not (contains? (set fields-fields) field))] @@ -372,9 +431,9 @@ (defn- apply-order-by [honeysql-form {subclauses :order-by}] (loop [honeysql-form honeysql-form, [{:keys [field direction]} & more] subclauses] - (let [honeysql-form (h/merge-order-by honeysql-form [(field->identitfier field) (case direction - :ascending :asc - :descending :desc)])] + (let [honeysql-form (h/merge-order-by honeysql-form [(field->breakout-identifier field) (case direction + :ascending :asc + :descending :desc)])] (if (seq more) (recur honeysql-form more) honeysql-form)))) @@ -382,6 +441,12 @@ (defn- string-length-fn [field-key] (hsql/call :length field-key)) +;; From the dox: Fields must contain only letters, numbers, and underscores, start with a letter or underscore, and be at most 128 characters long. +(defn- format-custom-field-name ^String [^String custom-field-name] + (s/join (take 128 (-> (s/trim custom-field-name) + (s/replace #"[^\w\d_]" "_") + (s/replace #"(^\d)" "_$1"))))) + (defrecord BigQueryDriver [] clojure.lang.Named @@ -392,7 +457,8 @@ (u/strict-extend BigQueryDriver sql/ISQLDriver (merge (sql/ISQLDriverDefaultsMixin) - {:apply-breakout (u/drop-first-arg apply-breakout) + {:apply-aggregation apply-aggregation + :apply-breakout (u/drop-first-arg apply-breakout) :apply-order-by (u/drop-first-arg apply-order-by) :column->base-type (constantly nil) ; these two are actually not applicable :connection-details->spec (constantly nil) ; since we don't use JDBC @@ -407,46 +473,45 @@ driver/IDriver (merge driver/IDriverDefaultsMixin - {:analyze-table analyze/generic-analyze-table - :can-connect? (u/drop-first-arg can-connect?) - :date-interval (u/drop-first-arg (comp prepare-value u/relative-date)) - :describe-database (u/drop-first-arg describe-database) - :describe-table (u/drop-first-arg describe-table) - :details-fields (constantly [{:name "project-id" - :display-name "Project ID" - :placeholder "praxis-beacon-120871" - :required true} - {:name "dataset-id" - :display-name "Dataset ID" - :placeholder "toucanSightings" - :required true} - {:name "client-id" - :display-name "Client ID" - :placeholder "1201327674725-y6ferb0feo1hfssr7t40o4aikqll46d4.apps.googleusercontent.com" - :required true} - {:name "client-secret" - :display-name "Client Secret" - :placeholder "dJNi4utWgMzyIFo2JbnsK6Np" - :required true} - {:name "auth-code" - :display-name "Auth Code" - :placeholder "4/HSk-KtxkSzTt61j5zcbee2Rmm5JHkRFbL5gD5lgkXek" - :required true}]) - :execute-query (u/drop-first-arg execute-query) + {:analyze-table analyze/generic-analyze-table + :can-connect? (u/drop-first-arg can-connect?) + :date-interval (u/drop-first-arg (comp prepare-value u/relative-date)) + :describe-database (u/drop-first-arg describe-database) + :describe-table (u/drop-first-arg describe-table) + :details-fields (constantly [{:name "project-id" + :display-name "Project ID" + :placeholder "praxis-beacon-120871" + :required true} + {:name "dataset-id" + :display-name "Dataset ID" + :placeholder "toucanSightings" + :required true} + {:name "client-id" + :display-name "Client ID" + :placeholder "1201327674725-y6ferb0feo1hfssr7t40o4aikqll46d4.apps.googleusercontent.com" + :required true} + {:name "client-secret" + :display-name "Client Secret" + :placeholder "dJNi4utWgMzyIFo2JbnsK6Np" + :required true} + {:name "auth-code" + :display-name "Auth Code" + :placeholder "4/HSk-KtxkSzTt61j5zcbee2Rmm5JHkRFbL5gD5lgkXek" + :required true}]) + :execute-query (u/drop-first-arg execute-query) ;; Don't enable foreign keys when testing because BigQuery *doesn't* have a notion of foreign keys. Joins are still allowed, which puts us in a weird position, however; ;; people can manually specifiy "foreign key" relationships in admin and everything should work correctly. ;; Since we can't infer any "FK" relationships during sync our normal FK tests are not appropriate for BigQuery, so they're disabled for the time being. ;; TODO - either write BigQuery-speciifc tests for FK functionality or add additional code to manually set up these FK relationships for FK tables - :features (constantly (set/union #{:basic-aggregations - :standard-deviation-aggregations - :native-parameters - ;; Expression aggregations *would* work, but BigQuery doesn't support the auto-generated column names. BQ column names - ;; can only be alphanumeric or underscores. If we slugified the auto-generated column names, we could enable this feature. - #_:expression-aggregations} - (when-not config/is-test? - ;; during unit tests don't treat bigquery as having FK support - #{:foreign-keys}))) - :field-values-lazy-seq (u/drop-first-arg field-values-lazy-seq) - :mbql->native (u/drop-first-arg mbql->native)})) + :features (constantly (set/union #{:basic-aggregations + :standard-deviation-aggregations + :native-parameters + :expression-aggregations} + (when-not config/is-test? + ;; during unit tests don't treat bigquery as having FK support + #{:foreign-keys}))) + :field-values-lazy-seq (u/drop-first-arg field-values-lazy-seq) + :format-custom-field-name (u/drop-first-arg format-custom-field-name) + :mbql->native (u/drop-first-arg mbql->native)})) (driver/register-driver! :bigquery driver) diff --git a/src/metabase/driver/crate.clj b/src/metabase/driver/crate.clj index 6d88591a90c6febca681fce0017ddc09e696d769..55082daf911962089bfc19d9a3e481c7ff75aa36 100644 --- a/src/metabase/driver/crate.clj +++ b/src/metabase/driver/crate.clj @@ -1,10 +1,12 @@ (ns metabase.driver.crate (:require [clojure.java.jdbc :as jdbc] + [clojure.tools.logging :as log] [honeysql.core :as hsql] [metabase.driver :as driver] [metabase.driver.crate.util :as crate-util] [metabase.driver.generic-sql :as sql] - [metabase.util :as u])) + [metabase.util :as u]) + (:import java.sql.DatabaseMetaData)) (def ^:private ^:const column->base-type "Map of Crate column types -> Field base types @@ -55,6 +57,39 @@ (defn- string-length-fn [field-key] (hsql/call :char_length field-key)) +(defn- describe-table-fields + [database, driver, {:keys [schema name]}] + (let [columns (jdbc/query + (sql/db->jdbc-connection-spec database) + [(format "select column_name, data_type as type_name + from information_schema.columns + where table_name like '%s' and table_schema like '%s' + and data_type != 'object_array'" name schema)])] ; clojure jdbc can't handle fields of type "object_array" atm + (set (for [{:keys [column_name type_name]} columns] + {:name column_name + :custom {:column-type type_name} + :base-type (or (column->base-type (keyword type_name)) + (do (log/warn (format "Don't know how to map column type '%s' to a Field base_type, falling back to :type/*." type_name)) + :type/*))})))) + +(defn- add-table-pks + [^DatabaseMetaData metadata, table] + (let [pks (->> (.getPrimaryKeys metadata nil nil (:name table)) + jdbc/result-set-seq + (mapv :column_name) + set)] + (update table :fields (fn [fields] + (set (for [field fields] + (if-not (contains? pks (:name field)) + field + (assoc field :pk? true)))))))) + +(defn- describe-table [driver database table] + (sql/with-metadata [metadata driver database] + (->> (describe-table-fields database driver table) + (assoc (select-keys table [:name :schema]) :fields) + ;; find PKs and mark them + (add-table-pks metadata)))) (defrecord CrateDriver [] clojure.lang.Named @@ -65,6 +100,7 @@ (merge (sql/IDriverSQLDefaultsMixin) {:can-connect? (u/drop-first-arg can-connect?) :date-interval crate-util/date-interval + :describe-table describe-table :details-fields (constantly [{:name "hosts" :display-name "Hosts" :default "localhost:5432"}]) @@ -75,6 +111,8 @@ :column->base-type (u/drop-first-arg column->base-type) :string-length-fn (u/drop-first-arg string-length-fn) :date crate-util/date + :quote-style (constantly :crate) + :field->alias (constantly nil) :unix-timestamp->timestamp crate-util/unix-timestamp->timestamp :current-datetime-fn (constantly now)})) diff --git a/src/metabase/driver/druid/query_processor.clj b/src/metabase/driver/druid/query_processor.clj index 7ec2887b4fc640deb8b0540d77b50901a5bac52c..8befd598664fa16eb7084a10eafe76a478045cd7 100644 --- a/src/metabase/driver/druid/query_processor.clj +++ b/src/metabase/driver/druid/query_processor.clj @@ -577,15 +577,16 @@ {:dimension (->rvalue field) :direction direction})))) -(defmethod handle-order-by ::grouped-timeseries [_ {[breakout-field] :breakout, [{field :field, direction :direction}] :order-by} druid-query] - (let [field (->rvalue field) - breakout-field (->rvalue breakout-field) - sort-by-breakout? (= field breakout-field)] - (if (and sort-by-breakout? - (= direction :descending)) - (assoc druid-query :descending true) - druid-query))) +;; Handle order by timstamp field +(defn- handle-order-by-timestamp [field direction druid-query] + (assoc druid-query :descending (and (instance? DateTimeField field) + (= direction :descending)))) + +(defmethod handle-order-by ::grouped-timeseries [_ {[{field :field, direction :direction}] :order-by} druid-query] + (handle-order-by-timestamp field direction druid-query)) +(defmethod handle-order-by ::select [_ {[{field :field, direction :direction}] :order-by} druid-query] + (handle-order-by-timestamp field direction druid-query)) ;;; ### handle-fields diff --git a/src/metabase/driver/generic_sql.clj b/src/metabase/driver/generic_sql.clj index 4c1364d7ba01a6182f3faa329a72342280d94834..dbec2699e8e5f9c135fab69c7843b3bfcb5edf3b 100644 --- a/src/metabase/driver/generic_sql.clj +++ b/src/metabase/driver/generic_sql.clj @@ -27,9 +27,12 @@ Methods marked *OPTIONAL* have default implementations in `ISQLDriverDefaultsMixin`." (active-tables ^java.util.Set [this, ^DatabaseMetaData metadata] - "Return a set of maps containing information about the active tables/views, collections, or equivalent that currently exist in DATABASE. + "*OPTIONAL* Return a set of maps containing information about the active tables/views, collections, or equivalent that currently exist in DATABASE. Each map should contain the key `:name`, which is the string name of the table. For databases that have a concept of schemas, - this map should also include the string name of the table's `:schema`.") + this map should also include the string name of the table's `:schema`. + + Two different implementations are provided in this namespace: `fast-active-tables` (the default), and `post-filtered-active-tables`. You should be fine using + the default, but refer to the documentation for those functions for more details on the differences.") ;; The following apply-* methods define how the SQL Query Processor handles given query clauses. Each method is called when a matching clause is present ;; in QUERY, and should return an appropriately modified version of KORMA-QUERY. Most drivers can use the default implementations for all of these methods, @@ -71,7 +74,7 @@ (field-percent-urls [this field] "*OPTIONAL*. Implementation of the `:field-percent-urls-fn` to be passed to `make-analyze-table`. The default implementation is `fast-field-percent-urls`, which avoids a full table scan. Substitue this with `slow-field-percent-urls` for databases - where this doesn't work, such as SQL Server") + where this doesn't work, such as SQL Server.") (field->alias ^String [this, ^Field field] "*OPTIONAL*. Return the alias that should be used to for FIELD, i.e. in an `AS` clause. The default implementation calls `name`, which diff --git a/src/metabase/driver/generic_sql/query_processor.clj b/src/metabase/driver/generic_sql/query_processor.clj index 3a19281b242c7d0705e087f545cd94d2a132088c..741a791a1a35a4ea6836755639fe631c7b6bd9da 100644 --- a/src/metabase/driver/generic_sql/query_processor.clj +++ b/src/metabase/driver/generic_sql/query_processor.clj @@ -151,7 +151,7 @@ (defn- apply-expression-aggregation [driver honeysql-form expression] (h/merge-select honeysql-form [(expression-aggregation->honeysql driver expression) - (hx/escape-dots (annotate/aggregation-name expression))])) + (hx/escape-dots (driver/format-custom-field-name driver (annotate/aggregation-name expression)))])) (defn- apply-single-aggregation [driver honeysql-form {:keys [aggregation-type field], :as aggregation}] (h/merge-select honeysql-form [(aggregation->honeysql driver aggregation-type field) diff --git a/src/metabase/driver/generic_sql/util/unprepare.clj b/src/metabase/driver/generic_sql/util/unprepare.clj index d65502ac180912785228c1ac8058e590ed548f71..56845e692a68a5e0a3500406c0c5609ebed4ffcd 100644 --- a/src/metabase/driver/generic_sql/util/unprepare.clj +++ b/src/metabase/driver/generic_sql/util/unprepare.clj @@ -7,20 +7,20 @@ (:import java.util.Date)) (defprotocol ^:private IUnprepare - (^:private unprepare-arg ^String [this])) + (^:private unprepare-arg ^String [this settings])) (extend-protocol IUnprepare - nil (unprepare-arg [this] "NULL") - String (unprepare-arg [this] (str \' (str/replace this "'" "\\\\'") \')) ; escape single-quotes - Boolean (unprepare-arg [this] (if this "TRUE" "FALSE")) - Number (unprepare-arg [this] (str this)) - Date (unprepare-arg [this] (first (hsql/format (hsql/call :timestamp (hx/literal (u/date->iso-8601 this))))))) ; TODO - this probably doesn't work for every DB! + nil (unprepare-arg [this _] "NULL") + String (unprepare-arg [this {:keys [quote-escape]}] (str \' (str/replace this "'" (str quote-escape "'")) \')) ; escape single-quotes + Boolean (unprepare-arg [this _] (if this "TRUE" "FALSE")) + Number (unprepare-arg [this _] (str this)) + Date (unprepare-arg [this {:keys [iso-8601-fn]}] (first (hsql/format (hsql/call iso-8601-fn (hx/literal (u/date->iso-8601 this))))))) (defn unprepare "Convert a normal SQL `[statement & prepared-statement-args]` vector into a flat, non-prepared statement." - ^String [[sql & args]] + ^String [[sql & args] & {:keys [quote-escape iso-8601-fn], :or {quote-escape "\\\\", iso-8601-fn :timestamp}}] (loop [sql sql, [arg & more-args, :as args] args] (if-not (seq args) sql - (recur (str/replace-first sql #"(?<!\?)\?(?!\?)" (unprepare-arg arg)) + (recur (str/replace-first sql #"(?<!\?)\?(?!\?)" (unprepare-arg arg {:quote-escape quote-escape, :iso-8601-fn iso-8601-fn})) more-args)))) diff --git a/src/metabase/driver/mongo/query_processor.clj b/src/metabase/driver/mongo/query_processor.clj index 4e27d66d57140600f9380fc92e11de81b4643a67..4dac3f89814c8677bfd63ebbcaad925229b68545 100644 --- a/src/metabase/driver/mongo/query_processor.clj +++ b/src/metabase/driver/mongo/query_processor.clj @@ -379,31 +379,63 @@ v)})))) -;;; ------------------------------------------------------------ Handling ISODate(...) forms ------------------------------------------------------------ -;; In Mongo it's fairly common use ISODate(...) forms in queries, which unfortunately are not valid JSON, +;;; ------------------------------------------------------------ Handling ISODate(...) and ObjectId(...) forms ------------------------------------------------------------ +;; In Mongo it's fairly common use ISODate(...) or ObjectId(...) forms in queries, which unfortunately are not valid JSON, ;; and thus cannot be parsed by Cheshire. But we are clever so we will: ;; ;; 1) Convert forms like ISODate(...) to valid JSON forms like ["___ISODate", ...] ;; 2) Parse Normally -;; 3) Walk the parsed JSON and convert forms like [:___ISODate ...] to JodaTime dates +;; 3) Walk the parsed JSON and convert forms like [:___ISODate ...] to JodaTime dates, and [:___ObjectId ...] to BSON IDs + +;; add more fn handlers here as needed +(def ^:private fn-name->decoder + {:ISODate (fn [arg] + (DateTime. arg)) + :ObjectId (fn [^String arg] + (ObjectId. arg))}) + +(defn- form->encoded-fn-name + "If FORM is an encoded fn call form return the key representing the fn call that was encoded. + If it doesn't represent an encoded fn, return `nil`. + + (form->encoded-fn-name [:___ObjectId \"583327789137b2700a1621fb\"]) -> :ObjectId" + [form] + (when (vector? form) + (when (u/string-or-keyword? (first form)) + (when-let [[_ k] (re-matches #"^___(\w+$)" (name (first form)))] + (let [k (keyword k)] + (when (contains? fn-name->decoder k) + k)))))) + +(defn- maybe-decode-fncall [form] + (if-let [fn-name (form->encoded-fn-name form)] + ((fn-name->decoder fn-name) (second form)) + form)) -(defn- encoded-iso-date? [form] - (and (vector? form) - (= (first form) "___ISODate"))) +(defn- decode-fncalls [query] + (walk/postwalk maybe-decode-fncall query)) -(defn- maybe-decode-iso-date-fncall [form] - (if (encoded-iso-date? form) - (DateTime. (second form)) - form)) +(defn- encode-fncalls-for-fn + "Walk QUERY-STRING and replace fncalls to fn with FN-NAME with encoded forms that can be parsed as valid JSON. + + (encode-fncalls-for-fn \"ObjectId\" \"{\\\"$match\\\":ObjectId(\\\"583327789137b2700a1621fb\\\")}\") + ;; -> \"{\\\"$match\\\":[\\\"___ObjectId\\\", \\\"583327789137b2700a1621fb\\\"]}\"" + [fn-name query-string] + (s/replace query-string + (re-pattern (format "%s\\(([^)]+)\\)" (name fn-name))) + (format "[\"___%s\", $1]" (name fn-name)))) -(defn- decode-iso-date-fncalls [query] - (walk/postwalk maybe-decode-iso-date-fncall query)) +(defn- encode-fncalls + "Replace occurances of `ISODate(...)` and similary function calls (invalid JSON, but legal in Mongo) + with legal JSON forms like `[:___ISODate ...]` that we can decode later. -(defn- encode-iso-date-fncalls - "Replace occurances of `ISODate(...)` function calls (invalid JSON, but legal in Mongo) - with legal JSON forms like `[:___ISODate ...]` that we can decode later." + Walks QUERY-STRING and encodes all the various fncalls we support." [query-string] - (s/replace query-string #"ISODate\(([^)]+)\)" "[\"___ISODate\", $1]")) + (loop [query-string query-string, [fn-name & more] (keys fn-name->decoder)] + (if-not fn-name + query-string + (recur (encode-fncalls-for-fn fn-name query-string) + more)))) ;;; ------------------------------------------------------------ Query Execution ------------------------------------------------------------ @@ -427,7 +459,7 @@ (string? collection) (map? database)]} (let [query (if (string? query) - (decode-iso-date-fncalls (json/parse-string (encode-iso-date-fncalls query) keyword)) + (decode-fncalls (json/parse-string (encode-fncalls query) keyword)) query) results (mc/aggregate *mongo-connection* collection query :allow-disk-use true) diff --git a/src/metabase/driver/presto.clj b/src/metabase/driver/presto.clj new file mode 100644 index 0000000000000000000000000000000000000000..97630c44ff7b08284a7b8d5a11700d335207def3 --- /dev/null +++ b/src/metabase/driver/presto.clj @@ -0,0 +1,344 @@ +(ns metabase.driver.presto + (:require [clojure.set :as set] + [clojure.string :as str] + [clj-http.client :as http] + (honeysql [core :as hsql] + [helpers :as h]) + [metabase.config :as config] + [metabase.driver :as driver] + [metabase.driver.generic-sql :as sql] + [metabase.driver.generic-sql.util.unprepare :as unprepare] + (metabase.models [field :as field] + [table :as table]) + [metabase.sync-database.analyze :as analyze] + [metabase.query-processor.util :as qputil] + [metabase.util :as u] + [metabase.util.honeysql-extensions :as hx]) + (:import java.util.Date + (metabase.query_processor.interface DateTimeValue Value))) + + +;;; Presto API helpers + +(defn- details->uri + [{:keys [ssl host port]} path] + (str (if ssl "https" "http") "://" host ":" port path)) + +(defn- details->request [{:keys [user password catalog report-timezone]}] + (merge {:headers (merge {"X-Presto-Source" "metabase" + "X-Presto-User" user} + (when catalog + {"X-Presto-Catalog" catalog}) + (when report-timezone + {"X-Presto-Time-Zone" report-timezone}))} + (when password + {:basic-auth [user password]}))) + +(defn- parse-time-with-tz [s] + ;; Try parsing with offset first then with full ZoneId + (or (u/ignore-exceptions (u/parse-date "HH:mm:ss.SSS ZZ" s)) + (u/parse-date "HH:mm:ss.SSS ZZZ" s))) + +(defn- parse-timestamp-with-tz [s] + ;; Try parsing with offset first then with full ZoneId + (or (u/ignore-exceptions (u/parse-date "yyyy-MM-dd HH:mm:ss.SSS ZZ" s)) + (u/parse-date "yyyy-MM-dd HH:mm:ss.SSS ZZZ" s))) + +(defn- field-type->parser [field-type] + (condp re-matches field-type + #"decimal.*" bigdec + #"time" (partial u/parse-date :hour-minute-second-ms) + #"time with time zone" parse-time-with-tz + #"timestamp" (partial u/parse-date "yyyy-MM-dd HH:mm:ss.SSS") + #"timestamp with time zone" parse-timestamp-with-tz + #".*" identity)) + +(defn- parse-presto-results [columns data] + (let [parsers (map (comp field-type->parser :type) columns)] + (for [row data] + (for [[value parser] (partition 2 (interleave row parsers))] + (when value + (parser value)))))) + +(defn- fetch-presto-results! [details {prev-columns :columns, prev-rows :rows} uri] + (let [{{:keys [columns data nextUri error]} :body} (http/get uri (assoc (details->request details) :as :json))] + (when error + (throw (ex-info (or (:message error) "Error running query.") error))) + (let [rows (parse-presto-results columns data) + results {:columns (or columns prev-columns) + :rows (vec (concat prev-rows rows))}] + (if (nil? nextUri) + results + (do (Thread/sleep 100) ; Might not be the best way, but the pattern is that we poll Presto at intervals + (fetch-presto-results! details results nextUri)))))) + +(defn- execute-presto-query! [details query] + (let [{{:keys [columns data nextUri error]} :body} (http/post (details->uri details "/v1/statement") + (assoc (details->request details) :body query, :as :json))] + (when error + (throw (ex-info (or (:message error) "Error preparing query.") error))) + (let [rows (parse-presto-results (or columns []) (or data [])) + results {:columns (or columns []) + :rows rows}] + (if (nil? nextUri) + results + (fetch-presto-results! details results nextUri))))) + + +;;; Generic helpers + +(defn- quote-name [nm] + (str \" (str/replace nm "\"" "\"\"") \")) + +(defn- quote+combine-names [& names] + (str/join \. (map quote-name names))) + + +;;; IDriver implementation + +(defn- field-avg-length [{field-name :name, :as field}] + (let [table (field/table field) + {:keys [details]} (table/database table) + sql (format "SELECT cast(round(avg(length(%s))) AS integer) FROM %s WHERE %s IS NOT NULL" + (quote-name field-name) + (quote+combine-names (:schema table) (:name table)) + (quote-name field-name)) + {[[v]] :rows} (execute-presto-query! details sql)] + (or v 0))) + +(defn- field-percent-urls [{field-name :name, :as field}] + (let [table (field/table field) + {:keys [details]} (table/database table) + sql (format "SELECT cast(count_if(url_extract_host(%s) <> '') AS double) / cast(count(*) AS double) FROM %s WHERE %s IS NOT NULL" + (quote-name field-name) + (quote+combine-names (:schema table) (:name table)) + (quote-name field-name)) + {[[v]] :rows} (execute-presto-query! details sql)] + (if (= v "NaN") 0.0 v))) + +(defn- analyze-table [driver table new-table-ids] + ((analyze/make-analyze-table driver + :field-avg-length-fn field-avg-length + :field-percent-urls-fn field-percent-urls) driver table new-table-ids)) + +(defn- can-connect? [{:keys [catalog] :as details}] + (let [{[[v]] :rows} (execute-presto-query! details (str "SHOW SCHEMAS FROM " (quote-name catalog) " LIKE 'information_schema'"))] + (= v "information_schema"))) + +(defn- date-interval [unit amount] + (hsql/call :date_add (hx/literal unit) amount :%now)) + +(defn- describe-schema [{{:keys [catalog] :as details} :details} {:keys [schema]}] + (let [sql (str "SHOW TABLES FROM " (quote+combine-names catalog schema)) + {:keys [rows]} (execute-presto-query! details sql) + tables (map first rows)] + (set (for [name tables] + {:name name, :schema schema})))) + +(defn- describe-database [{{:keys [catalog] :as details} :details :as database}] + (let [sql (str "SHOW SCHEMAS FROM " (quote-name catalog)) + {:keys [rows]} (execute-presto-query! details sql) + schemas (remove #{"information_schema"} (map first rows))] ; inspecting "information_schema" breaks weirdly + {:tables (apply set/union (for [name schemas] + (describe-schema database {:schema name})))})) + +(defn- presto-type->base-type [field-type] + (condp re-matches field-type + #"boolean" :type/Boolean + #"tinyint" :type/Integer + #"smallint" :type/Integer + #"integer" :type/Integer + #"bigint" :type/BigInteger + #"real" :type/Float + #"double" :type/Float + #"decimal.*" :type/Decimal + #"varchar.*" :type/Text + #"char.*" :type/Text + #"varbinary.*" :type/* + #"json" :type/Text ; TODO - this should probably be Dictionary or something + #"date" :type/Date + #"time.*" :type/DateTime + #"array" :type/Array + #"map" :type/Dictionary + #"row.*" :type/* ; TODO - again, but this time we supposedly have a schema + #".*" :type/*)) + +(defn- describe-table [{{:keys [catalog] :as details} :details} {schema :schema, table-name :name}] + (let [sql (str "DESCRIBE " (quote+combine-names catalog schema table-name)) + {:keys [rows]} (execute-presto-query! details sql)] + {:schema schema + :name table-name + :fields (set (for [[name type] rows] + {:name name, :base-type (presto-type->base-type type)}))})) + +(defprotocol ^:private IPrepareValue + (^:private prepare-value [this])) +(extend-protocol IPrepareValue + nil (prepare-value [_] nil) + DateTimeValue (prepare-value [{:keys [value]}] (prepare-value value)) + Value (prepare-value [{:keys [value]}] (prepare-value value)) + String (prepare-value [this] (hx/literal (str/replace this "'" "''"))) + Boolean (prepare-value [this] (hsql/raw (if this "TRUE" "FALSE"))) + Date (prepare-value [this] (hsql/call :from_iso8601_timestamp (hx/literal (u/date->iso-8601 this)))) + Number (prepare-value [this] this) + Object (prepare-value [this] (throw (Exception. (format "Don't know how to prepare value %s %s" (class this) this))))) + +(defn- execute-query [{:keys [database settings], {sql :query, params :params} :native, :as outer-query}] + (let [sql (str "-- " (qputil/query->remark outer-query) "\n" + (unprepare/unprepare (cons sql params) :quote-escape "'", :iso-8601-fn :from_iso8601_timestamp)) + details (merge (:details database) settings) + {:keys [columns rows]} (execute-presto-query! details sql)] + {:columns (map (comp keyword :name) columns) + :rows rows})) + +(defn- field-values-lazy-seq [{field-name :name, :as field}] + ;; TODO - look into making this actually lazy + (let [table (field/table field) + {:keys [details]} (table/database table) + sql (format "SELECT %s FROM %s LIMIT %d" + (quote-name field-name) + (quote+combine-names (:schema table) (:name table)) + driver/max-sync-lazy-seq-results) + {:keys [rows]} (execute-presto-query! details sql)] + (for [row rows] + (first row)))) + +(defn- humanize-connection-error-message [message] + (condp re-matches message + #"^java.net.ConnectException: Connection refused.*$" + (driver/connection-error-messages :cannot-connect-check-host-and-port) + + #"^clojure.lang.ExceptionInfo: Catalog .* does not exist.*$" + (driver/connection-error-messages :database-name-incorrect) + + #"^java.net.UnknownHostException.*$" + (driver/connection-error-messages :invalid-hostname) + + #".*" ; default + message)) + +(defn- table-rows-seq [{:keys [details]} {:keys [schema name]}] + (let [sql (format "SELECT * FROM %s" (quote+combine-names schema name)) + {:keys [rows], :as result} (execute-presto-query! details sql) + columns (map (comp keyword :name) (:columns result))] + (for [row rows] + (zipmap columns row)))) + + +;;; ISQLDriver implementation + +(defn- apply-page [honeysql-query {{:keys [items page]} :page}] + (let [offset (* (dec page) items)] + (if (zero? offset) + ;; if there's no offset we can simply use limit + (h/limit honeysql-query items) + ;; if we need to do an offset we have to do nesting to generate a row number and where on that + (let [over-clause (format "row_number() OVER (%s)" + (first (hsql/format (select-keys honeysql-query [:order-by]) + :allow-dashed-names? true + :quoting :ansi)))] + (-> (apply h/select (map last (:select honeysql-query))) + (h/from (h/merge-select honeysql-query [(hsql/raw over-clause) :__rownum__])) + (h/where [:> :__rownum__ offset]) + (h/limit items)))))) + +(defn- date [unit expr] + (case unit + :default expr + :minute (hsql/call :date_trunc (hx/literal :minute) expr) + :minute-of-hour (hsql/call :minute expr) + :hour (hsql/call :date_trunc (hx/literal :hour) expr) + :hour-of-day (hsql/call :hour expr) + :day (hsql/call :date_trunc (hx/literal :day) expr) + ;; Presto is ISO compliant, so we need to offset Monday = 1 to Sunday = 1 + :day-of-week (hx/+ (hx/mod (hsql/call :day_of_week expr) 7) 1) + :day-of-month (hsql/call :day expr) + :day-of-year (hsql/call :day_of_year expr) + ;; Similar to DoW, sicne Presto is ISO compliant the week starts on Monday, we need to shift that to Sunday + :week (hsql/call :date_add (hx/literal :day) -1 (hsql/call :date_trunc (hx/literal :week) (hsql/call :date_add (hx/literal :day) 1 expr))) + ;; Offset by one day forward to "fake" a Sunday starting week + :week-of-year (hsql/call :week (hsql/call :date_add (hx/literal :day) 1 expr)) + :month (hsql/call :date_trunc (hx/literal :month) expr) + :month-of-year (hsql/call :month expr) + :quarter (hsql/call :date_trunc (hx/literal :quarter) expr) + :quarter-of-year (hsql/call :quarter expr) + :year (hsql/call :year expr))) + +(defn- string-length-fn [field-key] + (hsql/call :length field-key)) + +(defn- unix-timestamp->timestamp [expr seconds-or-milliseconds] + (case seconds-or-milliseconds + :seconds (hsql/call :from_unixtime expr) + :milliseconds (recur (hx// expr 1000.0) :seconds))) + + +;;; Driver implementation + +(defrecord PrestoDriver [] + clojure.lang.Named + (getName [_] "Presto")) + +(u/strict-extend PrestoDriver + driver/IDriver + (merge (sql/IDriverSQLDefaultsMixin) + {:analyze-table analyze-table + :can-connect? (u/drop-first-arg can-connect?) + :date-interval (u/drop-first-arg date-interval) + :describe-database (u/drop-first-arg describe-database) + :describe-table (u/drop-first-arg describe-table) + :describe-table-fks (constantly nil) ; no FKs in Presto + :details-fields (constantly [{:name "host" + :display-name "Host" + :default "localhost"} + {:name "port" + :display-name "Port" + :type :integer + :default 8080} + {:name "catalog" + :display-name "Database name" + :placeholder "hive" + :required true} + {:name "user" + :display-name "Database username" + :placeholder "What username do you use to login to the database" + :default "metabase"} + {:name "password" + :display-name "Database password" + :type :password + :placeholder "*******"} + {:name "ssl" + :display-name "Use a secure connection (SSL)?" + :type :boolean + :default false}]) + :execute-query (u/drop-first-arg execute-query) + :features (constantly (set/union #{:set-timezone + :basic-aggregations + :standard-deviation-aggregations + :expressions + :native-parameters + :expression-aggregations} + (when-not config/is-test? + ;; during unit tests don't treat presto as having FK support + #{:foreign-keys}))) + :field-values-lazy-seq (u/drop-first-arg field-values-lazy-seq) + :humanize-connection-error-message (u/drop-first-arg humanize-connection-error-message) + :table-rows-seq (u/drop-first-arg table-rows-seq)}) + + sql/ISQLDriver + (merge (sql/ISQLDriverDefaultsMixin) + {:apply-page (u/drop-first-arg apply-page) + :column->base-type (constantly nil) + :connection-details->spec (constantly nil) + :current-datetime-fn (constantly :%now) + :date (u/drop-first-arg date) + :excluded-schemas (constantly #{"information_schema"}) + :field-percent-urls (u/drop-first-arg field-percent-urls) + :prepare-value (u/drop-first-arg prepare-value) + :quote-style (constantly :ansi) + :stddev-fn (constantly :stddev_samp) + :string-length-fn (u/drop-first-arg string-length-fn) + :unix-timestamp->timestamp (u/drop-first-arg unix-timestamp->timestamp)})) + + +(driver/register-driver! :presto (PrestoDriver.)) diff --git a/src/metabase/email.clj b/src/metabase/email.clj index 0bbc09ffc3fb8b3a32d263a82ad25fdbf0ec3499..9731b50acdbdae802f760dda254ccf090546ef74 100644 --- a/src/metabase/email.clj +++ b/src/metabase/email.clj @@ -6,7 +6,8 @@ [metabase.util :as u]) (:import javax.mail.Session)) -;; ## CONFIG +;;; CONFIG +;; TODO - smtp-port should be switched to type :integer (defsetting email-from-address "Email address you want to use as the sender of Metabase." :default "notifications@metabase.com") (defsetting email-smtp-host "The address of the SMTP server that handles your emails.") diff --git a/src/metabase/middleware.clj b/src/metabase/middleware.clj index f460dac615cd142fa07cc597e4c9ec6ac8160221..88f19c572b8683105bff19f4bfbbbbf5b27c4800 100644 --- a/src/metabase/middleware.clj +++ b/src/metabase/middleware.clj @@ -92,10 +92,10 @@ [session-id] (when (and session-id (init-status/complete?)) (when-let [session (or (session-with-id session-id) - (println "no matching session with ID") ; NOCOMMIT + (println "no matching session with ID") ; DEBUG )] (if (session-expired? session) - (println (format "session-is-expired! %d min / %d min" (session-age-minutes session) (config/config-int :max-session-age))) ; NOCOMMIT + (printf "session-is-expired! %d min / %d min\n" (session-age-minutes session) (config/config-int :max-session-age)) ; DEBUG {:metabase-user-id (:user_id session) :is-superuser? (:is_superuser session)})))) diff --git a/src/metabase/models/dashboard.clj b/src/metabase/models/dashboard.clj index 2dedbb90f356e56bd0f8adcb87102d70b07c4b31..660819acfdaf2294b49759750e760996da2c48ec 100644 --- a/src/metabase/models/dashboard.clj +++ b/src/metabase/models/dashboard.clj @@ -88,23 +88,24 @@ :creator_id user-id) (events/publish-event! :dashboard-create))) + + (defn update-dashboard! "Update a `Dashboard`" - [{:keys [id name description parameters caveats points_of_interest show_in_getting_started enable_embedding embedding_params], :as dashboard} user-id] + [dashboard user-id] {:pre [(map? dashboard) - (integer? id) - (u/maybe? u/sequence-of-maps? parameters) + (u/maybe? u/sequence-of-maps? (:parameters dashboard)) (integer? user-id)]} - (db/update-non-nil-keys! Dashboard id - :description description - :name name - :parameters parameters - :caveats caveats - :points_of_interest points_of_interest - :enable_embedding enable_embedding - :embedding_params embedding_params - :show_in_getting_started show_in_getting_started) - (u/prog1 (Dashboard id) + (db/update! Dashboard (u/get-id dashboard) + (merge + ;; description is allowed to be `nil` + (when (contains? dashboard :description) + {:description (:description dashboard)}) + ;; only set everything else if its non-nil + (into {} (for [k [:name :parameters :caveats :points_of_interest :show_in_getting_started :enable_embedding :embedding_params] + :when (k dashboard)] + {k (k dashboard)})))) + (u/prog1 (Dashboard (u/get-id dashboard)) (events/publish-event! :dashboard-update (assoc <> :actor_id user-id)))) diff --git a/src/metabase/models/humanization.clj b/src/metabase/models/humanization.clj index 0780fd56b7b2c9887100965d6c9cd05b13d62c5b..0878761bf883ac73bea68073a9cd14282847bad7 100644 --- a/src/metabase/models/humanization.clj +++ b/src/metabase/models/humanization.clj @@ -83,7 +83,7 @@ (re-humanize-table-and-field-names!)) (defsetting enable-advanced-humanization - "Metabase can attempt to transform your table and field names into more sensible human readable versions, e.g. \"somehorriblename\" becomes \"Some Horrible Name\". + "Metabase can attempt to transform your table and field names into more sensible, human-readable versions, e.g. \"somehorriblename\" becomes \"Some Horrible Name\". This doesn’t work all that well if the names are in a language other than English, however. Do you want us to take a guess?" :type :boolean :default true diff --git a/src/metabase/models/interface.clj b/src/metabase/models/interface.clj index 804877b9dd93f5c4cba623b15d2d48aa53bb7016..308210288029306ae3d095a390eb1a00a84bfffd 100644 --- a/src/metabase/models/interface.clj +++ b/src/metabase/models/interface.clj @@ -1,15 +1,20 @@ (ns metabase.models.interface (:require [clojure.core.memoize :as memoize] [cheshire.core :as json] + [taoensso.nippy :as nippy] [toucan.models :as models] [metabase.config :as config] [metabase.util :as u] - [metabase.util.encryption :as encryption])) + [metabase.util.encryption :as encryption]) + (:import java.sql.Blob)) ;;; ------------------------------------------------------------ Toucan Extensions ------------------------------------------------------------ (models/set-root-namespace! 'metabase.models) + +;;; types + (defn- json-in [obj] (if (string? obj) obj @@ -39,6 +44,24 @@ :in encrypted-json-in :out (comp cached-encrypted-json-out u/jdbc-clob->str)) +(defn compress + "Compress OBJ, returning a byte array." + [obj] + (nippy/freeze obj {:compressor nippy/snappy-compressor})) + +(defn decompress + "Decompress COMPRESSED-BYTES." + [compressed-bytes] + (if (instance? Blob compressed-bytes) + (recur (.getBytes ^Blob compressed-bytes 0 (.length ^Blob compressed-bytes))) + (nippy/thaw compressed-bytes {:compressor nippy/snappy-compressor}))) + +(models/add-type! :compressed + :in compress + :out decompress) + + +;;; properties (defn- add-created-at-timestamp [obj & _] (assoc obj :created_at (u/new-sql-timestamp))) @@ -50,6 +73,11 @@ :insert (comp add-created-at-timestamp add-updated-at-timestamp) :update add-updated-at-timestamp) +;; like `timestamped?`, but for models that only have an `:updated_at` column +(models/add-property! :updated-at-timestamped? + :insert add-updated-at-timestamp + :update add-updated-at-timestamp) + ;;; ------------------------------------------------------------ New Permissions Stuff ------------------------------------------------------------ diff --git a/src/metabase/models/query.clj b/src/metabase/models/query.clj new file mode 100644 index 0000000000000000000000000000000000000000..74c80cba809607118cb40df8ac24b4ee3354e5be --- /dev/null +++ b/src/metabase/models/query.clj @@ -0,0 +1,41 @@ +(ns metabase.models.query + (:require (toucan [db :as db] + [models :as models]) + [metabase.db :as mdb] + [metabase.util :as u] + [metabase.util.honeysql-extensions :as hx])) + +(models/defmodel Query :query) + +;;; Helper Fns + +(defn average-execution-time-ms + "Fetch the average execution time (in milliseconds) for query with QUERY-HASH if available. + Returns `nil` if no information is available." + ^Integer [^bytes query-hash] + {:pre [(instance? (Class/forName "[B") query-hash)]} + (db/select-one-field :average_execution_time Query :query_hash query-hash)) + +(defn- int-casting-type + "Return appropriate type for use in SQL `CAST(x AS type)` statement. + MySQL doesn't accept `integer`, so we have to use `unsigned`; Postgres doesn't accept `unsigned`. + so we have to use `integer`. Yay SQL dialect differences :D" + [] + (if (= (mdb/db-type) :mysql) + :unsigned + :integer)) + +(defn update-average-execution-time! + "Update the recorded average execution time for query with QUERY-HASH." + ^Integer [^bytes query-hash, ^Integer execution-time-ms] + {:pre [(instance? (Class/forName "[B") query-hash)]} + (or + ;; if there's already a matching Query update the rolling average + (db/update-where! Query {:query_hash query-hash} + :average_execution_time (hx/cast (int-casting-type) (hx/round (hx/+ (hx/* 0.9 :average_execution_time) + (* 0.1 execution-time-ms)) + 0))) + ;; otherwise add a new entry, using the value of EXECUTION-TIME-MS as a starting point + (db/insert! Query + :query_hash query-hash + :average_execution_time execution-time-ms))) diff --git a/src/metabase/models/query_cache.clj b/src/metabase/models/query_cache.clj new file mode 100644 index 0000000000000000000000000000000000000000..e4277919d641cb58629f75311f2da3093c13794a --- /dev/null +++ b/src/metabase/models/query_cache.clj @@ -0,0 +1,12 @@ +(ns metabase.models.query-cache + "A model used to cache query results in the database." + (:require [toucan.models :as models] + [metabase.util :as u])) + +(models/defmodel QueryCache :query_cache) + +(u/strict-extend (class QueryCache) + models/IModel + (merge models/IModelDefaults + {:types (constantly {:results :compressed}) + :properties (constantly {:updated-at-timestamped? true})})) diff --git a/src/metabase/models/setting.clj b/src/metabase/models/setting.clj index 970f490a27efab049bce36401c38a49df17a718a..713a2b92f49c6ae73399e1fd6b0d55835e7aaaca 100644 --- a/src/metabase/models/setting.clj +++ b/src/metabase/models/setting.clj @@ -53,7 +53,7 @@ (def ^:private Type - (s/enum :string :boolean :json)) + (s/enum :string :boolean :json :integer)) (def ^:private SettingDefinition {:name s/Keyword @@ -152,6 +152,12 @@ ^Boolean [setting-or-name] (string->boolean (get-string setting-or-name))) +(defn get-integer + "Get integer value of (presumably `:integer`) SETTING-OR-NAME. This is the default getter for `:integer` settings." + ^Integer [setting-or-name] + (when-let [s (get-string setting-or-name)] + (Integer/parseInt s))) + (defn get-json "Get the string value of SETTING-OR-NAME and parse it as JSON." [setting-or-name] @@ -160,6 +166,7 @@ (def ^:private default-getter-for-type {:string get-string :boolean get-boolean + :integer get-integer :json get-json}) (defn get @@ -220,6 +227,15 @@ false "false" nil nil)))) +(defn set-integer! + "Set the value of integer SETTING-OR-NAME." + [setting-or-name new-value] + (set-string! setting-or-name (when new-value + (assert (or (integer? new-value) + (and (string? new-value) + (re-matches #"^\d+$" new-value)))) + (str new-value)))) + (defn set-json! "Serialize NEW-VALUE for SETTING-OR-NAME as a JSON string and save it." [setting-or-name new-value] @@ -229,6 +245,7 @@ (def ^:private default-setter-for-type {:string set-string! :boolean set-boolean! + :integer set-integer! :json set-json!}) (defn set! @@ -316,7 +333,7 @@ You may optionally pass any of the OPTIONS below: * `:default` - The default value of the setting. (default: `nil`) - * `:type` - `:string` (default), `:boolean`, or `:json`. Non-`:string` settings have special default getters and setters that automatically coerce values to the correct types. + * `:type` - `:string` (default), `:boolean`, `:integer`, or `:json`. Non-`:string` settings have special default getters and setters that automatically coerce values to the correct types. * `:internal?` - This `Setting` is for internal use and shouldn't be exposed in the UI (i.e., not returned by the corresponding endpoints). Default: `false` * `:getter` - A custom getter fn, which takes no arguments. Overrides the default implementation. diff --git a/src/metabase/public_settings.clj b/src/metabase/public_settings.clj index 107170404c3abdb8e97b213e3541995cc9523398..4d9900614fa575d603f9838e7abeefbc22074568 100644 --- a/src/metabase/public_settings.clj +++ b/src/metabase/public_settings.clj @@ -53,6 +53,40 @@ :type :boolean :default false) + +(defsetting enable-query-caching + "Enabling caching will save the results of queries that take a long time to run." + :type :boolean + :default false) + +(defsetting query-caching-max-kb + "The maximum size of the cache per card, in kilobytes:" + ;; (This size is a measurement of the length of *uncompressed* serialized result *rows*. The actual size of + ;; the results as stored will vary somewhat, since this measurement doesn't include metadata returned with the + ;; results, and doesn't consider whether the results are compressed, as the `:db` backend does.) + :type :integer + :default 1000) + +(defsetting query-caching-max-ttl + "The absoulte maximum time to keep any cached query results, in seconds." + :type :integer + :default (* 60 60 24 100)) ; 100 days + +(defsetting query-caching-min-ttl + "Metabase will cache all saved questions with an average query execution time longer than + this many seconds:" + :type :integer + :default 60) + +(defsetting query-caching-ttl-ratio + "To determine how long each saved question's cached result should stick around, we take the + query's average execution time and multiply that by whatever you input here. So if a query + takes on average 2 minutes to run, and you input 10 for your multiplier, its cache entry + will persist for 20 minutes." + :type :integer + :default 10) + + (defn remove-public-uuid-if-public-sharing-is-disabled "If public sharing is *disabled* and OBJECT has a `:public_uuid`, remove it so people don't try to use it (since it won't work). Intended for use as part of a `post-select` implementation for Cards and Dashboards." @@ -81,6 +115,7 @@ :anon_tracking_enabled (anon-tracking-enabled) :custom_geojson (setting/get :custom-geojson) :email_configured ((resolve 'metabase.email/email-configured?)) + :enable_query_caching (enable-query-caching) :engines ((resolve 'metabase.driver/available-drivers)) :ga_code "UA-60817802-1" :google_auth_client_id (setting/get :google-auth-client-id) diff --git a/src/metabase/query_processor.clj b/src/metabase/query_processor.clj index f08dfa5b4bee3857c06b897dce41b4a81b8fcf7b..834d312848664a35d1e38417a565cc2ed27cb2cf 100644 --- a/src/metabase/query_processor.clj +++ b/src/metabase/query_processor.clj @@ -4,13 +4,15 @@ [schema.core :as s] [toucan.db :as db] [metabase.driver :as driver] - [metabase.models.query-execution :refer [QueryExecution], :as query-execution] + (metabase.models [query :as query] + [query-execution :refer [QueryExecution], :as query-execution]) [metabase.query-processor.util :as qputil] (metabase.query-processor.middleware [add-implicit-clauses :as implicit-clauses] [add-row-count-and-status :as row-count-and-status] [add-settings :as add-settings] [annotate-and-sort :as annotate-and-sort] [catch-exceptions :as catch-exceptions] + [cache :as cache] [cumulative-aggregations :as cumulative-ags] [dev :as dev] [driver-specific :as driver-specific] @@ -79,8 +81,9 @@ driver-specific/process-query-in-context ; (drivers can inject custom middleware if they implement IDriver's `process-query-in-context`) add-settings/add-settings resolve-driver/resolve-driver ; ▲▲▲ DRIVER RESOLUTION POINT ▲▲▲ All functions *above* will have access to the driver during PRE- *and* POST-PROCESSING - catch-exceptions/catch-exceptions - log-query/log-initial-query) + log-query/log-initial-query + cache/maybe-return-cached-results + catch-exceptions/catch-exceptions) query)) ;; ▲▲▲ PRE-PROCESSING ▲▲▲ happens from BOTTOM-TO-TOP, e.g. the results of `expand-macros` are (eventually) passed to `expand-resolve` @@ -100,9 +103,10 @@ ;;; +----------------------------------------------------------------------------------------------------+ (defn- save-query-execution! - "Save (or update) a `QueryExecution`." + "Save a `QueryExecution` and update the average execution time for the corresponding `Query`." [query-execution] (u/prog1 query-execution + (query/update-average-execution-time! (:hash query-execution) (:running_time query-execution)) (db/insert! QueryExecution (dissoc query-execution :json_query)))) (defn- save-and-return-failed-query! @@ -126,17 +130,20 @@ (defn- save-and-return-successful-query! "Save QueryExecution state and construct a completed (successful) query response" [query-execution query-result] - ;; record our query execution and format response - (-> (assoc query-execution - :running_time (- (System/currentTimeMillis) - (:start_time_millis query-execution)) - :result_rows (get query-result :row_count 0)) - (dissoc :start_time_millis) - save-query-execution! - ;; at this point we've saved and we just need to massage things into our final response format - (dissoc :error :result_rows :hash :executor_id :native :card_id :dashboard_id :pulse_id) - (merge query-result) - (assoc :status :completed))) + (let [query-execution (-> (assoc query-execution + :running_time (- (System/currentTimeMillis) + (:start_time_millis query-execution)) + :result_rows (get query-result :row_count 0)) + (dissoc :start_time_millis))] + ;; only insert a new record into QueryExecution if the results *were not* cached (i.e., only if a Query was actually ran) + (when-not (:cached query-result) + (save-query-execution! query-execution)) + ;; ok, now return the results in the normal response format + (merge (dissoc query-execution :error :result_rows :hash :executor_id :native :card_id :dashboard_id :pulse_id) + query-result + {:status :completed + :average_execution_time (when (:cached query-result) + (query/average-execution-time-ms (:hash query-execution)))}))) (defn- assert-query-status-successful diff --git a/src/metabase/query_processor/middleware/cache.clj b/src/metabase/query_processor/middleware/cache.clj new file mode 100644 index 0000000000000000000000000000000000000000..06bea49173d50e81db33bda3737b1238e842219a --- /dev/null +++ b/src/metabase/query_processor/middleware/cache.clj @@ -0,0 +1,146 @@ +(ns metabase.query-processor.middleware.cache + "Middleware that returns cached results for queries when applicable. + + If caching is enabled (`enable-query-caching` is `true`) cached results will be returned for Cards if possible. There's + a global default TTL defined by the setting `query-caching-default-ttl`, but individual Cards can override this value + with custom TTLs with a value for `:cache_ttl`. + + For all other queries, caching is skipped. + + Various caching backends are defined in `metabase.query-processor.middleware.cache-backend` namespaces. + The default backend is `db`, which uses the application database; this value can be changed by setting the env var + `MB_QP_CACHE_BACKEND`. + + Refer to `metabase.query-processor.middleware.cache-backend.interface` for more details about how the cache backends themselves." + (:require [clojure.tools.logging :as log] + [metabase.config :as config] + [metabase.public-settings :as public-settings] + [metabase.query-processor.middleware.cache-backend.interface :as i] + [metabase.query-processor.util :as qputil] + [metabase.util :as u])) + +(def ^:dynamic ^Boolean *ignore-cached-results* + "Should we force the query to run, ignoring cached results even if they're available? + Setting this to `true` will run the query again and will still save the updated results." + false) + + +;;; ------------------------------------------------------------ Backend ------------------------------------------------------------ + +(def ^:private backend-instance + (atom nil)) + +(defn- valid-backend? [instance] (extends? i/IQueryProcessorCacheBackend (class instance))) + +(defn- get-backend-instance-in-namespace + "Return a valid query cache backend `instance` in BACKEND-NS-SYMB, or throw an Exception if none exists." + ;; if for some reason the resolved var doesn't satisfy `IQueryProcessorCacheBackend` we'll reload the namespace + ;; it belongs to and try one more time. + ;; This fixes the issue in dev sessions where the interface namespace gets reloaded causing the cache implementation + ;; to no longer satisfy the protocol + ([backend-ns-symb] + (get-backend-instance-in-namespace backend-ns-symb :allow-reload)) + ([backend-ns-symb allow-reload?] + (let [varr (ns-resolve backend-ns-symb 'instance)] + (cond + (not varr) (throw (Exception. (str "No var named 'instance' found in namespace " backend-ns-symb))) + (valid-backend? @varr) @varr + allow-reload? (do (require backend-ns-symb :reload) + (get-backend-instance-in-namespace backend-ns-symb false)) + :else (throw (Exception. (format "%s/instance doesn't satisfy IQueryProcessorCacheBackend" backend-ns-symb))))))) + +(defn- set-backend! + "Set the cache backend to the cache defined by the keyword BACKEND. + + (This should be something like `:db`, `:redis`, or `:memcached`. See the + documentation in `metabase.query-processor.middleware.cache-backend.interface` for details on how this works.)" + ([] + (set-backend! (config/config-kw :mb-qp-cache-backend))) + ([backend] + (let [backend-ns (symbol (str "metabase.query-processor.middleware.cache-backend." (munge (name backend))))] + (require backend-ns) + (log/info "Using query processor cache backend:" (u/format-color 'blue backend) (u/emoji "💾")) + (reset! backend-instance (get-backend-instance-in-namespace backend-ns))))) + + + +;;; ------------------------------------------------------------ Cache Operations ------------------------------------------------------------ + +(defn- cached-results [query-hash max-age-seconds] + (when-not *ignore-cached-results* + (when-let [results (i/cached-results @backend-instance query-hash max-age-seconds)] + (assert (u/is-temporal? (:updated_at results)) + "cached-results should include an `:updated_at` field containing the date when the query was last ran.") + (log/info "Returning cached results for query" (u/emoji "💾")) + (assoc results :cached true)))) + +(defn- save-results! [query-hash results] + (log/info "Caching results for next time for query" (u/emoji "💾")) + (i/save-results! @backend-instance query-hash results)) + + +;;; ------------------------------------------------------------ Middleware ------------------------------------------------------------ + +(defn- is-cacheable? ^Boolean [{cache-ttl :cache_ttl}] + (boolean (and (public-settings/enable-query-caching) + cache-ttl))) + +(defn- results-are-below-max-byte-threshold? + "Measure the size of the `:rows` in QUERY-RESULTS and see whether they're smaller than `query-caching-max-kb` + *before* compression." + ^Boolean [{{rows :rows} :data}] + (let [max-bytes (* (public-settings/query-caching-max-kb) 1024)] + ;; We don't want to serialize the entire result set since that could explode if the query is one that returns a + ;; huge number of rows. (We also want to keep `:rows` lazy.) + ;; So we'll serialize one row at a time, and keep a running total of bytes; if we pass the `query-caching-max-kb` + ;; threshold, we'll fail right away. + (loop [total-bytes 0, [row & more] rows] + (cond + (> total-bytes max-bytes) false + (not row) true + :else (recur (+ total-bytes (count (str row))) + more))))) + +(defn- save-results-if-successful! [query-hash results] + (when (and (= (:status results) :completed) + (or (results-are-below-max-byte-threshold? results) + (log/info "Results are too large to cache." (u/emoji "😫")))) + (save-results! query-hash results))) + +(defn- run-query-and-save-results-if-successful! [query-hash qp query] + (let [start-time-ms (System/currentTimeMillis) + results (qp query) + total-time-ms (- (System/currentTimeMillis) start-time-ms) + min-ttl-ms (* (public-settings/query-caching-min-ttl) 1000)] + (log/info (format "Query took %d ms to run; miminum for cache eligibility is %d ms" total-time-ms min-ttl-ms)) + (when (>= total-time-ms min-ttl-ms) + (save-results-if-successful! query-hash results)) + results)) + +(defn- run-query-with-cache [qp {cache-ttl :cache_ttl, :as query}] + (let [query-hash (qputil/query-hash query)] + (or (cached-results query-hash cache-ttl) + (run-query-and-save-results-if-successful! query-hash qp query)))) + + +(defn maybe-return-cached-results + "Middleware for caching results of a query if applicable. + In order for a query to be eligible for caching: + + * Caching (the `enable-query-caching` Setting) must be enabled + * The query must pass a `:cache_ttl` value. For Cards, this can be the value of `:cache_ttl`, + otherwise falling back to the value of the `query-caching-default-ttl` Setting. + * The query must already be permissions-checked. Since the cache bypasses the normal + query processor pipeline, the ad-hoc permissions-checking middleware isn't applied for cached results. + (The various `/api/card/` endpoints that make use of caching do `can-read?` checks for the Card *before* + running the query, satisfying this requirement.) + * The result *rows* of the query must be less than `query-caching-max-kb` when serialized (before compression)." + [qp] + ;; choose the caching backend if needed + (when-not @backend-instance + (set-backend!)) + ;; ok, now do the normal middleware thing + (fn [query] + (if-not (is-cacheable? query) + (qp query) + (run-query-with-cache qp query)))) diff --git a/src/metabase/query_processor/middleware/cache_backend/db.clj b/src/metabase/query_processor/middleware/cache_backend/db.clj new file mode 100644 index 0000000000000000000000000000000000000000..4964d8a88a83ad027b33f3781bbc0e965e568585 --- /dev/null +++ b/src/metabase/query_processor/middleware/cache_backend/db.clj @@ -0,0 +1,42 @@ +(ns metabase.query-processor.middleware.cache-backend.db + (:require [toucan.db :as db] + (metabase.models [interface :as models] + [query-cache :refer [QueryCache]]) + [metabase.public-settings :as public-settings] + [metabase.query-processor.middleware.cache-backend.interface :as i] + [metabase.util :as u])) + +(defn- cached-results + "Return cached results for QUERY-HASH if they exist and are newer than MAX-AGE-SECONDS." + [query-hash max-age-seconds] + (when-let [{:keys [results updated_at]} (db/select-one [QueryCache :results :updated_at] + :query_hash query-hash + :updated_at [:>= (u/->Timestamp (- (System/currentTimeMillis) + (* 1000 max-age-seconds)))])] + (assoc results :updated_at updated_at))) + +(defn- purge-old-cache-entries! + "Delete any cache entries that are older than the global max age `max-cache-entry-age-seconds` (currently 3 months)." + [] + (db/simple-delete! QueryCache + :updated_at [:<= (u/->Timestamp (- (System/currentTimeMillis) + (* 1000 (public-settings/query-caching-max-ttl))))])) + +(defn- save-results! + "Save the RESULTS of query with QUERY-HASH, updating an existing QueryCache entry + if one already exists, otherwise creating a new entry." + [query-hash results] + (purge-old-cache-entries!) + (or (db/update-where! QueryCache {:query_hash query-hash} + :updated_at (u/new-sql-timestamp) + :results (models/compress results)) ; have to manually call these here since Toucan doesn't call type conversion fns for update-where! (yet) + (db/insert! QueryCache + :query_hash query-hash + :results results)) + :ok) + +(def instance + "Implementation of `IQueryProcessorCacheBackend` that uses the database for caching results." + (reify i/IQueryProcessorCacheBackend + (cached-results [_ query-hash max-age-seconds] (cached-results query-hash max-age-seconds)) + (save-results! [_ query-hash results] (save-results! query-hash results)))) diff --git a/src/metabase/query_processor/middleware/cache_backend/interface.clj b/src/metabase/query_processor/middleware/cache_backend/interface.clj new file mode 100644 index 0000000000000000000000000000000000000000..4dbf762ca981c6f32ee4cc28fa0d0995f5be2c46 --- /dev/null +++ b/src/metabase/query_processor/middleware/cache_backend/interface.clj @@ -0,0 +1,37 @@ +(ns metabase.query-processor.middleware.cache-backend.interface + "Interface used to define different Query Processor cache backends. + Defining a backend is straightforward: define a new namespace with the pattern + + metabase.query-processor.middleware.cache-backend.<backend> + + Where backend is a key representing the backend, e.g. `db`, `redis`, or `memcached`. + + In that namespace, create an object that reifies (or otherwise implements) `IQueryProcessorCacheBackend`. + This object *must* be stored in a var called `instance`. + + That's it. See `metabase.query-processor.middleware.cache-backend.db` for a complete example of how this is done.") + + +(defprotocol IQueryProcessorCacheBackend + "Protocol that different Metabase cache backends must implement. + + QUERY-HASH as passed below is a byte-array representing a 256-byte SHA3 hash; encode this as needed for use as a + cache entry key. RESULTS are passed (and should be returned) as a Clojure object, and individual backends are free + to encode this as appropriate when storing the results. (It's probably not a bad idea to compress the results; this + is what the `:db` backend does.)" + + (cached-results [this, query-hash, ^Integer max-age-seconds] + "Return cached results for the query with byte array QUERY-HASH if those results are present in the cache and are less + than MAX-AGE-SECONDS old. Otherwise, return `nil`. + + This method must also return a Timestamp from when the query was last ran. This must be `assoc`ed with the query results + under the key `:updated_at`. + + (cached-results [_ query-hash max-age-seconds] + (when-let [[results updated-at] (maybe-fetch-results query-hash max-age-seconds)] + (assoc results :updated_at updated-at)))") + + (save-results! [this query-hash results] + "Add a cache entry with the RESULTS of running query with byte array QUERY-HASH. + This should replace any prior entries for QUERY-HASH and update the cache timestamp to the current system time. + (This is also an appropriate point to purge any entries older than the value of the `query-caching-max-ttl` Setting.)")) diff --git a/src/metabase/query_processor/util.clj b/src/metabase/query_processor/util.clj index 3846fb546662a6f6e15dd7d0f4ac8dcb01127db0..dec320e4dc655fec44f52390c155f7996624686f 100644 --- a/src/metabase/query_processor/util.clj +++ b/src/metabase/query_processor/util.clj @@ -3,8 +3,7 @@ (:require (buddy.core [codecs :as codecs] [hash :as hash]) [cheshire.core :as json] - [toucan.db :as db] - [metabase.models.query-execution :refer [QueryExecution]])) + [toucan.db :as db])) (defn mbql-query? "Is the given query an MBQL query?" @@ -41,7 +40,7 @@ (This is done so irrelevant info or options that don't affect query results doesn't result in the same query producing different hashes.)" [query] {:pre [(map? query)]} - (let [{:keys [constraints parameters], :as query} (select-keys query [:database :type :query :parameters :constraints])] + (let [{:keys [constraints parameters], :as query} (select-keys query [:database :type :query :native :parameters :constraints])] (cond-> query (empty? constraints) (dissoc :constraints) (empty? parameters) (dissoc :parameters)))) @@ -50,17 +49,3 @@ "Return a 256-bit SHA3 hash of QUERY as a key for the cache. (This is returned as a byte array.)" [query] (hash/sha3-256 (json/generate-string (select-keys-for-hashing query)))) - - -;;; ------------------------------------------------------------ Historic Duration Info ------------------------------------------------------------ - -(defn query-average-duration - "Return the average running time of QUERY over the last 10 executions in milliseconds. - Returns `nil` if there's not available data." - ^Float [query] - (when-let [running-times (db/select-field :running_time QueryExecution - :hash (query-hash query) - {:order-by [[:started_at :desc]] - :limit 10})] - (float (/ (reduce + running-times) - (count running-times))))) diff --git a/src/metabase/util/honeysql_extensions.clj b/src/metabase/util/honeysql_extensions.clj index 66a5ccaef18b7da1b29dd32bd7fce661c6dd6aae..70bfe6de28eaa0af1de1aae991c085fa5f54f685 100644 --- a/src/metabase/util/honeysql_extensions.clj +++ b/src/metabase/util/honeysql_extensions.clj @@ -21,6 +21,25 @@ (intern 'honeysql.format 'quote-fns (assoc quote-fns :h2 (comp s/upper-case ansi-quote-fn)))) + +;; `:crate` quote style that correctly quotes nested column identifiers +(defn- str-insert + "Insert C in string S at index I." + [s c i] + (str c (subs s 0 i) c (subs s i))) + +(defn- crate-column-identifier + [^CharSequence s] + (let [idx (s/index-of s "[")] + (if (nil? idx) + (str \" s \") + (str-insert s "\"" idx)))) + +(let [quote-fns @(resolve 'honeysql.format/quote-fns)] + (intern 'honeysql.format 'quote-fns + (assoc quote-fns :crate crate-column-identifier))) + + ;; register the `extract` function with HoneySQL ;; (hsql/format (hsql/call :extract :a :b)) -> "extract(a from b)" (defmethod hformat/fn-handler "extract" [_ unit expr] @@ -92,10 +111,15 @@ (hsql/call :cast x (hsql/raw (name c)))) (defn format - "SQL `FORMAT` function." + "SQL `format` function." [format-str expr] (hsql/call :format expr (literal format-str))) +(defn round + "SQL `round` function." + [x decimal-places] + (hsql/call :round x decimal-places)) + (defn ->date "CAST X to a `date`." [x] (cast :date x)) (defn ->datetime "CAST X to a `datetime`." [x] (cast :datetime x)) (defn ->timestamp "CAST X to a `timestamp`." [x] (cast :timestamp x)) diff --git a/src/metabase/util/urls.clj b/src/metabase/util/urls.clj index db1c2948f50289fd71a4222729b61d59259f920c..3c27b5ee2033afecfc12bad23d45101565f0e1e0 100644 --- a/src/metabase/util/urls.clj +++ b/src/metabase/util/urls.clj @@ -18,16 +18,16 @@ (defn dashboard-url "Return an appropriate URL for a `Dashboard` with ID. - (dashboard-url 10) -> \"http://localhost:3000/dash/10\"" + (dashboard-url 10) -> \"http://localhost:3000/dashboard/10\"" [^Integer id] - (format "%s/dash/%d" (public-settings/site-url) id)) + (format "%s/dashboard/%d" (public-settings/site-url) id)) (defn card-url "Return an appropriate URL for a `Card` with ID. - (card-url 10) -> \"http://localhost:3000/card/10\"" + (card-url 10) -> \"http://localhost:3000/question/10\"" [^Integer id] - (format "%s/card/%d" (public-settings/site-url) id)) + (format "%s/question/%d" (public-settings/site-url) id)) (defn segment-url "Return an appropriate URL for a `Segment` with ID. diff --git a/test/metabase/api/card_test.clj b/test/metabase/api/card_test.clj index 34a759795d2831efa8e133687e49cebdbeaf7780..a42630ea44f06bb291c5756a1f8802529f291577 100644 --- a/test/metabase/api/card_test.clj +++ b/test/metabase/api/card_test.clj @@ -36,7 +36,8 @@ :embedding_params nil :made_public_by_id nil :public_uuid nil - :query_type "query"}) + :query_type "query" + :cache_ttl nil}) ;; ## GET /api/card ;; Filter cards by database @@ -246,6 +247,20 @@ (set-archived! true) (set-archived! false)]))) +;; Can we clear the description of a Card? (#4738) +(expect + nil + (with-temp-card [card {:description "What a nice Card"}] + ((user->client :rasta) :put 200 (str "card/" (u/get-id card)) {:description nil}) + (db/select-one-field :description Card :id (u/get-id card)))) + +;; description should be blankable as well +(expect + "" + (with-temp-card [card {:description "What a nice Card"}] + ((user->client :rasta) :put 200 (str "card/" (u/get-id card)) {:description ""}) + (db/select-one-field :description Card :id (u/get-id card)))) + ;; Can we update a card's embedding_params? (expect {:abc "enabled"} @@ -513,6 +528,22 @@ "Not found." ((user->client :rasta) :get 404 "card/" :collection :some_fake_collection_slug)) +;; Make sure GET /api/card?collection=<slug> still works with Collections with URL-encoded Slugs (#4535) +(expect + [] + (tt/with-temp Collection [collection {:name "Obsługa klienta"}] + (do + (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) + ((user->client :rasta) :get 200 "card/" :collection "obs%C5%82uga_klienta")))) + +;; ...even if the slug isn't passed in URL-encoded +(expect + [] + (tt/with-temp Collection [collection {:name "Obsługa klienta"}] + (do + (perms/grant-collection-readwrite-permissions! (perms-group/all-users) collection) + ((user->client :rasta) :get 200 "card/" :collection "obsługa_klienta")))) + ;;; ------------------------------------------------------------ Bulk Collections Update (POST /api/card/collections) ------------------------------------------------------------ diff --git a/test/metabase/api/dashboard_test.clj b/test/metabase/api/dashboard_test.clj index 5ee7f99801c38c1b1de019ed090be47362e6cd41..3020f1edddba5fbd21ffe2a5a0ffcbe17e3a2bcb 100644 --- a/test/metabase/api/dashboard_test.clj +++ b/test/metabase/api/dashboard_test.clj @@ -159,6 +159,19 @@ :creator_id (user->id :trashbird)}) (Dashboard dashboard-id)]))) +;; Can we clear the description of a Dashboard? (#4738) +(expect + nil + (tt/with-temp Dashboard [dashboard {:description "What a nice Dashboard"}] + ((user->client :rasta) :put 200 (str "dashboard/" (u/get-id dashboard)) {:description nil}) + (db/select-one-field :description Dashboard :id (u/get-id dashboard)))) + +(expect + "" + (tt/with-temp Dashboard [dashboard {:description "What a nice Dashboard"}] + ((user->client :rasta) :put 200 (str "dashboard/" (u/get-id dashboard)) {:description ""}) + (db/select-one-field :description Dashboard :id (u/get-id dashboard)))) + ;; ## DELETE /api/dashboard/:id (expect diff --git a/test/metabase/api/dataset_test.clj b/test/metabase/api/dataset_test.clj index 20a86830f56f070d9df5a9b72ec7eb20930ce132..637acfad10827deecd13941e0ba37f3ff514f5b3 100644 --- a/test/metabase/api/dataset_test.clj +++ b/test/metabase/api/dataset_test.clj @@ -52,22 +52,23 @@ ;; Just a basic sanity check to make sure Query Processor endpoint is still working correctly. (expect [;; API call response - {:data {:rows [[1000]] - :columns ["count"] - :cols [{:base_type "type/Integer", :special_type "type/Number", :name "count", :display_name "count", :id nil, :table_id nil, - :description nil, :target nil, :extra_info {}, :source "aggregation"}] - :native_form true} - :row_count 1 - :status "completed" - :context "ad-hoc" - :json_query (-> (wrap-inner-query - (query checkins - (ql/aggregation (ql/count)))) - (assoc :type "query") - (assoc-in [:query :aggregation] [{:aggregation-type "count", :custom-name nil}]) - (assoc :constraints default-query-constraints)) - :started_at true - :running_time true} + {:data {:rows [[1000]] + :columns ["count"] + :cols [{:base_type "type/Integer", :special_type "type/Number", :name "count", :display_name "count", :id nil, :table_id nil, + :description nil, :target nil, :extra_info {}, :source "aggregation"}] + :native_form true} + :row_count 1 + :status "completed" + :context "ad-hoc" + :json_query (-> (wrap-inner-query + (query checkins + (ql/aggregation (ql/count)))) + (assoc :type "query") + (assoc-in [:query :aggregation] [{:aggregation-type "count", :custom-name nil}]) + (assoc :constraints default-query-constraints)) + :started_at true + :running_time true + :average_execution_time nil} ;; QueryExecution record in the DB {:hash true :row_count 1 diff --git a/test/metabase/cmd/load_from_h2_test.clj b/test/metabase/cmd/load_from_h2_test.clj index c9a6c119457d11dc399285b187257b64bdddf860..3d3942f4773bce5f0f0b16c0e2fcd3145abd288e 100644 --- a/test/metabase/cmd/load_from_h2_test.clj +++ b/test/metabase/cmd/load_from_h2_test.clj @@ -13,7 +13,10 @@ (def ^:private models-to-exclude "Models that should *not* be migrated in `load-from-h2`." - #{"LegacyQueryExecution"}) + #{"LegacyQueryExecution" + "Query" + "QueryCache" + "QueryExecution"}) (defn- all-model-names [] (set (for [ns (ns-find/find-namespaces (classpath/classpath)) diff --git a/test/metabase/driver/bigquery_test.clj b/test/metabase/driver/bigquery_test.clj index d6767253f02cc9514439c1ae831eef331aeb3370..976bf1d3410d88076d6b5c1b41f58b063ec2f709 100644 --- a/test/metabase/driver/bigquery_test.clj +++ b/test/metabase/driver/bigquery_test.clj @@ -1,10 +1,14 @@ (ns metabase.driver.bigquery-test - (:require metabase.driver.bigquery + (:require [expectations :refer :all] + metabase.driver.bigquery [metabase.models.database :as database] [metabase.query-processor :as qp] + [metabase.query-processor.expand :as ql] + [metabase.query-processor-test :as qptest] [metabase.test.data :as data] (metabase.test.data [datasets :refer [expect-with-engine]] - [interface :refer [def-database-definition]]))) + [interface :refer [def-database-definition]]) + [metabase.test.util :as tu])) ;; Test native queries @@ -29,3 +33,58 @@ :type :native :database (data/id)})) [:cols :columns])) + +;; make sure that the bigquery driver can handle named columns with characters that aren't allowed in BQ itself +(expect-with-engine :bigquery + {:rows [[113]] + :columns ["User_ID_Plus_Venue_ID"]} + (qptest/rows+column-names + (qp/process-query {:database (data/id) + :type "query" + :query {:source_table (data/id :checkins) + :aggregation [["named" ["max" ["+" ["field-id" (data/id :checkins :user_id)] + ["field-id" (data/id :checkins :venue_id)]]] + "User ID Plus Venue ID"]]}}))) + +;; make sure BigQuery can handle two aggregations with the same name (#4089) +(tu/resolve-private-vars metabase.driver.bigquery + deduplicate-aliases update-select-subclause-aliases) + +(expect + ["sum" "count" "sum_2" "avg" "sum_3" "min"] + (deduplicate-aliases ["sum" "count" "sum" "avg" "sum" "min"])) + +(expect + ["sum" "count" "sum_2" "avg" "sum_2_2" "min"] + (deduplicate-aliases ["sum" "count" "sum" "avg" "sum_2" "min"])) + +(expect + ["sum" "count" nil "sum_2"] + (deduplicate-aliases ["sum" "count" nil "sum"])) + +(expect + [[:user_id "user_id_2"] :venue_id] + (update-select-subclause-aliases [[:user_id "user_id"] :venue_id] + ["user_id_2" nil])) + + +(expect-with-engine :bigquery + {:rows [[7929 7929]], :columns ["sum" "sum_2"]} + (qptest/rows+column-names + (qp/process-query {:database (data/id) + :type "query" + :query (-> {} + (ql/source-table (data/id :checkins)) + (ql/aggregation (ql/sum (ql/field-id (data/id :checkins :user_id))) + (ql/sum (ql/field-id (data/id :checkins :user_id)))))}))) + +(expect-with-engine :bigquery + {:rows [[7929 7929 7929]], :columns ["sum" "sum_2" "sum_3"]} + (qptest/rows+column-names + (qp/process-query {:database (data/id) + :type "query" + :query (-> {} + (ql/source-table (data/id :checkins)) + (ql/aggregation (ql/sum (ql/field-id (data/id :checkins :user_id))) + (ql/sum (ql/field-id (data/id :checkins :user_id))) + (ql/sum (ql/field-id (data/id :checkins :user_id)))))}))) diff --git a/test/metabase/driver/generic_sql/util/unprepare_test.clj b/test/metabase/driver/generic_sql/util/unprepare_test.clj index b20a4d91e9bbf2cad244e8258bd19c5de1c573de..ce98bb08df85eeff569c6943d0b1a9155d57d096 100644 --- a/test/metabase/driver/generic_sql/util/unprepare_test.clj +++ b/test/metabase/driver/generic_sql/util/unprepare_test.clj @@ -8,3 +8,12 @@ "Cam's Cool Toucan" true #inst "2017-01-01T00:00:00.000Z"])) + +(expect + "SELECT 'Cam''s Cool Toucan' FROM TRUE WHERE x ?? y AND z = from_iso8601_timestamp('2017-01-01T00:00:00.000Z')" + (unprepare/unprepare ["SELECT ? FROM ? WHERE x ?? y AND z = ?" + "Cam's Cool Toucan" + true + #inst "2017-01-01T00:00:00.000Z"] + :quote-escape "'" + :iso-8601-fn :from_iso8601_timestamp)) diff --git a/test/metabase/driver/generic_sql_test.clj b/test/metabase/driver/generic_sql_test.clj index bbb68a74743a7d5775c342a05c8827bc168405f3..4c51952cf9e49066ce8c3f5276433e1511db5218 100644 --- a/test/metabase/driver/generic_sql_test.clj +++ b/test/metabase/driver/generic_sql_test.clj @@ -19,7 +19,7 @@ (def ^:private generic-sql-engines (delay (set (for [engine datasets/all-valid-engines :let [driver (driver/engine->driver engine)] - :when (not= engine :bigquery) ; bigquery doesn't use the generic sql implementations of things like `field-avg-length` + :when (not (contains? #{:bigquery :presto} engine)) ; bigquery and presto don't use the generic sql implementations of things like `field-avg-length` :when (extends? ISQLDriver (class driver))] (do (require (symbol (str "metabase.test.data." (name engine))) :reload) ; otherwise it gets all snippy if you try to do `lein test metabase.driver.generic-sql-test` engine))))) diff --git a/test/metabase/driver/mongo_test.clj b/test/metabase/driver/mongo_test.clj index 3332a298277eff2370b30b79f5f3f47781186cdc..1387e674bc6939b26d293749344eb763b1c821f4 100644 --- a/test/metabase/driver/mongo_test.clj +++ b/test/metabase/driver/mongo_test.clj @@ -175,21 +175,33 @@ (ql/filter (ql/= $bird_id "abcdefabcdefabcdefabcdef")))))) -;;; ------------------------------------------------------------ Test that we can handle native queries with "ISODate(...)" forms (#3741) ------------------------------------------------------------ +;;; ------------------------------------------------------------ Test that we can handle native queries with "ISODate(...)" and "ObjectId(...) forms (#3741, #4448) ------------------------------------------------------------ (tu/resolve-private-vars metabase.driver.mongo.query-processor - maybe-decode-iso-date-fncall decode-iso-date-fncalls encode-iso-date-fncalls) + maybe-decode-fncall decode-fncalls encode-fncalls) (expect "[{\"$match\":{\"date\":{\"$gte\":[\"___ISODate\", \"2012-01-01\"]}}}]" - (encode-iso-date-fncalls "[{\"$match\":{\"date\":{\"$gte\":ISODate(\"2012-01-01\")}}}]")) + (encode-fncalls "[{\"$match\":{\"date\":{\"$gte\":ISODate(\"2012-01-01\")}}}]")) + +(expect + "[{\"$match\":{\"entityId\":{\"$eq\":[\"___ObjectId\", \"583327789137b2700a1621fb\"]}}}]" + (encode-fncalls "[{\"$match\":{\"entityId\":{\"$eq\":ObjectId(\"583327789137b2700a1621fb\")}}}]")) (expect (DateTime. "2012-01-01") - (maybe-decode-iso-date-fncall ["___ISODate" "2012-01-01"])) + (maybe-decode-fncall ["___ISODate" "2012-01-01"])) + +(expect + (ObjectId. "583327789137b2700a1621fb") + (maybe-decode-fncall ["___ObjectId" "583327789137b2700a1621fb"])) (expect [{:$match {:date {:$gte (DateTime. "2012-01-01")}}}] - (decode-iso-date-fncalls [{:$match {:date {:$gte ["___ISODate" "2012-01-01"]}}}])) + (decode-fncalls [{:$match {:date {:$gte ["___ISODate" "2012-01-01"]}}}])) + +(expect + [{:$match {:entityId {:$eq (ObjectId. "583327789137b2700a1621fb")}}}] + (decode-fncalls [{:$match {:entityId {:$eq ["___ObjectId" "583327789137b2700a1621fb"]}}}])) (datasets/expect-with-engine :mongo 5 @@ -197,3 +209,11 @@ :collection "checkins"} :type :native :database (data/id)})))) + +(datasets/expect-with-engine :mongo + 0 + ;; this query shouldn't match anything, so we're just checking that it completes successfully + (count (rows (qp/process-query {:native {:query "[{\"$match\": {\"_id\": {\"$eq\": ObjectId(\"583327789137b2700a1621fb\")}}}]" + :collection "venues"} + :type :native + :database (data/id)})))) diff --git a/test/metabase/driver/presto_test.clj b/test/metabase/driver/presto_test.clj new file mode 100644 index 0000000000000000000000000000000000000000..86502b962ef26390c1e6f3ae9c777b69409dc94d --- /dev/null +++ b/test/metabase/driver/presto_test.clj @@ -0,0 +1,143 @@ +(ns metabase.driver.presto-test + (:require [expectations :refer :all] + [toucan.db :as db] + [metabase.driver :as driver] + [metabase.driver.generic-sql :as sql] + [metabase.models.table :as table] + [metabase.test.data :as data] + [metabase.test.data.datasets :as datasets] + [metabase.test.util :refer [resolve-private-vars]]) + (:import (metabase.driver.presto PrestoDriver))) + +(resolve-private-vars metabase.driver.presto details->uri details->request parse-presto-results quote-name quote+combine-names apply-page) + +;;; HELPERS + +(expect + "http://localhost:8080/" + (details->uri {:host "localhost", :port 8080, :ssl false} "/")) + +(expect + "https://localhost:8443/" + (details->uri {:host "localhost", :port 8443, :ssl true} "/")) + +(expect + "http://localhost:8080/v1/statement" + (details->uri {:host "localhost", :port 8080, :ssl false} "/v1/statement")) + +(expect + {:headers {"X-Presto-Source" "metabase" + "X-Presto-User" "user"}} + (details->request {:user "user"})) + +(expect + {:headers {"X-Presto-Source" "metabase" + "X-Presto-User" "user"} + :basic-auth ["user" "test"]} + (details->request {:user "user", :password "test"})) + +(expect + {:headers {"X-Presto-Source" "metabase" + "X-Presto-User" "user" + "X-Presto-Catalog" "test_data" + "X-Presto-Time-Zone" "America/Toronto"}} + (details->request {:user "user", :catalog "test_data", :report-timezone "America/Toronto"})) + +(expect + [["2017-04-03" + #inst "2017-04-03T14:19:17.417000000-00:00" + #inst "2017-04-03T10:19:17.417000000-00:00" + 3.1416M + "test"]] + (parse-presto-results [{:type "date"} {:type "timestamp with time zone"} {:type "timestamp"} {:type "decimal(10,4)"} {:type "varchar(255)"}] + [["2017-04-03", "2017-04-03 10:19:17.417 America/Toronto", "2017-04-03 10:19:17.417", "3.1416", "test"]])) + +(expect + "\"weird.table\"\" name\"" + (quote-name "weird.table\" name")) + +(expect + "\"weird . \"\"schema\".\"weird.table\"\" name\"" + (quote+combine-names "weird . \"schema" "weird.table\" name")) + +;; DESCRIBE-DATABASE +(datasets/expect-with-engine :presto + {:tables #{{:name "categories" :schema "default"} + {:name "venues" :schema "default"} + {:name "checkins" :schema "default"} + {:name "users" :schema "default"}}} + (driver/describe-database (PrestoDriver.) (data/db))) + +;; DESCRIBE-TABLE +(datasets/expect-with-engine :presto + {:name "venues" + :schema "default" + :fields #{{:name "name", + :base-type :type/Text} + {:name "latitude" + :base-type :type/Float} + {:name "longitude" + :base-type :type/Float} + {:name "price" + :base-type :type/Integer} + {:name "category_id" + :base-type :type/Integer} + {:name "id" + :base-type :type/Integer}}} + (driver/describe-table (PrestoDriver.) (data/db) (db/select-one 'Table :id (data/id :venues)))) + +;;; ANALYZE-TABLE +(datasets/expect-with-engine :presto + {:row_count 100 + :fields [{:id (data/id :venues :category_id), :values [2 3 4 5 6 7 10 11 12 13 14 15 18 19 20 29 40 43 44 46 48 49 50 58 64 67 71 74]} + {:id (data/id :venues :id)} + {:id (data/id :venues :latitude)} + {:id (data/id :venues :longitude)} + {:id (data/id :venues :name), :values (db/select-one-field :values 'FieldValues, :field_id (data/id :venues :name))} + {:id (data/id :venues :price), :values [1 2 3 4]}]} + (let [venues-table (db/select-one 'Table :id (data/id :venues))] + (driver/analyze-table (PrestoDriver.) venues-table (set (mapv :id (table/fields venues-table)))))) + +;;; FIELD-VALUES-LAZY-SEQ +(datasets/expect-with-engine :presto + ["Red Medicine" + "Stout Burgers & Beers" + "The Apple Pan" + "Wurstküche" + "Brite Spot Family Restaurant"] + (take 5 (driver/field-values-lazy-seq (PrestoDriver.) (db/select-one 'Field :id (data/id :venues :name))))) + +;;; TABLE-ROWS-SEQ +(datasets/expect-with-engine :presto + [{:name "Red Medicine", :price 3, :category_id 4, :id 1} + {:name "Stout Burgers & Beers", :price 2, :category_id 11, :id 2} + {:name "The Apple Pan", :price 2, :category_id 11, :id 3} + {:name "Wurstküche", :price 2, :category_id 29, :id 4} + {:name "Brite Spot Family Restaurant", :price 2, :category_id 20, :id 5}] + (for [row (take 5 (sort-by :id (driver/table-rows-seq (PrestoDriver.) + (db/select-one 'Database :id (data/id)) + (db/select-one 'RawTable :id (db/select-one-field :raw_table_id 'Table, :id (data/id :venues))))))] + (-> (dissoc row :latitude :longitude) + (update :price int) + (update :category_id int) + (update :id int)))) + +;;; FIELD-PERCENT-URLS +(datasets/expect-with-engine :presto + 0.5 + (data/dataset half-valid-urls + (sql/field-percent-urls (PrestoDriver.) (db/select-one 'Field :id (data/id :urls :url))))) + +;;; APPLY-PAGE +(expect + {:select ["name" "id"] + :from [{:select [[:default.categories.name "name"] [:default.categories.id "id"] [{:s "row_number() OVER (ORDER BY \"default\".\"categories\".\"id\" ASC)"} :__rownum__]] + :from [:default.categories] + :order-by [[:default.categories.id :asc]]}] + :where [:> :__rownum__ 5] + :limit 5} + (apply-page {:select [[:default.categories.name "name"] [:default.categories.id "id"]] + :from [:default.categories] + :order-by [[:default.categories.id :asc]]} + {:page {:page 2 + :items 5}})) diff --git a/test/metabase/events/revision_test.clj b/test/metabase/events/revision_test.clj index 72c08795905261c829981144aba90e986bdc4abc..ef56ae4507f773109b1879a3d40dd5d45ef6f5bc 100644 --- a/test/metabase/events/revision_test.clj +++ b/test/metabase/events/revision_test.clj @@ -41,6 +41,7 @@ :made_public_by_id nil :name (:name card) :public_uuid nil + :cache_ttl nil :query_type "query" :table_id (id :categories) :visualization_settings {}}) diff --git a/test/metabase/permissions_collection_test.clj b/test/metabase/permissions_collection_test.clj index a4d86a18d98bf5b22125afd3990a91eb20302c4a..d6e82ec37120919812faf7c81eb07854a1964be5 100644 --- a/test/metabase/permissions_collection_test.clj +++ b/test/metabase/permissions_collection_test.clj @@ -19,9 +19,9 @@ (defn- api-call-was-successful? {:style/indent 0} [response] (when (and (string? response) (not= response "You don't have permissions to do that.")) - (println "users in db:" (db/select-field :email 'User)) ; NOCOMMIT (println "RESPONSE:" response)) ; DEBUG - (not= response "You don't have permissions to do that.")) + (and (not= response "You don't have permissions to do that.") + (not= response "Unauthenticated"))) (defn- can-run-query? [username] (api-call-was-successful? ((test-users/user->client username) :post (format "card/%d/query" (u/get-id *card:db2-count-of-venues*))))) @@ -52,9 +52,12 @@ (perms-test/expect-with-test-data true (tt/with-temp Collection [collection] + (println "[In the occasionally failing test]") ; DEBUG (set-card-collection! collection) (permissions/grant-collection-read-permissions! (group/all-users) collection) - (can-run-query? :rasta))) + ;; try it twice because sometimes it randomly fails :unamused: + (or (can-run-query? :rasta) + (can-run-query? :rasta)))) ;; Make sure a User isn't allowed to save a Card they have collections readwrite permissions for ;; if they don't have data perms for the query diff --git a/test/metabase/query_processor/middleware/cache_test.clj b/test/metabase/query_processor/middleware/cache_test.clj new file mode 100644 index 0000000000000000000000000000000000000000..9a2f3afe91e5fcb54beedccd69ec417579ac5ef2 --- /dev/null +++ b/test/metabase/query_processor/middleware/cache_test.clj @@ -0,0 +1,187 @@ +(ns metabase.query-processor.middleware.cache-test + "Tests for the Query Processor cache." + (:require [expectations :refer :all] + [toucan.db :as db] + [metabase.models.query-cache :refer [QueryCache]] + [metabase.query-processor.middleware.cache :as cache] + [metabase.test.util :as tu])) + +(tu/resolve-private-vars metabase.query-processor.middleware.cache + is-cacheable? + results-are-below-max-byte-threshold?) + +(def ^:private mock-results + {:row_count 8 + :status :completed + :data {:rows [[:toucan 71] + [:bald-eagle 92] + [:hummingbird 11] + [:owl 10] + [:chicken 69] + [:robin 96] + [:osprey 72] + [:flamingo 70]]}}) + +(def ^:private ^:dynamic ^Integer *query-execution-delay-ms* 0) + +(defn- mock-qp [& _] + (Thread/sleep *query-execution-delay-ms*) + mock-results) + +(def ^:private maybe-return-cached-results (cache/maybe-return-cached-results mock-qp)) + +(defn- clear-cache! [] (db/simple-delete! QueryCache)) + +(defn- cached? [results] + (if (:cached results) + :cached + :not-cached)) + +(defn- run-query [& {:as query-kvs}] + (cached? (maybe-return-cached-results (merge {:cache_ttl 60, :query :abc} query-kvs)))) + + +;;; ------------------------------------------------------------ tests for is-cacheable? ------------------------------------------------------------ + +;; something is-cacheable? if it includes a cach_ttl and the caching setting is enabled +(expect + (tu/with-temporary-setting-values [enable-query-caching true] + (is-cacheable? {:cache_ttl 100}))) + +(expect + false + (tu/with-temporary-setting-values [enable-query-caching false] + (is-cacheable? {:cache_ttl 100}))) + +(expect + false + (tu/with-temporary-setting-values [enable-query-caching true] + (is-cacheable? {:cache_ttl nil}))) + + +;;; ------------------------------------------------------------ results-are-below-max-byte-threshold? ------------------------------------------------------------ + +(expect + (tu/with-temporary-setting-values [query-caching-max-kb 128] + (results-are-below-max-byte-threshold? {:data {:rows [[1 "ABCDEF"] + [3 "GHIJKL"]]}}))) + +(expect + false + (tu/with-temporary-setting-values [query-caching-max-kb 1] + (results-are-below-max-byte-threshold? {:data {:rows (repeat 500 [1 "ABCDEF"])}}))) + +;; check that `results-are-below-max-byte-threshold?` is lazy and fails fast if the query is over the threshold rather than serializing the entire thing +(expect + false + (let [lazy-seq-realized? (atom false)] + (tu/with-temporary-setting-values [query-caching-max-kb 1] + (results-are-below-max-byte-threshold? {:data {:rows (lazy-cat (repeat 500 [1 "ABCDEF"]) + (do (reset! lazy-seq-realized? true) + [2 "GHIJKL"]))}}) + @lazy-seq-realized?))) + + +;;; ------------------------------------------------------------ End-to-end middleware tests ------------------------------------------------------------ + +;; if there's nothing in the cache, cached results should *not* be returned +(expect + :not-cached + (tu/with-temporary-setting-values [enable-query-caching true + query-caching-min-ttl 0] + (clear-cache!) + (run-query))) + +;; if we run the query twice, the second run should return cached results +(expect + :cached + (tu/with-temporary-setting-values [enable-query-caching true + query-caching-min-ttl 0] + (clear-cache!) + (run-query) + (run-query))) + +;; ...but if the cache entry is past it's TTL, the cached results shouldn't be returned +(expect + :not-cached + (tu/with-temporary-setting-values [enable-query-caching true + query-caching-min-ttl 0] + (clear-cache!) + (run-query :cache_ttl 1) + (Thread/sleep 2000) + (run-query :cache_ttl 1))) + +;; if caching is disabled then cache shouldn't be used even if there's something valid in there +(expect + :not-cached + (tu/with-temporary-setting-values [enable-query-caching true + query-caching-min-ttl 0] + (clear-cache!) + (run-query) + (tu/with-temporary-setting-values [enable-query-caching false + query-caching-min-ttl 0] + (run-query)))) + + +;; check that `query-caching-max-kb` is respected and queries aren't cached if they're past the threshold +(expect + :not-cached + (tu/with-temporary-setting-values [enable-query-caching true + query-caching-max-kb 0 + query-caching-min-ttl 0] + (clear-cache!) + (run-query) + (run-query))) + +;; check that `query-caching-max-ttl` is respected. Whenever a new query is cached the cache should evict any entries older that `query-caching-max-ttl`. +;; Set max-ttl to one second, run query `:abc`, then wait two seconds, and run `:def`. This should trigger the cache flush for entries past `:max-ttl`; +;; and the cached entry for `:abc` should be deleted. Running `:abc` a subsequent time should not return cached results +(expect + :not-cached + (tu/with-temporary-setting-values [enable-query-caching true + query-caching-max-ttl 1 + query-caching-min-ttl 0] + (clear-cache!) + (run-query) + (Thread/sleep 2000) + (run-query, :query :def) + (run-query))) + +;; check that *ignore-cached-results* is respected when returning results... +(expect + :not-cached + (tu/with-temporary-setting-values [enable-query-caching true + query-caching-min-ttl 0] + (clear-cache!) + (run-query) + (binding [cache/*ignore-cached-results* true] + (run-query)))) + +;; ...but if it's set those results should still be cached for next time. +(expect + :cached + (tu/with-temporary-setting-values [enable-query-caching true + query-caching-min-ttl 0] + (clear-cache!) + (binding [cache/*ignore-cached-results* true] + (run-query)) + (run-query))) + +;; if the cache takes less than the min TTL to execute, it shouldn't be cached +(expect + :not-cached + (tu/with-temporary-setting-values [enable-query-caching true + query-caching-min-ttl 60] + (clear-cache!) + (run-query) + (run-query))) + +;; ...but if it takes *longer* than the min TTL, it should be cached +(expect + :cached + (tu/with-temporary-setting-values [enable-query-caching true + query-caching-min-ttl 1] + (binding [*query-execution-delay-ms* 1200] + (clear-cache!) + (run-query) + (run-query)))) diff --git a/test/metabase/query_processor/sql_parameters_test.clj b/test/metabase/query_processor/sql_parameters_test.clj index 1fbf45c83e4e6c00ad3494fe62c0542ea2802f8d..dda33ecf8b2442c1e0c354b4914fd7f1595774aa 100644 --- a/test/metabase/query_processor/sql_parameters_test.clj +++ b/test/metabase/query_processor/sql_parameters_test.clj @@ -428,9 +428,10 @@ (generic-sql/quote-name datasets/*driver* identifier)) (defn- checkins-identifier [] - ;; HACK ! I don't have all day to write protocol methods to make this work the "right" way so for BigQuery we will just hackily return the correct identifier here - (if (= datasets/*engine* :bigquery) - "[test_data.checkins]" + ;; HACK ! I don't have all day to write protocol methods to make this work the "right" way so for BigQuery and Presto we will just hackily return the correct identifier here + (case datasets/*engine* + :bigquery "[test_data.checkins]" + :presto "\"default\".\"checkins\"" (let [{table-name :name, schema :schema} (db/select-one ['Table :name :schema], :id (data/id :checkins))] (str (when (seq schema) (str (quote-name schema) \.)) diff --git a/test/metabase/query_processor/util_test.clj b/test/metabase/query_processor/util_test.clj index 160278cdc2f1852da20ac0c35c5eda8b99d69a4e..357583558a259029c48d0101d354c459d7c69542 100644 --- a/test/metabase/query_processor/util_test.clj +++ b/test/metabase/query_processor/util_test.clj @@ -94,3 +94,14 @@ (qputil/query-hash {:query :abc}) (qputil/query-hash {:query :abc, :constraints nil}) (qputil/query-hash {:query :abc, :constraints {}}))) + +;; make sure two different natiev queries have different hashes! +(expect + false + (array= + (qputil/query-hash {:database 2 + :type "native" + :native {:query "SELECT pg_sleep(15), 1 AS one"}}) + (qputil/query-hash {:database 2 + :type "native" + :native {:query "SELECT pg_sleep(15), 2 AS two"}}))) diff --git a/test/metabase/query_processor_test.clj b/test/metabase/query_processor_test.clj index 04989644a6e345eb4e400ba9936cf784657e10bd..3e0fb4f0391f77122d15acba55ae9645e5121b12 100644 --- a/test/metabase/query_processor_test.clj +++ b/test/metabase/query_processor_test.clj @@ -294,7 +294,7 @@ {:style/indent 0} [results] (vec (or (get-in results [:data :rows]) - (println (u/pprint-to-str 'red results)) + (println (u/pprint-to-str 'red results)) ; DEBUG (throw (Exception. "Error!"))))) (defn rows+column-names diff --git a/test/metabase/query_processor_test/aggregation_test.clj b/test/metabase/query_processor_test/aggregation_test.clj index 759d5e26e57a62c485d473a0a5534ccaf22ad3fd..d6b8267866176ce7e2bc01ae7810ebcb0366b60f 100644 --- a/test/metabase/query_processor_test/aggregation_test.clj +++ b/test/metabase/query_processor_test/aggregation_test.clj @@ -149,8 +149,8 @@ (ql/aggregation (ql/avg $price) (ql/count) (ql/sum $price)))))) ;; make sure that multiple aggregations of the same type have the correct metadata (#4003) -;; (TODO - this isn't tested against Mongo or BigQuery because those drivers don't currently work correctly with multiple columns with the same name) -(datasets/expect-with-engines (disj non-timeseries-engines :mongo :bigquery) +;; (TODO - this isn't tested against Mongo, BigQuery or Presto because those drivers don't currently work correctly with multiple columns with the same name) +(datasets/expect-with-engines (disj non-timeseries-engines :mongo :bigquery :presto) [(aggregate-col :count) (assoc (aggregate-col :count) :display_name "count_2" diff --git a/test/metabase/query_processor_test/date_bucketing_test.clj b/test/metabase/query_processor_test/date_bucketing_test.clj index eeee25db76c9fc3660e461a2f403d29bcd9cb6a5..603e8d93057fb3fdafc4cd9473181f0a7b6fbef5 100644 --- a/test/metabase/query_processor_test/date_bucketing_test.clj +++ b/test/metabase/query_processor_test/date_bucketing_test.clj @@ -37,7 +37,7 @@ ["2015-06-02 08:20:00" 1] ["2015-06-02 11:11:00" 1]] - (contains? #{:redshift :sqlserver :bigquery :mongo :postgres :vertica :h2 :oracle} *engine*) + (contains? #{:redshift :sqlserver :bigquery :mongo :postgres :vertica :h2 :oracle :presto} *engine*) [["2015-06-01T10:31:00.000Z" 1] ["2015-06-01T16:06:00.000Z" 1] ["2015-06-01T17:23:00.000Z" 1] @@ -246,7 +246,7 @@ (contains? #{:sqlserver :sqlite :crate :oracle} *engine*) [[23 54] [24 46] [25 39] [26 61]] - (contains? #{:mongo :redshift :bigquery :postgres :vertica :h2} *engine*) + (contains? #{:mongo :redshift :bigquery :postgres :vertica :h2 :presto} *engine*) [[23 46] [24 47] [25 40] [26 60] [27 7]] :else diff --git a/test/metabase/sync_database_test.clj b/test/metabase/sync_database_test.clj index e3f963012af804337e025d6502f7d67f0ff1ecf7..d44d2967019187c16c3f7692855d908b6abc442e 100644 --- a/test/metabase/sync_database_test.clj +++ b/test/metabase/sync_database_test.clj @@ -326,21 +326,23 @@ ;;; -------------------- Make sure that if a Field's cardinality passes `metabase.sync-database.analyze/low-cardinality-threshold` (currently 300) (#3215) -------------------- +(defn- insert-range-sql [rang] + (str "INSERT INTO blueberries_consumed (num) VALUES " + (str/join ", " (for [n rang] + (str "(" n ")"))))) + (expect false (let [details {:db (str "mem:" (tu/random-name) ";DB_CLOSE_DELAY=10")}] (binding [mdb/*allow-potentailly-unsafe-connections* true] (tt/with-temp Database [db {:engine :h2, :details details}] - (let [driver (driver/engine->driver :h2) - spec (sql/connection-details->spec driver details) - exec! #(doseq [statement %] - (println (jdbc/execute! spec [statement]))) - insert-range #(str "INSERT INTO blueberries_consumed (num) VALUES " - (str/join ", " (for [n %] - (str "(" n ")"))))] + (let [driver (driver/engine->driver :h2) + spec (sql/connection-details->spec driver details) + exec! #(doseq [statement %] + (jdbc/execute! spec [statement]))] ;; create the `blueberries_consumed` table and insert a 100 values (exec! ["CREATE TABLE blueberries_consumed (num INTEGER NOT NULL);" - (insert-range (range 100))]) + (insert-range-sql (range 100))]) (sync-database! db, :full-sync? true) (let [table-id (db/select-one-id Table :db_id (u/get-id db)) field-id (db/select-one-id Field :table_id table-id)] @@ -348,6 +350,6 @@ (assert (= (count (db/select-one-field :values FieldValues :field_id field-id)) 100)) ;; ok, now insert enough rows to push the field past the `low-cardinality-threshold` and sync again, there should be no more field values - (exec! [(insert-range (range 100 (+ 100 @(resolve 'metabase.sync-database.analyze/low-cardinality-threshold))))]) + (exec! [(insert-range-sql (range 100 (+ 100 @(resolve 'metabase.sync-database.analyze/low-cardinality-threshold))))]) (sync-database! db, :full-sync? true) (db/exists? FieldValues :field_id field-id))))))) diff --git a/test/metabase/test/data/druid.clj b/test/metabase/test/data/druid.clj index 59d063b122a1466449b2f4e63ff5fba7feb91a44..0f24351c182126adabbbb5461eb799b8bf687d45 100644 --- a/test/metabase/test/data/druid.clj +++ b/test/metabase/test/data/druid.clj @@ -27,38 +27,10 @@ ;;; Setting Up a Server w/ Druid Test Data -;; Unfortunately the process of loading test data onto an external server for CI purposes is a little involved. Before testing against Druid, you'll need to perform the following steps: -;; For EC2 instances, make sure to expose ports `8082` & `8090` for Druid while loading data. Once data has finished loading, you only need to expose port `8082`. -;; -;; 1. Setup Zookeeper -;; 1A. Download & extract Zookeeper from `http://zookeeper.apache.org/releases.html#download` -;; 1B. Create `zookeeper/conf/zoo.cfg` -- see the Getting Started Guide: `http://zookeeper.apache.org/doc/r3.4.6/zookeeperStarted.html` -;; 1C. `zookeeper/bin/zkServer.sh start` -;; 1D. `zookeeper/bin/zkServer.sh status` (to make sure it started correctly) -;; 2. Setup Druid -;; 2A. Download & extract Druid from `http://druid.io/downloads.html` -;; 2B. `cp druid/run_druid_server.sh druid/run_historical.sh` and bump the `-Xmx` setting to `6g` or so -;; 2C. `cd druid && ./run_druid_server.sh coordinator` -;; 2D. `cd druid && ./run_druid_server.sh broker` -;; 2E. `cd druid && ./run_historical.sh historical` -;; 2E. `cd druid && ./run_druid_server.sh overlord` -;; 3. Generate flattened test data file. Optionally pick a <filename> -;; 3A. From this namespace in the REPL, run `(generate-json-for-batch-ingestion <filename>)` -;; 3B. `scp` or otherwise upload this file to the box running druid (if applicable) -;; 4. Launch Druid Indexing Task -;; 4A. Run the indexing task on the remote instance. -;; -;; (run-indexing-task <remote-host> :base-dir <dir-where-you-uploaded-file>, :filename <file>) -;; e.g. -;; (run-indexing-task "http://ec2-52-90-109-199.compute-1.amazonaws.com", :base-dir "/home/ec2-user", :filename "checkins.json") -;; -;; The task will keep you apprised of its progress until it completes (takes 1-2 minutes) -;; 4B. Keep an eye on `<host>:8082/druid/v2/datasources`. (e.g. "http://ec2-52-90-109-199.compute-1.amazonaws.com:8082/druid/v2/datasources") -;; This endpoint will return an empty array until the broker knows about the newly ingested segments. When it returns an array with the string `"checkins"` you're ready to -;; run the tests. -;; 4C. Kill the `overlord` process once the data has finished loading. -;; 5. Running Tests -;; 5A. You can run tests like `ENGINES=druid MB_DRUID_PORT=8082 MB_DRUID_HOST=http://ec2-52-90-109-199.compute-1.amazonaws.com lein test` +;; Unfortunately the process of loading test data onto an external server for CI purposes is a little involved. +;; A complete step-by-step guide is available on the wiki at `https://github.com/metabase/metabase/wiki/Setting-up-Druid-for-CI-on-EC2` +;; Refer to that page for more information. + (def ^:private ^:const default-filename "Default filename for batched ingestion data file." "checkins.json") @@ -126,7 +98,7 @@ (def ^:private ^:const indexer-timeout-seconds "Maximum number of seconds we should wait for the indexing task to finish before deciding it's failed." - 180) + 300) ; five minutes (resolve-private-vars metabase.driver.druid GET POST) @@ -139,7 +111,7 @@ (println "STATUS URL: " (str indexer-endpoint "/" task "/status")) (loop [remaining-seconds indexer-timeout-seconds] (let [status (get-in (GET status-url) [:status :status])] - (println (format "%s (%d seconds elapsed)" status (- indexer-timeout-seconds remaining-seconds))) + (printf "%s (%d seconds elapsed)\n" status (- indexer-timeout-seconds remaining-seconds)) (when (not= status "SUCCESS") (when (<= remaining-seconds 0) (throw (Exception. (str "Failed to finish indexing druid data after " indexer-timeout-seconds " seconds!")))) diff --git a/test/metabase/test/data/generic_sql.clj b/test/metabase/test/data/generic_sql.clj index 5a93ba82eb14fe52b3ceb367b61a30b36f82f0cb..49a3b4fe11ae5f064e3f9f7ccb691e58422c2611 100644 --- a/test/metabase/test/data/generic_sql.clj +++ b/test/metabase/test/data/generic_sql.clj @@ -13,7 +13,8 @@ [interface :as i]) [metabase.util :as u] [metabase.util.honeysql-extensions :as hx]) - (:import clojure.lang.Keyword + (:import java.sql.SQLException + clojure.lang.Keyword (metabase.test.data.interface DatabaseDefinition FieldDefinition TableDefinition))) @@ -218,7 +219,7 @@ :quoting (sql/quote-style driver) :allow-dashed-names? true)))] (try (jdbc/execute! spec sql+args) - (catch java.sql.SQLException e + (catch SQLException e (println (u/format-color 'red "INSERT FAILED: \n%s\n" sql+args)) (jdbc/print-sql-exception-chain e))))) @@ -249,15 +250,15 @@ (let [sql (s/replace sql #";+" ";")] (try (jdbc/execute! (database->spec driver context dbdef) [sql] {:transaction? false, :multi? true}) - (catch java.sql.SQLException e + (catch SQLException e (println "Error executing SQL:" sql) - (println (format "Caught SQLException:\n%s" - (with-out-str (jdbc/print-sql-exception-chain e)))) + (printf "Caught SQLException:\n%s\n" + (with-out-str (jdbc/print-sql-exception-chain e))) (throw e)) (catch Throwable e (println "Error executing SQL:" sql) - (println (format "Caught Exception: %s %s\n%s" (class e) (.getMessage e) - (with-out-str (.printStackTrace e)))) + (printf "Caught Exception: %s %s\n%s\n" (class e) (.getMessage e) + (with-out-str (.printStackTrace e))) (throw e))))))) diff --git a/test/metabase/test/data/presto.clj b/test/metabase/test/data/presto.clj new file mode 100644 index 0000000000000000000000000000000000000000..790907b1abf8aef6f032d5bdc710f1c8ee235c7c --- /dev/null +++ b/test/metabase/test/data/presto.clj @@ -0,0 +1,107 @@ +(ns metabase.test.data.presto + (:require [clojure.string :as s] + [environ.core :refer [env]] + (honeysql [core :as hsql] + [helpers :as h]) + [metabase.driver.generic-sql.util.unprepare :as unprepare] + [metabase.test.data.interface :as i] + [metabase.test.util :refer [resolve-private-vars]] + [metabase.util :as u] + [metabase.util.honeysql-extensions :as hx]) + (:import java.util.Date + metabase.driver.presto.PrestoDriver + (metabase.query_processor.interface DateTimeValue Value))) + +(resolve-private-vars metabase.driver.presto execute-presto-query! presto-type->base-type quote-name quote+combine-names) + +;;; Helpers + +(defn- get-env-var [env-var] + (or (env (keyword (format "mb-presto-%s" (name env-var)))) + (throw (Exception. (format "In order to test Presto, you must specify the env var MB_PRESTO_%s." + (s/upper-case (s/replace (name env-var) #"-" "_"))))))) + + +;;; IDatasetLoader implementation + +(defn- database->connection-details [context {:keys [database-name]}] + (merge {:host (get-env-var :host) + :port (get-env-var :port) + :user "metabase" + :ssl false} + (when (= context :db) + {:catalog database-name}))) + +(defn- qualify-name + ;; we have to use the default schema from the in-memory connectory + ([db-name] [db-name]) + ([db-name table-name] [db-name "default" table-name]) + ([db-name table-name field-name] [db-name "default" table-name field-name])) + +(defn- qualify+quote-name [& names] + (apply quote+combine-names (apply qualify-name names))) + +(defn- field-base-type->dummy-value [field-type] + ;; we need a dummy value for every base-type to make a properly typed SELECT statement + (if (keyword? field-type) + (case field-type + :type/Boolean "TRUE" + :type/Integer "1" + :type/BigInteger "cast(1 AS bigint)" + :type/Float "1.0" + :type/Decimal "DECIMAL '1.0'" + :type/Text "cast('' AS varchar(255))" + :type/Date "current_timestamp" ; this should probably be a date type, but the test data begs to differ + :type/DateTime "current_timestamp" + "from_hex('00')") ; this might not be the best default ever + ;; we were given a native type, map it back to a base-type and try again + (field-base-type->dummy-value (presto-type->base-type field-type)))) + +(defn- create-table-sql [{:keys [database-name]} {:keys [table-name], :as tabledef}] + (let [field-definitions (conj (:field-definitions tabledef) {:field-name "id", :base-type :type/Integer}) + dummy-values (map (comp field-base-type->dummy-value :base-type) field-definitions) + columns (map :field-name field-definitions)] + ;; Presto won't let us use the `CREATE TABLE (...)` form, but we can still do it creatively if we select the right types out of thin air + (format "CREATE TABLE %s AS SELECT * FROM (VALUES (%s)) AS t (%s) WHERE 1 = 0" + (qualify+quote-name database-name table-name) + (s/join \, dummy-values) + (s/join \, (map quote-name columns))))) + +(defn- drop-table-if-exists-sql [{:keys [database-name]} {:keys [table-name]}] + (str "DROP TABLE IF EXISTS " (qualify+quote-name database-name table-name))) + +(defn- insert-sql [{:keys [database-name]} {:keys [table-name], :as tabledef} rows] + (let [field-definitions (conj (:field-definitions tabledef) {:field-name "id"}) + columns (map (comp keyword :field-name) field-definitions) + [query & params] (-> (apply h/columns columns) + (h/insert-into (apply hsql/qualify (qualify-name database-name table-name))) + (h/values rows) + (hsql/format :allow-dashed-names? true, :quoting :ansi))] + (if (nil? params) + query + (unprepare/unprepare (cons query params) :quote-escape "'", :iso-8601-fn :from_iso8601_timestamp)))) + +(defn- create-db! [{:keys [table-definitions] :as dbdef}] + (let [details (database->connection-details :db dbdef)] + (doseq [tabledef table-definitions + :let [rows (:rows tabledef) + keyed-rows (map-indexed (fn [i row] (conj row (inc i))) rows) ; generate an ID for each row because we don't have auto increments + batches (partition 100 100 nil keyed-rows)]] ; make 100 rows batches since we have to inline everything + (execute-presto-query! details (drop-table-if-exists-sql dbdef tabledef)) + (execute-presto-query! details (create-table-sql dbdef tabledef)) + (doseq [batch batches] + (execute-presto-query! details (insert-sql dbdef tabledef batch)))))) + + +;;; IDatasetLoader implementation + +(u/strict-extend PrestoDriver + i/IDatasetLoader + (merge i/IDatasetLoaderDefaultsMixin + {:engine (constantly :presto) + :database->connection-details (u/drop-first-arg database->connection-details) + :create-db! (u/drop-first-arg create-db!) + :default-schema (constantly "default") + :format-name (u/drop-first-arg s/lower-case) + ;; FIXME Presto actually has very good timezone support + :has-questionable-timezone-support? (constantly true)})) diff --git a/test/metabase/test/data/sqlserver.clj b/test/metabase/test/data/sqlserver.clj index 858ebdac8104c515b51a290010c551413c59999b..d4bb90ec570292308bc1cf8edddfa39f86105052 100644 --- a/test/metabase/test/data/sqlserver.clj +++ b/test/metabase/test/data/sqlserver.clj @@ -102,7 +102,7 @@ (with-redefs [+suffix identity] (doseq [db leftover-dbs] (u/ignore-exceptions - (println (format "Deleting leftover SQL Server DB '%s'..." db)) + (printf "Deleting leftover SQL Server DB '%s'...\n" db) ;; Don't try to kill other connections to this DB with SET SINGLE_USER -- some other instance (eg CI) might be using it (jdbc/execute! connection-spec [(format "DROP DATABASE \"%s\";" db)]) (println "[ok]"))))))) diff --git a/test/metabase/test/data/users.clj b/test/metabase/test/data/users.clj index d08a28c4017d6e02b3bd783927be181eb1144f30..5698ebae330aae5620c9422b05d3653aaa14eb40 100644 --- a/test/metabase/test/data/users.clj +++ b/test/metabase/test/data/users.clj @@ -9,7 +9,8 @@ (metabase.models [permissions-group :as perms-group] [user :refer [User]]) [metabase.util :as u] - [metabase.test.util :refer [random-name]])) + [metabase.test.util :refer [random-name]]) + (:import clojure.lang.ExceptionInfo)) ;;; ------------------------------------------------------------ User Definitions ------------------------------------------------------------ @@ -59,7 +60,7 @@ (when-not (init-status/complete?) (when (<= max-wait-seconds 0) (throw (Exception. "Metabase still hasn't finished initializing."))) - (println (format "Metabase is not yet initialized, waiting 1 second (max wait remaining: %d seconds)..." max-wait-seconds)) + (printf "Metabase is not yet initialized, waiting 1 second (max wait remaining: %d seconds)...\n" max-wait-seconds) (Thread/sleep 1000) (recur (dec max-wait-seconds)))))) @@ -71,6 +72,7 @@ {:pre [(string? email) (string? first) (string? last) (string? password) (m/boolean? superuser) (m/boolean? active)]} (wait-for-initiailization) (or (User :email email) + (println "Creating test user:" email) ; DEBUG (db/insert! User :email email :first_name first @@ -123,7 +125,7 @@ (fn [id] (@m id)))) -(def ^:private tokens (atom {})) +(defonce ^:private tokens (atom {})) (defn- username->token [username] (or (@tokens username) @@ -134,18 +136,15 @@ (defn- client-fn [username & args] (try (apply http/client (username->token username) args) - (catch Throwable e + (catch ExceptionInfo e (let [{:keys [status-code]} (ex-data e)] (when-not (= status-code 401) (throw e)) ;; If we got a 401 unauthenticated clear the tokens cache + recur + (printf "Got 401 (Unauthenticated) for %s. Clearing cached auth tokens and retrying request.\n" username) ; DEBUG (reset! tokens {}) (apply client-fn username args))))) -;; TODO - does it make sense just to make this a non-higher-order function? Or a group of functions, e.g. -;; (GET :rasta 200 "field/10/values") -;; vs. -;; ((user->client :rasta) :get 200 "field/10/values") (defn user->client "Returns a `metabase.http-client/client` partially bound with the credentials for User with USERNAME. In addition, it forces lazy creation of the User if needed. @@ -160,4 +159,4 @@ "Delete all users besides the 4 persistent test users. This is a HACK to work around tests that don't properly clean up after themselves; one day we should be able to remove this. (TODO)" [] - (db/delete! 'User :id [:not-in (map user->id [:crowberto :lucky :rasta :trashbird])])) + (db/delete! User :id [:not-in (map user->id [:crowberto :lucky :rasta :trashbird])])) diff --git a/test/metabase/timeseries_query_processor_test.clj b/test/metabase/timeseries_query_processor_test.clj index 5d05675e8e36c968b57db2d0015acc32577c06ec..50765b41ab1df0e3fea23515c7eac14062e67a29 100644 --- a/test/metabase/timeseries_query_processor_test.clj +++ b/test/metabase/timeseries_query_processor_test.clj @@ -45,7 +45,7 @@ (defn- data [results] (when-let [data (or (:data results) - (println (u/pprint-to-str results)))] + (println (u/pprint-to-str results)))] ; DEBUG (-> data (select-keys [:columns :rows]) (update :rows vec)))) @@ -69,6 +69,44 @@ (data (data/run-query checkins (ql/limit 2)))) +;;; "bare rows" query, limit, order-by timestamp desc +(expect-with-timeseries-dbs + {:columns ["id" + "timestamp" + "count" + "user_last_login" + "user_name" + "venue_category_name" + "venue_latitude" + "venue_longitude" + "venue_name" + "venue_price"] + :rows [["693", "2015-12-29T08:00:00.000Z", 1, "2014-07-03T19:30:00.000Z", "Frans Hevel", "Mexican", "34.0489", "-118.238", "Señor Fish", "2"] + ["570", "2015-12-26T08:00:00.000Z", 1, "2014-07-03T01:30:00.000Z", "Kfir Caj", "Chinese", "37.7949", "-122.406", "Empress of China", "3"]]} + (data (data/run-query checkins + (ql/order-by (ql/desc $timestamp)) + (ql/limit 2)))) + +;;; "bare rows" query, limit, order-by timestamp asc +(expect-with-timeseries-dbs + {:columns ["id" + "timestamp" + "count" + "user_last_login" + "user_name" + "venue_category_name" + "venue_latitude" + "venue_longitude" + "venue_name" + "venue_price"] + :rows [["931", "2013-01-03T08:00:00.000Z", 1, "2014-01-01T08:30:00.000Z", "Simcha Yan", "Thai", "34.094", "-118.344", "Kinaree Thai Bistro", "1"] + ["285", "2013-01-10T08:00:00.000Z", 1, "2014-07-03T01:30:00.000Z", "Kfir Caj", "Thai", "34.1021", "-118.306", "Ruen Pair Thai Restaurant", "2"]]} + (data (data/run-query checkins + (ql/order-by (ql/asc $timestamp)) + (ql/limit 2)))) + + + ;;; fields clause (expect-with-timeseries-dbs {:columns ["venue_name" "venue_category_name" "timestamp"], @@ -78,6 +116,28 @@ (ql/fields $venue_name $venue_category_name) (ql/limit 2)))) +;;; fields clause, order by timestamp asc +(expect-with-timeseries-dbs + {:columns ["venue_name" "venue_category_name" "timestamp"], + :rows [["Kinaree Thai Bistro" "Thai" "2013-01-03T08:00:00.000Z"] + ["Ruen Pair Thai Restaurant" "Thai" "2013-01-10T08:00:00.000Z"]]} + (data (data/run-query checkins + (ql/fields $venue_name $venue_category_name) + (ql/order-by (ql/asc $timestamp)) + (ql/limit 2)))) + +;;; fields clause, order by timestamp desc +(expect-with-timeseries-dbs + {:columns ["venue_name" "venue_category_name" "timestamp"], + :rows [["Señor Fish" "Mexican" "2015-12-29T08:00:00.000Z"] + ["Empress of China" "Chinese" "2015-12-26T08:00:00.000Z"]]} + (data (data/run-query checkins + (ql/fields $venue_name $venue_category_name) + (ql/order-by (ql/desc $timestamp)) + (ql/limit 2)))) + + + ;;; count (expect-with-timeseries-dbs [1000] diff --git a/webpack.config.js b/webpack.config.js index 6183bf73437d7e3d01f62dc603001ac35541fb55..ee560eb282106139e925d4155bb37dc81732a63a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -9,6 +9,7 @@ var webpackPostcssTools = require('webpack-postcss-tools'); var ExtractTextPlugin = require('extract-text-webpack-plugin'); var HtmlWebpackPlugin = require('html-webpack-plugin'); +var HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin'); var UnusedFilesWebpackPlugin = require("unused-files-webpack-plugin").default; var BannerWebpackPlugin = require('banner-webpack-plugin'); @@ -27,18 +28,16 @@ function hasArg(arg) { var SRC_PATH = __dirname + '/frontend/src/metabase'; var BUILD_PATH = __dirname + '/resources/frontend_client'; +// default NODE_ENV to development +var NODE_ENV = process.env["NODE_ENV"] || "development"; // Need to scan the CSS files for variable and custom media used across files // NOTE: this requires "webpack -w" (watch mode) to be restarted when variables change :( -var isWatching = hasArg("-w") || hasArg("--watch"); -if (isWatching) { - console.warn("Warning: in webpack watch mode you must restart webpack if you change any CSS variables or custom media queries"); +var IS_WATCHING = hasArg("-w") || hasArg("--watch"); +if (IS_WATCHING) { + process.stderr.write("Warning: in webpack watch mode you must restart webpack if you change any CSS variables or custom media queries\n"); } -// default NODE_ENV to development -var NODE_ENV = process.env["NODE_ENV"] || "development"; -process.stderr.write("webpack env: " + NODE_ENV + "\n"); - // Babel: var BABEL_CONFIG = { cacheDirectory: ".babel_cache" @@ -159,19 +158,25 @@ var config = module.exports = { filename: '../../index.html', chunks: ["app-main", "styles"], template: __dirname + '/resources/frontend_client/index_template.html', - inject: 'head' + inject: 'head', + alwaysWriteToDisk: true, }), new HtmlWebpackPlugin({ filename: '../../public.html', chunks: ["app-public", "styles"], template: __dirname + '/resources/frontend_client/index_template.html', - inject: 'head' + inject: 'head', + alwaysWriteToDisk: true, }), new HtmlWebpackPlugin({ filename: '../../embed.html', chunks: ["app-embed", "styles"], template: __dirname + '/resources/frontend_client/index_template.html', - inject: 'head' + inject: 'head', + alwaysWriteToDisk: true, + }), + new HtmlWebpackHarddiskPlugin({ + outputPath: __dirname + '/resources/frontend_client/app/dist' }), new webpack.DefinePlugin({ 'process.env': { diff --git a/yarn.lock b/yarn.lock index 135d29ee41aea23d194c31b2991d5313bdf9d5df..3e9c0ec3a1ace600051a9e8209b8e5bda3aec287 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3213,7 +3213,7 @@ fb-watchman@^1.8.0, fb-watchman@^1.9.0: dependencies: bser "1.0.2" -fbjs@^0.8.1, fbjs@^0.8.4, fbjs@^0.8.8: +fbjs@^0.8.1, fbjs@^0.8.4, fbjs@^0.8.8, fbjs@^0.8.9: version "0.8.9" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.9.tgz#180247fbd347dcc9004517b904f865400a0c8f14" dependencies: @@ -3723,6 +3723,12 @@ html-minifier@^3.2.3: relateurl "0.2.x" uglify-js "2.7.x" +html-webpack-harddisk-plugin@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/html-webpack-harddisk-plugin/-/html-webpack-harddisk-plugin-0.1.0.tgz#432024961a21ac668fa2b5dfe24629c60b9c58d7" + dependencies: + mkdirp "^0.5.1" + html-webpack-plugin@^2.14.0: version "2.28.0" resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-2.28.0.tgz#2e7863b57e5fd48fe263303e2ffc934c3064d009" @@ -6530,6 +6536,18 @@ promise@^7.1.1: dependencies: asap "~2.0.3" +prop-types@15.5.0-alpha.0: + version "15.5.0-alpha.0" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.0-alpha.0.tgz#a342108678256db125eee3d1ae2f889af3531bd7" + dependencies: + fbjs "^0.8.9" + +prop-types@^15.5.4, prop-types@~15.5.0: + version "15.5.4" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.4.tgz#2ed3692716a5060f8cc020946d8238e7419d92c0" + dependencies: + fbjs "^0.8.9" + proxy-addr@~1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.3.tgz#dc97502f5722e888467b3fa2297a7b1ff47df074" @@ -6697,13 +6715,14 @@ react-docgen@^2.12.1: node-dir "^0.1.10" recast "^0.11.5" -react-dom@^15.2.1: - version "15.4.2" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.4.2.tgz#015363f05b0a1fd52ae9efdd3a0060d90695208f" +react-dom@15.5.0: + version "15.5.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.5.0.tgz#86a8d6dcde388473815039de3840706e1f28f697" dependencies: - fbjs "^0.8.1" + fbjs "^0.8.9" loose-envify "^1.1.0" object-assign "^4.1.0" + prop-types "~15.5.0" react-draggable@^2.1.0, react-draggable@^2.2.3: version "2.2.3" @@ -6858,13 +6877,14 @@ react-virtualized@^8.6.0: dom-helpers "^2.4.0 || ^3.0.0" loose-envify "^1.3.0" -react@^15.2.1: - version "15.4.2" - resolved "https://registry.yarnpkg.com/react/-/react-15.4.2.tgz#41f7991b26185392ba9bae96c8889e7e018397ef" +react@15.5.0: + version "15.5.0" + resolved "https://registry.yarnpkg.com/react/-/react-15.5.0.tgz#1f8e4b492dcfbf77479eb4fdfc48da425002e505" dependencies: - fbjs "^0.8.4" + fbjs "^0.8.9" loose-envify "^1.1.0" object-assign "^4.1.0" + prop-types "15.5.0-alpha.0" read-cache@^1.0.0: version "1.0.0"