Skip to content
Snippets Groups Projects
Unverified Commit a266ca33 authored by Alexander Lesnenko's avatar Alexander Lesnenko Committed by GitHub
Browse files

data model ui for json fields (#21599)

parent 828a4350
No related branches found
No related tags found
No related merge requests found
Showing
with 349 additions and 278 deletions
export interface Field {
id: number;
table_id: number;
name: string;
base_type: string;
description: string | null;
nfc_path: string[] | null;
}
......@@ -5,6 +5,7 @@ export * from "./collection";
export * from "./dashboard";
export * from "./database";
export * from "./table";
export * from "./field";
export * from "./timeline";
export * from "./settings";
export * from "./slack";
......
import { ForeignKey } from "../api/foreignKey";
import { Database } from "./database";
import { Field } from "./field";
export type VisibilityType =
| null
| "details-only"
| "hidden"
| "normal"
| "retired"
| "sensitive"
| "technical"
| "cruft";
export interface Table {
id: number;
db_id: number;
db?: Database;
name: string;
description: string | null;
display_name: string;
schema: string;
fks?: ForeignKey[];
schema_name?: string;
visibility_type: VisibilityType;
fields?: Field[];
}
import { Database } from "metabase-types/api";
export interface DatabaseEntity extends Database {
fetchIdfields: () => void;
}
export * from "./table";
export * from "./database";
import { Table } from "metabase-types/api";
export interface TableEntity extends Table {
updateProperty: (name: string, value: string | number | null) => void;
}
......@@ -4,7 +4,6 @@ import PropTypes from "prop-types";
import { Link, withRouter } from "react-router";
import { t } from "ttag";
import InputBlurChange from "metabase/components/InputBlurChange";
import Select, { Option } from "metabase/core/components/Select";
import Button from "metabase/core/components/Button";
import * as MetabaseCore from "metabase/lib/core";
......@@ -18,6 +17,8 @@ import _ from "underscore";
import cx from "classnames";
import * as MetabaseAnalytics from "metabase/lib/analytics";
import { ColumnItemInput } from "./ColumnItem.styled";
import { getFieldRawName } from "../../../utils";
@withRouter
export default class Column extends Component {
......@@ -47,52 +48,58 @@ export default class Column extends Component {
render() {
const { field, idfields, dragHandle } = this.props;
return (
<div className="p1 mt1 mb3 flex bordered rounded">
<div className="py2 pl2 pr1 mt1 mb3 flex bordered rounded">
<div className="flex flex-column flex-auto">
<div>
<InputBlurChange
style={{ minWidth: 420 }}
className="AdminInput TableEditor-field-name float-left bordered inline-block rounded text-bold"
type="text"
value={this.props.field.display_name || ""}
onBlurChange={this.handleChangeName}
/>
<div className="clearfix">
<div className="flex flex-auto">
<div className="pl1 flex-auto">
<FieldVisibilityPicker
className="block"
field={field}
updateField={this.updateField}
/>
</div>
<div className="flex-auto px1">
<SemanticTypeAndTargetPicker
className="block"
field={field}
updateField={this.updateField}
idfields={idfields}
/>
<div className="text-monospace mb1" style={{ fontSize: "12px" }}>
{getFieldRawName(field)}
</div>
<div className="flex flex-column">
<div>
<ColumnItemInput
variant="primary"
style={{ minWidth: 420 }}
className="AdminInput TableEditor-field-name float-left inline-block rounded text-bold"
type="text"
value={this.props.field.display_name || ""}
onBlurChange={this.handleChangeName}
/>
<div className="clearfix">
<div className="flex flex-auto">
<div className="pl1 flex-auto">
<FieldVisibilityPicker
className="block"
field={field}
updateField={this.updateField}
/>
</div>
<div className="flex-auto px1">
<SemanticTypeAndTargetPicker
className="block"
field={field}
updateField={this.updateField}
idfields={idfields}
/>
</div>
<Link
to={`${this.props.location.pathname}/${this.props.field.id}`}
className="text-brand-hover mr1"
>
<Button icon="gear" style={{ padding: 10 }} />
</Link>
</div>
<Link
to={`${this.props.location.pathname}/${this.props.field.id}`}
className="text-brand-hover mr1"
>
<Button icon="gear" style={{ padding: 10 }} />
</Link>
</div>
</div>
</div>
<div className="MetadataTable-title flex flex-column flex-full mt1 mr1">
<InputBlurChange
className="AdminInput TableEditor-field-description bordered rounded"
type="text"
value={this.props.field.description || ""}
onBlurChange={this.handleChangeDescription}
placeholder={t`No column description yet`}
/>
<div className="MetadataTable-title flex flex-column flex-full mt1 mr1">
<ColumnItemInput
variant="secondary"
className="AdminInput TableEditor-field-description rounded"
type="text"
value={this.props.field.description || ""}
onBlurChange={this.handleChangeDescription}
placeholder={t`No column description yet`}
/>
</div>
</div>
</div>
{dragHandle}
......
import styled from "@emotion/styled";
import InputBlurChange from "metabase/components/InputBlurChange";
import { color } from "metabase/lib/colors";
interface ColumnItemInputProps {
variant: "primary" | "secondary";
}
export const ColumnItemInput = styled(InputBlurChange)<ColumnItemInputProps>`
border-color: ${color("border")};
background-color: ${props =>
color(props.variant === "primary" ? "white" : "bg-light")};
`;
export { default } from "./ColumnItem";
export * from "./ColumnItem";
......@@ -14,7 +14,8 @@ import PopoverWithTrigger from "metabase/components/PopoverWithTrigger";
import Icon from "metabase/components/Icon";
import Grabber from "metabase/components/Grabber";
import ColumnItem from "./ColumnItem";
import ColumnItem from "../ColumnItem";
import { SortButtonContainer } from "./ColumnsList.styled";
export default class ColumnsList extends Component {
constructor(props) {
......@@ -98,19 +99,21 @@ export default class ColumnsList extends Component {
const { fieldOrder } = this.state;
return (
<div id="ColumnsList" className="my3">
<div className="flex">
<div className="flex-align-right">
<ColumnOrderDropdown table={table} />
</div>
</div>
<div className="text-uppercase text-medium py1">
<div
style={{ minWidth: 420 }}
className="float-left px1"
>{t`Column`}</div>
<div className="flex clearfix" style={{ paddingRight: 47 }}>
<div className="flex-half pl2">{t`Visibility`}</div>
<div className="flex-half">{t`Type`}</div>
<div className="relative">
<div
style={{ minWidth: 420 }}
className="float-left px1"
>{t`Column`}</div>
<div className="flex">
<div className="flex-half pl3">{t`Visibility`}</div>
<div className="flex-half">
<span>{t`Type`}</span>
</div>
</div>
<SortButtonContainer>
<ColumnOrderDropdown table={table} />
</SortButtonContainer>
</div>
</div>
<SortableColumns
......@@ -174,11 +177,10 @@ class ColumnOrderDropdown extends Component {
className="text-brand text-bold"
style={{ textTransform: "none", letterSpacing: 0 }}
>
{t`Column order: ${COLUMN_ORDERS[table.field_order]}`}
<Icon
className="ml1"
name="chevrondown"
size={12}
name="sort_arrows"
size={14}
style={{ transform: "translateY(2px)" }}
/>
</span>
......
import styled from "@emotion/styled";
export const SortButtonContainer = styled.div`
position: absolute;
right: 0.5rem;
top: 0;
`;
export { default } from "./ColumnsList";
......@@ -2,13 +2,11 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { Link, withRouter } from "react-router";
import { t } from "ttag";
import Databases from "metabase/entities/databases";
import { DatabaseDataSelector } from "metabase/query_builder/components/DataSelector";
import SaveStatus from "metabase/components/SaveStatus";
import Toggle from "metabase/core/components/Toggle";
import Icon from "metabase/components/Icon";
import { PLUGIN_FEATURE_LEVEL_PERMISSIONS } from "metabase/plugins";
......@@ -76,11 +74,6 @@ export default class MetadataHeader extends Component {
</div>
<div className="MetadataEditor-headerSection flex flex-align-right align-center flex-no-shrink">
<SaveStatus />
<div className="mr1 text-medium">{t`Show original schema`}</div>
<Toggle
value={this.props.isShowingSchema}
onChange={this.props.toggleShowSchema}
/>
{this.renderTableSettingsButton()}
</div>
</div>
......
/* eslint-disable react/prop-types */
import React, { Component } from "react";
import PropTypes from "prop-types";
import { t } from "ttag";
import withTableMetadataLoaded from "metabase/admin/datamodel/hoc/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 { table } = this.props;
if (!table || !table.fields) {
return false;
}
const tdClassName = "py2 px1 border-bottom";
const fields = table.fields.map(field => {
return (
<tr key={field.id}>
<td className={tdClassName}>
<span className="TableEditor-field-name text-bold">
{field.name}
</span>
</td>
<td className={tdClassName}>
<span className="text-bold">{field.base_type}</span>
</td>
<td className={tdClassName} />
</tr>
);
});
return (
<div className="MetadataTable px2 full">
<div className="flex flex-column px1">
<div className="TableEditor-table-name text-bold">{table.name}</div>
</div>
<table className="mt2 full">
<thead className="text-uppercase text-medium py1">
<tr>
<th className={tdClassName}>{t`Column`}</th>
<th className={tdClassName}>{t`Data Type`}</th>
<th className={tdClassName}>{t`Additional Info`}</th>
</tr>
</thead>
<tbody>{fields}</tbody>
</table>
</div>
);
}
}
import { css } from "@emotion/react";
import styled from "@emotion/styled";
import { color } from "metabase/lib/colors";
interface CellProps {
hideBorderBottom?: boolean;
isSecondary?: boolean;
}
const cellStyles = (props: CellProps) => css`
padding: 1rem 0.5rem;
font-weight: bold;
color: ${props.isSecondary ? color("text-medium") : color("text-dark")};
border-bottom: ${props.hideBorderBottom
? "none"
: `1px solid ${color("border")}`};
`;
export const ColumnNameCell = styled.td`
${cellStyles}
font-size: 16px;
`;
export const DataTypeCell = styled.td`
${cellStyles}
font-weight: bold;
`;
export const HeaderCell = styled.th`
padding: 1rem 0.5rem;
border-bottom: 1px solid ${color("border")};
`;
import React from "react";
import { t } from "ttag";
import _ from "underscore";
import withTableMetadataLoaded from "metabase/admin/datamodel/hoc/withTableMetadataLoaded";
import Tables from "metabase/entities/tables";
import { Field, Table } from "metabase-types/api";
import { State } from "metabase-types/store";
import { getFieldRawName } from "../../../utils";
import {
ColumnNameCell,
DataTypeCell,
HeaderCell,
} from "./MetadataSchema.styled";
import { getFieldsTable } from "./utils";
interface FieldRowProps {
field: Field;
hideBorderBottom: boolean;
isSecondary?: boolean;
}
const FieldRow = ({ field, hideBorderBottom, isSecondary }: FieldRowProps) => (
<tr>
<ColumnNameCell
data-testid="table-name"
isSecondary={isSecondary}
hideBorderBottom={hideBorderBottom}
>
{field.name}
</ColumnNameCell>
<DataTypeCell hideBorderBottom={hideBorderBottom}>
{field.base_type}
</DataTypeCell>
<DataTypeCell hideBorderBottom={hideBorderBottom} />
</tr>
);
interface MetadataSchemaProps {
table: Table;
}
const MetadataSchema = ({ table }: MetadataSchemaProps) => {
if (!table || !table.fields) {
return false;
}
const fields = getFieldsTable(table.fields);
return (
<div className="mt3 full">
<div className="flex flex-column px1">
<div
data-testid="table-name-input"
className="TableEditor-table-name text-bold"
>
{table.name}
</div>
</div>
<table className="mt2 full">
<thead className="text-uppercase text-medium py1">
<tr>
<HeaderCell>{t`Column`}</HeaderCell>
<HeaderCell>{t`Data Type`}</HeaderCell>
<HeaderCell>{t`Additional Info`}</HeaderCell>
</tr>
</thead>
<tbody>
{fields.map(field => {
const hasNested = field.nested?.length > 0;
return (
<React.Fragment key={field.id}>
<FieldRow field={field} hideBorderBottom={hasNested} />
{field.nested?.map((nestedField, index) => {
const isLast = index === field.nested.length - 1;
return (
<FieldRow
key={nestedField.id}
field={nestedField}
hideBorderBottom={!isLast}
isSecondary
/>
);
})}
</React.Fragment>
);
})}
</tbody>
</table>
</div>
);
};
export default _.compose(
Tables.load({
id: (_state: State, { tableId }: { tableId: number }) => tableId,
wrapped: true,
}),
withTableMetadataLoaded,
)(MetadataSchema);
export { default } from "./MetadataSchema";
import _ from "underscore";
import { Field } from "metabase-types/api";
export const getFieldsTable = (fields: Field[]) => {
const [nonNested, nested] = _.partition(
fields,
field => field.nfc_path == null,
);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const nestedByParent = _.groupBy(nested, field => field.nfc_path![0]);
return nonNested.map(field => ({
...field,
nested: nestedByParent[field.name],
}));
};
/* eslint-disable react/prop-types */
import React, { Component } from "react";
import PropTypes from "prop-types";
import ColumnsList from "./ColumnsList";
import { t } from "ttag";
import InputBlurChange from "metabase/components/InputBlurChange";
import Databases from "metabase/entities/databases";
import Tables from "metabase/entities/tables";
import withTableMetadataLoaded from "metabase/admin/datamodel/hoc/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: "getObjectUnfiltered",
})
@withTableMetadataLoaded
export default class MetadataTable extends Component {
constructor(props, context) {
super(props, context);
this.onDescriptionChange = this.onDescriptionChange.bind(this);
this.onNameChange = this.onNameChange.bind(this);
this.updateProperty = this.updateProperty.bind(this);
}
static propTypes = {
table: PropTypes.object,
idfields: PropTypes.array,
updateField: PropTypes.func.isRequired,
};
componentDidMount() {
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.table.visibility_type;
}
updateProperty(name, value) {
this.setState({ saving: true });
this.props.table.updateProperty(name, value);
}
onNameChange(event) {
if (!_.isEmpty(event.target.value)) {
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.table.display_name;
}
}
onDescriptionChange(event) {
this.updateProperty("description", event.target.value);
}
renderVisibilityType(text, type, any) {
const classes = cx(
"mx1",
"text-bold",
"text-brand-hover",
"cursor-pointer",
"text-default",
{
"text-brand":
this.props.table.visibility_type === type ||
(any && this.props.table.visibility_type),
},
);
return (
<span
className={classes}
onClick={this.updateProperty.bind(null, "visibility_type", type)}
>
{text}
</span>
);
}
renderVisibilityWidget() {
let subTypes;
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>
{this.renderVisibilityType(t`Technical Data`, "technical")}
{this.renderVisibilityType(t`Irrelevant/Cruft`, "cruft")}
</span>
);
}
return (
<span id="VisibilityTypes">
{this.renderVisibilityType(t`Queryable`, null)}
{this.renderVisibilityType(t`Hidden`, "hidden", true)}
{subTypes}
</span>
);
}
render() {
const { table } = this.props;
if (!table) {
return false;
}
return (
<div className="MetadataTable full px3">
<div className="MetadataTable-title flex flex-column">
<InputBlurChange
className="AdminInput TableEditor-table-name text-bold rounded-top bordered"
name="display_name"
type="text"
value={table.display_name || ""}
onBlurChange={this.onNameChange}
/>
<InputBlurChange
className="AdminInput TableEditor-table-description rounded-bottom bordered"
name="description"
type="text"
value={table.description || ""}
onBlurChange={this.onDescriptionChange}
placeholder={t`No table description yet`}
/>
</div>
<div className="MetadataTable-header flex align-center py2 text-medium">
<span className="mx1 text-uppercase">{t`Visibility`}</span>
{this.renderVisibilityWidget()}
</div>
<div className={"mt2 " + (this.isHidden() ? "disabled" : "")}>
{this.props.idfields && (
<ColumnsList
table={table}
updateField={this.props.updateField}
idfields={this.props.idfields}
/>
)}
</div>
</div>
);
}
}
import styled from "@emotion/styled";
import { color } from "metabase/lib/colors";
import InputBlurChange from "metabase/components/InputBlurChange";
import { css } from "@emotion/react";
interface VisibilityTypeProps {
isSelected: boolean;
}
export const VisibilityType = styled.span<VisibilityTypeProps>`
margin: 0 0.5rem;
font-weight: bold;
color: ${props => (props.isSelected ? color("brand") : color("text-dark"))};
&:hover {
color: ${color("brand")};
}
`;
const headerInputsStyles = css`
background-color: ${color("bg-light")};
padding: 0.75rem 1.5rem;
z-index: 1;
outline: none;
border-color: ${color("border")};
&:hover,
&:focus {
z-index: 2;
}
&:focus {
border-color: ${color("brand")};
}
&:focus:not(:focus-visible) {
outline: none;
}
`;
export const TableNameInput = styled(InputBlurChange)`
${headerInputsStyles}
font-weight: 700;
font-size: 20px;
color: ${color("text-dark")};
border-radius: 8px 8px 0 0;
`;
export const TableDescriptionInput = styled(InputBlurChange)`
${headerInputsStyles}
color: ${color("text-dark")};
margin-top: -1px;
border-radius: 0 0 8px 8px;
font-weight: 400;
font-size: 14px;
`;
export const TableName = styled.div`
font-weight: 700;
font-size: 20px;
padding: 0.75rem 0.5rem;
border: 1px solid transparent;
`;
export const TableDescription = styled.div`
font-weight: 400;
font-size: 14px;
padding: 0.75rem 0.5rem;
border: 1px solid transparent;
margin-top: -1px;
`;
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