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

Merge pull request #2885 from metabase/data-reference

Data reference feature
parents 4d702734 aed75790
Branches
Tags
No related merge requests found
Showing
with 271 additions and 45 deletions
......@@ -36,6 +36,13 @@ import NotFound from "metabase/components/NotFound.jsx";
import Unauthorized from "metabase/components/Unauthorized.jsx";
import ReferenceApp from "metabase/reference/containers/ReferenceApp.jsx";
import ReferenceEntity from "metabase/reference/containers/ReferenceEntity.jsx";
import ReferenceEntityList from "metabase/reference/containers/ReferenceEntityList.jsx";
import ReferenceFieldsList from "metabase/reference/containers/ReferenceFieldsList.jsx";
import ReferenceRevisionsList from "metabase/reference/containers/ReferenceRevisionsList.jsx";
import ReferenceGettingStartedGuide from "metabase/reference/containers/ReferenceGettingStartedGuide.jsx";
export default class Routes extends Component {
// this lets us forward props we've injected from the Angular controller
_forwardProps(ComposedComponent, propNames) {
......@@ -72,6 +79,27 @@ export default class Routes extends Component {
<Route path="settings" component={this._forwardProps(SettingsEditorApp, ["refreshSiteSettings"])} />
</Route>
<Route path="/reference" component={ReferenceApp}>
<Route path="guide" component={ReferenceGettingStartedGuide} />
<Route path="metrics" component={ReferenceEntityList} />
<Route path="metrics/:metricId" component={ReferenceEntity} />
<Route path="metrics/:metricId/questions" component={ReferenceEntityList} />
<Route path="metrics/:metricId/revisions" component={ReferenceRevisionsList} />
<Route path="segments" component={ReferenceEntityList} />
<Route path="segments/:segmentId" component={ReferenceEntity} />
<Route path="segments/:segmentId/fields" component={ReferenceFieldsList} />
<Route path="segments/:segmentId/fields/:fieldId" component={ReferenceEntity} />
<Route path="segments/:segmentId/questions" component={ReferenceEntityList} />
<Route path="segments/:segmentId/revisions" component={ReferenceRevisionsList} />
<Route path="databases" component={ReferenceEntityList} />
<Route path="databases/:databaseId" component={ReferenceEntity} />
<Route path="databases/:databaseId/tables" component={ReferenceEntityList} />
<Route path="databases/:databaseId/tables/:tableId" component={ReferenceEntity} />
<Route path="databases/:databaseId/tables/:tableId/fields" component={ReferenceFieldsList} />
<Route path="databases/:databaseId/tables/:tableId/fields/:fieldId" component={ReferenceEntity} />
<Route path="databases/:databaseId/tables/:tableId/questions" component={ReferenceEntityList} />
</Route>
<Route path="/auth/forgot_password" component={ForgotPasswordApp} />
<Route path="/auth/login" component={this._forwardProps(LoginApp, ["onChangeLocation", "setSessionFn"])} />
<Route path="/auth/logout" component={this._forwardProps(LogoutApp, ["onChangeLocation"])} />
......
......@@ -5,7 +5,7 @@ import Select from "metabase/components/Select.jsx";
import * as MetabaseCore from "metabase/lib/core";
import { titleize, humanize } from "metabase/lib/formatting";
import { isNumeric } from "metabase/lib/schema_metadata";
import { isNumericBaseType } from "metabase/lib/schema_metadata";
import _ from "underscore";
......@@ -77,7 +77,7 @@ export default class Column extends Component {
let specialTypes = MetabaseCore.field_special_types.slice(0);
specialTypes.push({'id': null, 'name': 'No special type', 'section': 'Other'});
// if we don't have a numeric base-type then prevent the options for unix timestamp conversion (#823)
if (!isNumeric(this.props.field)) {
if (!isNumericBaseType(this.props.field)) {
specialTypes = specialTypes.filter((f) => !(f.id && f.id.startsWith("timestamp_")));
}
......
......@@ -42,7 +42,7 @@ export default class MetadataHeader extends Component {
var triggerElement = (
<span className="text-bold cursor-pointer text-default">
{database.name}
<Icon className="ml1" name="chevrondown" width="8px" height="8px"/>
<Icon className="ml1" name="chevrondown" size={8}/>
</span>
);
return (
......
......@@ -34,7 +34,7 @@ export default class MetadataSchemaList extends Component {
return (
<div className="MetadataEditor-table-list AdminList flex-no-shrink">
<div className="AdminList-search">
<Icon name="search" width="16" height="16"/>
<Icon name="search" size={16}/>
<input
className="AdminInput pl4 border-bottom"
type="text"
......
......@@ -73,7 +73,7 @@ export default class MetadataTableList extends Component {
return (
<div className="MetadataEditor-table-list AdminList flex-no-shrink">
<div className="AdminList-search">
<Icon name="search" width="16" height="16"/>
<Icon name="search" size={16}/>
<input
className="AdminInput pl4 border-bottom"
type="text"
......@@ -85,7 +85,7 @@ export default class MetadataTableList extends Component {
{ (this.props.onBack || this.props.schema) &&
<h4 className="p2 border-bottom">
{ this.props.onBack &&
<span className="text-brand cursor-pointer" onClick={this.props.onBack}><Icon name="chevronleft" width={10} height={10}/> Schemas</span>
<span className="text-brand cursor-pointer" onClick={this.props.onBack}><Icon name="chevronleft" size={10}/> Schemas</span>
}
{ this.props.onBack && this.props.schema && <span className="mx1">-</span>}
{ this.props.schema && <span> {this.props.schema.name}</span>}
......
......@@ -17,12 +17,12 @@ export default class RevisionDiff extends Component {
let icon;
if (before != null && after != null) {
icon = <Icon name="pencil" className="text-brand" width={16} height={16} />
icon = <Icon name="pencil" className="text-brand" size={16} />
} else if (before != null) {
icon = <Icon name="add" className="text-error" width={16} height={16} />
icon = <Icon name="add" className="text-error" size={16} />
} else {
// TODO: "minus" icon
icon = <Icon name="add" className="text-green" width={16} height={16} />
icon = <Icon name="add" className="text-green" size={16} />
}
return (
......
......@@ -28,7 +28,7 @@ export default class UserRoleSelect extends Component {
const triggerElement = (
<div className={"flex align-center"}>
<span className="mr1">{roleDef.name}</span>
<Icon className="text-grey-2" name="chevrondown" width="10" height="10"/>
<Icon className="text-grey-2" name="chevrondown" size={10}/>
</div>
);
......
......@@ -207,7 +207,7 @@ export default class SettingsSlackForm extends Component {
<div className="pt3">
<a href="https://api.slack.com/docs/oauth-test-tokens" target="_blank" className="Button Button--primary" style={{padding:0}}>
<div className="float-left py2 pl2">Get an API token from Slack</div>
<Icon className="float-right p2 text-white cursor-pointer" style={{opacity:0.6}} name="external" width={18} height={18}/>
<Icon className="float-right p2 text-white cursor-pointer" style={{opacity:0.6}} name="external" size={18}/>
</a>
</div>
<div className="py2">
......
......@@ -29,6 +29,10 @@ import Routes from "./Routes.jsx";
import auth from "metabase/auth/auth";
/* ducks */
import metadata from "metabase/redux/metadata";
import requests from "metabase/redux/requests";
/* admin */
import settings from "metabase/admin/settings/settings";
import * as people from "metabase/admin/people/reducers";
......@@ -37,7 +41,6 @@ import datamodel from "metabase/admin/datamodel/metadata";
/* dashboards */
import dashboard from "metabase/dashboard/dashboard";
import metadata from "metabase/dashboard/metadata";
import * as home from "metabase/home/reducers";
/* questions / query builder */
......@@ -46,6 +49,9 @@ import labels from "metabase/questions/labels";
import undo from "metabase/questions/undo";
import * as qb from "metabase/query_builder/reducers";
/* data reference */
import reference from "metabase/reference/reference";
/* pulses */
import * as pulse from "metabase/pulse/reducers";
......@@ -68,6 +74,7 @@ const reducers = combineReducers({
auth,
currentUser,
metadata,
requests,
// main app reducers
dashboard,
......@@ -76,6 +83,7 @@ const reducers = combineReducers({
pulse: combineReducers(pulse),
qb: combineReducers(qb),
questions,
reference,
setup: combineReducers(setup),
undo,
user: combineReducers(user),
......@@ -135,6 +143,29 @@ angular.module('metabase', [
$routeProvider.when('/admin/people/', route);
$routeProvider.when('/admin/settings/', { ...route, template: '<div class="full-height" mb-redux-component />' });
$routeProvider.when('/reference', route);
$routeProvider.when('/reference/guide', route);
$routeProvider.when('/reference/metrics', route);
$routeProvider.when('/reference/metrics/:metricId', route);
$routeProvider.when('/reference/metrics/:metricId/questions', route);
$routeProvider.when('/reference/metrics/:metricId/questions/:cardId', route);
$routeProvider.when('/reference/metrics/:metricId/revisions', route);
$routeProvider.when('/reference/segments', route);
$routeProvider.when('/reference/segments/:segmentId', route);
$routeProvider.when('/reference/segments/:segmentId/fields', route);
$routeProvider.when('/reference/segments/:segmentId/fields/:fieldId', route);
$routeProvider.when('/reference/segments/:segmentId/questions', route);
$routeProvider.when('/reference/segments/:segmentId/questions/:cardId', route);
$routeProvider.when('/reference/segments/:segmentId/revisions', route);
$routeProvider.when('/reference/databases', route);
$routeProvider.when('/reference/databases/:databaseId', route);
$routeProvider.when('/reference/databases/:databaseId/tables', route);
$routeProvider.when('/reference/databases/:databaseId/tables/:tableId', route);
$routeProvider.when('/reference/databases/:databaseId/tables/:tableId/fields', route);
$routeProvider.when('/reference/databases/:databaseId/tables/:tableId/fields/:fieldId', route);
$routeProvider.when('/reference/databases/:databaseId/tables/:tableId/questions', route);
$routeProvider.when('/reference/databases/:databaseId/tables/:tableId/questions/:cardId', route);
$routeProvider.when('/auth/', { redirectTo: () => ('/auth/login') });
$routeProvider.when('/auth/forgot_password', { ...route, template: '<div mb-redux-component class="full-height" />' });
$routeProvider.when('/auth/login', { ...route, template: '<div mb-redux-component class="full-height" />' });
......
......@@ -168,7 +168,7 @@ export default class AccordianList extends Component {
<h3 className="List-section-title">{section.name}</h3>
{ section.items.length > 0 &&
<span className="flex-align-right">
<Icon name={sectionIsOpen(sectionIndex) ? "chevronup" : "chevrondown"} width={12} height={12} />
<Icon name={sectionIsOpen(sectionIndex) ? "chevronup" : "chevrondown"} size={12} />
</span>
}
</div>
......@@ -211,7 +211,7 @@ export default class AccordianList extends Component {
{ this.renderItemExtra(item, itemIndex) }
{ showItemArrows &&
<div className="List-item-arrow flex align-center px1">
<Icon name="chevronright" width={8} height={8} />
<Icon name="chevronright" size={8} />
</div>
}
</li>
......
......@@ -88,7 +88,7 @@ export default class ActionButton extends Component {
this.props.activeText
: this.state.result === "success" ?
<span>
<Icon name='check' width="12px" height="12px" />
<Icon name='check' size={12} />
<span className="ml1">{this.props.successText}</span>
</span>
: this.state.result === "failed" ?
......
......@@ -3,9 +3,12 @@
--breadcrumbs-padding: 2.075em;
--breadcrumb-page-color: #636060;
--breadcrumb-divider-spacing: 0.75em;
/* taken from Sidebar.css, should probably factor them out into variables */
--sidebar-breadcrumbs-color: #9CAEBE;
--sidebar-breadcrumb-page-color: #2D86D4;
}
.Breadcrumbs {
:local(.breadcrumbs) {
display: flex;
align-items: center;
padding-top: var(--breadcrumbs-padding);
......@@ -13,29 +16,59 @@
color: var(--breadcrumbs-color);
}
.Breadcrumb {
:local(.breadcrumb) {
font-size: 0.75rem;
font-weight: bold;
text-transform: uppercase;
}
.Breadcrumb-divider {
:local(.breadcrumbDivider) {
margin-left: var(--breadcrumb-divider-spacing);
margin-right: var(--breadcrumb-divider-spacing);
flex-shrink: 0;
}
/* the breadcrumb path will always inherit the color of the breadcrumbs object */
.Breadcrumb.Breadcrumb--path {
:local(.breadcrumb.breadcrumbPath) {
color: currentColor;
transition: color .3s linear;
}
.Breadcrumb.Breadcrumb--path:hover {
:local(.breadcrumb.breadcrumbPath):hover {
color: var(--breadcrumb-page-color);
transition: color .3s linear;
}
/* the breadcrumb page (current page) should be a different contrasting color */
.Breadcrumb.Breadcrumb--page {
:local(.breadcrumb.breadcrumbPage) {
color: var(--breadcrumb-page-color);
}
:local(.sidebarBreadcrumbs) {
composes: flex from "style";
composes: breadcrumbs;
color: var(--sidebar-breadcrumbs-color);
max-width: 100%;
}
:local(.sidebarBreadcrumb) {
composes: breadcrumb;
height: 15px;
}
/* the breadcrumb path will always inherit the color of the breadcrumbs object */
:local(.sidebarBreadcrumb.breadcrumbPath) {
color: currentColor;
transition: color .3s linear;
}
:local(.sidebarBreadcrumb.breadcrumbPath):hover {
color: var(--sidebar-breadcrumb-page-color);
transition: color .3s linear;
}
/* the breadcrumb page (current page) should be a different contrasting color */
:local(.sidebarBreadcrumb.breadcrumbPage) {
color: var(--sidebar-breadcrumb-page-color);
}
import React, { Component, PropTypes } from "react";
import "./Breadcrumbs.css";
import S from "./Breadcrumbs.css";
import Icon from "metabase/components/Icon.jsx"
import Icon from "metabase/components/Icon.jsx";
import Ellipsified from "metabase/components/Ellipsified.jsx";
import cx from 'classnames';
export default class Breadcrumbs extends Component {
static propTypes = {
crumbs: PropTypes.array
crumbs: PropTypes.array,
inSidebar: PropTypes.bool,
placeholder: PropTypes.string
};
static defaultProps = {
crumbs: []
crumbs: [],
inSidebar: false,
placeholder: null
};
render() {
const children = [];
for (let [index, crumb] of this.props.crumbs.entries()) {
crumb = Array.isArray(crumb) ? crumb : [crumb];
if (crumb.length > 1) {
children.push(<a className="Breadcrumb Breadcrumb--path" href={crumb[1]}>{crumb[0]}</a>);
} else {
children.push(<h2 className="Breadcrumb Breadcrumb--page">{crumb}</h2>);
}
if (index < this.props.crumbs.length - 1) {
children.push(<Icon name="chevronright" className="Breadcrumb-divider" width={12} height={12} />);
}
}
const {
crumbs,
inSidebar,
placeholder
} = this.props;
const breadcrumbClass = inSidebar ? S.sidebarBreadcrumb : S.breadcrumb;
const breadcrumbsClass = inSidebar ? S.sidebarBreadcrumbs : S.breadcrumbs;
return (
<section className="Breadcrumbs">
{children}
<section className={breadcrumbsClass}>
{ crumbs.length <= 1 && placeholder ?
<span className={cx(breadcrumbClass, S.breadcrumbPage)}>
{placeholder}
</span> :
crumbs
.map(breadcrumb => Array.isArray(breadcrumb) ?
breadcrumb : [breadcrumb]
)
.map((breadcrumb, index) =>
<Ellipsified
key={index}
tooltip={breadcrumb[0]}
tooltipMaxWidth="100%"
className={cx(
breadcrumbClass,
breadcrumb.length > 1 ?
S.breadcrumbPath : S.breadcrumbPage
)}
>
{ breadcrumb.length > 1 ?
<a href={breadcrumb[1]}>{breadcrumb[0]}</a> :
<span>{breadcrumb[0]}</span>
}
</Ellipsified>
)
.map((breadcrumb, index, breadcrumbs) =>
index < breadcrumbs.length - 1 ?
[
breadcrumb,
<Icon
key={`${index}-separator`}
name="chevronright"
className={S.breadcrumbDivider}
width={12}
height={12}
/>
] :
breadcrumb
)
}
</section>
);
}
......
......@@ -87,13 +87,13 @@ export default class Calendar extends Component {
return (
<div className="Calendar-header flex align-center">
<div className="bordered rounded p1 cursor-pointer transition-border border-hover px1" onClick={this.previous}>
<Icon name="chevronleft" width="10" height="12" />
<Icon name="chevronleft" size={10} />
</div>
<span className="flex-full" />
<h4 className="bordered border-hover cursor-pointer rounded p1" onClick={this.cycleMode}>{this.state.current.format("MMMM YYYY")}</h4>
<span className="flex-full" />
<div className="bordered border-hover rounded p1 transition-border cursor-pointer px1" onClick={this.next}>
<Icon name="chevronright" width="10" height="12" />
<Icon name="chevronright" size={10} />
</div>
</div>
)
......
......@@ -38,7 +38,7 @@ export default class CheckBox extends Component {
return (
<div style={style} className={cx("cursor-pointer", className)} onClick={() => this.onClick()}>
<div style={checkboxStyle}>
{ checked ? <Icon style={{ color: invertChecked ? "white" : checkColor }} name="check" width={size - padding * 2} height={size - padding * 2} /> : null }
{ checked ? <Icon style={{ color: invertChecked ? "white" : checkColor }} name="check" size={size - padding * 2} /> : null }
</div>
</div>
)
......
......@@ -26,7 +26,7 @@ export default class ColumnarSelector extends Component {
'flex': true,
'no-decoration': true
});
var checkIcon = lastColumn ? <Icon name="check" width="14" height="14"/> : null;
var checkIcon = lastColumn ? <Icon name="check" size={14}/> : null;
var descriptionElement;
var description = column.itemDescriptionFn && column.itemDescriptionFn(item);
if (description) {
......
:root {
--title-color: #606E7B;
--subtitle-color: #AAB7C3;
--muted-color: #DEEAF1;
--blue-color: #2D86D4;
}
:local(.detail) {
composes: flex align-center from "style";
composes: relative from "style";
padding-left: 70px;
}
:local(.detailBody) {
composes: flex-full from "style";
max-width: 550px;
padding-top: 20px;
padding-bottom: 20px;
}
:local(.detailTitle) {
composes: text-bold from "style";
composes: inline-block from "style";
color: var(--title-color);
font-size: 18px;
}
:local(.detailSubtitle) {
composes: text-dark mt2 from "style";
font-size: 16px;
}
:local(.detailSubtitleLight) {
composes: mt2 from "style";
color: var(--subtitle-color);
font-size: 16px;
}
:local(.detailTextArea) {
composes: text-dark input p2 from "style";
resize: none;
font-size: 16px;
width: 100%;
min-height: 100px;
}
/* eslint "react/prop-types": "warn" */
import React, { Component, PropTypes } from "react";
import { Link } from "react-router";
import S from "./Detail.css";
import cx from "classnames";
import pure from "recompose/pure";
const Detail = ({ name, description, placeholder, url, icon, isEditing, field }) =>
<div className={cx(S.detail)}>
<div className={S.detailBody}>
<div className={S.detailTitle}>
{ url ?
<Link to={url} className={S.detailName}>{name}</Link> :
<span className={S.detailName}>{name}</span>
}
</div>
<div className={cx(description ? S.detailSubtitle : S.detailSubtitleLight, { "mt1" : true })}>
{ isEditing ?
<textarea
className={S.detailTextArea}
placeholder={placeholder}
{...field}
defaultValue={description}
/> :
description || placeholder || 'No description yet'
}
{ isEditing && field.error && field.touched &&
<span className="text-error">{field.error}</span>
}
</div>
</div>
</div>
Detail.propTypes = {
name: PropTypes.string.isRequired,
url: PropTypes.string,
description: PropTypes.string,
placeholder: PropTypes.string,
icon: PropTypes.string,
isEditing: PropTypes.bool,
field: PropTypes.object
};
export default pure(Detail);
......@@ -28,13 +28,14 @@ export default class Ellipsified extends Component {
}
render() {
const { showTooltip, children, style, className, tooltip, alwaysShowTooltip } = this.props;
const { showTooltip, children, style, className, tooltip, alwaysShowTooltip, tooltipMaxWidth } = this.props;
const { isTruncated } = this.state;
return (
<Tooltip
tooltip={tooltip || children}
tooltip={tooltip || children || ' '}
verticalAttachments={["top", "bottom"]}
isEnabled={showTooltip && (isTruncated || alwaysShowTooltip) || false}
maxWidth={tooltipMaxWidth}
>
<div ref="content" className={className} style={{ ...style, overflow: "hidden", whiteSpace: "nowrap", textOverflow: "ellipsis" }}>
{children}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment