Protoboard selection (#4263)

* Add protoboard types

* Add new component GridSizer

* Remove obsolete CardSelectList component

* Update CardSelectCardComponent

* import Gridsizer styles

* Add Dashboard step to wizard

* Remove obsolete type

* update Protoboard types to be more specific

* Move css import, read selected value existing in state

* Use lodash debounce, add defaultl width prop

* Add defaut props for checked and disabled, always prevent default
pull/4269/head
Daniel Campbell 2018-08-21 15:42:27 -07:00 committed by GitHub
parent 789d3d7f1e
commit e98973b0ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 425 additions and 388 deletions

View File

@ -3,54 +3,12 @@
------------------------------------------------------------------------------
*/
$card-select--gutter: 4px;
$card-breakpoint-col-2: 600px;
$card-breakpoint-col-3: 700px;
$card-breakpoint-col-4: 800px;
$card-breakpoint-col-5: 900px;
$card-breakpoint-col-6: 1000px;
.card-select--card-holder {
width: 100%;
padding-bottom: 100%;
float: left;
position: relative;
@media only screen and (min-width: $card-breakpoint-col-2) {
width: 50%;
padding-bottom: 50%;
}
@media only screen and (min-width: $card-breakpoint-col-3) {
width: 33.3333%;
padding-bottom: 33.3333%;
}
@media only screen and (min-width: $card-breakpoint-col-4) {
width: 25%;
padding-bottom: 25%;
}
@media only screen and (min-width: $card-breakpoint-col-5) {
width: 20%;
padding-bottom: 20%;
}
@media only screen and (min-width: $card-breakpoint-col-6) {
width: 16.6667%;
padding-bottom: 16.6667%;
}
}
.card-select--card {
min-width: 100%;
min-height: 100%;
background-color: $g1-raven;
border-radius: $radius;
border: $ix-border solid transparent;
width: calc(100% - #{$card-select--gutter});
height: calc(100% - #{$card-select--gutter});
margin: $card-select--gutter / 2;
position: absolute;
transition: border-color .2s ease, background-color .2s ease;
box-sizing: border-box;
}
@ -79,7 +37,7 @@ $card-breakpoint-col-6: 1000px;
.card-select--image {
justify-content: center;
width: 60%;
max-width: 60%;
.card-select--placeholder {
color: $ix-text-default;
transition: color .2s ease;

View File

@ -1,39 +1,32 @@
// Libraries
import React, {PureComponent} from 'react'
import classnames from 'classnames'
// Types
import {CardSelectCardProps} from 'src/types/cardSelect'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface State {
checked: boolean
interface Props {
id: string
name?: string
label: string
image?: string
checked?: boolean
disabled?: boolean
onClick: () => void
}
@ErrorHandling
class CardSelectCard extends PureComponent<CardSelectCardProps, State> {
public static defaultProps: Partial<CardSelectCardProps> = {
class CardSelectCard extends PureComponent<Props> {
public static defaultProps: Partial<Props> = {
checked: false,
disabled: false,
}
constructor(props) {
super(props)
this.state = {
checked: this.props.checked,
}
}
public render() {
const {id, label, disabled} = this.props
const {checked} = this.state
const {id, label, checked, name, disabled} = this.props
return (
<div className="card-select--card-holder">
<div
data-toggle="card_toggle"
onClick={this.toggleChecked}
onClick={this.handleClick}
className={classnames('card-select--card', {
'card-select--checked': checked,
'card-select--disabled': disabled,
@ -43,7 +36,7 @@ class CardSelectCard extends PureComponent<CardSelectCardProps, State> {
<label className="card-select--container">
<input
id={`card_select_${id}`}
name={`card_select_${id}`}
name={name}
type="checkbox"
value={id}
checked={checked}
@ -63,24 +56,9 @@ class CardSelectCard extends PureComponent<CardSelectCardProps, State> {
<span className="card-select--label">{label}</span>
</label>
</div>
</div>
)
}
private toggleChecked = e => {
const {disabled} = this.props
if (disabled) {
return
}
const {checked} = this.state
e.preventDefault()
this.setState({
checked: !checked,
})
}
private get cardImage() {
const {image, label} = this.props
@ -90,6 +68,14 @@ class CardSelectCard extends PureComponent<CardSelectCardProps, State> {
return <span className="card-select--placeholder icon dash-j" />
}
private handleClick = e => {
const {onClick, disabled} = this.props
e.preventDefault()
if (!disabled) {
onClick()
}
}
}
export default CardSelectCard

View File

@ -1,14 +0,0 @@
/*
Card Selector Wrapper
------------------------------------------------------------------------------
*/
.card-select--wrapper {
border: none;
box-sizing: border-box;
}
.card-select--cards {
display: inline-block;
width: 100%;
}

View File

@ -1,29 +0,0 @@
import React, {PureComponent, ReactElement} from 'react'
import {ErrorHandling} from 'src/shared/decorators/errors'
import CardSelectCard from 'src/reusable_ui/components/card_select/CardSelectCard'
import {CardSelectCardProps} from 'src/types/cardSelect'
interface Props {
children: Array<ReactElement<CardSelectCardProps>>
legend: string
}
@ErrorHandling
class CardSelectList extends PureComponent<Props> {
public static Card = CardSelectCard
public render() {
const {children, legend} = this.props
return (
<fieldset className="card-select--wrapper">
<legend>{legend}</legend>
<div className="card-select--cards">{children}</div>
</fieldset>
)
}
}
export default CardSelectList

View File

@ -10,9 +10,11 @@ describe('Card Select Card', () => {
const props = {
id: 'card_id',
label: 'Card Label',
name: undefined,
image: undefined,
checked: undefined,
disabled: undefined,
onClick: undefined,
...override,
}
@ -46,27 +48,6 @@ describe('Card Select Card', () => {
expect(field.prop('type')).toBe('checkbox')
})
describe('toggleChecked method', () => {
it('sets checked to true if false', () => {
expect(wrapper.find('input').prop('checked')).toBe(false)
wrapper
.find('div')
.filterWhere(div => div.prop('data-toggle'))
.simulate('click', {preventDefault() {}})
expect(wrapper.find('input').prop('checked')).toBe(true)
})
it('sets checked to false if true', () => {
wrapper = wrapperSetup({checked: true})
expect(wrapper.find('input').prop('checked')).toBe(true)
wrapper
.find('div')
.filterWhere(div => div.prop('data-toggle'))
.simulate('click', {preventDefault() {}})
expect(wrapper.find('input').prop('checked')).toBe(false)
})
})
it('matches snapshot with minimal props', () => {
expect(wrapper).toMatchSnapshot()
})

View File

@ -1,54 +0,0 @@
import React from 'react'
import {shallow} from 'enzyme'
import CardSelectList from 'src/reusable_ui/components/card_select/CardSelectList'
import CardSelectCard from 'src/reusable_ui/components/card_select/CardSelectCard'
describe('Card Select Card', () => {
let wrapper
const wrapperSetup = (override = {}) => {
const props = {
children: undefined,
legend: 'legend',
...override,
}
return shallow(<CardSelectList {...props} />)
}
const childSetup = (override = {}) => {
const props = {
id: 'card_id',
label: 'Card Label',
image: undefined,
checked: undefined,
disabled: undefined,
...override,
}
return shallow(<CardSelectCard {...props} />)
}
const cardChildren = [childSetup(), childSetup()]
beforeEach(() => {
wrapper = wrapperSetup({children: cardChildren})
})
it('mounts without exploding', () => {
expect(wrapper).toHaveLength(1)
})
it('renders one fieldset', () => {
expect(wrapper.find('fieldset')).toHaveLength(1)
})
it('renders one legend', () => {
expect(wrapper.find('legend')).toHaveLength(1)
})
it('matches snapshot with two children', () => {
expect(wrapper).toMatchSnapshot()
})
})

View File

@ -2,13 +2,10 @@
exports[`Card Select Card matches snapshot with minimal props 1`] = `
<div
className="card-select--card-holder"
>
<div
className="card-select--card card-select--active"
data-toggle="card_toggle"
onClick={[Function]}
>
>
<label
className="card-select--container"
>
@ -16,7 +13,6 @@ exports[`Card Select Card matches snapshot with minimal props 1`] = `
checked={false}
disabled={false}
id="card_select_card_id"
name="card_select_card_id"
type="checkbox"
value="card_id"
/>
@ -36,19 +32,15 @@ exports[`Card Select Card matches snapshot with minimal props 1`] = `
Card Label
</span>
</label>
</div>
</div>
`;
exports[`Card Select Card with image matches snapshot when provided image source 1`] = `
<div
className="card-select--card-holder"
>
<div
className="card-select--card card-select--active"
data-toggle="card_toggle"
onClick={[Function]}
>
>
<label
className="card-select--container"
>
@ -56,7 +48,6 @@ exports[`Card Select Card with image matches snapshot when provided image source
checked={false}
disabled={false}
id="card_select_card_id"
name="card_select_card_id"
type="checkbox"
value="card_id"
/>
@ -77,6 +68,5 @@ exports[`Card Select Card with image matches snapshot when provided image source
Card Label
</span>
</label>
</div>
</div>
`;

View File

@ -1,89 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Card Select Card matches snapshot with two children 1`] = `
<fieldset
className="card-select--wrapper"
>
<legend>
legend
</legend>
<div
className="card-select--cards"
>
<div
className="card-select--card-holder"
>
<div
className="card-select--card card-select--active"
data-toggle="card_toggle"
onClick={[Function]}
>
<label
className="card-select--container"
>
<input
checked={false}
disabled={false}
id="card_select_card_id"
name="card_select_card_id"
type="checkbox"
value="card_id"
/>
<span
className="card-select--checkmark icon checkmark"
/>
<div
className="card-select--image"
>
<span
className="card-select--placeholder icon dash-j"
/>
</div>
<span
className="card-select--label"
>
Card Label
</span>
</label>
</div>
</div>
<div
className="card-select--card-holder"
>
<div
className="card-select--card card-select--active"
data-toggle="card_toggle"
onClick={[Function]}
>
<label
className="card-select--container"
>
<input
checked={false}
disabled={false}
id="card_select_card_id"
name="card_select_card_id"
type="checkbox"
value="card_id"
/>
<span
className="card-select--checkmark icon checkmark"
/>
<div
className="card-select--image"
>
<span
className="card-select--placeholder icon dash-j"
/>
</div>
<span
className="card-select--label"
>
Card Label
</span>
</label>
</div>
</div>
</div>
</fieldset>
`;

View File

@ -0,0 +1,52 @@
/*
Grid Sizer
------------------------------------------------------------------------------
*/
$card-select--gutter: 4px;
.grid-sizer {
width: 100%;
display: inline-block;
}
.grid-sizer--cell {
float: left;
position: relative;
width: calc(100% - #{$card-select--gutter});
height: calc(100% - #{$card-select--gutter});
margin: $card-select--gutter / 2;
}
.grid-sizer--col-2 {
width: calc(50% - #{$card-select--gutter});
padding-bottom: 50%;
}
.grid-sizer--col-3 {
width: calc(33.3333% - #{$card-select--gutter});
padding-bottom: 33.3333%;
}
.grid-sizer--col-4 {
width: calc(25% - #{$card-select--gutter});
padding-bottom: 25%;
}
.grid-sizer--col-5 {
width: calc(20% - #{$card-select--gutter});
padding-bottom: 20%;
}
.grid-sizer--col-6 {
width: calc(16.6667% - #{$card-select--gutter});
padding-bottom: 16.6667%;
}
.grid-sizer--content {
top: 0;
bottom: 0;
left: 0;
right: 0;
position: absolute;
}

View File

@ -0,0 +1,111 @@
// Libraries
import React, {PureComponent} from 'react'
import _ from 'lodash'
// Components
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
children?: JSX.Element[]
cellWidth?: number
width?: number
}
interface State {
columns: number
}
@ErrorHandling
class CardSelectList extends PureComponent<Props, State> {
public static defaultProps: Partial<Props> = {
cellWidth: 160,
width: null,
}
constructor(props) {
super(props)
this.state = {
columns: null,
}
}
public componentDidMount() {
const {width} = this.props
const widthValue = width || this.getWidth()
this.setColumns(widthValue)
if (!width) {
window.addEventListener(
'resize',
_.debounce(() => this.setColumns(this.getWidth()), 250),
false
)
}
}
public componentDidUpdate() {
const {width} = this.props
if (width) {
this.setColumns(width)
}
}
public componentWillUnmount() {
window.removeEventListener(
'resize',
_.debounce(() => this.setColumns(this.getWidth()), 250),
false
)
}
public render() {
return (
<div id="grid_sizer" className="grid-sizer">
{this.sizeChildren}
</div>
)
}
private get sizeChildren() {
const {columns} = this.state
const {children} = this.props
if (columns) {
const wrappedChildren = children.map((child, i) => (
<div
key={`grid_cell_${i}`}
className={`grid-sizer--cell grid-sizer--col-${columns}`}
>
<div className="grid-sizer--content">{child}</div>
</div>
))
return wrappedChildren
}
return children
}
private getWidth = () => {
const ele = document.getElementById('grid_sizer')
const computedWidth = window
.getComputedStyle(ele, null)
.getPropertyValue('width')
const widthValue = Number(
computedWidth.substring(0, computedWidth.length - 2)
)
return widthValue
}
private setColumns = (width: number) => {
const {cellWidth} = this.props
const columns = Math.round(width / cellWidth)
this.setState({
columns,
})
}
}
export default CardSelectList

View File

@ -7,6 +7,7 @@
position: relative;
overflow: hidden;
margin: $ix-marg-b 0 0 0;
width: 100%;
}
.wizard-step--child {

View File

@ -9,10 +9,11 @@ import WizardOverlay from 'src/reusable_ui/components/wizard/WizardOverlay'
import WizardStep from 'src/reusable_ui/components/wizard/WizardStep'
import SourceStep from 'src/sources/components/SourceStep'
import KapacitorStep from 'src/sources/components/KapacitorStep'
import DashboardStep from 'src/sources/components/DashboardStep'
import CompletionStep from 'src/sources/components/CompletionStep'
// Types
import {Kapacitor, Source} from 'src/types'
import {Kapacitor, Source, Protoboard} from 'src/types'
import {ToggleWizard} from 'src/types/wizard'
interface Props {
@ -28,6 +29,8 @@ interface State {
sourceError: boolean
kapacitor: Kapacitor
kapacitorError: boolean
dashboardError: boolean
dashboards: Protoboard[]
}
@ErrorHandling
@ -43,6 +46,7 @@ class ConnectionWizard extends PureComponent<Props & WithRouterProps, State> {
public sourceStepRef: any
public kapacitorStepRef: any
public completionStepRef: any
public dashboardStepRef: any
constructor(props: Props & WithRouterProps) {
super(props)
@ -51,12 +55,21 @@ class ConnectionWizard extends PureComponent<Props & WithRouterProps, State> {
kapacitor: null,
sourceError: false,
kapacitorError: false,
dashboardError: false,
dashboards: null,
}
}
public render() {
const {isVisible, toggleVisibility, jumpStep, showNewKapacitor} = this.props
const {source, sourceError, kapacitor, kapacitorError} = this.state
const {
source,
sourceError,
kapacitor,
kapacitorError,
dashboardError,
} = this.state
return (
<WizardOverlay
visible={isVisible}
@ -102,6 +115,13 @@ class ConnectionWizard extends PureComponent<Props & WithRouterProps, State> {
showNewKapacitor={showNewKapacitor}
/>
</WizardStep>
<WizardStep
title="Select Dashboards"
isComplete={this.isDashboardComplete}
isErrored={dashboardError}
>
{this.dashboardStep}
</WizardStep>
<WizardStep
title="Setup Complete"
tipText=""
@ -120,6 +140,16 @@ class ConnectionWizard extends PureComponent<Props & WithRouterProps, State> {
)
}
private get dashboardStep() {
const {dashboards} = this.state
if (dashboards) {
return <DashboardStep dashboards={dashboards} />
}
return null
}
private handleSourceNext = async () => {
const response = await this.sourceStepRef.next()
this.setState({source: response.payload})
@ -147,6 +177,10 @@ class ConnectionWizard extends PureComponent<Props & WithRouterProps, State> {
this.setState({kapacitorError: b})
}
private isDashboardComplete = () => {
return false
}
private isCompletionComplete = () => {
return false
}

View File

@ -0,0 +1,5 @@
.dashboard-step {
position: relative;
min-width: 100%;
min-height: 100%;
}

View File

@ -0,0 +1,81 @@
// Libraries
import React, {Component} from 'react'
// Components
import {ErrorHandling} from 'src/shared/decorators/errors'
import GridSizer from 'src/reusable_ui/components/grid_sizer/GridSizer'
import CardSelectCard from 'src/reusable_ui/components/card_select/CardSelectCard'
// Types
import {Protoboard} from 'src/types'
interface State {
selected: object
}
interface Props {
dashboards?: Protoboard[]
}
@ErrorHandling
class DashboardStep extends Component<Props, State> {
public constructor(props) {
super(props)
this.state = {
selected: {},
}
}
public render() {
return (
<div className="dashboard-step">
<GridSizer>{this.dashboardCards}</GridSizer>
</div>
)
}
private get dashboardCards() {
const {selected} = this.state
const {dashboards} = this.props
const cards =
dashboards &&
dashboards.map((dashboard, i) => {
const {meta} = dashboard
return (
<CardSelectCard
key={`${dashboard.id}_${i}`}
id={meta.name}
name={meta.name}
label={meta.name}
checked={selected[meta.name]}
onClick={this.toggleChecked(meta.name)}
/>
)
})
if (cards.length) {
return cards
}
return null
}
private toggleChecked = (name: string) => () => {
const {selected} = this.state
const newSelected = selected
if (selected[name]) {
newSelected[name] = false
} else {
newSelected[name] = true
}
this.setState({
selected: newSelected,
})
}
}
export default DashboardStep

View File

@ -73,6 +73,7 @@
@import 'components/histogram-chart';
@import 'src/sources/components/ConnectionLink';
@import 'src/sources/components/KapacitorStep';
@import 'src/sources/components/DashboardStep';
// Reusable UI Components
@ -82,6 +83,7 @@
@import '../reusable_ui/components/overlays/Overlay.scss';
@import '../dashboards/components/import_dashboard_mappings/ImportDashboardMappings.scss';
@import '../reusable_ui/components/card_select/CardSelectCard.scss';
@import '../reusable_ui/components/grid_sizer/GridSizer.scss';
@import '../reusable_ui/components/wizard/WizardController.scss';
@import '../reusable_ui/components/wizard/WizardButtonBar.scss';
@import '../reusable_ui/components/wizard/WizardFullScreen.scss';

View File

@ -1,7 +0,0 @@
export interface CardSelectCardProps {
id: string
label: string
image?: string
checked?: boolean
disabled?: boolean
}

View File

@ -199,3 +199,23 @@ export type NewDefaultCell = Pick<
Cell,
Exclude<keyof Cell, 'i' | 'axes' | 'colors' | 'links' | 'legend'>
>
export interface ProtoboardMetadata {
name: string
icon: string
version: string
dashboardVersion: string
description: string
author: string
license: string
url: string
}
export interface ProtoboardData {
cells: object
}
export interface Protoboard {
id: string
meta: ProtoboardMetadata
data: ProtoboardData
}

View File

@ -1,7 +1,15 @@
import {LayoutCell, LayoutQuery} from './layouts'
import {Service, NewService, ServiceLinks} from './services'
import {Links, Organization, Role, Permission, User, Me} from './auth'
import {Cell, CellQuery, Legend, Axes, Dashboard, CellType} from './dashboards'
import {
Cell,
CellQuery,
Legend,
Axes,
Dashboard,
CellType,
Protoboard,
} from './dashboards'
import {
Template,
TemplateQuery,
@ -66,6 +74,7 @@ export {
Cell,
CellQuery,
CellType,
Protoboard,
Legend,
Status,
Query,