Merge pull request #3458 from influxdata/ts-party/misc-shared-components

Convert a Handful of Components to TypeScript
pull/10616/head
Alex Paxton 2018-05-17 11:55:45 -07:00 committed by GitHub
commit 9fc462abae
20 changed files with 744 additions and 614 deletions

View File

@ -223,7 +223,7 @@ class DashboardPage extends Component {
dashboardActions.addDashboardCellAsync(dashboard)
}
handleCloneCell = cell => () => {
handleCloneCell = cell => {
const {dashboardActions, dashboard} = this.props
dashboardActions.cloneDashboardCellAsync(dashboard, cell)
}

View File

@ -1,14 +1,11 @@
import React, {PureComponent} from 'react'
import classnames from 'classnames'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Query {
rawText: string
}
import {QueryConfig} from 'src/types/query'
interface Props {
isActive: boolean
query: Query
query: QueryConfig
onSelect: (index: number) => void
onDelete: (index: number) => void
queryTabText: string

View File

@ -48,6 +48,7 @@ import DeprecationWarning from 'src/admin/components/DeprecationWarning'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {Source, Kapacitor} from 'src/types'
import {Notification} from 'src/types/notifications'
import {ServiceProperties, SpecificConfigOptions} from 'src/types/kapacitor'
import SlackConfigs from 'src/kapacitor/components/config/SlackConfigs'
import {
@ -101,14 +102,6 @@ interface Sections {
victorops: Section
}
interface Notification {
id?: string
type: string
icon: string
duration: number
message: string
}
interface Props {
source: Source
kapacitor: Kapacitor

View File

@ -2,9 +2,11 @@ import React, {SFC} from 'react'
import _ from 'lodash'
import {TEMP_VAR_DASHBOARD_TIME} from 'src/shared/constants'
import {QueryConfig} from 'src/types/query'
interface Query {
query: string
config: QueryConfig
text: string
}
interface Props {
@ -12,7 +14,9 @@ interface Props {
}
const CustomTimeIndicator: SFC<Props> = ({queries}) => {
const q = queries.find(({query}) => !query.includes(TEMP_VAR_DASHBOARD_TIME))
const q = queries.find(
query => query.text.includes(TEMP_VAR_DASHBOARD_TIME) === false
)
const customLower = _.get(q, ['queryConfig', 'range', 'lower'], null)
const customUpper = _.get(q, ['queryConfig', 'range', 'upper'], null)

View File

@ -1,105 +0,0 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import _ from 'lodash'
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
import LayoutCellMenu from 'shared/components/LayoutCellMenu'
import LayoutCellHeader from 'shared/components/LayoutCellHeader'
import {notify} from 'src/shared/actions/notifications'
import {notifyCSVDownloadFailed} from 'src/shared/copy/notifications'
import download from 'src/external/download.js'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {dataToCSV} from 'src/shared/parsing/dataToCSV'
import {timeSeriesToTableGraph} from 'src/utils/timeSeriesTransformers'
@ErrorHandling
class LayoutCell extends Component {
handleDeleteCell = cell => () => {
this.props.onDeleteCell(cell)
}
handleSummonOverlay = cell => () => {
this.props.onSummonOverlayTechnologies(cell)
}
handleCSVDownload = cell => () => {
const joinedName = cell.name.split(' ').join('_')
const {cellData} = this.props
const {data} = timeSeriesToTableGraph(cellData)
try {
download(dataToCSV(data), `${joinedName}.csv`, 'text/plain')
} catch (error) {
notify(notifyCSVDownloadFailed())
console.error(error)
}
}
render() {
const {cell, children, isEditable, cellData, onCloneCell} = this.props
const queries = _.get(cell, ['queries'], [])
// Passing the cell ID into the child graph so that further along
// we can detect if "this cell is the one being resized"
const child = children.length ? children[0] : children
const layoutCellGraph = React.cloneElement(child, {cellID: cell.i})
return (
<div className="dash-graph">
<Authorized requiredRole={EDITOR_ROLE}>
<LayoutCellMenu
cell={cell}
queries={queries}
dataExists={!!cellData.length}
isEditable={isEditable}
onDelete={this.handleDeleteCell}
onEdit={this.handleSummonOverlay}
onClone={onCloneCell}
onCSVDownload={this.handleCSVDownload}
/>
</Authorized>
<LayoutCellHeader cellName={cell.name} isEditable={isEditable} />
<div className="dash-graph--container">
{queries.length ? (
layoutCellGraph
) : (
<div className="graph-empty">
<Authorized requiredRole={EDITOR_ROLE}>
<button
className="no-query--button btn btn-md btn-primary"
onClick={this.handleSummonOverlay(cell)}
>
<span className="icon plus" /> Add Data
</button>
</Authorized>
</div>
)}
</div>
</div>
)
}
}
const {arrayOf, bool, func, node, number, shape, string} = PropTypes
LayoutCell.propTypes = {
cell: shape({
i: string.isRequired,
name: string.isRequired,
isEditing: bool,
x: number.isRequired,
y: number.isRequired,
queries: arrayOf(shape()),
}).isRequired,
children: node.isRequired,
onDeleteCell: func,
onCloneCell: func,
onSummonOverlayTechnologies: func,
isEditable: bool,
onCancelEditCell: func,
cellData: arrayOf(shape({})),
}
export default LayoutCell

View File

@ -0,0 +1,119 @@
import React, {Component, ReactElement} from 'react'
import _ from 'lodash'
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
import LayoutCellMenu from 'src/shared/components/LayoutCellMenu'
import LayoutCellHeader from 'src/shared/components/LayoutCellHeader'
import {notify} from 'src/shared/actions/notifications'
import {notifyCSVDownloadFailed} from 'src/shared/copy/notifications'
import download from 'src/external/download.js'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {dataToCSV} from 'src/shared/parsing/dataToCSV'
import {timeSeriesToTableGraph} from 'src/utils/timeSeriesTransformers'
import {Cell, CellQuery} from 'src/types/dashboard'
interface Series {
columns: string[]
name: string
values: number[][]
}
interface Result {
statement_id: number
series: Series[]
}
interface Response {
results: Result[]
}
interface Data {
response: Response[]
}
interface Props {
cell: Cell
children: ReactElement<any>
onDeleteCell: (cell: Cell) => void
onCloneCell: (cell: Cell) => void
onSummonOverlayTechnologies: (cell: Cell) => void
isEditable: boolean
onCancelEditCell: () => void
cellData: Data[]
}
@ErrorHandling
export default class LayoutCell extends Component<Props> {
public render() {
const {cell, isEditable, cellData, onDeleteCell, onCloneCell} = this.props
return (
<div className="dash-graph">
<Authorized requiredRole={EDITOR_ROLE}>
<LayoutCellMenu
cell={cell}
queries={this.queries}
dataExists={!!cellData.length}
isEditable={isEditable}
onDelete={onDeleteCell}
onEdit={this.handleSummonOverlay}
onClone={onCloneCell}
onCSVDownload={this.handleCSVDownload}
/>
</Authorized>
<LayoutCellHeader cellName={cell.name} isEditable={isEditable} />
<div className="dash-graph--container">{this.renderGraph}</div>
</div>
)
}
private get queries(): CellQuery[] {
const {cell} = this.props
return _.get(cell, ['queries'], [])
}
private get renderGraph(): JSX.Element {
const {cell, children} = this.props
if (this.queries.length) {
const child = React.Children.only(children)
return React.cloneElement(child, {cellID: cell.id})
}
return this.emptyGraph
}
private get emptyGraph(): JSX.Element {
return (
<div className="graph-empty">
<Authorized requiredRole={EDITOR_ROLE}>
<button
className="no-query--button btn btn-md btn-primary"
onClick={this.handleSummonOverlay}
>
<span className="icon plus" /> Add Data
</button>
</Authorized>
</div>
)
}
private handleSummonOverlay = (): void => {
const {cell, onSummonOverlayTechnologies} = this.props
onSummonOverlayTechnologies(cell)
}
private handleCSVDownload = (): void => {
const {cellData, cell} = this.props
const joinedName = cell.name.split(' ').join('_')
const {data} = timeSeriesToTableGraph(cellData)
try {
download(dataToCSV(data), `${joinedName}.csv`, 'text/plain')
} catch (error) {
notify(notifyCSVDownloadFailed())
console.error(error)
}
}
}

View File

@ -1,156 +0,0 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import classnames from 'classnames'
import MenuTooltipButton from 'src/shared/components/MenuTooltipButton'
import CustomTimeIndicator from 'src/shared/components/CustomTimeIndicator'
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
import {EDITING} from 'src/shared/annotations/helpers'
import {cellSupportsAnnotations} from 'src/shared/constants/index'
import {
addingAnnotation,
editingAnnotation,
dismissEditingAnnotation,
} from 'src/shared/actions/annotations'
import {ErrorHandling} from 'src/shared/decorators/errors'
@ErrorHandling
class LayoutCellMenu extends Component {
state = {
subMenuIsOpen: false,
}
handleToggleSubMenu = () => {
this.setState({subMenuIsOpen: !this.state.subMenuIsOpen})
}
render() {
const {subMenuIsOpen} = this.state
const {
mode,
cell,
onEdit,
onClone,
queries,
onDelete,
isEditable,
dataExists,
onCSVDownload,
onStartAddingAnnotation,
onStartEditingAnnotation,
onDismissEditingAnnotation,
} = this.props
const menuOptions = [
{
text: 'Configure',
action: onEdit(cell),
},
{
text: 'Add Annotation',
action: onStartAddingAnnotation,
disabled: !cellSupportsAnnotations(cell.type),
},
{
text: 'Edit Annotations',
action: onStartEditingAnnotation,
disabled: !cellSupportsAnnotations(cell.type),
},
{
text: 'Download CSV',
action: onCSVDownload(cell),
disabled: !dataExists,
},
]
return (
<div
className={classnames('dash-graph-context', {
'dash-graph-context__open': subMenuIsOpen,
})}
>
<div
className={`${
isEditable
? 'dash-graph--custom-indicators dash-graph--draggable'
: 'dash-graph--custom-indicators'
}`}
>
{queries && <CustomTimeIndicator queries={queries} />}
</div>
{isEditable &&
mode !== EDITING && (
<div className="dash-graph-context--buttons">
{queries.length ? (
<MenuTooltipButton
icon="pencil"
menuOptions={menuOptions}
informParent={this.handleToggleSubMenu}
/>
) : null}
<Authorized requiredRole={EDITOR_ROLE}>
<MenuTooltipButton
icon="duplicate"
menuOptions={[{text: 'Clone Cell', action: onClone(cell)}]}
informParent={this.handleToggleSubMenu}
/>
</Authorized>
<MenuTooltipButton
icon="trash"
theme="danger"
menuOptions={[{text: 'Confirm', action: onDelete(cell)}]}
informParent={this.handleToggleSubMenu}
/>
</div>
)}
{mode === 'editing' &&
cellSupportsAnnotations(cell.type) && (
<div className="dash-graph-context--buttons">
<div
className="btn btn-xs btn-success"
onClick={onDismissEditingAnnotation}
>
Done Editing
</div>
</div>
)}
</div>
)
}
}
const {arrayOf, bool, func, shape, string} = PropTypes
LayoutCellMenu.propTypes = {
mode: string,
onEdit: func,
onClone: func,
onDelete: func,
cell: shape(),
isEditable: bool,
dataExists: bool,
onCSVDownload: func,
queries: arrayOf(shape()),
onStartAddingAnnotation: func.isRequired,
onStartEditingAnnotation: func.isRequired,
onDismissEditingAnnotation: func.isRequired,
}
const mapStateToProps = ({annotations: {mode}}) => ({
mode,
})
const mapDispatchToProps = dispatch => ({
onStartAddingAnnotation: bindActionCreators(addingAnnotation, dispatch),
onStartEditingAnnotation: bindActionCreators(editingAnnotation, dispatch),
onDismissEditingAnnotation: bindActionCreators(
dismissEditingAnnotation,
dispatch
),
})
export default connect(mapStateToProps, mapDispatchToProps)(LayoutCellMenu)

View File

@ -0,0 +1,214 @@
import React, {Component} from 'react'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import classnames from 'classnames'
import MenuTooltipButton, {
MenuItem,
} from 'src/shared/components/MenuTooltipButton'
import CustomTimeIndicator from 'src/shared/components/CustomTimeIndicator'
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
import {EDITING} from 'src/shared/annotations/helpers'
import {cellSupportsAnnotations} from 'src/shared/constants/index'
import {Cell} from 'src/types/dashboard'
import {QueryConfig} from 'src/types/query'
import {
addingAnnotation,
editingAnnotation,
dismissEditingAnnotation,
} from 'src/shared/actions/annotations'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Query {
text: string
config: QueryConfig
}
interface Props {
cell: Cell
isEditable: boolean
dataExists: boolean
mode: string
onEdit: () => void
onClone: (cell: Cell) => void
onDelete: (cell: Cell) => void
onCSVDownload: () => void
onStartAddingAnnotation: () => void
onStartEditingAnnotation: () => void
onDismissEditingAnnotation: () => void
queries: Query[]
}
interface State {
subMenuIsOpen: boolean
}
@ErrorHandling
class LayoutCellMenu extends Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
subMenuIsOpen: false,
}
}
public render() {
const {queries} = this.props
return (
<div className={this.contextMenuClassname}>
<div className={this.customIndicatorsClassname}>
{queries && <CustomTimeIndicator queries={queries} />}
</div>
{this.renderMenu}
</div>
)
}
private get renderMenu(): JSX.Element {
const {isEditable, mode, cell, onDismissEditingAnnotation} = this.props
if (mode === EDITING && cellSupportsAnnotations(cell.type)) {
return (
<div className="dash-graph-context--buttons">
<div
className="btn btn-xs btn-success"
onClick={onDismissEditingAnnotation}
>
Done Editing
</div>
</div>
)
}
if (isEditable && mode !== EDITING) {
return (
<div className="dash-graph-context--buttons">
{this.pencilMenu}
<Authorized requiredRole={EDITOR_ROLE}>
<MenuTooltipButton
icon="duplicate"
menuItems={this.cloneMenuItems}
informParent={this.handleToggleSubMenu}
/>
</Authorized>
<MenuTooltipButton
icon="trash"
theme="danger"
menuItems={this.deleteMenuItems}
informParent={this.handleToggleSubMenu}
/>
</div>
)
}
}
private get pencilMenu(): JSX.Element {
const {queries} = this.props
if (!queries.length) {
return
}
return (
<MenuTooltipButton
icon="pencil"
menuItems={this.editMenuItems}
informParent={this.handleToggleSubMenu}
/>
)
}
private get contextMenuClassname(): string {
const {subMenuIsOpen} = this.state
return classnames('dash-graph-context', {
'dash-graph-context__open': subMenuIsOpen,
})
}
private get customIndicatorsClassname(): string {
const {isEditable} = this.props
return classnames('dash-graph--custom-indicators', {
'dash-graph--draggable': isEditable,
})
}
private get editMenuItems(): MenuItem[] {
const {
cell,
dataExists,
onStartAddingAnnotation,
onStartEditingAnnotation,
onCSVDownload,
} = this.props
return [
{
text: 'Configure',
action: this.handleEditCell,
disabled: false,
},
{
text: 'Add Annotation',
action: onStartAddingAnnotation,
disabled: !cellSupportsAnnotations(cell.type),
},
{
text: 'Edit Annotations',
action: onStartEditingAnnotation,
disabled: !cellSupportsAnnotations(cell.type),
},
{
text: 'Download CSV',
action: onCSVDownload,
disabled: !dataExists,
},
]
}
private get cloneMenuItems(): MenuItem[] {
return [{text: 'Clone Cell', action: this.handleCloneCell, disabled: false}]
}
private get deleteMenuItems(): MenuItem[] {
return [{text: 'Confirm', action: this.handleDeleteCell, disabled: false}]
}
private handleEditCell = (): void => {
const {onEdit} = this.props
onEdit()
}
private handleDeleteCell = (): void => {
const {onDelete, cell} = this.props
onDelete(cell)
}
private handleCloneCell = (): void => {
const {onClone, cell} = this.props
onClone(cell)
}
private handleToggleSubMenu = (): void => {
this.setState({subMenuIsOpen: !this.state.subMenuIsOpen})
}
}
const mapStateToProps = ({annotations: {mode}}) => ({
mode,
})
const mapDispatchToProps = dispatch => ({
onStartAddingAnnotation: bindActionCreators(addingAnnotation, dispatch),
onStartEditingAnnotation: bindActionCreators(editingAnnotation, dispatch),
onDismissEditingAnnotation: bindActionCreators(
dismissEditingAnnotation,
dispatch
),
})
export default connect(mapStateToProps, mapDispatchToProps)(LayoutCellMenu)

View File

@ -1,102 +0,0 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import OnClickOutside from 'react-onclickoutside'
import classnames from 'classnames'
import {ErrorHandling} from 'src/shared/decorators/errors'
@ErrorHandling
class MenuTooltipButton extends Component {
state = {
expanded: false,
}
handleButtonClick = () => {
const {informParent} = this.props
this.setState({expanded: !this.state.expanded})
informParent()
}
handleMenuItemClick = menuItemAction => () => {
const {informParent} = this.props
this.setState({expanded: false})
menuItemAction()
informParent()
}
handleClickOutside = () => {
const {informParent} = this.props
const {expanded} = this.state
if (expanded === false) {
return
}
this.setState({expanded: false})
informParent()
}
renderMenuOptions = () => {
const {menuOptions} = this.props
const {expanded} = this.state
if (expanded === false) {
return null
}
return menuOptions.map((option, i) => (
<div
key={i}
className={`dash-graph-context--menu-item${
option.disabled ? ' disabled' : ''
}`}
onClick={
option.disabled ? null : this.handleMenuItemClick(option.action)
}
>
{option.text}
</div>
))
}
render() {
const {icon, theme} = this.props
const {expanded} = this.state
return (
<div
className={classnames('dash-graph-context--button', {active: expanded})}
onClick={this.handleButtonClick}
>
<span className={`icon ${icon}`} />
{expanded ? (
<div className={`dash-graph-context--menu ${theme}`}>
{this.renderMenuOptions()}
</div>
) : null}
</div>
)
}
}
const {arrayOf, bool, func, shape, string} = PropTypes
MenuTooltipButton.defaultProps = {
theme: 'default',
}
MenuTooltipButton.propTypes = {
theme: string, // accepted values: default, primary, warning, success, danger
icon: string.isRequired,
menuOptions: arrayOf(
shape({
text: string.isRequired,
action: func.isRequired,
disabled: bool,
})
).isRequired,
informParent: func,
}
export default OnClickOutside(MenuTooltipButton)

View File

@ -0,0 +1,117 @@
import React, {Component} from 'react'
import {ClickOutside} from 'src/shared/components/ClickOutside'
import classnames from 'classnames'
import {ErrorHandling} from 'src/shared/decorators/errors'
type MenuItemAction = () => void
export interface MenuItem {
text: string
action: MenuItemAction
disabled?: boolean
}
interface Props {
theme?: string
icon: string
informParent: () => void
menuItems: MenuItem[]
}
interface State {
expanded: boolean
}
@ErrorHandling
export default class MenuTooltipButton extends Component<Props, State> {
public static defaultProps: Partial<Props> = {
theme: 'default',
}
constructor(props: Props) {
super(props)
this.state = {
expanded: false,
}
}
public render() {
const {expanded} = this.state
return (
<ClickOutside onClickOutside={this.handleClickOutside}>
<div className={this.className} onClick={this.handleButtonClick}>
{this.icon}
{expanded && this.renderMenu}
</div>
</ClickOutside>
)
}
private handleButtonClick = (): void => {
const {informParent} = this.props
this.setState({expanded: !this.state.expanded})
informParent()
}
private handleMenuItemClick = (action: MenuItemAction) => (): void => {
const {informParent} = this.props
this.setState({expanded: false})
action()
informParent()
}
private handleClickOutside = (): void => {
const {informParent} = this.props
const {expanded} = this.state
if (expanded === false) {
return
}
this.setState({expanded: false})
informParent()
}
private get className(): string {
const {expanded} = this.state
return classnames('dash-graph-context--button', {active: expanded})
}
private get icon(): JSX.Element {
const {icon} = this.props
return <span className={`icon ${icon}`} />
}
private get renderMenu(): JSX.Element {
const {menuItems, theme} = this.props
const {expanded} = this.state
if (expanded === false) {
return null
}
return (
<div className={`dash-graph-context--menu ${theme}`}>
{menuItems.map((option, i) => (
<div
key={i}
className={`dash-graph-context--menu-item${
option.disabled ? ' disabled' : ''
}`}
onClick={
option.disabled ? null : this.handleMenuItemClick(option.action)
}
>
{option.text}
</div>
))}
</div>
)
}
}

View File

@ -1,111 +0,0 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import classnames from 'classnames'
import {dismissNotification as dismissNotificationAction} from 'shared/actions/notifications'
import {NOTIFICATION_TRANSITION} from 'shared/constants/index'
import {ErrorHandling} from 'src/shared/decorators/errors'
@ErrorHandling
class Notification extends Component {
constructor(props) {
super(props)
this.state = {
opacity: 1,
height: 0,
dismissed: false,
}
}
componentDidMount() {
const {
notification: {duration},
} = this.props
this.updateHeight()
if (duration >= 0) {
// Automatically dismiss notification after duration prop
this.dismissTimer = setTimeout(this.handleDismiss, duration)
}
}
updateHeight() {
if (this.notificationRef) {
const {height} = this.notificationRef.getBoundingClientRect()
this.setState({height})
}
}
componentWillUnmount() {
clearTimeout(this.dismissTimer)
clearTimeout(this.deleteTimer)
}
handleDismiss = () => {
const {
notification: {id},
dismissNotification,
} = this.props
this.setState({dismissed: true})
this.deleteTimer = setTimeout(
() => dismissNotification(id),
NOTIFICATION_TRANSITION
)
}
onNotificationRef = ref => {
this.notificationRef = ref
this.updateHeight()
}
render() {
const {
notification: {type, message, icon},
} = this.props
const {height, dismissed} = this.state
const notificationContainerClass = classnames('notification-container', {
show: !!height,
'notification-dismissed': dismissed,
})
const notificationClass = `notification notification-${type}`
const notificationMargin = 4
const style = {height: height + notificationMargin}
return (
<div className={notificationContainerClass} style={style}>
<div className={notificationClass} ref={this.onNotificationRef}>
<span className={`icon ${icon}`} />
<div className="notification-message">{message}</div>
<button className="notification-close" onClick={this.handleDismiss} />
</div>
</div>
)
}
}
const {func, number, shape, string} = PropTypes
Notification.propTypes = {
notification: shape({
id: string.isRequired,
type: string.isRequired,
message: string.isRequired,
duration: number.isRequired,
icon: string.isRequired,
}).isRequired,
dismissNotification: func.isRequired,
}
const mapDispatchToProps = dispatch => ({
dismissNotification: bindActionCreators(dismissNotificationAction, dispatch),
})
export default connect(null, mapDispatchToProps)(Notification)

View File

@ -0,0 +1,132 @@
import React, {Component, CSSProperties} from 'react'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import {Notification as NotificationType} from 'src/types/notifications'
import classnames from 'classnames'
import {dismissNotification as dismissNotificationAction} from 'src/shared/actions/notifications'
import {NOTIFICATION_TRANSITION} from 'src/shared/constants/index'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
notification: NotificationType
dismissNotification: (id: string) => void
}
interface State {
opacity: number
height: number
dismissed: boolean
}
@ErrorHandling
class Notification extends Component<Props, State> {
private notificationRef: HTMLElement
private dismissalTimer: number
private deletionTimer: number
constructor(props) {
super(props)
this.state = {
opacity: 1,
height: 0,
dismissed: false,
}
}
public componentDidMount() {
const {
notification: {duration},
} = this.props
this.updateHeight()
if (duration >= 0) {
// Automatically dismiss notification after duration prop
this.dismissalTimer = window.setTimeout(this.handleDismiss, duration)
}
}
public componentWillUnmount() {
clearTimeout(this.dismissalTimer)
clearTimeout(this.deletionTimer)
}
public render() {
const {
notification: {message, icon},
} = this.props
return (
<div className={this.containerClassname} style={this.notificationStyle}>
<div
className={this.notificationClassname}
ref={this.handleNotificationRef}
>
<span className={`icon ${icon}`} />
<div className="notification-message">{message}</div>
<button className="notification-close" onClick={this.handleDismiss} />
</div>
</div>
)
}
private get notificationClassname(): string {
const {
notification: {type},
} = this.props
return `notification notification-${type}`
}
private get containerClassname(): string {
const {height, dismissed} = this.state
return classnames('notification-container', {
show: !!height,
'notification-dismissed': dismissed,
})
}
private get notificationStyle(): CSSProperties {
const {height} = this.state
const NOTIFICATION_MARGIN = 4
return {height: height + NOTIFICATION_MARGIN}
}
private updateHeight = (): void => {
if (this.notificationRef) {
const {height} = this.notificationRef.getBoundingClientRect()
this.setState({height})
}
}
private handleDismiss = (): void => {
const {
notification: {id},
dismissNotification,
} = this.props
this.setState({dismissed: true})
this.deletionTimer = window.setTimeout(
() => dismissNotification(id),
NOTIFICATION_TRANSITION
)
}
private handleNotificationRef = (ref: HTMLElement): void => {
this.notificationRef = ref
this.updateHeight()
}
}
const mapDispatchToProps = dispatch => ({
dismissNotification: bindActionCreators(dismissNotificationAction, dispatch),
})
export default connect(null, mapDispatchToProps)(Notification)

View File

@ -1,44 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import Notification from 'shared/components/Notification'
const Notifications = ({notifications, inPresentationMode}) => (
<div
className={`${
inPresentationMode
? 'notification-center__presentation-mode'
: 'notification-center'
}`}
>
{notifications.map(n => <Notification key={n.id} notification={n} />)}
</div>
)
const {arrayOf, bool, number, shape, string} = PropTypes
Notifications.propTypes = {
notifications: arrayOf(
shape({
id: string.isRequired,
type: string.isRequired,
message: string.isRequired,
duration: number.isRequired,
icon: string,
})
),
inPresentationMode: bool,
}
const mapStateToProps = ({
notifications,
app: {
ephemeral: {inPresentationMode},
},
}) => ({
notifications,
inPresentationMode,
})
export default connect(mapStateToProps, null)(Notifications)

View File

@ -0,0 +1,47 @@
import React, {PureComponent} from 'react'
import {connect} from 'react-redux'
import {Notification as NotificationType} from 'src/types/notifications'
import Notification from 'src/shared/components/Notification'
interface Props {
inPresentationMode?: boolean
notifications: NotificationType[]
}
class Notifications extends PureComponent<Props> {
public static defaultProps: Partial<Props> = {
inPresentationMode: false,
}
public render() {
const {notifications} = this.props
return (
<div className={this.className}>
{notifications.map(n => <Notification key={n.id} notification={n} />)}
</div>
)
}
private get className(): string {
const {inPresentationMode} = this.props
if (inPresentationMode) {
return 'notification-center__presentation-mode'
}
return 'notification-center'
}
}
const mapStateToProps = ({
notifications,
app: {
ephemeral: {inPresentationMode},
},
}): Props => ({
notifications,
inPresentationMode,
})
export default connect(mapStateToProps, null)(Notifications)

View File

@ -1,51 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import QueryMakerTab from 'src/data_explorer/components/QueryMakerTab'
import buildInfluxQLQuery from 'utils/influxql'
const QueryTabList = ({
queries,
timeRange,
onAddQuery,
onDeleteQuery,
activeQueryIndex,
setActiveQueryIndex,
}) => (
<div className="query-maker--tabs">
{queries.map((q, i) => (
<QueryMakerTab
isActive={i === activeQueryIndex}
key={i}
queryIndex={i}
query={q}
onSelect={setActiveQueryIndex}
onDelete={onDeleteQuery}
queryTabText={
q.rawText || buildInfluxQLQuery(timeRange, q) || `Query ${i + 1}`
}
/>
))}
<div
className="query-maker--new btn btn-sm btn-primary"
onClick={onAddQuery}
>
<span className="icon plus" />
</div>
</div>
)
const {arrayOf, func, number, shape, string} = PropTypes
QueryTabList.propTypes = {
queries: arrayOf(shape({})).isRequired,
timeRange: shape({
upper: string,
lower: string,
}).isRequired,
onAddQuery: func.isRequired,
onDeleteQuery: func.isRequired,
activeQueryIndex: number.isRequired,
setActiveQueryIndex: func.isRequired,
}
export default QueryTabList

View File

@ -0,0 +1,55 @@
import React, {PureComponent} from 'react'
import QueryMakerTab from 'src/data_explorer/components/QueryMakerTab'
import buildInfluxQLQuery from 'src/utils/influxql'
import {QueryConfig, TimeRange} from 'src/types/query'
interface Props {
queries: QueryConfig[]
onAddQuery: () => void
onDeleteQuery: (index: number) => void
activeQueryIndex: number
setActiveQueryIndex: (index: number) => void
timeRange: TimeRange
}
export default class QueryTabList extends PureComponent<Props> {
public render() {
const {
queries,
onAddQuery,
onDeleteQuery,
activeQueryIndex,
setActiveQueryIndex,
} = this.props
return (
<div className="query-maker--tabs">
{queries.map((q, i) => (
<QueryMakerTab
key={i}
isActive={i === activeQueryIndex}
query={q}
onSelect={setActiveQueryIndex}
onDelete={onDeleteQuery}
queryTabText={this.queryTabText(i, q)}
queryIndex={i}
/>
))}
<div
className="query-maker--new btn btn-sm btn-primary"
onClick={onAddQuery}
>
<span className="icon plus" />
</div>
</div>
)
}
private queryTabText = (i: number, query: QueryConfig): string => {
const {timeRange} = this.props
return (
query.rawText || buildInfluxQLQuery(timeRange, query) || `Query ${i + 1}`
)
}
}

View File

@ -1,5 +1,4 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
@ -8,36 +7,36 @@ import {updateThresholdsListType} from 'src/dashboards/actions/cellEditorOverlay
import {
THRESHOLD_TYPE_TEXT,
THRESHOLD_TYPE_BG,
} from 'shared/constants/thresholds'
} from 'src/shared/constants/thresholds'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface PropsFromRedux {
thresholdsListType: string
}
interface PropsFromParent {
containerClass: string
handleUpdateThresholdsListType: (newType: string) => void
}
type Props = PropsFromRedux & PropsFromParent
@ErrorHandling
class ThresholdsListTypeToggle extends Component {
handleToggleThresholdsListType = newType => () => {
const {handleUpdateThresholdsListType} = this.props
handleUpdateThresholdsListType(newType)
}
render() {
const {thresholdsListType, containerClass} = this.props
class ThresholdsListTypeToggle extends Component<Props> {
public render() {
const {containerClass} = this.props
return (
<div className={containerClass}>
<label>Threshold Coloring</label>
<ul className="nav nav-tablist nav-tablist-sm">
<li
className={`${
thresholdsListType === THRESHOLD_TYPE_BG ? 'active' : ''
}`}
className={this.bgTabClassName}
onClick={this.handleToggleThresholdsListType(THRESHOLD_TYPE_BG)}
>
Background
</li>
<li
className={`${
thresholdsListType === THRESHOLD_TYPE_TEXT ? 'active' : ''
}`}
className={this.textTabClassName}
onClick={this.handleToggleThresholdsListType(THRESHOLD_TYPE_TEXT)}
>
Text
@ -46,16 +45,37 @@ class ThresholdsListTypeToggle extends Component {
</div>
)
}
}
const {func, string} = PropTypes
ThresholdsListTypeToggle.propTypes = {
thresholdsListType: string.isRequired,
handleUpdateThresholdsListType: func.isRequired,
containerClass: string.isRequired,
private get bgTabClassName(): string {
const {thresholdsListType} = this.props
if (thresholdsListType === THRESHOLD_TYPE_BG) {
return 'active'
}
return ''
}
private get textTabClassName(): string {
const {thresholdsListType} = this.props
if (thresholdsListType === THRESHOLD_TYPE_TEXT) {
return 'active'
}
return ''
}
private handleToggleThresholdsListType = (newType: string) => (): void => {
const {handleUpdateThresholdsListType} = this.props
handleUpdateThresholdsListType(newType)
}
}
const mapStateToProps = ({cellEditorOverlay: {thresholdsListType}}) => ({
const mapStateToProps = ({
cellEditorOverlay: {thresholdsListType},
}): PropsFromRedux => ({
thresholdsListType,
})

View File

@ -57,8 +57,6 @@
text-align: right !important;
user-select: none;
}
.graph-container > div > div > div > div {
}
/* Vertical Axis Labels */
.dygraph-ylabel,
@ -171,7 +169,7 @@
display: block !important;
position: absolute;
padding: 11px;
z-index: 500;
z-index: $dygraph-legend-z;
border-radius: 3px;
min-width: 350px;
user-select: text;

View File

@ -2,6 +2,10 @@
Page Layout
----------------------------------------------------------------------------
*/
$dygraph-legend-z: 500;
$dash-ceo-z: $dygraph-legend-z + 10;
.chronograf-root {
display: flex;
align-items: stretch;

View File

@ -5,7 +5,6 @@
$overlay-controls-height: 60px;
$overlay-controls-bg: $g2-kevlar;
$overlay-z: 100;
// Make Overlay Technology full screen
@ -26,7 +25,7 @@ $overlay-z: 100;
top: 0;
bottom: 0;
right: 0;
z-index: $overlay-z;
z-index: $dash-ceo-z;
padding: 0 30px;
/*