Merge branch 'master' into tempvars/select-default
commit
54aa7c694b
|
@ -16,6 +16,7 @@ module.exports = {
|
||||||
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
|
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
|
||||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||||
transformIgnorePatterns: ['/node_modules/(?!dygraphs)'],
|
transformIgnorePatterns: ['/node_modules/(?!dygraphs)'],
|
||||||
|
snapshotSerializers: ['enzyme-to-json/serializer'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
runner: 'jest-runner-eslint',
|
runner: 'jest-runner-eslint',
|
||||||
|
|
|
@ -37,6 +37,7 @@
|
||||||
"@types/chai": "^4.1.2",
|
"@types/chai": "^4.1.2",
|
||||||
"@types/chroma-js": "^1.3.4",
|
"@types/chroma-js": "^1.3.4",
|
||||||
"@types/codemirror": "^0.0.56",
|
"@types/codemirror": "^0.0.56",
|
||||||
|
"@types/d3-scale": "^2.0.1",
|
||||||
"@types/dygraphs": "^1.1.6",
|
"@types/dygraphs": "^1.1.6",
|
||||||
"@types/enzyme": "^3.1.9",
|
"@types/enzyme": "^3.1.9",
|
||||||
"@types/jest": "^22.1.4",
|
"@types/jest": "^22.1.4",
|
||||||
|
@ -72,6 +73,7 @@
|
||||||
"css-loader": "^0.28.11",
|
"css-loader": "^0.28.11",
|
||||||
"envify": "^3.4.0",
|
"envify": "^3.4.0",
|
||||||
"enzyme": "^3.3.0",
|
"enzyme": "^3.3.0",
|
||||||
|
"enzyme-to-json": "^3.3.4",
|
||||||
"eslint": "^3.14.1",
|
"eslint": "^3.14.1",
|
||||||
"eslint-config-prettier": "^2.9.0",
|
"eslint-config-prettier": "^2.9.0",
|
||||||
"eslint-loader": "^2.0.0",
|
"eslint-loader": "^2.0.0",
|
||||||
|
@ -127,6 +129,7 @@
|
||||||
"chroma-js": "^1.3.6",
|
"chroma-js": "^1.3.6",
|
||||||
"classnames": "^2.2.3",
|
"classnames": "^2.2.3",
|
||||||
"codemirror": "^5.36.0",
|
"codemirror": "^5.36.0",
|
||||||
|
"d3-scale": "^2.1.0",
|
||||||
"dygraphs": "2.1.0",
|
"dygraphs": "2.1.0",
|
||||||
"enzyme-adapter-react-16": "^1.1.1",
|
"enzyme-adapter-react-16": "^1.1.1",
|
||||||
"eslint-plugin-babel": "^4.1.2",
|
"eslint-plugin-babel": "^4.1.2",
|
||||||
|
|
|
@ -2,7 +2,6 @@ import React, {SFC, ReactChildren} from 'react'
|
||||||
|
|
||||||
import SideNav from 'src/side_nav'
|
import SideNav from 'src/side_nav'
|
||||||
import Notifications from 'src/shared/components/Notifications'
|
import Notifications from 'src/shared/components/Notifications'
|
||||||
import Overlay from 'src/shared/components/OverlayTechnology'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: ReactChildren
|
children: ReactChildren
|
||||||
|
@ -10,7 +9,6 @@ interface Props {
|
||||||
|
|
||||||
const App: SFC<Props> = ({children}) => (
|
const App: SFC<Props> = ({children}) => (
|
||||||
<div className="chronograf-root">
|
<div className="chronograf-root">
|
||||||
<Overlay />
|
|
||||||
<Notifications />
|
<Notifications />
|
||||||
<SideNav />
|
<SideNav />
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import React, {Component, MouseEvent} from 'react'
|
import React, {Component, MouseEvent} from 'react'
|
||||||
import {connect} from 'react-redux'
|
|
||||||
|
|
||||||
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
|
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
|
||||||
|
|
||||||
|
@ -8,11 +7,7 @@ import ImportDashboardOverlay from 'src/dashboards/components/ImportDashboardOve
|
||||||
import SearchBar from 'src/hosts/components/SearchBar'
|
import SearchBar from 'src/hosts/components/SearchBar'
|
||||||
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
|
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
|
||||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||||
import {
|
import OverlayTechnology from 'src/reusable_ui/components/overlays/OverlayTechnology'
|
||||||
showOverlay as showOverlayAction,
|
|
||||||
ShowOverlayActionCreator,
|
|
||||||
} from 'src/shared/actions/overlayTechnology'
|
|
||||||
import {OverlayContext} from 'src/shared/components/OverlayTechnology'
|
|
||||||
|
|
||||||
import {Dashboard} from 'src/types'
|
import {Dashboard} from 'src/types'
|
||||||
import {Notification} from 'src/types/notifications'
|
import {Notification} from 'src/types/notifications'
|
||||||
|
@ -27,12 +22,12 @@ interface Props {
|
||||||
onExportDashboard: (dashboard: Dashboard) => () => void
|
onExportDashboard: (dashboard: Dashboard) => () => void
|
||||||
onImportDashboard: (dashboard: Dashboard) => void
|
onImportDashboard: (dashboard: Dashboard) => void
|
||||||
notify: (message: Notification) => void
|
notify: (message: Notification) => void
|
||||||
showOverlay: ShowOverlayActionCreator
|
|
||||||
dashboardLink: string
|
dashboardLink: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
searchTerm: string
|
searchTerm: string
|
||||||
|
isOverlayVisible: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@ErrorHandling
|
@ErrorHandling
|
||||||
|
@ -42,6 +37,7 @@ class DashboardsPageContents extends Component<Props, State> {
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
|
isOverlayVisible: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,31 +79,34 @@ class DashboardsPageContents extends Component<Props, State> {
|
||||||
const {onCreateDashboard} = this.props
|
const {onCreateDashboard} = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="panel-heading">
|
<>
|
||||||
<h2 className="panel-title">{this.panelTitle}</h2>
|
<div className="panel-heading">
|
||||||
<div className="panel-controls">
|
<h2 className="panel-title">{this.panelTitle}</h2>
|
||||||
<SearchBar
|
<div className="panel-controls">
|
||||||
placeholder="Filter by Name..."
|
<SearchBar
|
||||||
onSearch={this.filterDashboards}
|
placeholder="Filter by Name..."
|
||||||
/>
|
onSearch={this.filterDashboards}
|
||||||
<Authorized requiredRole={EDITOR_ROLE}>
|
/>
|
||||||
<>
|
<Authorized requiredRole={EDITOR_ROLE}>
|
||||||
<button
|
<>
|
||||||
className="btn btn-sm btn-default"
|
<button
|
||||||
onClick={this.showImportOverlay}
|
className="btn btn-sm btn-default"
|
||||||
>
|
onClick={this.handleToggleOverlay}
|
||||||
<span className="icon import" /> Import Dashboard
|
>
|
||||||
</button>
|
<span className="icon import" /> Import Dashboard
|
||||||
<button
|
</button>
|
||||||
className="btn btn-sm btn-primary"
|
<button
|
||||||
onClick={onCreateDashboard}
|
className="btn btn-sm btn-primary"
|
||||||
>
|
onClick={onCreateDashboard}
|
||||||
<span className="icon plus" /> Create Dashboard
|
>
|
||||||
</button>
|
<span className="icon plus" /> Create Dashboard
|
||||||
</>
|
</button>
|
||||||
</Authorized>
|
</>
|
||||||
|
</Authorized>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{this.renderImportOverlay}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,30 +135,24 @@ class DashboardsPageContents extends Component<Props, State> {
|
||||||
this.setState({searchTerm})
|
this.setState({searchTerm})
|
||||||
}
|
}
|
||||||
|
|
||||||
private showImportOverlay = (): void => {
|
private handleToggleOverlay = (): void => {
|
||||||
const {showOverlay, onImportDashboard, notify} = this.props
|
this.setState({isOverlayVisible: !this.state.isOverlayVisible})
|
||||||
const options = {
|
}
|
||||||
dismissOnClickOutside: false,
|
|
||||||
dismissOnEscape: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
showOverlay(
|
private get renderImportOverlay(): JSX.Element {
|
||||||
<OverlayContext.Consumer>
|
const {onImportDashboard, notify} = this.props
|
||||||
{({onDismissOverlay}) => (
|
const {isOverlayVisible} = this.state
|
||||||
<ImportDashboardOverlay
|
|
||||||
onDismissOverlay={onDismissOverlay}
|
return (
|
||||||
onImportDashboard={onImportDashboard}
|
<OverlayTechnology visible={isOverlayVisible}>
|
||||||
notify={notify}
|
<ImportDashboardOverlay
|
||||||
/>
|
onDismissOverlay={this.handleToggleOverlay}
|
||||||
)}
|
onImportDashboard={onImportDashboard}
|
||||||
</OverlayContext.Consumer>,
|
notify={notify}
|
||||||
options
|
/>
|
||||||
|
</OverlayTechnology>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
export default DashboardsPageContents
|
||||||
showOverlay: showOverlayAction,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(null, mapDispatchToProps)(DashboardsPageContents)
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import React, {PureComponent} from 'react'
|
import React, {PureComponent} from 'react'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
|
||||||
import Container from 'src/shared/components/overlay/OverlayContainer'
|
import Container from 'src/reusable_ui/components/overlays/OverlayContainer'
|
||||||
import Heading from 'src/shared/components/overlay/OverlayHeading'
|
import Heading from 'src/reusable_ui/components/overlays/OverlayHeading'
|
||||||
import Body from 'src/shared/components/overlay/OverlayBody'
|
import Body from 'src/reusable_ui/components/overlays/OverlayBody'
|
||||||
import DragAndDrop from 'src/shared/components/DragAndDrop'
|
import DragAndDrop from 'src/shared/components/DragAndDrop'
|
||||||
import {notifyDashboardImportFailed} from 'src/shared/copy/notifications'
|
import {notifyDashboardImportFailed} from 'src/shared/copy/notifications'
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ import QueryMaker from 'src/data_explorer/components/QueryMaker'
|
||||||
import Visualization from 'src/data_explorer/components/Visualization'
|
import Visualization from 'src/data_explorer/components/Visualization'
|
||||||
import WriteDataForm from 'src/data_explorer/components/WriteDataForm'
|
import WriteDataForm from 'src/data_explorer/components/WriteDataForm'
|
||||||
import ResizeContainer from 'src/shared/components/ResizeContainer'
|
import ResizeContainer from 'src/shared/components/ResizeContainer'
|
||||||
import OverlayTechnologies from 'src/shared/components/OverlayTechnologies'
|
import OverlayTechnology from 'src/reusable_ui/components/overlays/OverlayTechnology'
|
||||||
import ManualRefresh from 'src/shared/components/ManualRefresh'
|
import ManualRefresh from 'src/shared/components/ManualRefresh'
|
||||||
import AutoRefreshDropdown from 'src/shared/components/AutoRefreshDropdown'
|
import AutoRefreshDropdown from 'src/shared/components/AutoRefreshDropdown'
|
||||||
import TimeRangeDropdown from 'src/shared/components/TimeRangeDropdown'
|
import TimeRangeDropdown from 'src/shared/components/TimeRangeDropdown'
|
||||||
|
@ -49,7 +49,7 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
showWriteForm: boolean
|
isWriteFormVisible: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@ErrorHandling
|
@ErrorHandling
|
||||||
|
@ -58,7 +58,7 @@ export class DataExplorer extends PureComponent<Props, State> {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
showWriteForm: false,
|
isWriteFormVisible: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,21 +101,19 @@ export class DataExplorer extends PureComponent<Props, State> {
|
||||||
queryConfigActions,
|
queryConfigActions,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
const {showWriteForm} = this.state
|
const {isWriteFormVisible} = this.state
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showWriteForm ? (
|
<OverlayTechnology visible={isWriteFormVisible}>
|
||||||
<OverlayTechnologies>
|
<WriteDataForm
|
||||||
<WriteDataForm
|
source={source}
|
||||||
source={source}
|
errorThrown={errorThrownAction}
|
||||||
errorThrown={errorThrownAction}
|
selectedDatabase={this.selectedDatabase}
|
||||||
selectedDatabase={this.selectedDatabase}
|
onClose={this.handleCloseWriteData}
|
||||||
onClose={this.handleCloseWriteData}
|
writeLineProtocol={writeLineProtocol}
|
||||||
writeLineProtocol={writeLineProtocol}
|
/>
|
||||||
/>
|
</OverlayTechnology>
|
||||||
</OverlayTechnologies>
|
|
||||||
) : null}
|
|
||||||
<PageHeader
|
<PageHeader
|
||||||
titleText="Data Explorer"
|
titleText="Data Explorer"
|
||||||
fullWidth={true}
|
fullWidth={true}
|
||||||
|
@ -154,11 +152,11 @@ export class DataExplorer extends PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleCloseWriteData = (): void => {
|
private handleCloseWriteData = (): void => {
|
||||||
this.setState({showWriteForm: false})
|
this.setState({isWriteFormVisible: false})
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleOpenWriteData = (): void => {
|
private handleOpenWriteData = (): void => {
|
||||||
this.setState({showWriteForm: true})
|
this.setState({isWriteFormVisible: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleChooseTimeRange = (timeRange: TimeRange): void => {
|
private handleChooseTimeRange = (timeRange: TimeRange): void => {
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
import React, {SFC} from 'react'
|
||||||
|
|
||||||
|
import PageHeader from 'src/shared/components/PageHeader'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onShowOverlay: () => void
|
||||||
|
overlay: JSX.Element
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmptyFluxPage: SFC<Props> = ({onShowOverlay, overlay}) => (
|
||||||
|
<div className="page">
|
||||||
|
<PageHeader titleText="Flux Editor" fullWidth={true} />
|
||||||
|
<div className="page-contents">
|
||||||
|
<div className="flux-empty">
|
||||||
|
<p>You do not have a configured Flux source</p>
|
||||||
|
<button className="btn btn-primary btn-md" onClick={onShowOverlay}>
|
||||||
|
Connect to Flux
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{overlay}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default EmptyFluxPage
|
|
@ -1,60 +1,70 @@
|
||||||
import React, {PureComponent} from 'react'
|
import React, {PureComponent} from 'react'
|
||||||
import {connect} from 'react-redux'
|
|
||||||
|
|
||||||
import FluxOverlay from 'src/flux/components/FluxOverlay'
|
import FluxOverlay from 'src/flux/components/FluxOverlay'
|
||||||
import {OverlayContext} from 'src/shared/components/OverlayTechnology'
|
import OverlayTechnology from 'src/reusable_ui/components/overlays/OverlayTechnology'
|
||||||
import PageHeader from 'src/shared/components/PageHeader'
|
import PageHeader from 'src/shared/components/PageHeader'
|
||||||
import {
|
|
||||||
showOverlay,
|
|
||||||
ShowOverlayActionCreator,
|
|
||||||
} from 'src/shared/actions/overlayTechnology'
|
|
||||||
|
|
||||||
import {Service} from 'src/types'
|
import {Service} from 'src/types'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
showOverlay: ShowOverlayActionCreator
|
|
||||||
service: Service
|
service: Service
|
||||||
}
|
}
|
||||||
|
|
||||||
class FluxHeader extends PureComponent<Props> {
|
interface State {
|
||||||
|
isOverlayVisible: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
class FluxHeader extends PureComponent<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isOverlayVisible: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
return (
|
return (
|
||||||
<PageHeader
|
<>
|
||||||
titleText="Flux Editor"
|
<PageHeader
|
||||||
fullWidth={true}
|
titleText="Flux Editor"
|
||||||
optionsComponents={this.optionsComponents}
|
fullWidth={true}
|
||||||
/>
|
optionsComponents={this.optionsComponents}
|
||||||
|
/>
|
||||||
|
{this.overlay}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleToggleOverlay = (): void => {
|
||||||
|
this.setState({isOverlayVisible: !this.state.isOverlayVisible})
|
||||||
|
}
|
||||||
|
|
||||||
private get optionsComponents(): JSX.Element {
|
private get optionsComponents(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<button onClick={this.overlay} className="btn btn-sm btn-default">
|
<button
|
||||||
|
onClick={this.handleToggleOverlay}
|
||||||
|
className="btn btn-sm btn-default"
|
||||||
|
>
|
||||||
Edit Connection
|
Edit Connection
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private overlay = () => {
|
private get overlay(): JSX.Element {
|
||||||
const {service} = this.props
|
const {service} = this.props
|
||||||
|
const {isOverlayVisible} = this.state
|
||||||
|
|
||||||
this.props.showOverlay(
|
return (
|
||||||
<OverlayContext.Consumer>
|
<OverlayTechnology visible={isOverlayVisible}>
|
||||||
{({onDismissOverlay}) => (
|
<FluxOverlay
|
||||||
<FluxOverlay
|
mode="edit"
|
||||||
mode="edit"
|
service={service}
|
||||||
service={service}
|
onDismiss={this.handleToggleOverlay}
|
||||||
onDismiss={onDismissOverlay}
|
/>
|
||||||
/>
|
</OverlayTechnology>
|
||||||
)}
|
|
||||||
</OverlayContext.Consumer>,
|
|
||||||
{}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mdtp = {
|
export default FluxHeader
|
||||||
showOverlay,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(null, mdtp)(FluxHeader)
|
|
||||||
|
|
|
@ -3,8 +3,9 @@ import {connect} from 'react-redux'
|
||||||
import {WithRouterProps} from 'react-router'
|
import {WithRouterProps} from 'react-router'
|
||||||
|
|
||||||
import {FluxPage} from 'src/flux'
|
import {FluxPage} from 'src/flux'
|
||||||
|
import EmptyFluxPage from 'src/flux/components/EmptyFluxPage'
|
||||||
import FluxOverlay from 'src/flux/components/FluxOverlay'
|
import FluxOverlay from 'src/flux/components/FluxOverlay'
|
||||||
import {OverlayContext} from 'src/shared/components/OverlayTechnology'
|
import OverlayTechnology from 'src/reusable_ui/components/overlays/OverlayTechnology'
|
||||||
import {Source, Service, Notification} from 'src/types'
|
import {Source, Service, Notification} from 'src/types'
|
||||||
import {Links} from 'src/types/flux'
|
import {Links} from 'src/types/flux'
|
||||||
import {notify as notifyAction} from 'src/shared/actions/notifications'
|
import {notify as notifyAction} from 'src/shared/actions/notifications'
|
||||||
|
@ -12,26 +13,37 @@ import {
|
||||||
updateScript as updateScriptAction,
|
updateScript as updateScriptAction,
|
||||||
UpdateScript,
|
UpdateScript,
|
||||||
} from 'src/flux/actions'
|
} from 'src/flux/actions'
|
||||||
import * as a from 'src/shared/actions/overlayTechnology'
|
import * as actions from 'src/shared/actions/services'
|
||||||
import * as b from 'src/shared/actions/services'
|
|
||||||
|
|
||||||
export const NotificationContext = React.createContext()
|
export const NotificationContext = React.createContext()
|
||||||
|
|
||||||
const actions = {...a, ...b}
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
sources: Source[]
|
sources: Source[]
|
||||||
services: Service[]
|
services: Service[]
|
||||||
children: ReactChildren
|
children: ReactChildren
|
||||||
showOverlay: a.ShowOverlayActionCreator
|
fetchServicesAsync: actions.FetchServicesAsync
|
||||||
fetchServicesAsync: b.FetchServicesAsync
|
|
||||||
notify: (message: Notification) => void
|
notify: (message: Notification) => void
|
||||||
updateScript: UpdateScript
|
updateScript: UpdateScript
|
||||||
script: string
|
script: string
|
||||||
links: Links
|
links: Links
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CheckServices extends PureComponent<Props & WithRouterProps> {
|
interface State {
|
||||||
|
isOverlayShown: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CheckServices extends PureComponent<
|
||||||
|
Props & WithRouterProps,
|
||||||
|
State
|
||||||
|
> {
|
||||||
|
constructor(props: Props & WithRouterProps) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isOverlayShown: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async componentDidMount() {
|
public async componentDidMount() {
|
||||||
const source = this.props.sources.find(
|
const source = this.props.sources.find(
|
||||||
s => s.id === this.props.params.sourceID
|
s => s.id === this.props.params.sourceID
|
||||||
|
@ -44,7 +56,7 @@ export class CheckServices extends PureComponent<Props & WithRouterProps> {
|
||||||
await this.props.fetchServicesAsync(source)
|
await this.props.fetchServicesAsync(source)
|
||||||
|
|
||||||
if (!this.props.services.length) {
|
if (!this.props.services.length) {
|
||||||
this.overlay()
|
this.setState({isOverlayShown: true})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +64,12 @@ export class CheckServices extends PureComponent<Props & WithRouterProps> {
|
||||||
const {services, notify, updateScript, links, script} = this.props
|
const {services, notify, updateScript, links, script} = this.props
|
||||||
|
|
||||||
if (!this.props.services.length) {
|
if (!this.props.services.length) {
|
||||||
return null // put loading spinner here
|
return (
|
||||||
|
<EmptyFluxPage
|
||||||
|
onShowOverlay={this.handleShowOverlay}
|
||||||
|
overlay={this.renderOverlay}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -65,6 +82,7 @@ export class CheckServices extends PureComponent<Props & WithRouterProps> {
|
||||||
notify={notify}
|
notify={notify}
|
||||||
updateScript={updateScript}
|
updateScript={updateScript}
|
||||||
/>
|
/>
|
||||||
|
{this.renderOverlay}
|
||||||
</NotificationContext.Provider>
|
</NotificationContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -75,31 +93,31 @@ export class CheckServices extends PureComponent<Props & WithRouterProps> {
|
||||||
return sources.find(s => s.id === params.sourceID)
|
return sources.find(s => s.id === params.sourceID)
|
||||||
}
|
}
|
||||||
|
|
||||||
private overlay() {
|
private get renderOverlay(): JSX.Element {
|
||||||
const {showOverlay, services} = this.props
|
const {isOverlayShown} = this.state
|
||||||
|
|
||||||
if (services.length) {
|
return (
|
||||||
return
|
<OverlayTechnology visible={isOverlayShown}>
|
||||||
}
|
<FluxOverlay
|
||||||
|
mode="new"
|
||||||
showOverlay(
|
source={this.source}
|
||||||
<OverlayContext.Consumer>
|
onDismiss={this.handleDismissOverlay}
|
||||||
{({onDismissOverlay}) => (
|
/>
|
||||||
<FluxOverlay
|
</OverlayTechnology>
|
||||||
mode="new"
|
|
||||||
source={this.source}
|
|
||||||
onDismiss={onDismissOverlay}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</OverlayContext.Consumer>,
|
|
||||||
{}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleShowOverlay = (): void => {
|
||||||
|
this.setState({isOverlayShown: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDismissOverlay = (): void => {
|
||||||
|
this.setState({isOverlayShown: false})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mdtp = {
|
const mdtp = {
|
||||||
fetchServicesAsync: actions.fetchServicesAsync,
|
fetchServicesAsync: actions.fetchServicesAsync,
|
||||||
showOverlay: actions.showOverlay,
|
|
||||||
updateScript: updateScriptAction,
|
updateScript: updateScriptAction,
|
||||||
notify: notifyAction,
|
notify: notifyAction,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,21 @@
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import {Source, Namespace, TimeRange, QueryConfig} from 'src/types'
|
import {
|
||||||
|
Source,
|
||||||
|
Namespace,
|
||||||
|
TimeRange,
|
||||||
|
QueryConfig,
|
||||||
|
RemoteDataState,
|
||||||
|
} from 'src/types'
|
||||||
import {getSource} from 'src/shared/apis'
|
import {getSource} from 'src/shared/apis'
|
||||||
import {getDatabasesWithRetentionPolicies} from 'src/shared/apis/databases'
|
import {getDatabasesWithRetentionPolicies} from 'src/shared/apis/databases'
|
||||||
import {
|
import {
|
||||||
buildHistogramQueryConfig,
|
buildHistogramQueryConfig,
|
||||||
buildTableQueryConfig,
|
buildTableQueryConfig,
|
||||||
buildLogQuery,
|
buildLogQuery,
|
||||||
|
parseHistogramQueryResponse,
|
||||||
} from 'src/logs/utils'
|
} from 'src/logs/utils'
|
||||||
import {getDeep} from 'src/utils/wrappers'
|
import {getDeep} from 'src/utils/wrappers'
|
||||||
import buildQuery from 'src/utils/influxql'
|
|
||||||
import {executeQueryAsync} from 'src/logs/api'
|
import {executeQueryAsync} from 'src/logs/api'
|
||||||
import {LogsState, Filter, TableData} from 'src/types/logs'
|
import {LogsState, Filter, TableData} from 'src/types/logs'
|
||||||
|
|
||||||
|
@ -41,6 +47,7 @@ export enum ActionTypes {
|
||||||
SetNamespace = 'LOGS_SET_NAMESPACE',
|
SetNamespace = 'LOGS_SET_NAMESPACE',
|
||||||
SetHistogramQueryConfig = 'LOGS_SET_HISTOGRAM_QUERY_CONFIG',
|
SetHistogramQueryConfig = 'LOGS_SET_HISTOGRAM_QUERY_CONFIG',
|
||||||
SetHistogramData = 'LOGS_SET_HISTOGRAM_DATA',
|
SetHistogramData = 'LOGS_SET_HISTOGRAM_DATA',
|
||||||
|
SetHistogramDataStatus = 'LOGS_SET_HISTOGRAM_DATA_STATUS',
|
||||||
SetTableQueryConfig = 'LOGS_SET_TABLE_QUERY_CONFIG',
|
SetTableQueryConfig = 'LOGS_SET_TABLE_QUERY_CONFIG',
|
||||||
SetTableData = 'LOGS_SET_TABLE_DATA',
|
SetTableData = 'LOGS_SET_TABLE_DATA',
|
||||||
ChangeZoom = 'LOGS_CHANGE_ZOOM',
|
ChangeZoom = 'LOGS_CHANGE_ZOOM',
|
||||||
|
@ -132,6 +139,11 @@ interface SetHistogramData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SetHistogramDataStatus {
|
||||||
|
type: ActionTypes.SetHistogramDataStatus
|
||||||
|
payload: RemoteDataState
|
||||||
|
}
|
||||||
|
|
||||||
interface SetTableQueryConfig {
|
interface SetTableQueryConfig {
|
||||||
type: ActionTypes.SetTableQueryConfig
|
type: ActionTypes.SetTableQueryConfig
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -156,7 +168,6 @@ interface SetSearchTerm {
|
||||||
interface ChangeZoomAction {
|
interface ChangeZoomAction {
|
||||||
type: ActionTypes.ChangeZoom
|
type: ActionTypes.ChangeZoom
|
||||||
payload: {
|
payload: {
|
||||||
data: object[]
|
|
||||||
timeRange: TimeRange
|
timeRange: TimeRange
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -168,6 +179,7 @@ export type Action =
|
||||||
| SetNamespaceAction
|
| SetNamespaceAction
|
||||||
| SetHistogramQueryConfig
|
| SetHistogramQueryConfig
|
||||||
| SetHistogramData
|
| SetHistogramData
|
||||||
|
| SetHistogramDataStatus
|
||||||
| ChangeZoomAction
|
| ChangeZoomAction
|
||||||
| SetTableData
|
| SetTableData
|
||||||
| SetTableQueryConfig
|
| SetTableQueryConfig
|
||||||
|
@ -220,9 +232,16 @@ export const removeFilter = (id: string): RemoveFilterAction => ({
|
||||||
payload: {id},
|
payload: {id},
|
||||||
})
|
})
|
||||||
|
|
||||||
const setHistogramData = (response): SetHistogramData => ({
|
const setHistogramData = (data): SetHistogramData => ({
|
||||||
type: ActionTypes.SetHistogramData,
|
type: ActionTypes.SetHistogramData,
|
||||||
payload: {data: [{response}]},
|
payload: {data},
|
||||||
|
})
|
||||||
|
|
||||||
|
const setHistogramDataStatus = (
|
||||||
|
status: RemoteDataState
|
||||||
|
): SetHistogramDataStatus => ({
|
||||||
|
type: ActionTypes.SetHistogramDataStatus,
|
||||||
|
payload: status,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const executeHistogramQueryAsync = () => async (
|
export const executeHistogramQueryAsync = () => async (
|
||||||
|
@ -240,9 +259,18 @@ export const executeHistogramQueryAsync = () => async (
|
||||||
|
|
||||||
if (_.every([queryConfig, timeRange, namespace, proxyLink])) {
|
if (_.every([queryConfig, timeRange, namespace, proxyLink])) {
|
||||||
const query = buildLogQuery(timeRange, queryConfig, filters, searchTerm)
|
const query = buildLogQuery(timeRange, queryConfig, filters, searchTerm)
|
||||||
const response = await executeQueryAsync(proxyLink, namespace, query)
|
|
||||||
|
|
||||||
dispatch(setHistogramData(response))
|
try {
|
||||||
|
dispatch(setHistogramDataStatus(RemoteDataState.Loading))
|
||||||
|
|
||||||
|
const response = await executeQueryAsync(proxyLink, namespace, query)
|
||||||
|
const data = parseHistogramQueryResponse(response)
|
||||||
|
|
||||||
|
dispatch(setHistogramData(data))
|
||||||
|
dispatch(setHistogramDataStatus(RemoteDataState.Done))
|
||||||
|
} catch {
|
||||||
|
dispatch(setHistogramDataStatus(RemoteDataState.Error))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -465,23 +493,10 @@ export const changeZoomAsync = (timeRange: TimeRange) => async (
|
||||||
getState: GetState
|
getState: GetState
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const state = getState()
|
const state = getState()
|
||||||
|
|
||||||
const namespace = getNamespace(state)
|
const namespace = getNamespace(state)
|
||||||
const proxyLink = getProxyLink(state)
|
const proxyLink = getProxyLink(state)
|
||||||
|
|
||||||
if (namespace && proxyLink) {
|
if (namespace && proxyLink) {
|
||||||
const queryConfig = buildHistogramQueryConfig(namespace, timeRange)
|
|
||||||
const query = buildQuery(timeRange, queryConfig)
|
|
||||||
const response = await executeQueryAsync(proxyLink, namespace, query)
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: ActionTypes.ChangeZoom,
|
|
||||||
payload: {
|
|
||||||
data: [{response}],
|
|
||||||
timeRange,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
await dispatch(setTimeRangeAsync(timeRange))
|
await dispatch(setTimeRangeAsync(timeRange))
|
||||||
await dispatch(executeTableQueryAsync())
|
await dispatch(executeTableQueryAsync())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
import React, {PureComponent} from 'react'
|
|
||||||
import LineGraph from 'src/shared/components/LineGraph'
|
|
||||||
import {DEFAULT_LINE_COLORS} from 'src/shared/constants/graphColorPalettes'
|
|
||||||
|
|
||||||
import {TimeRange} from 'src/types'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onZoom: (timeRange: TimeRange) => void
|
|
||||||
timeRange: TimeRange
|
|
||||||
data: object[]
|
|
||||||
}
|
|
||||||
|
|
||||||
class LogViewerChart extends PureComponent<Props> {
|
|
||||||
public render() {
|
|
||||||
const {timeRange, data, onZoom} = this.props
|
|
||||||
return (
|
|
||||||
<LineGraph
|
|
||||||
onZoom={onZoom}
|
|
||||||
queries={[]}
|
|
||||||
data={data}
|
|
||||||
displayOptions={{animatedZooms: false}}
|
|
||||||
setResolution={this.setResolution}
|
|
||||||
isBarGraph={true}
|
|
||||||
timeRange={timeRange}
|
|
||||||
colors={DEFAULT_LINE_COLORS}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private setResolution = () => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LogViewerChart
|
|
|
@ -1,6 +1,9 @@
|
||||||
import React, {PureComponent} from 'react'
|
import React, {PureComponent} from 'react'
|
||||||
import uuid from 'uuid'
|
import uuid from 'uuid'
|
||||||
|
import _ from 'lodash'
|
||||||
import {connect} from 'react-redux'
|
import {connect} from 'react-redux'
|
||||||
|
import {AutoSizer} from 'react-virtualized'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getSourceAndPopulateNamespacesAsync,
|
getSourceAndPopulateNamespacesAsync,
|
||||||
setTimeRangeAsync,
|
setTimeRangeAsync,
|
||||||
|
@ -15,15 +18,16 @@ import {
|
||||||
} from 'src/logs/actions'
|
} from 'src/logs/actions'
|
||||||
import {getSourcesAsync} from 'src/shared/actions/sources'
|
import {getSourcesAsync} from 'src/shared/actions/sources'
|
||||||
import LogViewerHeader from 'src/logs/components/LogViewerHeader'
|
import LogViewerHeader from 'src/logs/components/LogViewerHeader'
|
||||||
import Graph from 'src/logs/components/LogsGraph'
|
import HistogramChart from 'src/shared/components/HistogramChart'
|
||||||
|
import LogsGraphContainer from 'src/logs/components/LogsGraphContainer'
|
||||||
import SearchBar from 'src/logs/components/LogsSearchBar'
|
import SearchBar from 'src/logs/components/LogsSearchBar'
|
||||||
import FilterBar from 'src/logs/components/LogsFilterBar'
|
import FilterBar from 'src/logs/components/LogsFilterBar'
|
||||||
import LogViewerChart from 'src/logs/components/LogViewerChart'
|
|
||||||
import LogsTable from 'src/logs/components/LogsTable'
|
import LogsTable from 'src/logs/components/LogsTable'
|
||||||
import {getDeep} from 'src/utils/wrappers'
|
import {getDeep} from 'src/utils/wrappers'
|
||||||
|
|
||||||
import {Source, Namespace, TimeRange} from 'src/types'
|
import {Source, Namespace, TimeRange, RemoteDataState} from 'src/types'
|
||||||
import {Filter} from 'src/types/logs'
|
import {Filter} from 'src/types/logs'
|
||||||
|
import {HistogramData, TimePeriod} from 'src/types/histogram'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
sources: Source[]
|
sources: Source[]
|
||||||
|
@ -42,7 +46,8 @@ interface Props {
|
||||||
removeFilter: (id: string) => void
|
removeFilter: (id: string) => void
|
||||||
changeFilter: (id: string, operator: string, value: string) => void
|
changeFilter: (id: string, operator: string, value: string) => void
|
||||||
timeRange: TimeRange
|
timeRange: TimeRange
|
||||||
histogramData: object[]
|
histogramData: HistogramData
|
||||||
|
histogramDataStatus: RemoteDataState
|
||||||
tableData: {
|
tableData: {
|
||||||
columns: string[]
|
columns: string[]
|
||||||
values: string[]
|
values: string[]
|
||||||
|
@ -97,7 +102,7 @@ class LogsPage extends PureComponent<Props, State> {
|
||||||
<div className="page">
|
<div className="page">
|
||||||
{this.header}
|
{this.header}
|
||||||
<div className="page-contents logs-viewer">
|
<div className="page-contents logs-viewer">
|
||||||
<Graph>{this.chart}</Graph>
|
<LogsGraphContainer>{this.chart}</LogsGraphContainer>
|
||||||
<SearchBar
|
<SearchBar
|
||||||
searchString={searchTerm}
|
searchString={searchTerm}
|
||||||
onSearch={this.handleSubmitSearch}
|
onSearch={this.handleSubmitSearch}
|
||||||
|
@ -170,24 +175,24 @@ class LogsPage extends PureComponent<Props, State> {
|
||||||
private get histogramTotal(): number {
|
private get histogramTotal(): number {
|
||||||
const {histogramData} = this.props
|
const {histogramData} = this.props
|
||||||
|
|
||||||
const values = getDeep<Array<[number, number]>>(
|
return _.sumBy(histogramData, 'value')
|
||||||
histogramData,
|
|
||||||
'0.response.results.0.series.0.values',
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
return values.reduce((acc, v) => acc + v[1], 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private get chart(): JSX.Element {
|
private get chart(): JSX.Element {
|
||||||
const {histogramData, timeRange} = this.props
|
const {histogramData, histogramDataStatus} = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LogViewerChart
|
<AutoSizer>
|
||||||
timeRange={timeRange}
|
{({width, height}) => (
|
||||||
data={histogramData}
|
<HistogramChart
|
||||||
onZoom={this.handleChartZoom}
|
data={histogramData}
|
||||||
/>
|
dataStatus={histogramDataStatus}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
onZoom={this.handleChartZoom}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AutoSizer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -262,11 +267,15 @@ class LogsPage extends PureComponent<Props, State> {
|
||||||
this.props.setNamespaceAsync(namespace)
|
this.props.setNamespaceAsync(namespace)
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleChartZoom = (timeRange: TimeRange) => {
|
private handleChartZoom = (t: TimePeriod) => {
|
||||||
if (timeRange.lower) {
|
const {start, end} = t
|
||||||
this.props.changeZoomAsync(timeRange)
|
const timeRange = {
|
||||||
this.setState({liveUpdating: true})
|
lower: new Date(start).toISOString(),
|
||||||
|
upper: new Date(end).toISOString(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.props.changeZoomAsync(timeRange)
|
||||||
|
this.setState({liveUpdating: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fetchNewDataset() {
|
private fetchNewDataset() {
|
||||||
|
@ -283,6 +292,7 @@ const mapStateToProps = ({
|
||||||
timeRange,
|
timeRange,
|
||||||
currentNamespace,
|
currentNamespace,
|
||||||
histogramData,
|
histogramData,
|
||||||
|
histogramDataStatus,
|
||||||
tableData,
|
tableData,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
filters,
|
filters,
|
||||||
|
@ -295,6 +305,7 @@ const mapStateToProps = ({
|
||||||
timeRange,
|
timeRange,
|
||||||
currentNamespace,
|
currentNamespace,
|
||||||
histogramData,
|
histogramData,
|
||||||
|
histogramDataStatus,
|
||||||
tableData,
|
tableData,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
filters,
|
filters,
|
||||||
|
|
|
@ -9,6 +9,8 @@ import {
|
||||||
IncrementQueryCountAction,
|
IncrementQueryCountAction,
|
||||||
ConcatMoreLogsAction,
|
ConcatMoreLogsAction,
|
||||||
} from 'src/logs/actions'
|
} from 'src/logs/actions'
|
||||||
|
|
||||||
|
import {RemoteDataState} from 'src/types'
|
||||||
import {LogsState} from 'src/types/logs'
|
import {LogsState} from 'src/types/logs'
|
||||||
|
|
||||||
const defaultState: LogsState = {
|
const defaultState: LogsState = {
|
||||||
|
@ -20,6 +22,7 @@ const defaultState: LogsState = {
|
||||||
tableQueryConfig: null,
|
tableQueryConfig: null,
|
||||||
tableData: {columns: [], values: []},
|
tableData: {columns: [], values: []},
|
||||||
histogramData: [],
|
histogramData: [],
|
||||||
|
histogramDataStatus: RemoteDataState.NotStarted,
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
filters: [],
|
filters: [],
|
||||||
queryCount: 0,
|
queryCount: 0,
|
||||||
|
@ -108,13 +111,14 @@ export default (state: LogsState = defaultState, action: Action) => {
|
||||||
return {...state, histogramQueryConfig: action.payload.queryConfig}
|
return {...state, histogramQueryConfig: action.payload.queryConfig}
|
||||||
case ActionTypes.SetHistogramData:
|
case ActionTypes.SetHistogramData:
|
||||||
return {...state, histogramData: action.payload.data}
|
return {...state, histogramData: action.payload.data}
|
||||||
|
case ActionTypes.SetHistogramDataStatus:
|
||||||
|
return {...state, histogramDataStatus: action.payload}
|
||||||
case ActionTypes.SetTableQueryConfig:
|
case ActionTypes.SetTableQueryConfig:
|
||||||
return {...state, tableQueryConfig: action.payload.queryConfig}
|
return {...state, tableQueryConfig: action.payload.queryConfig}
|
||||||
case ActionTypes.SetTableData:
|
case ActionTypes.SetTableData:
|
||||||
return {...state, tableData: action.payload.data}
|
return {...state, tableData: action.payload.data}
|
||||||
case ActionTypes.ChangeZoom:
|
case ActionTypes.ChangeZoom:
|
||||||
const {timeRange, data} = action.payload
|
return {...state, timeRange: action.payload.timeRange}
|
||||||
return {...state, timeRange, histogramData: data}
|
|
||||||
case ActionTypes.SetSearchTerm:
|
case ActionTypes.SetSearchTerm:
|
||||||
const {searchTerm} = action.payload
|
const {searchTerm} = action.payload
|
||||||
return {...state, searchTerm}
|
return {...state, searchTerm}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import uuid from 'uuid'
|
||||||
import {Filter} from 'src/types/logs'
|
import {Filter} from 'src/types/logs'
|
||||||
import {TimeRange, Namespace, QueryConfig} from 'src/types'
|
import {TimeRange, Namespace, QueryConfig} from 'src/types'
|
||||||
import {NULL_STRING} from 'src/shared/constants/queryFillOptions'
|
import {NULL_STRING} from 'src/shared/constants/queryFillOptions'
|
||||||
|
import {getDeep} from 'src/utils/wrappers'
|
||||||
import {
|
import {
|
||||||
quoteIfTimestamp,
|
quoteIfTimestamp,
|
||||||
buildSelect,
|
buildSelect,
|
||||||
|
@ -12,6 +13,8 @@ import {
|
||||||
buildFill,
|
buildFill,
|
||||||
} from 'src/utils/influxql'
|
} from 'src/utils/influxql'
|
||||||
|
|
||||||
|
import {HistogramData} from 'src/types/histogram'
|
||||||
|
|
||||||
const BIN_COUNT = 30
|
const BIN_COUNT = 30
|
||||||
|
|
||||||
const histogramFields = [
|
const histogramFields = [
|
||||||
|
@ -156,7 +159,7 @@ const computeSeconds = (range: TimeRange) => {
|
||||||
const createGroupBy = (range: TimeRange) => {
|
const createGroupBy = (range: TimeRange) => {
|
||||||
const seconds = computeSeconds(range)
|
const seconds = computeSeconds(range)
|
||||||
const time = `${Math.max(Math.floor(seconds / BIN_COUNT), 1)}s`
|
const time = `${Math.max(Math.floor(seconds / BIN_COUNT), 1)}s`
|
||||||
const tags = []
|
const tags = ['severity']
|
||||||
|
|
||||||
return {time, tags}
|
return {time, tags}
|
||||||
}
|
}
|
||||||
|
@ -197,3 +200,39 @@ export const buildTableQueryConfig = (
|
||||||
fill: null,
|
fill: null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const parseHistogramQueryResponse = (
|
||||||
|
response: object
|
||||||
|
): HistogramData => {
|
||||||
|
const series = getDeep<any[]>(response, 'results.0.series', [])
|
||||||
|
const data = series.reduce((acc, current) => {
|
||||||
|
const group = getDeep<string>(current, 'tags.severity', '')
|
||||||
|
|
||||||
|
if (!current.columns || !current.values) {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeColIndex = current.columns.findIndex(v => v === 'time')
|
||||||
|
const countColIndex = current.columns.findIndex(v => v === 'count')
|
||||||
|
|
||||||
|
if (timeColIndex < 0 || countColIndex < 0) {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
|
||||||
|
const vs = current.values.map(v => {
|
||||||
|
const time = v[timeColIndex]
|
||||||
|
const value = v[countColIndex]
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: `${group}-${value}-${time}`,
|
||||||
|
time,
|
||||||
|
value,
|
||||||
|
group,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return [...acc, ...vs]
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, {PureComponent, ReactChildren} from 'react'
|
import React, {PureComponent, ReactChildren} from 'react'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children?: ReactChildren
|
children?: ReactChildren | JSX.Element
|
||||||
title: string
|
title: string
|
||||||
onDismiss?: () => void
|
onDismiss?: () => void
|
||||||
}
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
import React, {Component} from 'react'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: JSX.Element
|
||||||
|
visible: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
showChildren: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
@ErrorHandling
|
||||||
|
class OverlayTechnology extends Component<Props, State> {
|
||||||
|
public static getDerivedStateFromProps(props) {
|
||||||
|
if (props.visible) {
|
||||||
|
return {showChildren: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private animationTimer: number
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
showChildren: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate(prevProps) {
|
||||||
|
if (prevProps.visible && !this.props.visible) {
|
||||||
|
clearTimeout(this.animationTimer)
|
||||||
|
this.animationTimer = window.setTimeout(this.hideChildren, 300)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return (
|
||||||
|
<div className={this.overlayClass}>
|
||||||
|
{this.childContainer}
|
||||||
|
<div className="overlay--mask" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private get childContainer(): JSX.Element {
|
||||||
|
const {children} = this.props
|
||||||
|
const {showChildren} = this.state
|
||||||
|
|
||||||
|
if (showChildren) {
|
||||||
|
return (
|
||||||
|
<div className="overlay--dialog" data-test="overlay-children">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="overlay--dialog" data-test="overlay-children" />
|
||||||
|
}
|
||||||
|
|
||||||
|
private get overlayClass(): string {
|
||||||
|
const {visible} = this.props
|
||||||
|
|
||||||
|
return classnames('overlay-tech', {show: visible})
|
||||||
|
}
|
||||||
|
|
||||||
|
private hideChildren = (): void => {
|
||||||
|
this.setState({showChildren: false})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OverlayTechnology
|
|
@ -1,34 +0,0 @@
|
||||||
import {ReactElement} from 'react'
|
|
||||||
|
|
||||||
type OverlayNodeType = ReactElement<any>
|
|
||||||
|
|
||||||
interface Options {
|
|
||||||
dismissOnClickOutside?: boolean
|
|
||||||
dismissOnEscape?: boolean
|
|
||||||
transitionTime?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ShowOverlayActionCreator = (
|
|
||||||
OverlayNode: OverlayNodeType,
|
|
||||||
options: Options
|
|
||||||
) => ShowOverlayAction
|
|
||||||
|
|
||||||
interface ShowOverlayAction {
|
|
||||||
type: 'SHOW_OVERLAY'
|
|
||||||
payload: {
|
|
||||||
OverlayNode
|
|
||||||
options
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const showOverlay = (
|
|
||||||
OverlayNode: OverlayNodeType,
|
|
||||||
options: Options
|
|
||||||
): ShowOverlayAction => ({
|
|
||||||
type: 'SHOW_OVERLAY',
|
|
||||||
payload: {OverlayNode, options},
|
|
||||||
})
|
|
||||||
|
|
||||||
export const dismissOverlay = () => ({
|
|
||||||
type: 'DISMISS_OVERLAY',
|
|
||||||
})
|
|
|
@ -1,6 +1,5 @@
|
||||||
import React, {PureComponent, ReactElement, DragEvent} from 'react'
|
import React, {PureComponent, ReactElement} from 'react'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
// import {notifyDashboardUploadFailed} from 'src/shared/copy/notifications'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
fileTypesToAccept?: string
|
fileTypesToAccept?: string
|
||||||
|
@ -14,7 +13,6 @@ interface State {
|
||||||
inputContent: string | null
|
inputContent: string | null
|
||||||
uploadContent: string
|
uploadContent: string
|
||||||
fileName: string
|
fileName: string
|
||||||
progress: string
|
|
||||||
dragClass: string
|
dragClass: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,53 +32,45 @@ class DragAndDrop extends PureComponent<Props, State> {
|
||||||
inputContent: null,
|
inputContent: null,
|
||||||
uploadContent: '',
|
uploadContent: '',
|
||||||
fileName: '',
|
fileName: '',
|
||||||
progress: '',
|
|
||||||
dragClass: 'drag-none',
|
dragClass: 'drag-none',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
window.addEventListener('dragover', this.handleWindowDragOver)
|
||||||
|
window.addEventListener('drop', this.handleFileDrop)
|
||||||
|
window.addEventListener('dragenter', this.handleDragEnter)
|
||||||
|
window.addEventListener('dragleave', this.handleDragLeave)
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
window.removeEventListener('dragover', this.handleWindowDragOver)
|
||||||
|
window.removeEventListener('drop', this.handleFileDrop)
|
||||||
|
window.removeEventListener('dragenter', this.handleDragEnter)
|
||||||
|
window.removeEventListener('dragleave', this.handleDragLeave)
|
||||||
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
return (
|
return (
|
||||||
<div className={this.containerClass}>
|
<div className={this.containerClass}>
|
||||||
{/* (Invisible, covers entire screen)
|
<div className={this.dragAreaClass} onClick={this.handleFileOpen}>
|
||||||
This div handles drag only*/}
|
{this.dragAreaHeader}
|
||||||
<div
|
<div className={this.infoClass} />
|
||||||
onDrop={this.handleFile(true)}
|
<input
|
||||||
onDragOver={this.handleDragOver}
|
type="file"
|
||||||
onDragEnter={this.handleDragEnter}
|
ref={r => (this.fileInput = r)}
|
||||||
onDragExit={this.handleDragLeave}
|
className="drag-and-drop--input"
|
||||||
onDragLeave={this.handleDragLeave}
|
accept={this.fileTypesToAccept}
|
||||||
className="drag-and-drop--dropzone"
|
onChange={this.handleFileClick}
|
||||||
/>
|
/>
|
||||||
{/* visible form, handles drag & click */}
|
{this.buttons}
|
||||||
{this.dragArea}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private get dragArea(): ReactElement<HTMLDivElement> {
|
private handleWindowDragOver = (event: DragEvent) => {
|
||||||
return (
|
event.preventDefault()
|
||||||
<div
|
|
||||||
className={this.dragAreaClass}
|
|
||||||
onClick={this.handleFileOpen}
|
|
||||||
onDrop={this.handleFile(true)}
|
|
||||||
onDragOver={this.handleDragOver}
|
|
||||||
onDragEnter={this.handleDragEnter}
|
|
||||||
onDragExit={this.handleDragLeave}
|
|
||||||
onDragLeave={this.handleDragLeave}
|
|
||||||
>
|
|
||||||
{this.dragAreaHeader}
|
|
||||||
<div className={this.infoClass} />
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
ref={r => (this.fileInput = r)}
|
|
||||||
className="drag-and-drop--input"
|
|
||||||
accept={this.fileTypesToAccept}
|
|
||||||
onChange={this.handleFile(false)}
|
|
||||||
/>
|
|
||||||
{this.buttons}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private get fileTypesToAccept(): string {
|
private get fileTypesToAccept(): string {
|
||||||
|
@ -155,17 +145,32 @@ class DragAndDrop extends PureComponent<Props, State> {
|
||||||
handleSubmit(uploadContent, fileName)
|
handleSubmit(uploadContent, fileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleFile = (drop: boolean) => (e: any): void => {
|
private handleFileClick = (e: any): void => {
|
||||||
let file
|
const file = e.currentTarget.files[0]
|
||||||
if (drop) {
|
|
||||||
file = e.dataTransfer.files[0]
|
if (!file) {
|
||||||
this.setState({
|
return
|
||||||
dragClass: 'drag-none',
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
file = e.currentTarget.files[0]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.readAsText(file)
|
||||||
|
reader.onload = loadEvent => {
|
||||||
|
this.setState({
|
||||||
|
uploadContent: loadEvent.target.result,
|
||||||
|
fileName: file.name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleFileDrop = (e: any): void => {
|
||||||
|
const file = e.dataTransfer.files[0]
|
||||||
|
this.setState({
|
||||||
|
dragClass: 'drag-none',
|
||||||
|
})
|
||||||
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -205,18 +210,13 @@ class DragAndDrop extends PureComponent<Props, State> {
|
||||||
this.fileInput.value = ''
|
this.fileInput.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleDragOver = (e: DragEvent<HTMLDivElement>) => {
|
private handleDragEnter = (e: DragEvent): void => {
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleDragEnter = (e: DragEvent<HTMLDivElement>): void => {
|
|
||||||
dragCounter += 1
|
dragCounter += 1
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.setState({dragClass: 'drag-over'})
|
this.setState({dragClass: 'drag-over'})
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleDragLeave = (e: DragEvent<HTMLDivElement>): void => {
|
private handleDragLeave = (e: DragEvent): void => {
|
||||||
dragCounter -= 1
|
dragCounter -= 1
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (dragCounter === 0) {
|
if (dragCounter === 0) {
|
||||||
|
|
|
@ -0,0 +1,245 @@
|
||||||
|
import React, {PureComponent, MouseEvent} from 'react'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import {scaleLinear, scaleTime, ScaleLinear, ScaleTime} from 'd3-scale'
|
||||||
|
|
||||||
|
import HistogramChartAxes from 'src/shared/components/HistogramChartAxes'
|
||||||
|
import HistogramChartBars from 'src/shared/components/HistogramChartBars'
|
||||||
|
import HistogramChartTooltip from 'src/shared/components/HistogramChartTooltip'
|
||||||
|
import HistogramChartSkeleton from 'src/shared/components/HistogramChartSkeleton'
|
||||||
|
import XBrush from 'src/shared/components/XBrush'
|
||||||
|
|
||||||
|
import extentBy from 'src/utils/extentBy'
|
||||||
|
import {getDeep} from 'src/utils/wrappers'
|
||||||
|
|
||||||
|
import {RemoteDataState} from 'src/types'
|
||||||
|
import {
|
||||||
|
TimePeriod,
|
||||||
|
HistogramData,
|
||||||
|
HistogramDatum,
|
||||||
|
Margins,
|
||||||
|
TooltipAnchor,
|
||||||
|
} from 'src/types/histogram'
|
||||||
|
|
||||||
|
const PADDING_TOP = 0.2
|
||||||
|
const TOOLTIP_HORIZONTAL_MARGIN = 5
|
||||||
|
const TOOLTIP_REFLECT_DIST = 100
|
||||||
|
|
||||||
|
// Rather than use these magical constants, we could also render a digit and
|
||||||
|
// capture its measured width with as state before rendering anything else.
|
||||||
|
// Doing so would be robust but overkill.
|
||||||
|
const DIGIT_WIDTH = 7
|
||||||
|
const PERIOD_DIGIT_WIDTH = 4
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: HistogramData
|
||||||
|
dataStatus: RemoteDataState
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
onZoom: (TimePeriod) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
hoverX: number
|
||||||
|
hoverY: number
|
||||||
|
hoverDatum?: HistogramDatum
|
||||||
|
hoverAnchor: TooltipAnchor
|
||||||
|
}
|
||||||
|
|
||||||
|
class HistogramChart extends PureComponent<Props, State> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {hoverX: -1, hoverY: -1, hoverAnchor: 'left'}
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const {width, height, data} = this.props
|
||||||
|
const {margins} = this
|
||||||
|
|
||||||
|
if (width === 0 || height === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.length) {
|
||||||
|
return (
|
||||||
|
<HistogramChartSkeleton
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
margins={margins}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const {hoverDatum, hoverX, hoverY, hoverAnchor} = this.state
|
||||||
|
const {
|
||||||
|
xScale,
|
||||||
|
yScale,
|
||||||
|
adjustedWidth,
|
||||||
|
adjustedHeight,
|
||||||
|
bodyTransform,
|
||||||
|
loadingClass,
|
||||||
|
} = this
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<svg
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
className={`histogram-chart ${loadingClass}`}
|
||||||
|
onMouseOver={this.handleMouseMove}
|
||||||
|
onMouseOut={this.handleMouseOut}
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="histogram-chart--bars-clip">
|
||||||
|
<rect x="0" y="0" width={adjustedWidth} height={adjustedHeight} />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<g className="histogram-chart--axes">
|
||||||
|
<HistogramChartAxes
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
margins={margins}
|
||||||
|
xScale={xScale}
|
||||||
|
yScale={yScale}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<g className="histogram-chart--brush" transform={bodyTransform}>
|
||||||
|
<XBrush
|
||||||
|
xScale={xScale}
|
||||||
|
width={adjustedWidth}
|
||||||
|
height={adjustedHeight}
|
||||||
|
onBrush={this.handleBrush}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
transform={bodyTransform}
|
||||||
|
className="histogram-chart--bars"
|
||||||
|
clipPath="url(#histogram-chart--bars-clip)"
|
||||||
|
>
|
||||||
|
<HistogramChartBars
|
||||||
|
width={adjustedWidth}
|
||||||
|
height={adjustedHeight}
|
||||||
|
data={data}
|
||||||
|
xScale={xScale}
|
||||||
|
yScale={yScale}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<HistogramChartTooltip
|
||||||
|
datum={hoverDatum}
|
||||||
|
x={hoverX}
|
||||||
|
y={hoverY}
|
||||||
|
anchor={hoverAnchor}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private get xScale(): ScaleTime<number, number> {
|
||||||
|
const {adjustedWidth} = this
|
||||||
|
const {data} = this.props
|
||||||
|
|
||||||
|
const [t0, t1] = extentBy(data, d => d.time)
|
||||||
|
|
||||||
|
return scaleTime()
|
||||||
|
.domain([new Date(t0.time), new Date(t1.time)])
|
||||||
|
.range([0, adjustedWidth])
|
||||||
|
}
|
||||||
|
|
||||||
|
private get yScale(): ScaleLinear<number, number> {
|
||||||
|
const {adjustedHeight, maxAggregateCount} = this
|
||||||
|
|
||||||
|
return scaleLinear()
|
||||||
|
.domain([0, maxAggregateCount + PADDING_TOP * maxAggregateCount])
|
||||||
|
.range([adjustedHeight, 0])
|
||||||
|
}
|
||||||
|
|
||||||
|
private get adjustedWidth(): number {
|
||||||
|
const {margins} = this
|
||||||
|
|
||||||
|
return this.props.width - margins.left - margins.right
|
||||||
|
}
|
||||||
|
|
||||||
|
private get adjustedHeight(): number {
|
||||||
|
const {margins} = this
|
||||||
|
|
||||||
|
return this.props.height - margins.top - margins.bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
private get bodyTransform(): string {
|
||||||
|
const {margins} = this
|
||||||
|
|
||||||
|
return `translate(${margins.left}, ${margins.top})`
|
||||||
|
}
|
||||||
|
|
||||||
|
private get margins(): Margins {
|
||||||
|
const {maxAggregateCount} = this
|
||||||
|
|
||||||
|
const domainTop = maxAggregateCount + PADDING_TOP * maxAggregateCount
|
||||||
|
const left = domainTop.toString().length * DIGIT_WIDTH + PERIOD_DIGIT_WIDTH
|
||||||
|
|
||||||
|
return {top: 5, right: 0, bottom: 20, left}
|
||||||
|
}
|
||||||
|
|
||||||
|
private get maxAggregateCount(): number {
|
||||||
|
const {data} = this.props
|
||||||
|
|
||||||
|
if (!data.length) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups = _.groupBy(data, 'time')
|
||||||
|
const counts = Object.values(groups).map(group =>
|
||||||
|
group.reduce((sum, current) => sum + current.value, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
return Math.max(...counts)
|
||||||
|
}
|
||||||
|
|
||||||
|
private get loadingClass(): string {
|
||||||
|
const {dataStatus} = this.props
|
||||||
|
|
||||||
|
return dataStatus === RemoteDataState.Loading ? 'loading' : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleBrush = (t: TimePeriod): void => {
|
||||||
|
this.props.onZoom(t)
|
||||||
|
this.setState({hoverDatum: null})
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleMouseMove = (e: MouseEvent<SVGElement>): void => {
|
||||||
|
const key = getDeep<string>(e, 'target.dataset.key', '')
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const {data} = this.props
|
||||||
|
const hoverDatum = data.find(d => d.key === key)
|
||||||
|
|
||||||
|
if (!hoverDatum) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const bar = e.target as SVGRectElement
|
||||||
|
const barRect = bar.getBoundingClientRect()
|
||||||
|
const barRectHeight = barRect.bottom - barRect.top
|
||||||
|
const hoverY = barRect.top + barRectHeight / 2
|
||||||
|
|
||||||
|
let hoverX = barRect.right + TOOLTIP_HORIZONTAL_MARGIN
|
||||||
|
let hoverAnchor: TooltipAnchor = 'left'
|
||||||
|
|
||||||
|
if (hoverX >= window.innerWidth - TOOLTIP_REFLECT_DIST) {
|
||||||
|
hoverX = window.innerWidth - barRect.left + TOOLTIP_HORIZONTAL_MARGIN
|
||||||
|
hoverAnchor = 'right'
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({hoverDatum, hoverX, hoverY, hoverAnchor})
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleMouseOut = (): void => {
|
||||||
|
this.setState({hoverDatum: null})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HistogramChart
|
|
@ -0,0 +1,92 @@
|
||||||
|
import React, {PureComponent} from 'react'
|
||||||
|
import {ScaleLinear, ScaleTime} from 'd3-scale'
|
||||||
|
|
||||||
|
import {Margins} from 'src/types/histogram'
|
||||||
|
|
||||||
|
const Y_TICK_COUNT = 5
|
||||||
|
const Y_TICK_PADDING_RIGHT = 5
|
||||||
|
const X_TICK_COUNT = 10
|
||||||
|
const X_TICK_PADDING_TOP = 8
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
margins: Margins
|
||||||
|
xScale: ScaleTime<number, number>
|
||||||
|
yScale: ScaleLinear<number, number>
|
||||||
|
}
|
||||||
|
|
||||||
|
class HistogramChartAxes extends PureComponent<Props> {
|
||||||
|
public render() {
|
||||||
|
const {xTickData, yTickData} = this
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{this.renderYTicks(yTickData)}
|
||||||
|
{this.renderYLabels(yTickData)}
|
||||||
|
{this.renderXLabels(xTickData)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderYTicks(yTickData) {
|
||||||
|
return yTickData.map(({x1, x2, y, key}) => (
|
||||||
|
<line className="y-tick" key={key} x1={x1} x2={x2} y1={y} y2={y} />
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderYLabels(yTickData) {
|
||||||
|
return yTickData.map(({x1, y, label, key}) => (
|
||||||
|
<text className="y-label" key={key} x={x1 - Y_TICK_PADDING_RIGHT} y={y}>
|
||||||
|
{label}
|
||||||
|
</text>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderXLabels(xTickData) {
|
||||||
|
return xTickData.map(({x, y, label, key}) => (
|
||||||
|
<text className="x-label" key={key} y={y} x={x}>
|
||||||
|
{label}
|
||||||
|
</text>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
private get xTickData() {
|
||||||
|
const {margins, xScale, width, height} = this.props
|
||||||
|
|
||||||
|
const y = height - margins.bottom + X_TICK_PADDING_TOP
|
||||||
|
const formatTime = xScale.tickFormat()
|
||||||
|
|
||||||
|
return xScale
|
||||||
|
.ticks(X_TICK_COUNT)
|
||||||
|
.filter(val => {
|
||||||
|
const x = xScale(val)
|
||||||
|
|
||||||
|
// Don't render labels that will be cut off
|
||||||
|
return x > margins.left && x < width - margins.right
|
||||||
|
})
|
||||||
|
.map(val => {
|
||||||
|
const x = xScale(val)
|
||||||
|
const label = formatTime(val)
|
||||||
|
const key = `${label}-${x}-${y}`
|
||||||
|
|
||||||
|
return {label, x, y, key}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private get yTickData() {
|
||||||
|
const {width, margins, yScale} = this.props
|
||||||
|
|
||||||
|
return yScale.ticks(Y_TICK_COUNT).map(val => {
|
||||||
|
const label = val
|
||||||
|
const x1 = margins.left
|
||||||
|
const x2 = margins.left + width
|
||||||
|
const y = margins.top + yScale(val)
|
||||||
|
const key = `${label}-${x1}-${x2}-${y}`
|
||||||
|
|
||||||
|
return {label, x1, x2, y, key}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HistogramChartAxes
|
|
@ -0,0 +1,131 @@
|
||||||
|
import React, {PureComponent} from 'react'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import {ScaleLinear, ScaleTime} from 'd3-scale'
|
||||||
|
|
||||||
|
import {HistogramData, HistogramDatum} from 'src/types/histogram'
|
||||||
|
|
||||||
|
const BAR_BORDER_RADIUS = 4
|
||||||
|
const BAR_PADDING_SIDES = 4
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
data: HistogramData
|
||||||
|
xScale: ScaleTime<number, number>
|
||||||
|
yScale: ScaleLinear<number, number>
|
||||||
|
}
|
||||||
|
|
||||||
|
class HistogramChartBars extends PureComponent<Props> {
|
||||||
|
public render() {
|
||||||
|
return this.renderData.map(group => {
|
||||||
|
const {key, clip, bars} = group
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g key={key} className="histogram-chart-bars--bars">
|
||||||
|
<defs>
|
||||||
|
<clipPath id={`histogram-chart-bars--clip-${key}`}>
|
||||||
|
<rect
|
||||||
|
x={clip.x}
|
||||||
|
y={clip.y}
|
||||||
|
width={clip.width}
|
||||||
|
height={clip.height}
|
||||||
|
rx={BAR_BORDER_RADIUS}
|
||||||
|
ry={BAR_BORDER_RADIUS}
|
||||||
|
/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
{bars.map(d => (
|
||||||
|
<rect
|
||||||
|
key={d.key}
|
||||||
|
className="histogram-chart-bars--bar"
|
||||||
|
x={d.x}
|
||||||
|
y={d.y}
|
||||||
|
width={d.width}
|
||||||
|
height={d.height}
|
||||||
|
clipPath={`url(#histogram-chart-bars--clip-${key})`}
|
||||||
|
data-group={d.group}
|
||||||
|
data-key={d.key}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private get renderData() {
|
||||||
|
const {data, xScale, yScale} = this.props
|
||||||
|
const {barWidth, sortFn} = this
|
||||||
|
|
||||||
|
const visibleData = data.filter(d => d.value !== 0)
|
||||||
|
const groups = Object.values(_.groupBy(visibleData, 'time'))
|
||||||
|
|
||||||
|
for (const group of groups) {
|
||||||
|
group.sort(sortFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups.map(group => {
|
||||||
|
const time = group[0].time
|
||||||
|
const x = xScale(time) - barWidth / 2
|
||||||
|
const groupTotal = _.sumBy(group, 'value')
|
||||||
|
|
||||||
|
const renderData = {
|
||||||
|
key: `${time}-${groupTotal}-${x}`,
|
||||||
|
clip: {
|
||||||
|
x,
|
||||||
|
y: yScale(groupTotal),
|
||||||
|
width: barWidth,
|
||||||
|
height: yScale(0) - yScale(groupTotal) + BAR_BORDER_RADIUS,
|
||||||
|
},
|
||||||
|
bars: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
let offset = 0
|
||||||
|
|
||||||
|
group.forEach((d: HistogramDatum) => {
|
||||||
|
const height = yScale(0) - yScale(d.value)
|
||||||
|
|
||||||
|
renderData.bars.push({
|
||||||
|
key: d.key,
|
||||||
|
group: d.group,
|
||||||
|
x,
|
||||||
|
y: yScale(d.value) - offset,
|
||||||
|
width: barWidth,
|
||||||
|
height,
|
||||||
|
})
|
||||||
|
|
||||||
|
offset += height
|
||||||
|
})
|
||||||
|
|
||||||
|
return renderData
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private get sortFn() {
|
||||||
|
const {data} = this.props
|
||||||
|
|
||||||
|
const counts = {}
|
||||||
|
|
||||||
|
for (const d of data) {
|
||||||
|
if (counts[d.group]) {
|
||||||
|
counts[d.group] += d.value
|
||||||
|
} else {
|
||||||
|
counts[d.group] = d.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (a, b) => counts[b.group] - counts[a.group]
|
||||||
|
}
|
||||||
|
|
||||||
|
private get barWidth() {
|
||||||
|
const {data, xScale, width} = this.props
|
||||||
|
|
||||||
|
const dataInView = data.filter(
|
||||||
|
d => xScale(d.time) >= 0 && xScale(d.time) <= width
|
||||||
|
)
|
||||||
|
const barCount = Object.values(_.groupBy(dataInView, 'time')).length
|
||||||
|
|
||||||
|
return Math.round(width / barCount - BAR_PADDING_SIDES)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HistogramChartBars
|
|
@ -0,0 +1,37 @@
|
||||||
|
import React, {SFC} from 'react'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
import {Margins} from 'src/types/histogram'
|
||||||
|
|
||||||
|
const NUM_TICKS = 5
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
margins: Margins
|
||||||
|
}
|
||||||
|
|
||||||
|
const HistogramChartSkeleton: SFC<Props> = props => {
|
||||||
|
const {margins, width, height} = props
|
||||||
|
|
||||||
|
const spacing = (height - margins.top - margins.bottom) / NUM_TICKS
|
||||||
|
const y1 = height - margins.bottom
|
||||||
|
const tickYs = _.range(0, NUM_TICKS).map(i => y1 - i * spacing)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg className="histogram-chart-skeleton" width={width} height={height}>
|
||||||
|
{tickYs.map((y, i) => (
|
||||||
|
<line
|
||||||
|
key={i}
|
||||||
|
className="y-tick"
|
||||||
|
x1={margins.left}
|
||||||
|
x2={width - margins.right}
|
||||||
|
y1={y}
|
||||||
|
y2={y}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HistogramChartSkeleton
|
|
@ -0,0 +1,42 @@
|
||||||
|
import React, {SFC, CSSProperties} from 'react'
|
||||||
|
|
||||||
|
import {HistogramDatum, TooltipAnchor} from 'src/types/histogram'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
datum: HistogramDatum
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
anchor?: TooltipAnchor
|
||||||
|
}
|
||||||
|
|
||||||
|
const HistogramChartTooltip: SFC<Props> = props => {
|
||||||
|
const {datum, x, y, anchor = 'left'} = props
|
||||||
|
|
||||||
|
if (!datum) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const style: CSSProperties = {
|
||||||
|
position: 'fixed',
|
||||||
|
top: y,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anchor === 'left') {
|
||||||
|
style.left = x
|
||||||
|
} else {
|
||||||
|
style.right = x
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="histogram-chart-tooltip"
|
||||||
|
style={style}
|
||||||
|
data-group={datum.group}
|
||||||
|
>
|
||||||
|
<div className="histogram-chart-tooltip--value">{datum.value}</div>
|
||||||
|
<div className="histogram-chart-tooltip--group">{datum.group}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HistogramChartTooltip
|
|
@ -1,104 +0,0 @@
|
||||||
import React, {PureComponent, Component} from 'react'
|
|
||||||
import {connect} from 'react-redux'
|
|
||||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
|
||||||
|
|
||||||
import {dismissOverlay} from 'src/shared/actions/overlayTechnology'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
OverlayNode?: Component<any>
|
|
||||||
dismissOnClickOutside?: boolean
|
|
||||||
dismissOnEscape?: boolean
|
|
||||||
transitionTime?: number
|
|
||||||
handleDismissOverlay: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
visible: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const OverlayContext = React.createContext()
|
|
||||||
|
|
||||||
@ErrorHandling
|
|
||||||
class Overlay extends PureComponent<Props, State> {
|
|
||||||
public static defaultProps: Partial<Props> = {
|
|
||||||
dismissOnClickOutside: false,
|
|
||||||
dismissOnEscape: false,
|
|
||||||
transitionTime: 300,
|
|
||||||
}
|
|
||||||
|
|
||||||
private animationTimer: number
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
visible: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentDidUpdate(prevProps) {
|
|
||||||
if (prevProps.OverlayNode === null && this.props.OverlayNode) {
|
|
||||||
return this.setState({visible: true})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
const {OverlayNode} = this.props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<OverlayContext.Provider
|
|
||||||
value={{
|
|
||||||
onDismissOverlay: this.handleAnimateDismiss,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className={this.overlayClass}>
|
|
||||||
<div className="overlay--dialog">{OverlayNode}</div>
|
|
||||||
<div className="overlay--mask" onClick={this.handleClickOutside} />
|
|
||||||
</div>
|
|
||||||
</OverlayContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private get overlayClass(): string {
|
|
||||||
const {visible} = this.state
|
|
||||||
return `overlay-tech ${visible ? 'show' : ''}`
|
|
||||||
}
|
|
||||||
|
|
||||||
public handleClickOutside = () => {
|
|
||||||
const {handleDismissOverlay, dismissOnClickOutside} = this.props
|
|
||||||
|
|
||||||
if (dismissOnClickOutside) {
|
|
||||||
handleDismissOverlay()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public handleAnimateDismiss = () => {
|
|
||||||
const {transitionTime} = this.props
|
|
||||||
this.setState({visible: false})
|
|
||||||
this.animationTimer = window.setTimeout(this.handleDismiss, transitionTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
public handleDismiss = () => {
|
|
||||||
const {handleDismissOverlay} = this.props
|
|
||||||
handleDismissOverlay()
|
|
||||||
clearTimeout(this.animationTimer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = ({
|
|
||||||
overlayTechnology: {
|
|
||||||
OverlayNode,
|
|
||||||
options: {dismissOnClickOutside, dismissOnEscape, transitionTime},
|
|
||||||
},
|
|
||||||
}) => ({
|
|
||||||
OverlayNode,
|
|
||||||
dismissOnClickOutside,
|
|
||||||
dismissOnEscape,
|
|
||||||
transitionTime,
|
|
||||||
})
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
handleDismissOverlay: dismissOverlay,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(Overlay)
|
|
|
@ -1,12 +0,0 @@
|
||||||
import React, {SFC} from 'react'
|
|
||||||
|
|
||||||
const SimpleOverlayTechnology: SFC = ({children}) => {
|
|
||||||
return (
|
|
||||||
<div className="overlay-tech show">
|
|
||||||
<div className="overlay--dialog">{children}</div>
|
|
||||||
<div className="overlay--mask" />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SimpleOverlayTechnology
|
|
|
@ -0,0 +1,163 @@
|
||||||
|
import React, {
|
||||||
|
PureComponent,
|
||||||
|
RefObject,
|
||||||
|
MouseEvent as ReactMouseEvent,
|
||||||
|
} from 'react'
|
||||||
|
import {ScaleTime} from 'd3-scale'
|
||||||
|
|
||||||
|
import {TimePeriod} from 'src/types/histogram'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
xScale: ScaleTime<number, number>
|
||||||
|
onBrush?: (t: TimePeriod) => void
|
||||||
|
onDoubleClick?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
dragging: boolean
|
||||||
|
dragStartPos: number
|
||||||
|
dragPos: number
|
||||||
|
}
|
||||||
|
|
||||||
|
class XBrush extends PureComponent<Props, State> {
|
||||||
|
private draggableArea: RefObject<SVGRectElement>
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
dragging: false,
|
||||||
|
dragStartPos: 0,
|
||||||
|
dragPos: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
this.draggableArea = React.createRef<SVGRectElement>()
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
// These are usually cleaned up on handleDragEnd; this ensures they will
|
||||||
|
// also be cleaned up if the component is destroyed mid-brush
|
||||||
|
document.removeEventListener('movemove', this.handleDrag)
|
||||||
|
document.removeEventListener('mouseup', this.handleDragEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const {width, height} = this.props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{this.renderSelection()}
|
||||||
|
<rect
|
||||||
|
ref={this.draggableArea}
|
||||||
|
className="x-brush--area"
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
onMouseDown={this.handleDragStart}
|
||||||
|
onDoubleClick={this.handleDoubleClick}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderSelection(): JSX.Element {
|
||||||
|
const {height} = this.props
|
||||||
|
const {dragging, dragStartPos, dragPos} = this.state
|
||||||
|
|
||||||
|
if (!dragging) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const x = Math.min(dragStartPos, dragPos)
|
||||||
|
const width = Math.abs(dragStartPos - dragPos)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<rect
|
||||||
|
className="x-brush--selection"
|
||||||
|
y={0}
|
||||||
|
height={height}
|
||||||
|
x={x}
|
||||||
|
width={width}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDragStart = (e: ReactMouseEvent<SVGRectElement>): void => {
|
||||||
|
// A user can mousedown (start a brush) then move outside of the current
|
||||||
|
// element while still holding the mouse down, therfore we must listen to
|
||||||
|
// mouse events everywhere, not just within this component.
|
||||||
|
document.addEventListener('mousemove', this.handleDrag)
|
||||||
|
document.addEventListener('mouseup', this.handleDragEnd)
|
||||||
|
|
||||||
|
const x = this.getX(e)
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
dragging: true,
|
||||||
|
dragStartPos: x,
|
||||||
|
dragPos: x,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDrag = (e: MouseEvent): void => {
|
||||||
|
const {dragging} = this.state
|
||||||
|
|
||||||
|
if (!dragging) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({dragPos: this.getX(e)})
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDragEnd = (): void => {
|
||||||
|
document.removeEventListener('movemove', this.handleDrag)
|
||||||
|
document.removeEventListener('mouseup', this.handleDragEnd)
|
||||||
|
|
||||||
|
const {xScale, onBrush} = this.props
|
||||||
|
const {dragging, dragPos, dragStartPos} = this.state
|
||||||
|
|
||||||
|
if (!dragging) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({dragging: false})
|
||||||
|
|
||||||
|
if (!onBrush || Math.round(dragPos) === Math.round(dragStartPos)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const startX = Math.min(dragStartPos, dragPos)
|
||||||
|
const endX = Math.max(dragStartPos, dragPos)
|
||||||
|
const start = xScale.invert(startX).getTime()
|
||||||
|
const end = xScale.invert(endX).getTime()
|
||||||
|
|
||||||
|
onBrush({start, end})
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDoubleClick = (): void => {
|
||||||
|
const {onDoubleClick} = this.props
|
||||||
|
|
||||||
|
if (onDoubleClick) {
|
||||||
|
onDoubleClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getX = (e: MouseEvent | ReactMouseEvent<SVGRectElement>): number => {
|
||||||
|
const {width} = this.props
|
||||||
|
|
||||||
|
const {left} = this.draggableArea.current.getBoundingClientRect()
|
||||||
|
const x = e.pageX - left
|
||||||
|
|
||||||
|
if (x < 0) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x > width) {
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default XBrush
|
|
@ -1,29 +0,0 @@
|
||||||
const initialState = {
|
|
||||||
options: {
|
|
||||||
dismissOnClickOutside: false,
|
|
||||||
dismissOnEscape: false,
|
|
||||||
transitionTime: 300,
|
|
||||||
},
|
|
||||||
OverlayNode: null,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function overlayTechnology(state = initialState, action) {
|
|
||||||
switch (action.type) {
|
|
||||||
case 'SHOW_OVERLAY': {
|
|
||||||
const {OverlayNode, options} = action.payload
|
|
||||||
|
|
||||||
return {...state, OverlayNode, options}
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'DISMISS_OVERLAY': {
|
|
||||||
const {options} = initialState
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
OverlayNode: null,
|
|
||||||
options,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return state
|
|
||||||
}
|
|
|
@ -14,7 +14,6 @@ import adminReducers from 'src/admin/reducers'
|
||||||
import kapacitorReducers from 'src/kapacitor/reducers'
|
import kapacitorReducers from 'src/kapacitor/reducers'
|
||||||
import dashboardUI from 'src/dashboards/reducers/ui'
|
import dashboardUI from 'src/dashboards/reducers/ui'
|
||||||
import cellEditorOverlay from 'src/dashboards/reducers/cellEditorOverlay'
|
import cellEditorOverlay from 'src/dashboards/reducers/cellEditorOverlay'
|
||||||
import overlayTechnology from 'src/shared/reducers/overlayTechnology'
|
|
||||||
import dashTimeV1 from 'src/dashboards/reducers/dashTimeV1'
|
import dashTimeV1 from 'src/dashboards/reducers/dashTimeV1'
|
||||||
import persistStateEnhancer from './persistStateEnhancer'
|
import persistStateEnhancer from './persistStateEnhancer'
|
||||||
import servicesReducer from 'src/shared/reducers/services'
|
import servicesReducer from 'src/shared/reducers/services'
|
||||||
|
@ -28,7 +27,6 @@ const rootReducer = combineReducers({
|
||||||
...adminReducers,
|
...adminReducers,
|
||||||
dashboardUI,
|
dashboardUI,
|
||||||
cellEditorOverlay,
|
cellEditorOverlay,
|
||||||
overlayTechnology,
|
|
||||||
dashTimeV1,
|
dashTimeV1,
|
||||||
logs: logsReducer,
|
logs: logsReducer,
|
||||||
routing: routerReducer,
|
routing: routerReducer,
|
||||||
|
|
|
@ -73,6 +73,7 @@
|
||||||
@import 'components/threshold-controls';
|
@import 'components/threshold-controls';
|
||||||
@import 'components/kapacitor-logs-table';
|
@import 'components/kapacitor-logs-table';
|
||||||
@import 'components/dropdown-placeholder';
|
@import 'components/dropdown-placeholder';
|
||||||
|
@import 'components/histogram-chart';
|
||||||
|
|
||||||
// Pages
|
// Pages
|
||||||
@import 'pages/config-endpoints';
|
@import 'pages/config-endpoints';
|
||||||
|
|
|
@ -3,16 +3,6 @@
|
||||||
------------------------------------------------------------------------------
|
------------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.drag-and-drop--dropzone {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
width: 1000%;
|
|
||||||
height: 1000%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
z-index: $drag-and-drop--z-dropzone;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag-and-drop--form {
|
.drag-and-drop--form {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: $drag-and-drop--z-form;
|
z-index: $drag-and-drop--z-form;
|
||||||
|
@ -69,7 +59,9 @@ input[type='file'].drag-and-drop--input {
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
margin-top: 18px;
|
margin-top: 18px;
|
||||||
|
|
||||||
> button.btn {margin: 0 4px;}
|
> button.btn {
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -81,4 +73,4 @@ input[type='file'].drag-and-drop--input {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: $g4-onyx;
|
background-color: $g4-onyx;
|
||||||
border-color: $g6-smoke;
|
border-color: $g6-smoke;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,85 +1,29 @@
|
||||||
$padding: 20px 30px;
|
/*
|
||||||
|
Create / Edit Template Variable Overlay
|
||||||
.edit-temp-var {
|
------------------------------------------------------------------------------
|
||||||
margin: 0 auto;
|
*/
|
||||||
width: 650px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-temp-var--header {
|
|
||||||
background-color: $g0-obsidian;
|
|
||||||
padding: $padding;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-temp-var--header > h1 {
|
|
||||||
color: #eeeff2;
|
|
||||||
letter-spacing: 0;
|
|
||||||
font-size: 19px;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-temp-var--header-controls > button {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-temp-var--body {
|
|
||||||
@include gradient-v($g3-castle, $g1-raven);
|
|
||||||
padding: $padding;
|
|
||||||
|
|
||||||
.delete {
|
|
||||||
margin: 20px auto 10px auto;
|
|
||||||
width: 90px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-temp-var--body-row {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
.name {
|
|
||||||
flex: 1 1 50%;
|
|
||||||
padding-right: 10px;
|
|
||||||
|
|
||||||
input {
|
|
||||||
color: $c-potassium;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.template-type {
|
|
||||||
flex: 1 1 50%;
|
|
||||||
padding-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-toggle, .dropdown-placeholder {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.temp-builder {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.temp-builder--mq-controls {
|
.temp-builder--mq-controls {
|
||||||
background: $g3-castle;
|
background: $g3-castle;
|
||||||
border-radius: $radius-small;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 10px 10px 0 10px;
|
padding: 2px 10px;
|
||||||
|
|
||||||
&:last-child {
|
&:first-of-type {
|
||||||
padding-bottom: 10px;
|
padding-top: 10px;
|
||||||
|
border-top-left-radius: $radius;
|
||||||
|
border-top-right-radius: $radius;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .temp-builder--mq-text, > .dropdown, .dropdown-placeholder {
|
&:last-of-type {
|
||||||
margin-right: 5px;
|
padding-bottom: 10px;
|
||||||
|
border-bottom-left-radius: $radius;
|
||||||
|
border-bottom-right-radius: $radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .temp-builder--mq-text,
|
||||||
|
> .dropdown,
|
||||||
|
.dropdown-placeholder {
|
||||||
|
margin-right: 4px;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
|
@ -92,18 +36,16 @@ $padding: 20px 30px;
|
||||||
@include no-user-select();
|
@include no-user-select();
|
||||||
background-color: $g5-pepper;
|
background-color: $g5-pepper;
|
||||||
border-radius: $radius-small;
|
border-radius: $radius-small;
|
||||||
padding: 8px;
|
padding: 0 8px;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: $c-pool;
|
color: $c-pool;
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-family: $code-font;
|
font-family: $code-font;
|
||||||
}
|
}
|
||||||
|
|
||||||
.temp-builder .temp-builder-results {
|
|
||||||
margin-top: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0% {
|
0% {
|
||||||
color: $g19-ghost;
|
color: $g19-ghost;
|
||||||
|
@ -118,18 +60,21 @@ $padding: 20px 30px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.temp-builder-results > p {
|
.temp-builder--validation {
|
||||||
|
@include no-user-select();
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: bold;
|
font-weight: 500;
|
||||||
color: $g19-ghost;
|
color: $g13-mist;
|
||||||
margin: 15px 0;
|
margin: 0 0 8px 0;
|
||||||
|
|
||||||
&.error {
|
&.error {
|
||||||
color: $c-fire;
|
color: $c-fire;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.warning {
|
&.warning {
|
||||||
color: $c-pineapple;
|
color: $c-pineapple;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.loading {
|
&.loading {
|
||||||
|
@ -137,24 +82,34 @@ $padding: 20px 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
> strong {
|
> strong {
|
||||||
color: $c-potassium;
|
color: $c-comet;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:only-child {
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.temp-builder-results--list {
|
.temp-builder--results {
|
||||||
|
margin-top: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.temp-builder--results-list {
|
||||||
max-height: 250px;
|
max-height: 250px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
}
|
||||||
li {
|
|
||||||
background-color: $g3-castle;
|
.temp-builder--results-item {
|
||||||
padding: 0 10px;
|
@include no-user-select();
|
||||||
display: flex;
|
background-color: $g3-castle;
|
||||||
align-items: center;
|
padding: 0 10px;
|
||||||
border-radius: $radius-small;
|
display: flex;
|
||||||
margin: 0;
|
align-items: center;
|
||||||
color: $g19-ghost;
|
border-radius: $radius;
|
||||||
font-weight: bold;
|
font-size: 12px;
|
||||||
list-style: none;
|
margin: 0;
|
||||||
}
|
color: $g13-mist;
|
||||||
|
font-weight: 600;
|
||||||
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
@keyframes blur-in {
|
||||||
|
from {
|
||||||
|
filter: blur(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
filter: blur(2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blur-out {
|
||||||
|
from {
|
||||||
|
filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
filter: blur(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogram-chart {
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:not(.loading) {
|
||||||
|
animation-duration: 0.1s;
|
||||||
|
animation-name: blur-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
animation-duration: 0.3s;
|
||||||
|
animation-name: blur-in;
|
||||||
|
animation-fill-mode: forwards;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogram-chart-bars--bar {
|
||||||
|
shape-rendering: crispEdges;
|
||||||
|
fill: $c-amethyst;
|
||||||
|
opacity: 1;
|
||||||
|
pointer: cursor;
|
||||||
|
shape-rendering: crispEdges;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogram-chart--axes, .histogram-chart-skeleton {
|
||||||
|
.x-label, .y-label {
|
||||||
|
fill: $g13-mist;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x-label {
|
||||||
|
text-anchor: middle;
|
||||||
|
alignment-baseline: hanging;
|
||||||
|
}
|
||||||
|
|
||||||
|
.y-label {
|
||||||
|
text-anchor: end;
|
||||||
|
alignment-baseline: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.y-tick {
|
||||||
|
stroke-width: 1;
|
||||||
|
stroke: $g5-pepper;
|
||||||
|
shape-rendering: crispEdges;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.histogram-chart-skeleton, .histogram-chart:not(.loading) .x-brush--area {
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogram-chart .x-brush--area {
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogram-chart .x-brush--selection {
|
||||||
|
fill: gray;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.histogram-chart-tooltip {
|
||||||
|
padding: 8px;
|
||||||
|
background-color: $g0-obsidian;
|
||||||
|
border-radius: 3px;
|
||||||
|
@extend %drop-shadow;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $g13-mist;
|
||||||
|
display: flex;
|
||||||
|
align-items: space-between;
|
||||||
|
transform: translate(0, -50%);
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
.histogram-chart-tooltip--value {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -551,12 +551,6 @@ $rule-builder--radius-lg: 5px;
|
||||||
padding-bottom: $rule-builder--padding-lg;
|
padding-bottom: $rule-builder--padding-lg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.endpoint-tab--parameters .faux-form {
|
|
||||||
margin-left: -6px;
|
|
||||||
margin-right: -6px;
|
|
||||||
width: calc(100% + 12px);
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
.endpoint-tab--parameters--empty {
|
.endpoint-tab--parameters--empty {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
|
@ -3,12 +3,29 @@
|
||||||
----------------------------------------------------------------------------
|
----------------------------------------------------------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$logs-viewer-graph-height: 180px;
|
$logs-viewer-graph-height: 220px;
|
||||||
$logs-viewer-search-height: 46px;
|
$logs-viewer-search-height: 46px;
|
||||||
$logs-viewer-filter-height: 42px;
|
$logs-viewer-filter-height: 42px;
|
||||||
$logs-viewer-results-text-indent: 33px;
|
$logs-viewer-results-text-indent: 33px;
|
||||||
$logs-viewer-gutter: 60px;
|
$logs-viewer-gutter: 60px;
|
||||||
|
|
||||||
|
$severity-emerg: $c-ruby;
|
||||||
|
$severity-alert: $c-fire;
|
||||||
|
$severity-crit: $c-curacao;
|
||||||
|
$severity-err: $c-tiger;
|
||||||
|
$severity-warning: $c-pineapple;
|
||||||
|
$severity-notice: $c-rainforest;
|
||||||
|
$severity-info: $c-star;
|
||||||
|
$severity-debug: $g9-mountain;
|
||||||
|
$severity-emerg-intense: $c-fire;
|
||||||
|
$severity-alert-intense: $c-curacao;
|
||||||
|
$severity-crit-intense: $c-tiger;
|
||||||
|
$severity-err-intense: $c-pineapple;
|
||||||
|
$severity-warning-intense: $c-thunder;
|
||||||
|
$severity-notice-intense: $c-honeydew;
|
||||||
|
$severity-info-intense: $c-comet;
|
||||||
|
$severity-debug-intense: $g10-wolf;
|
||||||
|
|
||||||
.logs-viewer {
|
.logs-viewer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -232,28 +249,28 @@ $logs-viewer-gutter: 60px;
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
|
|
||||||
&.emerg-severity {
|
&.emerg-severity {
|
||||||
@include gradient-diag-up($c-ruby, $c-fire);
|
@include gradient-diag-up($severity-emerg, $severity-emerg-intense);
|
||||||
}
|
}
|
||||||
&.alert-severity {
|
&.alert-severity {
|
||||||
@include gradient-diag-up($c-fire, $c-curacao);
|
@include gradient-diag-up($severity-alert, $severity-alert-intense);
|
||||||
}
|
}
|
||||||
&.crit-severity {
|
&.crit-severity {
|
||||||
@include gradient-diag-up($c-curacao, $c-tiger);
|
@include gradient-diag-up($severity-crit, $severity-crit-intense);
|
||||||
}
|
}
|
||||||
&.err-severity {
|
&.err-severity {
|
||||||
@include gradient-diag-up($c-tiger, $c-pineapple);
|
@include gradient-diag-up($severity-err, $severity-err-intense);
|
||||||
}
|
}
|
||||||
&.warning-severity {
|
&.warning-severity {
|
||||||
@include gradient-diag-up($c-pineapple, $c-thunder);
|
@include gradient-diag-up($severity-warning, $severity-warning-intense);
|
||||||
}
|
}
|
||||||
&.notice-severity {
|
&.notice-severity {
|
||||||
@include gradient-diag-up($c-rainforest, $c-honeydew);
|
@include gradient-diag-up($severity-notice, $severity-notice-intense);
|
||||||
}
|
}
|
||||||
&.info-severity {
|
&.info-severity {
|
||||||
@include gradient-diag-up($c-star, $c-comet);
|
@include gradient-diag-up($severity-info, $severity-info-intense);
|
||||||
}
|
}
|
||||||
&.debug-severity {
|
&.debug-severity {
|
||||||
@include gradient-diag-up($g9-mountain, $g10-wolf);
|
@include gradient-diag-up($severity-debug, $severity-debug-intense);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -310,3 +327,79 @@ $logs-viewer-gutter: 60px;
|
||||||
background-color: $c-laser;
|
background-color: $c-laser;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logs-viewer .histogram-chart-bars--bar, .logs-viewer .histogram-chart-tooltip {
|
||||||
|
&[data-group="emerg"] {
|
||||||
|
fill: $severity-emerg;
|
||||||
|
color: $severity-emerg;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-group="alert"] {
|
||||||
|
fill: $severity-alert;
|
||||||
|
color: $severity-alert;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-group="crit"] {
|
||||||
|
fill: $severity-crit;
|
||||||
|
color: $severity-crit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-group="err"] {
|
||||||
|
fill: $severity-err;
|
||||||
|
color: $severity-err;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-group="warning"] {
|
||||||
|
fill: $severity-warning;
|
||||||
|
color: $severity-warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-group="notice"] {
|
||||||
|
fill: $severity-notice;
|
||||||
|
color: $severity-notice;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-group="info"] {
|
||||||
|
fill: $severity-info;
|
||||||
|
color: $severity-info;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-group="debug"] {
|
||||||
|
fill: $severity-debug;
|
||||||
|
color: $severity-debug;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-viewer .histogram-chart-bars--bar:hover {
|
||||||
|
&[data-group="emerg"] {
|
||||||
|
fill: $severity-emerg-intense;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-group="alert"] {
|
||||||
|
fill: $severity-alert-intense;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-group="crit"] {
|
||||||
|
fill: $severity-crit-intense;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-group="err"] {
|
||||||
|
fill: $severity-err-intense;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-group="warning"] {
|
||||||
|
fill: $severity-warning-intense;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-group="notice"] {
|
||||||
|
fill: $severity-notice-intense;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-group="info"] {
|
||||||
|
fill: $severity-info-intense;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-group="debug"] {
|
||||||
|
fill: $severity-debug-intense;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,4 +8,21 @@
|
||||||
@import '../components/time-machine/flux-builder';
|
@import '../components/time-machine/flux-builder';
|
||||||
@import '../components/time-machine/flux-explorer';
|
@import '../components/time-machine/flux-explorer';
|
||||||
@import '../components/time-machine/visualization';
|
@import '../components/time-machine/visualization';
|
||||||
@import '../components/time-machine/add-func-button';
|
@import '../components/time-machine/add-func-button';
|
||||||
|
|
||||||
|
// Flux Page Empty state
|
||||||
|
.flux-empty {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%,-50%);
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> p {
|
||||||
|
color: $g11-sidewalk;
|
||||||
|
font-size: 16px;
|
||||||
|
@include no-user-select();
|
||||||
|
}
|
||||||
|
}
|
|
@ -423,3 +423,42 @@ button.btn-link-alert {
|
||||||
$c-thunder
|
$c-thunder
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Buttons Groups
|
||||||
|
-----------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
.btn-group--left,
|
||||||
|
.btn-group--center,
|
||||||
|
.btn-group--right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group--left > .btn {
|
||||||
|
margin-right: 4px;
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group--center > .btn {
|
||||||
|
margin-left: 2px;
|
||||||
|
margin-right: 2px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group--right > .btn {
|
||||||
|
margin-left: 4px;
|
||||||
|
&:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -43,7 +43,7 @@ $grid--col-12: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group-submit {
|
.form-group-submit {
|
||||||
margin-top: 30px;
|
margin-top: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.col {
|
.col {
|
||||||
|
@ -220,3 +220,68 @@ $grid--col-12: 100%;
|
||||||
&-11 { margin-left: $grid--col-11; }
|
&-11 { margin-left: $grid--col-11; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wrapp form sets to ensure proper spacing
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
div.faux-form {
|
||||||
|
width: calc(100% + 12px);
|
||||||
|
margin-left: -6px;
|
||||||
|
margin-right: -6px;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
.form-group.col-xs-1,
|
||||||
|
.form-group.col-xs-2,
|
||||||
|
.form-group.col-xs-3,
|
||||||
|
.form-group.col-xs-4,
|
||||||
|
.form-group.col-xs-5,
|
||||||
|
.form-group.col-xs-6,
|
||||||
|
.form-group.col-xs-7,
|
||||||
|
.form-group.col-xs-8,
|
||||||
|
.form-group.col-xs-9,
|
||||||
|
.form-group.col-xs-10,
|
||||||
|
.form-group.col-xs-11,
|
||||||
|
.form-group.col-xs-12,
|
||||||
|
.form-group.col-sm-1,
|
||||||
|
.form-group.col-sm-2,
|
||||||
|
.form-group.col-sm-3,
|
||||||
|
.form-group.col-sm-4,
|
||||||
|
.form-group.col-sm-5,
|
||||||
|
.form-group.col-sm-6,
|
||||||
|
.form-group.col-sm-7,
|
||||||
|
.form-group.col-sm-8,
|
||||||
|
.form-group.col-sm-9,
|
||||||
|
.form-group.col-sm-10,
|
||||||
|
.form-group.col-sm-11,
|
||||||
|
.form-group.col-sm-12,
|
||||||
|
.form-group.col-md-1,
|
||||||
|
.form-group.col-md-2,
|
||||||
|
.form-group.col-md-3,
|
||||||
|
.form-group.col-md-4,
|
||||||
|
.form-group.col-md-5,
|
||||||
|
.form-group.col-md-6,
|
||||||
|
.form-group.col-md-7,
|
||||||
|
.form-group.col-md-8,
|
||||||
|
.form-group.col-md-9,
|
||||||
|
.form-group.col-md-10,
|
||||||
|
.form-group.col-md-11,
|
||||||
|
.form-group.col-md-12,
|
||||||
|
.form-group.col-lg-1,
|
||||||
|
.form-group.col-lg-2,
|
||||||
|
.form-group.col-lg-3,
|
||||||
|
.form-group.col-lg-4,
|
||||||
|
.form-group.col-lg-5,
|
||||||
|
.form-group.col-lg-6,
|
||||||
|
.form-group.col-lg-7,
|
||||||
|
.form-group.col-lg-8,
|
||||||
|
.form-group.col-lg-9,
|
||||||
|
.form-group.col-lg-10,
|
||||||
|
.form-group.col-lg-11,
|
||||||
|
.form-group.col-lg-12 {
|
||||||
|
padding-left: 6px;
|
||||||
|
padding-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> *:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -484,65 +484,6 @@ $dash-editable-header-padding: 7px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
Fake form padding without <form>
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
div.faux-form {
|
|
||||||
.form-group.col-xs-1,
|
|
||||||
.form-group.col-xs-2,
|
|
||||||
.form-group.col-xs-3,
|
|
||||||
.form-group.col-xs-4,
|
|
||||||
.form-group.col-xs-5,
|
|
||||||
.form-group.col-xs-6,
|
|
||||||
.form-group.col-xs-7,
|
|
||||||
.form-group.col-xs-8,
|
|
||||||
.form-group.col-xs-9,
|
|
||||||
.form-group.col-xs-10,
|
|
||||||
.form-group.col-xs-11,
|
|
||||||
.form-group.col-xs-12,
|
|
||||||
.form-group.col-sm-1,
|
|
||||||
.form-group.col-sm-2,
|
|
||||||
.form-group.col-sm-3,
|
|
||||||
.form-group.col-sm-4,
|
|
||||||
.form-group.col-sm-5,
|
|
||||||
.form-group.col-sm-6,
|
|
||||||
.form-group.col-sm-7,
|
|
||||||
.form-group.col-sm-8,
|
|
||||||
.form-group.col-sm-9,
|
|
||||||
.form-group.col-sm-10,
|
|
||||||
.form-group.col-sm-11,
|
|
||||||
.form-group.col-sm-12,
|
|
||||||
.form-group.col-md-1,
|
|
||||||
.form-group.col-md-2,
|
|
||||||
.form-group.col-md-3,
|
|
||||||
.form-group.col-md-4,
|
|
||||||
.form-group.col-md-5,
|
|
||||||
.form-group.col-md-6,
|
|
||||||
.form-group.col-md-7,
|
|
||||||
.form-group.col-md-8,
|
|
||||||
.form-group.col-md-9,
|
|
||||||
.form-group.col-md-10,
|
|
||||||
.form-group.col-md-11,
|
|
||||||
.form-group.col-md-12,
|
|
||||||
.form-group.col-lg-1,
|
|
||||||
.form-group.col-lg-2,
|
|
||||||
.form-group.col-lg-3,
|
|
||||||
.form-group.col-lg-4,
|
|
||||||
.form-group.col-lg-5,
|
|
||||||
.form-group.col-lg-6,
|
|
||||||
.form-group.col-lg-7,
|
|
||||||
.form-group.col-lg-8,
|
|
||||||
.form-group.col-lg-9,
|
|
||||||
.form-group.col-lg-10,
|
|
||||||
.form-group.col-lg-11,
|
|
||||||
.form-group.col-lg-12 {
|
|
||||||
padding-left: 6px;
|
|
||||||
padding-right: 6px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Stretch to fit Dropdowns
|
Stretch to fit Dropdowns
|
||||||
-----------------------------------------------------------------------------
|
-----------------------------------------------------------------------------
|
||||||
|
|
|
@ -37,8 +37,8 @@ class DatabasesTemplateBuilder extends PureComponent<
|
||||||
const {databasesStatus} = this.state
|
const {databasesStatus} = this.state
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="temp-builder databases-temp-builder">
|
<>
|
||||||
<div className="form-group">
|
<div className="form-group col-xs-12">
|
||||||
<label>Meta Query</label>
|
<label>Meta Query</label>
|
||||||
<div className="temp-builder--mq-controls">
|
<div className="temp-builder--mq-controls">
|
||||||
<div className="temp-builder--mq-text">SHOW DATABASES</div>
|
<div className="temp-builder--mq-text">SHOW DATABASES</div>
|
||||||
|
@ -49,7 +49,7 @@ class DatabasesTemplateBuilder extends PureComponent<
|
||||||
loadingStatus={databasesStatus}
|
loadingStatus={databasesStatus}
|
||||||
onUpdateDefaultTemplateValue={onUpdateDefaultTemplateValue}
|
onUpdateDefaultTemplateValue={onUpdateDefaultTemplateValue}
|
||||||
/>
|
/>
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -74,8 +74,8 @@ class KeysTemplateBuilder extends PureComponent<Props, State> {
|
||||||
} = this.state
|
} = this.state
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="temp-builder measurements-temp-builder">
|
<>
|
||||||
<div className="form-group">
|
<div className="form-group col-xs-12">
|
||||||
<label>Meta Query</label>
|
<label>Meta Query</label>
|
||||||
<div className="temp-builder--mq-controls">
|
<div className="temp-builder--mq-controls">
|
||||||
<div className="temp-builder--mq-text">{queryPrefix}</div>
|
<div className="temp-builder--mq-text">{queryPrefix}</div>
|
||||||
|
@ -84,7 +84,8 @@ class KeysTemplateBuilder extends PureComponent<Props, State> {
|
||||||
items={databases.map(text => ({text}))}
|
items={databases.map(text => ({text}))}
|
||||||
onChoose={this.handleChooseDatabaseDropdown}
|
onChoose={this.handleChooseDatabaseDropdown}
|
||||||
selected={selectedDatabase}
|
selected={selectedDatabase}
|
||||||
buttonSize=""
|
buttonSize="btn-sm"
|
||||||
|
className="dropdown-stretch"
|
||||||
/>
|
/>
|
||||||
</DropdownLoadingPlaceholder>
|
</DropdownLoadingPlaceholder>
|
||||||
<div className="temp-builder--mq-text">FROM</div>
|
<div className="temp-builder--mq-text">FROM</div>
|
||||||
|
@ -93,7 +94,8 @@ class KeysTemplateBuilder extends PureComponent<Props, State> {
|
||||||
items={measurements.map(text => ({text}))}
|
items={measurements.map(text => ({text}))}
|
||||||
onChoose={this.handleChooseMeasurementDropdown}
|
onChoose={this.handleChooseMeasurementDropdown}
|
||||||
selected={selectedMeasurement}
|
selected={selectedMeasurement}
|
||||||
buttonSize=""
|
buttonSize="btn-sm"
|
||||||
|
className="dropdown-stretch"
|
||||||
/>
|
/>
|
||||||
</DropdownLoadingPlaceholder>
|
</DropdownLoadingPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
|
@ -103,7 +105,7 @@ class KeysTemplateBuilder extends PureComponent<Props, State> {
|
||||||
loadingStatus={keysStatus}
|
loadingStatus={keysStatus}
|
||||||
onUpdateDefaultTemplateValue={onUpdateDefaultTemplateValue}
|
onUpdateDefaultTemplateValue={onUpdateDefaultTemplateValue}
|
||||||
/>
|
/>
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,8 +55,8 @@ class MeasurementsTemplateBuilder extends PureComponent<
|
||||||
} = this.state
|
} = this.state
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="temp-builder measurements-temp-builder">
|
<>
|
||||||
<div className="form-group">
|
<div className="form-group col-xs-12">
|
||||||
<label>Meta Query</label>
|
<label>Meta Query</label>
|
||||||
<div className="temp-builder--mq-controls">
|
<div className="temp-builder--mq-controls">
|
||||||
<div className="temp-builder--mq-text">SHOW MEASUREMENTS ON</div>
|
<div className="temp-builder--mq-text">SHOW MEASUREMENTS ON</div>
|
||||||
|
@ -65,7 +65,8 @@ class MeasurementsTemplateBuilder extends PureComponent<
|
||||||
items={databases.map(text => ({text}))}
|
items={databases.map(text => ({text}))}
|
||||||
onChoose={this.handleChooseDatabaseDropdown}
|
onChoose={this.handleChooseDatabaseDropdown}
|
||||||
selected={selectedDatabase}
|
selected={selectedDatabase}
|
||||||
buttonSize=""
|
buttonSize="btn-sm"
|
||||||
|
className="dropdown-stretch"
|
||||||
/>
|
/>
|
||||||
</DropdownLoadingPlaceholder>
|
</DropdownLoadingPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
|
@ -75,7 +76,7 @@ class MeasurementsTemplateBuilder extends PureComponent<
|
||||||
loadingStatus={measurementsStatus}
|
loadingStatus={measurementsStatus}
|
||||||
onUpdateDefaultTemplateValue={onUpdateDefaultTemplateValue}
|
onUpdateDefaultTemplateValue={onUpdateDefaultTemplateValue}
|
||||||
/>
|
/>
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -56,12 +56,12 @@ class CustomMetaQueryTemplateBuilder extends PureComponent<
|
||||||
const {metaQueryInput} = this.state
|
const {metaQueryInput} = this.state
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="temp-builder csv-temp-builder">
|
<>
|
||||||
<div className="form-group">
|
<div className="form-group col-xs-12">
|
||||||
<label>Meta Query</label>
|
<label>Meta Query</label>
|
||||||
<div className="temp-builder--mq-controls">
|
<div className="temp-builder--mq-controls">
|
||||||
<textarea
|
<textarea
|
||||||
className="form-control"
|
className="form-control input-sm"
|
||||||
value={metaQueryInput}
|
value={metaQueryInput}
|
||||||
onChange={this.handleMetaQueryInputChange}
|
onChange={this.handleMetaQueryInputChange}
|
||||||
onBlur={this.handleMetaQueryChange}
|
onBlur={this.handleMetaQueryChange}
|
||||||
|
@ -69,7 +69,7 @@ class CustomMetaQueryTemplateBuilder extends PureComponent<
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{this.renderResults()}
|
{this.renderResults()}
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,8 +79,10 @@ class CustomMetaQueryTemplateBuilder extends PureComponent<
|
||||||
|
|
||||||
if (this.showInvalidMetaQueryMessage) {
|
if (this.showInvalidMetaQueryMessage) {
|
||||||
return (
|
return (
|
||||||
<div className="temp-builder-results">
|
<div className="form-group col-xs-12 temp-builder--results">
|
||||||
<p className="error">Meta Query is not valid.</p>
|
<p className="temp-builder--validation error">
|
||||||
|
Meta Query is not valid.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,8 +77,8 @@ class KeysTemplateBuilder extends PureComponent<TemplateBuilderProps, State> {
|
||||||
} = this.state
|
} = this.state
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="temp-builder measurements-temp-builder">
|
<>
|
||||||
<div className="form-group">
|
<div className="form-group col-xs-12">
|
||||||
<label>Meta Query</label>
|
<label>Meta Query</label>
|
||||||
<div className="temp-builder--mq-controls">
|
<div className="temp-builder--mq-controls">
|
||||||
<div className="temp-builder--mq-text">SHOW TAG VALUES ON</div>
|
<div className="temp-builder--mq-text">SHOW TAG VALUES ON</div>
|
||||||
|
@ -87,7 +87,8 @@ class KeysTemplateBuilder extends PureComponent<TemplateBuilderProps, State> {
|
||||||
items={databases.map(text => ({text}))}
|
items={databases.map(text => ({text}))}
|
||||||
onChoose={this.handleChooseDatabaseDropdown}
|
onChoose={this.handleChooseDatabaseDropdown}
|
||||||
selected={selectedDatabase}
|
selected={selectedDatabase}
|
||||||
buttonSize=""
|
buttonSize="btn-sm"
|
||||||
|
className="dropdown-stretch"
|
||||||
/>
|
/>
|
||||||
</DropdownLoadingPlaceholder>
|
</DropdownLoadingPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
|
@ -98,7 +99,8 @@ class KeysTemplateBuilder extends PureComponent<TemplateBuilderProps, State> {
|
||||||
items={measurements.map(text => ({text}))}
|
items={measurements.map(text => ({text}))}
|
||||||
onChoose={this.handleChooseMeasurementDropdown}
|
onChoose={this.handleChooseMeasurementDropdown}
|
||||||
selected={selectedMeasurement}
|
selected={selectedMeasurement}
|
||||||
buttonSize=""
|
buttonSize="btn-sm"
|
||||||
|
className="dropdown-stretch"
|
||||||
/>
|
/>
|
||||||
</DropdownLoadingPlaceholder>
|
</DropdownLoadingPlaceholder>
|
||||||
<div className="temp-builder--mq-text">WITH KEY</div>
|
<div className="temp-builder--mq-text">WITH KEY</div>
|
||||||
|
@ -107,7 +109,8 @@ class KeysTemplateBuilder extends PureComponent<TemplateBuilderProps, State> {
|
||||||
items={tagKeys.map(text => ({text}))}
|
items={tagKeys.map(text => ({text}))}
|
||||||
onChoose={this.handleChooseTagKeyDropdown}
|
onChoose={this.handleChooseTagKeyDropdown}
|
||||||
selected={selectedTagKey}
|
selected={selectedTagKey}
|
||||||
buttonSize=""
|
buttonSize="btn-sm"
|
||||||
|
className="dropdown-stretch"
|
||||||
/>
|
/>
|
||||||
</DropdownLoadingPlaceholder>
|
</DropdownLoadingPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
|
@ -117,7 +120,7 @@ class KeysTemplateBuilder extends PureComponent<TemplateBuilderProps, State> {
|
||||||
loadingStatus={tagValuesStatus}
|
loadingStatus={tagValuesStatus}
|
||||||
onUpdateDefaultTemplateValue={onUpdateDefaultTemplateValue}
|
onUpdateDefaultTemplateValue={onUpdateDefaultTemplateValue}
|
||||||
/>
|
/>
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React, {Component} from 'react'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
|
|
||||||
import TemplateControlDropdown from 'src/tempVars/components/TemplateControlDropdown'
|
import TemplateControlDropdown from 'src/tempVars/components/TemplateControlDropdown'
|
||||||
import SimpleOverlayTechnology from 'src/shared/components/SimpleOverlayTechnology'
|
import OverlayTechnology from 'src/reusable_ui/components/overlays/OverlayTechnology'
|
||||||
import TemplateVariableEditor from 'src/tempVars/components/TemplateVariableEditor'
|
import TemplateVariableEditor from 'src/tempVars/components/TemplateVariableEditor'
|
||||||
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
|
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
|
||||||
|
|
||||||
|
@ -64,15 +64,13 @@ class TemplateControlBar extends Component<Props, State> {
|
||||||
<strong>Template Variables</strong>
|
<strong>Template Variables</strong>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isAdding && (
|
<OverlayTechnology visible={isAdding}>
|
||||||
<SimpleOverlayTechnology>
|
<TemplateVariableEditor
|
||||||
<TemplateVariableEditor
|
source={source}
|
||||||
source={source}
|
onCreate={this.handleCreateTemplate}
|
||||||
onCreate={this.handleCreateTemplate}
|
onCancel={this.handleCancelAddVariable}
|
||||||
onCancel={this.handleCancelAddVariable}
|
/>
|
||||||
/>
|
</OverlayTechnology>
|
||||||
</SimpleOverlayTechnology>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<Authorized requiredRole={EDITOR_ROLE}>
|
<Authorized requiredRole={EDITOR_ROLE}>
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, {PureComponent} from 'react'
|
import React, {PureComponent} from 'react'
|
||||||
|
|
||||||
import Dropdown from 'src/shared/components/Dropdown'
|
import Dropdown from 'src/shared/components/Dropdown'
|
||||||
import SimpleOverlayTechnology from 'src/shared/components/SimpleOverlayTechnology'
|
import OverlayTechnology from 'src/reusable_ui/components/overlays/OverlayTechnology'
|
||||||
import TemplateVariableEditor from 'src/tempVars/components/TemplateVariableEditor'
|
import TemplateVariableEditor from 'src/tempVars/components/TemplateVariableEditor'
|
||||||
import {calculateDropdownWidth} from 'src/dashboards/constants/templateControlBar'
|
import {calculateDropdownWidth} from 'src/dashboards/constants/templateControlBar'
|
||||||
import Authorized, {isUserAuthorized, EDITOR_ROLE} from 'src/auth/Authorized'
|
import Authorized, {isUserAuthorized, EDITOR_ROLE} from 'src/auth/Authorized'
|
||||||
|
@ -76,18 +76,16 @@ class TemplateControlDropdown extends PureComponent<Props, State> {
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</Authorized>
|
</Authorized>
|
||||||
{isEditing && (
|
<OverlayTechnology visible={isEditing}>
|
||||||
<SimpleOverlayTechnology>
|
<TemplateVariableEditor
|
||||||
<TemplateVariableEditor
|
template={template}
|
||||||
template={template}
|
source={source}
|
||||||
source={source}
|
onCreate={onCreateTemplate}
|
||||||
onCreate={onCreateTemplate}
|
onUpdate={this.handleUpdateTemplate}
|
||||||
onUpdate={this.handleUpdateTemplate}
|
onDelete={this.handleDelete}
|
||||||
onDelete={this.handleDelete}
|
onCancel={this.handleHideSettings}
|
||||||
onCancel={this.handleHideSettings}
|
/>
|
||||||
/>
|
</OverlayTechnology>
|
||||||
</SimpleOverlayTechnology>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,29 +17,33 @@ class TemplateMetaQueryPreview extends PureComponent<Props> {
|
||||||
const {items, loadingStatus, onUpdateDefaultTemplateValue} = this.props
|
const {items, loadingStatus, onUpdateDefaultTemplateValue} = this.props
|
||||||
|
|
||||||
if (loadingStatus === RemoteDataState.NotStarted) {
|
if (loadingStatus === RemoteDataState.NotStarted) {
|
||||||
return <div className="temp-builder-results" />
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loadingStatus === RemoteDataState.Loading) {
|
if (loadingStatus === RemoteDataState.Loading) {
|
||||||
return (
|
return (
|
||||||
<div className="temp-builder-results">
|
<div className="form-group col-xs-12 temp-builder--results">
|
||||||
<p className="loading">Loading meta query preview...</p>
|
<p className="temp-builder--validation loading">
|
||||||
|
Loading Meta Query preview...
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loadingStatus === RemoteDataState.Error) {
|
if (loadingStatus === RemoteDataState.Error) {
|
||||||
return (
|
return (
|
||||||
<div className="temp-builder-results">
|
<div className="form-group col-xs-12 temp-builder--results">
|
||||||
<p className="error">Meta Query failed to execute</p>
|
<p className="temp-builder--validation error">
|
||||||
|
Meta Query failed to execute
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="temp-builder-results">
|
<div className="form-group col-xs-12 temp-builder--results">
|
||||||
<p className="warning">
|
<p className="temp-builder--validation warning">
|
||||||
Meta Query is syntactically correct but returned no results
|
Meta Query is syntactically correct but returned no results
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -49,8 +53,8 @@ class TemplateMetaQueryPreview extends PureComponent<Props> {
|
||||||
const pluralizer = items.length === 1 ? '' : 's'
|
const pluralizer = items.length === 1 ? '' : 's'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="temp-builder-results">
|
<div className="form-group col-xs-12 temp-builder--results">
|
||||||
<p>
|
<p className="temp-builder--validation">
|
||||||
Meta Query returned <strong>{items.length}</strong> value{pluralizer}
|
Meta Query returned <strong>{items.length}</strong> value{pluralizer}
|
||||||
</p>
|
</p>
|
||||||
{items.length > 0 && (
|
{items.length > 0 && (
|
||||||
|
|
|
@ -7,9 +7,9 @@ import TemplatePreviewListItem from 'src/tempVars/components/TemplatePreviewList
|
||||||
|
|
||||||
import {TemplateValue} from 'src/types'
|
import {TemplateValue} from 'src/types'
|
||||||
|
|
||||||
const LI_HEIGHT = 35
|
|
||||||
const LI_MARGIN_BOTTOM = 3
|
|
||||||
const RESULTS_TO_DISPLAY = 10
|
const RESULTS_TO_DISPLAY = 10
|
||||||
|
const LI_HEIGHT = 28
|
||||||
|
const LI_MARGIN_BOTTOM = 2
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
items: TemplateValue[]
|
items: TemplateValue[]
|
||||||
|
@ -22,27 +22,20 @@ class TemplatePreviewList extends PureComponent<Props> {
|
||||||
const {items, onUpdateDefaultTemplateValue} = this.props
|
const {items, onUpdateDefaultTemplateValue} = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul
|
<div
|
||||||
className="temp-builder-results--list"
|
className="temp-builder--results-list"
|
||||||
style={{height: `${this.resultsListHeight}px`}}
|
style={{height: `${this.resultsListHeight}px`}}
|
||||||
>
|
>
|
||||||
<FancyScrollbar>
|
<FancyScrollbar autoHide={false}>
|
||||||
{items.map(item => {
|
{items.map(item => (
|
||||||
return (
|
<TemplatePreviewListItem
|
||||||
<TemplatePreviewListItem
|
key={uuid.v4()}
|
||||||
key={uuid.v4()}
|
onClick={onUpdateDefaultTemplateValue}
|
||||||
onClick={onUpdateDefaultTemplateValue}
|
item={item}
|
||||||
style={{
|
/>
|
||||||
height: `${LI_HEIGHT}px`,
|
))}
|
||||||
marginBottom: `${LI_MARGIN_BOTTOM}px`,
|
|
||||||
zIndex: 9010,
|
|
||||||
}}
|
|
||||||
item={item}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</FancyScrollbar>
|
</FancyScrollbar>
|
||||||
</ul>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,18 +6,23 @@ import {TemplateValue} from 'src/types'
|
||||||
interface Props {
|
interface Props {
|
||||||
item: TemplateValue
|
item: TemplateValue
|
||||||
onClick: (item: TemplateValue) => void
|
onClick: (item: TemplateValue) => void
|
||||||
style: React.CSSProperties
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LI_HEIGHT = 28
|
||||||
|
const LI_MARGIN_BOTTOM = 2
|
||||||
|
|
||||||
class TemplatePreviewListItem extends PureComponent<Props> {
|
class TemplatePreviewListItem extends PureComponent<Props> {
|
||||||
public render() {
|
public render() {
|
||||||
const {item, style} = this.props
|
const {item} = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
style={style}
|
style={{
|
||||||
className={classNames('temp-builder-results--list-item', {
|
height: `${LI_HEIGHT}px`,
|
||||||
|
marginBottom: `${LI_MARGIN_BOTTOM}px`,
|
||||||
|
}}
|
||||||
|
className={classNames('temp-builder--results-item', {
|
||||||
active: this.isDefault,
|
active: this.isDefault,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
|
|
@ -8,6 +8,9 @@ import {connect} from 'react-redux'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
|
||||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||||
|
import OverlayContainer from 'src/reusable_ui/components/overlays/OverlayContainer'
|
||||||
|
import OverlayHeading from 'src/reusable_ui/components/overlays/OverlayHeading'
|
||||||
|
import OverlayBody from 'src/reusable_ui/components/overlays/OverlayBody'
|
||||||
import Dropdown from 'src/shared/components/Dropdown'
|
import Dropdown from 'src/shared/components/Dropdown'
|
||||||
import ConfirmButton from 'src/shared/components/ConfirmButton'
|
import ConfirmButton from 'src/shared/components/ConfirmButton'
|
||||||
import {getDeep} from 'src/utils/wrappers'
|
import {getDeep} from 'src/utils/wrappers'
|
||||||
|
@ -70,7 +73,7 @@ const TEMPLATE_BUILDERS = {
|
||||||
[TemplateType.MetaQuery]: MetaQueryTemplateBuilder,
|
[TemplateType.MetaQuery]: MetaQueryTemplateBuilder,
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatName = name => `:${name.replace(/:/g, '')}:`
|
const formatName = name => `:${name.replace(/:/g, '').replace(/\s/g, '')}:`
|
||||||
|
|
||||||
const DEFAULT_TEMPLATE = DEFAULT_TEMPLATES[TemplateType.Databases]
|
const DEFAULT_TEMPLATE = DEFAULT_TEMPLATES[TemplateType.Databases]
|
||||||
|
|
||||||
|
@ -107,21 +110,18 @@ class TemplateVariableEditor extends PureComponent<Props, State> {
|
||||||
const TemplateBuilder = this.templateBuilder
|
const TemplateBuilder = this.templateBuilder
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="edit-temp-var">
|
<OverlayContainer maxWidth={650}>
|
||||||
<div className="edit-temp-var--header">
|
<OverlayHeading title={this.title}>
|
||||||
<h1>{isNew ? 'Create' : 'Edit'} Template Variable</h1>
|
<div className="btn-group--right">
|
||||||
<div className="edit-temp-var--header-controls">
|
|
||||||
<button
|
<button
|
||||||
className="btn btn-default"
|
className="btn btn-default btn-sm"
|
||||||
style={{zIndex: 9010}}
|
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
style={{zIndex: 9010}}
|
className="btn btn-success btn-sm"
|
||||||
className="btn btn-success"
|
|
||||||
type="button"
|
type="button"
|
||||||
onClick={this.handleSave}
|
onClick={this.handleSave}
|
||||||
disabled={!this.canSave}
|
disabled={!this.canSave}
|
||||||
|
@ -129,31 +129,31 @@ class TemplateVariableEditor extends PureComponent<Props, State> {
|
||||||
{this.saveButtonText}
|
{this.saveButtonText}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</OverlayHeading>
|
||||||
<div className="edit-temp-var--body" style={{zIndex: 9010}}>
|
<OverlayBody>
|
||||||
<div className="edit-temp-var--body-row" style={{zIndex: 9010}}>
|
<div className="faux-form">
|
||||||
<div style={{zIndex: 9010}} className="form-group name">
|
<div className="form-group col-sm-6">
|
||||||
<label>Name</label>
|
<label>Name</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control"
|
className="form-control input-sm form-astronaut"
|
||||||
value={nextTemplate.tempVar}
|
value={nextTemplate.tempVar}
|
||||||
onChange={this.handleChangeName}
|
onChange={this.handleChangeName}
|
||||||
onKeyPress={this.handleNameKeyPress}
|
onKeyPress={this.handleNameKeyPress}
|
||||||
onBlur={this.formatName}
|
onBlur={this.formatName}
|
||||||
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div style={{zIndex: 9010}} className="form-group template-type">
|
<div className="form-group col-sm-6">
|
||||||
<label>Type</label>
|
<label>Type</label>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
items={TEMPLATE_TYPES_LIST}
|
items={TEMPLATE_TYPES_LIST}
|
||||||
onChoose={this.handleChooseType}
|
onChoose={this.handleChooseType}
|
||||||
selected={this.dropdownSelection}
|
selected={this.dropdownSelection}
|
||||||
buttonSize=""
|
buttonSize="btn-sm"
|
||||||
|
className="dropdown-stretch"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="edit-temp-var--body-row">
|
|
||||||
<TemplateBuilder
|
<TemplateBuilder
|
||||||
template={nextTemplate}
|
template={nextTemplate}
|
||||||
source={source}
|
source={source}
|
||||||
|
@ -163,20 +163,42 @@ class TemplateVariableEditor extends PureComponent<Props, State> {
|
||||||
this.handleUpdateSelectedTemplateValue
|
this.handleUpdateSelectedTemplateValue
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<div className="form-group text-center form-group-submit col-xs-12">
|
||||||
|
<ConfirmButton
|
||||||
|
text={this.confirmText}
|
||||||
|
confirmAction={this.handleDelete}
|
||||||
|
type="btn-danger"
|
||||||
|
size="btn-sm"
|
||||||
|
customClass="delete"
|
||||||
|
disabled={isNew || this.isDeleting}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ConfirmButton
|
</OverlayBody>
|
||||||
text={this.isDeleting ? 'Deleting...' : 'Delete'}
|
</OverlayContainer>
|
||||||
confirmAction={this.handleDelete}
|
|
||||||
type="btn-danger"
|
|
||||||
size="btn-xs"
|
|
||||||
customClass="delete"
|
|
||||||
disabled={isNew || this.isDeleting}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get confirmText(): string {
|
||||||
|
if (this.isDeleting) {
|
||||||
|
return 'Deleting...'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Delete'
|
||||||
|
}
|
||||||
|
|
||||||
|
private get title(): string {
|
||||||
|
const {isNew} = this.state
|
||||||
|
|
||||||
|
let prefix = 'Edit'
|
||||||
|
|
||||||
|
if (isNew) {
|
||||||
|
prefix = 'Create'
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${prefix} Template Variable`
|
||||||
|
}
|
||||||
|
|
||||||
private get templateBuilder(): ComponentClass<TemplateBuilderProps> {
|
private get templateBuilder(): ComponentClass<TemplateBuilderProps> {
|
||||||
const {
|
const {
|
||||||
nextTemplate: {type},
|
nextTemplate: {type},
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
type UnixTime = number
|
||||||
|
|
||||||
|
export interface HistogramDatum {
|
||||||
|
key: string
|
||||||
|
time: UnixTime
|
||||||
|
value: number
|
||||||
|
group: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimePeriod {
|
||||||
|
start: UnixTime
|
||||||
|
end: UnixTime
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HistogramData = HistogramDatum[]
|
||||||
|
|
||||||
|
export type TooltipAnchor = 'left' | 'right'
|
||||||
|
|
||||||
|
export interface Margins {
|
||||||
|
top: number
|
||||||
|
right: number
|
||||||
|
bottom: number
|
||||||
|
left: number
|
||||||
|
}
|
|
@ -1,4 +1,10 @@
|
||||||
import {QueryConfig, TimeRange, Namespace, Source} from 'src/types'
|
import {
|
||||||
|
QueryConfig,
|
||||||
|
TimeRange,
|
||||||
|
Namespace,
|
||||||
|
Source,
|
||||||
|
RemoteDataState,
|
||||||
|
} from 'src/types'
|
||||||
|
|
||||||
export interface Filter {
|
export interface Filter {
|
||||||
id: string
|
id: string
|
||||||
|
@ -19,6 +25,7 @@ export interface LogsState {
|
||||||
timeRange: TimeRange
|
timeRange: TimeRange
|
||||||
histogramQueryConfig: QueryConfig | null
|
histogramQueryConfig: QueryConfig | null
|
||||||
histogramData: object[]
|
histogramData: object[]
|
||||||
|
histogramDataStatus: RemoteDataState
|
||||||
tableQueryConfig: QueryConfig | null
|
tableQueryConfig: QueryConfig | null
|
||||||
tableData: TableData
|
tableData: TableData
|
||||||
searchTerm: string | null
|
searchTerm: string | null
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
export default function extentBy<T>(
|
||||||
|
collection: T[],
|
||||||
|
keyFn: (v: any) => number
|
||||||
|
): T[] {
|
||||||
|
let min = Infinity
|
||||||
|
let max = -Infinity
|
||||||
|
let minItem
|
||||||
|
let maxItem
|
||||||
|
|
||||||
|
for (const item of collection) {
|
||||||
|
const val = keyFn(item)
|
||||||
|
|
||||||
|
if (val <= min) {
|
||||||
|
min = val
|
||||||
|
minItem = item
|
||||||
|
}
|
||||||
|
|
||||||
|
if (val >= max) {
|
||||||
|
max = val
|
||||||
|
maxItem = item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [minItem, maxItem]
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
import {parseHistogramQueryResponse} from 'src/logs/utils'
|
||||||
|
|
||||||
|
describe('parseHistogramQueryResponse', () => {
|
||||||
|
test('it parses a nonempty response correctly', () => {
|
||||||
|
const NONEMPTY_RESPONSE = {
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
statement_id: 0,
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'syslog',
|
||||||
|
tags: {severity: 'debug'},
|
||||||
|
columns: ['time', 'count'],
|
||||||
|
values: [[1530129062000, 0], [1530129093000, 0]],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'syslog',
|
||||||
|
tags: {severity: 'err'},
|
||||||
|
columns: ['time', 'count'],
|
||||||
|
values: [[1530129062000, 0], [1530129093000, 0]],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const expected = [
|
||||||
|
{
|
||||||
|
group: 'debug',
|
||||||
|
key: 'debug-0-1530129062000',
|
||||||
|
time: 1530129062000,
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: 'debug',
|
||||||
|
key: 'debug-0-1530129093000',
|
||||||
|
time: 1530129093000,
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: 'err',
|
||||||
|
key: 'err-0-1530129062000',
|
||||||
|
time: 1530129062000,
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: 'err',
|
||||||
|
key: 'err-0-1530129093000',
|
||||||
|
time: 1530129093000,
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const actual = parseHistogramQueryResponse(NONEMPTY_RESPONSE)
|
||||||
|
|
||||||
|
expect(actual).toEqual(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('it parses an empty response correctly', () => {
|
||||||
|
const EMPTY_RESPONSE = {results: [{statement_id: 0}]}
|
||||||
|
const expected = []
|
||||||
|
const actual = parseHistogramQueryResponse(EMPTY_RESPONSE)
|
||||||
|
|
||||||
|
expect(actual).toEqual(expected)
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,105 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {mount, shallow} from 'enzyme'
|
||||||
|
|
||||||
|
import HistogramChart from 'src/shared/components/HistogramChart'
|
||||||
|
import HistogramChartTooltip from 'src/shared/components/HistogramChartTooltip'
|
||||||
|
|
||||||
|
import {RemoteDataState} from 'src/types'
|
||||||
|
|
||||||
|
describe('HistogramChart', () => {
|
||||||
|
test('displays a HistogramChartSkeleton if empty data is passed', () => {
|
||||||
|
const props = {
|
||||||
|
data: [],
|
||||||
|
dataStatus: RemoteDataState.Done,
|
||||||
|
width: 600,
|
||||||
|
height: 400,
|
||||||
|
onZoom: () => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapper = mount(<HistogramChart {...props} />)
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('displays a nothing if passed width and height of 0', () => {
|
||||||
|
const props = {
|
||||||
|
data: [],
|
||||||
|
dataStatus: RemoteDataState.Done,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
onZoom: () => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapper = mount(<HistogramChart {...props} />)
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('displays the visualization with bars if nonempty data is passed', () => {
|
||||||
|
const props = {
|
||||||
|
data: [
|
||||||
|
{key: '0', time: 0, value: 0, group: 'a'},
|
||||||
|
{key: '1', time: 1, value: 1, group: 'a'},
|
||||||
|
{key: '2', time: 2, value: 2, group: 'b'},
|
||||||
|
],
|
||||||
|
dataStatus: RemoteDataState.Done,
|
||||||
|
width: 600,
|
||||||
|
height: 400,
|
||||||
|
onZoom: () => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapper = mount(<HistogramChart {...props} />)
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('displays a HistogramChartTooltip when hovering over bars', () => {
|
||||||
|
const props = {
|
||||||
|
data: [
|
||||||
|
{key: '0', time: 0, value: 0, group: 'a'},
|
||||||
|
{key: '1', time: 1, value: 1, group: 'a'},
|
||||||
|
{key: '2', time: 2, value: 2, group: 'b'},
|
||||||
|
],
|
||||||
|
dataStatus: RemoteDataState.Done,
|
||||||
|
width: 600,
|
||||||
|
height: 400,
|
||||||
|
onZoom: () => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapper = mount(<HistogramChart {...props} />)
|
||||||
|
|
||||||
|
const fakeMouseOverEvent = {
|
||||||
|
target: {
|
||||||
|
dataset: {
|
||||||
|
key: '0',
|
||||||
|
},
|
||||||
|
getBoundingClientRect() {
|
||||||
|
return {top: 10, right: 10, bottom: 5, left: 5}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper
|
||||||
|
.find('.histogram-chart')
|
||||||
|
.first()
|
||||||
|
.simulate('mouseover', fakeMouseOverEvent)
|
||||||
|
|
||||||
|
const tooltip = wrapper.find(HistogramChartTooltip)
|
||||||
|
|
||||||
|
expect(tooltip).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('has a "loading" class if data is reloading', () => {
|
||||||
|
const props = {
|
||||||
|
data: [{key: '', time: 0, value: 0, group: ''}],
|
||||||
|
dataStatus: RemoteDataState.Loading,
|
||||||
|
width: 600,
|
||||||
|
height: 400,
|
||||||
|
onZoom: () => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapper = shallow(<HistogramChart {...props} />)
|
||||||
|
|
||||||
|
expect(wrapper.find('.histogram-chart').hasClass('loading')).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,406 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`HistogramChart displays a HistogramChartSkeleton if empty data is passed 1`] = `
|
||||||
|
<HistogramChart
|
||||||
|
data={Array []}
|
||||||
|
dataStatus="Done"
|
||||||
|
height={400}
|
||||||
|
onZoom={[Function]}
|
||||||
|
width={600}
|
||||||
|
>
|
||||||
|
<HistogramChartSkeleton
|
||||||
|
height={400}
|
||||||
|
margins={
|
||||||
|
Object {
|
||||||
|
"bottom": 20,
|
||||||
|
"left": 11,
|
||||||
|
"right": 0,
|
||||||
|
"top": 5,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
width={600}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="histogram-chart-skeleton"
|
||||||
|
height={400}
|
||||||
|
width={600}
|
||||||
|
>
|
||||||
|
<line
|
||||||
|
className="y-tick"
|
||||||
|
key="0"
|
||||||
|
x1={11}
|
||||||
|
x2={600}
|
||||||
|
y1={380}
|
||||||
|
y2={380}
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
className="y-tick"
|
||||||
|
key="1"
|
||||||
|
x1={11}
|
||||||
|
x2={600}
|
||||||
|
y1={305}
|
||||||
|
y2={305}
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
className="y-tick"
|
||||||
|
key="2"
|
||||||
|
x1={11}
|
||||||
|
x2={600}
|
||||||
|
y1={230}
|
||||||
|
y2={230}
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
className="y-tick"
|
||||||
|
key="3"
|
||||||
|
x1={11}
|
||||||
|
x2={600}
|
||||||
|
y1={155}
|
||||||
|
y2={155}
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
className="y-tick"
|
||||||
|
key="4"
|
||||||
|
x1={11}
|
||||||
|
x2={600}
|
||||||
|
y1={80}
|
||||||
|
y2={80}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</HistogramChartSkeleton>
|
||||||
|
</HistogramChart>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`HistogramChart displays a HistogramChartTooltip when hovering over bars 1`] = `
|
||||||
|
<HistogramChartTooltip
|
||||||
|
anchor="left"
|
||||||
|
datum={
|
||||||
|
Object {
|
||||||
|
"group": "a",
|
||||||
|
"key": "0",
|
||||||
|
"time": 0,
|
||||||
|
"value": 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
x={15}
|
||||||
|
y={7.5}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="histogram-chart-tooltip"
|
||||||
|
data-group="a"
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"left": 15,
|
||||||
|
"position": "fixed",
|
||||||
|
"top": 7.5,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="histogram-chart-tooltip--value"
|
||||||
|
>
|
||||||
|
0
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="histogram-chart-tooltip--group"
|
||||||
|
>
|
||||||
|
a
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</HistogramChartTooltip>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`HistogramChart displays a nothing if passed width and height of 0 1`] = `
|
||||||
|
<HistogramChart
|
||||||
|
data={Array []}
|
||||||
|
dataStatus="Done"
|
||||||
|
height={0}
|
||||||
|
onZoom={[Function]}
|
||||||
|
width={0}
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`HistogramChart displays the visualization with bars if nonempty data is passed 1`] = `
|
||||||
|
<HistogramChart
|
||||||
|
data={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"group": "a",
|
||||||
|
"key": "0",
|
||||||
|
"time": 0,
|
||||||
|
"value": 0,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"group": "a",
|
||||||
|
"key": "1",
|
||||||
|
"time": 1,
|
||||||
|
"value": 1,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"group": "b",
|
||||||
|
"key": "2",
|
||||||
|
"time": 2,
|
||||||
|
"value": 2,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
dataStatus="Done"
|
||||||
|
height={400}
|
||||||
|
onZoom={[Function]}
|
||||||
|
width={600}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="histogram-chart "
|
||||||
|
height={400}
|
||||||
|
onMouseOut={[Function]}
|
||||||
|
onMouseOver={[Function]}
|
||||||
|
width={600}
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<clipPath
|
||||||
|
id="histogram-chart--bars-clip"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
height={375}
|
||||||
|
width={575}
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<g
|
||||||
|
className="histogram-chart--axes"
|
||||||
|
>
|
||||||
|
<HistogramChartAxes
|
||||||
|
height={400}
|
||||||
|
margins={
|
||||||
|
Object {
|
||||||
|
"bottom": 20,
|
||||||
|
"left": 25,
|
||||||
|
"right": 0,
|
||||||
|
"top": 5,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
width={600}
|
||||||
|
xScale={[Function]}
|
||||||
|
yScale={[Function]}
|
||||||
|
>
|
||||||
|
<line
|
||||||
|
className="y-tick"
|
||||||
|
key="0-25-625-380"
|
||||||
|
x1={25}
|
||||||
|
x2={625}
|
||||||
|
y1={380}
|
||||||
|
y2={380}
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
className="y-tick"
|
||||||
|
key="0.5-25-625-301.875"
|
||||||
|
x1={25}
|
||||||
|
x2={625}
|
||||||
|
y1={301.875}
|
||||||
|
y2={301.875}
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
className="y-tick"
|
||||||
|
key="1-25-625-223.75"
|
||||||
|
x1={25}
|
||||||
|
x2={625}
|
||||||
|
y1={223.75}
|
||||||
|
y2={223.75}
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
className="y-tick"
|
||||||
|
key="1.5-25-625-145.625"
|
||||||
|
x1={25}
|
||||||
|
x2={625}
|
||||||
|
y1={145.625}
|
||||||
|
y2={145.625}
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
className="y-tick"
|
||||||
|
key="2-25-625-67.5"
|
||||||
|
x1={25}
|
||||||
|
x2={625}
|
||||||
|
y1={67.5}
|
||||||
|
y2={67.5}
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
className="y-label"
|
||||||
|
key="0-25-625-380"
|
||||||
|
x={20}
|
||||||
|
y={380}
|
||||||
|
>
|
||||||
|
0
|
||||||
|
</text>
|
||||||
|
<text
|
||||||
|
className="y-label"
|
||||||
|
key="0.5-25-625-301.875"
|
||||||
|
x={20}
|
||||||
|
y={301.875}
|
||||||
|
>
|
||||||
|
0.5
|
||||||
|
</text>
|
||||||
|
<text
|
||||||
|
className="y-label"
|
||||||
|
key="1-25-625-223.75"
|
||||||
|
x={20}
|
||||||
|
y={223.75}
|
||||||
|
>
|
||||||
|
1
|
||||||
|
</text>
|
||||||
|
<text
|
||||||
|
className="y-label"
|
||||||
|
key="1.5-25-625-145.625"
|
||||||
|
x={20}
|
||||||
|
y={145.625}
|
||||||
|
>
|
||||||
|
1.5
|
||||||
|
</text>
|
||||||
|
<text
|
||||||
|
className="y-label"
|
||||||
|
key="2-25-625-67.5"
|
||||||
|
x={20}
|
||||||
|
y={67.5}
|
||||||
|
>
|
||||||
|
2
|
||||||
|
</text>
|
||||||
|
<text
|
||||||
|
className="x-label"
|
||||||
|
key=".001-287.5-388"
|
||||||
|
x={287.5}
|
||||||
|
y={388}
|
||||||
|
>
|
||||||
|
.001
|
||||||
|
</text>
|
||||||
|
<text
|
||||||
|
className="x-label"
|
||||||
|
key=".002-575-388"
|
||||||
|
x={575}
|
||||||
|
y={388}
|
||||||
|
>
|
||||||
|
.002
|
||||||
|
</text>
|
||||||
|
</HistogramChartAxes>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
className="histogram-chart--brush"
|
||||||
|
transform="translate(25, 5)"
|
||||||
|
>
|
||||||
|
<XBrush
|
||||||
|
height={375}
|
||||||
|
onBrush={[Function]}
|
||||||
|
width={575}
|
||||||
|
xScale={[Function]}
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
className="x-brush--area"
|
||||||
|
height={375}
|
||||||
|
onDoubleClick={[Function]}
|
||||||
|
onMouseDown={[Function]}
|
||||||
|
width={575}
|
||||||
|
/>
|
||||||
|
</XBrush>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
className="histogram-chart--bars"
|
||||||
|
clipPath="url(#histogram-chart--bars-clip)"
|
||||||
|
transform="translate(25, 5)"
|
||||||
|
>
|
||||||
|
<HistogramChartBars
|
||||||
|
data={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"group": "a",
|
||||||
|
"key": "0",
|
||||||
|
"time": 0,
|
||||||
|
"value": 0,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"group": "a",
|
||||||
|
"key": "1",
|
||||||
|
"time": 1,
|
||||||
|
"value": 1,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"group": "b",
|
||||||
|
"key": "2",
|
||||||
|
"time": 2,
|
||||||
|
"value": 2,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
height={375}
|
||||||
|
width={575}
|
||||||
|
xScale={[Function]}
|
||||||
|
yScale={[Function]}
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
className="histogram-chart-bars--bars"
|
||||||
|
key="1-1-193.5"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<clipPath
|
||||||
|
id="histogram-chart-bars--clip-1-1-193.5"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
height={160.25}
|
||||||
|
rx={4}
|
||||||
|
ry={4}
|
||||||
|
width={188}
|
||||||
|
x={193.5}
|
||||||
|
y={218.75}
|
||||||
|
/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<rect
|
||||||
|
className="histogram-chart-bars--bar"
|
||||||
|
clipPath="url(#histogram-chart-bars--clip-1-1-193.5)"
|
||||||
|
data-group="a"
|
||||||
|
data-key="1"
|
||||||
|
height={156.25}
|
||||||
|
key="1"
|
||||||
|
width={188}
|
||||||
|
x={193.5}
|
||||||
|
y={218.75}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
className="histogram-chart-bars--bars"
|
||||||
|
key="2-2-481"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<clipPath
|
||||||
|
id="histogram-chart-bars--clip-2-2-481"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
height={316.5}
|
||||||
|
rx={4}
|
||||||
|
ry={4}
|
||||||
|
width={188}
|
||||||
|
x={481}
|
||||||
|
y={62.5}
|
||||||
|
/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<rect
|
||||||
|
className="histogram-chart-bars--bar"
|
||||||
|
clipPath="url(#histogram-chart-bars--clip-2-2-481)"
|
||||||
|
data-group="b"
|
||||||
|
data-key="2"
|
||||||
|
height={312.5}
|
||||||
|
key="2"
|
||||||
|
width={188}
|
||||||
|
x={481}
|
||||||
|
y={62.5}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</HistogramChartBars>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<HistogramChartTooltip
|
||||||
|
anchor="left"
|
||||||
|
x={-1}
|
||||||
|
y={-1}
|
||||||
|
/>
|
||||||
|
</HistogramChart>
|
||||||
|
`;
|
|
@ -4,7 +4,7 @@ import {shallow} from 'enzyme'
|
||||||
import TemplateControlBar from 'src/tempVars/components/TemplateControlBar'
|
import TemplateControlBar from 'src/tempVars/components/TemplateControlBar'
|
||||||
import TemplateControlDropdown from 'src/tempVars/components/TemplateControlDropdown'
|
import TemplateControlDropdown from 'src/tempVars/components/TemplateControlDropdown'
|
||||||
import TemplateVariableEditor from 'src/tempVars/components/TemplateVariableEditor'
|
import TemplateVariableEditor from 'src/tempVars/components/TemplateVariableEditor'
|
||||||
import SimpleOverlayTechnology from 'src/shared/components/SimpleOverlayTechnology'
|
import OverlayTechnology from 'src/reusable_ui/components/overlays/OverlayTechnology'
|
||||||
import {source} from 'test/resources'
|
import {source} from 'test/resources'
|
||||||
|
|
||||||
import {TemplateType, TemplateValueType} from 'src/types'
|
import {TemplateType, TemplateValueType} from 'src/types'
|
||||||
|
@ -63,14 +63,24 @@ describe('TemplateControlBar', () => {
|
||||||
|
|
||||||
it('renders an TemplateVariableEditor overlay when adding a template variable', () => {
|
it('renders an TemplateVariableEditor overlay when adding a template variable', () => {
|
||||||
const props = {...defaultProps}
|
const props = {...defaultProps}
|
||||||
const wrapper = shallow(<TemplateControlBar {...props} />)
|
const wrapper = shallow(<TemplateControlBar {...props} />, {
|
||||||
|
context: {
|
||||||
|
store: {},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
expect(wrapper.find(SimpleOverlayTechnology)).toHaveLength(0)
|
const children = wrapper
|
||||||
|
.find(OverlayTechnology)
|
||||||
|
.dive()
|
||||||
|
.find("[data-test='overlay-children']")
|
||||||
|
.children()
|
||||||
|
|
||||||
|
expect(children).toHaveLength(0)
|
||||||
|
|
||||||
wrapper.find('[data-test="add-template-variable"]').simulate('click')
|
wrapper.find('[data-test="add-template-variable"]').simulate('click')
|
||||||
|
|
||||||
const elements = wrapper
|
const elements = wrapper
|
||||||
.find(SimpleOverlayTechnology)
|
.find(OverlayTechnology)
|
||||||
.dive()
|
.dive()
|
||||||
.find(TemplateVariableEditor)
|
.find(TemplateVariableEditor)
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {shallow} from 'enzyme'
|
import {shallow} from 'enzyme'
|
||||||
|
|
||||||
import SimpleOverlayTechnology from 'src/shared/components/SimpleOverlayTechnology'
|
import OverlayTechnology from 'src/reusable_ui/components/overlays/OverlayTechnology'
|
||||||
import TemplateVariableEditor from 'src/tempVars/components/TemplateVariableEditor'
|
import TemplateVariableEditor from 'src/tempVars/components/TemplateVariableEditor'
|
||||||
import TemplateControlDropdown from 'src/tempVars/components/TemplateControlDropdown'
|
import TemplateControlDropdown from 'src/tempVars/components/TemplateControlDropdown'
|
||||||
import {source} from 'test/resources'
|
import {source} from 'test/resources'
|
||||||
|
@ -34,14 +34,24 @@ const defaultProps = {
|
||||||
|
|
||||||
describe('TemplateControlDropdown', () => {
|
describe('TemplateControlDropdown', () => {
|
||||||
it('should show a TemplateVariableEditor overlay when the settings icon is clicked', () => {
|
it('should show a TemplateVariableEditor overlay when the settings icon is clicked', () => {
|
||||||
const wrapper = shallow(<TemplateControlDropdown {...defaultProps} />)
|
const wrapper = shallow(<TemplateControlDropdown {...defaultProps} />, {
|
||||||
|
context: {
|
||||||
|
store: {},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
expect(wrapper.find(SimpleOverlayTechnology)).toHaveLength(0)
|
const children = wrapper
|
||||||
|
.find(OverlayTechnology)
|
||||||
|
.dive()
|
||||||
|
.find("[data-test='overlay-children']")
|
||||||
|
.children()
|
||||||
|
|
||||||
|
expect(children).toHaveLength(0)
|
||||||
|
|
||||||
wrapper.find("[data-test='edit']").simulate('click')
|
wrapper.find("[data-test='edit']").simulate('click')
|
||||||
|
|
||||||
const elements = wrapper
|
const elements = wrapper
|
||||||
.find(SimpleOverlayTechnology)
|
.find(OverlayTechnology)
|
||||||
.dive()
|
.dive()
|
||||||
.find(TemplateVariableEditor)
|
.find(TemplateVariableEditor)
|
||||||
|
|
||||||
|
|
59
ui/yarn.lock
59
ui/yarn.lock
|
@ -32,6 +32,16 @@
|
||||||
version "0.0.56"
|
version "0.0.56"
|
||||||
resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-0.0.56.tgz#1fcf68df0d0a49791d843dadda7d94891ac88669"
|
resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-0.0.56.tgz#1fcf68df0d0a49791d843dadda7d94891ac88669"
|
||||||
|
|
||||||
|
"@types/d3-scale@^2.0.1":
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-2.0.1.tgz#f94cd991c50422b2e68d8f43be3f9fffdb1ae7be"
|
||||||
|
dependencies:
|
||||||
|
"@types/d3-time" "*"
|
||||||
|
|
||||||
|
"@types/d3-time@*":
|
||||||
|
version "1.0.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-1.0.8.tgz#6c083127b330b3c2fc65cd0f3a6e9cbd9607b28c"
|
||||||
|
|
||||||
"@types/dygraphs@^1.1.6":
|
"@types/dygraphs@^1.1.6":
|
||||||
version "1.1.6"
|
version "1.1.6"
|
||||||
resolved "https://registry.yarnpkg.com/@types/dygraphs/-/dygraphs-1.1.6.tgz#20ff1a01e353e813ff97898c0fee5defc66626be"
|
resolved "https://registry.yarnpkg.com/@types/dygraphs/-/dygraphs-1.1.6.tgz#20ff1a01e353e813ff97898c0fee5defc66626be"
|
||||||
|
@ -2606,6 +2616,49 @@ cyclist@~0.2.2:
|
||||||
version "0.2.2"
|
version "0.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
|
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
|
||||||
|
|
||||||
|
d3-array@^1.2.0:
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.1.tgz#d1ca33de2f6ac31efadb8e050a021d7e2396d5dc"
|
||||||
|
|
||||||
|
d3-collection@1:
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.4.tgz#342dfd12837c90974f33f1cc0a785aea570dcdc2"
|
||||||
|
|
||||||
|
d3-color@1:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.2.0.tgz#d1ea19db5859c86854586276ec892cf93148459a"
|
||||||
|
|
||||||
|
d3-format@1:
|
||||||
|
version "1.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.3.0.tgz#a3ac44269a2011cdb87c7b5693040c18cddfff11"
|
||||||
|
|
||||||
|
d3-interpolate@1:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.2.0.tgz#40d81bd8e959ff021c5ea7545bc79b8d22331c41"
|
||||||
|
dependencies:
|
||||||
|
d3-color "1"
|
||||||
|
|
||||||
|
d3-scale@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-2.1.0.tgz#8d3fd3e2a7c9080782a523c08507c5248289eef8"
|
||||||
|
dependencies:
|
||||||
|
d3-array "^1.2.0"
|
||||||
|
d3-collection "1"
|
||||||
|
d3-format "1"
|
||||||
|
d3-interpolate "1"
|
||||||
|
d3-time "1"
|
||||||
|
d3-time-format "2"
|
||||||
|
|
||||||
|
d3-time-format@2:
|
||||||
|
version "2.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.1.1.tgz#85b7cdfbc9ffca187f14d3c456ffda268081bb31"
|
||||||
|
dependencies:
|
||||||
|
d3-time "1"
|
||||||
|
|
||||||
|
d3-time@1:
|
||||||
|
version "1.0.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.0.8.tgz#dbd2d6007bf416fe67a76d17947b784bffea1e84"
|
||||||
|
|
||||||
d@1:
|
d@1:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f"
|
resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f"
|
||||||
|
@ -3024,6 +3077,12 @@ enzyme-adapter-utils@^1.3.0:
|
||||||
object.assign "^4.0.4"
|
object.assign "^4.0.4"
|
||||||
prop-types "^15.6.0"
|
prop-types "^15.6.0"
|
||||||
|
|
||||||
|
enzyme-to-json@^3.3.4:
|
||||||
|
version "3.3.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/enzyme-to-json/-/enzyme-to-json-3.3.4.tgz#67c6040e931182f183418af2eb9f4323258aa77f"
|
||||||
|
dependencies:
|
||||||
|
lodash "^4.17.4"
|
||||||
|
|
||||||
enzyme@^3.3.0:
|
enzyme@^3.3.0:
|
||||||
version "3.3.0"
|
version "3.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.3.0.tgz#0971abd167f2d4bf3f5bd508229e1c4b6dc50479"
|
resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.3.0.tgz#0971abd167f2d4bf3f5bd508229e1c4b6dc50479"
|
||||||
|
|
Loading…
Reference in New Issue