Add ability to show a line graph and change to other line graph type in veo

pull/10616/head
Iris Scholten 2018-10-10 16:26:36 -07:00
parent b109015749
commit ede25ecdce
15 changed files with 594 additions and 47 deletions

View File

@ -78,7 +78,7 @@ class VEO extends PureComponent<Props, State> {
onCancel={onHide}
onSave={this.handleSave}
/>
<TimeMachine />
<TimeMachine activeTab={activeTab} />
</div>
)
}

View File

@ -3,8 +3,8 @@ import React, {PureComponent} from 'react'
// Components
import VEOHeaderName from 'src/dashboards/components/VEOHeaderName'
import TimeMachineTabs from 'src/shared/components/TimeMachineTabs'
import {
Radio,
ButtonShape,
Button,
ComponentColor,
@ -43,26 +43,10 @@ class VEOHeader extends PureComponent<Props> {
<VEOHeaderName name={name} onRename={onSetName} />
</Page.Header.Left>
<Page.Header.Center>
<Radio shape={ButtonShape.StretchToFit}>
<Radio.Button
id="deceo-tab-queries"
titleText="Queries"
value={TimeMachineTab.Queries}
active={activeTab === TimeMachineTab.Queries}
onClick={onSetActiveTab}
>
Queries
</Radio.Button>
<Radio.Button
id="deceo-tab-vis"
titleText="Visualization"
value={TimeMachineTab.Visualization}
active={activeTab === TimeMachineTab.Visualization}
onClick={onSetActiveTab}
>
Visualization
</Radio.Button>
</Radio>
<TimeMachineTabs
activeTab={activeTab}
onSetActiveTab={onSetActiveTab}
/>
</Page.Header.Center>
<Page.Header.Right>
<Button

View File

@ -10,6 +10,7 @@ import {setActiveTimeMachineID} from 'src/shared/actions/v2/timeMachines'
// Utils
import {DE_TIME_MACHINE_ID} from 'src/shared/constants/timeMachine'
import {TimeMachineTab} from 'src/types/v2/timeMachine'
interface StateProps {}
@ -17,7 +18,9 @@ interface DispatchProps {
onSetActiveTimeMachineID: typeof setActiveTimeMachineID
}
interface PassedProps {}
interface PassedProps {
activeTab: TimeMachineTab
}
interface State {}
@ -31,9 +34,13 @@ class DataExplorer extends PureComponent<Props, State> {
}
public render() {
const {activeTab} = this.props
return (
<div className="data-explorer">
<TimeMachine />
<div className="time-machine-page">
<TimeMachine activeTab={activeTab} />
</div>
</div>
)
}

View File

@ -1,24 +1,54 @@
// Libraries
import React, {PureComponent} from 'react'
// Components
import DataExplorer from 'src/dataExplorer/components/DataExplorer'
import TimeMachineTabs from 'src/shared/components/TimeMachineTabs'
import {Page} from 'src/pageLayout'
class DataExplorerPage extends PureComponent {
// Types
import {TimeMachineTab} from 'src/types/v2/timeMachine'
interface State {
activeTab: TimeMachineTab
}
class DataExplorerPage extends PureComponent<null, State> {
constructor(props) {
super(props)
this.state = {
activeTab: TimeMachineTab.Queries,
}
}
public render() {
const {activeTab} = this.state
return (
<Page>
<Page.Header>
<Page.Header.Left>
<Page.Title title="Data Explorer" />
</Page.Header.Left>
<Page.Header.Center>
<TimeMachineTabs
activeTab={activeTab}
onSetActiveTab={this.handleSetActiveTab}
/>
</Page.Header.Center>
<Page.Header.Right />
</Page.Header>
<Page.Contents fullWidth={true} scrollable={false}>
<DataExplorer />
<DataExplorer activeTab={activeTab} />
</Page.Contents>
</Page>
)
}
private handleSetActiveTab = (activeTab: TimeMachineTab): void => {
this.setState({activeTab})
}
}
export default DataExplorerPage

View File

@ -1,9 +1,10 @@
import {TimeRange} from 'src/types/v2'
import {TimeRange, ViewType} from 'src/types/v2'
export type Action =
| SetActiveTimeMachineIDAction
| SetNameAction
| SetTimeRangeAction
| SetTypeAction
interface SetActiveTimeMachineIDAction {
type: 'SET_ACTIVE_TIME_MACHINE_ID'
@ -36,3 +37,13 @@ export const setTimeRange = (timeRange: TimeRange): SetTimeRangeAction => ({
type: 'SET_TIME_RANGE',
payload: {timeRange},
})
interface SetTypeAction {
type: 'SET_VIEW_TYPE'
payload: {type: ViewType}
}
export const setType = (type: ViewType): SetTypeAction => ({
type: 'SET_VIEW_TYPE',
payload: {type},
})

View File

@ -199,9 +199,7 @@ class RefreshingView extends PureComponent<Props> {
}
}
const mstp = ({sources, routing}: AppState): StateProps => {
const sourceID = routing.locationBeforeTransitions.query.sourceID
const source = sources.find(s => s.id === sourceID)
const mstp = ({source}: AppState): StateProps => {
const link = source.links.query
return {

View File

@ -0,0 +1,51 @@
.time-machine-page {
margin: 0 auto;
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
border-radius: 0 0 $radius $radius;
}
.time-machine {
display: flex;
flex-direction: column;
align-items: stretch;
height: 100%;
}
.time-machine-container {
height: calc(100% - 120px);
position: relative;
}
.time-machine-top {
width: 100%;
height: 100%;
min-height: 100px;
display: flex;
flex-direction: column;
align-items: stretch;
}
.time-machine-vis {
padding: 0 20px 20px 20px;
width: 100%;
height: 100%;
.graph-container {
top: 0;
height: 100%;
border-radius: 4px;
overflow: hidden;
}
}
.time-machine-cusomization {
background-color: $g2-kevlar;
height: 100%;
.simple-spinner {
margin-right: 10px;
}
}

View File

@ -1,17 +1,130 @@
// Libraries
import React, {PureComponent} from 'react'
import React, {PureComponent, ComponentClass} from 'react'
import {connect} from 'react-redux'
// Components
import TimeMachineControls from 'src/shared/components/TimeMachineControls'
import ViewComponent from 'src/shared/components/cells/View'
import ViewTypeSelector from 'src/shared/components/ViewTypeSelector'
import Threesizer from 'src/shared/components/threesizer/Threesizer'
class TimeMachine extends PureComponent {
// Actions
import {setType} from 'src/shared/actions/v2/timeMachines'
// Constants
import {HANDLE_HORIZONTAL} from 'src/shared/constants'
// Types
import {AppState, View, TimeRange} from 'src/types/v2'
import {TimeMachineTab} from 'src/types/v2/timeMachine'
interface StateProps {
view: View
timeRange: TimeRange
}
interface PassedProps {
activeTab: TimeMachineTab
}
interface DispatchProps {
onUpdateType: typeof setType
}
type Props = StateProps & PassedProps & DispatchProps
class TimeMachine extends PureComponent<Props> {
public render() {
return (
<div className="time-machine">
<TimeMachineControls />
<div className="time-machine-container">
<Threesizer
orientation={HANDLE_HORIZONTAL}
divisions={this.horizontalDivisions}
/>
</div>
</div>
)
}
private get horizontalDivisions() {
return [
{
name: '',
handleDisplay: 'none',
headerButtons: [],
menuOptions: [],
render: () => this.visualization,
headerOrientation: HANDLE_HORIZONTAL,
size: 0.33,
},
{
name: '',
handlePixels: 8,
headerButtons: [],
menuOptions: [],
render: () => this.customizationPanels,
headerOrientation: HANDLE_HORIZONTAL,
size: 0.67,
},
]
}
private get visualization(): JSX.Element {
const {view, timeRange} = this.props
const noop = () => {}
return (
<div className="time-machine-top">
<div className="time-machine-vis">
<div className="graph-container">
<ViewComponent
view={view}
onZoom={noop}
templates={[]}
timeRange={timeRange}
autoRefresh={0}
manualRefresh={0}
onEditCell={noop}
/>
</div>
</div>
</div>
)
}
private get customizationPanels(): JSX.Element {
const {view, onUpdateType, activeTab} = this.props
return (
<div className="time-machine-customization">
{activeTab === TimeMachineTab.Queries ? (
<div />
) : (
<ViewTypeSelector
type={view.properties.type}
onUpdateType={onUpdateType}
/>
)}
</div>
)
}
}
export default TimeMachine
const mstp = (state: AppState) => {
const {
timeMachines: {activeTimeMachineID, timeMachines},
} = state
const timeMachine = timeMachines[activeTimeMachineID]
return {
view: timeMachine.view,
timeRange: timeMachine.timeRange,
}
}
const mdtp = {
onUpdateType: setType,
}
export default connect(mstp, mdtp)(TimeMachine) as ComponentClass<PassedProps>

View File

@ -0,0 +1,44 @@
// Libraries
import React, {PureComponent} from 'react'
// Components
import {Radio, ButtonShape} from 'src/clockface'
// Types
import {TimeMachineTab} from 'src/types/v2/timeMachine'
interface Props {
activeTab: TimeMachineTab
onSetActiveTab: (activeTab: TimeMachineTab) => void
}
class TimeMachineTabs extends PureComponent<Props> {
public render() {
const {activeTab, onSetActiveTab} = this.props
return (
<Radio shape={ButtonShape.StretchToFit}>
<Radio.Button
id="deceo-tab-queries"
titleText="Queries"
value={TimeMachineTab.Queries}
active={activeTab === TimeMachineTab.Queries}
onClick={onSetActiveTab}
>
Queries
</Radio.Button>
<Radio.Button
id="deceo-tab-vis"
titleText="Visualization"
value={TimeMachineTab.Visualization}
active={activeTab === TimeMachineTab.Visualization}
onClick={onSetActiveTab}
>
Visualization
</Radio.Button>
</Radio>
)
}
}
export default TimeMachineTabs

View File

@ -0,0 +1,159 @@
/*
Graph Type Selector Styles
------------------------------------------------------------------------------
*/
$graph-type--card: 190px;
$graph-type--margin: 4px;
$graph-type--graphic: 150px;
.graph-type-selector {
background-color: $g3-castle;
}
.graph-type-selector--container {
min-width: 200px;
padding: 30px;
}
.graph-type-selector--grid {
width: 100%;
display: inline-block;
margin: 0 (-10px / 2);
margin-bottom: -10px;
}
.graph-type-selector--option {
float: left;
width: $graph-type--card;
padding-bottom: $graph-type--card;
position: relative;
> div > p {
margin: 0;
font-size: 14px;
font-weight: 900;
position: absolute;
bottom: 7%;
left: 10px;
width: calc(100% - 20px);
text-align: center;
display: inline-block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
// Actual "card"
> div {
background-color: $g2-kevlar;
color: $g11-sidewalk;
border-radius: $ix-radius;
width: $graph-type--card - ($graph-type--margin / 2);
height: $graph-type--card - ($graph-type--margin / 2);
position: absolute;
top: 10px / 2;
left: 10px / 2;
transition: color 0.25s ease, border-color 0.25s ease,
background-color 0.25s ease;
&:hover {
cursor: pointer;
background-color: $g4-onyx;
color: $g15-platinum;
}
}
}
// Active state "card"
.graph-type-selector--option.active > div,
.graph-type-selector--option.active > div:hover {
background-color: $g5-pepper;
color: $g18-cloud;
}
.graph-type-selector--graphic {
width: $graph-type--graphic;
height: $graph-type--graphic;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
> svg,
> svg * {
transform: translate3d(0, 0, 0);
}
> svg {
width: 100%;
height: 100%;
}
}
.graph-type-selector--graphic-line {
stroke-width: 1px;
fill: none;
stroke-linecap: round;
stroke-miterlimit: 10;
&.graphic-line-a {
stroke: $g11-sidewalk;
}
&.graphic-line-b {
stroke: $g9-mountain;
}
&.graphic-line-c {
stroke: $g7-graphite;
}
&.graphic-line-d {
stroke: $g13-mist;
}
}
.graph-type-selector--graphic-fill {
opacity: 0.045;
&.graphic-fill-a {
fill: $g11-sidewalk;
}
&.graphic-fill-b {
fill: $g9-mountain;
}
&.graphic-fill-c {
fill: $g7-graphite;
}
&.graphic-fill-d {
fill: $g13-mist;
opacity: 1;
}
}
.graph-type-selector--option.active .graph-type-selector--graphic {
.graph-type-selector--graphic-line.graphic-line-a {
stroke: $c-pool;
}
.graph-type-selector--graphic-line.graphic-line-b {
stroke: $c-dreamsicle;
}
.graph-type-selector--graphic-line.graphic-line-c {
stroke: $c-rainforest;
}
.graph-type-selector--graphic-line.graphic-line-d {
stroke: $g17-whisper;
}
.graph-type-selector--graphic-fill.graphic-fill-a {
fill: $c-pool;
}
.graph-type-selector--graphic-fill.graphic-fill-b {
fill: $c-dreamsicle;
}
.graph-type-selector--graphic-fill.graphic-fill-c {
fill: $c-rainforest;
}
.graph-type-selector--graphic-fill.graphic-fill-a,
.graph-type-selector--graphic-fill.graphic-fill-b,
.graph-type-selector--graphic-fill.graphic-fill-c {
opacity: 0.22;
}
.graph-type-selector--graphic-fill.graphic-fill-d {
fill: $g17-whisper;
opacity: 1;
}
}

View File

@ -0,0 +1,50 @@
// Libraries
import React, {Component} from 'react'
import classnames from 'classnames'
// Constants
import {GRAPH_TYPES} from 'src/dashboards/graphics/graph'
// Decorators
import {ErrorHandling} from 'src/shared/decorators/errors'
// Types
import {ViewType} from 'src/types/v2'
interface Props {
type: string
onUpdateType: (newType: ViewType) => void
}
@ErrorHandling
class ViewTypeSelector extends Component<Props> {
public render() {
const {type} = this.props
return (
<div className="graph-type-selector--container">
<div className="graph-type-selector--grid">
{GRAPH_TYPES.map(graphType => (
<div
key={graphType.type}
className={classnames('graph-type-selector--option', {
active: graphType.type === type,
})}
>
<div onClick={this.handleSelectType(graphType.type)}>
{graphType.graphic}
<p>{graphType.menuOption}</p>
</div>
</div>
))}
</div>
</div>
)
}
private handleSelectType = (newType: ViewType) => (): void => {
this.props.onUpdateType(newType)
}
}
export default ViewTypeSelector

View File

@ -30,7 +30,7 @@ interface Props {
orientation: string
activeHandleID: string
headerOrientation: string
render: (visibility: string) => ReactElement<any>
render: (visibility: string, pixels: number) => ReactElement<any>
onHandleStartDrag: (id: string, e: MouseEvent<HTMLElement>) => void
onDoubleClick: (id: string) => void
onMaximize: (id: string) => void
@ -46,15 +46,17 @@ class Division extends PureComponent<Props> {
}
private collapseThreshold: number = 0
private ref: React.RefObject<HTMLDivElement>
private divisionRef: React.RefObject<HTMLDivElement>
private divisionPixels: number = 0
constructor(props) {
super(props)
this.ref = React.createRef<HTMLDivElement>()
this.divisionRef = React.createRef<HTMLDivElement>()
}
public componentDidMount() {
const {name} = this.props
this.calcDivisionPixels()
if (!name) {
return 0
@ -70,18 +72,25 @@ class Division extends PureComponent<Props> {
this.collapseThreshold = width + NAME_OFFSET
}
public componentDidUpdate() {
this.calcDivisionPixels()
}
public render() {
const {render} = this.props
return (
<div
className={this.containerClass}
style={this.containerStyle}
ref={this.ref}
ref={this.divisionRef}
>
{this.renderDragHandle}
<div className={this.contentsClass} style={this.contentStyle}>
{this.renderHeader}
<div className="threesizer--body">{render(this.visibility)}</div>
<div className="threesizer--body">
{render(this.visibility, this.divisionPixels)}
</div>
</div>
</div>
)
@ -273,11 +282,11 @@ class Division extends PureComponent<Props> {
return true
}
if (!this.ref || this.props.size >= 0.33) {
if (!this.divisionRef || this.props.size >= 0.33) {
return false
}
const {width} = this.ref.current.getBoundingClientRect()
const {width} = this.divisionRef.current.getBoundingClientRect()
return width <= this.collapseThreshold
}
@ -312,6 +321,19 @@ class Division extends PureComponent<Props> {
const {id, onMaximize} = this.props
onMaximize(id)
}
private calcDivisionPixels = (): void => {
const {orientation} = this.props
const {clientWidth, clientHeight} = this.divisionRef.current
let divisionPixels = clientWidth
if (orientation === HANDLE_HORIZONTAL) {
divisionPixels = clientHeight
}
this.divisionPixels = divisionPixels
}
}
export default Division

View File

@ -38,7 +38,7 @@ interface DivisionProps {
size?: number
headerButtons?: JSX.Element[]
menuOptions: MenuItem[]
render: (visibility?: string) => ReactElement<any>
render: (visibility: string, pixels: number) => ReactElement<any>
}
interface DivisionState extends DivisionProps {

View File

@ -1,6 +1,3 @@
// Utils
import {getNewView} from 'src/dashboards/utils/cellGetters'
// Constants
import {
VEO_TIME_MACHINE_ID,
@ -8,11 +5,12 @@ import {
} from 'src/shared/constants/timeMachine'
// Types
import {View, TimeRange} from 'src/types/v2'
import {View, TimeRange, ViewType, ViewShape} from 'src/types/v2'
import {Action} from 'src/shared/actions/v2/timeMachines'
import {InfluxLanguages} from 'src/types/v2/dashboards'
interface TimeMachineState {
view: Partial<View>
view: View
timeRange: TimeRange
}
@ -24,8 +22,77 @@ export interface TimeMachinesState {
}
const initialStateHelper = (): TimeMachineState => ({
view: getNewView(),
timeRange: {lower: 'now() - 1h'},
view: {
id: '1',
name: 'CELLL YO',
properties: {
shape: ViewShape.ChronografV2,
queries: [
{
text:
'SELECT mean("usage_user") FROM "telegraf"."autogen"."cpu" WHERE time > now() - 15m GROUP BY time(10s) FILL(0)',
type: InfluxLanguages.InfluxQL,
source: 'v1',
},
],
axes: {
x: {
bounds: ['', ''],
label: '',
prefix: '',
suffix: '',
base: '10',
scale: 'linear',
},
y: {
bounds: ['', ''],
label: '',
prefix: '',
suffix: '',
base: '10',
scale: 'linear',
},
y2: {
bounds: ['', ''],
label: '',
prefix: '',
suffix: '',
base: '10',
scale: 'linear',
},
},
type: ViewType.Line,
colors: [
{
id: '63b61e02-7649-4d88-84bd-97722e2a2514',
type: 'scale',
hex: '#31C0F6',
name: 'Nineteen Eighty Four',
value: '0',
},
{
id: 'd77c12d4-d257-48e1-8ba5-7bee8e3df593',
type: 'scale',
hex: '#A500A5',
name: 'Nineteen Eighty Four',
value: '0',
},
{
id: 'cd6948ad-7ae6-40d3-bc37-3aec32f7fe98',
type: 'scale',
hex: '#FF7E27',
name: 'Nineteen Eighty Four',
value: '0',
},
],
legend: {},
decimalPlaces: {
isEnforced: false,
digits: 3,
},
},
},
})
const INITIAL_STATE: TimeMachinesState = {
@ -70,6 +137,15 @@ const timeMachineReducer = (
newActiveTimeMachine = {...activeTimeMachine, timeRange}
break
}
case 'SET_VIEW_TYPE': {
const {type} = action.payload
const properties = {...activeTimeMachine.view.properties, type}
const view = {...activeTimeMachine.view, properties}
newActiveTimeMachine = {...activeTimeMachine, view}
break
}
}
if (newActiveTimeMachine) {

View File

@ -44,7 +44,9 @@
@import 'src/dashboards/components/rename_dashboard/RenameDashboard';
@import 'src/dashboards/components/dashboard_empty/DashboardEmpty';
@import 'src/dashboards/components/VEO';
@import 'src/shared/components/TimeMachine';
@import 'src/shared/components/TimeMachineControls';
@import 'src/shared/components/ViewTypeSelector';
@import 'src/dataExplorer/components/DataExplorer';
@import 'src/shared/components/views/Markdown';
@import 'src/onboarding/OnboardingWizard.scss';