Merge branch 'master' into tempvars/select-default

pull/10616/head
ebb-tide 2018-06-27 15:30:12 -07:00
commit 54aa7c694b
61 changed files with 2376 additions and 753 deletions

View File

@ -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',

View File

@ -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",

View File

@ -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}

View File

@ -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

View File

@ -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'

View File

@ -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 => {

View File

@ -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

View File

@ -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

View File

@ -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,
}

View File

@ -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())
}

View File

@ -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

View File

@ -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,

View File

@ -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}

View File

@ -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
}

View File

@ -1,7 +1,7 @@
import React, {PureComponent, ReactChildren} from 'react'
interface Props {
children?: ReactChildren
children?: ReactChildren | JSX.Element
title: string
onDismiss?: () => void
}

View File

@ -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

View File

@ -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',
})

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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,

View File

@ -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';

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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
-----------------------------------------------------------------------------

View File

@ -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>
</>
)
}

View File

@ -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>
</>
)
}

View File

@ -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>
</>
)
}

View File

@ -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>
)
}

View File

@ -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>
</>
)
}

View File

@ -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

View File

@ -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>
)
}

View File

@ -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 && (

View File

@ -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>
)
}

View File

@ -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,
})}
>

View File

@ -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},

24
ui/src/types/histogram.ts Normal file
View File

@ -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
}

View File

@ -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

25
ui/src/utils/extentBy.tsx Normal file
View File

@ -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]
}

View File

@ -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)
})
})

View File

@ -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)
})
})

View File

@ -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>
`;

View File

@ -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)

View File

@ -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)

View File

@ -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"