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

Refactor data model tab (#9957)

parent 8fd14f1e
No related branches found
No related tags found
No related merge requests found
Showing
with 361 additions and 378 deletions
......@@ -35,12 +35,19 @@ import type { FieldValues } from "metabase/meta/types/Field";
* Wrapper class for field metadata objects. Belongs to a Table.
*/
export default class Field extends Base {
displayName: string;
description: string;
table: Table;
name_field: ?Field;
displayName({ includeSchema, includeTable } = {}) {
return (
(includeTable && this.table
? this.table.displayName({ includeSchema }) + ""
: "") + this.display_name
);
}
fieldType() {
return getFieldType(this);
}
......
......@@ -13,13 +13,14 @@ import type { SchemaName } from "metabase/meta/types/Table";
import type { FieldMetadata } from "metabase/meta/types/Metadata";
import type { ConcreteField, DatetimeUnit } from "metabase/meta/types/Query";
import { titleize, humanize } from "metabase/lib/formatting";
import Dimension from "../Dimension";
import _ from "underscore";
/** This is the primary way people interact with tables */
export default class Table extends Base {
displayName: string;
description: string;
schema: ?SchemaName;
......@@ -41,6 +42,14 @@ export default class Table extends Base {
return this.fields.map(field => field.dimension());
}
displayName({ includeSchema } = {}) {
return (
(includeSchema && this.schema
? titleize(humanize(this.schema)) + "."
: "") + this.display_name
);
}
dateFields(): Field[] {
return this.fields.filter(field => field.isDate());
}
......
......@@ -115,7 +115,7 @@ export default class FieldRemapping extends React.Component {
"Change Remapping Type",
"No Remapping",
);
await deleteFieldDimension(field.id);
await deleteFieldDimension({ id: field.id });
this.setState({ hasChanged: false });
} else if (mappingType.type === "foreign") {
// Try to find a entity name field from target table and choose it as remapping target field if it exists
......@@ -127,11 +127,14 @@ export default class FieldRemapping extends React.Component {
"Change Remapping Type",
"Foreign Key",
);
await updateFieldDimension(field.id, {
type: "external",
name: field.display_name,
human_readable_field_id: entityNameFieldId,
});
await updateFieldDimension(
{ id: field.id },
{
type: "external",
name: field.display_name,
human_readable_field_id: entityNameFieldId,
},
);
} else {
// Enter a special state where we are choosing an initial value for FK target
this.setState({
......@@ -145,11 +148,14 @@ export default class FieldRemapping extends React.Component {
"Change Remapping Type",
"Custom Remappings",
);
await updateFieldDimension(field.id, {
type: "internal",
name: field.display_name,
human_readable_field_id: null,
});
await updateFieldDimension(
{ id: field.id },
{
type: "internal",
name: field.display_name,
human_readable_field_id: null,
},
);
this.setState({ hasChanged: true });
} else {
throw new Error(t`Unrecognized mapping type`);
......@@ -157,7 +163,7 @@ export default class FieldRemapping extends React.Component {
// TODO Atte Keinänen 7/11/17: It's a pretty heavy approach to reload the whole table after a single field
// has been updated; would be nicer to just fetch a single field. MetabaseApi.field_get seems to exist for that
await fetchTableMetadata(table.id, true);
await fetchTableMetadata({ id: table.id }, { reload: true });
};
onForeignKeyFieldChange = async foreignKeyClause => {
......@@ -174,13 +180,16 @@ export default class FieldRemapping extends React.Component {
const dimension = Dimension.parseMBQL(foreignKeyClause);
if (dimension && dimension instanceof FKDimension) {
MetabaseAnalytics.trackEvent("Data Model", "Update FK Remapping Target");
await updateFieldDimension(field.id, {
type: "external",
name: field.display_name,
human_readable_field_id: dimension.destination().field().id,
});
await updateFieldDimension(
{ id: field.id },
{
type: "external",
name: field.display_name,
human_readable_field_id: dimension.destination().field().id,
},
);
await fetchTableMetadata(table.id, true);
await fetchTableMetadata({ id: table.id }, { reload: true });
this.refs.fkPopover.close();
} else {
......@@ -190,7 +199,7 @@ export default class FieldRemapping extends React.Component {
onUpdateRemappings = remappings => {
const { field, updateFieldValues } = this.props;
return updateFieldValues(field.id, Array.from(remappings));
return updateFieldValues({ id: field.id }, Array.from(remappings));
};
// TODO Atte Keinänen 7/11/17: Should we have stricter criteria for valid remapping targets?
......
......@@ -15,11 +15,10 @@ export default class ObjectRetireModal extends Component {
}
async handleSubmit() {
const { object, objectType } = this.props;
let payload = {
const payload = {
id: this.props.object.id,
revision_message: ReactDOM.findDOMNode(this.refs.revision_message).value,
};
payload[objectType + "Id"] = object.id;
await this.props.onRetire(payload);
this.props.onClose();
......
......@@ -7,7 +7,6 @@ import Select, { Option } from "metabase/components/Select.jsx";
import Icon from "metabase/components/Icon";
import { t } from "ttag";
import * as MetabaseCore from "metabase/lib/core";
import { titleize, humanize } from "metabase/lib/formatting";
import { isNumericBaseType, isCurrency } from "metabase/lib/schema_metadata";
import { TYPE, isa, isFK } from "metabase/lib/types";
import currency from "metabase/lib/currency";
......@@ -21,43 +20,35 @@ import MetabaseAnalytics from "metabase/lib/analytics";
@withRouter
export default class Column extends Component {
constructor(props, context) {
super(props, context);
this.onDescriptionChange = this.onDescriptionChange.bind(this);
this.onNameChange = this.onNameChange.bind(this);
this.onVisibilityChange = this.onVisibilityChange.bind(this);
}
static propTypes = {
field: PropTypes.object,
idfields: PropTypes.array.isRequired,
updateField: PropTypes.func.isRequired,
};
updateProperty(name, value) {
this.props.field[name] = value;
this.props.updateField(this.props.field);
}
updateField = properties =>
this.props.updateField({
...this.props.field.getPlainObject(),
...properties,
});
onNameChange(event) {
onNameChange = event => {
if (!_.isEmpty(event.target.value)) {
this.updateProperty("display_name", event.target.value);
this.updateField({ display_name: event.target.value });
} else {
// if the user set this to empty then simply reset it because that's not allowed!
event.target.value = this.props.field.display_name;
}
}
};
onDescriptionChange(event) {
this.updateProperty("description", event.target.value);
}
onDescriptionChange = event =>
this.updateField({ description: event.target.value });
onVisibilityChange(type) {
this.updateProperty("visibility_type", type.id);
}
onVisibilityChange = ({ id: visibility_type }) =>
this.updateField({ visibility_type });
render() {
const { field, idfields, updateField } = this.props;
const { field, idfields } = this.props;
return (
<li className="mt1 mb3 flex">
......@@ -76,14 +67,14 @@ export default class Column extends Component {
<FieldVisibilityPicker
className="block"
field={field}
updateField={updateField}
updateField={this.updateField}
/>
</div>
<div className="flex-full px1">
<SpecialTypeAndTargetPicker
className="block"
field={field}
updateField={updateField}
updateField={this.updateField}
idfields={idfields}
/>
</div>
......@@ -120,11 +111,8 @@ export class FieldVisibilityPicker extends Component {
className?: string,
};
onVisibilityChange = visibilityType => {
const { field } = this.props;
field.visibility_type = visibilityType.id;
this.props.updateField(field);
};
onVisibilityChange = ({ id: visibility_type }) =>
this.props.updateField({ visibility_type });
render() {
const { field, className } = this.props;
......@@ -133,9 +121,9 @@ export class FieldVisibilityPicker extends Component {
<Select
className={cx("TableEditor-field-visibility block", className)}
placeholder={t`Select a field visibility`}
value={_.find(MetabaseCore.field_visibility_types, type => {
return type.id === field.visibility_type;
})}
value={MetabaseCore.field_visibility_types.find(
type => type.id === field.visibility_type,
)}
options={MetabaseCore.field_visibility_types}
onChange={this.onVisibilityChange}
triggerClasses={this.props.triggerClasses}
......@@ -152,26 +140,24 @@ export class SpecialTypeAndTargetPicker extends Component {
selectSeparator?: React$Element<any>,
};
onSpecialTypeChange = async special_type => {
onSpecialTypeChange = async ({ id: special_type }) => {
const { field, updateField } = this.props;
// FIXME: mutation
field.special_type = special_type.id;
// If we are changing the field from a FK to something else, we should delete any FKs present
if (field.target && field.target.id != null && isFK(field.special_type)) {
// we have something that used to be an FK and is now not an FK
// clean up after ourselves
field.target = null;
field.fk_target_field_id = null;
await updateField({
special_type,
target: null,
k_target_field_id: null,
});
} else {
await updateField({ special_type });
}
await updateField(field);
MetabaseAnalytics.trackEvent(
"Data Model",
"Update Field Special-Type",
field.special_type,
special_type,
);
};
......@@ -192,11 +178,8 @@ export class SpecialTypeAndTargetPicker extends Component {
);
};
onTargetChange = async target_field => {
const { field, updateField } = this.props;
field.fk_target_field_id = target_field.id;
await updateField(field);
onTargetChange = async ({ id: fk_target_field_id }) => {
await this.props.updateField({ fk_target_field_id });
MetabaseAnalytics.trackEvent("Data Model", "Update Field Target");
};
......@@ -221,7 +204,7 @@ export class SpecialTypeAndTargetPicker extends Component {
// If all FK target fields are in the same schema (like `PUBLIC` for sample dataset)
// or if there are no schemas at all, omit the schema name
const includeSchemaName =
const includeSchema =
_.uniq(idfields.map(idField => idField.table.schema)).length > 1;
return (
......@@ -229,8 +212,7 @@ export class SpecialTypeAndTargetPicker extends Component {
<Select
className={cx("TableEditor-field-special-type", className)}
placeholder={t`Select a special type`}
value={_.find(
MetabaseCore.field_special_types,
value={MetabaseCore.field_special_types.find(
type => type.id === field.special_type,
)}
options={specialTypes}
......@@ -260,7 +242,7 @@ export class SpecialTypeAndTargetPicker extends Component {
searchCaseSensitive={false}
>
{Object.values(currency).map(c => (
<Option name={c.name} value={c.code}>
<Option name={c.name} value={c.code} key={c.code}>
<span className="flex full align-center">
<span>{c.name}</span>
<span className="text-bold text-light ml1">{c.symbol}</span>
......@@ -275,20 +257,12 @@ export class SpecialTypeAndTargetPicker extends Component {
className={cx("TableEditor-field-target", className)}
triggerClasses={this.props.triggerClasses}
placeholder={t`Select a target`}
value={
field.fk_target_field_id &&
_.find(
idfields,
idField => idField.id === field.fk_target_field_id,
)
}
value={idfields.find(
idField => idField.id === field.fk_target_field_id,
)}
options={idfields}
optionNameFn={idField =>
includeSchemaName
? titleize(humanize(idField.table.schema)) +
"." +
idField.displayName
: idField.displayName
optionNameFn={field =>
field.displayName({ includeTable: true, includeSchema })
}
onChange={this.onTargetChange}
/>
......
......@@ -5,13 +5,13 @@ import ColumnItem from "./ColumnItem.jsx";
export default class ColumnsList extends Component {
static propTypes = {
tableMetadata: PropTypes.object,
fields: PropTypes.array,
idfields: PropTypes.array,
updateField: PropTypes.func.isRequired,
};
render() {
let { tableMetadata } = this.props;
const { fields = [] } = this.props;
return (
<div id="ColumnsList" className="my3">
<h2 className="px1 text-orange">{t`Columns`}</h2>
......@@ -26,12 +26,12 @@ export default class ColumnsList extends Component {
</div>
</div>
<ol className="border-top border-bottom">
{tableMetadata.fields.map(field => (
{fields.map(field => (
<ColumnItem
key={field.id}
field={field}
idfields={this.props.idfields}
updateField={this.props.updateField}
idfields={this.props.idfields}
/>
))}
</ol>
......
......@@ -7,8 +7,10 @@ import Toggle from "metabase/components/Toggle.jsx";
import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx";
import ColumnarSelector from "metabase/components/ColumnarSelector.jsx";
import Icon from "metabase/components/Icon.jsx";
import Databases from "metabase/entities/databases";
@withRouter
@Databases.loadList()
export default class MetadataHeader extends Component {
static propTypes = {
databaseId: PropTypes.number,
......@@ -18,6 +20,21 @@ export default class MetadataHeader extends Component {
toggleShowSchema: PropTypes.func.isRequired,
};
setDatabaseIdIfUnset() {
const { databaseId, databases = [], selectDatabase } = this.props;
if (databaseId === undefined && databases.length > 0) {
selectDatabase(databases[0]);
}
}
componentDidUpdate() {
this.setDatabaseIdIfUnset();
}
componentWillMount() {
this.setDatabaseIdIfUnset();
}
setSaving() {
this.refs.status.setSaving.apply(this, arguments);
}
......
import React, { Component } from "react";
import PropTypes from "prop-types";
import { t } from "ttag";
import withTableMetadataLoaded from "metabase/admin/datamodel/withTableMetadataLoaded";
import Tables from "metabase/entities/tables";
@Tables.load({ id: (state, { tableId }) => tableId, wrapped: true })
@withTableMetadataLoaded
export default class MetadataSchema extends Component {
static propTypes = {
tableMetadata: PropTypes.object,
};
render() {
const { tableMetadata } = this.props;
if (!tableMetadata) {
const { table } = this.props;
if (!table || !table.fields) {
return false;
}
const tdClassName = "py2 px1 border-bottom";
let fields = tableMetadata.fields.map(field => {
const fields = table.fields.map(field => {
return (
<tr key={field.id}>
<td className={tdClassName}>
......@@ -34,9 +38,7 @@ export default class MetadataSchema extends Component {
return (
<div className="MetadataTable px2 full">
<div className="flex flex-column px1">
<div className="TableEditor-table-name text-bold">
{tableMetadata.name}
</div>
<div className="TableEditor-table-name text-bold">{table.name}</div>
</div>
<table className="mt2 full">
<thead className="text-uppercase text-medium py1">
......
......@@ -6,13 +6,20 @@ import ColumnsList from "./ColumnsList.jsx";
import SegmentsList from "./SegmentsList.jsx";
import { t } from "ttag";
import InputBlurChange from "metabase/components/InputBlurChange.jsx";
import ProgressBar from "metabase/components/ProgressBar.jsx";
import { normal } from "metabase/lib/colors";
import Databases from "metabase/entities/databases";
import Tables from "metabase/entities/tables";
import withTableMetadataLoaded from "metabase/admin/datamodel/withTableMetadataLoaded";
import _ from "underscore";
import cx from "classnames";
@Databases.load({ id: (state, { databaseId }) => databaseId, wrapped: true })
@Tables.load({
id: (state, { tableId }) => tableId,
wrapped: true,
selectorName: "getTable",
})
@withTableMetadataLoaded
export default class MetadataTable extends Component {
constructor(props, context) {
super(props, context);
......@@ -22,20 +29,34 @@ export default class MetadataTable extends Component {
}
static propTypes = {
tableMetadata: PropTypes.object,
idfields: PropTypes.array.isRequired,
updateTable: PropTypes.func.isRequired,
table: PropTypes.object,
idfields: PropTypes.array,
updateField: PropTypes.func.isRequired,
onRetireMetric: PropTypes.func.isRequired,
onRetireSegment: PropTypes.func.isRequired,
};
componentWillMount() {
const { database } = this.props;
if (database) {
database.fetchIdfields();
}
}
componentDidUpdate({ database: { id: prevId } = {} }) {
const { database = {} } = this.props;
if (database.id !== prevId) {
database.fetchIdfields();
}
}
isHidden() {
return !!this.props.tableMetadata.visibility_type;
return !!this.props.table.visibility_type;
}
updateProperty(name, value) {
this.props.tableMetadata[name] = value;
this.setState({ saving: true });
this.props.updateTable(this.props.tableMetadata);
this.props.table.update({ [name]: value });
}
onNameChange(event) {
......@@ -43,7 +64,7 @@ export default class MetadataTable extends Component {
this.updateProperty("display_name", event.target.value);
} else {
// if the user set this to empty then simply reset it because that's not allowed!
event.target.value = this.props.tableMetadata.display_name;
event.target.value = this.props.table.display_name;
}
}
......@@ -60,8 +81,8 @@ export default class MetadataTable extends Component {
"text-default",
{
"text-brand":
this.props.tableMetadata.visibility_type === type ||
(any && this.props.tableMetadata.visibility_type),
this.props.table.visibility_type === type ||
(any && this.props.table.visibility_type),
},
);
return (
......@@ -76,7 +97,7 @@ export default class MetadataTable extends Component {
renderVisibilityWidget() {
let subTypes;
if (this.props.tableMetadata.visibility_type) {
if (this.props.table.visibility_type) {
subTypes = (
<span id="VisibilitySubTypes" className="border-left mx2">
<span className="mx2 text-uppercase text-medium">{t`Why Hide?`}</span>
......@@ -95,8 +116,8 @@ export default class MetadataTable extends Component {
}
render() {
const { tableMetadata } = this.props;
if (!tableMetadata) {
const { table, onRetireMetric, onRetireSegment } = this.props;
if (!table) {
return false;
}
......@@ -106,13 +127,13 @@ export default class MetadataTable extends Component {
<InputBlurChange
className="AdminInput TableEditor-table-name text-bold border-bottom rounded-top"
type="text"
value={tableMetadata.display_name || ""}
value={table.display_name || ""}
onBlurChange={this.onNameChange}
/>
<InputBlurChange
className="AdminInput TableEditor-table-description rounded-bottom"
type="text"
value={tableMetadata.description || ""}
value={table.description || ""}
onBlurChange={this.onDescriptionChange}
placeholder={t`No table description yet`}
/>
......@@ -120,30 +141,17 @@ export default class MetadataTable extends Component {
<div className="MetadataTable-header flex align-center py2 text-medium">
<span className="mx1 text-uppercase">{t`Visibility`}</span>
{this.renderVisibilityWidget()}
<span className="flex-align-right flex align-center">
<span className="text-uppercase mr1">{t`Metadata Strength`}</span>
<span style={{ width: 64 }}>
<ProgressBar
percentage={tableMetadata.metadataStrength}
color={normal.grey2}
/>
</span>
</span>
</div>
<div className={"mt2 " + (this.isHidden() ? "disabled" : "")}>
<SegmentsList
tableMetadata={tableMetadata}
onRetire={this.props.onRetireSegment}
/>
<MetricsList
tableMetadata={tableMetadata}
onRetire={this.props.onRetireMetric}
/>
<ColumnsList
tableMetadata={tableMetadata}
idfields={this.props.idfields}
updateField={this.props.updateField}
/>
<SegmentsList onRetire={onRetireSegment} tableMetadata={table} />
<MetricsList onRetire={onRetireMetric} tableMetadata={table} />
{this.props.idfields && (
<ColumnsList
fields={table.fields}
updateField={this.props.updateField}
idfields={this.props.idfields}
/>
)}
</div>
</div>
);
......
import React, { Component } from "react";
import PropTypes from "prop-types";
import ProgressBar from "metabase/components/ProgressBar.jsx";
import Icon from "metabase/components/Icon.jsx";
import { t, ngettext, msgid } from "ttag";
import { normal } from "metabase/lib/colors";
import _ from "underscore";
import cx from "classnames";
......@@ -56,12 +53,6 @@ export default class MetadataTableList extends Component {
onClick={this.props.selectTable.bind(null, table)}
>
{table.display_name}
<span className="flex-align-right" style={{ width: 17 }}>
<ProgressBar
percentage={table.metadataStrength}
color={selected ? normal.grey2 : normal.grey1}
/>
</span>
</a>
</li>
);
......
......@@ -4,8 +4,11 @@ import PropTypes from "prop-types";
import MetadataTableList from "./MetadataTableList.jsx";
import MetadataSchemaList from "./MetadataSchemaList.jsx";
import Tables from "metabase/entities/tables";
import { titleize, humanize } from "metabase/lib/formatting";
@Tables.loadList()
export default class MetadataTablePicker extends Component {
constructor(props, context) {
super(props, context);
......@@ -19,7 +22,7 @@ export default class MetadataTablePicker extends Component {
static propTypes = {
tableId: PropTypes.number,
tables: PropTypes.array.isRequired,
databaseId: PropTypes.number,
selectTable: PropTypes.func.isRequired,
};
......@@ -27,8 +30,8 @@ export default class MetadataTablePicker extends Component {
this.componentWillReceiveProps(this.props);
}
componentWillReceiveProps(newProps) {
const { tables } = newProps;
componentWillReceiveProps({ tables: allTables, databaseId, tableId }) {
const tables = allTables.filter(({ db_id }) => db_id === databaseId);
let schemas = {};
let selectedSchema;
for (let table of tables) {
......@@ -38,7 +41,7 @@ export default class MetadataTablePicker extends Component {
tables: [],
};
schemas[name].tables.push(table);
if (table.id === newProps.tableId) {
if (table.id === tableId) {
selectedSchema = schemas[name];
}
}
......
......@@ -9,10 +9,11 @@ export default class MetricItem extends Component {
static propTypes = {
metric: PropTypes.object.isRequired,
onRetire: PropTypes.func.isRequired,
tableMetadata: PropTypes.object.isRequired,
};
render() {
let { metric, tableMetadata } = this.props;
let { metric, onRetire, tableMetadata } = this.props;
let description = Query.generateQueryDescription(
tableMetadata,
......@@ -28,7 +29,7 @@ export default class MetricItem extends Component {
<ObjectActionSelect
object={metric}
objectType="metric"
onRetire={this.props.onRetire}
onRetire={onRetire}
/>
</td>
</tr>
......
......@@ -11,12 +11,8 @@ export default class MetricsList extends Component {
};
render() {
let { tableMetadata } = this.props;
tableMetadata.metrics = tableMetadata.metrics || [];
tableMetadata.metrics = tableMetadata.metrics.filter(
mtrc => mtrc.archived === false,
);
const { onRetire, tableMetadata } = this.props;
const { metrics = [] } = tableMetadata;
return (
<div id="MetricsList" className="my3">
......@@ -39,17 +35,17 @@ export default class MetricsList extends Component {
</tr>
</thead>
<tbody>
{tableMetadata.metrics.map(metric => (
{metrics.map(metric => (
<MetricItem
key={metric.id}
metric={metric}
onRetire={onRetire}
tableMetadata={tableMetadata}
onRetire={this.props.onRetire}
/>
))}
</tbody>
</table>
{tableMetadata.metrics.length === 0 && (
{metrics.length === 0 && (
<div className="flex layout-centered m4 text-medium">
{t`Create metrics to add them to the View dropdown in the query builder`}
</div>
......
......@@ -7,15 +7,15 @@ import Query from "metabase/lib/query";
export default class SegmentItem extends Component {
static propTypes = {
onRetire: PropTypes.func.isRequired,
segment: PropTypes.object.isRequired,
tableMetadata: PropTypes.object.isRequired,
onRetire: PropTypes.func.isRequired,
};
render() {
let { segment, tableMetadata } = this.props;
const { onRetire, segment, tableMetadata } = this.props;
let description = Query.generateQueryDescription(
const description = Query.generateQueryDescription(
tableMetadata,
segment.definition,
{ sections: ["filter"], jsx: true },
......@@ -39,7 +39,7 @@ export default class SegmentItem extends Component {
<ObjectActionSelect
object={segment}
objectType="segment"
onRetire={this.props.onRetire}
onRetire={onRetire}
/>
</td>
</tr>
......
......@@ -11,12 +11,8 @@ export default class SegmentsList extends Component {
};
render() {
let { tableMetadata } = this.props;
tableMetadata.segments = tableMetadata.segments || [];
tableMetadata.segments = tableMetadata.segments.filter(
sgmt => sgmt.archived === false,
);
const { onRetire, tableMetadata } = this.props;
const { segments = [] } = tableMetadata;
return (
<div id="SegmentsList" className="my3">
......@@ -39,17 +35,17 @@ export default class SegmentsList extends Component {
</tr>
</thead>
<tbody>
{tableMetadata.segments.map(segment => (
{segments.map(segment => (
<SegmentItem
key={segment.id}
onRetire={onRetire}
segment={segment}
tableMetadata={tableMetadata}
onRetire={this.props.onRetire}
/>
))}
</tbody>
</table>
{tableMetadata.segments.length === 0 && (
{segments.length === 0 && (
<div className="flex layout-centered m4 text-medium">
{t`Create segments to add them to the Filter dropdown in the query builder`}
</div>
......
......@@ -5,18 +5,20 @@ import Revision from "./Revision.jsx";
import { t } from "ttag";
import Breadcrumbs from "metabase/components/Breadcrumbs.jsx";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
import Tables from "metabase/entities/tables";
import { assignUserColors } from "metabase/lib/formatting";
@Tables.load({ id: (state, { object: { table_id } }) => table_id })
export default class RevisionHistory extends Component {
static propTypes = {
object: PropTypes.object,
revisions: PropTypes.array,
tableMetadata: PropTypes.object,
table: PropTypes.object,
};
render() {
const { object, revisions, tableMetadata, user } = this.props;
const { object, revisions, table, user } = this.props;
let userColorAssignments = {};
if (revisions) {
......@@ -35,10 +37,7 @@ export default class RevisionHistory extends Component {
crumbs={[
[
t`Datamodel`,
"/admin/datamodel/database/" +
tableMetadata.db_id +
"/table/" +
tableMetadata.id,
`/admin/datamodel/database/${table.db_id}/table/${table.id}`,
],
[this.props.objectType + t` History`],
]}
......@@ -51,9 +50,9 @@ export default class RevisionHistory extends Component {
{revisions.map(revision => (
<Revision
revision={revision}
objectName={name}
objectName={object.name}
currentUser={user}
tableMetadata={tableMetadata}
tableMetadata={table}
userColor={userColorAssignments[revision.user.id]}
/>
))}
......
......@@ -40,11 +40,8 @@ import ColumnSettings from "metabase/visualizations/components/ColumnSettings";
// SELECTORS
import { getMetadata } from "metabase/selectors/metadata";
import { getDatabaseIdfields } from "metabase/admin/datamodel/selectors";
// ACTIONS
import * as metadataActions from "metabase/redux/metadata";
import * as datamodelActions from "../datamodel";
import { rescanFieldValues, discardFieldValues } from "../field";
// LIB
......@@ -58,26 +55,29 @@ import type { ColumnSettings as ColumnSettingsType } from "metabase/meta/types/D
import type { DatabaseId } from "metabase/meta/types/Database";
import type { TableId } from "metabase/meta/types/Table";
import type { FieldId } from "metabase/meta/types/Field";
import Databases from "metabase/entities/databases";
import Tables from "metabase/entities/tables";
import Fields from "metabase/entities/fields";
const mapStateToProps = (state, props) => {
const databaseId = parseInt(props.params.databaseId);
return {
databaseId: parseInt(props.params.databaseId),
databaseId,
tableId: parseInt(props.params.tableId),
fieldId: parseInt(props.params.fieldId),
metadata: getMetadata(state),
idfields: getDatabaseIdfields(state),
idfields: Databases.selectors.getIdfields(state, { databaseId }),
};
};
const mapDispatchToProps = {
fetchDatabaseMetadata: metadataActions.fetchDatabaseMetadata,
fetchTableMetadata: metadataActions.fetchTableMetadata,
fetchFieldValues: metadataActions.fetchFieldValues,
updateField: metadataActions.updateField,
updateFieldValues: metadataActions.updateFieldValues,
updateFieldDimension: metadataActions.updateFieldDimension,
deleteFieldDimension: metadataActions.deleteFieldDimension,
fetchDatabaseIdfields: datamodelActions.fetchDatabaseIdfields,
fetchDatabaseMetadata: Databases.actions.fetchDatabaseMetadata,
fetchTableMetadata: Tables.actions.fetchTableMetadata,
fetchFieldValues: Fields.actions.fetchFieldValues,
updateField: Fields.actions.update,
updateFieldValues: Fields.actions.updateFieldValues,
updateFieldDimension: Fields.actions.updateFieldDimension,
deleteFieldDimension: Fields.actions.deleteFieldDimension,
rescanFieldValues,
discardFieldValues,
};
......@@ -100,14 +100,13 @@ export default class FieldApp extends React.Component {
metadata: Metadata,
idfields: Object[],
fetchDatabaseMetadata: number => Promise<void>,
fetchTableMetadata: number => Promise<void>,
fetchFieldValues: number => Promise<void>,
fetchDatabaseMetadata: Object => Promise<void>,
fetchTableMetadata: Object => Promise<void>,
fetchFieldValues: Object => Promise<void>,
updateField: any => Promise<void>,
updateFieldValues: any => Promise<void>,
updateFieldDimension: (FieldId, any) => Promise<void>,
deleteFieldDimension: FieldId => Promise<void>,
fetchDatabaseIdfields: DatabaseId => Promise<void>,
updateFieldDimension: (Object, any) => Promise<void>,
deleteFieldDimension: Object => Promise<void>,
rescanFieldValues: FieldId => Promise<void>,
discardFieldValues: FieldId => Promise<void>,
......@@ -124,38 +123,30 @@ export default class FieldApp extends React.Component {
fieldId,
fetchDatabaseMetadata,
fetchTableMetadata,
fetchDatabaseIdfields,
fetchFieldValues,
} = this.props;
// A complete database metadata is needed in case that foreign key is changed
// and then we need to show FK remapping options for a new table
await fetchDatabaseMetadata(databaseId);
await Promise.all([
// A complete database metadata is needed in case that foreign key is
// changed and then we need to show FK remapping options for a new table
fetchDatabaseMetadata({ id: databaseId }),
// Only fetchTableMetadata hydrates `dimension` in the field object
// Force reload to ensure that we are not showing stale information
await fetchTableMetadata(tableId, true);
// Only fetchTableMetadata hydrates `dimension` in the field object
// Force reload to ensure that we are not showing stale information
fetchTableMetadata({ id: tableId }, { reload: true }),
// load field values if has_field_values === "list"
const field = this.props.metadata.field(fieldId);
if (field && field.has_field_values === "list") {
await fetchFieldValues(fieldId);
}
// TODO Atte Keinänen 7/10/17: Migrate this to redux/metadata
await fetchDatabaseIdfields(databaseId);
// always load field values even though it's only needed if
// has_field_values === "list"
fetchFieldValues({ id: fieldId }),
]);
}
linkWithSaveStatus = (saveMethod: Function) => {
const self = this;
return async (...args: any[]) => {
self.saveStatus && self.saveStatus.setSaving();
await saveMethod(...args);
self.saveStatus && self.saveStatus.setSaved();
};
linkWithSaveStatus = (saveMethod: Function) => async (...args: any[]) => {
this.saveStatus && this.saveStatus.setSaving();
await saveMethod(...args);
this.saveStatus && this.saveStatus.setSaved();
};
onUpdateField = this.linkWithSaveStatus(this.props.updateField);
onUpdateFieldProperties = this.linkWithSaveStatus(async fieldProps => {
const { metadata, fieldId } = this.props;
const field = metadata.fields[fieldId];
......@@ -254,7 +245,6 @@ export default class FieldApp extends React.Component {
idfields={idfields}
table={table}
metadata={metadata}
onUpdateField={this.onUpdateField}
onUpdateFieldValues={this.onUpdateFieldValues}
onUpdateFieldProperties={this.onUpdateFieldProperties}
onUpdateFieldDimension={this.onUpdateFieldDimension}
......@@ -286,7 +276,6 @@ const FieldGeneralPane = ({
idfields,
table,
metadata,
onUpdateField,
onUpdateFieldValues,
onUpdateFieldProperties,
onUpdateFieldDimension,
......@@ -312,7 +301,7 @@ const FieldGeneralPane = ({
<div style={{ maxWidth: 400 }}>
<FieldVisibilityPicker
field={field.getPlainObject()}
updateField={onUpdateField}
updateField={onUpdateFieldProperties}
/>
</div>
</Section>
......@@ -321,7 +310,7 @@ const FieldGeneralPane = ({
<SectionHeader title={t`Field Type`} />
<SpecialTypeAndTargetPicker
field={field.getPlainObject()}
updateField={onUpdateField}
updateField={onUpdateFieldProperties}
idfields={idfields}
selectSeparator={<SelectSeparator />}
/>
......@@ -424,11 +413,14 @@ export class FieldHeader extends React.Component {
// Update the dimension name if it exists
// TODO: Have a separate input field for the dimension name?
if (!_.isEmpty(field.dimensions)) {
await updateFieldDimension(field.id, {
type: field.dimensions.type,
human_readable_field_id: field.dimensions.human_readable_field_id,
name,
});
await updateFieldDimension(
{ id: field.id },
{
type: field.dimensions.type,
human_readable_field_id: field.dimensions.human_readable_field_id,
name,
},
);
}
// todo: how to treat empty / too long strings? see how this is done in Column
......
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { push } from "react-router-redux";
import _ from "underscore";
import { t } from "ttag";
import MetabaseAnalytics from "metabase/lib/analytics";
......@@ -11,31 +11,34 @@ import MetadataHeader from "../components/database/MetadataHeader.jsx";
import MetadataTablePicker from "../components/database/MetadataTablePicker.jsx";
import MetadataTable from "../components/database/MetadataTable.jsx";
import MetadataSchema from "../components/database/MetadataSchema.jsx";
import {
getDatabases,
getDatabaseIdfields,
getEditingDatabaseWithTableMetadataStrengths,
getEditingTable,
} from "../selectors";
import * as metadataActions from "../datamodel";
metrics as Metrics,
segments as Segments,
databases as Databases,
fields as Fields,
} from "metabase/entities";
const mapStateToProps = (state, props) => {
const mapStateToProps = (state, { params }) => {
const databaseId = params.databaseId
? parseInt(params.databaseId)
: undefined;
const tableId = params.tableId ? parseInt(params.tableId) : undefined;
return {
databaseId: parseInt(props.params.databaseId),
tableId: parseInt(props.params.tableId),
databases: getDatabases(state, props),
idfields: getDatabaseIdfields(state, props),
databaseMetadata: getEditingDatabaseWithTableMetadataStrengths(
state,
props,
),
editingTable: getEditingTable(state, props),
databaseId,
tableId,
idfields: Databases.selectors.getIdfields(state, { databaseId }),
};
};
const mapDispatchToProps = {
...metadataActions,
selectDatabase: ({ id }) => push("/admin/datamodel/database/" + id),
selectTable: ({ id, db_id }) =>
push(`/admin/datamodel/database/${db_id}/table/${id}`),
updateField: field => Fields.actions.update(field),
onRetireMetric: ({ id, ...rest }) =>
Metrics.actions.setArchived({ id }, true, rest),
onRetireSegment: ({ id, ...rest }) =>
Segments.actions.setArchived({ id }, true, rest),
};
@connect(
......@@ -55,21 +58,14 @@ export default class MetadataEditor extends Component {
static propTypes = {
databaseId: PropTypes.number,
tableId: PropTypes.number,
databases: PropTypes.array.isRequired,
selectDatabase: PropTypes.func.isRequired,
databaseMetadata: PropTypes.object,
selectTable: PropTypes.func.isRequired,
idfields: PropTypes.array.isRequired,
editingTable: PropTypes.number,
updateTable: PropTypes.func.isRequired,
idfields: PropTypes.array,
updateField: PropTypes.func.isRequired,
onRetireMetric: PropTypes.func.isRequired,
onRetireSegment: PropTypes.func.isRequired,
};
componentWillMount() {
// if we know what database we are initialized with, include that
this.props.initializeMetadata(this.props.databaseId, this.props.tableId);
}
toggleShowSchema() {
this.setState({ isShowingSchema: !this.state.isShowingSchema });
MetabaseAnalytics.trackEvent(
......@@ -80,44 +76,12 @@ export default class MetadataEditor extends Component {
}
render() {
let tableMetadata = this.props.databaseMetadata
? _.findWhere(this.props.databaseMetadata.tables, {
id: this.props.editingTable,
})
: null;
let content;
if (tableMetadata) {
if (this.state.isShowingSchema) {
content = <MetadataSchema tableMetadata={tableMetadata} />;
} else {
content = (
<MetadataTable
tableMetadata={tableMetadata}
idfields={this.props.idfields}
updateTable={table => this.props.updateTable(table)}
updateField={field => this.props.updateField(field)}
onRetireSegment={this.props.onRetireSegment}
onRetireMetric={this.props.onRetireMetric}
/>
);
}
} else {
content = (
<div style={{ paddingTop: "10rem" }} className="full text-centered">
<AdminEmptyText
message={t`Select any table to see its schema and add or edit metadata.`}
/>
</div>
);
}
const { databaseId, tableId } = this.props;
return (
<div className="p3">
<MetadataHeader
ref="header"
databaseId={
this.props.databaseMetadata ? this.props.databaseMetadata.id : null
}
databases={this.props.databases}
databaseId={databaseId}
selectDatabase={this.props.selectDatabase}
isShowingSchema={this.state.isShowingSchema}
toggleShowSchema={this.toggleShowSchema}
......@@ -126,16 +90,33 @@ export default class MetadataEditor extends Component {
style={{ minHeight: "60vh" }}
className="flex flex-row flex-full mt2 full-height"
>
<MetadataTablePicker
tableId={this.props.editingTable}
tables={
this.props.databaseMetadata
? this.props.databaseMetadata.tables
: []
}
selectTable={this.props.selectTable}
/>
{content}
{databaseId && (
<MetadataTablePicker
tableId={tableId}
databaseId={databaseId}
selectTable={this.props.selectTable}
/>
)}
{tableId ? (
this.state.isShowingSchema ? (
<MetadataSchema tableId={tableId} />
) : (
<MetadataTable
tableId={tableId}
databaseId={databaseId}
idfields={this.props.idfields}
updateField={this.props.updateField}
onRetireMetric={this.props.onRetireMetric}
onRetireSegment={this.props.onRetireSegment}
/>
)
) : (
<div style={{ paddingTop: "10rem" }} className="full text-centered">
<AdminEmptyText
message={t`Select any table to see its schema and add or edit metadata.`}
/>
</div>
)}
</div>
</div>
);
......
......@@ -3,76 +3,77 @@ import { connect } from "react-redux";
import { push } from "react-router-redux";
import MetabaseAnalytics from "metabase/lib/analytics";
import { getMetadata } from "metabase/selectors/metadata";
import Metrics from "metabase/entities/metrics";
import Tables from "metabase/entities/tables";
import { updatePreviewSummary } from "../datamodel";
import { getPreviewSummary } from "../selectors";
import withTableMetadataLoaded from "../withTableMetadataLoaded";
import MetricForm from "./MetricForm.jsx";
import { metricEditSelectors } from "../selectors";
import * as actions from "../datamodel";
import { clearRequestState } from "metabase/redux/requests";
import { fetchTableMetadata } from "metabase/redux/metadata";
import { getMetadata } from "metabase/selectors/metadata";
const mapDispatchToProps = {
...actions,
fetchTableMetadata,
clearRequestState,
updatePreviewSummary,
createMetric: Metrics.actions.create,
onChangeLocation: push,
};
const mapStateToProps = (state, props) => ({
...metricEditSelectors(state, props),
metadata: getMetadata(state, props),
metadata: getMetadata(state),
previewSummary: getPreviewSummary(state),
});
@connect(
mapStateToProps,
mapDispatchToProps,
)
export default class MetricApp extends Component {
async componentWillMount() {
const { params, location } = this.props;
let tableId;
if (params.id) {
const metricId = parseInt(params.id);
const { payload: metric } = await this.props.getMetric({ metricId });
tableId = metric.table_id;
} else if (location.query.table) {
tableId = parseInt(location.query.table);
}
@Metrics.load({
id: (state, props) => parseInt(props.params.id),
wrapped: true,
})
@Tables.load({ id: (state, props) => props.metric.table_id, wrapped: true })
@withTableMetadataLoaded
class UpdateMetricForm extends Component {
onSubmit = async metric => {
await this.props.metric.update(metric);
MetabaseAnalytics.trackEvent("Data Model", "Metric Updated");
const { id: tableId, db_id: databaseId } = this.props.table;
this.props.onChangeLocation(
`/admin/datamodel/database/${databaseId}/table/${tableId}`,
);
};
if (tableId != null) {
// TODO Atte Keinänen 6/8/17: Use only global metadata (`fetchTableMetadata`)
this.props.loadTableMetadata(tableId);
this.props.fetchTableMetadata(tableId);
}
render() {
return <MetricForm {...this.props} onSubmit={this.onSubmit} />;
}
}
async onSubmit(metric, f) {
let { tableMetadata } = this.props;
if (metric.id != null) {
await this.props.updateMetric(metric);
this.props.clearRequestState({ statePath: ["entities", "metrics"] });
MetabaseAnalytics.trackEvent("Data Model", "Metric Updated");
} else {
await this.props.createMetric(metric);
this.props.clearRequestState({ statePath: ["entities", "metrics"] });
MetabaseAnalytics.trackEvent("Data Model", "Metric Created");
}
@Tables.load({
id: (state, props) => parseInt(props.location.query.table),
wrapped: true,
})
@withTableMetadataLoaded
class CreateMetricForm extends Component {
onSubmit = async metric => {
const { id: tableId, db_id: databaseId } = this.props.table;
await this.props.createMetric({ ...metric, table_id: tableId });
MetabaseAnalytics.trackEvent("Data Model", "Metric Updated");
this.props.onChangeLocation(
"/admin/datamodel/database/" +
tableMetadata.db_id +
"/table/" +
tableMetadata.id,
`/admin/datamodel/database/${databaseId}/table/${tableId}`,
);
};
render() {
return <MetricForm {...this.props} onSubmit={this.onSubmit} />;
}
}
@connect(
mapStateToProps,
mapDispatchToProps,
)
export default class MetricApp extends Component {
render() {
return (
<div>
<MetricForm {...this.props} onSubmit={this.onSubmit.bind(this)} />
</div>
return this.props.params.id ? (
<UpdateMetricForm {...this.props} />
) : (
<CreateMetricForm {...this.props} />
);
}
}
......@@ -10,7 +10,6 @@ import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.j
import { t } from "ttag";
import { formatValue } from "metabase/lib/formatting";
import { metricFormSelectors } from "../selectors";
import { reduxForm } from "redux-form";
import Query from "metabase/lib/query";
......@@ -52,7 +51,7 @@ import Table from "metabase-lib/lib/metadata/Table";
return errors;
},
},
(state, props) => metricFormSelectors(state, props),
(state, { metric }) => ({ initialValues: metric }),
)
export default class MetricForm extends Component {
updatePreviewSummary(datasetQuery) {
......@@ -66,7 +65,11 @@ export default class MetricForm extends Component {
}
renderActionButtons() {
const { invalid, handleSubmit, tableMetadata } = this.props;
const {
invalid,
handleSubmit,
table: { db_id: databaseId, id: tableId },
} = this.props;
return (
<div>
<button
......@@ -77,12 +80,7 @@ export default class MetricForm extends Component {
onClick={handleSubmit}
>{t`Save changes`}</button>
<Link
to={
"/admin/datamodel/database/" +
tableMetadata.db_id +
"/table/" +
tableMetadata.id
}
to={`/admin/datamodel/database/${databaseId}/table/${tableId}`}
className="Button ml2"
>{t`Cancel`}</Link>
</div>
......@@ -94,13 +92,13 @@ export default class MetricForm extends Component {
fields: { id, name, description, definition, revision_message },
metric,
metadata,
tableMetadata,
table,
handleSubmit,
previewSummary,
} = this.props;
return (
<LoadingAndErrorWrapper loading={!tableMetadata}>
<LoadingAndErrorWrapper loading={!table && !table.aggregation_options}>
{() => (
<form className="full" onSubmit={handleSubmit}>
<div className="wrapper py4">
......@@ -123,26 +121,26 @@ export default class MetricForm extends Component {
}}
metadata={
metadata &&
tableMetadata &&
table &&
metadata.tables &&
metadata.tables[tableMetadata.id].fields &&
metadata.tables[table.id].fields &&
Object.assign(new Metadata(), metadata, {
tables: {
...metadata.tables,
[tableMetadata.id]: Object.assign(
[table.id]: Object.assign(
new Table(),
metadata.tables[tableMetadata.id],
metadata.tables[table.id],
{
aggregation_options: tableMetadata.aggregation_options.filter(
a => a.short !== "rows",
),
aggregation_options: (
table.aggregation_options || []
).filter(a => a.short !== "rows"),
metrics: [],
},
),
},
})
}
tableMetadata={tableMetadata}
tableMetadata={table}
previewSummary={
previewSummary == null
? ""
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment