Skip to content
Snippets Groups Projects
Commit e75b706f authored by Tom Robinson's avatar Tom Robinson
Browse files

Split up massive FieldApp file

parent 405c0c23
Branches
Tags
No related merge requests found
import React from "react";
import { t } from "c-3po";
import _ from "underscore";
import cx from "classnames";
import SelectButton from "metabase/components/SelectButton";
import Select from "metabase/components/Select";
import PopoverWithTrigger from "metabase/components/PopoverWithTrigger";
import FieldList from "metabase/query_builder/components/FieldList";
import InputBlurChange from "metabase/components/InputBlurChange";
import ButtonWithStatus from "metabase/components/ButtonWithStatus";
import SelectSeparator from "../components/SelectSeparator";
import MetabaseAnalytics from "metabase/lib/analytics";
import { DatetimeFieldDimension } from "metabase-lib/lib/Dimension";
import Question from "metabase-lib/lib/Question";
const MAP_OPTIONS = {
original: { type: "original", name: t`Use original value` },
foreign: { type: "foreign", name: t`Use foreign key` },
custom: { type: "custom", name: t`Custom mapping` },
};
export default class FieldRemapping extends React.Component {
state = {
isChoosingInitialFkTarget: false,
dismissedInitialFkTargetPopover: false,
};
constructor(props, context) {
super(props, context);
}
getMappingTypeForField = field => {
if (this.state.isChoosingInitialFkTarget) {
return MAP_OPTIONS.foreign;
}
if (_.isEmpty(field.dimensions)) {
return MAP_OPTIONS.original;
}
if (field.dimensions.type === "external") {
return MAP_OPTIONS.foreign;
}
if (field.dimensions.type === "internal") {
return MAP_OPTIONS.custom;
}
throw new Error(t`Unrecognized mapping type`);
};
getAvailableMappingTypes = () => {
const { field } = this.props;
const hasForeignKeys =
field.special_type === "type/FK" && this.getForeignKeys().length > 0;
// Only show the "custom" option if we have some values that can be mapped to user-defined custom values
// (for a field without user-defined remappings, every key of `field.remappings` has value `undefined`)
const hasMappableNumeralValues =
field.remapping.size > 0 &&
[...field.remapping.keys()].every(key => typeof key === "number");
return [
MAP_OPTIONS.original,
...(hasForeignKeys ? [MAP_OPTIONS.foreign] : []),
...(hasMappableNumeralValues > 0 ? [MAP_OPTIONS.custom] : []),
];
};
getFKTargetTableEntityNameOrNull = () => {
const fks = this.getForeignKeys();
const fkTargetFields = fks[0] && fks[0].dimensions.map(dim => dim.field());
if (fkTargetFields) {
// TODO Atte Keinänen 7/11/17: Should there be `isName(field)` in Field.js?
const nameField = fkTargetFields.find(
field => field.special_type === "type/Name",
);
return nameField ? nameField.id : null;
} else {
throw new Error(
t`Current field isn't a foreign key or FK target table metadata is missing`,
);
}
};
clearEditingStates = () => {
this.setState({
isChoosingInitialFkTarget: false,
dismissedInitialFkTargetPopover: false,
});
};
onSetMappingType = async mappingType => {
const {
table,
field,
fetchTableMetadata,
updateFieldDimension,
deleteFieldDimension,
} = this.props;
this.clearEditingStates();
if (mappingType.type === "original") {
MetabaseAnalytics.trackEvent(
"Data Model",
"Change Remapping Type",
"No Remapping",
);
await deleteFieldDimension(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
const entityNameFieldId = this.getFKTargetTableEntityNameOrNull();
if (entityNameFieldId) {
MetabaseAnalytics.trackEvent(
"Data Model",
"Change Remapping Type",
"Foreign Key",
);
await updateFieldDimension(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({
hasChanged: true,
isChoosingInitialFkTarget: true,
});
}
} else if (mappingType.type === "custom") {
MetabaseAnalytics.trackEvent(
"Data Model",
"Change Remapping Type",
"Custom Remappings",
);
await updateFieldDimension(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`);
}
// 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);
};
onForeignKeyFieldChange = async foreignKeyClause => {
const {
table,
field,
fetchTableMetadata,
updateFieldDimension,
} = this.props;
this.clearEditingStates();
// TODO Atte Keinänen 7/10/17: Use Dimension class when migrating to metabase-lib
if (foreignKeyClause.length === 3 && foreignKeyClause[0] === "fk->") {
MetabaseAnalytics.trackEvent("Data Model", "Update FK Remapping Target");
await updateFieldDimension(field.id, {
type: "external",
name: field.display_name,
human_readable_field_id: foreignKeyClause[2],
});
await fetchTableMetadata(table.id, true);
this.refs.fkPopover.close();
} else {
throw new Error(t`The selected field isn't a foreign key`);
}
};
onUpdateRemappings = remappings => {
const { field, updateFieldValues } = this.props;
return updateFieldValues(field.id, Array.from(remappings));
};
// TODO Atte Keinänen 7/11/17: Should we have stricter criteria for valid remapping targets?
isValidFKRemappingTarget = dimension =>
!(dimension.defaultDimension() instanceof DatetimeFieldDimension);
getForeignKeys = () => {
const { table, field } = this.props;
// this method has a little odd structure due to using fieldOptions(); basically filteredFKs should
// always be an array with a single value
const metadata = table.metadata;
const fieldOptions = Question.create({
metadata,
databaseId: table.db.id,
tableId: table.id,
})
.query()
.fieldOptions();
const unfilteredFks = fieldOptions.fks;
const filteredFKs = unfilteredFks.filter(fk => fk.field.id === field.id);
return filteredFKs.map(filteredFK => ({
field: filteredFK.field,
dimension: filteredFK.dimension,
dimensions: filteredFK.dimensions.filter(this.isValidFKRemappingTarget),
}));
};
onFkPopoverDismiss = () => {
const { isChoosingInitialFkTarget } = this.state;
if (isChoosingInitialFkTarget) {
this.setState({ dismissedInitialFkTargetPopover: true });
}
};
render() {
const { field, table, fields } = this.props;
const {
isChoosingInitialFkTarget,
hasChanged,
dismissedInitialFkTargetPopover,
} = this.state;
const mappingType = this.getMappingTypeForField(field);
const isFKMapping = mappingType === MAP_OPTIONS.foreign;
const hasFKMappingValue =
isFKMapping && field.dimensions.human_readable_field_id !== null;
const fkMappingField =
hasFKMappingValue && fields[field.dimensions.human_readable_field_id];
return (
<div>
<Select
value={mappingType}
onChange={this.onSetMappingType}
options={this.getAvailableMappingTypes()}
/>
{mappingType === MAP_OPTIONS.foreign && [
<SelectSeparator key="foreignKeySeparator" />,
<PopoverWithTrigger
ref="fkPopover"
triggerElement={
<SelectButton
hasValue={hasFKMappingValue}
className={cx("flex inline-block no-decoration", {
"border-error": dismissedInitialFkTargetPopover,
"border-dark": !dismissedInitialFkTargetPopover,
})}
>
{fkMappingField ? (
fkMappingField.display_name
) : (
<span className="text-medium">{t`Choose a field`}</span>
)}
</SelectButton>
}
isInitiallyOpen={isChoosingInitialFkTarget}
onClose={this.onFkPopoverDismiss}
>
<FieldList
className="text-purple"
field={fkMappingField}
fieldOptions={{
count: 0,
dimensions: [],
fks: this.getForeignKeys(),
}}
tableMetadata={table}
onFieldChange={this.onForeignKeyFieldChange}
hideSectionHeader
/>
</PopoverWithTrigger>,
dismissedInitialFkTargetPopover && (
<div className="text-error my2">{t`Please select a column to use for display.`}</div>
),
hasChanged && hasFKMappingValue && <RemappingNamingTip />,
]}
{mappingType === MAP_OPTIONS.custom && (
<div className="mt3">
{hasChanged && <RemappingNamingTip />}
<ValueRemappings
remappings={field && field.remapping}
updateRemappings={this.onUpdateRemappings}
/>
</div>
)}
</div>
);
}
}
// consider renaming this component to something more descriptive
export class ValueRemappings extends React.Component {
state = {
editingRemappings: new Map(),
};
componentWillMount() {
this._updateEditingRemappings(this.props.remappings);
}
componentWillReceiveProps(nextProps) {
if (nextProps.remappings !== this.props.remappings) {
this._updateEditingRemappings(nextProps.remappings);
}
}
_updateEditingRemappings(remappings) {
const editingRemappings = new Map(
[...remappings].map(([original, mappedOrUndefined]) => {
// Use currently the original value as the "default custom mapping" as the current backend implementation
// requires that all original values must have corresponding mappings
// Additionally, the defensive `.toString` ensures that the mapped value definitely will be string
const mappedString =
mappedOrUndefined !== undefined
? mappedOrUndefined.toString()
: original.toString();
return [original, mappedString];
}),
);
const containsUnsetMappings = [...remappings].some(
([_, mappedOrUndefined]) => {
return mappedOrUndefined === undefined;
},
);
if (containsUnsetMappings) {
// Save the initial values to make sure that we aren't left in a potentially broken state where
// the dimension type is "internal" but we don't have any values in metabase_fieldvalues
this.props.updateRemappings(editingRemappings);
}
this.setState({ editingRemappings });
}
onSetRemapping(original, newMapped) {
this.setState({
editingRemappings: new Map([
...this.state.editingRemappings,
[original, newMapped],
]),
});
}
onSaveClick = () => {
MetabaseAnalytics.trackEvent("Data Model", "Update Custom Remappings");
// Returns the promise so that ButtonWithStatus can show the saving status
return this.props.updateRemappings(this.state.editingRemappings);
};
customValuesAreNonEmpty = () => {
return Array.from(this.state.editingRemappings.values()).every(
value => value !== "",
);
};
render() {
const { editingRemappings } = this.state;
return (
<div className="bordered rounded py2 px4 border-dark">
<div className="flex align-center my1 pb2 border-bottom">
<h3>{t`Original value`}</h3>
<h3 className="ml-auto">{t`Mapped value`}</h3>
</div>
<ol>
{[...editingRemappings].map(([original, mapped]) => (
<li className="mb1">
<FieldValueMapping
original={original}
mapped={mapped}
setMapping={newMapped =>
this.onSetRemapping(original, newMapped)
}
/>
</li>
))}
</ol>
<div className="flex align-center">
<ButtonWithStatus
className="ml-auto"
disabled={!this.customValuesAreNonEmpty()}
onClickOperation={this.onSaveClick}
>
{t`Save`}
</ButtonWithStatus>
</div>
</div>
);
}
}
class FieldValueMapping extends React.Component {
onInputChange = e => {
this.props.setMapping(e.target.value);
};
render() {
const { original, mapped } = this.props;
return (
<div className="flex align-center">
<h3>{original}</h3>
<InputBlurChange
className="AdminInput input ml-auto"
value={mapped}
onChange={this.onInputChange}
placeholder={t`Enter value`}
/>
</div>
);
}
}
export const RemappingNamingTip = () => (
<div className="bordered rounded p1 mt1 mb2 border-brand">
<span className="text-brand text-bold">{t`Tip: `}</span>
{t`You might want to update the field name to make sure it still makes sense based on your remapping choices.`}
</div>
);
import React from "react";
const Section = ({ children }) => (
<section className="my4 pb4 border-bottom">{children}</section>
);
export const SectionHeader = ({ title, description }) => (
<div className="mb2">
<h4>{title}</h4>
{description && <p className="mb0 text-medium mt1">{description}</p>}
</div>
);
export default Section;
import React from "react";
import Icon from "metabase/components/Icon";
const SelectSeparator = () => (
<Icon name="chevronright" size={12} className="mx2 text-medium" />
);
export default SelectSeparator;
import React from "react";
import { t } from "c-3po";
import ActionButton from "metabase/components/ActionButton.jsx";
export default class UpdateCachedFieldValues extends React.Component {
render() {
return (
<div>
<ActionButton
className="Button mr2"
actionFn={this.props.rescanFieldValues}
normalText={t`Re-scan this field`}
activeText={t`Starting…`}
failedText={t`Failed to start scan`}
successText={t`Scan triggered!`}
/>
<ActionButton
className="Button Button--danger"
actionFn={this.props.discardFieldValues}
normalText={t`Discard cached field values`}
activeText={t`Starting…`}
failedText={t`Failed to discard values`}
successText={t`Discard triggered!`}
/>
</div>
);
}
}
......@@ -4,42 +4,41 @@
* TODO Atte Keinänen 7/6/17: This uses the standard metadata API; we should migrate also other parts of admin section
*/
import React, { Component } from "react";
import React from "react";
import { Link } from "react-router";
import { connect } from "react-redux";
import _ from "underscore";
import cx from "classnames";
import { t } from "c-3po";
import Icon from "metabase/components/Icon";
import InputBlurChange from "metabase/components/InputBlurChange";
import Select from "metabase/components/Select";
import SaveStatus from "metabase/components/SaveStatus";
import Breadcrumbs from "metabase/components/Breadcrumbs";
import Radio from "metabase/components/Radio";
import ButtonWithStatus from "metabase/components/ButtonWithStatus";
import MetabaseAnalytics from "metabase/lib/analytics";
import { getMetadata } from "metabase/selectors/metadata";
import * as metadataActions from "metabase/redux/metadata";
import * as datamodelActions from "../datamodel";
import ActionButton from "metabase/components/ActionButton.jsx";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
import SelectButton from "metabase/components/SelectButton";
import PopoverWithTrigger from "metabase/components/PopoverWithTrigger";
import FieldList from "metabase/query_builder/components/FieldList";
import {
FieldVisibilityPicker,
SpecialTypeAndTargetPicker,
} from "metabase/admin/datamodel/components/database/ColumnItem";
import Section, { SectionHeader } from "../components/Section";
import SelectSeparator from "../components/SelectSeparator";
import FieldRemapping from "../components/FieldRemapping";
import UpdateCachedFieldValues from "../components/UpdateCachedFieldValues";
import ColumnSettings from "metabase/visualizations/components/ColumnSettings";
import { getGlobalSettingsForColumn } from "metabase/visualizations/lib/settings/column";
import { getDatabaseIdfields } from "metabase/admin/datamodel/selectors";
import Metadata from "metabase-lib/lib/metadata/Metadata";
import Question from "metabase-lib/lib/Question";
import { DatetimeFieldDimension } from "metabase-lib/lib/Dimension";
import { rescanFieldValues, discardFieldValues } from "../field";
......@@ -70,7 +69,7 @@ const mapDispatchToProps = {
};
@connect(mapStateToProps, mapDispatchToProps)
export default class FieldApp extends Component {
export default class FieldApp extends React.Component {
state = {
tab: "general",
};
......@@ -262,6 +261,10 @@ export default class FieldApp extends Component {
</Section>
<Section>
<SectionHeader
title={t`Display values`}
description={t`Choose to show the original value from the database, or have this field display associated or custom information.`}
/>
<FieldRemapping
field={field}
table={table}
......@@ -275,6 +278,10 @@ export default class FieldApp extends Component {
</Section>
<Section>
<SectionHeader
title={t`Cached field values`}
description={t`Metabase can scan the values for this field to enable checkbox filters in dashboards and questions.`}
/>
<UpdateCachedFieldValues
rescanFieldValues={() =>
this.props.rescanFieldValues(field.id)
......@@ -314,11 +321,7 @@ export const BackButton = ({ databaseId, tableId }) => (
</Link>
);
const SelectSeparator = () => (
<Icon name="chevronright" size={12} className="mx2 text-medium" />
);
export class FieldHeader extends Component {
export class FieldHeader extends React.Component {
onNameChange = e => {
this.updateNameDebounced(e.target.value);
};
......@@ -368,459 +371,3 @@ export class FieldHeader extends Component {
);
}
}
// consider renaming this component to something more descriptive
export class ValueRemappings extends Component {
state = {
editingRemappings: new Map(),
};
componentWillMount() {
this._updateEditingRemappings(this.props.remappings);
}
componentWillReceiveProps(nextProps) {
if (nextProps.remappings !== this.props.remappings) {
this._updateEditingRemappings(nextProps.remappings);
}
}
_updateEditingRemappings(remappings) {
const editingRemappings = new Map(
[...remappings].map(([original, mappedOrUndefined]) => {
// Use currently the original value as the "default custom mapping" as the current backend implementation
// requires that all original values must have corresponding mappings
// Additionally, the defensive `.toString` ensures that the mapped value definitely will be string
const mappedString =
mappedOrUndefined !== undefined
? mappedOrUndefined.toString()
: original.toString();
return [original, mappedString];
}),
);
const containsUnsetMappings = [...remappings].some(
([_, mappedOrUndefined]) => {
return mappedOrUndefined === undefined;
},
);
if (containsUnsetMappings) {
// Save the initial values to make sure that we aren't left in a potentially broken state where
// the dimension type is "internal" but we don't have any values in metabase_fieldvalues
this.props.updateRemappings(editingRemappings);
}
this.setState({ editingRemappings });
}
onSetRemapping(original, newMapped) {
this.setState({
editingRemappings: new Map([
...this.state.editingRemappings,
[original, newMapped],
]),
});
}
onSaveClick = () => {
MetabaseAnalytics.trackEvent("Data Model", "Update Custom Remappings");
// Returns the promise so that ButtonWithStatus can show the saving status
return this.props.updateRemappings(this.state.editingRemappings);
};
customValuesAreNonEmpty = () => {
return Array.from(this.state.editingRemappings.values()).every(
value => value !== "",
);
};
render() {
const { editingRemappings } = this.state;
return (
<div className="bordered rounded py2 px4 border-dark">
<div className="flex align-center my1 pb2 border-bottom">
<h3>{t`Original value`}</h3>
<h3 className="ml-auto">{t`Mapped value`}</h3>
</div>
<ol>
{[...editingRemappings].map(([original, mapped]) => (
<li className="mb1">
<FieldValueMapping
original={original}
mapped={mapped}
setMapping={newMapped =>
this.onSetRemapping(original, newMapped)
}
/>
</li>
))}
</ol>
<div className="flex align-center">
<ButtonWithStatus
className="ml-auto"
disabled={!this.customValuesAreNonEmpty()}
onClickOperation={this.onSaveClick}
>
{t`Save`}
</ButtonWithStatus>
</div>
</div>
);
}
}
export class FieldValueMapping extends Component {
onInputChange = e => {
this.props.setMapping(e.target.value);
};
render() {
const { original, mapped } = this.props;
return (
<div className="flex align-center">
<h3>{original}</h3>
<InputBlurChange
className="AdminInput input ml-auto"
value={mapped}
onChange={this.onInputChange}
placeholder={t`Enter value`}
/>
</div>
);
}
}
export const Section = ({ children }) => (
<section className="my4 pb4 border-bottom">{children}</section>
);
export const SectionHeader = ({ title, description }) => (
<div className="mb2">
<h4>{title}</h4>
{description && <p className="mb0 text-medium mt1">{description}</p>}
</div>
);
const MAP_OPTIONS = {
original: { type: "original", name: t`Use original value` },
foreign: { type: "foreign", name: t`Use foreign key` },
custom: { type: "custom", name: t`Custom mapping` },
};
export class FieldRemapping extends Component {
state = {
isChoosingInitialFkTarget: false,
dismissedInitialFkTargetPopover: false,
};
constructor(props, context) {
super(props, context);
}
getMappingTypeForField = field => {
if (this.state.isChoosingInitialFkTarget) {
return MAP_OPTIONS.foreign;
}
if (_.isEmpty(field.dimensions)) {
return MAP_OPTIONS.original;
}
if (field.dimensions.type === "external") {
return MAP_OPTIONS.foreign;
}
if (field.dimensions.type === "internal") {
return MAP_OPTIONS.custom;
}
throw new Error(t`Unrecognized mapping type`);
};
getAvailableMappingTypes = () => {
const { field } = this.props;
const hasForeignKeys =
field.special_type === "type/FK" && this.getForeignKeys().length > 0;
// Only show the "custom" option if we have some values that can be mapped to user-defined custom values
// (for a field without user-defined remappings, every key of `field.remappings` has value `undefined`)
const hasMappableNumeralValues =
field.remapping.size > 0 &&
[...field.remapping.keys()].every(key => typeof key === "number");
return [
MAP_OPTIONS.original,
...(hasForeignKeys ? [MAP_OPTIONS.foreign] : []),
...(hasMappableNumeralValues > 0 ? [MAP_OPTIONS.custom] : []),
];
};
getFKTargetTableEntityNameOrNull = () => {
const fks = this.getForeignKeys();
const fkTargetFields = fks[0] && fks[0].dimensions.map(dim => dim.field());
if (fkTargetFields) {
// TODO Atte Keinänen 7/11/17: Should there be `isName(field)` in Field.js?
const nameField = fkTargetFields.find(
field => field.special_type === "type/Name",
);
return nameField ? nameField.id : null;
} else {
throw new Error(
t`Current field isn't a foreign key or FK target table metadata is missing`,
);
}
};
clearEditingStates = () => {
this.setState({
isChoosingInitialFkTarget: false,
dismissedInitialFkTargetPopover: false,
});
};
onSetMappingType = async mappingType => {
const {
table,
field,
fetchTableMetadata,
updateFieldDimension,
deleteFieldDimension,
} = this.props;
this.clearEditingStates();
if (mappingType.type === "original") {
MetabaseAnalytics.trackEvent(
"Data Model",
"Change Remapping Type",
"No Remapping",
);
await deleteFieldDimension(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
const entityNameFieldId = this.getFKTargetTableEntityNameOrNull();
if (entityNameFieldId) {
MetabaseAnalytics.trackEvent(
"Data Model",
"Change Remapping Type",
"Foreign Key",
);
await updateFieldDimension(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({
hasChanged: true,
isChoosingInitialFkTarget: true,
});
}
} else if (mappingType.type === "custom") {
MetabaseAnalytics.trackEvent(
"Data Model",
"Change Remapping Type",
"Custom Remappings",
);
await updateFieldDimension(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`);
}
// 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);
};
onForeignKeyFieldChange = async foreignKeyClause => {
const {
table,
field,
fetchTableMetadata,
updateFieldDimension,
} = this.props;
this.clearEditingStates();
// TODO Atte Keinänen 7/10/17: Use Dimension class when migrating to metabase-lib
if (foreignKeyClause.length === 3 && foreignKeyClause[0] === "fk->") {
MetabaseAnalytics.trackEvent("Data Model", "Update FK Remapping Target");
await updateFieldDimension(field.id, {
type: "external",
name: field.display_name,
human_readable_field_id: foreignKeyClause[2],
});
await fetchTableMetadata(table.id, true);
this.refs.fkPopover.close();
} else {
throw new Error(t`The selected field isn't a foreign key`);
}
};
onUpdateRemappings = remappings => {
const { field, updateFieldValues } = this.props;
return updateFieldValues(field.id, Array.from(remappings));
};
// TODO Atte Keinänen 7/11/17: Should we have stricter criteria for valid remapping targets?
isValidFKRemappingTarget = dimension =>
!(dimension.defaultDimension() instanceof DatetimeFieldDimension);
getForeignKeys = () => {
const { table, field } = this.props;
// this method has a little odd structure due to using fieldOptions(); basically filteredFKs should
// always be an array with a single value
const metadata = table.metadata;
const fieldOptions = Question.create({
metadata,
databaseId: table.db.id,
tableId: table.id,
})
.query()
.fieldOptions();
const unfilteredFks = fieldOptions.fks;
const filteredFKs = unfilteredFks.filter(fk => fk.field.id === field.id);
return filteredFKs.map(filteredFK => ({
field: filteredFK.field,
dimension: filteredFK.dimension,
dimensions: filteredFK.dimensions.filter(this.isValidFKRemappingTarget),
}));
};
onFkPopoverDismiss = () => {
const { isChoosingInitialFkTarget } = this.state;
if (isChoosingInitialFkTarget) {
this.setState({ dismissedInitialFkTargetPopover: true });
}
};
render() {
const { field, table, fields } = this.props;
const {
isChoosingInitialFkTarget,
hasChanged,
dismissedInitialFkTargetPopover,
} = this.state;
const mappingType = this.getMappingTypeForField(field);
const isFKMapping = mappingType === MAP_OPTIONS.foreign;
const hasFKMappingValue =
isFKMapping && field.dimensions.human_readable_field_id !== null;
const fkMappingField =
hasFKMappingValue && fields[field.dimensions.human_readable_field_id];
return (
<div>
<SectionHeader
title={t`Display values`}
description={t`Choose to show the original value from the database, or have this field display associated or custom information.`}
/>
<Select
value={mappingType}
onChange={this.onSetMappingType}
options={this.getAvailableMappingTypes()}
/>
{mappingType === MAP_OPTIONS.foreign && [
<SelectSeparator key="foreignKeySeparator" />,
<PopoverWithTrigger
ref="fkPopover"
triggerElement={
<SelectButton
hasValue={hasFKMappingValue}
className={cx("flex inline-block no-decoration", {
"border-error": dismissedInitialFkTargetPopover,
"border-dark": !dismissedInitialFkTargetPopover,
})}
>
{fkMappingField ? (
fkMappingField.display_name
) : (
<span className="text-medium">{t`Choose a field`}</span>
)}
</SelectButton>
}
isInitiallyOpen={isChoosingInitialFkTarget}
onClose={this.onFkPopoverDismiss}
>
<FieldList
className="text-purple"
field={fkMappingField}
fieldOptions={{
count: 0,
dimensions: [],
fks: this.getForeignKeys(),
}}
tableMetadata={table}
onFieldChange={this.onForeignKeyFieldChange}
hideSectionHeader
/>
</PopoverWithTrigger>,
dismissedInitialFkTargetPopover && (
<div className="text-error my2">{t`Please select a column to use for display.`}</div>
),
hasChanged && hasFKMappingValue && <RemappingNamingTip />,
]}
{mappingType === MAP_OPTIONS.custom && (
<div className="mt3">
{hasChanged && <RemappingNamingTip />}
<ValueRemappings
remappings={field && field.remapping}
updateRemappings={this.onUpdateRemappings}
/>
</div>
)}
</div>
);
}
}
export const RemappingNamingTip = () => (
<div className="bordered rounded p1 mt1 mb2 border-brand">
<span className="text-brand text-bold">{t`Tip: `}</span>
{t`You might want to update the field name to make sure it still makes sense based on your remapping choices.`}
</div>
);
export class UpdateCachedFieldValues extends Component {
render() {
return (
<div>
<SectionHeader
title={t`Cached field values`}
description={t`Metabase can scan the values for this field to enable checkbox filters in dashboards and questions.`}
/>
<ActionButton
className="Button mr2"
actionFn={this.props.rescanFieldValues}
normalText={t`Re-scan this field`}
activeText={t`Starting…`}
failedText={t`Failed to start scan`}
successText={t`Scan triggered!`}
/>
<ActionButton
className="Button Button--danger"
actionFn={this.props.discardFieldValues}
normalText={t`Discard cached field values`}
activeText={t`Starting…`}
failedText={t`Failed to discard values`}
successText={t`Discard triggered!`}
/>
</div>
);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment