Merge branch 'master' into tempvars/select-default
commit
54aa7c694b
|
@ -16,6 +16,7 @@ module.exports = {
|
|||
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||
transformIgnorePatterns: ['/node_modules/(?!dygraphs)'],
|
||||
snapshotSerializers: ['enzyme-to-json/serializer'],
|
||||
},
|
||||
{
|
||||
runner: 'jest-runner-eslint',
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
"@types/chai": "^4.1.2",
|
||||
"@types/chroma-js": "^1.3.4",
|
||||
"@types/codemirror": "^0.0.56",
|
||||
"@types/d3-scale": "^2.0.1",
|
||||
"@types/dygraphs": "^1.1.6",
|
||||
"@types/enzyme": "^3.1.9",
|
||||
"@types/jest": "^22.1.4",
|
||||
|
@ -72,6 +73,7 @@
|
|||
"css-loader": "^0.28.11",
|
||||
"envify": "^3.4.0",
|
||||
"enzyme": "^3.3.0",
|
||||
"enzyme-to-json": "^3.3.4",
|
||||
"eslint": "^3.14.1",
|
||||
"eslint-config-prettier": "^2.9.0",
|
||||
"eslint-loader": "^2.0.0",
|
||||
|
@ -127,6 +129,7 @@
|
|||
"chroma-js": "^1.3.6",
|
||||
"classnames": "^2.2.3",
|
||||
"codemirror": "^5.36.0",
|
||||
"d3-scale": "^2.1.0",
|
||||
"dygraphs": "2.1.0",
|
||||
"enzyme-adapter-react-16": "^1.1.1",
|
||||
"eslint-plugin-babel": "^4.1.2",
|
||||
|
|
|
@ -2,7 +2,6 @@ import React, {SFC, ReactChildren} from 'react'
|
|||
|
||||
import SideNav from 'src/side_nav'
|
||||
import Notifications from 'src/shared/components/Notifications'
|
||||
import Overlay from 'src/shared/components/OverlayTechnology'
|
||||
|
||||
interface Props {
|
||||
children: ReactChildren
|
||||
|
@ -10,7 +9,6 @@ interface Props {
|
|||
|
||||
const App: SFC<Props> = ({children}) => (
|
||||
<div className="chronograf-root">
|
||||
<Overlay />
|
||||
<Notifications />
|
||||
<SideNav />
|
||||
{children}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import React, {Component, MouseEvent} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
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 FancyScrollbar from 'src/shared/components/FancyScrollbar'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {
|
||||
showOverlay as showOverlayAction,
|
||||
ShowOverlayActionCreator,
|
||||
} from 'src/shared/actions/overlayTechnology'
|
||||
import {OverlayContext} from 'src/shared/components/OverlayTechnology'
|
||||
import OverlayTechnology from 'src/reusable_ui/components/overlays/OverlayTechnology'
|
||||
|
||||
import {Dashboard} from 'src/types'
|
||||
import {Notification} from 'src/types/notifications'
|
||||
|
@ -27,12 +22,12 @@ interface Props {
|
|||
onExportDashboard: (dashboard: Dashboard) => () => void
|
||||
onImportDashboard: (dashboard: Dashboard) => void
|
||||
notify: (message: Notification) => void
|
||||
showOverlay: ShowOverlayActionCreator
|
||||
dashboardLink: string
|
||||
}
|
||||
|
||||
interface State {
|
||||
searchTerm: string
|
||||
isOverlayVisible: boolean
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
|
@ -42,6 +37,7 @@ class DashboardsPageContents extends Component<Props, State> {
|
|||
|
||||
this.state = {
|
||||
searchTerm: '',
|
||||
isOverlayVisible: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -83,31 +79,34 @@ class DashboardsPageContents extends Component<Props, State> {
|
|||
const {onCreateDashboard} = this.props
|
||||
|
||||
return (
|
||||
<div className="panel-heading">
|
||||
<h2 className="panel-title">{this.panelTitle}</h2>
|
||||
<div className="panel-controls">
|
||||
<SearchBar
|
||||
placeholder="Filter by Name..."
|
||||
onSearch={this.filterDashboards}
|
||||
/>
|
||||
<Authorized requiredRole={EDITOR_ROLE}>
|
||||
<>
|
||||
<button
|
||||
className="btn btn-sm btn-default"
|
||||
onClick={this.showImportOverlay}
|
||||
>
|
||||
<span className="icon import" /> Import Dashboard
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={onCreateDashboard}
|
||||
>
|
||||
<span className="icon plus" /> Create Dashboard
|
||||
</button>
|
||||
</>
|
||||
</Authorized>
|
||||
<>
|
||||
<div className="panel-heading">
|
||||
<h2 className="panel-title">{this.panelTitle}</h2>
|
||||
<div className="panel-controls">
|
||||
<SearchBar
|
||||
placeholder="Filter by Name..."
|
||||
onSearch={this.filterDashboards}
|
||||
/>
|
||||
<Authorized requiredRole={EDITOR_ROLE}>
|
||||
<>
|
||||
<button
|
||||
className="btn btn-sm btn-default"
|
||||
onClick={this.handleToggleOverlay}
|
||||
>
|
||||
<span className="icon import" /> Import Dashboard
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={onCreateDashboard}
|
||||
>
|
||||
<span className="icon plus" /> Create Dashboard
|
||||
</button>
|
||||
</>
|
||||
</Authorized>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{this.renderImportOverlay}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -136,30 +135,24 @@ class DashboardsPageContents extends Component<Props, State> {
|
|||
this.setState({searchTerm})
|
||||
}
|
||||
|
||||
private showImportOverlay = (): void => {
|
||||
const {showOverlay, onImportDashboard, notify} = this.props
|
||||
const options = {
|
||||
dismissOnClickOutside: false,
|
||||
dismissOnEscape: false,
|
||||
}
|
||||
private handleToggleOverlay = (): void => {
|
||||
this.setState({isOverlayVisible: !this.state.isOverlayVisible})
|
||||
}
|
||||
|
||||
showOverlay(
|
||||
<OverlayContext.Consumer>
|
||||
{({onDismissOverlay}) => (
|
||||
<ImportDashboardOverlay
|
||||
onDismissOverlay={onDismissOverlay}
|
||||
onImportDashboard={onImportDashboard}
|
||||
notify={notify}
|
||||
/>
|
||||
)}
|
||||
</OverlayContext.Consumer>,
|
||||
options
|
||||
private get renderImportOverlay(): JSX.Element {
|
||||
const {onImportDashboard, notify} = this.props
|
||||
const {isOverlayVisible} = this.state
|
||||
|
||||
return (
|
||||
<OverlayTechnology visible={isOverlayVisible}>
|
||||
<ImportDashboardOverlay
|
||||
onDismissOverlay={this.handleToggleOverlay}
|
||||
onImportDashboard={onImportDashboard}
|
||||
notify={notify}
|
||||
/>
|
||||
</OverlayTechnology>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
showOverlay: showOverlayAction,
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(DashboardsPageContents)
|
||||
export default DashboardsPageContents
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
import Container from 'src/shared/components/overlay/OverlayContainer'
|
||||
import Heading from 'src/shared/components/overlay/OverlayHeading'
|
||||
import Body from 'src/shared/components/overlay/OverlayBody'
|
||||
import Container from 'src/reusable_ui/components/overlays/OverlayContainer'
|
||||
import Heading from 'src/reusable_ui/components/overlays/OverlayHeading'
|
||||
import Body from 'src/reusable_ui/components/overlays/OverlayBody'
|
||||
import DragAndDrop from 'src/shared/components/DragAndDrop'
|
||||
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 WriteDataForm from 'src/data_explorer/components/WriteDataForm'
|
||||
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 AutoRefreshDropdown from 'src/shared/components/AutoRefreshDropdown'
|
||||
import TimeRangeDropdown from 'src/shared/components/TimeRangeDropdown'
|
||||
|
@ -49,7 +49,7 @@ interface Props {
|
|||
}
|
||||
|
||||
interface State {
|
||||
showWriteForm: boolean
|
||||
isWriteFormVisible: boolean
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
|
@ -58,7 +58,7 @@ export class DataExplorer extends PureComponent<Props, State> {
|
|||
super(props)
|
||||
|
||||
this.state = {
|
||||
showWriteForm: false,
|
||||
isWriteFormVisible: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -101,21 +101,19 @@ export class DataExplorer extends PureComponent<Props, State> {
|
|||
queryConfigActions,
|
||||
} = this.props
|
||||
|
||||
const {showWriteForm} = this.state
|
||||
const {isWriteFormVisible} = this.state
|
||||
|
||||
return (
|
||||
<>
|
||||
{showWriteForm ? (
|
||||
<OverlayTechnologies>
|
||||
<WriteDataForm
|
||||
source={source}
|
||||
errorThrown={errorThrownAction}
|
||||
selectedDatabase={this.selectedDatabase}
|
||||
onClose={this.handleCloseWriteData}
|
||||
writeLineProtocol={writeLineProtocol}
|
||||
/>
|
||||
</OverlayTechnologies>
|
||||
) : null}
|
||||
<OverlayTechnology visible={isWriteFormVisible}>
|
||||
<WriteDataForm
|
||||
source={source}
|
||||
errorThrown={errorThrownAction}
|
||||
selectedDatabase={this.selectedDatabase}
|
||||
onClose={this.handleCloseWriteData}
|
||||
writeLineProtocol={writeLineProtocol}
|
||||
/>
|
||||
</OverlayTechnology>
|
||||
<PageHeader
|
||||
titleText="Data Explorer"
|
||||
fullWidth={true}
|
||||
|
@ -154,11 +152,11 @@ export class DataExplorer extends PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
private handleCloseWriteData = (): void => {
|
||||
this.setState({showWriteForm: false})
|
||||
this.setState({isWriteFormVisible: false})
|
||||
}
|
||||
|
||||
private handleOpenWriteData = (): void => {
|
||||
this.setState({showWriteForm: true})
|
||||
this.setState({isWriteFormVisible: true})
|
||||
}
|
||||
|
||||
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 {connect} from 'react-redux'
|
||||
|
||||
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 {
|
||||
showOverlay,
|
||||
ShowOverlayActionCreator,
|
||||
} from 'src/shared/actions/overlayTechnology'
|
||||
|
||||
import {Service} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
showOverlay: ShowOverlayActionCreator
|
||||
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() {
|
||||
return (
|
||||
<PageHeader
|
||||
titleText="Flux Editor"
|
||||
fullWidth={true}
|
||||
optionsComponents={this.optionsComponents}
|
||||
/>
|
||||
<>
|
||||
<PageHeader
|
||||
titleText="Flux Editor"
|
||||
fullWidth={true}
|
||||
optionsComponents={this.optionsComponents}
|
||||
/>
|
||||
{this.overlay}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
private handleToggleOverlay = (): void => {
|
||||
this.setState({isOverlayVisible: !this.state.isOverlayVisible})
|
||||
}
|
||||
|
||||
private get optionsComponents(): JSX.Element {
|
||||
return (
|
||||
<button onClick={this.overlay} className="btn btn-sm btn-default">
|
||||
<button
|
||||
onClick={this.handleToggleOverlay}
|
||||
className="btn btn-sm btn-default"
|
||||
>
|
||||
Edit Connection
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
private overlay = () => {
|
||||
private get overlay(): JSX.Element {
|
||||
const {service} = this.props
|
||||
const {isOverlayVisible} = this.state
|
||||
|
||||
this.props.showOverlay(
|
||||
<OverlayContext.Consumer>
|
||||
{({onDismissOverlay}) => (
|
||||
<FluxOverlay
|
||||
mode="edit"
|
||||
service={service}
|
||||
onDismiss={onDismissOverlay}
|
||||
/>
|
||||
)}
|
||||
</OverlayContext.Consumer>,
|
||||
{}
|
||||
return (
|
||||
<OverlayTechnology visible={isOverlayVisible}>
|
||||
<FluxOverlay
|
||||
mode="edit"
|
||||
service={service}
|
||||
onDismiss={this.handleToggleOverlay}
|
||||
/>
|
||||
</OverlayTechnology>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const mdtp = {
|
||||
showOverlay,
|
||||
}
|
||||
|
||||
export default connect(null, mdtp)(FluxHeader)
|
||||
export default FluxHeader
|
||||
|
|
|
@ -3,8 +3,9 @@ import {connect} from 'react-redux'
|
|||
import {WithRouterProps} from 'react-router'
|
||||
|
||||
import {FluxPage} from 'src/flux'
|
||||
import EmptyFluxPage from 'src/flux/components/EmptyFluxPage'
|
||||
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 {Links} from 'src/types/flux'
|
||||
import {notify as notifyAction} from 'src/shared/actions/notifications'
|
||||
|
@ -12,26 +13,37 @@ import {
|
|||
updateScript as updateScriptAction,
|
||||
UpdateScript,
|
||||
} from 'src/flux/actions'
|
||||
import * as a from 'src/shared/actions/overlayTechnology'
|
||||
import * as b from 'src/shared/actions/services'
|
||||
import * as actions from 'src/shared/actions/services'
|
||||
|
||||
export const NotificationContext = React.createContext()
|
||||
|
||||
const actions = {...a, ...b}
|
||||
|
||||
interface Props {
|
||||
sources: Source[]
|
||||
services: Service[]
|
||||
children: ReactChildren
|
||||
showOverlay: a.ShowOverlayActionCreator
|
||||
fetchServicesAsync: b.FetchServicesAsync
|
||||
fetchServicesAsync: actions.FetchServicesAsync
|
||||
notify: (message: Notification) => void
|
||||
updateScript: UpdateScript
|
||||
script: string
|
||||
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() {
|
||||
const source = this.props.sources.find(
|
||||
s => s.id === this.props.params.sourceID
|
||||
|
@ -44,7 +56,7 @@ export class CheckServices extends PureComponent<Props & WithRouterProps> {
|
|||
await this.props.fetchServicesAsync(source)
|
||||
|
||||
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
|
||||
|
||||
if (!this.props.services.length) {
|
||||
return null // put loading spinner here
|
||||
return (
|
||||
<EmptyFluxPage
|
||||
onShowOverlay={this.handleShowOverlay}
|
||||
overlay={this.renderOverlay}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -65,6 +82,7 @@ export class CheckServices extends PureComponent<Props & WithRouterProps> {
|
|||
notify={notify}
|
||||
updateScript={updateScript}
|
||||
/>
|
||||
{this.renderOverlay}
|
||||
</NotificationContext.Provider>
|
||||
)
|
||||
}
|
||||
|
@ -75,31 +93,31 @@ export class CheckServices extends PureComponent<Props & WithRouterProps> {
|
|||
return sources.find(s => s.id === params.sourceID)
|
||||
}
|
||||
|
||||
private overlay() {
|
||||
const {showOverlay, services} = this.props
|
||||
private get renderOverlay(): JSX.Element {
|
||||
const {isOverlayShown} = this.state
|
||||
|
||||
if (services.length) {
|
||||
return
|
||||
}
|
||||
|
||||
showOverlay(
|
||||
<OverlayContext.Consumer>
|
||||
{({onDismissOverlay}) => (
|
||||
<FluxOverlay
|
||||
mode="new"
|
||||
source={this.source}
|
||||
onDismiss={onDismissOverlay}
|
||||
/>
|
||||
)}
|
||||
</OverlayContext.Consumer>,
|
||||
{}
|
||||
return (
|
||||
<OverlayTechnology visible={isOverlayShown}>
|
||||
<FluxOverlay
|
||||
mode="new"
|
||||
source={this.source}
|
||||
onDismiss={this.handleDismissOverlay}
|
||||
/>
|
||||
</OverlayTechnology>
|
||||
)
|
||||
}
|
||||
|
||||
private handleShowOverlay = (): void => {
|
||||
this.setState({isOverlayShown: true})
|
||||
}
|
||||
|
||||
private handleDismissOverlay = (): void => {
|
||||
this.setState({isOverlayShown: false})
|
||||
}
|
||||
}
|
||||
|
||||
const mdtp = {
|
||||
fetchServicesAsync: actions.fetchServicesAsync,
|
||||
showOverlay: actions.showOverlay,
|
||||
updateScript: updateScriptAction,
|
||||
notify: notifyAction,
|
||||
}
|
||||
|
|
|
@ -1,15 +1,21 @@
|
|||
import moment from 'moment'
|
||||
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 {getDatabasesWithRetentionPolicies} from 'src/shared/apis/databases'
|
||||
import {
|
||||
buildHistogramQueryConfig,
|
||||
buildTableQueryConfig,
|
||||
buildLogQuery,
|
||||
parseHistogramQueryResponse,
|
||||
} from 'src/logs/utils'
|
||||
import {getDeep} from 'src/utils/wrappers'
|
||||
import buildQuery from 'src/utils/influxql'
|
||||
import {executeQueryAsync} from 'src/logs/api'
|
||||
import {LogsState, Filter, TableData} from 'src/types/logs'
|
||||
|
||||
|
@ -41,6 +47,7 @@ export enum ActionTypes {
|
|||
SetNamespace = 'LOGS_SET_NAMESPACE',
|
||||
SetHistogramQueryConfig = 'LOGS_SET_HISTOGRAM_QUERY_CONFIG',
|
||||
SetHistogramData = 'LOGS_SET_HISTOGRAM_DATA',
|
||||
SetHistogramDataStatus = 'LOGS_SET_HISTOGRAM_DATA_STATUS',
|
||||
SetTableQueryConfig = 'LOGS_SET_TABLE_QUERY_CONFIG',
|
||||
SetTableData = 'LOGS_SET_TABLE_DATA',
|
||||
ChangeZoom = 'LOGS_CHANGE_ZOOM',
|
||||
|
@ -132,6 +139,11 @@ interface SetHistogramData {
|
|||
}
|
||||
}
|
||||
|
||||
interface SetHistogramDataStatus {
|
||||
type: ActionTypes.SetHistogramDataStatus
|
||||
payload: RemoteDataState
|
||||
}
|
||||
|
||||
interface SetTableQueryConfig {
|
||||
type: ActionTypes.SetTableQueryConfig
|
||||
payload: {
|
||||
|
@ -156,7 +168,6 @@ interface SetSearchTerm {
|
|||
interface ChangeZoomAction {
|
||||
type: ActionTypes.ChangeZoom
|
||||
payload: {
|
||||
data: object[]
|
||||
timeRange: TimeRange
|
||||
}
|
||||
}
|
||||
|
@ -168,6 +179,7 @@ export type Action =
|
|||
| SetNamespaceAction
|
||||
| SetHistogramQueryConfig
|
||||
| SetHistogramData
|
||||
| SetHistogramDataStatus
|
||||
| ChangeZoomAction
|
||||
| SetTableData
|
||||
| SetTableQueryConfig
|
||||
|
@ -220,9 +232,16 @@ export const removeFilter = (id: string): RemoveFilterAction => ({
|
|||
payload: {id},
|
||||
})
|
||||
|
||||
const setHistogramData = (response): SetHistogramData => ({
|
||||
const setHistogramData = (data): SetHistogramData => ({
|
||||
type: ActionTypes.SetHistogramData,
|
||||
payload: {data: [{response}]},
|
||||
payload: {data},
|
||||
})
|
||||
|
||||
const setHistogramDataStatus = (
|
||||
status: RemoteDataState
|
||||
): SetHistogramDataStatus => ({
|
||||
type: ActionTypes.SetHistogramDataStatus,
|
||||
payload: status,
|
||||
})
|
||||
|
||||
export const executeHistogramQueryAsync = () => async (
|
||||
|
@ -240,9 +259,18 @@ export const executeHistogramQueryAsync = () => async (
|
|||
|
||||
if (_.every([queryConfig, timeRange, namespace, proxyLink])) {
|
||||
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
|
||||
): Promise<void> => {
|
||||
const state = getState()
|
||||
|
||||
const namespace = getNamespace(state)
|
||||
const proxyLink = getProxyLink(state)
|
||||
|
||||
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(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 uuid from 'uuid'
|
||||
import _ from 'lodash'
|
||||
import {connect} from 'react-redux'
|
||||
import {AutoSizer} from 'react-virtualized'
|
||||
|
||||
import {
|
||||
getSourceAndPopulateNamespacesAsync,
|
||||
setTimeRangeAsync,
|
||||
|
@ -15,15 +18,16 @@ import {
|
|||
} from 'src/logs/actions'
|
||||
import {getSourcesAsync} from 'src/shared/actions/sources'
|
||||
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 FilterBar from 'src/logs/components/LogsFilterBar'
|
||||
import LogViewerChart from 'src/logs/components/LogViewerChart'
|
||||
import LogsTable from 'src/logs/components/LogsTable'
|
||||
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 {HistogramData, TimePeriod} from 'src/types/histogram'
|
||||
|
||||
interface Props {
|
||||
sources: Source[]
|
||||
|
@ -42,7 +46,8 @@ interface Props {
|
|||
removeFilter: (id: string) => void
|
||||
changeFilter: (id: string, operator: string, value: string) => void
|
||||
timeRange: TimeRange
|
||||
histogramData: object[]
|
||||
histogramData: HistogramData
|
||||
histogramDataStatus: RemoteDataState
|
||||
tableData: {
|
||||
columns: string[]
|
||||
values: string[]
|
||||
|
@ -97,7 +102,7 @@ class LogsPage extends PureComponent<Props, State> {
|
|||
<div className="page">
|
||||
{this.header}
|
||||
<div className="page-contents logs-viewer">
|
||||
<Graph>{this.chart}</Graph>
|
||||
<LogsGraphContainer>{this.chart}</LogsGraphContainer>
|
||||
<SearchBar
|
||||
searchString={searchTerm}
|
||||
onSearch={this.handleSubmitSearch}
|
||||
|
@ -170,24 +175,24 @@ class LogsPage extends PureComponent<Props, State> {
|
|||
private get histogramTotal(): number {
|
||||
const {histogramData} = this.props
|
||||
|
||||
const values = getDeep<Array<[number, number]>>(
|
||||
histogramData,
|
||||
'0.response.results.0.series.0.values',
|
||||
[]
|
||||
)
|
||||
|
||||
return values.reduce((acc, v) => acc + v[1], 0)
|
||||
return _.sumBy(histogramData, 'value')
|
||||
}
|
||||
|
||||
private get chart(): JSX.Element {
|
||||
const {histogramData, timeRange} = this.props
|
||||
const {histogramData, histogramDataStatus} = this.props
|
||||
|
||||
return (
|
||||
<LogViewerChart
|
||||
timeRange={timeRange}
|
||||
data={histogramData}
|
||||
onZoom={this.handleChartZoom}
|
||||
/>
|
||||
<AutoSizer>
|
||||
{({width, height}) => (
|
||||
<HistogramChart
|
||||
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)
|
||||
}
|
||||
|
||||
private handleChartZoom = (timeRange: TimeRange) => {
|
||||
if (timeRange.lower) {
|
||||
this.props.changeZoomAsync(timeRange)
|
||||
this.setState({liveUpdating: true})
|
||||
private handleChartZoom = (t: TimePeriod) => {
|
||||
const {start, end} = t
|
||||
const timeRange = {
|
||||
lower: new Date(start).toISOString(),
|
||||
upper: new Date(end).toISOString(),
|
||||
}
|
||||
|
||||
this.props.changeZoomAsync(timeRange)
|
||||
this.setState({liveUpdating: true})
|
||||
}
|
||||
|
||||
private fetchNewDataset() {
|
||||
|
@ -283,6 +292,7 @@ const mapStateToProps = ({
|
|||
timeRange,
|
||||
currentNamespace,
|
||||
histogramData,
|
||||
histogramDataStatus,
|
||||
tableData,
|
||||
searchTerm,
|
||||
filters,
|
||||
|
@ -295,6 +305,7 @@ const mapStateToProps = ({
|
|||
timeRange,
|
||||
currentNamespace,
|
||||
histogramData,
|
||||
histogramDataStatus,
|
||||
tableData,
|
||||
searchTerm,
|
||||
filters,
|
||||
|
|
|
@ -9,6 +9,8 @@ import {
|
|||
IncrementQueryCountAction,
|
||||
ConcatMoreLogsAction,
|
||||
} from 'src/logs/actions'
|
||||
|
||||
import {RemoteDataState} from 'src/types'
|
||||
import {LogsState} from 'src/types/logs'
|
||||
|
||||
const defaultState: LogsState = {
|
||||
|
@ -20,6 +22,7 @@ const defaultState: LogsState = {
|
|||
tableQueryConfig: null,
|
||||
tableData: {columns: [], values: []},
|
||||
histogramData: [],
|
||||
histogramDataStatus: RemoteDataState.NotStarted,
|
||||
searchTerm: '',
|
||||
filters: [],
|
||||
queryCount: 0,
|
||||
|
@ -108,13 +111,14 @@ export default (state: LogsState = defaultState, action: Action) => {
|
|||
return {...state, histogramQueryConfig: action.payload.queryConfig}
|
||||
case ActionTypes.SetHistogramData:
|
||||
return {...state, histogramData: action.payload.data}
|
||||
case ActionTypes.SetHistogramDataStatus:
|
||||
return {...state, histogramDataStatus: action.payload}
|
||||
case ActionTypes.SetTableQueryConfig:
|
||||
return {...state, tableQueryConfig: action.payload.queryConfig}
|
||||
case ActionTypes.SetTableData:
|
||||
return {...state, tableData: action.payload.data}
|
||||
case ActionTypes.ChangeZoom:
|
||||
const {timeRange, data} = action.payload
|
||||
return {...state, timeRange, histogramData: data}
|
||||
return {...state, timeRange: action.payload.timeRange}
|
||||
case ActionTypes.SetSearchTerm:
|
||||
const {searchTerm} = action.payload
|
||||
return {...state, searchTerm}
|
||||
|
|
|
@ -4,6 +4,7 @@ import uuid from 'uuid'
|
|||
import {Filter} from 'src/types/logs'
|
||||
import {TimeRange, Namespace, QueryConfig} from 'src/types'
|
||||
import {NULL_STRING} from 'src/shared/constants/queryFillOptions'
|
||||
import {getDeep} from 'src/utils/wrappers'
|
||||
import {
|
||||
quoteIfTimestamp,
|
||||
buildSelect,
|
||||
|
@ -12,6 +13,8 @@ import {
|
|||
buildFill,
|
||||
} from 'src/utils/influxql'
|
||||
|
||||
import {HistogramData} from 'src/types/histogram'
|
||||
|
||||
const BIN_COUNT = 30
|
||||
|
||||
const histogramFields = [
|
||||
|
@ -156,7 +159,7 @@ const computeSeconds = (range: TimeRange) => {
|
|||
const createGroupBy = (range: TimeRange) => {
|
||||
const seconds = computeSeconds(range)
|
||||
const time = `${Math.max(Math.floor(seconds / BIN_COUNT), 1)}s`
|
||||
const tags = []
|
||||
const tags = ['severity']
|
||||
|
||||
return {time, tags}
|
||||
}
|
||||
|
@ -197,3 +200,39 @@ export const buildTableQueryConfig = (
|
|||
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'
|
||||
|
||||
interface Props {
|
||||
children?: ReactChildren
|
||||
children?: ReactChildren | JSX.Element
|
||||
title: string
|
||||
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 {notifyDashboardUploadFailed} from 'src/shared/copy/notifications'
|
||||
|
||||
interface Props {
|
||||
fileTypesToAccept?: string
|
||||
|
@ -14,7 +13,6 @@ interface State {
|
|||
inputContent: string | null
|
||||
uploadContent: string
|
||||
fileName: string
|
||||
progress: string
|
||||
dragClass: string
|
||||
}
|
||||
|
||||
|
@ -34,53 +32,45 @@ class DragAndDrop extends PureComponent<Props, State> {
|
|||
inputContent: null,
|
||||
uploadContent: '',
|
||||
fileName: '',
|
||||
progress: '',
|
||||
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() {
|
||||
return (
|
||||
<div className={this.containerClass}>
|
||||
{/* (Invisible, covers entire screen)
|
||||
This div handles drag only*/}
|
||||
<div
|
||||
onDrop={this.handleFile(true)}
|
||||
onDragOver={this.handleDragOver}
|
||||
onDragEnter={this.handleDragEnter}
|
||||
onDragExit={this.handleDragLeave}
|
||||
onDragLeave={this.handleDragLeave}
|
||||
className="drag-and-drop--dropzone"
|
||||
/>
|
||||
{/* visible form, handles drag & click */}
|
||||
{this.dragArea}
|
||||
<div className={this.dragAreaClass} onClick={this.handleFileOpen}>
|
||||
{this.dragAreaHeader}
|
||||
<div className={this.infoClass} />
|
||||
<input
|
||||
type="file"
|
||||
ref={r => (this.fileInput = r)}
|
||||
className="drag-and-drop--input"
|
||||
accept={this.fileTypesToAccept}
|
||||
onChange={this.handleFileClick}
|
||||
/>
|
||||
{this.buttons}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get dragArea(): ReactElement<HTMLDivElement> {
|
||||
return (
|
||||
<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 handleWindowDragOver = (event: DragEvent) => {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
private get fileTypesToAccept(): string {
|
||||
|
@ -155,17 +145,32 @@ class DragAndDrop extends PureComponent<Props, State> {
|
|||
handleSubmit(uploadContent, fileName)
|
||||
}
|
||||
|
||||
private handleFile = (drop: boolean) => (e: any): void => {
|
||||
let file
|
||||
if (drop) {
|
||||
file = e.dataTransfer.files[0]
|
||||
this.setState({
|
||||
dragClass: 'drag-none',
|
||||
})
|
||||
} else {
|
||||
file = e.currentTarget.files[0]
|
||||
private handleFileClick = (e: any): void => {
|
||||
const file = e.currentTarget.files[0]
|
||||
|
||||
if (!file) {
|
||||
return
|
||||
}
|
||||
|
||||
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) {
|
||||
return
|
||||
}
|
||||
|
@ -205,18 +210,13 @@ class DragAndDrop extends PureComponent<Props, State> {
|
|||
this.fileInput.value = ''
|
||||
}
|
||||
|
||||
private handleDragOver = (e: DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
private handleDragEnter = (e: DragEvent<HTMLDivElement>): void => {
|
||||
private handleDragEnter = (e: DragEvent): void => {
|
||||
dragCounter += 1
|
||||
e.preventDefault()
|
||||
this.setState({dragClass: 'drag-over'})
|
||||
}
|
||||
|
||||
private handleDragLeave = (e: DragEvent<HTMLDivElement>): void => {
|
||||
private handleDragLeave = (e: DragEvent): void => {
|
||||
dragCounter -= 1
|
||||
e.preventDefault()
|
||||
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 dashboardUI from 'src/dashboards/reducers/ui'
|
||||
import cellEditorOverlay from 'src/dashboards/reducers/cellEditorOverlay'
|
||||
import overlayTechnology from 'src/shared/reducers/overlayTechnology'
|
||||
import dashTimeV1 from 'src/dashboards/reducers/dashTimeV1'
|
||||
import persistStateEnhancer from './persistStateEnhancer'
|
||||
import servicesReducer from 'src/shared/reducers/services'
|
||||
|
@ -28,7 +27,6 @@ const rootReducer = combineReducers({
|
|||
...adminReducers,
|
||||
dashboardUI,
|
||||
cellEditorOverlay,
|
||||
overlayTechnology,
|
||||
dashTimeV1,
|
||||
logs: logsReducer,
|
||||
routing: routerReducer,
|
||||
|
|
|
@ -73,6 +73,7 @@
|
|||
@import 'components/threshold-controls';
|
||||
@import 'components/kapacitor-logs-table';
|
||||
@import 'components/dropdown-placeholder';
|
||||
@import 'components/histogram-chart';
|
||||
|
||||
// Pages
|
||||
@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 {
|
||||
position: relative;
|
||||
z-index: $drag-and-drop--z-form;
|
||||
|
@ -69,7 +59,9 @@ input[type='file'].drag-and-drop--input {
|
|||
flex-wrap: nowrap;
|
||||
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;
|
||||
background-color: $g4-onyx;
|
||||
border-color: $g6-smoke;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,85 +1,29 @@
|
|||
$padding: 20px 30px;
|
||||
|
||||
.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%;
|
||||
}
|
||||
/*
|
||||
Create / Edit Template Variable Overlay
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
.temp-builder--mq-controls {
|
||||
background: $g3-castle;
|
||||
border-radius: $radius-small;
|
||||
display: flex;
|
||||
padding: 10px 10px 0 10px;
|
||||
padding: 2px 10px;
|
||||
|
||||
&:last-child {
|
||||
padding-bottom: 10px;
|
||||
&:first-of-type {
|
||||
padding-top: 10px;
|
||||
border-top-left-radius: $radius;
|
||||
border-top-right-radius: $radius;
|
||||
}
|
||||
|
||||
> .temp-builder--mq-text, > .dropdown, .dropdown-placeholder {
|
||||
margin-right: 5px;
|
||||
&:last-of-type {
|
||||
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;
|
||||
|
||||
&:last-child {
|
||||
|
@ -92,18 +36,16 @@ $padding: 20px 30px;
|
|||
@include no-user-select();
|
||||
background-color: $g5-pepper;
|
||||
border-radius: $radius-small;
|
||||
padding: 8px;
|
||||
padding: 0 8px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
white-space: nowrap;
|
||||
color: $c-pool;
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
font-family: $code-font;
|
||||
}
|
||||
|
||||
.temp-builder .temp-builder-results {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
color: $g19-ghost;
|
||||
|
@ -118,18 +60,21 @@ $padding: 20px 30px;
|
|||
}
|
||||
}
|
||||
|
||||
.temp-builder-results > p {
|
||||
.temp-builder--validation {
|
||||
@include no-user-select();
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
color: $g19-ghost;
|
||||
margin: 15px 0;
|
||||
font-weight: 500;
|
||||
color: $g13-mist;
|
||||
margin: 0 0 8px 0;
|
||||
|
||||
&.error {
|
||||
color: $c-fire;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: $c-pineapple;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&.loading {
|
||||
|
@ -137,24 +82,34 @@ $padding: 20px 30px;
|
|||
}
|
||||
|
||||
> 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;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
li {
|
||||
background-color: $g3-castle;
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: $radius-small;
|
||||
margin: 0;
|
||||
color: $g19-ghost;
|
||||
font-weight: bold;
|
||||
list-style: none;
|
||||
}
|
||||
}
|
||||
|
||||
.temp-builder--results-item {
|
||||
@include no-user-select();
|
||||
background-color: $g3-castle;
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: $radius;
|
||||
font-size: 12px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
.endpoint-tab--parameters .faux-form {
|
||||
margin-left: -6px;
|
||||
margin-right: -6px;
|
||||
width: calc(100% + 12px);
|
||||
display: inline-block;
|
||||
}
|
||||
.endpoint-tab--parameters--empty {
|
||||
align-items: 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-filter-height: 42px;
|
||||
$logs-viewer-results-text-indent: 33px;
|
||||
$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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -232,28 +249,28 @@ $logs-viewer-gutter: 60px;
|
|||
margin-left: 2px;
|
||||
|
||||
&.emerg-severity {
|
||||
@include gradient-diag-up($c-ruby, $c-fire);
|
||||
@include gradient-diag-up($severity-emerg, $severity-emerg-intense);
|
||||
}
|
||||
&.alert-severity {
|
||||
@include gradient-diag-up($c-fire, $c-curacao);
|
||||
@include gradient-diag-up($severity-alert, $severity-alert-intense);
|
||||
}
|
||||
&.crit-severity {
|
||||
@include gradient-diag-up($c-curacao, $c-tiger);
|
||||
@include gradient-diag-up($severity-crit, $severity-crit-intense);
|
||||
}
|
||||
&.err-severity {
|
||||
@include gradient-diag-up($c-tiger, $c-pineapple);
|
||||
@include gradient-diag-up($severity-err, $severity-err-intense);
|
||||
}
|
||||
&.warning-severity {
|
||||
@include gradient-diag-up($c-pineapple, $c-thunder);
|
||||
@include gradient-diag-up($severity-warning, $severity-warning-intense);
|
||||
}
|
||||
&.notice-severity {
|
||||
@include gradient-diag-up($c-rainforest, $c-honeydew);
|
||||
@include gradient-diag-up($severity-notice, $severity-notice-intense);
|
||||
}
|
||||
&.info-severity {
|
||||
@include gradient-diag-up($c-star, $c-comet);
|
||||
@include gradient-diag-up($severity-info, $severity-info-intense);
|
||||
}
|
||||
&.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;
|
||||
}
|
||||
}
|
||||
|
||||
.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-explorer';
|
||||
@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
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
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 {
|
||||
margin-top: 30px;
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.col {
|
||||
|
@ -220,3 +220,68 @@ $grid--col-12: 100%;
|
|||
&-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
|
||||
-----------------------------------------------------------------------------
|
||||
|
|
|
@ -37,8 +37,8 @@ class DatabasesTemplateBuilder extends PureComponent<
|
|||
const {databasesStatus} = this.state
|
||||
|
||||
return (
|
||||
<div className="temp-builder databases-temp-builder">
|
||||
<div className="form-group">
|
||||
<>
|
||||
<div className="form-group col-xs-12">
|
||||
<label>Meta Query</label>
|
||||
<div className="temp-builder--mq-controls">
|
||||
<div className="temp-builder--mq-text">SHOW DATABASES</div>
|
||||
|
@ -49,7 +49,7 @@ class DatabasesTemplateBuilder extends PureComponent<
|
|||
loadingStatus={databasesStatus}
|
||||
onUpdateDefaultTemplateValue={onUpdateDefaultTemplateValue}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -74,8 +74,8 @@ class KeysTemplateBuilder extends PureComponent<Props, State> {
|
|||
} = this.state
|
||||
|
||||
return (
|
||||
<div className="temp-builder measurements-temp-builder">
|
||||
<div className="form-group">
|
||||
<>
|
||||
<div className="form-group col-xs-12">
|
||||
<label>Meta Query</label>
|
||||
<div className="temp-builder--mq-controls">
|
||||
<div className="temp-builder--mq-text">{queryPrefix}</div>
|
||||
|
@ -84,7 +84,8 @@ class KeysTemplateBuilder extends PureComponent<Props, State> {
|
|||
items={databases.map(text => ({text}))}
|
||||
onChoose={this.handleChooseDatabaseDropdown}
|
||||
selected={selectedDatabase}
|
||||
buttonSize=""
|
||||
buttonSize="btn-sm"
|
||||
className="dropdown-stretch"
|
||||
/>
|
||||
</DropdownLoadingPlaceholder>
|
||||
<div className="temp-builder--mq-text">FROM</div>
|
||||
|
@ -93,7 +94,8 @@ class KeysTemplateBuilder extends PureComponent<Props, State> {
|
|||
items={measurements.map(text => ({text}))}
|
||||
onChoose={this.handleChooseMeasurementDropdown}
|
||||
selected={selectedMeasurement}
|
||||
buttonSize=""
|
||||
buttonSize="btn-sm"
|
||||
className="dropdown-stretch"
|
||||
/>
|
||||
</DropdownLoadingPlaceholder>
|
||||
</div>
|
||||
|
@ -103,7 +105,7 @@ class KeysTemplateBuilder extends PureComponent<Props, State> {
|
|||
loadingStatus={keysStatus}
|
||||
onUpdateDefaultTemplateValue={onUpdateDefaultTemplateValue}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -55,8 +55,8 @@ class MeasurementsTemplateBuilder extends PureComponent<
|
|||
} = this.state
|
||||
|
||||
return (
|
||||
<div className="temp-builder measurements-temp-builder">
|
||||
<div className="form-group">
|
||||
<>
|
||||
<div className="form-group col-xs-12">
|
||||
<label>Meta Query</label>
|
||||
<div className="temp-builder--mq-controls">
|
||||
<div className="temp-builder--mq-text">SHOW MEASUREMENTS ON</div>
|
||||
|
@ -65,7 +65,8 @@ class MeasurementsTemplateBuilder extends PureComponent<
|
|||
items={databases.map(text => ({text}))}
|
||||
onChoose={this.handleChooseDatabaseDropdown}
|
||||
selected={selectedDatabase}
|
||||
buttonSize=""
|
||||
buttonSize="btn-sm"
|
||||
className="dropdown-stretch"
|
||||
/>
|
||||
</DropdownLoadingPlaceholder>
|
||||
</div>
|
||||
|
@ -75,7 +76,7 @@ class MeasurementsTemplateBuilder extends PureComponent<
|
|||
loadingStatus={measurementsStatus}
|
||||
onUpdateDefaultTemplateValue={onUpdateDefaultTemplateValue}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -56,12 +56,12 @@ class CustomMetaQueryTemplateBuilder extends PureComponent<
|
|||
const {metaQueryInput} = this.state
|
||||
|
||||
return (
|
||||
<div className="temp-builder csv-temp-builder">
|
||||
<div className="form-group">
|
||||
<>
|
||||
<div className="form-group col-xs-12">
|
||||
<label>Meta Query</label>
|
||||
<div className="temp-builder--mq-controls">
|
||||
<textarea
|
||||
className="form-control"
|
||||
className="form-control input-sm"
|
||||
value={metaQueryInput}
|
||||
onChange={this.handleMetaQueryInputChange}
|
||||
onBlur={this.handleMetaQueryChange}
|
||||
|
@ -69,7 +69,7 @@ class CustomMetaQueryTemplateBuilder extends PureComponent<
|
|||
</div>
|
||||
</div>
|
||||
{this.renderResults()}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -79,8 +79,10 @@ class CustomMetaQueryTemplateBuilder extends PureComponent<
|
|||
|
||||
if (this.showInvalidMetaQueryMessage) {
|
||||
return (
|
||||
<div className="temp-builder-results">
|
||||
<p className="error">Meta Query is not valid.</p>
|
||||
<div className="form-group col-xs-12 temp-builder--results">
|
||||
<p className="temp-builder--validation error">
|
||||
Meta Query is not valid.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -77,8 +77,8 @@ class KeysTemplateBuilder extends PureComponent<TemplateBuilderProps, State> {
|
|||
} = this.state
|
||||
|
||||
return (
|
||||
<div className="temp-builder measurements-temp-builder">
|
||||
<div className="form-group">
|
||||
<>
|
||||
<div className="form-group col-xs-12">
|
||||
<label>Meta Query</label>
|
||||
<div className="temp-builder--mq-controls">
|
||||
<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}))}
|
||||
onChoose={this.handleChooseDatabaseDropdown}
|
||||
selected={selectedDatabase}
|
||||
buttonSize=""
|
||||
buttonSize="btn-sm"
|
||||
className="dropdown-stretch"
|
||||
/>
|
||||
</DropdownLoadingPlaceholder>
|
||||
</div>
|
||||
|
@ -98,7 +99,8 @@ class KeysTemplateBuilder extends PureComponent<TemplateBuilderProps, State> {
|
|||
items={measurements.map(text => ({text}))}
|
||||
onChoose={this.handleChooseMeasurementDropdown}
|
||||
selected={selectedMeasurement}
|
||||
buttonSize=""
|
||||
buttonSize="btn-sm"
|
||||
className="dropdown-stretch"
|
||||
/>
|
||||
</DropdownLoadingPlaceholder>
|
||||
<div className="temp-builder--mq-text">WITH KEY</div>
|
||||
|
@ -107,7 +109,8 @@ class KeysTemplateBuilder extends PureComponent<TemplateBuilderProps, State> {
|
|||
items={tagKeys.map(text => ({text}))}
|
||||
onChoose={this.handleChooseTagKeyDropdown}
|
||||
selected={selectedTagKey}
|
||||
buttonSize=""
|
||||
buttonSize="btn-sm"
|
||||
className="dropdown-stretch"
|
||||
/>
|
||||
</DropdownLoadingPlaceholder>
|
||||
</div>
|
||||
|
@ -117,7 +120,7 @@ class KeysTemplateBuilder extends PureComponent<TemplateBuilderProps, State> {
|
|||
loadingStatus={tagValuesStatus}
|
||||
onUpdateDefaultTemplateValue={onUpdateDefaultTemplateValue}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, {Component} from 'react'
|
|||
import classnames from 'classnames'
|
||||
|
||||
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 Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
|
@ -64,15 +64,13 @@ class TemplateControlBar extends Component<Props, State> {
|
|||
<strong>Template Variables</strong>
|
||||
</div>
|
||||
)}
|
||||
{isAdding && (
|
||||
<SimpleOverlayTechnology>
|
||||
<TemplateVariableEditor
|
||||
source={source}
|
||||
onCreate={this.handleCreateTemplate}
|
||||
onCancel={this.handleCancelAddVariable}
|
||||
/>
|
||||
</SimpleOverlayTechnology>
|
||||
)}
|
||||
<OverlayTechnology visible={isAdding}>
|
||||
<TemplateVariableEditor
|
||||
source={source}
|
||||
onCreate={this.handleCreateTemplate}
|
||||
onCancel={this.handleCancelAddVariable}
|
||||
/>
|
||||
</OverlayTechnology>
|
||||
</div>
|
||||
<Authorized requiredRole={EDITOR_ROLE}>
|
||||
<button
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
|
||||
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 {calculateDropdownWidth} from 'src/dashboards/constants/templateControlBar'
|
||||
import Authorized, {isUserAuthorized, EDITOR_ROLE} from 'src/auth/Authorized'
|
||||
|
@ -76,18 +76,16 @@ class TemplateControlDropdown extends PureComponent<Props, State> {
|
|||
/>
|
||||
</label>
|
||||
</Authorized>
|
||||
{isEditing && (
|
||||
<SimpleOverlayTechnology>
|
||||
<TemplateVariableEditor
|
||||
template={template}
|
||||
source={source}
|
||||
onCreate={onCreateTemplate}
|
||||
onUpdate={this.handleUpdateTemplate}
|
||||
onDelete={this.handleDelete}
|
||||
onCancel={this.handleHideSettings}
|
||||
/>
|
||||
</SimpleOverlayTechnology>
|
||||
)}
|
||||
<OverlayTechnology visible={isEditing}>
|
||||
<TemplateVariableEditor
|
||||
template={template}
|
||||
source={source}
|
||||
onCreate={onCreateTemplate}
|
||||
onUpdate={this.handleUpdateTemplate}
|
||||
onDelete={this.handleDelete}
|
||||
onCancel={this.handleHideSettings}
|
||||
/>
|
||||
</OverlayTechnology>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -17,29 +17,33 @@ class TemplateMetaQueryPreview extends PureComponent<Props> {
|
|||
const {items, loadingStatus, onUpdateDefaultTemplateValue} = this.props
|
||||
|
||||
if (loadingStatus === RemoteDataState.NotStarted) {
|
||||
return <div className="temp-builder-results" />
|
||||
return null
|
||||
}
|
||||
|
||||
if (loadingStatus === RemoteDataState.Loading) {
|
||||
return (
|
||||
<div className="temp-builder-results">
|
||||
<p className="loading">Loading meta query preview...</p>
|
||||
<div className="form-group col-xs-12 temp-builder--results">
|
||||
<p className="temp-builder--validation loading">
|
||||
Loading Meta Query preview...
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (loadingStatus === RemoteDataState.Error) {
|
||||
return (
|
||||
<div className="temp-builder-results">
|
||||
<p className="error">Meta Query failed to execute</p>
|
||||
<div className="form-group col-xs-12 temp-builder--results">
|
||||
<p className="temp-builder--validation error">
|
||||
Meta Query failed to execute
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<div className="temp-builder-results">
|
||||
<p className="warning">
|
||||
<div className="form-group col-xs-12 temp-builder--results">
|
||||
<p className="temp-builder--validation warning">
|
||||
Meta Query is syntactically correct but returned no results
|
||||
</p>
|
||||
</div>
|
||||
|
@ -49,8 +53,8 @@ class TemplateMetaQueryPreview extends PureComponent<Props> {
|
|||
const pluralizer = items.length === 1 ? '' : 's'
|
||||
|
||||
return (
|
||||
<div className="temp-builder-results">
|
||||
<p>
|
||||
<div className="form-group col-xs-12 temp-builder--results">
|
||||
<p className="temp-builder--validation">
|
||||
Meta Query returned <strong>{items.length}</strong> value{pluralizer}
|
||||
</p>
|
||||
{items.length > 0 && (
|
||||
|
|
|
@ -7,9 +7,9 @@ import TemplatePreviewListItem from 'src/tempVars/components/TemplatePreviewList
|
|||
|
||||
import {TemplateValue} from 'src/types'
|
||||
|
||||
const LI_HEIGHT = 35
|
||||
const LI_MARGIN_BOTTOM = 3
|
||||
const RESULTS_TO_DISPLAY = 10
|
||||
const LI_HEIGHT = 28
|
||||
const LI_MARGIN_BOTTOM = 2
|
||||
|
||||
interface Props {
|
||||
items: TemplateValue[]
|
||||
|
@ -22,27 +22,20 @@ class TemplatePreviewList extends PureComponent<Props> {
|
|||
const {items, onUpdateDefaultTemplateValue} = this.props
|
||||
|
||||
return (
|
||||
<ul
|
||||
className="temp-builder-results--list"
|
||||
<div
|
||||
className="temp-builder--results-list"
|
||||
style={{height: `${this.resultsListHeight}px`}}
|
||||
>
|
||||
<FancyScrollbar>
|
||||
{items.map(item => {
|
||||
return (
|
||||
<TemplatePreviewListItem
|
||||
key={uuid.v4()}
|
||||
onClick={onUpdateDefaultTemplateValue}
|
||||
style={{
|
||||
height: `${LI_HEIGHT}px`,
|
||||
marginBottom: `${LI_MARGIN_BOTTOM}px`,
|
||||
zIndex: 9010,
|
||||
}}
|
||||
item={item}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<FancyScrollbar autoHide={false}>
|
||||
{items.map(item => (
|
||||
<TemplatePreviewListItem
|
||||
key={uuid.v4()}
|
||||
onClick={onUpdateDefaultTemplateValue}
|
||||
item={item}
|
||||
/>
|
||||
))}
|
||||
</FancyScrollbar>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -6,18 +6,23 @@ import {TemplateValue} from 'src/types'
|
|||
interface Props {
|
||||
item: TemplateValue
|
||||
onClick: (item: TemplateValue) => void
|
||||
style: React.CSSProperties
|
||||
}
|
||||
|
||||
const LI_HEIGHT = 28
|
||||
const LI_MARGIN_BOTTOM = 2
|
||||
|
||||
class TemplatePreviewListItem extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {item, style} = this.props
|
||||
const {item} = this.props
|
||||
|
||||
return (
|
||||
<li
|
||||
onClick={this.handleClick}
|
||||
style={style}
|
||||
className={classNames('temp-builder-results--list-item', {
|
||||
style={{
|
||||
height: `${LI_HEIGHT}px`,
|
||||
marginBottom: `${LI_MARGIN_BOTTOM}px`,
|
||||
}}
|
||||
className={classNames('temp-builder--results-item', {
|
||||
active: this.isDefault,
|
||||
})}
|
||||
>
|
||||
|
|
|
@ -8,6 +8,9 @@ import {connect} from 'react-redux'
|
|||
import _ from 'lodash'
|
||||
|
||||
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 ConfirmButton from 'src/shared/components/ConfirmButton'
|
||||
import {getDeep} from 'src/utils/wrappers'
|
||||
|
@ -70,7 +73,7 @@ const TEMPLATE_BUILDERS = {
|
|||
[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]
|
||||
|
||||
|
@ -107,21 +110,18 @@ class TemplateVariableEditor extends PureComponent<Props, State> {
|
|||
const TemplateBuilder = this.templateBuilder
|
||||
|
||||
return (
|
||||
<div className="edit-temp-var">
|
||||
<div className="edit-temp-var--header">
|
||||
<h1>{isNew ? 'Create' : 'Edit'} Template Variable</h1>
|
||||
<div className="edit-temp-var--header-controls">
|
||||
<OverlayContainer maxWidth={650}>
|
||||
<OverlayHeading title={this.title}>
|
||||
<div className="btn-group--right">
|
||||
<button
|
||||
className="btn btn-default"
|
||||
style={{zIndex: 9010}}
|
||||
className="btn btn-default btn-sm"
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
style={{zIndex: 9010}}
|
||||
className="btn btn-success"
|
||||
className="btn btn-success btn-sm"
|
||||
type="button"
|
||||
onClick={this.handleSave}
|
||||
disabled={!this.canSave}
|
||||
|
@ -129,31 +129,31 @@ class TemplateVariableEditor extends PureComponent<Props, State> {
|
|||
{this.saveButtonText}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="edit-temp-var--body" style={{zIndex: 9010}}>
|
||||
<div className="edit-temp-var--body-row" style={{zIndex: 9010}}>
|
||||
<div style={{zIndex: 9010}} className="form-group name">
|
||||
</OverlayHeading>
|
||||
<OverlayBody>
|
||||
<div className="faux-form">
|
||||
<div className="form-group col-sm-6">
|
||||
<label>Name</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
className="form-control input-sm form-astronaut"
|
||||
value={nextTemplate.tempVar}
|
||||
onChange={this.handleChangeName}
|
||||
onKeyPress={this.handleNameKeyPress}
|
||||
onBlur={this.formatName}
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
<div style={{zIndex: 9010}} className="form-group template-type">
|
||||
<div className="form-group col-sm-6">
|
||||
<label>Type</label>
|
||||
<Dropdown
|
||||
items={TEMPLATE_TYPES_LIST}
|
||||
onChoose={this.handleChooseType}
|
||||
selected={this.dropdownSelection}
|
||||
buttonSize=""
|
||||
buttonSize="btn-sm"
|
||||
className="dropdown-stretch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="edit-temp-var--body-row">
|
||||
<TemplateBuilder
|
||||
template={nextTemplate}
|
||||
source={source}
|
||||
|
@ -163,20 +163,42 @@ class TemplateVariableEditor extends PureComponent<Props, State> {
|
|||
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>
|
||||
<ConfirmButton
|
||||
text={this.isDeleting ? 'Deleting...' : 'Delete'}
|
||||
confirmAction={this.handleDelete}
|
||||
type="btn-danger"
|
||||
size="btn-xs"
|
||||
customClass="delete"
|
||||
disabled={isNew || this.isDeleting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</OverlayBody>
|
||||
</OverlayContainer>
|
||||
)
|
||||
}
|
||||
|
||||
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> {
|
||||
const {
|
||||
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 {
|
||||
id: string
|
||||
|
@ -19,6 +25,7 @@ export interface LogsState {
|
|||
timeRange: TimeRange
|
||||
histogramQueryConfig: QueryConfig | null
|
||||
histogramData: object[]
|
||||
histogramDataStatus: RemoteDataState
|
||||
tableQueryConfig: QueryConfig | null
|
||||
tableData: TableData
|
||||
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 TemplateControlDropdown from 'src/tempVars/components/TemplateControlDropdown'
|
||||
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 {TemplateType, TemplateValueType} from 'src/types'
|
||||
|
@ -63,14 +63,24 @@ describe('TemplateControlBar', () => {
|
|||
|
||||
it('renders an TemplateVariableEditor overlay when adding a template variable', () => {
|
||||
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')
|
||||
|
||||
const elements = wrapper
|
||||
.find(SimpleOverlayTechnology)
|
||||
.find(OverlayTechnology)
|
||||
.dive()
|
||||
.find(TemplateVariableEditor)
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react'
|
||||
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 TemplateControlDropdown from 'src/tempVars/components/TemplateControlDropdown'
|
||||
import {source} from 'test/resources'
|
||||
|
@ -34,14 +34,24 @@ const defaultProps = {
|
|||
|
||||
describe('TemplateControlDropdown', () => {
|
||||
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')
|
||||
|
||||
const elements = wrapper
|
||||
.find(SimpleOverlayTechnology)
|
||||
.find(OverlayTechnology)
|
||||
.dive()
|
||||
.find(TemplateVariableEditor)
|
||||
|
||||
|
|
59
ui/yarn.lock
59
ui/yarn.lock
|
@ -32,6 +32,16 @@
|
|||
version "0.0.56"
|
||||
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":
|
||||
version "1.1.6"
|
||||
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"
|
||||
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:
|
||||
version "1.0.0"
|
||||
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"
|
||||
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:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.3.0.tgz#0971abd167f2d4bf3f5bd508229e1c4b6dc50479"
|
||||
|
|
Loading…
Reference in New Issue