Merge branch 'master' into ifql/join

ifql/join
ebb-tide 2018-05-29 12:36:57 -07:00
commit 46c30ed92f
57 changed files with 3411 additions and 1605 deletions

View File

@ -1,6 +1,7 @@
## v1.5.1.0 [unreleased]
### Features
1. [#3522](https://github.com/influxdata/chronograf/pull/3522): Add support for Template Variables in Cell Titles
### UI Improvements

View File

@ -41,6 +41,7 @@
"@types/jest": "^22.1.4",
"@types/lodash": "^4.14.104",
"@types/node": "^9.4.6",
"@types/papaparse": "^4.1.34",
"@types/prop-types": "^15.5.2",
"@types/react": "^16.0.38",
"@types/react-dnd": "^2.0.36",
@ -133,6 +134,7 @@
"lodash": "^4.3.0",
"moment": "^2.13.0",
"nano-date": "^2.0.1",
"papaparse": "^4.4.0",
"prop-types": "^15.6.1",
"query-string": "^5.0.0",
"react": "^16.3.1",
@ -157,4 +159,4 @@
"rome": "^2.1.22",
"uuid": "^3.2.1"
}
}
}

View File

@ -103,7 +103,7 @@ export class DataExplorer extends PureComponent<Props, State> {
const {showWriteForm} = this.state
return (
<div className="data-explorer">
<>
{showWriteForm ? (
<OverlayTechnologies>
<WriteDataForm
@ -150,7 +150,7 @@ export class DataExplorer extends PureComponent<Props, State> {
editQueryStatus={queryConfigActions.editQueryStatus}
/>
</ResizeContainer>
</div>
</>
)
}

View File

@ -1,6 +1,9 @@
import _ from 'lodash'
import AJAX from 'src/utils/ajax'
import {Service} from 'src/types'
import {Service, ScriptResult} from 'src/types'
import {updateService} from 'src/shared/apis'
import {parseResults} from 'src/shared/parsing/ifql'
export const getSuggestions = async (url: string) => {
try {
@ -36,24 +39,28 @@ export const getAST = async (request: ASTRequest) => {
}
}
export const getTimeSeries = async (service: Service, script: string) => {
export const getTimeSeries = async (
service: Service,
script: string
): Promise<ScriptResult[]> => {
const and = encodeURIComponent('&')
const mark = encodeURIComponent('?')
const garbage = script.replace(/\s/g, '') // server cannot handle whitespace
try {
const data = await AJAX({
const {data} = await AJAX({
method: 'POST',
url: `${
service.links.proxy
}?path=/v1/query${mark}orgName=defaulorgname${and}q=${garbage}`,
headers: {'Content-Type': 'text/plain'},
})
return data
return parseResults(data)
} catch (error) {
console.error('Problem fetching data', error)
throw error.data.message
throw _.get(error, 'headers.x-influx-error', false) ||
_.get(error, 'data.message', 'unknown error 🤷')
}
}

View File

@ -0,0 +1,9 @@
import React, {SFC} from 'react'
const NoResult: SFC = () => (
<div className="graph-empty">
<p>No Results</p>
</div>
)
export default NoResult

View File

@ -0,0 +1,58 @@
import React, {PureComponent, CSSProperties} from 'react'
import _ from 'lodash'
import {ScriptResult} from 'src/types'
import {ErrorHandling} from 'src/shared/decorators/errors'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import TableSidebarItem from 'src/ifql/components/TableSidebarItem'
import {vis} from 'src/ifql/constants'
interface Props {
data: ScriptResult[]
selectedResultID: string
onSelectResult: (id: string) => void
}
@ErrorHandling
export default class TableSidebar extends PureComponent<Props> {
public render() {
const {data, selectedResultID, onSelectResult} = this.props
return (
<div className="time-machine--sidebar">
{!this.isDataEmpty && (
<div className="query-builder--heading" style={this.headingStyle}>
Results
</div>
)}
<FancyScrollbar>
<div className="time-machine-vis--sidebar query-builder--list">
{data.map(({name, id}) => {
return (
<TableSidebarItem
id={id}
key={id}
name={name}
onSelect={onSelectResult}
isSelected={id === selectedResultID}
/>
)
})}
</div>
</FancyScrollbar>
</div>
)
}
get headingStyle(): CSSProperties {
return {
height: `${vis.TABLE_ROW_HEIGHT + 2.5}px`,
backgroundColor: '#31313d',
borderBottom: '2px solid #383846', // $g5-pepper
}
}
get isDataEmpty(): boolean {
return _.isEmpty(this.props.data)
}
}

View File

@ -0,0 +1,36 @@
import React, {PureComponent} from 'react'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
name: string
id: string
isSelected: boolean
onSelect: (id: string) => void
}
@ErrorHandling
export default class TableSidebarItem extends PureComponent<Props> {
public render() {
return (
<div
className={`query-builder--list-item ${this.active}`}
onClick={this.handleClick}
>
{this.props.name}
</div>
)
}
private get active(): string {
if (this.props.isSelected) {
return 'active'
}
return ''
}
private handleClick = (): void => {
this.props.onSelect(this.props.id)
}
}

View File

@ -9,16 +9,17 @@ import {
OnChangeScript,
OnSubmitScript,
FlatBody,
Status,
ScriptStatus,
ScriptResult,
} from 'src/types/ifql'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {HANDLE_VERTICAL, HANDLE_HORIZONTAL} from 'src/shared/constants'
interface Props {
data: string
data: ScriptResult[]
script: string
body: Body[]
status: Status
status: ScriptStatus
suggestions: Suggestion[]
onChangeScript: OnChangeScript
onSubmitScript: OnSubmitScript

View File

@ -0,0 +1,65 @@
import React, {PureComponent} from 'react'
import _ from 'lodash'
import {Grid, GridCellProps, AutoSizer, ColumnSizer} from 'react-virtualized'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {ScriptResult} from 'src/types'
import {vis} from 'src/ifql/constants'
@ErrorHandling
export default class TimeMachineTable extends PureComponent<ScriptResult> {
public render() {
const {data} = this.props
return (
<div style={{flex: '1 1 auto'}}>
<AutoSizer>
{({height, width}) => (
<ColumnSizer
width={width}
columnMinWidth={vis.TIME_COLUMN_WIDTH}
columnCount={this.columnCount}
>
{({adjustedWidth, getColumnWidth}) => (
<Grid
className="table-graph--scroll-window"
cellRenderer={this.cellRenderer}
columnCount={this.columnCount}
columnWidth={getColumnWidth}
height={height}
rowCount={data.length}
rowHeight={vis.TABLE_ROW_HEIGHT}
width={adjustedWidth}
/>
)}
</ColumnSizer>
)}
</AutoSizer>
</div>
)
}
private get columnCount(): number {
return _.get(this.props.data, '0', []).length
}
private cellRenderer = ({
columnIndex,
key,
rowIndex,
style,
}: GridCellProps): React.ReactNode => {
const {data} = this.props
const headerRowClass = !rowIndex ? 'table-graph-cell__fixed-row' : ''
return (
<div
key={key}
style={style}
className={`table-graph-cell ${headerRowClass}`}
>
{data[rowIndex][columnIndex]}
</div>
)
}
}

View File

@ -1,40 +1,79 @@
import React, {PureComponent} from 'react'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import React, {PureComponent, CSSProperties} from 'react'
import _ from 'lodash'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {ScriptResult} from 'src/types'
import TableSidebar from 'src/ifql/components/TableSidebar'
import TimeMachineTable from 'src/ifql/components/TimeMachineTable'
import {HANDLE_PIXELS} from 'src/shared/constants'
import NoResults from 'src/ifql/components/NoResults'
interface Props {
data: string
data: ScriptResult[]
}
interface State {
selectedResultID: string | null
}
@ErrorHandling
class TimeMachineVis extends PureComponent<Props> {
class TimeMachineVis extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {selectedResultID: this.initialResultID}
}
public componentDidUpdate(__, prevState) {
if (prevState.selectedResultID === null) {
this.setState({selectedResultID: this.initialResultID})
}
}
public render() {
return (
<div className="time-machine-visualization">
<div className="time-machine--graph">
<FancyScrollbar>
<div className="time-machine--graph-body">
{this.data.map((d, i) => {
return (
<div key={i} className="data-row">
{d}
</div>
)
})}
</div>
</FancyScrollbar>
<div className="time-machine-visualization" style={this.style}>
{this.hasResults && (
<TableSidebar
data={this.props.data}
selectedResultID={this.state.selectedResultID}
onSelectResult={this.handleSelectResult}
/>
)}
<div className="time-machine--vis">
{this.shouldShowTable && (
<TimeMachineTable {...this.selectedResult} />
)}
{!this.hasResults && <NoResults />}
</div>
</div>
)
}
private get data(): string[] {
const {data} = this.props
if (!data) {
return ['Your query was syntactically correct but returned no data']
}
private get initialResultID(): string {
return _.get(this.props.data, '0.id', null)
}
return this.props.data.split('\n')
private handleSelectResult = (selectedResultID: string): void => {
this.setState({selectedResultID})
}
private get style(): CSSProperties {
return {
padding: `${HANDLE_PIXELS}px`,
}
}
private get hasResults(): boolean {
return !!this.props.data.length
}
private get shouldShowTable(): boolean {
return !!this.props.data && !!this.selectedResult
}
private get selectedResult(): ScriptResult {
return this.props.data.find(d => d.id === this.state.selectedResultID)
}
}

View File

@ -3,5 +3,6 @@ import * as editor from 'src/ifql/constants/editor'
import * as argTypes from 'src/ifql/constants/argumentTypes'
import * as funcNames from 'src/ifql/constants/funcNames'
import * as builder from 'src/ifql/constants/builder'
import * as vis from 'src/ifql/constants/vis'
export {ast, funcNames, argTypes, editor, builder}
export {ast, funcNames, argTypes, editor, builder, vis}

View File

@ -0,0 +1,2 @@
export const TABLE_ROW_HEIGHT = 30
export const TIME_COLUMN_WIDTH = 170

View File

@ -1,10 +1,17 @@
import React, {PureComponent, ReactChildren} from 'react'
import {connect} from 'react-redux'
import {withRouter, WithRouterProps} from 'react-router'
import {WithRouterProps} from 'react-router'
import {IFQLPage} from 'src/ifql'
import IFQLOverlay from 'src/ifql/components/IFQLOverlay'
import {OverlayContext} from 'src/shared/components/OverlayTechnology'
import {Source, Service} from 'src/types'
import {Source, Service, Notification} from 'src/types'
import {Links} from 'src/types/ifql'
import {notify as notifyAction} from 'src/shared/actions/notifications'
import {
updateScript as updateScriptAction,
UpdateScript,
} from 'src/ifql/actions'
import * as a from 'src/shared/actions/overlayTechnology'
import * as b from 'src/shared/actions/services'
@ -16,6 +23,10 @@ interface Props {
children: ReactChildren
showOverlay: a.ShowOverlay
fetchServicesAsync: b.FetchServicesAsync
notify: (message: Notification) => void
updateScript: UpdateScript
script: string
links: Links
}
export class CheckServices extends PureComponent<Props & WithRouterProps> {
@ -36,7 +47,22 @@ export class CheckServices extends PureComponent<Props & WithRouterProps> {
}
public render() {
return this.props.children
const {services, sources, notify, updateScript, links, script} = this.props
if (!this.props.services.length) {
return null // put loading spinner here
}
return (
<IFQLPage
sources={sources}
services={services}
links={links}
script={script}
notify={notify}
updateScript={updateScript}
/>
)
}
private overlay() {
@ -65,8 +91,17 @@ export class CheckServices extends PureComponent<Props & WithRouterProps> {
const mdtp = {
fetchServicesAsync: actions.fetchServicesAsync,
showOverlay: actions.showOverlay,
updateScript: updateScriptAction,
notify: notifyAction,
}
const mstp = ({sources, services}) => ({sources, services})
const mstp = ({sources, services, links, script}) => {
return {
links: links.ifql,
script,
sources,
services,
}
}
export default connect(mstp, mdtp)(withRouter(CheckServices))
export default connect(mstp, mdtp)(CheckServices)

View File

@ -1,25 +1,22 @@
import React, {PureComponent} from 'react'
import {connect} from 'react-redux'
import _ from 'lodash'
import CheckServices from 'src/ifql/containers/CheckServices'
import TimeMachine from 'src/ifql/components/TimeMachine'
import IFQLHeader from 'src/ifql/components/IFQLHeader'
import {ErrorHandling} from 'src/shared/decorators/errors'
import KeyboardShortcuts from 'src/shared/components/KeyboardShortcuts'
import {notify as notifyAction} from 'src/shared/actions/notifications'
import {analyzeSuccess} from 'src/shared/copy/notifications'
import {
updateScript as updateScriptAction,
UpdateScript,
} from 'src/ifql/actions'
analyzeSuccess,
ifqlTimeSeriesError,
} from 'src/shared/copy/notifications'
import {UpdateScript} from 'src/ifql/actions'
import {bodyNodes} from 'src/ifql/helpers'
import {getSuggestions, getAST, getTimeSeries} from 'src/ifql/apis'
import {builder, argTypes} from 'src/ifql/constants'
import {Source, Service, Notification} from 'src/types'
import {Source, Service, Notification, ScriptResult} from 'src/types'
import {
Suggestion,
FlatBody,
@ -28,6 +25,7 @@ import {
Handlers,
DeleteFuncNodeArgs,
Func,
ScriptStatus,
} from 'src/types/ifql'
interface Status {
@ -42,9 +40,6 @@ interface Props {
notify: (message: Notification) => void
script: string
updateScript: UpdateScript
params: {
sourceID: string
}
}
interface Body extends FlatBody {
@ -54,9 +49,9 @@ interface Body extends FlatBody {
interface State {
body: Body[]
ast: object
data: string
data: ScriptResult[]
status: ScriptStatus
suggestions: Suggestion[]
status: Status
}
export const IFQLContext = React.createContext()
@ -68,7 +63,7 @@ export class IFQLPage extends PureComponent<Props, State> {
this.state = {
body: [],
ast: null,
data: 'Hit "Get Data!" or Ctrl + Enter to run your script',
data: [],
suggestions: [],
status: {
type: 'none',
@ -78,7 +73,7 @@ export class IFQLPage extends PureComponent<Props, State> {
}
public async componentDidMount() {
const {links, script} = this.props
const {links} = this.props
try {
const suggestions = await getSuggestions(links.suggestions)
@ -87,7 +82,7 @@ export class IFQLPage extends PureComponent<Props, State> {
console.error('Could not get function suggestions: ', error)
}
this.getASTResponse(script)
this.getTimeSeries()
}
public render() {
@ -95,27 +90,25 @@ export class IFQLPage extends PureComponent<Props, State> {
const {script} = this.props
return (
<CheckServices>
<IFQLContext.Provider value={this.handlers}>
<KeyboardShortcuts onControlEnter={this.getTimeSeries}>
<div className="page hosts-list-page">
{this.header}
<TimeMachine
data={data}
body={body}
script={script}
status={status}
suggestions={suggestions}
onAnalyze={this.handleAnalyze}
onAppendFrom={this.handleAppendFrom}
onAppendJoin={this.handleAppendJoin}
onChangeScript={this.handleChangeScript}
onSubmitScript={this.handleSubmitScript}
/>
</div>
</KeyboardShortcuts>
</IFQLContext.Provider>
</CheckServices>
<IFQLContext.Provider value={this.handlers}>
<KeyboardShortcuts onControlEnter={this.getTimeSeries}>
<div className="page hosts-list-page">
{this.header}
<TimeMachine
data={data}
body={body}
script={script}
status={status}
suggestions={suggestions}
onAnalyze={this.handleAnalyze}
onAppendFrom={this.handleAppendFrom}
onAppendJoin={this.handleAppendJoin}
onChangeScript={this.handleChangeScript}
onSubmitScript={this.handleSubmitScript}
/>
</div>
</KeyboardShortcuts>
</IFQLContext.Provider>
)
}
@ -406,6 +399,10 @@ export class IFQLPage extends PureComponent<Props, State> {
private getASTResponse = async (script: string) => {
const {links} = this.props
if (!script) {
return
}
try {
const ast = await getAST({url: links.ast, body: script})
const body = bodyNodes(ast, this.state.suggestions)
@ -419,18 +416,28 @@ export class IFQLPage extends PureComponent<Props, State> {
}
private getTimeSeries = async () => {
const {script} = this.props
this.setState({data: 'fetching data...'})
const {script, links, notify} = this.props
try {
const {data} = await getTimeSeries(this.service, script)
this.setState({data})
} catch (error) {
this.setState({data: error})
console.error('Could not get timeSeries', error)
if (!script) {
return
}
this.getASTResponse(script)
try {
await getAST({url: links.ast, body: script})
} catch (error) {
this.setState({status: this.parseError(error)})
return console.error('Could not parse AST', error)
}
try {
const data = await getTimeSeries(this.service, script)
this.setState({data})
} catch (error) {
this.setState({data: []})
notify(ifqlTimeSeriesError(error))
console.error('Could not get timeSeries', error)
}
}
private parseError = (error): Status => {
@ -440,13 +447,4 @@ export class IFQLPage extends PureComponent<Props, State> {
}
}
const mapStateToProps = ({links, services, sources, script}) => {
return {links: links.ifql, services, sources, script}
}
const mapDispatchToProps = {
notify: notifyAction,
updateScript: updateScriptAction,
}
export default connect(mapStateToProps, mapDispatchToProps)(IFQLPage)
export default IFQLPage

View File

@ -36,7 +36,7 @@ import {
} from 'src/kapacitor'
import {AdminChronografPage, AdminInfluxDBPage} from 'src/admin'
import {SourcePage, ManageSources} from 'src/sources'
import {IFQLPage} from 'src/ifql'
import {CheckServices} from 'src/ifql'
import NotFound from 'src/shared/components/NotFound'
import {getLinksAsync} from 'src/shared/actions/links'
@ -158,7 +158,7 @@ class Root extends PureComponent<{}, State> {
<Route path="manage-sources" component={ManageSources} />
<Route path="manage-sources/new" component={SourcePage} />
<Route path="manage-sources/:id/edit" component={SourcePage} />
<Route path="delorean" component={IFQLPage} />
<Route path="delorean" component={CheckServices} />
</Route>
</Route>
<Route path="*" component={NotFound} />

View File

@ -56,6 +56,7 @@ export const saveToLocalStorage = ({
timeRange,
dataExplorer,
dashTimeV1: {ranges},
logs,
script,
}: LocalStorage): void => {
try {
@ -72,6 +73,7 @@ export const saveToLocalStorage = ({
dataExplorer,
dataExplorerQueryConfigs,
script,
logs,
})
)
} catch (err) {

View File

@ -1,13 +1,22 @@
import {Source, Namespace, TimeRange} from 'src/types'
import {Source, Namespace, TimeRange, QueryConfig} from 'src/types'
import {getSource} from 'src/shared/apis'
import {getDatabasesWithRetentionPolicies} from 'src/shared/apis/databases'
import {buildHistogramQueryConfig} from 'src/logs/utils'
import {getDeep} from 'src/utils/wrappers'
import buildQuery from 'src/utils/influxql'
import {executeQueryAsync} from 'src/logs/api'
import {LogsState} from 'src/types/localStorage'
type GetState = () => {logs: LogsState}
export enum ActionTypes {
SetSource = 'LOGS_SET_SOURCE',
SetNamespaces = 'LOGS_SET_NAMESPACES',
SetTimeRange = 'LOGS_SET_TIMERANGE',
SetNamespace = 'LOGS_SET_NAMESPACE',
SetHistogramQueryConfig = 'LOGS_SET_HISTOGRAM_QUERY_CONFIG',
SetHistogramData = 'LOGS_SET_HISTOGRAM_DATA',
ChangeZoom = 'LOGS_CHANGE_ZOOM',
}
interface SetSourceAction {
@ -38,11 +47,36 @@ interface SetTimeRangeAction {
}
}
interface SetHistogramQueryConfig {
type: ActionTypes.SetHistogramQueryConfig
payload: {
queryConfig: QueryConfig
}
}
interface SetHistogramData {
type: ActionTypes.SetHistogramData
payload: {
data: object[]
}
}
interface ChangeZoomAction {
type: ActionTypes.ChangeZoom
payload: {
data: object[]
timeRange: TimeRange
}
}
export type Action =
| SetSourceAction
| SetNamespacesAction
| SetTimeRangeAction
| SetNamespaceAction
| SetHistogramQueryConfig
| SetHistogramData
| ChangeZoomAction
export const setSource = (source: Source): SetSourceAction => ({
type: ActionTypes.SetSource,
@ -51,12 +85,75 @@ export const setSource = (source: Source): SetSourceAction => ({
},
})
export const setNamespace = (namespace: Namespace): SetNamespaceAction => ({
type: ActionTypes.SetNamespace,
payload: {
namespace,
},
})
export const executeHistogramQueryAsync = () => async (
dispatch,
getState: GetState
): Promise<void> => {
const state = getState()
const queryConfig = getDeep<QueryConfig | null>(
state,
'logs.histogramQueryConfig',
null
)
const timeRange = getDeep<TimeRange | null>(state, 'logs.timeRange', null)
const namespace = getDeep<Namespace | null>(
state,
'logs.currentNamespace',
null
)
const proxyLink = getDeep<string | null>(
state,
'logs.currentSource.links.proxy',
null
)
if (queryConfig && timeRange && namespace && proxyLink) {
const query = buildQuery(timeRange, queryConfig)
const response = await executeQueryAsync(proxyLink, namespace, query)
dispatch({
type: ActionTypes.SetHistogramData,
payload: {
data: [{response}],
},
})
}
}
export const setHistogramQueryConfigAsync = () => async (
dispatch,
getState: GetState
): Promise<void> => {
const state = getState()
const namespace = getDeep<Namespace | null>(
state,
'logs.currentNamespace',
null
)
const timeRange = getDeep<TimeRange | null>(state, 'logs.timeRange', null)
if (timeRange && namespace) {
const queryConfig = buildHistogramQueryConfig(namespace, timeRange)
dispatch({
type: ActionTypes.SetHistogramQueryConfig,
payload: {queryConfig},
})
dispatch(executeHistogramQueryAsync())
}
}
export const setNamespaceAsync = (namespace: Namespace) => async (
dispatch
): Promise<void> => {
dispatch({
type: ActionTypes.SetNamespace,
payload: {namespace},
})
dispatch(setHistogramQueryConfigAsync())
}
export const setNamespaces = (
namespaces: Namespace[]
@ -67,27 +164,70 @@ export const setNamespaces = (
},
})
export const setTimeRange = (timeRange: TimeRange): SetTimeRangeAction => ({
type: ActionTypes.SetTimeRange,
payload: {
timeRange,
},
})
export const setTimeRangeAsync = (timeRange: TimeRange) => async (
dispatch
): Promise<void> => {
dispatch({
type: ActionTypes.SetTimeRange,
payload: {
timeRange,
},
})
dispatch(setHistogramQueryConfigAsync())
}
export const getSourceAsync = (sourceID: string) => async dispatch => {
export const populateNamespacesAsync = (proxyLink: string) => async (
dispatch
): Promise<void> => {
const namespaces = await getDatabasesWithRetentionPolicies(proxyLink)
if (namespaces && namespaces.length > 0) {
dispatch(setNamespaces(namespaces))
dispatch(setNamespaceAsync(namespaces[0]))
}
}
export const getSourceAndPopulateNamespacesAsync = (sourceID: string) => async (
dispatch
): Promise<void> => {
const response = await getSource(sourceID)
const source = response.data
const proxyLink = getDeep<string | null>(source, 'links.proxy', null)
if (proxyLink) {
const namespaces = await getDatabasesWithRetentionPolicies(proxyLink)
if (namespaces && namespaces.length > 0) {
dispatch(setNamespaces(namespaces))
dispatch(setNamespace(namespaces[0]))
}
dispatch(setSource(source))
dispatch(populateNamespacesAsync(proxyLink))
}
}
export const changeZoomAsync = (timeRange: TimeRange) => async (
dispatch,
getState: GetState
): Promise<void> => {
const state = getState()
const namespace = getDeep<Namespace | null>(
state,
'logs.currentNamespace',
null
)
const proxyLink = getDeep<string | null>(
state,
'logs.currentSource.links.proxy',
null
)
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,
},
})
}
}

24
ui/src/logs/api/index.ts Normal file
View File

@ -0,0 +1,24 @@
import {proxy} from 'src/utils/queryUrlGenerator'
import {Namespace} from 'src/types'
import {TimeSeriesResponse} from 'src/types/series'
export const executeQueryAsync = async (
proxyLink: string,
namespace: Namespace,
query: string
): Promise<TimeSeriesResponse> => {
try {
const {data} = await proxy({
source: proxyLink,
db: namespace.database,
rp: namespace.retentionPolicy,
query,
tempVars: [],
resolution: null,
})
return data
} catch (error) {
throw error
}
}

View File

@ -0,0 +1,33 @@
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: (lower: string, upper: string) => 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

@ -26,24 +26,32 @@ class LogViewerHeader extends PureComponent<Props> {
public render(): JSX.Element {
const {timeRange} = this.props
return (
<>
<Dropdown
className="dropdown-300"
items={this.sourceDropDownItems}
selected={this.selectedSource}
onChoose={this.handleChooseSource}
/>
<Dropdown
className="dropdown-300"
items={this.namespaceDropDownItems}
selected={this.selectedNamespace}
onChoose={this.handleChooseNamespace}
/>
<TimeRangeDropdown
onChooseTimeRange={this.handleChooseTimeRange}
selected={timeRange}
/>
</>
<div className="page-header full-width">
<div className="page-header__container">
<div className="page-header__left">
<h1 className="page-header__title">Log Viewer</h1>
</div>
<div className="page-header__right">
<Dropdown
className="dropdown-300"
items={this.sourceDropDownItems}
selected={this.selectedSource}
onChoose={this.handleChooseSource}
/>
<Dropdown
className="dropdown-180"
iconName="disks"
items={this.namespaceDropDownItems}
selected={this.selectedNamespace}
onChoose={this.handleChooseNamespace}
/>
<TimeRangeDropdown
onChooseTimeRange={this.handleChooseTimeRange}
selected={timeRange}
/>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,94 @@
import React, {PureComponent} from 'react'
import classnames from 'classnames'
import {Filter} from 'src/logs/containers/LogsPage'
interface Props {
filter: Filter
onDelete: (id: string) => () => void
onToggleStatus: (id: string) => () => void
onToggleOperator: (id: string) => () => void
}
interface State {
expanded: boolean
}
class LogsFilter extends PureComponent<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
expanded: false,
}
}
public render() {
const {
filter: {id},
onDelete,
} = this.props
const {expanded} = this.state
return (
<li className={this.className} onMouseLeave={this.handleMouseLeave}>
{this.label}
<div className="logs-viewer--filter-remove" onClick={onDelete(id)} />
{expanded && this.renderTooltip}
</li>
)
}
private get label(): JSX.Element {
const {
filter: {key, operator, value},
} = this.props
return (
<span
onMouseEnter={this.handleMouseEnter}
>{`${key} ${operator} ${value}`}</span>
)
}
private get className(): string {
const {expanded} = this.state
const {
filter: {enabled},
} = this.props
return classnames('logs-viewer--filter', {
active: expanded,
disabled: !enabled,
})
}
private handleMouseEnter = (): void => {
this.setState({expanded: true})
}
private handleMouseLeave = (): void => {
this.setState({expanded: false})
}
private get renderTooltip(): JSX.Element {
const {
filter: {id, enabled, operator},
onDelete,
onToggleStatus,
onToggleOperator,
} = this.props
const toggleStatusText = enabled ? 'Disable' : 'Enable'
const toggleOperatorText = operator === '==' ? '!=' : '=='
return (
<ul className="logs-viewer--filter-tooltip">
<li onClick={onToggleStatus(id)}>{toggleStatusText}</li>
<li onClick={onToggleOperator(id)}>{toggleOperatorText}</li>
<li onClick={onDelete(id)}>Delete</li>
</ul>
)
}
}
export default LogsFilter

View File

@ -0,0 +1,84 @@
import React, {PureComponent} from 'react'
import {Filter} from 'src/logs/containers/LogsPage'
import FilterBlock from 'src/logs/components/LogsFilter'
interface Props {
numResults: number
filters: Filter[]
onUpdateFilters: (fitlers: Filter[]) => void
}
class LogsFilters extends PureComponent<Props> {
public render() {
const {numResults} = this.props
return (
<div className="logs-viewer--filter-bar">
<label className="logs-viewer--results-text">
Query returned <strong>{numResults} Events</strong>
</label>
<ul className="logs-viewer--filters">{this.renderFilters}</ul>
</div>
)
}
private get renderFilters(): JSX.Element[] {
const {filters} = this.props
return filters.map(filter => (
<FilterBlock
key={filter.id}
filter={filter}
onDelete={this.handleDeleteFilter}
onToggleStatus={this.handleToggleFilterStatus}
onToggleOperator={this.handleToggleFilterOperator}
/>
))
}
private handleDeleteFilter = (id: string) => (): void => {
const {filters, onUpdateFilters} = this.props
const filteredFilters = filters.filter(filter => filter.id !== id)
onUpdateFilters(filteredFilters)
}
private handleToggleFilterStatus = (id: string) => (): void => {
const {filters, onUpdateFilters} = this.props
const filteredFilters = filters.map(filter => {
if (filter.id === id) {
return {...filter, enabled: !filter.enabled}
}
return filter
})
onUpdateFilters(filteredFilters)
}
private handleToggleFilterOperator = (id: string) => (): void => {
const {filters, onUpdateFilters} = this.props
const filteredFilters = filters.map(filter => {
if (filter.id === id) {
return {...filter, operator: this.toggleOperator(filter.operator)}
}
return filter
})
onUpdateFilters(filteredFilters)
}
private toggleOperator = (op: string): string => {
if (op === '==') {
return '!='
}
return '=='
}
}
export default LogsFilters

View File

@ -0,0 +1,13 @@
import React, {PureComponent} from 'react'
class LogsGraphContainer extends PureComponent {
public render() {
return (
<div className="logs-viewer--graph-container">
<div className="logs-viewer--graph">{this.props.children}</div>
</div>
)
}
}
export default LogsGraphContainer

View File

@ -0,0 +1,43 @@
import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react'
interface Props {
searchString: string
onChange: (e: ChangeEvent<HTMLInputElement>) => void
onSearch: () => void
}
class LogsSearchBar extends PureComponent<Props> {
public render() {
const {searchString, onSearch, onChange} = this.props
return (
<div className="logs-viewer--search-bar">
<div className="logs-viewer--search-input">
<input
type="text"
placeholder="Search logs using Keywords or Regular Expressions..."
value={searchString}
onChange={onChange}
onKeyDown={this.handleInputKeyDown}
className="form-control input-sm"
spellCheck={false}
autoComplete="off"
/>
<span className="icon search" />
</div>
<button className="btn btn-sm btn-primary" onClick={onSearch}>
<span className="icon search" />
Search
</button>
</div>
)
}
private handleInputKeyDown = (e: KeyboardEvent<HTMLInputElement>): void => {
if (e.key === 'Enter') {
return this.props.onSearch()
}
}
}
export default LogsSearchBar

View File

@ -0,0 +1,17 @@
import React, {PureComponent} from 'react'
interface Props {
thing: string
}
class LogsTableContainer extends PureComponent<Props> {
public render() {
return (
<div className="logs-viewer--table-container">
<p>{this.props.thing}</p>
</div>
)
}
}
export default LogsTableContainer

View File

@ -1,6 +1,7 @@
import React, {Component} from 'react'
import classnames from 'classnames'
import moment from 'moment'
import _ from 'lodash'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import timeRanges from 'src/logs/data/timeRanges'
@ -56,16 +57,12 @@ class TimeRangeDropdown extends Component<Props, State> {
public render() {
const {selected, preventCustomTimeRange, page} = this.props
const {customTimeRange, isCustomTimeRangeOpen, isOpen} = this.state
const {customTimeRange, isCustomTimeRangeOpen} = this.state
return (
<ClickOutside onClickOutside={this.handleClickOutside}>
<div className="time-range-dropdown">
<div
className={classnames('dropdown dropdown-290', {
open: isOpen,
})}
>
<div className={this.dropdownClassName}>
<div
className="btn btn-sm btn-default dropdown-toggle"
onClick={this.toggleMenu}
@ -132,6 +129,21 @@ class TimeRangeDropdown extends Component<Props, State> {
)
}
private get dropdownClassName(): string {
const {
isOpen,
customTimeRange: {lower, upper},
} = this.state
const absoluteTimeRange = !_.isEmpty(lower) && !_.isEmpty(upper)
return classnames('dropdown', {
'dropdown-290': absoluteTimeRange,
'dropdown-120': !absoluteTimeRange,
open: isOpen,
})
}
private findTimeRangeInputValue = ({upper, lower}: TimeRange) => {
if (upper && lower) {
if (upper === 'now()') {

View File

@ -1,9 +1,29 @@
import React, {PureComponent} from 'react'
import React, {PureComponent, ChangeEvent} from 'react'
import {connect} from 'react-redux'
import {getSourceAsync, setTimeRange, setNamespace} from 'src/logs/actions'
import {
getSourceAndPopulateNamespacesAsync,
setTimeRangeAsync,
setNamespaceAsync,
executeHistogramQueryAsync,
changeZoomAsync,
} from 'src/logs/actions'
import {getSourcesAsync} from 'src/shared/actions/sources'
import {Source, Namespace, TimeRange} from 'src/types'
import LogViewerHeader from 'src/logs/components/LogViewerHeader'
import Graph from 'src/logs/components/LogsGraph'
import Table from 'src/logs/components/LogsTable'
import SearchBar from 'src/logs/components/LogsSearchBar'
import FilterBar from 'src/logs/components/LogsFilterBar'
import LogViewerChart from 'src/logs/components/LogViewerChart'
import {Source, Namespace, TimeRange} from 'src/types'
export interface Filter {
id: string
key: string
value: string
operator: string
enabled: boolean
}
interface Props {
sources: Source[]
@ -12,12 +32,39 @@ interface Props {
currentNamespace: Namespace
getSource: (sourceID: string) => void
getSources: () => void
setTimeRange: (timeRange: TimeRange) => void
setNamespace: (namespace: Namespace) => void
setTimeRangeAsync: (timeRange: TimeRange) => void
setNamespaceAsync: (namespace: Namespace) => void
changeZoomAsync: (timeRange: TimeRange) => void
executeHistogramQueryAsync: () => void
timeRange: TimeRange
histogramData: object[]
}
class LogsPage extends PureComponent<Props> {
interface State {
searchString: string
filters: Filter[]
}
const DUMMY_FILTERS = [
{
id: '0',
key: 'host',
value: 'prod1-rsavage.local',
operator: '==',
enabled: true,
},
]
class LogsPage extends PureComponent<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
searchString: '',
filters: DUMMY_FILTERS,
}
}
public componentDidUpdate() {
if (!this.props.currentSource) {
this.props.getSource(this.props.sources[0].id)
@ -29,27 +76,47 @@ class LogsPage extends PureComponent<Props> {
}
public render() {
const {searchString, filters} = this.state
return (
<div className="page hosts-list-page">
<div className="page-header full-width">
<div className="page-header__container">
<div className="page-header__left">
<h1 className="page-header__title">Log Viewer</h1>
</div>
<div className="page-header__right">{this.header}</div>
</div>
<div className="page">
{this.header}
<div className="page-contents logs-viewer">
<Graph>{this.chart}</Graph>
<SearchBar
searchString={searchString}
onChange={this.handleSearchInputChange}
onSearch={this.handleSubmitSearch}
/>
<FilterBar
numResults={300}
filters={filters}
onUpdateFilters={this.handleUpdateFilters}
/>
<Table thing="snooo" />
</div>
</div>
)
}
private get chart(): JSX.Element {
const {histogramData, timeRange} = this.props
return (
<LogViewerChart
timeRange={timeRange}
data={histogramData}
onZoom={this.handleChartZoom}
/>
)
}
private get header(): JSX.Element {
const {
sources,
currentSource,
currentNamespaces,
timeRange,
currentNamespace,
timeRange,
} = this.props
return (
@ -66,8 +133,23 @@ class LogsPage extends PureComponent<Props> {
)
}
private handleSearchInputChange = (
e: ChangeEvent<HTMLInputElement>
): void => {
this.setState({searchString: e.target.value})
}
private handleSubmitSearch = (): void => {
// do the thing
}
private handleUpdateFilters = (filters: Filter[]): void => {
this.setState({filters})
}
private handleChooseTimerange = (timeRange: TimeRange) => {
this.props.setTimeRange(timeRange)
this.props.setTimeRangeAsync(timeRange)
this.props.executeHistogramQueryAsync()
}
private handleChooseSource = (sourceID: string) => {
@ -75,27 +157,41 @@ class LogsPage extends PureComponent<Props> {
}
private handleChooseNamespace = (namespace: Namespace) => {
// Do flip
this.props.setNamespace(namespace)
this.props.setNamespaceAsync(namespace)
}
private handleChartZoom = (lower, upper) => {
if (lower) {
this.props.changeZoomAsync({lower, upper})
}
}
}
const mapStateToProps = ({
sources,
logs: {currentSource, currentNamespaces, timeRange, currentNamespace},
logs: {
currentSource,
currentNamespaces,
timeRange,
currentNamespace,
histogramData,
},
}) => ({
sources,
currentSource,
currentNamespaces,
timeRange,
currentNamespace,
histogramData,
})
const mapDispatchToProps = {
getSource: getSourceAsync,
getSource: getSourceAndPopulateNamespacesAsync,
getSources: getSourcesAsync,
setTimeRange,
setNamespace,
setTimeRangeAsync,
setNamespaceAsync,
executeHistogramQueryAsync,
changeZoomAsync,
}
export default connect(mapStateToProps, mapDispatchToProps)(LogsPage)

View File

@ -1,18 +1,13 @@
import {Source, Namespace, TimeRange} from 'src/types'
import {ActionTypes, Action} from 'src/logs/actions'
import {LogsState} from 'src/types/localStorage'
interface LogsState {
currentSource: Source | null
currentNamespaces: Namespace[]
currentNamespace: Namespace | null
timeRange: TimeRange
}
const defaultState = {
const defaultState: LogsState = {
currentSource: null,
currentNamespaces: [],
timeRange: {lower: 'now() - 1m', upper: null},
currentNamespace: null,
histogramQueryConfig: null,
histogramData: [],
}
export default (state: LogsState = defaultState, action: Action) => {
@ -25,6 +20,13 @@ export default (state: LogsState = defaultState, action: Action) => {
return {...state, timeRange: action.payload.timeRange}
case ActionTypes.SetNamespace:
return {...state, currentNamespace: action.payload.namespace}
case ActionTypes.SetHistogramQueryConfig:
return {...state, histogramQueryConfig: action.payload.queryConfig}
case ActionTypes.SetHistogramData:
return {...state, histogramData: action.payload.data}
case ActionTypes.ChangeZoom:
const {timeRange, data} = action.payload
return {...state, timeRange, histogramData: data}
default:
return state
}

View File

@ -0,0 +1,42 @@
import uuid from 'uuid'
import {TimeRange, Namespace, QueryConfig} from 'src/types'
const fields = [
{
alias: '',
args: [
{
alias: '',
type: 'field',
value: 'message',
},
],
type: 'func',
value: 'count',
},
]
const defaultQueryConfig = {
areTagsAccepted: false,
fields,
fill: '0',
groupBy: {time: '2m', tags: []},
measurement: 'syslog',
rawText: null,
shifts: [],
tags: {},
}
export const buildHistogramQueryConfig = (
namespace: Namespace,
range: TimeRange
): QueryConfig => {
const id = uuid.v4()
return {
...defaultQueryConfig,
id,
range,
database: namespace.database,
retentionPolicy: namespace.retentionPolicy,
}
}

View File

@ -4,24 +4,7 @@ import {removeUnselectedTemplateValues} from 'src/dashboards/constants'
import {intervalValuesPoints} from 'src/shared/constants'
interface TemplateQuery {
db: string
rp: string
influxql: string
}
interface TemplateValue {
type: string
value: string
selected: boolean
}
interface Template {
type: string
tempVar: string
query: TemplateQuery
values: TemplateValue[]
}
import {Template} from 'src/types'
interface Query {
host: string | string[]

View File

@ -4,6 +4,7 @@ import _ from 'lodash'
import {fetchTimeSeries} from 'src/shared/apis/query'
import {DEFAULT_TIME_SERIES} from 'src/shared/constants/series'
import {TimeSeriesServerResponse, TimeSeriesResponse} from 'src/types/series'
import {Template} from 'src/types'
interface Axes {
bounds: {
@ -21,25 +22,6 @@ interface Query {
id: string
}
interface TemplateQuery {
db: string
rp: string
influxql: string
}
interface TemplateValue {
type: string
value: string
selected: boolean
}
interface Template {
type: string
tempVar: string
query: TemplateQuery
values: TemplateValue[]
}
export interface Props {
type: string
autoRefresh: number
@ -49,6 +31,7 @@ export interface Props {
axes: Axes
editQueryStatus: () => void
grabDataForDownload: (timeSeries: TimeSeriesServerResponse[]) => void
onSetResolution?: (resolution: number) => void
}
interface State {
@ -180,9 +163,13 @@ const AutoRefresh = (
)
}
private setResolution = resolution => {
private setResolution = (resolution: number) => {
const {onSetResolution} = this.props
if (resolution !== this.state.resolution) {
this.setState({resolution})
if (onSetResolution) {
onSetResolution(resolution)
}
}
}

View File

@ -88,8 +88,8 @@ class Dygraph extends Component {
this.dygraph = new Dygraphs(graphRef, timeSeries, {
...defaultOptions,
...options,
...OPTIONS,
...options,
})
const {w} = this.dygraph.getArea()

View File

@ -4,7 +4,7 @@ import WidgetCell from 'shared/components/WidgetCell'
import LayoutCell from 'shared/components/LayoutCell'
import RefreshingGraph from 'shared/components/RefreshingGraph'
import {buildQueriesForLayouts} from 'utils/buildQueriesForLayouts'
import {IS_STATIC_LEGEND} from 'src/shared/constants'
import {IS_STATIC_LEGEND, defaultIntervalValue} from 'src/shared/constants'
import _ from 'lodash'
@ -24,6 +24,7 @@ const getSource = (cell, source, sources, defaultSource) => {
class LayoutState extends Component {
state = {
cellData: [],
resolution: defaultIntervalValue,
}
grabDataForDownload = cellData => {
@ -36,9 +37,14 @@ class LayoutState extends Component {
{...this.props}
{...this.state}
grabDataForDownload={this.grabDataForDownload}
onSetResolution={this.handleSetResolution}
/>
)
}
handleSetResolution = resolution => {
this.setState({resolution})
}
}
const Layout = (
@ -60,6 +66,7 @@ const Layout = (
sources,
onZoom,
cellData,
resolution,
templates,
timeRange,
isEditable,
@ -69,6 +76,7 @@ const Layout = (
autoRefresh,
manualRefresh,
onDeleteCell,
onSetResolution,
onCancelEditCell,
onStopAddAnnotation,
onSummonOverlayTechnologies,
@ -79,7 +87,9 @@ const Layout = (
<LayoutCell
cell={cell}
cellData={cellData}
templates={templates}
isEditable={isEditable}
resolution={resolution}
onEditCell={onEditCell}
onCloneCell={onCloneCell}
onDeleteCell={onDeleteCell}
@ -115,6 +125,7 @@ const Layout = (
timeRange,
host
)}
onSetResolution={onSetResolution}
/>
)}
</LayoutCell>

View File

@ -2,7 +2,6 @@ import React, {Component, ReactElement} from 'react'
import _ from 'lodash'
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
import LayoutCellMenu from 'src/shared/components/LayoutCellMenu'
import LayoutCellHeader from 'src/shared/components/LayoutCellHeader'
import {notify} from 'src/shared/actions/notifications'
@ -11,7 +10,9 @@ import download from 'src/external/download.js'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {dataToCSV} from 'src/shared/parsing/dataToCSV'
import {timeSeriesToTableGraph} from 'src/utils/timeSeriesTransformers'
import {Cell, CellQuery} from 'src/types/dashboard'
import {PREDEFINED_TEMP_VARS} from 'src/shared/constants'
import {Cell, CellQuery, Template} from 'src/types/'
import {TimeSeriesServerResponse} from 'src/types/series'
interface Props {
@ -23,6 +24,8 @@ interface Props {
isEditable: boolean
onCancelEditCell: () => void
cellData: TimeSeriesServerResponse[]
templates: Template[]
resolution: number
}
@ErrorHandling
@ -44,12 +47,51 @@ export default class LayoutCell extends Component<Props> {
onCSVDownload={this.handleCSVDownload}
/>
</Authorized>
<LayoutCellHeader cellName={cell.name} isEditable={isEditable} />
<LayoutCellHeader cellName={this.cellName} isEditable={isEditable} />
<div className="dash-graph--container">{this.renderGraph}</div>
</div>
)
}
private get cellName(): string {
const {
cell: {name},
} = this.props
return this.replaceTemplateVariables(name)
}
private get userDefinedTemplateVariables(): Template[] {
const {templates} = this.props
return templates.filter(temp => {
const isPredefinedTempVar: boolean = !!PREDEFINED_TEMP_VARS.find(
t => t === temp.tempVar
)
return !isPredefinedTempVar
})
}
private replaceTemplateVariables = (str: string): string => {
const isTemplated: boolean = _.get(str.match(/:/g), 'length', 0) >= 2 // tempVars are wrapped in :
if (isTemplated) {
const renderedString = _.reduce<Template, string>(
this.userDefinedTemplateVariables,
(acc, template) => {
const {tempVar} = template
const templateValue = template.values.find(v => v.selected)
const value = templateValue.value
const regex = new RegExp(tempVar, 'g')
return acc.replace(regex, value)
},
str
)
return renderedString
}
return str
}
private get queries(): CellQuery[] {
const {cell} = this.props
return _.get(cell, ['queries'], [])

View File

@ -199,6 +199,7 @@ LineGraph.propTypes = {
displayOptions: shape({
stepPlot: bool,
stackedGraph: bool,
animatedZooms: bool,
}),
activeQueryIndex: number,
ruleValues: shape({}),

View File

@ -40,6 +40,7 @@ const RefreshingGraph = ({
fieldOptions,
timeFormat,
decimalPlaces,
onSetResolution,
resizerTopHeight,
staticLegend,
manualRefresh, // when changed, re-mounts the component
@ -72,6 +73,7 @@ const RefreshingGraph = ({
prefix={prefix}
suffix={suffix}
inView={inView}
onSetResolution={onSetResolution}
/>
)
}
@ -92,6 +94,7 @@ const RefreshingGraph = ({
prefix={prefix}
suffix={suffix}
inView={inView}
onSetResolution={onSetResolution}
/>
)
}
@ -118,6 +121,7 @@ const RefreshingGraph = ({
grabDataForDownload={grabDataForDownload}
handleSetHoverTime={handleSetHoverTime}
isInCEO={isInCEO}
onSetResolution={onSetResolution}
/>
)
}
@ -147,6 +151,7 @@ const RefreshingGraph = ({
grabDataForDownload={grabDataForDownload}
handleSetHoverTime={handleSetHoverTime}
showSingleStat={type === 'line-plus-single-stat'}
onSetResolution={onSetResolution}
/>
)
}
@ -197,6 +202,7 @@ RefreshingGraph.propTypes = {
hoverTime: string.isRequired,
handleSetHoverTime: func.isRequired,
isInCEO: bool,
onSetResolution: func,
}
RefreshingGraph.defaultProps = {

View File

@ -438,8 +438,9 @@ export const DEFAULT_SOURCE = {
metaUrl: '',
}
export const defaultIntervalValue = '333'
export const intervalValuesPoints = [
{value: '333', type: 'points', selected: true},
{value: defaultIntervalValue, type: 'points', selected: true},
]
export const interval = {

View File

@ -640,3 +640,8 @@ export const ifqlUpdated = {
...defaultSuccessNotification,
message: 'Connection Updated. Rejoice!',
}
export const ifqlTimeSeriesError = (message: string) => ({
...defaultErrorNotification,
message: `Could not get data: ${message}`,
})

View File

@ -0,0 +1,38 @@
import Papa from 'papaparse'
import _ from 'lodash'
import uuid from 'uuid'
import {ScriptResult} from 'src/types'
export const parseResults = (response: string): ScriptResult[] => {
const trimmedReponse = response.trim()
if (_.isEmpty(trimmedReponse)) {
return []
}
return trimmedReponse.split(/\n\s*\n/).map(parseResult)
}
export const parseResult = (raw: string, index: number): ScriptResult => {
const lines = raw.split('\n')
const rawMetadata: string = lines
.filter(line => line.startsWith('#'))
.map(line => line.slice(1))
.join('\n')
const rawData: string = lines.filter(line => !line.startsWith('#')).join('\n')
const metadata = Papa.parse(rawMetadata).data
const data = Papa.parse(rawData).data
const headerRow = _.get(data, '0', [])
const measurementHeaderIndex = headerRow.findIndex(v => v === '_measurement')
const name = _.get(data, `1.${measurementHeaderIndex}`, `Result ${index}`)
return {
id: uuid.v4(),
name,
data,
metadata,
}
}

View File

@ -80,7 +80,7 @@ class SideNav extends PureComponent<Props> {
<FeatureFlag name="log-viewer">
<NavBlock
highlightWhen={['logs']}
icon="cubo-node"
icon="text-block"
link={'/logs'}
location={location}
>

View File

@ -83,6 +83,7 @@
@import 'pages/tickscript-editor';
@import 'pages/time-machine';
@import 'pages/manage-providers';
@import 'pages/logs-viewer';
// TODO
@import 'unsorted';

View File

@ -174,7 +174,7 @@
min-width: 350px;
user-select: text;
transform: translateX(-50%);
box-shadow: 0 0 10px 2px $g2-kevlar;
@extend %drop-shadow;
&.hidden {
display: none !important;

View File

@ -13,15 +13,25 @@
@include gradient-v($g2-kevlar, $g0-obsidian);
}
.time-machine--graph {
width: calc(100% - 60px);
height: calc(100% - 60px);
.time-machine--sidebar,
.time-machine--vis {
background-color: $g3-castle;
border-radius: $radius;
display: flex;
flex-direction: column;
align-items: stretch;
flex-wrap: nowrap;
height: inherit;
}
.time-machine--sidebar {
flex-basis: 25%;
margin-right: 5px;
}
.time-machine--vis {
flex-basis: 75%;
flex-grow: 1;
}
.time-machine--graph-header {
@ -34,7 +44,6 @@
.time-machine--graph-header .nav.nav-tablist {
width: 180px;
li {
justify-content: center;
flex: 1 0 0;

View File

@ -125,3 +125,8 @@ $scrollbar-offset: 3px;
cursor: default;
}
}
// Shadows
%drop-shadow {
box-shadow: 0 0 10px 2px $g2-kevlar;
}

View File

@ -0,0 +1,204 @@
/*
Styles for Logs Viewer Page
----------------------------------------------------------------------------
*/
$logs-viewer-graph-height: 240px;
$logs-viewer-search-height: 46px;
$logs-viewer-filter-height: 42px;
$logs-viewer-gutter: 60px;
.logs-viewer {
display: flex;
flex-direction: column;
align-items: stretch;
flex-wrap: nowrap;
}
.logs-viewer--graph-container {
padding: 22px ($logs-viewer-gutter - 16px) 10px ($logs-viewer-gutter - 16px);
height: $logs-viewer-graph-height;
@include gradient-v($g2-kevlar, $g0-obsidian);
display: flex;
}
.logs-viewer--search-bar {
display: flex;
align-items: flex-end;
flex-wrap: nowrap;
padding: 0 $logs-viewer-gutter;
height: $logs-viewer-search-height;
background-color: $g3-castle;
}
.logs-viewer--table-container {
padding: 12px $logs-viewer-gutter 30px $logs-viewer-gutter;
height: calc(100% - #{$logs-viewer-graph-height + $logs-viewer-search-height + $logs-viewer-filter-height});
background-color: $g3-castle;
}
// Search Bar
.logs-viewer--search-input {
flex: 1 0 0;
margin-right: 8px;
position: relative;
> span.icon.search {
font-size: 14px;
position: absolute;
top: 50%;
left: 12px;
transform: translateY(-50%);
color: $g8-storm;
transition: color 0.25s ease;
}
> input.form-control.input-sm {
padding-left: 30px;
}
> input.form-control.input-sm:focus + span.icon.search {
color: $c-pool;
}
}
// Filters Bar
.logs-viewer--filter-bar {
display: flex;
align-items: center;
@include no-user-select();
padding: 0 $logs-viewer-gutter;
height: $logs-viewer-filter-height;
background-color: $g3-castle;
}
.logs-viewer--results-text {
margin: 0 12px 0 33px;
padding: 0;
font-size: 13px;
line-height: 13px;
font-weight: 500;
color: $g9-mountain;
strong {
color: $g15-platinum;
font-weight: 700;
}
}
.logs-viewer--filters {
flex: 1 0 0;
margin: 0;
padding: 0;
display: flex;
align-items: center;
}
.logs-viewer--filter {
position: relative;
font-size: 12px;
display: flex;
align-items: center;
list-style: none;
padding: 0 2px 0 8px;
height: 26px;
border-radius: 4px;
background-color: $g5-pepper;
color: $g13-mist;
font-weight: 600;
margin: 2px;
&.disabled {
background-color: $g4-onyx;
color: $g9-mountain;
font-style: italic;
}
&.active {
background-color: $g6-smoke;
color: $g15-platinum;
}
}
.logs-viewer--filter-remove {
outline: none;
width: 24px;
height: 24px;
background-color: transparent;
border: 0;
position: relative;
&:before,
&:after {
position: absolute;
top: 50%;
left: 50%;
width: 12px;
height: 2px;
border-radius: 1px;
background-color: $g8-storm;
transition: background-color 0.25s ease;
content: '';
}
&:before {
transform: translate(-50%, -50%) rotate(-45deg);
}
&:after {
transform: translate(-50%, -50%) rotate(45deg);
}
&:hover {
cursor: pointer;
&:before,
&:after {
background-color: $c-dreamsicle;
}
}
}
.logs-viewer--filter-tooltip {
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: stretch;
border-radius: $radius;
z-index: 9999;
@extend %drop-shadow;
background-color: $g4-onyx;
margin: 0;
padding: 0;
list-style: none;
overflow: hidden;
> li {
height: 26px;
line-height: 26px;
padding: 0 8px;
font-size: 13px;
font-weight: 600;
color: $g11-sidewalk;
transition: background-color 0.25s ease, color 0.25s ease;
margin: 0;
&:hover {
cursor: pointer;
background-color: $g5-pepper;
color: $g18-cloud;
}
}
}
// Graph
.logs-viewer--graph {
position: relative;
width: 100%;
height: 100%;
padding: 8px 16px;
}

View File

@ -96,7 +96,7 @@ export enum CellType {
Guide = 'guide',
}
interface TemplateValue {
export interface TemplateValue {
value: string
type: string
selected: boolean

View File

@ -10,7 +10,7 @@ export type OnGenerateScript = (script: string) => void
export type OnChangeScript = (script: string) => void
export type OnSubmitScript = () => void
export interface Status {
export interface ScriptStatus {
type: string
text: string
}
@ -122,3 +122,12 @@ export interface Links {
suggestions: string
ast: string
}
// ScriptResult is the result of a request to IFQL
// https://github.com/influxdata/platform/blob/master/query/docs/SPEC.md#response-format
export interface ScriptResult {
id: string
name: string
data: string[][]
metadata: string[][]
}

View File

@ -1,3 +1,4 @@
import {LayoutCell, LayoutQuery} from './layouts'
import {Service, NewService} from './services'
import {AuthLinks, Organization, Role, User, Me} from './auth'
import {Template, Cell, CellQuery, Legend, Axes} from './dashboard'
@ -21,6 +22,7 @@ import {AlertRule, Kapacitor, Task} from './kapacitor'
import {Source, SourceLinks} from './sources'
import {DropdownAction, DropdownItem} from './shared'
import {Notification, NotificationFunc} from './notifications'
import {ScriptResult, ScriptStatus} from './ifql'
export {
Me,
@ -58,4 +60,8 @@ export {
Axes,
Service,
NewService,
LayoutCell,
LayoutQuery,
ScriptResult,
ScriptStatus,
}

16
ui/src/types/layouts.ts Normal file
View File

@ -0,0 +1,16 @@
export interface LayoutCell {
x: number
y: number
w: number
h: number
i: string
name: string
queries: LayoutQuery[]
}
export interface LayoutQuery {
query: string
label: string
wheres: string[]
groupbys: string[]
}

View File

@ -1,4 +1,13 @@
import {QueryConfig, TimeRange} from 'src/types'
import {QueryConfig, TimeRange, Namespace, Source} from 'src/types'
export interface LogsState {
currentSource: Source | null
currentNamespaces: Namespace[]
currentNamespace: Namespace | null
timeRange: TimeRange
histogramQueryConfig: QueryConfig | null
histogramData: object[]
}
export interface LocalStorage {
VERSION: VERSION
@ -8,6 +17,7 @@ export interface LocalStorage {
dataExplorerQueryConfigs: DataExplorerQueryConfigs
timeRange: TimeRange
script: string
logs: LogsState
}
export type VERSION = string

View File

@ -1,18 +1,23 @@
import {buildQuery} from 'utils/influxql'
import {buildQuery} from 'src/utils/influxql'
import {TYPE_SHIFTED, TYPE_QUERY_CONFIG} from 'src/dashboards/constants'
import {
TEMP_VAR_DASHBOARD_TIME,
TEMP_VAR_UPPER_DASHBOARD_TIME,
} from 'src/shared/constants'
import {timeRanges} from 'shared/data/timeRanges'
import {timeRanges} from 'src/shared/data/timeRanges'
import {Source, LayoutQuery, TimeRange} from 'src/types'
const buildCannedDashboardQuery = (query, {lower, upper}, host) => {
const buildCannedDashboardQuery = (
query: LayoutQuery,
{lower, upper}: TimeRange,
host: string
): string => {
const {defaultGroupBy} = timeRanges.find(range => range.lower === lower) || {
defaultGroupBy: '5m',
}
const {wheres, groupbys} = query
let text = query.text
let text = query.query
if (upper) {
text += ` where time > '${lower}' AND time < '${upper}'`
@ -43,7 +48,12 @@ const buildCannedDashboardQuery = (query, {lower, upper}, host) => {
return text
}
export const buildQueriesForLayouts = (cell, source, timeRange, host) => {
export const buildQueriesForLayouts = (
cell,
source: Source,
timeRange: TimeRange,
host: string
) => {
return cell.queries.map(query => {
let queryText
// Canned dashboards use an different a schema different from queryConfig.

View File

@ -8,15 +8,9 @@ import {
TYPE_IFQL,
} from 'src/dashboards/constants'
import {shiftTimeRange} from 'src/shared/query/helpers'
import {QueryConfig, Field, GroupBy, TimeShift} from 'src/types'
import {QueryConfig, Field, GroupBy, TimeShift, TimeRange} from 'src/types'
export const quoteIfTimestamp = ({
lower,
upper,
}: {
lower: string
upper: string
}): {lower: string; upper: string} => {
export const quoteIfTimestamp = ({lower, upper}: TimeRange): TimeRange => {
if (lower && lower.includes('Z') && !lower.includes("'")) {
lower = `'${lower}'`
}
@ -29,7 +23,7 @@ export const quoteIfTimestamp = ({
}
export default function buildInfluxQLQuery(
timeRange,
timeRange: TimeRange,
config: QueryConfig,
shift: string = ''
): string {
@ -66,7 +60,7 @@ function buildSelect(
// type arg will reason about new query types i.e. IFQL, GraphQL, or queryConfig
export const buildQuery = (
type: string,
timeRange: object,
timeRange: TimeRange,
config: QueryConfig,
shift: TimeShift | null = null
): string => {
@ -201,5 +195,7 @@ function buildFill(fill: string): string {
return ` FILL(${fill})`
}
export const buildRawText = (config: QueryConfig, timeRange: object): string =>
config.rawText || buildInfluxQLQuery(timeRange, config) || ''
export const buildRawText = (
config: QueryConfig,
timeRange: TimeRange
): string => config.rawText || buildInfluxQLQuery(timeRange, config) || ''

View File

@ -6,14 +6,14 @@ const setup = () => {
const props = {
script: '',
body: [],
data: '',
status: {type: '', text: ''},
data: [],
suggestions: [],
onSubmitScript: () => {},
onChangeScript: () => {},
onAnalyze: () => {},
onAppendFrom: () => {},
onAppendJoin: () => {},
status: {type: '', text: ''},
}
const wrapper = shallow(<TimeMachine {...props} />)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,53 @@
import {parseResults} from 'src/shared/parsing/ifql'
import {
RESPONSE_NO_METADATA,
RESPONSE_METADATA,
RESPONSE_NO_MEASUREMENT,
LARGE_RESPONSE,
EXPECTED_METADATA,
EXPECTED_COLUMNS,
} from 'test/shared/parsing/constants'
describe('IFQL response parser', () => {
it('parseResults into the right number of tables', () => {
const result = parseResults(LARGE_RESPONSE)
expect(result).toHaveLength(47)
})
describe('headers', () => {
it('can parse headers when no metadata is present', () => {
const actual = parseResults(RESPONSE_NO_METADATA)[0].data[0]
expect(actual).toEqual(EXPECTED_COLUMNS)
})
it('can parse headers when metadata is present', () => {
const actual = parseResults(RESPONSE_METADATA)[0].data[0]
expect(actual).toEqual(EXPECTED_COLUMNS)
})
it('returns the approriate metadata', () => {
const actual = parseResults(RESPONSE_METADATA)[0].metadata
expect(actual).toEqual(EXPECTED_METADATA)
})
})
describe('name', () => {
it('uses the measurement as a name when present', () => {
const actual = parseResults(RESPONSE_METADATA)[0].name
const expected = 'cpu'
expect(actual).toBe(expected)
})
it('uses the index as a name if a measurement column is not present', () => {
const actual = parseResults(RESPONSE_NO_MEASUREMENT)[0].name
const expected = 'Result 0'
expect(actual).toBe(expected)
})
})
})

View File

@ -65,6 +65,10 @@
version "9.6.6"
resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.6.tgz#439b91f9caf3983cad2eef1e11f6bedcbf9431d2"
"@types/papaparse@^4.1.34":
version "4.1.34"
resolved "https://registry.yarnpkg.com/@types/papaparse/-/papaparse-4.1.34.tgz#893628fbb70243313e46d1b962989c023184783b"
"@types/prop-types@*", "@types/prop-types@^15.5.2":
version "15.5.2"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.2.tgz#3c6b8dceb2906cc87fe4358e809f9d20c8d59be1"
@ -6297,6 +6301,10 @@ pako@~1.0.5:
version "1.0.6"
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258"
papaparse@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-4.4.0.tgz#6bcdbda80873e00cfb0bdcd7a4571c72a9a40168"
parallel-transform@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.1.0.tgz#d410f065b05da23081fcd10f28854c29bda33b06"