diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ef2e2989c07fffba2498ae2efe32368d508b6987..c6c1ab8111e2664387c94dba0c091c31e0d343fd 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,5 +1,7 @@ -###### TODO +###### Before submitting the PR, please make sure you do the following +- [ ] If there are changes to the backend codebase, run the backend tests with `lein test && lein eastwood && lein bikeshed && lein docstring-checker && ./bin/reflection-linter` +- [ ] Run the frontend and integration tests with `yarn && yarn run prettier && yarn run lint && yarn run flow && ./bin/build version uberjar && yarn run test`) - [ ] Sign the [Contributor License Agreement](https://docs.google.com/a/metabase.com/forms/d/1oV38o7b9ONFSwuzwmERRMi9SYrhYeOrkbmNaq9pOJ_E/viewform) (unless it's a tiny documentation change). diff --git a/docs/developers-guide.md b/docs/developers-guide.md index 49e6896d9e55fb363581a6023eb1258324dcab92..115ee34fa09b2845364382d83462335db678333d 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -124,6 +124,7 @@ Integration tests use an enforced file naming convention `<test-suite-name>.inte Useful commands: ```bash ./bin/build version uberjar # Builds the JAR without frontend assets; run this every time you need to update the backend +lein run refresh-integration-test-db-metadata # Scan the sample dataset and re-run sync/classification/field values caching yarn run test-integrated-watch # Watches for file changes and runs the tests that have changed yarn run test-integrated-watch -- TestFileName # Watches the files in paths that match the given (regex) string ``` diff --git a/frontend/src/metabase/admin/databases/components/CreatedDatabaseModal.jsx b/frontend/src/metabase/admin/databases/components/CreatedDatabaseModal.jsx index f6eecbacf615870c7721b6f5427aa24a4d0c4700..296d2ed7282ab34fd7932a6072d7a0158743bbcb 100644 --- a/frontend/src/metabase/admin/databases/components/CreatedDatabaseModal.jsx +++ b/frontend/src/metabase/admin/databases/components/CreatedDatabaseModal.jsx @@ -22,12 +22,12 @@ export default class CreatedDatabaseModal extends Component { {jt`We're analyzing its schema now to make some educated guesses about its metadata. ${( <Link to={`/admin/datamodel/database/${databaseId}`}> - View this database + {t`View this database`} </Link> )} in the Data Model section to see what we've found and to make edits, or ${( <Link to={Urls.question(null, `?db=${databaseId}`)}> - ask a question + {t`ask a question`} </Link> )} about this database.`} diff --git a/frontend/src/metabase/admin/people/components/GroupsListing.jsx b/frontend/src/metabase/admin/people/components/GroupsListing.jsx index 785bda2aba2d9a248237c608844a307794e21320..657b199b7ba308d32f3fd65615f84ba923e56c84 100644 --- a/frontend/src/metabase/admin/people/components/GroupsListing.jsx +++ b/frontend/src/metabase/admin/people/components/GroupsListing.jsx @@ -54,7 +54,7 @@ function DeleteGroupModal({ group, onConfirm = () => {}, onClose = () => {} }) { return ( <ModalContent title={t`Remove this group?`} onClose={onClose}> <p className="px4 pb4"> - {t`Are you sure? All members of this group will lose any permissions settings the have based on this group. + {t`Are you sure? All members of this group will lose any permissions settings they have based on this group. This can't be undone.`} </p> <div className="Form-actions"> diff --git a/frontend/src/metabase/admin/people/containers/AdminPeopleApp.jsx b/frontend/src/metabase/admin/people/containers/AdminPeopleApp.jsx index 31cd184a93cc6ea73d466eecb0fb74bf3fc10270..57507ab1f4ee8bc5a655ccefd8143e7c3de9bbef 100644 --- a/frontend/src/metabase/admin/people/containers/AdminPeopleApp.jsx +++ b/frontend/src/metabase/admin/people/containers/AdminPeopleApp.jsx @@ -1,6 +1,7 @@ /* eslint "react/prop-types": "warn" */ import React, { Component } from "react"; import PropTypes from "prop-types"; +import { t } from "c-3po"; import { LeftNavPane, @@ -20,8 +21,8 @@ export default class AdminPeopleApp extends Component { <AdminLayout sidebar={ <LeftNavPane> - <LeftNavPaneItem name="People" path="/admin/people" index /> - <LeftNavPaneItem name="Groups" path="/admin/people/groups" /> + <LeftNavPaneItem name={t`People`} path="/admin/people" index /> + <LeftNavPaneItem name={t`Groups`} path="/admin/people/groups" /> </LeftNavPane> } > diff --git a/frontend/src/metabase/admin/permissions/components/PermissionsConfirm.jsx b/frontend/src/metabase/admin/permissions/components/PermissionsConfirm.jsx index f30332c2bd3ab266f6dfd0633c9ac7b99df93f54..8243e0f1c1b83c3e01fc5673778b40817e5442d1 100644 --- a/frontend/src/metabase/admin/permissions/components/PermissionsConfirm.jsx +++ b/frontend/src/metabase/admin/permissions/components/PermissionsConfirm.jsx @@ -68,7 +68,7 @@ const PermissionsConfirm = ({ diff }) => ( <div> <GroupName group={group} /> {database.native === "none" - ? t` will no longer able to ` + ? t` will no longer be able to ` : t` will now be able to `} {database.native === "read" ? ( <span className="text-gold">read</span> diff --git a/frontend/src/metabase/admin/permissions/selectors.js b/frontend/src/metabase/admin/permissions/selectors.js index b4cf5a762c278ace0b1215ce5201dd92e5d0950d..347ae36892f91b0b284cdce1029ebb0464b198cf 100644 --- a/frontend/src/metabase/admin/permissions/selectors.js +++ b/frontend/src/metabase/admin/permissions/selectors.js @@ -142,9 +142,10 @@ function getPermissionWarningModal( ); if (permissionWarning) { return { - title: t`${ - value === "controlled" ? "Limit" : "Revoke" - } access even though "${defaultGroup.name}" has greater access?`, + title: + (value === "controlled" ? t`Limit` : t`Revoke`) + + " " + + t`access even though "${defaultGroup.name}" has greater access?`, message: permissionWarning, confirmButtonText: value === "controlled" ? t`Limit access` : t`Revoke access`, diff --git a/frontend/src/metabase/admin/settings/components/SettingsXrayForm.jsx b/frontend/src/metabase/admin/settings/components/SettingsXrayForm.jsx index f426b8de0052ec9160ea87254861a8251230593b..d1cc135aa9c04a0220ece047f73f99eda5df13de 100644 --- a/frontend/src/metabase/admin/settings/components/SettingsXrayForm.jsx +++ b/frontend/src/metabase/admin/settings/components/SettingsXrayForm.jsx @@ -43,7 +43,7 @@ const SettingsXrayForm = ({ settings, elements, updateSetting }) => { </p> <p className="text-paragraph"> <em>{jt`${( - <strong>Note:</strong> + <strong>{t`Note`}:</strong> )} "Extended" is required for viewing time series x-rays.`}</em> </p> diff --git a/frontend/src/metabase/admin/settings/components/widgets/EmbeddingLegalese.jsx b/frontend/src/metabase/admin/settings/components/widgets/EmbeddingLegalese.jsx index 4d04483001d724d56060ab50366be5e0d5640493..6bd1b1d2a2a9eac9c8d8cb5fd74ebbc53201520b 100644 --- a/frontend/src/metabase/admin/settings/components/widgets/EmbeddingLegalese.jsx +++ b/frontend/src/metabase/admin/settings/components/widgets/EmbeddingLegalese.jsx @@ -16,7 +16,7 @@ const EmbeddingLegalese = ({ onChange }) => ( </a>. </p> <p className="text-grey-4" style={{ lineHeight: 1.48 }}> - {t`In plain english, when you embed charts or dashboards from Metabase in your own application, that application isn't subject to the Affero General Public License that covers the rest of Metabase, provided you keep the Metabase logo and the "Powered by Metabase" visible on those embeds. You should however, read the license text linked above as that is the actual license that you will be agreeing to by enabling this feature.`} + {t`In plain English, when you embed charts or dashboards from Metabase in your own application, that application isn't subject to the Affero General Public License that covers the rest of Metabase, provided you keep the Metabase logo and the "Powered by Metabase" visible on those embeds. You should, however, read the license text linked above as that is the actual license that you will be agreeing to by enabling this feature.`} </p> <div className="flex layout-centered mt4"> <button diff --git a/frontend/src/metabase/admin/settings/components/widgets/EmbeddingLevel.jsx b/frontend/src/metabase/admin/settings/components/widgets/EmbeddingLevel.jsx index 5dc3be243ed3d221eaac5b827317c6e712973ebe..b7fe24ea5f5cbe4a16b72f26a34dab34bc3f26ff 100644 --- a/frontend/src/metabase/admin/settings/components/widgets/EmbeddingLevel.jsx +++ b/frontend/src/metabase/admin/settings/components/widgets/EmbeddingLevel.jsx @@ -50,7 +50,7 @@ class PremiumTokenInput extends Component { const PremiumExplanation = ({ showEnterScreen }) => ( <div> <h2>Premium embedding</h2> - <p className="mt1">{t`Premium embedding lets you disable "Powered by Metabase" on your embeded dashboards and questions.`}</p> + <p className="mt1">{t`Premium embedding lets you disable "Powered by Metabase" on your embedded dashboards and questions.`}</p> <div className="mt2 mb3"> <a className="link mx1" diff --git a/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx b/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx index 6d92dcd7c341edcd26c7e9d35dfd4435e96adcf0..87eecc50b87a56ec360f3c50c1b18d26241adf83 100644 --- a/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx +++ b/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx @@ -119,7 +119,7 @@ export default class PublicLinksListing extends Component { <td className="flex layout-centered"> <Confirm title={t`Disable this link?`} - content={t`They won't work any more, and can't be restored, but you can create new links.`} + content={t`They won't work anymore, and can't be restored, but you can create new links.`} action={() => { this.revoke(link); this.trackEvent("Revoked link"); diff --git a/frontend/src/metabase/admin/settings/selectors.js b/frontend/src/metabase/admin/settings/selectors.js index b9a6f0be5e94578342be8281b5f0e1cd5ec77b3b..23932ad0ed9961f4c075aee66775fe7f3c896140 100644 --- a/frontend/src/metabase/admin/settings/selectors.js +++ b/frontend/src/metabase/admin/settings/selectors.js @@ -268,7 +268,7 @@ const SECTIONS = [ }, { key: "ldap-group-base", - display_name: t`"Group search base`, + display_name: t`Group search base`, type: "string", }, { diff --git a/frontend/src/metabase/auth/containers/LoginApp.jsx b/frontend/src/metabase/auth/containers/LoginApp.jsx index 060acb30bbc316bd69145731caa5e7c8e10b2cc1..3133d35bc293fba4b5159faa8032949fb1795c4f 100644 --- a/frontend/src/metabase/auth/containers/LoginApp.jsx +++ b/frontend/src/metabase/auth/containers/LoginApp.jsx @@ -211,7 +211,7 @@ export default class LoginApp extends Component { })} disabled={!this.state.valid} > - Sign in + {t`Sign in`} </button> <Link to={ diff --git a/frontend/src/metabase/components/DatabaseDetailsForm.jsx b/frontend/src/metabase/components/DatabaseDetailsForm.jsx index f90ae68d62311a578519c34e08c430f766cd909a..13c1cfe40e2dde59c369c8553eec45bfd9e26e6e 100644 --- a/frontend/src/metabase/components/DatabaseDetailsForm.jsx +++ b/frontend/src/metabase/components/DatabaseDetailsForm.jsx @@ -242,7 +242,7 @@ export default class DatabaseDetailsForm extends Component { <h3 >{t`This is a large database, so let me choose when Metabase syncs and scans`}</h3> <div style={{ maxWidth: "40rem" }} className="pt1"> - {t`By default, Metabase does a lightweight hourly sync, and an intensive daily scan of field values. + {t`By default, Metabase does a lightweight hourly sync and an intensive daily scan of field values. If you have a large database, we recommend turning this on and reviewing when and how often the field value scans happen.`} </div> </div> @@ -260,7 +260,7 @@ export default class DatabaseDetailsForm extends Component { <div className="Grid-cell--top"> {jt`${( <a href={credentialsURL} target="_blank"> - Click here + {t`Click here`} </a> )} to generate a Client ID and Client Secret for your project.`} {t`Choose "Other" as the application type. Name it whatever you'd like.`} @@ -287,7 +287,7 @@ export default class DatabaseDetailsForm extends Component { <div className="Grid-cell--top"> {jt`${( <a href={authURL} target="_blank"> - Click here + {t`Click here`} </a> )} to get an auth code`} {engine === "bigquery" && ( @@ -322,7 +322,7 @@ export default class DatabaseDetailsForm extends Component { <div className="Grid-cell--top ml1"> {jt`${( <a href={enableAPIURL} target="_blank"> - Click here + {t`Click here`} </a> )} to go to the console if you haven't already done so.`} </div> diff --git a/frontend/src/metabase/dashboards/containers/Dashboards.jsx b/frontend/src/metabase/dashboards/containers/Dashboards.jsx index 9e398cd85bfa8062e81a891e954eaa72d61a45e5..d6671c9640d68f42246cae5deb94c42484d15697 100644 --- a/frontend/src/metabase/dashboards/containers/Dashboards.jsx +++ b/frontend/src/metabase/dashboards/containers/Dashboards.jsx @@ -219,8 +219,7 @@ export class Dashboards extends Component { message={ <div className="mt4"> <h3 className="text-grey-5">{t`No results found`}</h3> - <p className="text-grey-4">{t`Try adjusting your filter to find what you’re - looking for.`}</p> + <p className="text-grey-4">{t`Try adjusting your filter to find what you’re looking for.`}</p> </div> } image="/app/img/empty_dashboard" diff --git a/frontend/src/metabase/home/components/Activity.jsx b/frontend/src/metabase/home/components/Activity.jsx index 355c2f8704c8bece7d32b74c093b60abbe608d94..cd97d3c3fc5a266758ba95392f017929635e1b20 100644 --- a/frontend/src/metabase/home/components/Activity.jsx +++ b/frontend/src/metabase/home/components/Activity.jsx @@ -89,7 +89,7 @@ export default class Activity extends Component { // this is a base to start with const description = { userName: this.userName(item.user, user), - summary: t`did some super awesome stuff thats hard to describe`, + summary: t`did some super awesome stuff that's hard to describe`, timeSince: item.timestamp.fromNow(), }; @@ -138,7 +138,7 @@ export default class Activity extends Component { } else { description.summary = ( <span> - {t`deleted an alert about- `} + {t`deleted an alert about - `} <span className="text-dark">{item.details.name}</span> </span> ); @@ -345,7 +345,7 @@ export default class Activity extends Component { if (item.model_exists) { description.summary = ( <span> - {t`added the filter `} + {t`added the filter`}{" "} <Link to={Urls.tableRowsQuery( item.database_id, @@ -386,7 +386,7 @@ export default class Activity extends Component { if (item.model_exists) { description.summary = ( <span> - {t`made changes to the filter `} + {t`made changes to the filter`}{" "} <Link to={Urls.tableRowsQuery( item.database_id, diff --git a/frontend/src/metabase/nav/containers/Navbar.jsx b/frontend/src/metabase/nav/containers/Navbar.jsx index 8c24fe6f8b3070b711c71afc168d57419e896420..142b0eaad733c7bc325ebcbb7e269bae99f90a4f 100644 --- a/frontend/src/metabase/nav/containers/Navbar.jsx +++ b/frontend/src/metabase/nav/containers/Navbar.jsx @@ -93,27 +93,27 @@ export default class Navbar extends Component { <ul className="sm-ml4 flex flex-full"> <AdminNavItem - name="Settings" + name={t`Settings`} path="/admin/settings" currentPath={this.props.path} /> <AdminNavItem - name="People" + name={t`People`} path="/admin/people" currentPath={this.props.path} /> <AdminNavItem - name="Data Model" + name={t`Data Model`} path="/admin/datamodel" currentPath={this.props.path} /> <AdminNavItem - name="Databases" + name={t`Databases`} path="/admin/databases" currentPath={this.props.path} /> <AdminNavItem - name="Permissions" + name={t`Permissions`} path="/admin/permissions" currentPath={this.props.path} /> diff --git a/frontend/src/metabase/public/components/widgets/EmbedCodePane.jsx b/frontend/src/metabase/public/components/widgets/EmbedCodePane.jsx index d7b471cb010a8abeb5d4d82c4e488d09b7fde4b3..7d575e5907281513ad5c39a35f1015c77ef5c09f 100644 --- a/frontend/src/metabase/public/components/widgets/EmbedCodePane.jsx +++ b/frontend/src/metabase/public/components/widgets/EmbedCodePane.jsx @@ -100,7 +100,7 @@ export default class EmbedCodePane extends Component { <div className="text-centered my2"> <h4>{jt`More ${( <ExternalLink href="https://github.com/metabase/embedding_reference_apps"> - examples on GitHub + {t`examples on GitHub`} </ExternalLink> )}`}</h4> </div> diff --git a/frontend/src/metabase/pulse/components/PulseEdit.jsx b/frontend/src/metabase/pulse/components/PulseEdit.jsx index b7c26174a6a28b935652bb9db8acb097c40b27bd..4740b0d3e8bd1ce65162f6fea2419ea619c68c5a 100644 --- a/frontend/src/metabase/pulse/components/PulseEdit.jsx +++ b/frontend/src/metabase/pulse/components/PulseEdit.jsx @@ -88,7 +88,7 @@ export default class PulseEdit extends Component { <span key={index}> {jt`This pulse will no longer be emailed to ${( <strong> - {c.recipients.length} {inflect("address", c.recipients.length)} + {c.recipients.length} {inflect(t`address`, c.recipients.length)} </strong> )} ${<strong>{c.schedule_type}</strong>}`}. </span> diff --git a/frontend/src/metabase/pulse/components/PulseEditCards.jsx b/frontend/src/metabase/pulse/components/PulseEditCards.jsx index df98dfd4a23f3e9cb6d2a2174712d9f3e76a8d8e..a270d142a5998a6794af9047c28afd1ec69cc601 100644 --- a/frontend/src/metabase/pulse/components/PulseEditCards.jsx +++ b/frontend/src/metabase/pulse/components/PulseEditCards.jsx @@ -116,7 +116,7 @@ export default class PulseEditCards extends Component { notices.push({ type: "warning", head: t`Looks like this pulse is getting big`, - body: t`We recommend keeping pulses small and focused to help keep them digestable and useful to the whole team.`, + body: t`We recommend keeping pulses small and focused to help keep them digestible and useful to the whole team.`, }); } return notices; diff --git a/frontend/src/metabase/query_builder/components/AlertListPopoverContent.jsx b/frontend/src/metabase/query_builder/components/AlertListPopoverContent.jsx index 4f6cf88b1cbea8914cc0a1aa7d92c83344305573..031dc330b875349400210436ed0deda4e18293d8 100644 --- a/frontend/src/metabase/query_builder/components/AlertListPopoverContent.jsx +++ b/frontend/src/metabase/query_builder/components/AlertListPopoverContent.jsx @@ -336,7 +336,7 @@ export class AlertCreatorTitle extends Component { const isAdmin = user.is_superuser; const isCurrentUser = alert.creator.id === user.id; const creator = - alert.creator.id === user.id ? "You" : alert.creator.first_name; + alert.creator.id === user.id ? t`You` : alert.creator.first_name; const text = !isCurrentUser && !isAdmin ? t`You're receiving ${creator}'s alerts` diff --git a/frontend/src/metabase/query_builder/components/AlertModals.jsx b/frontend/src/metabase/query_builder/components/AlertModals.jsx index 24d955b8ba7c28c6f1b8d289dac659d5b23985e8..c035d3591c20ed58ef3c188fa008514d7ff72897 100644 --- a/frontend/src/metabase/query_builder/components/AlertModals.jsx +++ b/frontend/src/metabase/query_builder/components/AlertModals.jsx @@ -215,7 +215,7 @@ export class AlertEducationalScreen extends Component { <p className={`${classes} ml2 text-left`} >{jt`When a raw data question ${( - <strong>returns any results</strong> + <strong>{t`returns any results`}</strong> )}`}</p> </div> <div @@ -226,7 +226,7 @@ export class AlertEducationalScreen extends Component { <p className={`${classes} mr2 text-right`} >{jt`When a line or bar ${( - <strong>crosses a goal line</strong> + <strong>{t`crosses a goal line`}</strong> )}`}</p> </div> <div @@ -236,7 +236,9 @@ export class AlertEducationalScreen extends Component { <RetinaImage src="app/assets/img/alerts/education-illustration-03-progress.png" /> <p className={`${classes} ml2 text-left`} - >{jt`When a progress bar ${<strong>reaches its goal</strong>}`}</p> + >{jt`When a progress bar ${( + <strong>{t`reaches its goal`}</strong> + )}`}</p> </div> </div> <Button @@ -646,15 +648,15 @@ export class RawDataAlertTip extends Component { export const MultiSeriesAlertTip = () => ( <div>{jt`${( - <strong>Heads up:</strong> + <strong>{t`Heads up`}:</strong> )} Goal-based alerts aren't yet supported for charts with more than one line, so this alert will be sent whenever the chart has ${( - <em>results</em> + <em>{t`results`}</em> )}.`}</div> ); export const NormalAlertTip = () => ( <div>{jt`${( - <strong>Tip:</strong> + <strong>{t`Tip`}:</strong> )} This kind of alert is most useful when your saved question doesn’t ${( - <em>usually</em> + <em>{t`usually`}</em> )} return any results, but you want to know when it does.`}</div> ); diff --git a/frontend/src/metabase/query_builder/components/QueryVisualization.jsx b/frontend/src/metabase/query_builder/components/QueryVisualization.jsx index a6d53f6b3f4075ea5434db93ddcea9f9fd326ebd..afe9679d88377ede4ddf2bbc214e062106b88ceb 100644 --- a/frontend/src/metabase/query_builder/components/QueryVisualization.jsx +++ b/frontend/src/metabase/query_builder/components/QueryVisualization.jsx @@ -145,10 +145,10 @@ export default class QueryVisualization extends Component { {result.data.rows_truncated != null ? jt`Showing first ${( <strong>{formatNumber(result.row_count)}</strong> - )} ${inflect("row", result.data.rows.length)}` + )} ${inflect(t`row`, result.data.rows.length)}` : jt`Showing ${( <strong>{formatNumber(result.row_count)}</strong> - )} ${inflect("row", result.data.rows.length)}`} + )} ${inflect(t`row`, result.data.rows.length)}`} </div> ), }); diff --git a/frontend/src/metabase/query_builder/components/VisualizationResult.jsx b/frontend/src/metabase/query_builder/components/VisualizationResult.jsx index fc3abe19269981bcaa524db43927fd763e7d5bc8..33008f7be878ceb5c1a16ac9d6db2dfbd248ec08 100644 --- a/frontend/src/metabase/query_builder/components/VisualizationResult.jsx +++ b/frontend/src/metabase/query_builder/components/VisualizationResult.jsx @@ -66,9 +66,9 @@ export default class VisualizationResult extends Component { <p> {jt`You can also ${( <a className="link" onClick={this.showCreateAlertModal}> - get an alert + {t`get an alert`} </a> - )} when there are any results.`} + )} when there are some results.`} </p> )} <button diff --git a/frontend/src/metabase/reference/components/GuideHeader.jsx b/frontend/src/metabase/reference/components/GuideHeader.jsx index 5770cd57305de58bf1a21c50328d489242989582..8362926d69fb5702db0372b6dbcc1b6c47ffd2c1 100644 --- a/frontend/src/metabase/reference/components/GuideHeader.jsx +++ b/frontend/src/metabase/reference/components/GuideHeader.jsx @@ -8,10 +8,9 @@ const GuideHeader = ({ startEditing, isSuperuser }) => ( <div> <div className="wrapper wrapper--trim sm-py4 sm-my3"> <div className="flex align-center"> - <h1 - className="text-dark" - style={{ fontWeight: 700 }} - >{t`Start here.`}</h1> + <h1 className="text-dark" style={{ fontWeight: 700 }}> + {t`Start here`}. + </h1> {isSuperuser && ( <span className="ml-auto"> <EditButton startEditing={startEditing} /> diff --git a/frontend/src/metabase/reference/databases/FieldSidebar.jsx b/frontend/src/metabase/reference/databases/FieldSidebar.jsx index 6ebc3d009e83c25e1d16eca863d83be5578c7c0a..27709b6000e58036681a8a642d5e95a21c000d05 100644 --- a/frontend/src/metabase/reference/databases/FieldSidebar.jsx +++ b/frontend/src/metabase/reference/databases/FieldSidebar.jsx @@ -46,12 +46,12 @@ const FieldSidebar = ({ /> { - // <SidebarItem - // key={`/auto/dashboard/field/${field.id}`} - // href={`/auto/dashboard/field/${field.id}`} - // icon='bolt' - // name={t`Generate a dashboard based on this field`} - // /> + <SidebarItem + key={`/auto/dashboard/field/${field.id}`} + href={`/auto/dashboard/field/${field.id}`} + icon="bolt" + name={t`Generate a dashboard based on this field`} + /> } {showXray && ( diff --git a/frontend/src/metabase/setup/components/DatabaseConnectionStep.jsx b/frontend/src/metabase/setup/components/DatabaseConnectionStep.jsx index 84185c292d019b0f5ace5c30699876c6ae31303d..0a151a3c965b994c0c2c73909d4e29d8fdee0c45 100644 --- a/frontend/src/metabase/setup/components/DatabaseConnectionStep.jsx +++ b/frontend/src/metabase/setup/components/DatabaseConnectionStep.jsx @@ -122,7 +122,7 @@ export default class DatabaseConnectionStep extends Component { return ( <label className="Select Form-offset mt1"> <select defaultValue={engine} onChange={this.chooseDatabaseEngine}> - <option value="">Select the type of Database you use</option> + <option value="">{t`Select the type of Database you use`}</option> {engineNames.map(opt => ( <option key={opt} value={opt}> {engines[opt]["driver-name"]} @@ -185,7 +185,7 @@ export default class DatabaseConnectionStep extends Component { formError={formError} hiddenFields={{ ssl: true }} submitFn={this.connectionDetailsCaptured} - submitButtonText={"Next"} + submitButtonText={t`Next`} /> ) : null} diff --git a/frontend/src/metabase/setup/components/PreferencesStep.jsx b/frontend/src/metabase/setup/components/PreferencesStep.jsx index d6f78e486ef7772a5335467bcb6793e3565302e2..a802869a409939fa8ee5404f75dffb6097006d89 100644 --- a/frontend/src/metabase/setup/components/PreferencesStep.jsx +++ b/frontend/src/metabase/setup/components/PreferencesStep.jsx @@ -103,7 +103,7 @@ export default class PreferencesStep extends Component { <div className="Form-field Form-offset"> <ul style={{ listStyle: "disc inside", lineHeight: "200%" }}> <li>{jt`Metabase ${( - <span style={{ fontWeight: "bold" }}>never</span> + <span style={{ fontWeight: "bold" }}>{t`never`}</span> )} collects anything about your data or question results.`}</li> <li>{t`All collection is completely anonymous.`}</li> <li diff --git a/frontend/src/metabase/visualizations/components/Visualization.jsx b/frontend/src/metabase/visualizations/components/Visualization.jsx index 425ad081f9c84a890328dc8e1dd3300111f5a50d..fd967a69ae01fb3c312b29520fbbd9fd22c1cd1e 100644 --- a/frontend/src/metabase/visualizations/components/Visualization.jsx +++ b/frontend/src/metabase/visualizations/components/Visualization.jsx @@ -473,7 +473,7 @@ export default class Visualization extends Component { </div> ) : ( <div> - {t`This is usually pretty fast, but seems to be taking awhile right now.`} + {t`This is usually pretty fast but seems to be taking awhile right now.`} </div> )} </div> diff --git a/frontend/src/metabase/visualizations/lib/fill_data.js b/frontend/src/metabase/visualizations/lib/fill_data.js index b95d52a5007ee24dc7b91819b9ccadf763310928..d11abafdc759c59f7d06284edbd96859ecaf53d5 100644 --- a/frontend/src/metabase/visualizations/lib/fill_data.js +++ b/frontend/src/metabase/visualizations/lib/fill_data.js @@ -35,7 +35,7 @@ function fillMissingValues(datas, xValues, fillValue, getKey = v => v) { } }); if (map.size > 0) { - console.warn(t`"xValues missing!`, map, newRows); + console.warn(t`xValues missing!`, map, newRows); } return newRows; }); diff --git a/frontend/src/metabase/xray/components/InsightCard.jsx b/frontend/src/metabase/xray/components/InsightCard.jsx index 3832837e9939a511156b462e5e1607a0c2c687f2..57d360ae1a1c95f990477f6a3770bb9aa26af830 100644 --- a/frontend/src/metabase/xray/components/InsightCard.jsx +++ b/frontend/src/metabase/xray/components/InsightCard.jsx @@ -175,17 +175,21 @@ export class VariationTrendInsight extends Component { render() { const { mode } = this.props; + const MODE_ADVERB_STRINGS = { + increasing: t`increasingly`, + decreasing: t`decreasingly`, + }; return ( <InsightText> - It looks like this data has grown {mode}ly{" "} + {t`It looks like this data has grown ${MODE_ADVERB_STRINGS[mode]}`}{" "} <TermWithDefinition definition={variationTrendDefinition} link={varianceLink} > - varied + {t`varied`} </TermWithDefinition>{" "} - over time. + {t`over time.`} </InsightText> ); } diff --git a/frontend/src/metabase/xray/components/PreviewBanner.jsx b/frontend/src/metabase/xray/components/PreviewBanner.jsx index 0c6a6d88d0f68dda1cce95926876ab8615975dfa..f6484916d996a75b31c5ed946a7c57fe496d59c5 100644 --- a/frontend/src/metabase/xray/components/PreviewBanner.jsx +++ b/frontend/src/metabase/xray/components/PreviewBanner.jsx @@ -1,6 +1,6 @@ import React from "react"; import Icon from "metabase/components/Icon"; -import { jt } from "c-3po"; +import { t, jt } from "c-3po"; const SURVEY_LINK = "https://docs.google.com/forms/d/e/1FAIpQLSc92WzF76ViiT8l4646lvFSWejNUhh4lhCSMXdZECILVwJG2A/viewform?usp=sf_link"; @@ -14,7 +14,7 @@ const PreviewBanner = () => ( /> <span>{jt`Welcome to the x-ray preview! We'd love ${( <a className="link" href={SURVEY_LINK} target="_blank"> - your feedback + {t`your feedback`} </a> )}`}</span> </div> diff --git a/frontend/src/metabase/xray/containers/TableXRay.jsx b/frontend/src/metabase/xray/containers/TableXRay.jsx index cb4b2992eed500aaeba15427a07c50ab48e590c6..7b70b2c9ac1fb47510c308d07e2c4a92f9a29bee 100644 --- a/frontend/src/metabase/xray/containers/TableXRay.jsx +++ b/frontend/src/metabase/xray/containers/TableXRay.jsx @@ -123,7 +123,7 @@ class TableXRay extends Component { </p> </div> <div className="ml-auto flex align-center"> - <h3 className="mr2">{t`Fidelity:`}</h3> + <h3 className="mr2">{t`Fidelity`}:</h3> <CostSelect xrayType="table" currentCost={params.cost} diff --git a/resources/automagic_dashboards/special/field.yaml b/resources/automagic_dashboards/special/field.yaml new file mode 100644 index 0000000000000000000000000000000000000000..2e5c8f19ce788705e4d4fe3336aca990045ce183 --- /dev/null +++ b/resources/automagic_dashboards/special/field.yaml @@ -0,0 +1,47 @@ +title: Analysis of [[this]] +table_type: GenericTable +metrics: +- Sum: [sum, [dimension, GenericNumber]] +- Avg: [avg, [dimension, GenericNumber]] +- Count: [count] +dimensions: + - Field: + field_type: "*" + score: 0 + - GenericNumber: GenericTable.Number +# bind just so they don't get used + - LongLat: GenericTable.Coordinate + - ZipCode: GenericTable.ZipCode +# only used for filters + - Country: + field_type: GenericTable.Country + - State: + field_type: GenericTable.State + - GenericCategoryMedium: + field_type: GenericTable.Category + max_cardinality: 12 + score: 90 + - Timestamp: + field_type: CreationTimestamp + score: 100 + - Timestamp: + field_type: DateTime + score: 90 +dashboard_filters: + - Timestamp + - State + - Country + - GenericCategoryMedium +cards: + - Distribution: + title: Distribution of [[Field]] + visualization: bar + metrics: Count + dimensions: Field + - ByNumber: + title: "[[GenericNumber]] by [[Field]]" + visualization: line + metrics: + - Sum + - Avg + dimensions: Field \ No newline at end of file diff --git a/src/metabase/api/automagic_dashboards.clj b/src/metabase/api/automagic_dashboards.clj index 21c3491bb842241e9c59a990c5bad4f66ee4b829..8d0ec3934f5ce96e429aeeef61b8981ef6f5f01a 100644 --- a/src/metabase/api/automagic_dashboards.clj +++ b/src/metabase/api/automagic_dashboards.clj @@ -82,10 +82,10 @@ [id] (-> id Metric api/check-404 magic/automagic-analysis)) -;; (api/defendpoint GET "/field/:id" -;; "Return an automagic dashboard analyzing field with id `id`." -;; [id] -;; (-> id Field api/check-404 :table_id Table magic/automagic-dashboard)) +(api/defendpoint GET "/field/:id" + "Return an automagic dashboard analyzing field with id `id`." + [id] + (-> id Field api/check-404 magic/automagic-analysis)) (api/defendpoint GET "/question/:id" "Return an automagic dashboard analyzing question with id `id`." diff --git a/src/metabase/api/tiles.clj b/src/metabase/api/tiles.clj index 0e1537c123ba1c0a63a7b4c51aebbab21fbba852..ccd5cb607c964b1ac0fb4b66085dc1b4844666b6 100644 --- a/src/metabase/api/tiles.clj +++ b/src/metabase/api/tiles.clj @@ -97,7 +97,7 @@ (let [output-stream (ByteArrayOutputStream.)] (try (when-not (ImageIO/write tile "png" output-stream) ; returns `true` if successful -- see JavaDoc - (throw (Exception. "No approprate image writer found!"))) + (throw (Exception. "No appropriate image writer found!"))) (.flush output-stream) (.toByteArray output-stream) (catch Throwable e diff --git a/src/metabase/automagic_dashboards/core.clj b/src/metabase/automagic_dashboards/core.clj index 799fd1cd912901e68688c0f6a712a84bc97b55a0..ad35b9e45a601ce5624df6702dd4917431788d88 100644 --- a/src/metabase/automagic_dashboards/core.clj +++ b/src/metabase/automagic_dashboards/core.clj @@ -234,10 +234,9 @@ `links_to`, ...) are used; 3) if there is still a tie, `score`." (comp last (partial sort-by (comp (fn [[_ definition]] - [(->> definition - :field_type - (map (comp count ancestors)) - (reduce + )) + [(transduce (map (comp count ancestors)) + + + (:field_type definition)) (count definition) (:score definition)]) first)))) @@ -245,8 +244,8 @@ (defn- bind-dimensions "Bind fields to dimensions and resolve overloading. Each field x aggregation pair will be bound to only one dimension. If multiple - dimension definitions match a single field, the field is bound to the specific - definition is used (see `most-specific-defintion` for details)." + dimension definitions match a single field, the field is bound to the most + specific definition used (see `most-specific-defintion` for details)." [context dimensions] (->> dimensions (mapcat (comp (partial make-binding context) first)) @@ -540,7 +539,7 @@ (remove (comp #{root} :table)) (take (- max-related (count indepth)))) :indepth indepth}))) - (log/info (format "Skipping %s: no cards fully match the topology." + (log/info (format "Skipping %s: no cards fully match bound dimensions." (full-name root)))))) (def ^:private ^{:arglists '([card])} table-like? @@ -573,4 +572,15 @@ (defmethod automagic-analysis (type Field) [field] - nil) + (-> "special/field.yaml" + rules/load-rule + (update :dimensions conj + {"Field" + {:field_type [(-> field table :entity_type) + ((some-fn :special_type :base_type) field)] + :named (:name field) + :score 100 + :aggregation (cond + (isa? (:base_type field) :type/DateTime) :month + (isa? (:base_type field) :type/Number) :default)}}) + (automagic-dashboard field))) diff --git a/src/metabase/automagic_dashboards/filters.clj b/src/metabase/automagic_dashboards/filters.clj index 52aadf565c1c1d8651f8a2927fa93248e73123bc..d4e88216339931b82512064211fbb08701dfe671 100644 --- a/src/metabase/automagic_dashboards/filters.clj +++ b/src/metabase/automagic_dashboards/filters.clj @@ -33,10 +33,11 @@ (defn- build-fk-map [tables field] - (->> (db/select [Field :id :fk_target_field_id :table_id] + (->> (db/select Field :fk_target_field_id [:not= nil] :table_id [:in tables]) - (filter (comp #{(:table_id field)} :table_id Field :fk_target_field_id)) + field/with-targets + (filter (comp #{(:table_id field)} :table_id :target)) (group-by :table_id) (keep (fn [[_ [fk & fks]]] ;; Bail out if there is more than one FK from the same table @@ -51,17 +52,16 @@ (defn- add-filter [dashcard filter-id field] - (if-let [targets (->> (conj (:series dashcard) (:card dashcard)) - (keep (fn [card] - (some-> card - (filter-for-card field) - (vector card)))) - not-empty)] - (update dashcard :parameter_mappings concat (for [[target card] targets] - {:parameter_id filter-id - :target target - :card_id (:id card)})) - dashcard)) + (let [mappings (->> (conj (:series dashcard) (:card dashcard)) + (keep (fn [card] + (when-let [target (filter-for-card card field)] + {:parameter_id filter-id + :target target + :card_id (:id card)}))) + not-empty)] + (cond + (nil? (:card dashcard)) dashcard + mappings (update dashcard :parameter_mappings concat mappings)))) (defn- filter-type [{:keys [base_type special_type] :as field}] @@ -89,11 +89,13 @@ (let [filter-id (-> candidate hash str) dashcards (:ordered_cards dashboard) dashcards-new (map #(add-filter % filter-id candidate) dashcards)] - (cond-> dashboard - (not= dashcards dashcards-new) - (-> (assoc :ordered_cards dashcards-new) + ;; Only add filters that apply to all cards. + (if (= (count dashcards) (count dashcards-new)) + (-> dashboard + (assoc :ordered_cards dashcards-new) (update :parameters conj {:id filter-id :type (filter-type candidate) :name (:display_name candidate) - :slug (:name candidate)}))))) + :slug (:name candidate)})) + dashboard))) dashboard)))) diff --git a/src/metabase/public_settings.clj b/src/metabase/public_settings.clj index 5a1c7d9d970b9283aa15cdea75ba7c73107f84dd..df5f518eae3be215460e3182cdeca5912d1cfbc5 100644 --- a/src/metabase/public_settings.clj +++ b/src/metabase/public_settings.clj @@ -101,7 +101,7 @@ :default 1000) (defsetting query-caching-max-ttl - (tru "The absoulte maximum time to keep any cached query results, in seconds.") + (tru "The absolute maximum time to keep any cached query results, in seconds.") :type :integer :default (* 60 60 24 100)) ; 100 days diff --git a/src/metabase/pulse/render.clj b/src/metabase/pulse/render.clj index 6eba700b5befdfd10e629b327aad3390d194125a..f4194e45eebe106920e17e39ec913720dd9c3cbc 100644 --- a/src/metabase/pulse/render.clj +++ b/src/metabase/pulse/render.clj @@ -543,7 +543,7 @@ (* 2 sparkline-dot-radius) (* 2 sparkline-dot-radius))) (when-not (ImageIO/write image "png" os) ; returns `true` if successful -- see JavaDoc - (let [^String msg (tru "No approprate image writer found!")] + (let [^String msg (tru "No appropriate image writer found!")] (throw (Exception. msg)))) (.toByteArray os))) diff --git a/test/metabase/automagic_dashboards/core_test.clj b/test/metabase/automagic_dashboards/core_test.clj index d3eb52803e20cf0211df397b514edcc0fc00a3a2..04c48e7f3f2481c55f163e724bff17438aa70eab 100644 --- a/test/metabase/automagic_dashboards/core_test.clj +++ b/test/metabase/automagic_dashboards/core_test.clj @@ -52,14 +52,6 @@ (-> (keep automagic-dashboard (Table)) count pos?)))) -;; Identity -(expect - :d1 - (-> [{:d1 {:field_type [:type/Category] :score 100}}] - (#'magic/most-specific-definition) - first - key)) - ;; Identity (expect :d1 diff --git a/test/metabase/driver/mysql_test.clj b/test/metabase/driver/mysql_test.clj index cfb4766e4d325bcdbafa7ab6e6ab369907849bdc..7840327daf2e2e1e2645e0b9ea44b1ea7952bcf0 100644 --- a/test/metabase/driver/mysql_test.clj +++ b/test/metabase/driver/mysql_test.clj @@ -74,7 +74,7 @@ ;; if someone says specifies `tinyInt1isBit=false`, it should come back as a number instead (expect-with-engine :mysql - #{{:name "number-of-cans", :base_type :type/Integer, :special_type :type/Category} + #{{:name "number-of-cans", :base_type :type/Integer, :special_type :type/Quantity} {:name "id", :base_type :type/Integer, :special_type :type/PK} {:name "thing", :base_type :type/Text, :special_type :type/Category}} (data/with-temp-db [db tiny-int-ones]