Skip to content
Snippets Groups Projects
Unverified Commit 922b7077 authored by Paul Rosenzweig's avatar Paul Rosenzweig Committed by GitHub
Browse files

Bulk hide/show for tables (#12569)

parent 876faf9e
Branches
Tags
No related merge requests found
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import Tables from "metabase/entities/tables";
import Icon from "metabase/components/Icon";
......@@ -9,7 +12,18 @@ import _ from "underscore";
import cx from "classnames";
import { regexpEscape } from "metabase/lib/string";
import { color } from "metabase/lib/colors";
@connect(
null,
{
setVisibilityForTables: (tables, visibility_type) =>
Tables.actions.bulkUpdate({
ids: tables.map(t => t.id),
visibility_type,
}),
},
)
export default class MetadataTableList extends Component {
constructor(props, context) {
super(props, context);
......@@ -37,44 +51,24 @@ export default class MetadataTableList extends Component {
});
}
partitionedTables() {
const regex = this.state.searchRegex;
const [hiddenTables, queryableTables] = _.chain(this.props.tables)
.filter(
table =>
!regex || regex.test(table.display_name) || regex.test(table.name),
)
.sortBy("display_name")
.partition(table => table.visibility_type != null)
.value();
return { hiddenTables, queryableTables };
}
render() {
let queryableTablesHeader, hiddenTablesHeader;
const queryableTables = [];
const hiddenTables = [];
if (this.props.tables) {
const tables = _.sortBy(this.props.tables, "display_name");
_.each(tables, table => {
const selected = this.props.tableId === table.id;
const row = (
<li key={table.id}>
<a
className={cx(
"AdminList-item flex align-center no-decoration text-wrap",
{
selected,
},
)}
onClick={this.props.selectTable.bind(null, table)}
>
{table.display_name}
</a>
</li>
);
const regex = this.state.searchRegex;
if (
!regex ||
regex.test(table.display_name) ||
regex.test(table.name)
) {
if (table.visibility_type) {
hiddenTables.push(row);
} else {
queryableTables.push(row);
}
}
});
}
const { hiddenTables, queryableTables } = this.partitionedTables();
const { setVisibilityForTables } = this.props;
if (queryableTables.length > 0) {
queryableTablesHeader = (
......@@ -83,6 +77,11 @@ export default class MetadataTableList extends Component {
ngettext(msgid`${n} Queryable Table`, `${n} Queryable Tables`, n))(
queryableTables.length,
)}
<ToggleHiddenButton
setVisibilityForTables={setVisibilityForTables}
tables={queryableTables}
isHidden={false}
/>
</li>
);
}
......@@ -92,6 +91,11 @@ export default class MetadataTableList extends Component {
{(n => ngettext(msgid`${n} Hidden Table`, `${n} Hidden Tables`, n))(
hiddenTables.length,
)}
<ToggleHiddenButton
setVisibilityForTables={setVisibilityForTables}
tables={hiddenTables}
isHidden={true}
/>
</li>
);
}
......@@ -99,6 +103,8 @@ export default class MetadataTableList extends Component {
queryableTablesHeader = <li className="AdminList-section">0 Tables</li>;
}
const { tableId, selectTable } = this.props;
return (
<div className="MetadataEditor-table-list AdminList flex-no-shrink">
<div className="AdminList-search">
......@@ -131,11 +137,78 @@ export default class MetadataTableList extends Component {
<ul className="AdminList-items">
{queryableTablesHeader}
{queryableTables}
{queryableTables.map(table => (
<TableRow
table={table}
selected={tableId === table.id}
selectTable={selectTable}
setVisibilityForTables={setVisibilityForTables}
/>
))}
{hiddenTablesHeader}
{hiddenTables}
{hiddenTables.map(table => (
<TableRow
table={table}
selected={tableId === table.id}
selectTable={selectTable}
setVisibilityForTables={setVisibilityForTables}
/>
))}
</ul>
</div>
);
}
}
function TableRow({
table,
selectTable,
toggleHidden,
selected,
setVisibilityForTables,
}) {
return (
<li key={table.id} className="hover-parent hover--visibility">
<a
className={cx(
"AdminList-item flex align-center no-decoration text-wrap justify-between",
{ selected },
)}
onClick={() => selectTable(table)}
>
{table.display_name}
<div className="hover-child float-right">
<ToggleHiddenButton
tables={[table]}
isHidden={table.visibility_type != null}
setVisibilityForTables={setVisibilityForTables}
/>
</div>
</a>
</li>
);
}
function ToggleHiddenButton({ setVisibilityForTables, tables, isHidden }) {
return (
<Icon
name={isHidden ? "eye" : "eye_crossed_out"}
onClick={e => {
e.stopPropagation();
setVisibilityForTables(tables, isHidden ? null : "hidden");
}}
tooltip={
tables.length > 1
? isHidden
? t`Unhide all`
: t`Hide all`
: isHidden
? t`Unhide`
: t`Hide`
}
size={18}
className={"float-right cursor-pointer"}
hover={{ color: color("brand") }}
/>
);
}
......@@ -172,6 +172,7 @@
color: var(--color-text-light);
font-weight: 700;
font-size: smaller;
padding-right: 15px; /* set so that table visibility icons align */
}
.AdminInput {
......
......@@ -20,7 +20,7 @@ import Metrics from "metabase/entities/metrics";
import Segments from "metabase/entities/segments";
import Fields from "metabase/entities/fields";
import { GET } from "metabase/lib/api";
import { GET, PUT } from "metabase/lib/api";
import { addValidOperatorsToFields } from "metabase/lib/schema_metadata";
......@@ -31,8 +31,10 @@ const listTablesForDatabase = async (...args) =>
// HACK: no /api/database/:dbId/tables endpoint
(await GET("/api/database/:dbId/metadata")(...args)).tables;
const listTablesForSchema = GET("/api/database/:dbId/schema/:schemaName");
const updateTables = PUT("/api/table");
// OBJECT ACTIONS
export const TABLES_BULK_UPDATE = "metabase/entities/TABLES_BULK_UPDATE";
export const FETCH_METADATA = "metabase/entities/FETCH_METADATA";
export const FETCH_TABLE_METADATA = "metabase/entities/FETCH_TABLE_METADATA";
export const FETCH_TABLE_FOREIGN_KEYS =
......@@ -56,6 +58,14 @@ const Tables = createEntity({
},
},
actions: {
// updates all tables in the `ids` key
bulkUpdate: compose(
withAction(TABLES_BULK_UPDATE),
withNormalize([TableSchema]),
)(updates => async (dispatch, getState) => updateTables(updates)),
},
// ACTION CREATORS
objectActions: {
// loads `query_metadata` for a single table
......
......@@ -185,5 +185,14 @@ describe("scenarios > admin > datamodel > table", () => {
cy.url().should("include", "/admin/datamodel/database/1/table/2");
cy.contains("Revenue");
});
it("should allow bulk hiding tables", () => {
cy.visit(ORDERS_URL);
cy.contains("4 Queryable Tables");
cy.get(".AdminList-section .Icon-eye_crossed_out").click();
cy.contains("4 Hidden Tables");
cy.get(".AdminList-section .Icon-eye").click();
cy.contains("4 Queryable Tables");
});
});
});
......@@ -47,18 +47,9 @@
(-> (api/read-check Table id)
(hydrate :db :pk_field)))
(api/defendpoint PUT "/:id"
"Update `Table` with ID."
[id :as {{:keys [display_name entity_type visibility_type description caveats points_of_interest
show_in_getting_started], :as body} :body}]
{display_name (s/maybe su/NonBlankString)
entity_type (s/maybe su/EntityTypeKeywordOrString)
visibility_type (s/maybe TableVisibilityType)
description (s/maybe su/NonBlankString)
caveats (s/maybe su/NonBlankString)
points_of_interest (s/maybe su/NonBlankString)
show_in_getting_started (s/maybe s/Bool)}
;; TODO: this should changed to `update-tables!` and update multiple tables in one db request
(defn- update-table!
[id {:keys [visibility_type] :as body}]
(api/write-check Table id)
(let [original-visibility-type (db/select-one-field :visibility_type Table :id id)]
;; always update visibility type; update display_name, show_in_getting_started, entity_type if non-nil; update
......@@ -78,6 +69,35 @@
(sync/sync-table! updated-table))
updated-table)))
(api/defendpoint PUT "/:id"
"Update `Table` with ID."
[id :as {{:keys [display_name entity_type visibility_type description caveats points_of_interest
show_in_getting_started], :as body} :body}]
{display_name (s/maybe su/NonBlankString)
entity_type (s/maybe su/EntityTypeKeywordOrString)
visibility_type (s/maybe TableVisibilityType)
description (s/maybe su/NonBlankString)
caveats (s/maybe su/NonBlankString)
points_of_interest (s/maybe su/NonBlankString)
show_in_getting_started (s/maybe s/Bool)}
(update-table! id body))
(api/defendpoint PUT "/"
"Update all `Table` in `ids`."
[:as {{:keys [ids display_name entity_type visibility_type description caveats points_of_interest
show_in_getting_started], :as body} :body}]
{ids (su/non-empty [su/IntGreaterThanZero])
display_name (s/maybe su/NonBlankString)
entity_type (s/maybe su/EntityTypeKeywordOrString)
visibility_type (s/maybe TableVisibilityType)
description (s/maybe su/NonBlankString)
caveats (s/maybe su/NonBlankString)
points_of_interest (s/maybe su/NonBlankString)
show_in_getting_started (s/maybe s/Bool)}
(db/transaction
(mapv #(update-table! % body) ids)))
(def ^:private auto-bin-str (deferred-tru "Auto bin"))
(def ^:private dont-bin-str (deferred-tru "Don''t bin"))
(def ^:private day-str (deferred-tru "Day"))
......
......@@ -353,6 +353,25 @@
(test-fun "technical")
@called)))
;; ## PUT /api/table
(tt/expect-with-temp [Table [table-1]
Table [table-2]]
[{:description "This nice table was updated!"
:visibility_type "hidden"
:display_name "Userz"
:id (u/get-id table-1)}
{:description "This nice table was updated!"
:visibility_type "hidden"
:display_name "Userz"
:id (u/get-id table-2)}]
(map #(select-keys % [:description :visibility_type :display_name :id])
((test-users/user->client :crowberto) :put 200 "table"
{:ids (map u/get-id [table-1 table-2])
:display_name "Userz"
:visibility_type "hidden"
:description "This nice table was updated!"})))
;; ## GET /api/table/:id/fks
;; We expect a single FK from CHECKINS.USER_ID -> USERS.ID
(expect
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment