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

Time groupings

parent 94b86141
No related branches found
No related tags found
No related merge requests found
......@@ -542,17 +542,35 @@
border-right: 1px solid color(var(--base-grey) shade(40%));
}
.FieldList-item .Icon {
.List-item {
display: flex;
border-radius: 6px;
border: 2px solid transparent;
}
.List-item .Icon {
color: #ddd;
}
.FieldList-item:hover,
.FieldList-item--selected {
.List-item:hover,
.List-item--selected {
background-color: #B8A2CC;
border-color: rgba(0,0,0,0.2);
color: white;
}
.FieldList-item:hover .Icon,
.FieldList-item--selected .Icon {
.List-item:hover .Icon,
.List-item--selected .Icon {
color: white;
}
.FieldList-grouping-trigger {
display: none;
}
.List-item:hover .FieldList-grouping-trigger,
.List-item--selected .FieldList-grouping-trigger {
display: flex;
border-left: 2px solid rgba(0,0,0,0.1);
color: rgba(255,255,255,0.5);
}
......@@ -340,6 +340,7 @@ var Query = {
typeof field === "number" ||
(Array.isArray(field) && (
(field[0] === 'fk->' && typeof field[1] === "number" && typeof field[2] === "number") ||
(field[0] === 'datetime_field' && Query.isValidField(field[1]) && field[2] === "as" && typeof field[3] === "string") ||
(field[0] === 'aggregation' && typeof field[1] === "number")
))
);
......
......@@ -12,7 +12,7 @@ export function computeFilterTimeRange(filter) {
}
let [operator, field, ...values] = expandedFilter;
let bucketing = parseBucketing(field);
let bucketing = parseFieldBucketing(field);
let start, end;
if (operator === "=" && values[0]) {
......@@ -63,7 +63,7 @@ export function expandTimeIntervalFilter(filter) {
export function generateTimeFilterValuesDescriptions(filter) {
let [operator, field, ...values] = filter;
let bucketing = parseBucketing(field);
let bucketing = parseFieldBucketing(field);
if (operator === "TIME_INTERVAL") {
let [n, unit] = values;
......@@ -132,6 +132,12 @@ export function generateTimeValueDescription(value, bucketing) {
}
}
export function formatBucketing(bucketing) {
let words = bucketing.split("-");
words[0] = inflection.capitalize(words[0]);
return words.join(" ");
}
export function absolute(date) {
if (typeof date === "string") {
return moment(date);
......@@ -142,10 +148,12 @@ export function absolute(date) {
}
}
function parseBucketing(field) {
export function parseFieldBucketing(field) {
if (Array.isArray(field)) {
if (field[0] === "datetime_field") {
return field[3];
} if (field[0] === "fk->") {
return "day";
} else {
console.warn("Unknown field format", field);
}
......@@ -153,6 +161,19 @@ function parseBucketing(field) {
return "day";
}
export function parseFieldTarget(field) {
if (Array.isArray(field)) {
if (field[0] === "datetime_field") {
return field[1];
} if (field[0] === "fk->") {
return field;
} else {
console.warn("Unknown field format", field);
}
}
return field;
}
// 271821 BC and 275760 AD and should be far enough in the past/future
function max() {
return moment(new Date(864000000000000));
......
"use strict";
import React, { Component, PropTypes } from "react";
import _ from "underscore";
import cx from "classnames";
import { getUmbrellaType, TIME, NUMBER, STRING, LOCATION } from 'metabase/lib/schema_metadata';
import Icon from "metabase/components/Icon.react";
import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.react";
import TimeGroupingPopover from "./TimeGroupingPopover.react";
import { isDate, getUmbrellaType, TIME, NUMBER, STRING, LOCATION } from 'metabase/lib/schema_metadata';
import { parseFieldBucketing, parseFieldTarget } from "metabase/lib/query_time";
import _ from "underscore";
import cx from "classnames";
export default class FieldList extends Component {
constructor(props) {
......@@ -50,14 +55,24 @@ export default class FieldList extends Component {
return <Icon name={name} width={width} height={height} />
}
renderTimeGroupingTrigger(field) {
return (
<div className="FieldList-grouping-trigger flex align-center p1 cursor-pointer">
<h4 className="mr1">by {parseFieldBucketing(field).split("-").join(" ")}</h4>
<Icon name="chevronright" width={16} height={16} />
</div>
)
}
render() {
let { tableName, fieldOptions } = this.props;
let { tableName, field, fieldOptions } = this.props;
let fieldTarget = parseFieldTarget(field);
let mainSection = {
name: tableName,
fields: fieldOptions.fields.map(field => ({
types: { base_type: field.base_type, special_type: field.special_type },
name: field.display_name,
field: field,
value: field.id
}))
};
......@@ -65,8 +80,7 @@ export default class FieldList extends Component {
let fkSections = fieldOptions.fks.map(fk => ({
name: fk.field.target.table.display_name,
fields: fk.fields.map(field => ({
types: { base_type: field.base_type, special_type: field.special_type },
name: field.display_name,
field: field,
value: ["fk->", fk.field.id, field.id]
}))
}));
......@@ -85,15 +99,34 @@ export default class FieldList extends Component {
</div>
: null }
{ this.state.openSection === sectionIndex ?
<ul className="border-bottom">
{section.fields.map((field, fieldIndex) => {
<ul className="border-bottom p1">
{section.fields.map((item, itemIndex) => {
return (
<li key={fieldIndex}>
<a className={cx('FieldList-item', 'flex align-center px2 py1 cursor-pointer', { 'FieldList-item--selected': _.isEqual(this.props.field, field.value) })}
onClick={this.props.setField.bind(null, field.value)}>
{ this.renderTypeIcon(field.types) }
<h4 className="ml1">{field.name}</h4>
</a>
<li key={itemIndex} className={cx("List-item flex", { 'List-item--selected': _.isEqual(fieldTarget, item.value) })}>
<a className="flex-full flex align-center px2 py1 cursor-pointer"
onClick={this.props.onFieldChange.bind(null, item.value)}
>
{ this.renderTypeIcon(item.field) }
<h4 className="ml1">{item.field.display_name}</h4>
</a>
{ this.props.enableTimeGrouping && isDate(item.field) ?
<PopoverWithTrigger
className="PopoverBody"
triggerElement={this.renderTimeGroupingTrigger(field)}
tetherOptions={{
attachment: 'top left',
targetAttachment: 'top right',
targetOffset: '0 0'
// constraints: [{ to: 'window', attachment: 'together', pin: ['top', 'bottom']}]
}}
>
<TimeGroupingPopover
field={field}
value={item.value}
onFieldChange={this.props.onFieldChange}
/>
</PopoverWithTrigger>
: null }
</li>
)
})}
......@@ -110,5 +143,6 @@ FieldList.propTypes = {
field: PropTypes.oneOfType([React.PropTypes.number, React.PropTypes.array]),
fieldOptions: PropTypes.object.isRequired,
tableName: PropTypes.string,
setField: PropTypes.func.isRequired
onFieldChange: PropTypes.func.isRequired,
enableTimeGrouping: PropTypes.bool
};
......@@ -5,6 +5,7 @@ import _ from "underscore";
import Icon from "metabase/components/Icon.react";
import Query from "metabase/lib/query";
import { parseFieldTarget, parseFieldBucketing, formatBucketing } from "metabase/lib/query_time";
import cx from "classnames";
......@@ -27,6 +28,9 @@ export default React.createClass({
let targetTitle, fkTitle, fkIcon;
let { field, fieldOptions } = this.props;
let bucketing = parseFieldBucketing(field);
field = parseFieldTarget(field);
if (Array.isArray(field) && field[0] === 'fk->') {
var fkDef = _.find(fieldOptions.fks, (fk) => _.isEqual(fk.field.id, field[1]));
if (fkDef) {
......@@ -44,9 +48,14 @@ export default React.createClass({
}
}
let bucketingTitle;
if (bucketing !== "day") {
bucketingTitle = ": " + formatBucketing(bucketing);
}
var titleElement;
if (fkTitle || targetTitle) {
titleElement = <span className="QueryOption">{fkTitle}{fkIcon}{targetTitle}</span>;
titleElement = <span className="QueryOption">{fkTitle}{fkIcon}{targetTitle}{bucketingTitle}</span>;
} else {
titleElement = <span className="QueryOption">field</span>;
}
......
......@@ -51,7 +51,8 @@ export default React.createClass({
tableName={this.props.tableName}
field={this.props.field}
fieldOptions={this.props.fieldOptions}
setField={this.setField}
onFieldChange={this.setField}
enableTimeGrouping={true}
/>
</Popover>
);
......
"use strict";
import React, { Component, PropTypes } from "react";
import { parseFieldBucketing, formatBucketing } from "metabase/lib/query_time";
import cx from "classnames";
const BUCKETINGS = [
// "default",
// "minute",
"hour",
"day",
"week",
"month",
"quarter",
"year",
null,
// "minute-of-hour",
// "hour-of-day",
"day-of-week",
// "day-of-month",
// "day-of-year",
"week-of-year",
"month-of-year",
// "quarter-of-year",
];
export default class TimeGroupingPopover extends Component {
constructor(props) {
super(props);
this.state = {};
}
setField(bucketing) {
this.props.onFieldChange(["datetime_field", this.props.value, "as", bucketing]);
}
render() {
let { field } = this.props;
return (
<div className="p2" style={{width:"250px"}}>
<h3 className="mx2">Group time by</h3>
<ul className="py1">
{ BUCKETINGS.map((bucketing, bucketingIndex) =>
bucketing == null ?
<hr />
:
<li className={cx("List-item", { "List-item--selected": parseFieldBucketing(field) === bucketing })}>
<a className="px2 py1 cursor-pointer" onClick={this.setField.bind(this, bucketing)}>
{formatBucketing(bucketing)}
</a>
</li>
)}
</ul>
</div>
);
}
}
TimeGroupingPopover.propTypes = {
field: PropTypes.oneOfType([React.PropTypes.number, React.PropTypes.array]),
value: PropTypes.oneOfType([React.PropTypes.number, React.PropTypes.array]),
onFieldChange: PropTypes.func.isRequired
};
......@@ -186,7 +186,7 @@ export default class FilterPopover extends Component {
field={this.state.filter[1]}
fieldOptions={Query.getFieldOptions(this.props.tableMetadata.fields, true)}
tableName={this.props.tableMetadata.display_name}
setField={this.setField}
onFieldChange={this.setField}
/>
</div>
);
......
......@@ -93,7 +93,6 @@ export default React.createClass({
if (isDate(fieldDef)) {
values = generateTimeFilterValuesDescriptions(this.props.filter);
console.log("XXX", values)
}
// the first 2 positions of the filter are always for fieldId + fieldOperator
......
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