Skip to content
Snippets Groups Projects
Unverified Commit ce479145 authored by Tom Robinson's avatar Tom Robinson Committed by GitHub
Browse files

Merge pull request #9502 from metabase/entities-fixes

Entities fixes
parents 0b935b66 dc29f6c3
Branches
Tags
No related merge requests found
Showing
with 148 additions and 95 deletions
......@@ -5,6 +5,8 @@ import FormTextAreaWidget from "./widgets/FormTextAreaWidget";
import FormPasswordWidget from "./widgets/FormPasswordWidget";
import FormColorWidget from "./widgets/FormColorWidget";
import FormSelectWidget from "./widgets/FormSelectWidget";
import FormNumericInputWidget from "./widgets/FormNumericInputWidget";
import FormToggleWidget from "./widgets/FormToggleWidget";
import FormCollectionWidget from "./widgets/FormCollectionWidget";
import FormHiddenWidget from "./widgets/FormHiddenWidget";
......@@ -14,6 +16,8 @@ const WIDGETS = {
color: FormColorWidget,
password: FormPasswordWidget,
select: FormSelectWidget,
integer: FormNumericInputWidget,
boolean: FormToggleWidget,
collection: FormCollectionWidget,
hidden: FormHiddenWidget,
};
......
......@@ -52,6 +52,7 @@ const StandardForm = ({
</div>
<div className={cx("flex", { "Form-offset": !newForm })}>
<div className="ml-auto flex align-center">
{error && <FormMessage message={error} formError />}
{onClose && (
<Button
type="button"
......@@ -76,7 +77,6 @@ const StandardForm = ({
{t`Reset`}
</Button>
)}
{error && <FormMessage message={error} formError />}
</div>
</div>
</form>
......
import React from "react";
import cx from "classnames";
import { formDomOnlyProps } from "metabase/lib/redux";
import NumericInput from "metabase/components/NumericInput";
const FormInputWidget = ({ placeholder, field, offset }) => (
<NumericInput
className={cx("Form-input full", { "Form-offset": offset })}
placeholder={placeholder}
{...formDomOnlyProps(field)}
/>
);
export default FormInputWidget;
import React from "react";
import Toggle from "metabase/components/Toggle";
const FormToggleWidget = ({ field }) => <Toggle {...field} />;
export default FormToggleWidget;
import React from "react";
import { t } from "c-3po";
import EntityForm from "metabase/entities/containers/EntityForm";
import ModalContent from "metabase/components/ModalContent";
const CollectionForm = ({ collection, onClose, ...props }) => (
<ModalContent
title={
collection && collection.id != null ? collection.name : t`New collection`
}
onClose={onClose}
>
<EntityForm entityType="collections" entityObject={collection} {...props} />
</ModalContent>
);
export default CollectionForm;
import React from "react";
import EntityObjectLoader from "metabase/entities/containers/EntityObjectLoader";
const CollectionLoader = ({ collectionId, ...props }) => (
<EntityObjectLoader
entityType="collections"
entityId={collectionId}
{...props}
/>
);
export default CollectionLoader;
import React from "react";
import { t } from "c-3po";
import EntityForm from "metabase/entities/containers/EntityForm";
import ModalContent from "metabase/components/ModalContent";
const DashboardForm = ({ dashboard, onClose, ...props }) => (
<ModalContent
title={
dashboard && dashboard.id != null ? dashboard.name : t`Create dashboard`
}
onClose={onClose}
>
<EntityForm
entityType="dashboards"
entityObject={dashboard}
onClose={onClose}
{...props}
/>
</ModalContent>
);
export default DashboardForm;
......@@ -4,6 +4,7 @@ import React from "react";
import PropTypes from "prop-types";
import { reduxForm, getValues } from "redux-form";
import { getIn } from "icepick";
import StandardForm from "metabase/components/form/StandardForm";
......@@ -154,9 +155,12 @@ function makeFormMethod(
const values =
getValue(originalMethod, object) || getValue(defaultValues, object);
for (const field of form.fields(object)) {
const value = getValue(field[methodName], object && object[field.name]);
const value = getValue(
field[methodName],
object && getValueAtPath(object, field.name),
);
if (value !== undefined) {
values[field.name] = value;
setValueAtPath(values, field.name, value);
}
}
return values;
......@@ -183,3 +187,21 @@ function makeForm(formDef: FormDef): Form {
makeFormMethod(form, "normalize", object => object);
return form;
}
function getObjectPath(path) {
return typeof path === "string" ? path.split(".") : path;
}
function getValueAtPath(object, path) {
return getIn(object, getObjectPath(path));
}
function setValueAtPath(object, path, value) {
path = getObjectPath(path);
for (let i = 0; i < path.length; i++) {
if (i === path.length - 1) {
object[path[i]] = value;
} else {
object = object[path[i]] = object[path[i]] || {};
}
}
}
......@@ -13,7 +13,7 @@ export default class EntityForm extends React.Component {
render() {
const {
entityDef,
object = this.props[entityDef.nameOne],
entityObject,
update,
create,
onClose,
......@@ -26,7 +26,7 @@ export default class EntityForm extends React.Component {
<Form
{...props}
form={entityDef.form}
initialValues={object}
initialValues={entityObject}
onSubmit={object =>
object.id != null ? update(object) : create(object)
}
......@@ -38,8 +38,8 @@ export default class EntityForm extends React.Component {
<ModalContent
title={
title ||
(object && object.id != null
? entityDef.objectSelectors.getName(object)
(entityObject && entityObject.id != null
? entityDef.objectSelectors.getName(entityObject)
: t`New ${entityDef.displayNameOne}`)
}
onClose={onClose}
......
......@@ -21,7 +21,7 @@ export function addEntityContainers(entity) {
entity.Loader = ({ id, ...props }) => (
<EntityObjectLoader entityType={entity.name} entityId={id} {...props} />
);
entity.Loader.displayName = `${ObjectName}Loader`;
entity.Loader.displayName = `${ObjectName}.Loader`;
// Entity.loadList higher-order component
entity.loadList = ({ query, ...props } = {}) =>
......@@ -31,23 +31,32 @@ export function addEntityContainers(entity) {
entity.ListLoader = ({ query, ...props }) => (
<EntityListLoader entityType={entity.name} entityQuery={query} {...props} />
);
entity.ListLoader.displayName = `${ObjectName}ListLoader`;
entity.ListLoader.displayName = `${ObjectName}.ListLoader`;
// Entity.Name component
entity.Name = ({ id, ...props }) => (
<EntityName entityType={entity.name} entityId={id} {...props} />
);
entity.Name.displayName = `${ObjectName}Name`;
entity.Name.displayName = `${ObjectName}.Name`;
// Entity.Form component
entity.Form = ({ user, ...props }) => (
<EntityForm entityType={entity.name} entityObject={user} {...props} />
entity.Form = ({ object, ...props }) => (
<EntityForm
entityType={entity.name}
entityObject={object || props[entity.nameOne]}
{...props}
/>
);
entity.Form.displayName = `${ObjectName}Form`;
entity.Form.displayName = `${ObjectName}.Form`;
// Entity.ModalForm component
entity.ModalForm = ({ user, ...props }) => (
<EntityForm modal entityType={entity.name} entityObject={user} {...props} />
entity.ModalForm = ({ object, ...props }) => (
<EntityForm
modal
entityType={entity.name}
entityObject={object || props[entity.nameOne]}
{...props}
/>
);
entity.ModalForm.displayName = `${ObjectName}ModalForm`;
entity.ModalForm.displayName = `${ObjectName}.ModalForm`;
}
/* @flow weak */
import { createEntity } from "metabase/lib/entities";
import { fetchData, createThunkAction } from "metabase/lib/redux";
import { normalize } from "normalizr";
import _ from "underscore";
import { createEntity } from "metabase/lib/entities";
import { fetchData, createThunkAction } from "metabase/lib/redux";
import MetabaseSettings from "metabase/lib/settings";
import { MetabaseApi } from "metabase/services";
import { DatabaseSchema } from "metabase/schema";
......@@ -17,6 +19,9 @@ const Databases = createEntity({
path: "/api/database",
schema: DatabaseSchema,
nameOne: "database",
nameMany: "databases",
// ACTION CREATORS
objectActions: {
fetchDatabaseMetadata: createThunkAction(
......@@ -46,28 +51,58 @@ const Databases = createEntity({
// FORM
form: {
fields: (values = {}) => [
{ name: "name" },
{ name: "engine", type: "select", options: ENGINE_OPTIONS },
...(FIELDS_BY_ENGINE[values.engine] || []),
{
name: "engine",
type: "select",
options: ENGINE_OPTIONS,
placeholder: `Select a database`,
initial: "postgres",
},
{
name: "name",
placeholder: `How would you like to refer to this database?`,
validate: value => (!value ? `required` : null),
},
...(getFieldsForEngine(values.engine, values) || []),
],
},
});
export default Databases;
// TODO: use the info returned by the backend
const FIELDS_BY_ENGINE = {
h2: [{ name: "details.db" }],
postgres: [
{ name: "details.host" },
{ name: "details.port" },
{ name: "details.dbname" },
{ name: "details.user" },
{ name: "details.password", type: "password" },
],
};
function getFieldsForEngine(engine, values) {
const info = (MetabaseSettings.get("engines") || {})[engine];
if (info) {
const fields = [];
for (const field of info["details-fields"]) {
if (
field.name.startsWith("tunnel-") &&
field.name !== "tunnel-enabled" &&
(!values.details || !values.details["tunnel-enabled"])
) {
continue;
}
fields.push({
name: "details." + field.name,
title: field["display-name"],
type: field.type,
placeholder: field.placeholder || field.default,
validate: value => (field.required && !value ? `required` : null),
normalize: value =>
value == "" || value == null
? "default" in field ? field.default : null
: value,
});
}
return fields;
} else {
return [];
}
}
const ENGINE_OPTIONS = Object.keys(FIELDS_BY_ENGINE).map(key => ({
name: key,
value: key,
const ENGINE_OPTIONS = Object.entries(
MetabaseSettings.get("engines") || {},
).map(([engine, info]) => ({
name: info["driver-name"],
value: engine,
}));
......@@ -3,6 +3,10 @@ import { createEntity } from "metabase/lib/entities";
const Groups = createEntity({
name: "groups",
path: "/api/permissions/group",
form: {
fields: [{ name: "name" }],
},
});
export default Groups;
......@@ -31,6 +31,10 @@ const Metrics = createEntity({
getColor: () => colors["text-medium"],
getIcon: question => "sum",
},
form: {
fields: [{ name: "name" }, { name: "description", type: "text" }],
},
});
export default Metrics;
......@@ -33,6 +33,10 @@ const Segments = createEntity({
getColor: () => colors["text-medium"],
getIcon: question => "segment",
},
form: {
fields: [{ name: "name" }, { name: "description", type: "text" }],
},
});
export default Segments;
......@@ -23,8 +23,8 @@ export default class EntitiesApp extends React.Component {
return (
<div className="p2">
{Object.values(entityDefs).map(entityDef => (
<div key={entityDef.name}>
<Link to={`/_internal/entities/${entityDef.name}`}>
<div key={entityDef.name} className="mb1">
<Link to={`/_internal/entities/${entityDef.name}`} className="link">
{capitalize(entityDef.name)}
</Link>
</div>
......@@ -37,7 +37,7 @@ export default class EntitiesApp extends React.Component {
import { List, WindowScroller } from "react-virtualized";
const EntityListApp = ({ params: { entityType } }) => (
<EntityListLoader entityType={entityType}>
<EntityListLoader entityType={entityType} wrapped>
{({ list }) => (
<div className="p2">
<h2 className="pb2">{capitalize(entityType)}</h2>
......@@ -49,16 +49,15 @@ const EntityListApp = ({ params: { entityType } }) => (
height={height}
isScrolling={isScrolling}
rowCount={list.length}
rowHeight={20}
rowHeight={22}
width={200}
rowRenderer={({ index, key, style }) => (
<div key={key} style={style}>
<div key={key} style={style} className="text-ellipsis">
<Link
className="text-nowrap link"
to={`/_internal/entities/${entityType}/${list[index].id}`}
>
{entityDefs[entityType].objectSelectors.getName(
list[index],
)}
{list[index].getName()}
</Link>
</div>
)}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment