Skip to content
Snippets Groups Projects
Unverified Commit 3c844bc5 authored by Kyle Doherty's avatar Kyle Doherty
Browse files

break out into components + segment page

parent a89c46fc
No related branches found
No related tags found
No related merge requests found
Showing with 394 additions and 269 deletions
......@@ -14,9 +14,9 @@ import MetabaseAnalytics from 'metabase/lib/analytics';
import { GettingStartedApi, XRayApi } from 'metabase/services';
import {
filterUntouchedFields,
isEmptyObject
import {
filterUntouchedFields,
isEmptyObject
} from "./utils.js"
export const FETCH_GUIDE = "metabase/reference/FETCH_GUIDE";
......@@ -103,10 +103,10 @@ export const fetchTableFingerPrint = createThunkAction(FETCH_TABLE_FINGERPRINT,
const FETCH_SEGMENT_FINGERPRINT = 'metabase/reference/FETCH_SEGMENT_FINGERPRINT';
export const fetchSegmentFingerPrint = createThunkAction(FETCH_SEGMENT_FINGERPRINT, function(segmentId) {
export const fetchSegmentFingerPrint = createThunkAction(FETCH_SEGMENT_FINGERPRINT, function(segmentId, cost) {
return async () => {
try {
let fingerprint = await XRayApi.segment_fingerprint({ segmentId });
let fingerprint = await XRayApi.segment_fingerprint({ segmentId, ...cost.method });
return fingerprint;
} catch (error) {
console.error(error);
......@@ -213,8 +213,8 @@ const fetchDataWrapper = (props, fn) => {
export const wrappedFetchGuide = async (props) => {
fetchDataWrapper(
props,
async () => {
props,
async () => {
await Promise.all(
[props.fetchGuide(),
props.fetchDashboards(),
......@@ -231,8 +231,8 @@ export const wrappedFetchDatabaseMetadata = (props, databaseID) => {
export const wrappedFetchDatabaseMetadataAndQuestion = async (props, databaseID) => {
fetchDataWrapper(
props,
async (dbID) => {
props,
async (dbID) => {
await Promise.all(
[props.fetchDatabaseMetadata(dbID),
props.fetchQuestions()]
......@@ -242,11 +242,11 @@ export const wrappedFetchDatabaseMetadataAndQuestion = async (props, databaseID)
export const wrappedFetchMetricDetail = async (props, metricID) => {
fetchDataWrapper(
props,
async (mID) => {
props,
async (mID) => {
await Promise.all(
[props.fetchMetricTable(mID),
props.fetchMetrics(),
props.fetchMetrics(),
props.fetchGuide()]
)}
)(metricID)
......@@ -254,11 +254,11 @@ export const wrappedFetchMetricDetail = async (props, metricID) => {
export const wrappedFetchMetricQuestions = async (props, metricID) => {
fetchDataWrapper(
props,
async (mID) => {
props,
async (mID) => {
await Promise.all(
[props.fetchMetricTable(mID),
props.fetchMetrics(),
props.fetchMetrics(),
props.fetchQuestions()]
)}
)(metricID)
......@@ -266,8 +266,8 @@ export const wrappedFetchMetricQuestions = async (props, metricID) => {
export const wrappedFetchMetricRevisions = async (props, metricID) => {
fetchDataWrapper(
props,
async (mID) => {
props,
async (mID) => {
await Promise.all(
[props.fetchMetricRevisions(mID),
props.fetchMetrics()]
......@@ -311,8 +311,8 @@ export const wrappedFetchSegmentDetail = (props, segmentID) => {
export const wrappedFetchSegmentQuestions = async (props, segmentID) => {
fetchDataWrapper(
props,
async (sID) => {
props,
async (sID) => {
await props.fetchSegments(sID);
await Promise.all(
[props.fetchSegmentTable(sID),
......@@ -323,8 +323,8 @@ export const wrappedFetchSegmentQuestions = async (props, segmentID) => {
export const wrappedFetchSegmentRevisions = async (props, segmentID) => {
fetchDataWrapper(
props,
async (sID) => {
props,
async (sID) => {
await props.fetchSegments(sID);
await Promise.all(
[props.fetchSegmentRevisions(sID),
......@@ -335,8 +335,8 @@ export const wrappedFetchSegmentRevisions = async (props, segmentID) => {
export const wrappedFetchSegmentFields = async (props, segmentID) => {
fetchDataWrapper(
props,
async (sID) => {
props,
async (sID) => {
await props.fetchSegments(sID);
await Promise.all(
[props.fetchSegmentFields(sID),
......@@ -346,7 +346,7 @@ export const wrappedFetchSegmentFields = async (props, segmentID) => {
}
// This is called when a component gets a new set of props.
// I *think* this is un-necessary in all cases as we're using multiple
// I *think* this is un-necessary in all cases as we're using multiple
// components where the old code re-used the same component
export const clearState = props => {
props.endEditing();
......@@ -364,9 +364,9 @@ const resetForm = (props) => {
}
// Update actions
// these use the "fetchDataWrapper" for now. It should probably be renamed.
// Using props to fire off actions, which imo should be refactored to
// dispatch directly, since there is no actual dependence with the props
// these use the "fetchDataWrapper" for now. It should probably be renamed.
// Using props to fire off actions, which imo should be refactored to
// dispatch directly, since there is no actual dependence with the props
// of that component
const updateDataWrapper = (props, fn) => {
......
......@@ -177,6 +177,9 @@ export const getFieldFingerprint = (state) =>
export const getTableFingerprint = (state) =>
state.reference.tableFingerprint && state.reference.tableFingerprint.fingerprint
export const getSegmentFingerprint = (state) =>
state.reference.segmentFingerprint && state.reference.segmentFingerprint.fingerprint
export const getTableConstituents = (state) =>
state.reference.tableFingerprint && (
Object.keys(state.reference.tableFingerprint.constituents).map(key =>
......@@ -184,3 +187,10 @@ export const getTableConstituents = (state) =>
)
)
export const getSegmentConstituents = (state) =>
state.reference.segmentFingerprint && (
Object.keys(state.reference.segmentFingerprint.constituents).map(key =>
state.reference.segmentFingerprint.constituents[key]
)
)
......@@ -57,7 +57,7 @@ import Unauthorized from "metabase/components/Unauthorized.jsx";
// Reference Guide
import GettingStartedGuideContainer from "metabase/reference/guide/GettingStartedGuideContainer.jsx";
// Reference Metrics
// Reference Metrics
import MetricListContainer from "metabase/reference/metrics/MetricListContainer.jsx";
import MetricDetailContainer from "metabase/reference/metrics/MetricDetailContainer.jsx";
import MetricQuestionsContainer from "metabase/reference/metrics/MetricQuestionsContainer.jsx";
......@@ -243,7 +243,7 @@ export const getRoutes = (store) =>
</Route>
{/* REFERENCE */}
<Route path="/xray" title="XRay">
<Route path="segment/:segmentId" component={SegmentXRay} />
<Route path="segment/:segmentId/:cost" component={SegmentXRay} />
<Route path="table/:tableId/:cost" component={TableXRay} />
<Route path="field/:fieldId/:cost" component={FieldXRay} />
<Route path="card/:cardId" component={CardXRay} />
......
import React from 'react'
import { Link } from 'react-router'
import Histogram from 'metabase/xray/Histogram'
import SimpleStat from 'metabase/xray/SimpleStat'
const Constituent = ({constituent}) =>
<div className="Grid my3 bg-white bordered rounded shadowed">
<div className="Grid-cell Cell--1of3 border-right">
<div className="p4">
<Link
to={`xray/field/${constituent.field.id}/approximate`}
className="text-brand-hover link transition-text"
>
<h2 className="text-bold">{constituent.field.display_name}</h2>
</Link>
<p className="text-measure text-paragraph">{constituent.field.description}</p>
<div className="flex align-center">
{ constituent.min && (
<SimpleStat
stat={constituent.min}
/>
)}
{ constituent.max && (
<SimpleStat
stat={constituent.max}
/>
)}
</div>
</div>
</div>
<div className="Grid-cell p3">
<div style={{ height: 220 }}>
<Histogram histogram={constituent.histogram.value} />
</div>
</div>
</div>
export default Constituent
......@@ -15,7 +15,7 @@ const CostSelect = ({ currentCost, onChange }) =>
key={cost}
onClick={() => onChange(cost)}
className={cx(
"flex align-center justify-center cursor-pointer bg-brand-hover",
"flex align-center justify-center cursor-pointer bg-brand-hover text-white-hover transition-background transition-text",
{ 'bg-brand text-white': currentCost === cost }
)}
>
......
import React from 'react'
import { Heading } from 'metabase/xray/components/XRayLayout'
import SimpleStat from 'metabase/xray/SimpleStat'
const atLeastOneStat = (fingerprint, stats) =>
stats.filter(s => fingerprint[s]).length > 0
const StatGroup = ({ heading, fingerprint, stats, showDescriptions }) =>
atLeastOneStat(fingerprint, stats) && (
<div className="my4">
<Heading heading={heading} />
<div className="bordered rounded shadowed bg-white">
<ol className="Grid Grid--1of4">
{ stats.map(stat =>
fingerprint[stat] && (
<li className="Grid-cell lg-p3 lg-px4 border-right border-bottom" key={stat}>
<SimpleStat
stat={fingerprint[stat]}
showDescription={showDescriptions}
/>
</li>
)
)}
</ol>
</div>
</div>
)
export default StatGroup
import React from 'react'
// A small wrapper to get consistent page structure
export const XRayPageWrapper = ({ children }) =>
<div className="wrapper bg-slate-extra-light pb4 full-height" style={{ paddingLeft: '6em', paddingRight: '6em' }}>
{ children }
</div>
// A unified heading for XRay pages
export const Heading = ({ heading }) =>
<h2 className="py3">{heading}</h2>
......@@ -5,21 +5,24 @@ import { connect } from 'react-redux'
import title from 'metabase/hoc/Title'
import { Link } from 'react-router'
import { isDate } from 'metabase/lib/schema_metadata'
import { fetchFieldFingerPrint, changeCost } from 'metabase/reference/reference'
import { getFieldFingerprint } from 'metabase/reference/selectors'
import LoadingAndErrorWrapper from 'metabase/components/LoadingAndErrorWrapper'
import COSTS from 'metabase/xray/costs'
import CostSelect from 'metabase/xray/components/CostSelect'
import Histogram from 'metabase/xray/Histogram'
import SimpleStat from 'metabase/xray/SimpleStat'
import { isDate } from 'metabase/lib/schema_metadata'
import {
PERIODICITY,
ROBOTS,
STATS_OVERVIEW,
VALUES_OVERVIEW
} from 'metabase/xray/stats'
const PERIODICITY = ['day', 'week', 'month', 'hour', 'quarter']
import LoadingAndErrorWrapper from 'metabase/components/LoadingAndErrorWrapper'
import CostSelect from 'metabase/xray/components/CostSelect'
import StatGroup from 'metabase/xray/components/StatGroup'
import Histogram from 'metabase/xray/Histogram'
import { Heading, XRayPageWrapper } from 'metabase/xray/components/XRayLayout'
type Props = {
fetchFieldFingerPrint: () => void,
......@@ -27,23 +30,30 @@ type Props = {
params: {},
}
const StatGroup = ({ fingerprint, stats, showDescriptions }) =>
<ol className="Grid Grid--1of3">
{ stats.map(stat =>
fingerprint[stat] && (
<li className="Grid-cell p4 border-right border-bottom" key={stat}>
<SimpleStat
stat={fingerprint[stat]}
showDescription={showDescriptions}
/>
</li>
)
)}
</ol>
const Heading = ({ heading }) =>
<h2 className="py3">{heading}</h2>
const Periodicity = ({fingerprint}) =>
<div>
<Heading heading="Time breakdown" />,
<div className="bg-white bordered rounded shadowed">
<div className="Grid Grid--gutters Grid--1of4">
{ PERIODICITY.map(period =>
fingerprint[`histogram-${period}`] && (
<div className="Grid-cell">
<div className="p4 border-right border-bottom">
<div style={{ height: 120}}>
<h4>
{fingerprint[`histogram-${period}`].label}
</h4>
<Histogram
histogram={fingerprint[`histogram-${period}`].value}
/>
</div>
</div>
</div>
)
)}
</div>
</div>
</div>
const mapStateToProps = state => ({
fingerprint: getFieldFingerprint(state)
......@@ -85,136 +95,72 @@ class FieldXRay extends Component {
render () {
const { fingerprint, params } = this.props
return (
<div className="wrapper bg-slate-extra-light" style={{ paddingLeft: '6em', paddingRight: '6em' }}>
<div className="full">
<LoadingAndErrorWrapper loading={!fingerprint}>
{ () => {
return (
<div className="full">
<div className="my4 flex align-center">
<div>
<Link
className="my2"
to={`/xray/table/${fingerprint.table.id}/approximate`}
>
{fingerprint.table.display_name}
</Link>
<h1 className="m0">
{fingerprint.field.display_name} stats
</h1>
<p className="text-paragraph text-measure">{fingerprint.field.description}</p>
</div>
<div className="ml-auto">
Fidelity:
<CostSelect
currentCost={params.cost}
onChange={this.changeCost}
/>
</div>
</div>
<div className="mt4">
<Heading heading="Distribution" />
<div className="bg-white bordered shadowed">
<div className="p4">
<div style={{ height: 300 }}>
<Histogram histogram={fingerprint.histogram.value} />
</div>
</div>
</div>
</div>
{
/*
* If the field is a date field we show more information about the periodicity
* */
isDate(fingerprint.field) && (
<div>
<Heading heading="Time breakdown" />,
<div className="bg-white bordered rounded shadowed">
<div className="Grid Grid--gutters Grid--1of4">
{ PERIODICITY.map(period =>
fingerprint[`histogram-${period}`] && (
<div className="Grid-cell">
<div className="p4 border-right border-bottom">
<div style={{ height: 120}}>
<h4>
{fingerprint[`histogram-${period}`].label}
</h4>
<Histogram
histogram={fingerprint[`histogram-${period}`].value}
/>
</div>
</div>
</div>
)
)}
</div>
</div>
</div>
)
}
<div className="my4">
<Heading heading="Values overview" />
<div className="bordered rounded shadowed bg-white">
<StatGroup
fingerprint={fingerprint}
stats={[
'min',
'max',
'count',
'sum',
'cardinality',
'sd',
'nil%',
'mean',
'median',
'mean-median-spread'
]}
/>
</div>
</div>
<div className="my4">
<Heading heading="Statistical overview" />
<div className="bordered rounded shadowed bg-white">
<StatGroup
fingerprint={fingerprint}
showDescriptions
stats={[
'kurtosis',
'skewness',
'entropy',
'var',
'sum-of-square',
]}
/>
</div>
</div>
<div className="my4">
<Heading heading="What the Robots say" />
<div className="bordered rounded shadowed bg-white">
<StatGroup
fingerprint={fingerprint}
showDescriptions
stats={[
'cardinality-vs-count',
'positive-definite?',
'has-nils?',
'all-distinct?',
]}
/>
<XRayPageWrapper>
<LoadingAndErrorWrapper
loading={!fingerprint}
noBackground
>
{ () =>
<div className="full">
<div className="my4 flex align-center">
<div>
<Link
className="my2 text-bold text-brand-hover inline-block bordered bg-white p1 h4 no-decoration"
to={`/xray/table/${fingerprint.table.id}/approximate`}
>
{fingerprint.table.display_name}
</Link>
<h1 className="m0">
{fingerprint.field.display_name} XRay
</h1>
<p className="mt0 text-paragraph text-measure">
{fingerprint.field.description}
</p>
</div>
<div className="ml-auto flex align-center">
<h3 className="mr2">Fidelity:</h3>
<CostSelect
currentCost={params.cost}
onChange={this.changeCost}
/>
</div>
</div>
<div className="mt4">
<Heading heading="Distribution" />
<div className="bg-white bordered shadowed">
<div className="lg-p4">
<div style={{ height: 300 }}>
<Histogram histogram={fingerprint.histogram.value} />
</div>
</div>
</div>
)
}}
</LoadingAndErrorWrapper>
</div>
</div>
</div>
{ isDate(fingerprint.field) && <Periodicity fingerprint={fingerprint} /> }
<StatGroup
heading="Values overview"
fingerprint={fingerprint}
stats={VALUES_OVERVIEW}
/>
<StatGroup
heading="Statistical overview"
fingerprint={fingerprint}
showDescriptions
stats={STATS_OVERVIEW}
/>
<StatGroup
heading="Robots"
fingerprint={fingerprint}
showDescriptions
stats={ROBOTS}
/>
</div>
}
</LoadingAndErrorWrapper>
</XRayPageWrapper>
)
}
}
......
/* @flow */
import React, { Component } from 'react'
import { connect } from 'react-redux'
import title from 'metabase/hoc/Title'
import LoadingAndErrorWrapper from 'metabase/components/LoadingAndErrorWrapper'
import { XRayPageWrapper } from 'metabase/xray/components/XRayLayout'
import {
fetchSegmentFingerPrint,
changeCost
} from 'metabase/reference/reference'
import { Link } from 'react-router'
import Histogram from 'metabase/xray/Histogram'
import SimpleStat from 'metabase/xray/SimpleStat'
import { fetchSegmentFingerPrint } from 'metabase/reference/reference'
import COSTS from 'metabase/xray/costs'
import CostSelect from 'metabase/xray/components/CostSelect'
import {
getSegmentConstituents,
getSegmentFingerprint
} from 'metabase/reference/selectors'
type Props = {
fetchSegmentFingerPrint: () => void,
fingerprint: {}
}
const mapStateToProps = state => ({
fingerprint: getSegmentFingerprint(state),
constituents: getSegmentConstituents(state)
})
const mapDispatchToProps = {
fetchSegmentFingerPrint,
changeCost
}
@connect(mapStateToProps, mapDispatchToProps)
@title(({ fingerprint }) => fingerprint.segment && fingerprint.segment.display_name || "Segment" )
class SegmentXRay extends Component {
props: Props
componentDidMount () {
this.props.fetchSegmentFingerPrint(this.props.params.segmentId)
this.fetchSegmentFingerPrint()
}
fetchSegmentFingerPrint () {
const { params } = this.props
const cost = COSTS[params.cost]
this.props.fetchSegmentFingerPrint(params.segmentId, cost)
}
componentDidUpdate (prevProps) {
if(prevProps.params.cost !== this.props.params.cost) {
this.fetchSegmentFingerPrint()
}
}
changeCost = (cost) => {
const { params } = this.props
// TODO - this feels kinda icky, would be nice to be able to just pass cost
this.props.changeCost(`table/${params.segmentId}/${cost}`)
}
render () {
const { constituents, fingerprint, params } = this.props
return (
<div style={{
maxWidth: 550,
marginLeft: 'calc(48px + 1rem)',
}}>
<h3>XRAY</h3>
<div className="Grid Grid--1of2 Grid--gutters mt1">
<div className="Grid-cell">
<h2>Fingerprint</h2>
<pre>
<code>
{ JSON.stringify(this.props.fingerprint, null, 2) }
</code>
</pre>
</div>
</div>
</div>
<XRayPageWrapper>
<LoadingAndErrorWrapper
loading={!constituents}
noBackground
>
{ () =>
<div className="full">
<div className="my4 flex align-center py2">
<h1>{ fingerprint.segment.display_name }</h1>
<div className="ml-auto flex align-center">
<h3 className="mr2">Fidelity:</h3>
<CostSelect
currentCost={params.cost}
onChange={this.changeCost}
/>
</div>
<ol>
{ constituents.map(c => {
return (
<li className="Grid my3 bg-white bordered rounded shadowed">
<div className="Grid-cell Cell--1of3 border-right">
<div className="p4">
<Link
to={`xray/field/${c.field.id}/approximate`}
className="text-brand-hover link transition-text"
>
<h2 className="text-bold">{c.field.display_name}</h2>
</Link>
<p className="text-measure text-paragraph">{c.field.description}</p>
<div className="flex align-center">
{ c.min && (
<SimpleStat
stat={c.min}
/>
)}
{ c.max && (
<SimpleStat
stat={c.max}
/>
)}
</div>
</div>
</div>
<div className="Grid-cell p3">
<div style={{ height: 220 }}>
<Histogram histogram={c.histogram.value} />
</div>
</div>
</li>
)
})}
</ol>
</div>
</div>
}
</LoadingAndErrorWrapper>
</XRayPageWrapper>
)
}
}
// TODO - create selectors
const mapStateToProps = state => ({
fingerprint: state.reference.segmentFingerprint,
})
const mapDispatchToProps = {
fetchSegmentFingerPrint: fetchSegmentFingerPrint
}
export default connect(mapStateToProps, mapDispatchToProps)(SegmentXRay)
export default SegmentXRay
......@@ -9,12 +9,12 @@ import {
changeCost
} from 'metabase/reference/reference'
import Histogram from 'metabase/xray/Histogram'
import { XRayPageWrapper } from 'metabase/xray/components/XRayLayout'
import COSTS from 'metabase/xray/costs'
import CostSelect from 'metabase/xray/components/CostSelect'
import { Link } from 'react-router'
import CostSelect from 'metabase/xray/components/CostSelect'
import Constituent from 'metabase/xray/components/Constituent'
import {
getTableConstituents,
......@@ -23,8 +23,6 @@ import {
import LoadingAndErrorWrapper from 'metabase/components/LoadingAndErrorWrapper'
import SimpleStat from 'metabase/xray/SimpleStat'
type Props = {
constituents: [],
fetchTableFingerPrint: () => void,
......@@ -75,61 +73,39 @@ class TableXRay extends Component {
const { constituents, fingerprint, params } = this.props
return (
<LoadingAndErrorWrapper loading={!constituents} className="bg-slate-extra-light">
{ () =>
<div className="wrapper" style={{ paddingLeft: '12em', paddingRight: '12em'}}>
<div className="my4 flex align-center py2">
<h1>{ fingerprint.table.display_name }</h1>
<div className="ml-auto">
Fidelity:
<CostSelect
currentCost={params.cost}
onChange={this.changeCost}
/>
<XRayPageWrapper>
<LoadingAndErrorWrapper
loading={!constituents}
noBackground
>
{ () =>
<div className="full">
<div className="my4 flex align-center py2">
<div>
<h1>{ fingerprint.table.display_name } XRay</h1>
<p className="m0 text-paragraph text-measure">{fingerprint.table.description}</p>
</div>
<div className="ml-auto flex align-center">
<h3 className="mr2">Fidelity:</h3>
<CostSelect
currentCost={params.cost}
onChange={this.changeCost}
/>
</div>
</div>
</div>
<div>
<ol>
{ constituents.map(c => {
return (
<li className="Grid my3 bg-white bordered rounded shadowed">
<div className="Grid-cell Cell--1of3 border-right">
<div className="p4">
<Link
to={`xray/field/${c.field.id}/approximate`}
className="text-brand-hover link transition-text"
>
<h2 className="text-bold">{c.field.display_name}</h2>
</Link>
<p className="text-measure text-paragraph">{c.field.description}</p>
<div className="flex align-center">
{ c.min && (
<SimpleStat
stat={c.min}
/>
)}
{ c.max && (
<SimpleStat
stat={c.max}
/>
)}
</div>
</div>
</div>
<div className="Grid-cell p3">
<div style={{ height: 220 }}>
<Histogram histogram={c.histogram.value} />
</div>
</div>
</li>
)
})}
{ constituents.map((constituent, index) =>
<li key={index}>
<Constituent
constituent={constituent}
/>
</li>
)}
</ol>
</div>
</div>
}
</LoadingAndErrorWrapper>
}
</LoadingAndErrorWrapper>
</XRayPageWrapper>
)
}
}
......
// keys for common values interesting for most folks
export const VALUES_OVERVIEW = [
'min',
'max',
'count',
'sum',
'cardinality',
'sd',
'nil%',
'mean',
'median',
'mean-median-spread'
]
// keys for common values interesting for stat folks
export const STATS_OVERVIEW = [
'kurtosis',
'skewness',
'entropy',
'var',
'sum-of-square',
]
export const ROBOTS = [
'cardinality-vs-count',
'positive-definite?',
'has-nils?',
'all-distinct?',
]
// periods we care about for showing periodicity
export const PERIODICITY = ['day', 'week', 'month', 'hour', 'quarter']
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