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

Merge branch 'master' of github.com:metabase/metabase into chart-improvements-2

parents b58dc459 9804276e
Branches
Tags
No related merge requests found
Showing
with 253 additions and 145 deletions
......@@ -6,6 +6,7 @@ import { push } from "react-router-redux";
import MetabaseAnalytics from "metabase/lib/analytics";
import { augmentTable } from "metabase/lib/table";
import { loadTableAndForeignKeys } from "metabase/lib/table";
import { isFK } from "metabase/lib/types";
// resource wrappers
......@@ -159,7 +160,7 @@ export const updateFieldSpecialType = createThunkAction("UPDATE_FIELD_SPECIAL_TY
return function(dispatch, getState) {
// 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 && field.special_type !== "fk") {
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;
......
import { TYPE } from "metabase/lib/types";
export const user_roles = [{
'id': 'user',
'name': 'User',
......@@ -9,78 +11,78 @@ export const user_roles = [{
}];
export const field_special_types = [{
'id': 'id',
'id': TYPE.PK,
'name': 'Entity Key',
'section': 'Overall Row',
'description': 'The primary key for this table.'
}, {
'id': 'name',
'id': TYPE.Name,
'name': 'Entity Name',
'section': 'Overall Row',
'description': 'The "name" of each record. Usually a column called "name", "title", etc.'
}, {
'id': 'fk',
'id': TYPE.FK,
'name': 'Foreign Key',
'section': 'Overall Row',
'description': 'Points to another table to make a connection.'
}, {
'id': 'avatar',
'id': TYPE.AvatarURL,
'name': 'Avatar Image URL',
'section': 'Common'
}, {
'id': 'category',
'id': TYPE.Category,
'name': 'Category',
'section': 'Common'
}, {
'id': 'city',
'id': TYPE.City,
'name': 'City',
'section': 'Common'
}, {
'id': 'country',
'id': TYPE.Country,
'name': 'Country',
'section': 'Common'
}, {
'id': 'desc',
'id': TYPE.Description,
'name': 'Description',
'section': 'Common'
}, {
'id': 'image',
'id': TYPE.ImageURL,
'name': 'Image URL',
'section': 'Common'
}, {
'id': 'json',
'id': TYPE.SerializedJSON,
'name': 'Field containing JSON',
'section': 'Common'
}, {
'id': 'latitude',
'id': TYPE.Latitude,
'name': 'Latitude',
'section': 'Common'
}, {
'id': 'longitude',
'id': TYPE.Longitude,
'name': 'Longitude',
'section': 'Common'
}, {
'id': 'number',
'id': TYPE.Number,
'name': 'Number',
'section': 'Common'
}, {
'id': 'state',
'id': TYPE.State,
'name': 'State',
'section': 'Common'
}, {
id: 'timestamp_seconds',
id: TYPE.UNIXTimestampSeconds,
name: 'UNIX Timestamp (Seconds)',
'section': 'Common'
}, {
id: 'timestamp_milliseconds',
id: TYPE.UNIXTimestampMilliseconds,
name: 'UNIX Timestamp (Milliseconds)',
'section': 'Common'
}, {
'id': 'url',
'id': TYPE.URL,
'name': 'URL',
'section': 'Common'
}, {
'id': 'zip_code',
'id': TYPE.ZipCode,
'name': 'Zip Code',
'section': 'Common'
}];
......
......@@ -5,6 +5,7 @@ import _ from "underscore";
import { getOperators } from "metabase/lib/schema_metadata";
import { createLookupByProperty } from "metabase/lib/table";
import { isFK, TYPE } from "metabase/lib/types";
export const NEW_QUERY_TEMPLATES = {
......@@ -517,7 +518,7 @@ var Query = {
display_name: field[1],
name: field[1],
// TODO: we need to do something better here because filtering depends on knowing a sensible type for the field
base_type: "IntegerField",
base_type: TYPE.Integer,
operators_lookup: {},
valid_operators: [],
active: true,
......@@ -549,17 +550,17 @@ var Query = {
}
},
getFieldOptions(fields, includeJoins = false, filterFn = (fields) => fields, usedFields = {}) {
getFieldOptions(fields, includeJoins = false, filterFn = _.identity, usedFields = {}) {
var results = {
count: 0,
fields: null,
fks: []
};
// filter based on filterFn, then remove fks if they'll be duplicated in the joins fields
results.fields = filterFn(fields).filter((f) => !usedFields[f.id] && (f.special_type !== "fk" || !includeJoins));
results.fields = filterFn(fields).filter((f) => !usedFields[f.id] && (!isFK(f.special_type) || !includeJoins));
results.count += results.fields.length;
if (includeJoins) {
results.fks = fields.filter((f) => f.special_type === "fk" && f.target).map((joinField) => {
results.fks = fields.filter((f) => isFK(f.special_type) && f.target).map((joinField) => {
var targetFields = filterFn(joinField.target.table.fields).filter(f => (!Array.isArray(f.id) || f.id[0] !== "aggregation") && !usedFields[f.id]);
results.count += targetFields.length;
return {
......@@ -717,14 +718,14 @@ var Query = {
},
getQueryColumns(tableMetadata, query) {
let columns = Query.getBreakouts(query).map(b => Query.getQueryColumn(tableMetadata, b))
let columns = Query.getBreakouts(query).map(b => Query.getQueryColumn(tableMetadata, b));
if (Query.getAggregationType(query) === "rows") {
if (columns.length === 0) {
return null;
}
} else {
// NOTE: incomplete (missing name etc), count/distinct are actually IntegerField, but close enough for now
columns.push({ base_type: "FloatField", special_type: "number" });
// NOTE: incomplete (missing name etc), count/distinct are actually TYPE.Integer, but close enough for now
columns.push({ base_type: TYPE.Float, special_type: TYPE.Number });
}
return columns;
}
......
import _ from "underscore";
import { isa, isFK, TYPE } from "metabase/lib/types";
// primary field types used for picking operators, etc
export const NUMBER = "NUMBER";
export const STRING = "STRING";
export const STRING_LIKE = "STRING_LIKE";
export const BOOLEAN = "BOOLEAN";
export const DATE_TIME = "DATE_TIME";
export const LOCATION = "LOCATION";
......@@ -20,82 +23,78 @@ export const UNKNOWN = "UNKNOWN";
// NOTE: be sure not to create cycles using the "other" types
const TYPES = {
[DATE_TIME]: {
base: ["DateTimeField", "DateField", "TimeField"],
special: ["timestamp_milliseconds", "timestamp_seconds"]
base: [TYPE.DateTime],
special: [TYPE.DateTime]
},
[NUMBER]: {
base: ["IntegerField", "DecimalField", "FloatField", "BigIntegerField"],
special: ["number"]
base: [TYPE.Number],
special: [TYPE.Number]
},
[STRING]: {
base: ["CharField", "TextField"],
special: ["name"]
base: [TYPE.Text],
special: [TYPE.Text]
},
[STRING_LIKE]: {
base: [TYPE.TextLike]
},
[BOOLEAN]: {
base: ["BooleanField"]
base: [TYPE.Boolean]
},
[COORDINATE]: {
special: ["latitude", "longitude"]
special: [TYPE.Coordinate]
},
[LOCATION]: {
special: ["city", "country", "state", "zip_code"]
special: [TYPE.Address]
},
[ENTITY]: {
special: ["fk", "id", "name"]
special: [TYPE.FK, TYPE.PK, TYPE.Name]
},
[SUMMABLE]: {
include: [NUMBER],
exclude: [ENTITY, LOCATION, DATE_TIME]
},
[CATEGORY]: {
base: ["BooleanField"],
special: ["category"],
base: [TYPE.Boolean],
special: [TYPE.Category],
include: [LOCATION]
},
// NOTE: this is defunct right now. see definition of isDimension below.
[DIMENSION]: {
field: ["dimension"],
include: [DATE_TIME, CATEGORY, ENTITY]
}
};
export function isFieldType(type, field) {
if (!field) {
return false;
}
if (!field) return false;
let def = TYPES[type];
const typeDefinition = TYPES[type];
// check to see if it belongs to any of the field types:
for (let prop of ["field", "base", "special"]) {
if (def[prop] && _.contains(def[prop], field[prop+"_type"])) {
return true;
for (const prop of ["base", "special"]) {
const allowedTypes = typeDefinition[prop];
if (!allowedTypes) continue;
const fieldType = field[prop + "_type"];
for (const allowedType of allowedTypes) {
if (isa(fieldType, allowedType)) return true;
}
}
// recursively check to see if it's NOT another field type:
if (def.exclude) {
for (let excludeType of def.exclude) {
if (isFieldType(excludeType, field)) {
return false;
}
}
for (const excludedType of (typeDefinition.exclude || [])) {
if (isFieldType(excludedType, field)) return false;
}
// recursively check to see if it's another field type:
if (def.include) {
for (let includeType of def.include) {
if (isFieldType(includeType, field)) {
return true;
}
}
for (const includedType of (typeDefinition.include || [])) {
if (isFieldType(includedType, field)) return true;
}
return false;
}
export function getFieldType(field) {
// try more specific types first, then more generic types
for (let type of [DATE_TIME, LOCATION, COORDINATE, NUMBER, STRING, BOOLEAN]) {
if (isFieldType(type, field)) {
return type;
}
for (const type of [DATE_TIME, LOCATION, COORDINATE, NUMBER, STRING, STRING_LIKE, BOOLEAN]) {
if (isFieldType(type, field)) return type;
}
}
......@@ -109,8 +108,7 @@ export const isCategory = isFieldType.bind(null, CATEGORY);
export const isDimension = (col) => (col && col.source !== "aggregation");
export const isMetric = (col) => (col && col.source !== "breakout") && isSummable(col);
export const isNumericBaseType = (field) => TYPES[NUMBER].base
.some(type => type === field.base_type);
export const isNumericBaseType = (field) => isa(field && field.base_type, TYPE.Number);
// operator argument constructors:
......@@ -195,7 +193,7 @@ function longitudeFieldSelectArgument(field, table) {
return {
type: "select",
values: table.fields
.filter(field => field.special_type === "longitude")
.filter(field => isa(field.special_type, TYPE.Longitude))
.map(field => ({
key: field.id,
name: field.display_name
......@@ -274,6 +272,12 @@ const OPERATORS_BY_TYPE_ORDERED = {
{ name: "STARTS_WITH", verboseName: "Starts with", advanced: true},
{ name: "ENDS_WITH", verboseName: "Ends with", advanced: true}
],
[STRING_LIKE]: [
{ name: "=", verboseName: "Is" },
{ name: "!=", verboseName: "Is not" },
{ name: "IS_NULL", verboseName: "Is empty", advanced: true },
{ name: "NOT_NULL", verboseName: "Not empty", advanced: true }
],
[DATE_TIME]: [
{ name: "=", verboseName: "Is" },
{ name: "<", verboseName: "Before" },
......@@ -319,10 +323,10 @@ const MORE_VERBOSE_NAMES = {
}
export function getOperators(field, table) {
let type = getFieldType(field) || UNKNOWN;
const type = getFieldType(field) || UNKNOWN;
return OPERATORS_BY_TYPE_ORDERED[type].map(operatorForType => {
let operator = OPERATORS[operatorForType.name];
let verboseNameLower = operatorForType.verboseName.toLowerCase();
const operator = OPERATORS[operatorForType.name];
const verboseNameLower = operatorForType.verboseName.toLowerCase();
return {
...operator,
...operatorForType,
......@@ -432,11 +436,7 @@ function populateFields(aggregator, fields) {
// TODO: unit test
export function getAggregators(table) {
const supportedAggregations = Aggregators.filter(function (agg) {
if (agg.requiredDriverFeature && table.db && !_.contains(table.db.features, agg.requiredDriverFeature)) {
return false;
} else {
return true;
}
return !(agg.requiredDriverFeature && table.db && !_.contains(table.db.features, agg.requiredDriverFeature));
});
return _.map(supportedAggregations, function(aggregator) {
return populateFields(aggregator, table.fields);
......@@ -473,11 +473,11 @@ export function addValidOperatorsToFields(table) {
export function hasLatitudeAndLongitudeColumns(columnDefs) {
let hasLatitude = false;
let hasLongitude = false;
for (let col of columnDefs) {
if (col.special_type === "latitude") {
for (const col of columnDefs) {
if (isa(col.special_type, TYPE.Latitude)) {
hasLatitude = true;
}
if (col.special_type === "longitude") {
if (isa(col.special_type, TYPE.Longitude)) {
hasLongitude = true;
}
}
......@@ -507,6 +507,7 @@ export const ICON_MAPPING = {
[LOCATION]: 'location',
[COORDINATE]: 'location',
[STRING]: 'string',
[STRING_LIKE]: 'string',
[NUMBER]: 'int',
[BOOLEAN]: 'io'
};
......@@ -528,7 +529,7 @@ export function computeMetadataStrength(table) {
table.fields.forEach(function(field) {
score(field.description);
score(field.special_type);
if (field.special_type === "fk") {
if (isFK(field.special_type)) {
score(field.target);
}
});
......
import _ from "underscore";
import MetabaseSettings from "metabase/lib/settings";
const PARENTS = MetabaseSettings.get("types");
/// Basically exactly the same as Clojure's isa?
/// Recurses through the type hierarchy until it can give you an anser.
/// isa(TYPE.BigInteger, TYPE.Number) -> true
/// isa(TYPE.Text, TYPE.Boolean) -> false
export function isa(child, ancestor) {
if (!child || !ancestor) return false;
if (child === ancestor) return true;
const parents = PARENTS[child];
if (!parents) {
if (child !== "type/*") console.error("Invalid type:", child); // the base type is the only type with no parents, so anything else that gets here is invalid
return false;
}
for (const parent of parents) {
if (isa(parent, ancestor)) return true;
}
return false;
}
// build a pretty sweet dictionary of top-level types, so people can do TYPE.Latitude instead of "type/Latitude" and get error messages / etc.
// this should also make it easier to keep track of things when we tweak the type hierarchy
export let TYPE = {};
for (const type of _.keys(PARENTS)) {
const key = type.substring(5); // strip off "type/"
TYPE[key] = type;
}
// convenience functions since these operations are super-common
// this will also make it easier to tweak how these checks work in the future,
// e.g. when we add an `is_pk` column and eliminate the PK special type we can just look for places that use isPK
export function isPK(type) {
return isa(type, TYPE.PK);
}
export function isFK(type) {
return isa(type, TYPE.FK);
}
......@@ -4,6 +4,7 @@ import Base from "./Base";
import Table from "./Table";
import { isDate, isNumeric, isBoolean, isString, isSummable, isCategory, isDimension, isMetric, getIconForField } from "metabase/lib/schema_metadata";
import { isPK } from "metabase/lib/types";
export default class Field extends Base {
static type = "fields";
......@@ -35,7 +36,7 @@ export default class Field extends Base {
isCategory() { return isCategory(this._object); }
isMetric() { return isMetric(this._object); }
isDimension() { return isDimension(this._object); }
isID() { return this.special_type === "id"; }
isID() { return isPK(this.special_type); }
values() {
return (this._object.values && this._object.values.length > 0 && this._object.values[0].values) || []
......
......@@ -5,6 +5,7 @@ import Icon from 'metabase/components/Icon.jsx';
import IconBorder from 'metabase/components/IconBorder.jsx';
import LoadingSpinner from 'metabase/components/LoadingSpinner.jsx';
import { foreignKeyCountsByOriginTable } from 'metabase/lib/schema_metadata';
import { isPK } from "metabase/lib/types";
import { singularize, inflect } from 'inflection';
import cx from "classnames";
......@@ -37,7 +38,7 @@ export default class QueryVisualizationObjectDetailTable extends Component {
for (var i=0; i < this.props.data.cols.length; i++) {
var coldef = this.props.data.cols[i];
if (coldef.special_type === "id") {
if (isPK(coldef.special_type)) {
return this.props.data.rows[0][i];
}
}
......
......@@ -14,6 +14,7 @@ import { formatSQL, humanize } from "metabase/lib/formatting";
import Query from "metabase/lib/query";
import { createQuery } from "metabase/lib/query";
import { loadTableAndForeignKeys } from "metabase/lib/table";
import { isPK, isFK } from "metabase/lib/types";
import Utils from "metabase/lib/utils";
import { applyParameters } from "metabase/meta/Card";
......@@ -854,7 +855,7 @@ export const cellClicked = createThunkAction(CELL_CLICKED, (rowIndex, columnInde
isForeignColumn = coldef.table_id && coldef.table_id !== sourceTableID && coldef.fk_field_id,
fieldRefForm = isForeignColumn ? ['fk->', coldef.fk_field_id, coldef.id] : ['field-id', coldef.id];
if (coldef.special_type === "id") {
if (isPK(coldef.special_type)) {
// action is on a PK column
let newCard = startNewCard("query", card.dataset_query.database);
......@@ -866,7 +867,7 @@ export const cellClicked = createThunkAction(CELL_CLICKED, (rowIndex, columnInde
dispatch(setCardAndRun(newCard));
MetabaseAnalytics.trackEvent("QueryBuilder", "Table Cell Click", "PK");
} else if (coldef.special_type === "fk") {
} else if (isFK(coldef.special_type)) {
// action is on an FK column
let newCard = startNewCard("query", card.dataset_query.database);
......@@ -919,7 +920,7 @@ export const followForeignKey = createThunkAction(FOLLOW_FOREIGN_KEY, (fk) => {
// extract the value we will use to filter our new query
var originValue;
for (var i=0; i < queryResult.data.cols.length; i++) {
if (queryResult.data.cols[i].special_type === "id") {
if (isPK(queryResult.data.cols[i].special_type)) {
originValue = queryResult.data.rows[0][i];
}
}
......@@ -945,7 +946,7 @@ export const loadObjectDetailFKReferences = createThunkAction(LOAD_OBJECT_DETAIL
function getObjectDetailIdValue(data) {
for (var i=0; i < data.cols.length; i++) {
var coldef = data.cols[i];
if (coldef.special_type === "id") {
if (isPK(coldef.special_type)) {
return data.rows[0][i];
}
}
......
......@@ -6,6 +6,7 @@ import _ from "underscore";
import { AngularResourceProxy } from "metabase/lib/redux";
import { loadTableAndForeignKeys } from "metabase/lib/table";
import { isPK, isFK } from "metabase/lib/types";
import NotFound from "metabase/components/NotFound.jsx";
import QueryHeader from "../QueryHeader.jsx";
......@@ -54,11 +55,7 @@ function cellIsClickable(queryResult, rowIndex, columnIndex) {
if (!coldef || !coldef.special_type) return false;
if (coldef.table_id != null && coldef.special_type === 'id' || (coldef.special_type === 'fk' && coldef.target)) {
return true;
} else {
return false;
}
return (coldef.table_id != null && (isPK(coldef.special_type) || (isFK(coldef.special_type) && coldef.target)));
}
function autocompleteResults(card, prefix) {
......
......@@ -8,6 +8,7 @@ import { isCardDirty } from "metabase/lib/card";
import * as DataGrid from "metabase/lib/data_grid";
import Query from "metabase/lib/query";
import { parseFieldTarget } from "metabase/lib/query_time";
import { isPK } from "metabase/lib/types";
import { applyParameters } from "metabase/meta/Card";
export const uiControls = state => state.qb.uiControls;
......@@ -90,8 +91,7 @@ export const isObjectDetail = createSelector(
let pkField;
for (var i=0; i < data.cols.length; i++) {
let coldef = data.cols[i];
if (coldef.table_id === dataset_query.query.source_table &&
coldef.special_type === "id") {
if (coldef.table_id === dataset_query.query.source_table && isPK(coldef.special_type)) {
pkField = coldef.id;
}
}
......
......@@ -4,6 +4,7 @@ import { Link } from "react-router";
import * as MetabaseCore from "metabase/lib/core";
import { isNumericBaseType } from "metabase/lib/schema_metadata";
import { isa, isFK, TYPE } from "metabase/lib/types";
import i from 'icepick';
......@@ -23,7 +24,7 @@ const Field = ({
icon,
isEditing,
formField
}) =>
}) =>
<div className={cx(S.item)}>
<div className={S.leftIcons}>
{ icon && <Icon className={S.chartIcon} name={icon} size={20} /> }
......@@ -58,9 +59,8 @@ const Field = ({
'name': 'No field type',
'section': 'Other'
})
.filter(type => !isNumericBaseType(field) ?
!(type.id && type.id.startsWith("timestamp_")) :
true
.filter(type =>
isNumericBaseType(field) || !isa(type && type.id, TYPE.UNIXTimestamp)
)
}
onChange={(type) => formField.special_type.onChange(type.id)}
......@@ -84,8 +84,8 @@ const Field = ({
</div>
<div className={F.fieldForeignKey}>
{ isEditing ?
(formField.special_type.value === 'fk' ||
(field.special_type === 'fk' && formField.special_type.value === undefined)) &&
(isFK(formField.special_type.value) ||
(isFK(field.special_type) && formField.special_type.value === undefined)) &&
<Select
triggerClasses={F.fieldSelect}
placeholder="Select a field type"
......@@ -98,7 +98,7 @@ const Field = ({
onChange={(foreignKey) => formField.fk_target_field_id.onChange(foreignKey.id)}
optionNameFn={(foreignKey) => foreignKey.name}
/> :
field.special_type === 'fk' &&
isFK(field.special_type) &&
<span>
{i.getIn(foreignKeys, [field.fk_target_field_id, "name"])}
</span>
......
......@@ -5,6 +5,7 @@ import pure from "recompose/pure";
import * as MetabaseCore from "metabase/lib/core";
import { isNumericBaseType } from "metabase/lib/schema_metadata";
import { isFK } from "metabase/lib/types";
import Select from "metabase/components/Select.jsx";
......@@ -57,8 +58,8 @@ const FieldTypeDetail = ({
</span>
<span className="ml4">
{ isEditing ?
(fieldTypeFormField.value === 'fk' ||
(field.special_type === 'fk' && fieldTypeFormField.value === undefined)) &&
(isFK(fieldTypeFormField.value) ||
(isFK(field.special_type) && fieldTypeFormField.value === undefined)) &&
<Select
triggerClasses="rounded bordered p1 inline-block"
placeholder="Select a field type"
......@@ -71,7 +72,7 @@ const FieldTypeDetail = ({
onChange={(foreignKey) => foreignKeyFormField.onChange(foreignKey.id)}
optionNameFn={(foreignKey) => foreignKey.name}
/> :
field.special_type === 'fk' &&
isFK(field.special_type) &&
<span>
{i.getIn(foreignKeys, [field.fk_target_field_id, "name"])}
</span>
......
......@@ -2,6 +2,7 @@ import i from "icepick";
import { titleize, humanize } from "metabase/lib/formatting";
import { startNewCard, serializeCardForUrl } from "metabase/lib/card";
import { isPK } from "metabase/lib/types";
export const idsToObjectMap = (ids, objects) => ids
.map(id => objects[id])
......@@ -128,12 +129,12 @@ export const databaseToForeignKeys = (database) => database && database.tables_l
// ignore tables without primary key
.filter(table => table && table.fields_lookup &&
Object.values(table.fields_lookup)
.find(field => field.special_type === 'id')
.find(field => isPK(field.special_type))
)
.map(table => ({
table: table,
field: table && table.fields_lookup && Object.values(table.fields_lookup)
.find(field => field.special_type === 'id')
.find(field => isPK(field.special_type))
}))
.map(({ table, field }) => ({
id: field.id,
......
......@@ -7,6 +7,7 @@ import BarChart from "./BarChart.jsx";
import Urls from "metabase/lib/urls";
import { formatValue } from "metabase/lib/formatting";
import { TYPE } from "metabase/lib/types";
import { isSameSeries } from "metabase/visualizations/lib/utils";
import { getSettings } from "metabase/lib/visualization_settings";
......@@ -66,7 +67,7 @@ export default class Scalar extends Component {
card: { ...s.card, display: "bar" },
data: {
cols: [
{ base_type: "TextField", display_name: "Name", name: "dimension" },
{ base_type: TYPE.Text, display_name: "Name", name: "dimension" },
{ ...s.data.cols[0], display_name: "Value", name: "metric" }],
rows: [
[s.card.name, s.data.rows[0][0]]
......
import "metabase/vendor";
window.MetabaseBootstrap = {
"timezones": [
"GMT",
"UTC",
"US\/Alaska",
"US\/Arizona",
"US\/Central",
"US\/Eastern",
"US\/Hawaii",
"US\/Mountain",
"US\/Pacific",
"America\/Costa_Rica"
]
timezones: [
"GMT",
"UTC",
"US\/Alaska",
"US\/Arizona",
"US\/Central",
"US\/Eastern",
"US\/Hawaii",
"US\/Mountain",
"US\/Pacific",
"America\/Costa_Rica"
],
types: {
"type/Address": ["type/*"],
"type/Array": ["type/Collection"],
"type/AvatarURL": ["type/URL"],
"type/BigInteger": ["type/Integer"],
"type/Boolean": ["type/*"],
"type/Category": ["type/Special"],
"type/City": ["type/Category", "type/Address", "type/Text"],
"type/Collection": ["type/*"],
"type/Coordinate": ["type/Float"],
"type/Country": ["type/Category", "type/Address", "type/Text"],
"type/Date": ["type/DateTime"],
"type/DateTime": ["type/*"],
"type/Decimal": ["type/Float"],
"type/Description": ["type/Text"],
"type/Dictionary": ["type/Collection"],
"type/FK": ["type/Special"],
"type/Float": ["type/Number"],
"type/IPAddress": ["type/TextLike"],
"type/ImageURL": ["type/URL"],
"type/Integer": ["type/Number"],
"type/Latitude": ["type/Coordinate"],
"type/Longitude": ["type/Coordinate"],
"type/Name": ["type/Category", "type/Address", "type/Text"],
"type/Number": ["type/*"],
"type/PK": ["type/Special"],
"type/SerializedJSON": ["type/Text", "type/Collection"],
"type/Special": ["type/*"],
"type/State": ["type/Category", "type/Address", "type/Text"],
"type/Text": ["type/*"],
"type/TextLike": ["type/*"],
"type/Time": ["type/DateTime"],
"type/UNIXTimestamp": ["type/Integer", "type/DateTime"],
"type/UNIXTimestampMilliseconds": ["type/UNIXTimestamp"],
"type/UNIXTimestampSeconds": ["type/UNIXTimestamp"],
"type/URL": ["type/Text"],
"type/UUID": ["type/Text"],
"type/Zip": ["type/Address"],
"type/ZipCode": ["type/Integer"]
}
};
import { pivot } from "metabase/lib/data_grid";
import { TYPE } from "metabase/lib/types";
function makeData(rows) {
return {
rows: rows,
cols: [
{ name: "D1", display_name: "Dimension 1", base_type: "TextField" },
{ name: "D2", display_name: "Dimension 2", base_type: "TextField" },
{ name: "M", display_name: "Metric", base_type: "IntegerField" }
{ name: "D1", display_name: "Dimension 1", base_type: TYPE.Text },
{ name: "D2", display_name: "Dimension 2", base_type: TYPE.Text },
{ name: "M", display_name: "Metric", base_type: TYPE.Integer }
]
}
};
}
describe("data_grid", () => {
......
......@@ -2,6 +2,7 @@ import {
getFieldType,
DATE_TIME,
STRING,
STRING_LIKE,
NUMBER,
BOOLEAN,
LOCATION,
......@@ -9,35 +10,40 @@ import {
foreignKeyCountsByOriginTable
} from 'metabase/lib/schema_metadata';
import { TYPE } from "metabase/lib/types";
describe('schema_metadata', () => {
describe('getFieldType', () => {
it('should know a date', () => {
expect(getFieldType({ base_type: 'DateField' })).toEqual(DATE_TIME)
expect(getFieldType({ base_type: 'DateTimeField' })).toEqual(DATE_TIME)
expect(getFieldType({ base_type: 'TimeField' })).toEqual(DATE_TIME)
expect(getFieldType({ special_type: 'timestamp_seconds' })).toEqual(DATE_TIME)
expect(getFieldType({ special_type: 'timestamp_milliseconds' })).toEqual(DATE_TIME)
expect(getFieldType({ base_type: TYPE.Date })).toEqual(DATE_TIME)
expect(getFieldType({ base_type: TYPE.DateTime })).toEqual(DATE_TIME)
expect(getFieldType({ base_type: TYPE.Time })).toEqual(DATE_TIME)
expect(getFieldType({ special_type: TYPE.UNIXTimestampSeconds })).toEqual(DATE_TIME)
expect(getFieldType({ special_type: TYPE.UNIXTimestampMilliseconds })).toEqual(DATE_TIME)
});
it('should know a number', () => {
expect(getFieldType({ base_type: 'BigIntegerField' })).toEqual(NUMBER)
expect(getFieldType({ base_type: 'IntegerField' })).toEqual(NUMBER)
expect(getFieldType({ base_type: 'FloatField' })).toEqual(NUMBER)
expect(getFieldType({ base_type: 'DecimalField' })).toEqual(NUMBER)
expect(getFieldType({ base_type: TYPE.BigInteger })).toEqual(NUMBER)
expect(getFieldType({ base_type: TYPE.Integer })).toEqual(NUMBER)
expect(getFieldType({ base_type: TYPE.Float })).toEqual(NUMBER)
expect(getFieldType({ base_type: TYPE.Decimal })).toEqual(NUMBER)
});
it('should know a string', () => {
expect(getFieldType({ base_type: 'CharField' })).toEqual(STRING)
expect(getFieldType({ base_type: 'TextField' })).toEqual(STRING)
expect(getFieldType({ base_type: TYPE.Text })).toEqual(STRING)
});
it('should know a bool', () => {
expect(getFieldType({ base_type: 'BooleanField' })).toEqual(BOOLEAN)
expect(getFieldType({ base_type: TYPE.Boolean })).toEqual(BOOLEAN)
});
it('should know a location', () => {
expect(getFieldType({ special_type: 'city' })).toEqual(LOCATION)
expect(getFieldType({ special_type: 'country' })).toEqual(LOCATION)
expect(getFieldType({ special_type: TYPE.City })).toEqual(LOCATION)
expect(getFieldType({ special_type: TYPE.Country })).toEqual(LOCATION)
});
it('should know a coordinate', () => {
expect(getFieldType({ special_type: 'latitude' })).toEqual(COORDINATE)
expect(getFieldType({ special_type: 'longitude' })).toEqual(COORDINATE)
expect(getFieldType({ special_type: TYPE.Latitude })).toEqual(COORDINATE)
expect(getFieldType({ special_type: TYPE.Longitude })).toEqual(COORDINATE)
});
it('should know something that is string-like', () => {
expect(getFieldType({ base_type: TYPE.TextLike })).toEqual(STRING_LIKE);
expect(getFieldType({ base_type: TYPE.IPAddress })).toEqual(STRING_LIKE);
});
it('should know what it doesn\'t know', () => {
expect(getFieldType({ base_type: 'DERP DERP DERP' })).toEqual(undefined)
......
......@@ -8,6 +8,8 @@ import {
getQuestion
} from 'metabase/reference/utils';
import { TYPE } from "metabase/lib/types";
describe("Reference utils.js", () => {
const getProps = ({
section = {
......@@ -153,7 +155,7 @@ describe("Reference utils.js", () => {
fields_lookup: {
1: {
id: 1,
special_type: 'id',
special_type: TYPE.PK,
display_name: 'bar',
description: 'foobar'
}
......@@ -166,7 +168,7 @@ describe("Reference utils.js", () => {
fields_lookup: {
2: {
id: 2,
special_type: 'id',
special_type: TYPE.PK,
display_name: 'foo',
description: 'barfoo'
}
......
......@@ -3,6 +3,8 @@ import {
computeTimeseriesDataInverval
} from 'metabase/visualizations/lib/timeseries';
import { TYPE } from "metabase/lib/types";
describe('visualization.lib.timeseries', () => {
describe('dimensionIsTimeseries', () => {
// examples from https://en.wikipedia.org/wiki/ISO_8601
......@@ -22,23 +24,23 @@ describe('visualization.lib.timeseries', () => {
"scanner 005"
];
it("should detect DateField column as timeseries", () => {
expect(dimensionIsTimeseries({ cols: [{ base_type: "DateField" }]})).toBe(true);
it("should detect Date column as timeseries", () => {
expect(dimensionIsTimeseries({ cols: [{ base_type: TYPE.Date }]})).toBe(true);
});
it("should detect TimeField column as timeseries", () => {
expect(dimensionIsTimeseries({ cols: [{ base_type: "TimeField" }]})).toBe(true);
it("should detect Time column as timeseries", () => {
expect(dimensionIsTimeseries({ cols: [{ base_type: TYPE.Time }]})).toBe(true);
});
it("should detect DateTimeField column as timeseries", () => {
expect(dimensionIsTimeseries({ cols: [{ base_type: "DateTimeField" }]})).toBe(true);
it("should detect DateTime column as timeseries", () => {
expect(dimensionIsTimeseries({ cols: [{ base_type: TYPE.DateTime }]})).toBe(true);
});
ISO_8601_DATES.forEach(isoDate => {
it("should detect values with ISO 8601 formatted string '" + isoDate + "' as timeseries", () => {
expect(dimensionIsTimeseries({ cols: [{ base_type: "TextField" }], rows: [[isoDate]]})).toBe(true);
expect(dimensionIsTimeseries({ cols: [{ base_type: TYPE.Text }], rows: [[isoDate]]})).toBe(true);
})
});
NOT_DATES.forEach(notDate => {
it("should not detect value '" + notDate + "' as timeseries", () => {
expect(dimensionIsTimeseries({ cols: [{ base_type: "TextField" }], rows: [[notDate]]})).toBe(false);
expect(dimensionIsTimeseries({ cols: [{ base_type: TYPE.Text }], rows: [[notDate]]})).toBe(false);
});
});
});
......
No preview for this file type
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment