fix: cell errors (#14168)

* fix: add tooltip for error message

* wip: styles for error

* chore: christmas time

* feat: introduce error formatting

* style(veo): scrollable errors

* style(cell): error tooltip

* chore: remove comment

* style: put styles on children
pull/14176/head
Andrew Watkins 2019-06-21 10:25:09 -07:00 committed by GitHub
parent 89acf847ea
commit 5dfba47d15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 230 additions and 66 deletions

View File

@ -4,20 +4,89 @@ $box-tooltip--caret-size: 6px;
position: fixed;
visibility: hidden;
z-index: $z--dygraph-legend;
border: $ix-border solid $c-pool;
}
.box-tooltip--contents {
border-width: $ix-border;
border-style: solid;
background-color: $g1-raven;
border-radius: $radius;
padding: 10px;
font-size: 13px;
word-break: break-all;
word-wrap: break-word;
width: calc(100% - #{$box-tooltip--caret-size * 2});
pre {
word-break: break-all;
word-wrap: break-word;
white-space: pre-wrap;
}
}
.box-tooltip.left .box-tooltip--caret {
.box-tooltip__primary {
.box-tooltip--contents {
border-color: $c-pool;
}
&.right .box-tooltip--caret {
border-right-color: $c-pool;
}
&.left .box-tooltip--caret {
border-left-color: $c-pool;
}
}
.box-tooltip__danger {
.box-tooltip--contents {
border-color: $c-dreamsicle;
}
&.right .box-tooltip--caret {
border-right-color: $c-dreamsicle;
}
&.left .box-tooltip--caret {
border-left-color: $c-dreamsicle;
}
}
.box-tooltip--caret {
position: absolute;
right: -$box-tooltip--caret-size;
transform: translate(0,-50%);
width: 0;
height: 0;
border-top: $box-tooltip--caret-size solid transparent;
border-bottom: $box-tooltip--caret-size solid transparent;
border-left: $box-tooltip--caret-size solid $c-pool;
border: $box-tooltip--caret-size solid transparent;
transform: translate(0,-50%);
}
.box-tooltip--caret-container {
position: absolute;
top: 0;
width: $box-tooltip--caret-size * 2;
height: 100%;
}
.box-tooltip.left {
.box-tooltip--contents {
margin-right: $box-tooltip--caret-size * 2;
}
.box-tooltip--caret-container {
right: 0;
}
.box-tooltip--caret {
left: 0;
}
}
.box-tooltip.right {
.box-tooltip--contents {
margin-left: $box-tooltip--caret-size * 2;
}
.box-tooltip--caret-container {
left: 0;
}
.box-tooltip--caret {
right: 0;
}
}

View File

@ -5,12 +5,22 @@ import {createPortal} from 'react-dom'
// Constants
import {TOOLTIP_PORTAL_ID} from 'src/portals/TooltipPortal'
// Types
import {ComponentColor} from '@influxdata/clockface'
interface Props {
triggerRect: DOMRect
children: JSX.Element
maxWidth?: number
color?: ComponentColor
}
const BoxTooltip: FunctionComponent<Props> = ({triggerRect, children}) => {
const BoxTooltip: FunctionComponent<Props> = ({
triggerRect,
children,
maxWidth = 300,
color = ComponentColor.Primary,
}) => {
const ref = useRef<HTMLDivElement>(null)
// Position the tooltip after it has rendered, taking into account its size
@ -23,8 +33,15 @@ const BoxTooltip: FunctionComponent<Props> = ({triggerRect, children}) => {
const rect = el.getBoundingClientRect()
// Always position the tooltip to the left of the trigger position
const left = triggerRect.left - rect.width
let left = triggerRect.left - rect.width
let caretClassName = 'left'
// If the width of the tooltip causes it to overflow left
// position it to the right of the trigger element
if (left < 0) {
left = triggerRect.left + triggerRect.width
caretClassName = 'right'
}
// Attempt to position the vertical midpoint of tooltip next to the
// vertical midpoint of the trigger rectangle
@ -44,7 +61,7 @@ const BoxTooltip: FunctionComponent<Props> = ({triggerRect, children}) => {
el.setAttribute(
'style',
`visibility: visible; top: ${top}px; left: ${left}px;`
`visibility: visible; top: ${top}px; left: ${left}px; max-width: ${maxWidth}px;`
)
// Position the caret (little arrow on the side of the tooltip) so that it
@ -55,12 +72,20 @@ const BoxTooltip: FunctionComponent<Props> = ({triggerRect, children}) => {
'style',
`top: ${caretTop}px;`
)
el.className = `box-tooltip box-tooltip__${color} ${caretClassName}`
})
return createPortal(
<div className="box-tooltip left" ref={ref}>
<div
className={`box-tooltip box-tooltip__${color}`}
ref={ref}
style={{maxWidth}}
>
{children}
<div className="box-tooltip--caret" />
<div className="box-tooltip--caret-container">
<div className="box-tooltip--caret" />
</div>
</div>,
document.querySelector(`#${TOOLTIP_PORTAL_ID}`)
)

View File

@ -1,11 +1,12 @@
.empty-graph-error {
width: 100%;
height: 100%;
border-radius: $radius;
background: $g2-kevlar;
position: relative;
color: $c-dreamsicle;
border: $ix-border solid $g5-pepper;
pre {
pre {
font-size: 14px;
padding: $ix-marg-c;
user-select: text !important;
@ -13,19 +14,7 @@
}
}
.empty-graph-error--icon {
.empty-graph-error--icon {
display: inline-block;
margin-right: $ix-marg-b;
}
.empty-graph-error--copy {
position: absolute;
top: $ix-marg-b;
right: $ix-marg-b;
opacity: 0.9;
display: none;
.empty-graph-error:hover & {
display: inherit;
}
}
}

View File

@ -0,0 +1,76 @@
// Libraries
import React, {useState, useRef, FunctionComponent} from 'react'
import CopyToClipboard from 'react-copy-to-clipboard'
// Components
import {
Button,
ComponentSize,
ComponentColor,
IconFont,
} from '@influxdata/clockface'
import BoxTooltip from 'src/shared/components/BoxTooltip'
interface Props {
message: string
testID?: string
}
const EmptyGraphError: FunctionComponent<Props> = ({message, testID}) => {
const [didCopy, setDidCopy] = useState(false)
const buttonText = didCopy ? 'Copied!' : 'Copy'
const buttonColor = didCopy ? ComponentColor.Success : ComponentColor.Default
const onClick = () => {
setDidCopy(true)
setTimeout(() => setDidCopy(false), 2000)
}
const trigger = useRef<HTMLDivElement>(null)
const [tooltipVisible, setTooltipVisible] = useState(false)
let triggerRect: DOMRect = null
if (trigger.current) {
triggerRect = trigger.current.getBoundingClientRect() as DOMRect
}
return (
<div className="cell--view-empty error" data-testid={testID}>
<div
onMouseEnter={() => setTooltipVisible(true)}
onMouseLeave={() => setTooltipVisible(false)}
ref={trigger}
>
<span
className={`icon ${IconFont.AlertTriangle} empty-graph-error--icon`}
/>
{tooltipVisible && (
<BoxTooltip
triggerRect={triggerRect as DOMRect}
color={ComponentColor.Danger}
>
<div className="box-tooltip--contents">
<pre>
<CopyToClipboard text={message}>
<Button
size={ComponentSize.ExtraSmall}
color={buttonColor}
titleText={buttonText}
text={buttonText}
onClick={onClick}
className="empty-graph-error--copy"
/>
</CopyToClipboard>
<code>{message}</code>
</pre>
</div>
</BoxTooltip>
)}
</div>
</div>
)
}
export default EmptyGraphError

View File

@ -3,6 +3,7 @@ import React, {PureComponent} from 'react'
// Components
import EmptyGraphMessage from 'src/shared/components/EmptyGraphMessage'
import EmptyGraphErrorTooltip from 'src/shared/components/EmptyGraphErrorTooltip'
import EmptyGraphError from 'src/shared/components/EmptyGraphError'
import Markdown from 'src/shared/components/views/Markdown'
@ -13,8 +14,14 @@ import {emptyGraphCopy} from 'src/shared/copy/cell'
import {RemoteDataState} from 'src/types'
import {DashboardQuery} from 'src/types'
export enum ErrorFormat {
Tooltip = 'tooltip',
Scroll = 'scroll',
}
interface Props {
errorMessage: string
errorFormat: ErrorFormat
isInitialFetch: boolean
loading: RemoteDataState
hasResults: boolean
@ -31,6 +38,7 @@ export default class EmptyQueryView extends PureComponent<Props> {
queries,
fallbackNote,
hasResults,
errorFormat,
} = this.props
if (loading === RemoteDataState.NotStarted || !queries.length) {
@ -43,9 +51,18 @@ export default class EmptyQueryView extends PureComponent<Props> {
}
if (errorMessage) {
return (
<EmptyGraphError message={errorMessage} testID="empty-graph--error" />
)
if (errorFormat === ErrorFormat.Tooltip)
return (
<EmptyGraphErrorTooltip
message={errorMessage}
testID="empty-graph--error"
/>
)
if (errorFormat === ErrorFormat.Scroll)
return (
<EmptyGraphError message={errorMessage} testID="empty-graph--error" />
)
}
if (

View File

@ -4,7 +4,7 @@ import {connect} from 'react-redux'
// Components
import TimeSeries from 'src/shared/components/TimeSeries'
import EmptyQueryView from 'src/shared/components/EmptyQueryView'
import EmptyQueryView, {ErrorFormat} from 'src/shared/components/EmptyQueryView'
import ViewSwitcher from 'src/shared/components/ViewSwitcher'
// Utils
@ -73,6 +73,7 @@ class RefreshingView extends PureComponent<Props, State> {
{({giraffeResult, files, loading, errorMessage, isInitialFetch}) => {
return (
<EmptyQueryView
errorFormat={ErrorFormat.Tooltip}
errorMessage={errorMessage}
hasResults={checkResultsLength(giraffeResult)}
loading={loading}

View File

@ -35,6 +35,11 @@ $cell--header-size: 36px;
}
}
.empty-graph-error--copy {
z-index: 1;
float: right;
}
.cell--view-empty {
width: 100%;
height: 100%;
@ -46,22 +51,10 @@ $cell--header-size: 36px;
@extend %no-user-select;
padding: 0 $ix-marg-d $ix-marg-d $ix-marg-d;
overflow: hidden;
.empty-graph-error {
position: absolute;
top: $ix-marg-c;
right: $ix-marg-c;
bottom: $ix-marg-c;
left: $ix-marg-c;
}
.empty-graph-error--scroll {
z-index: 0;
position: absolute;
}
.empty-graph-error--copy {
z-index: 1;
&.error {
font-size: 40px;
color: $c-dreamsicle;
}
}
@ -108,7 +101,7 @@ $cell--header-size: 36px;
transform: translateY(-50%);
width: 100%;
pointer-events: none;
.cell--header-note & {
padding-left: $cell--header-size;
}

View File

@ -49,4 +49,8 @@
> .single-stat {
border-radius: 0 0 $radius $radius;
}
> .cell--view-empty {
padding-top: 32px;
}
}

View File

@ -5,7 +5,7 @@ import {FromFluxResult} from '@influxdata/giraffe'
import {AutoSizer} from 'react-virtualized'
// Components
import EmptyQueryView from 'src/shared/components/EmptyQueryView'
import EmptyQueryView, {ErrorFormat} from 'src/shared/components/EmptyQueryView'
import ViewSwitcher from 'src/shared/components/ViewSwitcher'
import RawFluxDataTable from 'src/timeMachine/components/RawFluxDataTable'
@ -68,11 +68,12 @@ const TimeMachineVis: SFC<Props> = ({
return (
<div className="time-machine--view">
<EmptyQueryView
errorMessage={errorMessage}
hasResults={checkResultsLength(giraffeResult)}
loading={loading}
errorFormat={ErrorFormat.Scroll}
errorMessage={errorMessage}
isInitialFetch={isInitialFetch}
queries={viewProperties.queries}
hasResults={checkResultsLength(giraffeResult)}
>
{isViewingRawData ? (
<AutoSizer>

View File

@ -19,7 +19,7 @@
align-items: center;
padding-left: 10px;
}
dt {
background-color: $g6-smoke;
font-weight: 600;
@ -46,17 +46,6 @@
padding: 15px;
}
.flux-functions-toolbar--tooltip-contents {
width: 100%;
max-width: 600px;
display: inline-flex;
align-items: center;
article {
margin-bottom: $ix-marg-c;
}
}
.flux-functions-toolbar--heading {
font-weight: 600;
margin-bottom: $ix-marg-a;

View File

@ -21,7 +21,7 @@ const FunctionTooltipContents: FunctionComponent<Props> = ({
func: {desc, args, example, link},
}) => {
return (
<div className="flux-functions-toolbar--tooltip-contents">
<div className="box-tooltip--contents">
<FancyScrollbar autoHeight={true} maxHeight={MAX_HEIGHT} autoHide={false}>
<TooltipDescription description={desc} />
<TooltipArguments argsList={args} />

View File

@ -59,7 +59,7 @@ const VariableTooltipContents: FunctionComponent<Props> = ({
}
return (
<div className="variable-tooltip--contents" onMouseEnter={handleMouseEnter}>
<div className="box-tooltip--contents" onMouseEnter={handleMouseEnter}>
<Form.Element label="Value">
<Dropdown
selectedID={get(values, 'selectedValue')}