You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iotdb.apache.org by ro...@apache.org on 2022/04/29 07:00:30 UTC
[iotdb] branch master updated: [IOTDB-2285] IoTDB Grafana Plugin: Grafana Connector Input Style (#5661)
This is an automated email from the ASF dual-hosted git repository.
rong pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git
The following commit(s) were added to refs/heads/master by this push:
new 8c6b8f82dc [IOTDB-2285] IoTDB Grafana Plugin: Grafana Connector Input Style (#5661)
8c6b8f82dc is described below
commit 8c6b8f82dc1402fd406d622ec8ee4568d07fde06
Author: CloudWise-Lukemiao <76...@users.noreply.github.com>
AuthorDate: Fri Apr 29 15:00:25 2022 +0800
[IOTDB-2285] IoTDB Grafana Plugin: Grafana Connector Input Style (#5661)
---
grafana-plugin/pkg/plugin/plugin.go | 77 +++++-
grafana-plugin/src/QueryEditor.tsx | 295 ++++++++++++++++++---
.../{WhereValue.tsx => AggregateFun.tsx} | 40 +--
.../componments/{WhereValue.tsx => FillValue.tsx} | 8 +-
grafana-plugin/src/componments/GroupBy.tsx | 58 ++++
grafana-plugin/src/componments/SelectValue.tsx | 1 +
grafana-plugin/src/componments/TimeSeries.tsx | 82 ++++++
grafana-plugin/src/componments/WhereValue.tsx | 1 +
grafana-plugin/src/datasource.ts | 66 ++++-
grafana-plugin/src/functions.ts | 3 +-
grafana-plugin/src/types.ts | 30 ++-
openapi/src/main/openapi3/iotdb-rest.yaml | 23 ++
.../protocol/rest/handler/QueryDataSetHandler.java | 14 +
.../protocol/rest/impl/GrafanaApiServiceImpl.java | 41 +++
14 files changed, 661 insertions(+), 78 deletions(-)
diff --git a/grafana-plugin/pkg/plugin/plugin.go b/grafana-plugin/pkg/plugin/plugin.go
index ae20888964..37c8cb6ca6 100644
--- a/grafana-plugin/pkg/plugin/plugin.go
+++ b/grafana-plugin/pkg/plugin/plugin.go
@@ -21,8 +21,11 @@ import (
"context"
"encoding/base64"
"encoding/json"
+ "errors"
"io"
"net/http"
+ "strconv"
+ "strings"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
@@ -98,7 +101,27 @@ type dataSourceModel struct {
Url string `json:"url"`
}
+type groupBy struct {
+ GroupByLevel string `json:"groupByLevel"`
+ SamplingInterval string `json:"samplingInterval"`
+ Step string `json:"step"`
+}
+
type queryParam struct {
+ Expression []string `json:"expression"`
+ PrefixPath []string `json:"prefixPath"`
+ StartTime int64 `json:"startTime"`
+ EndTime int64 `json:"endTime"`
+ Condition string `json:"condition"`
+ Control string `json:"control"`
+ Aggregated string `json:"aggregated"`
+ Paths []string `json:"paths"`
+ AggregateFun string `json:"aggregateFun"`
+ FillClauses string `json:"fillClauses"`
+ GroupBy groupBy `json:"groupBy"`
+}
+
+type QueryDataReq struct {
Expression []string `json:"expression"`
PrefixPath []string `json:"prefixPath"`
StartTime int64 `json:"startTime"`
@@ -112,6 +135,8 @@ type QueryDataResponse struct {
Timestamps []int64 `json:"timestamps"`
Values [][]float32 `json:"values"`
ColumnNames interface{} `json:"columnNames"`
+ Code int32 `json:"code"`
+ Message string `json:"message"`
}
type loginStatus struct {
@@ -119,12 +144,17 @@ type loginStatus struct {
Message string `json:"message"`
}
+func NewQueryDataReq(expression []string, prefixPath []string, startTime int64, endTime int64, condition string, control string) *QueryDataReq {
+ return &QueryDataReq{Expression: expression, PrefixPath: prefixPath, StartTime: startTime, EndTime: endTime, Condition: condition, Control: control}
+}
+
func (d *IoTDBDataSource) query(cxt context.Context, pCtx backend.PluginContext, query backend.DataQuery) backend.DataResponse {
response := backend.DataResponse{}
var authorization = "Basic " + base64.StdEncoding.EncodeToString([]byte(d.Username+":"+d.Password))
// Unmarshal the JSON into our queryModel.
var qp queryParam
+ var qdReq QueryDataReq
response.Error = json.Unmarshal(query.JSON, &qp)
if response.Error != nil {
return response
@@ -133,10 +163,35 @@ func (d *IoTDBDataSource) query(cxt context.Context, pCtx backend.PluginContext,
qp.EndTime = query.TimeRange.To.UnixNano() / 1000000
client := &http.Client{}
- qpJson, _ := json.Marshal(qp)
+ if qp.Aggregated == "Aggregation" {
+ qp.Control = ""
+ var expressions []string = qp.Paths[len(qp.Paths)-1:]
+ var paths []string = qp.Paths[0 : len(qp.Paths)-1]
+ path := "root." + strings.Join(paths, ".")
+ var prefixPaths = []string{path}
+ if qp.AggregateFun != "" {
+ expressions[0] = qp.AggregateFun + "(" + expressions[0] + ")"
+ }
+ if qp.GroupBy.SamplingInterval != "" && qp.GroupBy.Step == "" {
+ qp.Control += " group by([" + strconv.FormatInt(qp.StartTime, 10) + "," + strconv.FormatInt(qp.EndTime, 10) + ")," + qp.GroupBy.SamplingInterval + ")"
+ }
+ if qp.GroupBy.SamplingInterval != "" && qp.GroupBy.Step != "" {
+ qp.Control += " group by([" + strconv.FormatInt(qp.StartTime, 10) + "," + strconv.FormatInt(qp.EndTime, 10) + ")," + qp.GroupBy.SamplingInterval + "," + qp.GroupBy.Step + ")"
+ }
+ if qp.GroupBy.GroupByLevel != "" {
+ qp.Control += " " + qp.GroupBy.GroupByLevel
+ }
+ if qp.FillClauses != "" {
+ qp.Control += " fill" + qp.FillClauses
+ }
+ qdReq = *NewQueryDataReq(expressions, prefixPaths, qp.StartTime, qp.EndTime, qp.Condition, qp.Control)
+ } else {
+ qdReq = *NewQueryDataReq(qp.Expression, qp.PrefixPath, qp.StartTime, qp.EndTime, qp.Condition, qp.Control)
+ }
+ qpJson, _ := json.Marshal(qdReq)
reader := bytes.NewReader(qpJson)
- var dataSourceUrl = DataSourceUrlHandler(d.Ulr);
+ var dataSourceUrl = DataSourceUrlHandler(d.Ulr)
request, _ := http.NewRequest(http.MethodPost, dataSourceUrl+"/grafana/v1/query/expression", reader)
request.Header.Set("Content-Type", "application/json")
@@ -155,7 +210,11 @@ func (d *IoTDBDataSource) query(cxt context.Context, pCtx backend.PluginContext,
}
defer rsp.Body.Close()
+ if queryDataResp.Code > 0 {
+ response.Error = errors.New(queryDataResp.Message)
+ log.DefaultLogger.Error(queryDataResp.Message)
+ }
// create data frame response.
frame := data.NewFrame("response")
for i := 0; i < len(queryDataResp.Expressions); i++ {
@@ -176,12 +235,12 @@ func (d *IoTDBDataSource) query(cxt context.Context, pCtx backend.PluginContext,
}
// Whether the last character of the URL for processing datasource configuration is "/"
-func DataSourceUrlHandler(url string) string{
- var lastCharacter = url[len(url)-1:len(url)]
- if lastCharacter == "/"{
- url = url[0:len(url)-1]
- }
- return url;
+func DataSourceUrlHandler(url string) string {
+ var lastCharacter = url[len(url)-1 : len(url)]
+ if lastCharacter == "/" {
+ url = url[0 : len(url)-1]
+ }
+ return url
}
// CheckHealth handles health checks sent from Grafana to the plugin.
@@ -194,7 +253,7 @@ func (d *IoTDBDataSource) CheckHealth(_ context.Context, req *backend.CheckHealt
var status = backend.HealthStatusError
var message = "Data source is not working properly"
- var dataSourceUrl = DataSourceUrlHandler(d.Ulr);
+ var dataSourceUrl = DataSourceUrlHandler(d.Ulr)
client := &http.Client{}
request, err := http.NewRequest(http.MethodGet, dataSourceUrl+"/grafana/v1/login", nil)
diff --git a/grafana-plugin/src/QueryEditor.tsx b/grafana-plugin/src/QueryEditor.tsx
index 5c998bd268..6027359582 100644
--- a/grafana-plugin/src/QueryEditor.tsx
+++ b/grafana-plugin/src/QueryEditor.tsx
@@ -16,24 +16,57 @@
*/
import defaults from 'lodash/defaults';
import React, { ChangeEvent, PureComponent } from 'react';
-import { QueryEditorProps } from '@grafana/data';
+import { QueryEditorProps, SelectableValue } from '@grafana/data';
import { DataSource } from './datasource';
-import { IoTDBOptions, IoTDBQuery } from './types';
-import { QueryInlineField } from './componments/Form';
+import { GroupBy, IoTDBOptions, IoTDBQuery } from './types';
+import { QueryField, QueryInlineField } from './componments/Form';
+import { TimeSeries } from './componments/TimeSeries';
import { SelectValue } from './componments/SelectValue';
-import { FromValue } from 'componments/FromValue';
-import { WhereValue } from 'componments/WhereValue';
-import { ControlValue } from 'componments/ControlValue';
+import { FromValue } from './componments/FromValue';
+import { WhereValue } from './componments/WhereValue';
+import { ControlValue } from './componments/ControlValue';
+import { FillValue } from './componments/FillValue';
+import { Segment } from '@grafana/ui';
+import { toOption } from './functions';
+
+import { GroupByLabel } from './componments/GroupBy';
+import { AggregateFun } from './componments/AggregateFun';
interface State {
expression: string[];
prefixPath: string[];
condition: string;
control: string;
+
+ timeSeries: string[];
+ options: Array<Array<SelectableValue<string>>>;
+ aggregateFun: string;
+ groupBy: GroupBy;
+ fillClauses: string;
+ isAggregated: boolean;
+ aggregated: string;
+ shouldAdd: boolean;
}
+const selectElement = [
+ '---remove---',
+ 'SUM',
+ 'COUNT',
+ 'AVG',
+ 'EXTREME',
+ 'MAX_VALUE',
+ 'MIN_VALUE',
+ 'FIRST_VALUE',
+ 'LAST_VALUE',
+ 'MAX_TIME',
+ 'MIN_TIME',
+];
+
const paths = [''];
const expressions = [''];
+const selectRaw = ['Raw', 'Aggregation'];
+const commonOption: SelectableValue<string> = { label: '*', value: '*' };
+const commonOptionDou: SelectableValue<string> = { label: '**', value: '**' };
type Props = QueryEditorProps<DataSource, IoTDBQuery, IoTDBOptions>;
export class QueryEditor extends PureComponent<Props, State> {
@@ -42,6 +75,18 @@ export class QueryEditor extends PureComponent<Props, State> {
prefixPath: paths,
condition: '',
control: '',
+ timeSeries: [],
+ options: [[toOption('')]],
+ aggregateFun: '',
+ groupBy: {
+ samplingInterval: '',
+ step: '',
+ groupByLevel: '',
+ },
+ fillClauses: '',
+ isAggregated: false,
+ aggregated: selectRaw[0],
+ shouldAdd: true,
};
onSelectValueChange = (exp: string[]) => {
@@ -67,45 +112,229 @@ export class QueryEditor extends PureComponent<Props, State> {
this.setState({ control: c });
};
+ onAggregationsChange = (a: string) => {
+ const { onChange, query } = this.props;
+ if (a === '---remove---') {
+ a = '';
+ }
+ this.setState({ aggregateFun: a });
+ onChange({ ...query, aggregateFun: a });
+ };
+
+ onFillsChange = (f: string) => {
+ const { onChange, query } = this.props;
+ onChange({ ...query, fillClauses: f });
+ this.setState({ fillClauses: f });
+ };
+
+ onGroupByChange = (g: GroupBy) => {
+ const { onChange, query } = this.props;
+ this.setState({ groupBy: g });
+ onChange({ ...query, groupBy: g });
+ };
+
onQueryTextChange = (event: ChangeEvent<HTMLInputElement>) => {
const { onChange, query } = this.props;
- onChange({ ...query, queryText: event.target.value });
+ onChange({ ...query });
+ };
+
+ onSelectRawChange = (event: ChangeEvent<HTMLInputElement>) => {
+ const { onChange, query } = this.props;
+ onChange({ ...query });
+ };
+
+ onTimeSeriesChange = (t: string[], options: Array<Array<SelectableValue<string>>>, isRemove: boolean) => {
+ const { onChange, query } = this.props;
+ const commonOption: SelectableValue<string> = { label: '*', value: '*' };
+ if (t.length === options.length) {
+ this.props.datasource
+ .nodeQuery(['root', ...t])
+ .then((a) => {
+ let b = a.map((a) => a.text).map(toOption);
+ if (b.length > 0) {
+ b = [commonOption, commonOptionDou, ...b];
+ }
+ onChange({ ...query, paths: t, options: [...options, b] });
+ if (isRemove) {
+ this.setState({ timeSeries: t, options: [...options, b], shouldAdd: true });
+ } else {
+ this.setState({ timeSeries: t, options: [...options, b] });
+ }
+ })
+ .catch((e) => {
+ if (e === 'measurement') {
+ onChange({ ...query, paths: t });
+ this.setState({ timeSeries: t, shouldAdd: false });
+ } else {
+ this.setState({ shouldAdd: false });
+ }
+ });
+ } else {
+ this.setState({ timeSeries: t });
+ onChange({ ...query, paths: t });
+ }
};
+ componentDidMount() {
+ if (this.props.query.aggregated) {
+ this.setState({ isAggregated: this.props.query.isAggregated, aggregated: this.props.query.aggregated });
+ } else {
+ this.props.query.aggregated = selectRaw[0];
+ }
+ if (this.state.options.length === 1 && this.state.options[0][0].value === '') {
+ this.props.datasource.nodeQuery(['root']).then((a) => {
+ let b = a.map((a) => a.text).map(toOption);
+ if (b.length > 0) {
+ b = [commonOption, commonOptionDou, ...b];
+ }
+ this.setState({ options: [b] });
+ });
+ }
+ }
+
render() {
const query = defaults(this.props.query);
- const { expression, prefixPath, condition, control } = query;
-
+ var { expression, prefixPath, condition, control, fillClauses, aggregateFun, paths, options, aggregated, groupBy } =
+ query;
return (
<>
{
<>
<div className="gf-form">
- <QueryInlineField label={'SELECT'}>
- <SelectValue
- expressions={expression ? expression : this.state.expression}
- onChange={this.onSelectValueChange}
- />
- </QueryInlineField>
- </div>
- <div className="gf-form">
- <QueryInlineField label={'FROM'}>
- <FromValue
- prefixPath={prefixPath ? prefixPath : this.state.prefixPath}
- onChange={this.onFromValueChange}
- />
- </QueryInlineField>
- </div>
- <div className="gf-form">
- <QueryInlineField label={'WHERE'}>
- <WhereValue condition={condition} onChange={this.onWhereValueChange} />
- </QueryInlineField>
- </div>
- <div className="gf-form">
- <QueryInlineField label={'CONTROL'}>
- <ControlValue control={control} onChange={this.onControlValueChange} />
- </QueryInlineField>
+ <Segment
+ onChange={({ value: value = '' }) => {
+ const { onChange, query } = this.props;
+ if (value === selectRaw[0]) {
+ this.props.query.aggregated = selectRaw[0];
+ this.props.query.aggregateFun = '';
+ const nextTimeSeries = this.props.query.paths.filter((_, i) => i < 0);
+ const nextOptions = this.props.query.options.filter((_, i) => i < 0);
+ this.onTimeSeriesChange(nextTimeSeries, nextOptions, true);
+ if (this.props.query.groupBy?.samplingInterval) {
+ this.props.query.groupBy.samplingInterval = '';
+ }
+ if (this.props.query.groupBy?.groupByLevel) {
+ this.props.query.groupBy.groupByLevel = '';
+ }
+ if (this.props.query.groupBy?.step) {
+ this.props.query.groupBy.step = '';
+ }
+ this.props.query.condition = '';
+ this.props.query.fillClauses = '';
+ this.props.query.isAggregated = false;
+ this.setState({
+ isAggregated: false,
+ aggregated: selectRaw[0],
+ shouldAdd: true,
+ aggregateFun: '',
+ fillClauses: '',
+ condition: '',
+ });
+ onChange({ ...query, aggregated: value, isAggregated: false });
+ } else {
+ this.props.query.aggregated = selectRaw[1];
+ this.props.query.expression = [''];
+ this.props.query.prefixPath = [''];
+ this.props.query.condition = '';
+ this.props.query.control = '';
+ this.props.query.isAggregated = true;
+ this.setState({
+ isAggregated: true,
+ aggregated: selectRaw[1],
+ expression: [''],
+ prefixPath: [''],
+ condition: '',
+ control: '',
+ });
+ onChange({ ...query, aggregated: value, isAggregated: true });
+ }
+ }}
+ options={selectRaw.map(toOption)}
+ value={aggregated ? aggregated : this.state.aggregated}
+ className="query-keyword width-6"
+ />
</div>
+ {!this.state.isAggregated && (
+ <>
+ <div className="gf-form">
+ <QueryInlineField label={'SELECT'}>
+ <SelectValue
+ expressions={expression ? expression : this.state.expression}
+ onChange={this.onSelectValueChange}
+ />
+ </QueryInlineField>
+ </div>
+ <div className="gf-form">
+ <QueryInlineField label={'FROM'}>
+ <FromValue
+ prefixPath={prefixPath ? prefixPath : this.state.prefixPath}
+ onChange={this.onFromValueChange}
+ />
+ </QueryInlineField>
+ </div>
+ <div className="gf-form">
+ <QueryInlineField label={'WHERE'}>
+ <WhereValue
+ condition={condition ? condition : this.state.condition}
+ onChange={this.onWhereValueChange}
+ />
+ </QueryInlineField>
+ </div>
+ <div className="gf-form">
+ <QueryInlineField label={'CONTROL'}>
+ <ControlValue
+ control={control ? control : this.state.control}
+ onChange={this.onControlValueChange}
+ />
+ </QueryInlineField>
+ </div>
+ </>
+ )}
+ {this.state.isAggregated && (
+ <>
+ <div className="gf-form">
+ <QueryInlineField label={'TIME-SERIES'}>
+ <TimeSeries
+ timeSeries={paths ? paths : this.state.timeSeries}
+ onChange={this.onTimeSeriesChange}
+ variableOptionGroup={options ? options : this.state.options}
+ shouldAdd={this.state.shouldAdd}
+ />
+ </QueryInlineField>
+ </div>
+ <div className="gf-form">
+ <QueryInlineField label={'FUNCTION'}>
+ <AggregateFun
+ aggregateFun={aggregateFun ? aggregateFun : this.state.aggregateFun}
+ onChange={this.onAggregationsChange}
+ variableOptionGroup={selectElement.map(toOption)}
+ />
+ </QueryInlineField>
+ </div>
+ <div className="gf-form">
+ <QueryInlineField label={'WHERE'}>
+ <WhereValue
+ condition={condition ? condition : this.state.condition}
+ onChange={this.onWhereValueChange}
+ />
+ </QueryInlineField>
+ </div>
+ <div className="gf-form">
+ <QueryInlineField label={'GROUP BY'}>
+ <QueryField label={'SAMPLING INTERVAL'} />
+ <GroupByLabel groupBy={groupBy ? groupBy : this.state.groupBy} onChange={this.onGroupByChange} />
+ </QueryInlineField>
+ </div>
+ <div className="gf-form">
+ <QueryInlineField label={'FILL'}>
+ <FillValue
+ fill={fillClauses ? fillClauses : this.state.fillClauses}
+ onChange={this.onFillsChange}
+ />
+ </QueryInlineField>
+ </div>
+ </>
+ )}
</>
}
</>
diff --git a/grafana-plugin/src/componments/WhereValue.tsx b/grafana-plugin/src/componments/AggregateFun.tsx
similarity index 56%
copy from grafana-plugin/src/componments/WhereValue.tsx
copy to grafana-plugin/src/componments/AggregateFun.tsx
index bb7c2be668..227bfdb818 100644
--- a/grafana-plugin/src/componments/WhereValue.tsx
+++ b/grafana-plugin/src/componments/AggregateFun.tsx
@@ -14,25 +14,31 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import React, { FunctionComponent } from 'react';
-import { SegmentInput } from '@grafana/ui';
+import { SelectableValue } from '@grafana/data';
+import { Segment } from '@grafana/ui';
export interface Props {
- condition: string;
- onChange: (conditionStr: string) => void;
+ aggregateFun: string;
+ onChange: (path: string) => void;
+ variableOptionGroup: Array<SelectableValue<string>>;
}
-export const WhereValue: FunctionComponent<Props> = ({ condition, onChange }) => (
- <>
- {
- <>
- <SegmentInput
- className="min-width-8"
- placeholder="(optional)"
- value={condition}
- onChange={(string) => onChange(string.toString())}
- />
- </>
- }
- </>
-);
+export const AggregateFun: FunctionComponent<Props> = ({ aggregateFun, onChange, variableOptionGroup }) => {
+ return (
+ <Segment
+ allowCustomValue={false}
+ options={[...variableOptionGroup]}
+ value={aggregateFun}
+ onChange={(item: SelectableValue<string>) => {
+ let itemString = '';
+ if (item.value) {
+ itemString = item.value;
+ }
+ onChange(itemString);
+ }}
+ className="width-6"
+ />
+ );
+};
diff --git a/grafana-plugin/src/componments/WhereValue.tsx b/grafana-plugin/src/componments/FillValue.tsx
similarity index 86%
copy from grafana-plugin/src/componments/WhereValue.tsx
copy to grafana-plugin/src/componments/FillValue.tsx
index bb7c2be668..e336506686 100644
--- a/grafana-plugin/src/componments/WhereValue.tsx
+++ b/grafana-plugin/src/componments/FillValue.tsx
@@ -18,18 +18,18 @@ import React, { FunctionComponent } from 'react';
import { SegmentInput } from '@grafana/ui';
export interface Props {
- condition: string;
- onChange: (conditionStr: string) => void;
+ fill: string;
+ onChange: (fillValue: string) => void;
}
-export const WhereValue: FunctionComponent<Props> = ({ condition, onChange }) => (
+export const FillValue: FunctionComponent<Props> = ({ fill, onChange }) => (
<>
{
<>
<SegmentInput
className="min-width-8"
placeholder="(optional)"
- value={condition}
+ value={fill}
onChange={(string) => onChange(string.toString())}
/>
</>
diff --git a/grafana-plugin/src/componments/GroupBy.tsx b/grafana-plugin/src/componments/GroupBy.tsx
new file mode 100644
index 0000000000..7b45e9de0f
--- /dev/null
+++ b/grafana-plugin/src/componments/GroupBy.tsx
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { GroupBy } from '../types';
+import React, { FunctionComponent } from 'react';
+import { InlineFormLabel, SegmentInput } from '@grafana/ui';
+
+export interface Props {
+ groupBy: GroupBy;
+ onChange: (groupBy: GroupBy) => void;
+}
+
+export const GroupByLabel: FunctionComponent<Props> = ({ groupBy, onChange }) => (
+ <>
+ {
+ <>
+ <SegmentInput
+ value={groupBy.samplingInterval}
+ onChange={(string) => onChange({ ...groupBy, samplingInterval: string.toString() })}
+ className="width-5"
+ placeholder="1s"
+ />
+ <InlineFormLabel className="query-keyword" width={9}>
+ SLIDING STEP
+ </InlineFormLabel>
+ <SegmentInput
+ className="width-5"
+ placeholder="(optional)"
+ value={groupBy.step}
+ onChange={(string) => onChange({ ...groupBy, step: string.toString() })}
+ />
+ <InlineFormLabel className="query-keyword" width={5}>
+ LEVEL
+ </InlineFormLabel>
+ <SegmentInput
+ className="width-5"
+ placeholder="(optional)"
+ value={groupBy.groupByLevel}
+ onChange={(string) => onChange({ ...groupBy, groupByLevel: string.toString() })}
+ />
+ </>
+ }
+ </>
+);
diff --git a/grafana-plugin/src/componments/SelectValue.tsx b/grafana-plugin/src/componments/SelectValue.tsx
index 9e510971b0..a40c557b8c 100644
--- a/grafana-plugin/src/componments/SelectValue.tsx
+++ b/grafana-plugin/src/componments/SelectValue.tsx
@@ -14,6 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import React, { FunctionComponent } from 'react';
import { HorizontalGroup, Icon, SegmentInput, VerticalGroup } from '@grafana/ui';
import { QueryInlineField } from './Form';
diff --git a/grafana-plugin/src/componments/TimeSeries.tsx b/grafana-plugin/src/componments/TimeSeries.tsx
new file mode 100644
index 0000000000..d236a280f4
--- /dev/null
+++ b/grafana-plugin/src/componments/TimeSeries.tsx
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { FunctionComponent } from 'react';
+import { SelectableValue } from '@grafana/data';
+import { Segment, Icon, InlineFormLabel } from '@grafana/ui';
+
+export interface Props {
+ timeSeries: string[];
+ onChange: (path: string[], options: Array<Array<SelectableValue<string>>>, isRemove: boolean) => void;
+ variableOptionGroup: Array<Array<SelectableValue<string>>>;
+ shouldAdd: boolean;
+}
+
+const removeText = '-- remove stat --';
+const removeOption: SelectableValue<string> = { label: removeText, value: removeText };
+
+export const TimeSeries: FunctionComponent<Props> = ({ timeSeries, onChange, variableOptionGroup, shouldAdd }) => {
+ return (
+ <>
+ <>
+ <InlineFormLabel width={3}>root</InlineFormLabel>
+ </>
+ {timeSeries &&
+ timeSeries.map((value, index) => (
+ <>
+ <Segment
+ allowCustomValue={false}
+ key={value + index}
+ value={value}
+ options={[removeOption, ...variableOptionGroup[index]]}
+ onChange={({ value: selectValue = '' }) => {
+ if (selectValue === removeText) {
+ const nextTimeSeries = timeSeries.filter((_, i) => i < index);
+ const nextOptions = variableOptionGroup.filter((_, i) => i < index);
+ onChange(nextTimeSeries, nextOptions, true);
+ } else if (selectValue !== value) {
+ const nextTimeSeries = timeSeries
+ .map((v, i) => (i === index ? selectValue : v))
+ .filter((_, i) => i <= index);
+ const nextOptions = variableOptionGroup.filter((_, i) => i <= index);
+ onChange(nextTimeSeries, nextOptions, true);
+ }
+ }}
+ />
+ </>
+ ))}
+ {shouldAdd && (
+ <Segment
+ Component={
+ <a className="gf-form-label query-part">
+ <Icon name="plus" />
+ </a>
+ }
+ allowCustomValue
+ onChange={(item: SelectableValue<string>) => {
+ let itemString = '';
+ if (item.value) {
+ itemString = item.value;
+ }
+ onChange([...timeSeries, itemString], variableOptionGroup, false);
+ }}
+ options={variableOptionGroup[variableOptionGroup.length - 1]}
+ />
+ )}
+ </>
+ );
+};
diff --git a/grafana-plugin/src/componments/WhereValue.tsx b/grafana-plugin/src/componments/WhereValue.tsx
index bb7c2be668..1f7a2fa4ef 100644
--- a/grafana-plugin/src/componments/WhereValue.tsx
+++ b/grafana-plugin/src/componments/WhereValue.tsx
@@ -14,6 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import React, { FunctionComponent } from 'react';
import { SegmentInput } from '@grafana/ui';
diff --git a/grafana-plugin/src/datasource.ts b/grafana-plugin/src/datasource.ts
index b8371e4006..b8b66d5de0 100644
--- a/grafana-plugin/src/datasource.ts
+++ b/grafana-plugin/src/datasource.ts
@@ -32,17 +32,32 @@ export class DataSource extends DataSourceWithBackend<IoTDBQuery, IoTDBOptions>
this.username = instanceSettings.jsonData.username;
}
applyTemplateVariables(query: IoTDBQuery, scopedVars: ScopedVars) {
- query.expression.map(
- (_, index) => (query.expression[index] = getTemplateSrv().replace(query.expression[index], scopedVars))
- );
- query.prefixPath.map(
- (_, index) => (query.prefixPath[index] = getTemplateSrv().replace(query.prefixPath[index], scopedVars))
- );
- if (query.condition) {
- query.condition = getTemplateSrv().replace(query.condition, scopedVars);
- }
- if (query.control) {
- query.control = getTemplateSrv().replace(query.control, scopedVars);
+ if (query.aggregated === 'Raw') {
+ query.expression.map(
+ (_, index) => (query.expression[index] = getTemplateSrv().replace(query.expression[index], scopedVars))
+ );
+ query.prefixPath.map(
+ (_, index) => (query.prefixPath[index] = getTemplateSrv().replace(query.prefixPath[index], scopedVars))
+ );
+ if (query.condition) {
+ query.condition = getTemplateSrv().replace(query.condition, scopedVars);
+ }
+ if (query.control) {
+ query.control = getTemplateSrv().replace(query.control, scopedVars);
+ }
+ } else {
+ if (query.groupBy?.samplingInterval) {
+ query.groupBy.samplingInterval = getTemplateSrv().replace(query.groupBy.samplingInterval, scopedVars);
+ }
+ if (query.groupBy?.step) {
+ query.groupBy.step = getTemplateSrv().replace(query.groupBy.step, scopedVars);
+ }
+ if (query.groupBy?.groupByLevel) {
+ query.groupBy.groupByLevel = getTemplateSrv().replace(query.groupBy.groupByLevel, scopedVars);
+ }
+ if (query.fillClauses) {
+ query.fillClauses = getTemplateSrv().replace(query.fillClauses, scopedVars);
+ }
}
return query;
}
@@ -53,6 +68,35 @@ export class DataSource extends DataSourceWithBackend<IoTDBQuery, IoTDBOptions>
return this.getVariablesResult(sql);
}
+ nodeQuery(query: any, options?: any): Promise<MetricFindValue[]> {
+ return this.getChildPaths(query);
+ }
+
+ async getChildPaths(detachedPath: string[]) {
+ const myHeader = new Headers();
+ myHeader.append('Content-Type', 'application/json');
+ const Authorization = 'Basic ' + Buffer.from(this.username + ':' + this.password).toString('base64');
+ myHeader.append('Authorization', Authorization);
+ if (this.url.substr(this.url.length - 1, 1) === '/') {
+ this.url = this.url.substr(0, this.url.length - 1);
+ }
+ return await getBackendSrv()
+ .datasourceRequest({
+ method: 'POST',
+ url: this.url + '/grafana/v1/node',
+ data: detachedPath,
+ headers: myHeader,
+ })
+ .then((response) => {
+ if (response.data instanceof Array) {
+ return response.data;
+ } else {
+ throw 'the result is not array';
+ }
+ })
+ .then((data) => data.map(toMetricFindValue));
+ }
+
async getVariablesResult(sql: object) {
const myHeader = new Headers();
myHeader.append('Content-Type', 'application/json');
diff --git a/grafana-plugin/src/functions.ts b/grafana-plugin/src/functions.ts
index 7c2b542582..b139068ccc 100644
--- a/grafana-plugin/src/functions.ts
+++ b/grafana-plugin/src/functions.ts
@@ -14,6 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { MetricFindValue } from '@grafana/data';
+import { MetricFindValue, SelectableValue } from '@grafana/data';
+export const toOption = (value: string) => ({ label: value, value } as SelectableValue<string>);
export const toMetricFindValue = (data: any) => ({ text: data } as MetricFindValue);
diff --git a/grafana-plugin/src/types.ts b/grafana-plugin/src/types.ts
index fa6077918d..10c2705aba 100644
--- a/grafana-plugin/src/types.ts
+++ b/grafana-plugin/src/types.ts
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { DataQuery, DataSourceJsonData } from '@grafana/data';
+import { DataQuery, DataSourceJsonData, SelectableValue } from '@grafana/data';
export interface IoTDBQuery extends DataQuery {
startTime: number;
@@ -22,9 +22,33 @@ export interface IoTDBQuery extends DataQuery {
expression: string[];
prefixPath: string[];
condition: string;
- queryText?: string;
- constant: number;
control: string;
+
+ paths: string[];
+ aggregateFun?: string;
+ aggregated: string;
+ isAggregated: boolean;
+ fillClauses: string;
+ groupBy?: GroupBy;
+ limitAll?: LimitAll;
+ options: Array<Array<SelectableValue<string>>>;
+}
+
+export interface GroupBy {
+ step: string;
+ samplingInterval: string;
+ groupByLevel: string;
+}
+
+export interface Fill {
+ dataType: string;
+ previous: string;
+ duration: string;
+}
+
+export interface LimitAll {
+ slimit: string;
+ limit: string;
}
/**
diff --git a/openapi/src/main/openapi3/iotdb-rest.yaml b/openapi/src/main/openapi3/iotdb-rest.yaml
index 732ea09275..59e2e621fb 100644
--- a/openapi/src/main/openapi3/iotdb-rest.yaml
+++ b/openapi/src/main/openapi3/iotdb-rest.yaml
@@ -142,6 +142,29 @@ paths:
schema:
$ref: '#/components/schemas/VariablesResult'
+ /grafana/v1/node:
+ post:
+ summary: node
+ description: node
+ operationId: node
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: array
+ description: node name (e.g., "root.a.b.c")
+ items:
+ type: string
+ responses:
+ "200":
+ description: NodesResult
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ type: string
+
components:
schemas:
SQL:
diff --git a/server/src/main/java/org/apache/iotdb/db/protocol/rest/handler/QueryDataSetHandler.java b/server/src/main/java/org/apache/iotdb/db/protocol/rest/handler/QueryDataSetHandler.java
index 0ffe5f8d39..6f68d4df19 100644
--- a/server/src/main/java/org/apache/iotdb/db/protocol/rest/handler/QueryDataSetHandler.java
+++ b/server/src/main/java/org/apache/iotdb/db/protocol/rest/handler/QueryDataSetHandler.java
@@ -315,4 +315,18 @@ public class QueryDataSetHandler {
}
return Response.ok().entity(results).build();
}
+
+ public static Response fillGrafanaNodesResult(QueryDataSet queryDataSet) throws IOException {
+ List<String> nodes = new ArrayList<>();
+ while (queryDataSet.hasNext()) {
+ RowRecord rowRecord = queryDataSet.next();
+ List<org.apache.iotdb.tsfile.read.common.Field> fields = rowRecord.getFields();
+ for (Field field : fields) {
+ String nodePaths = field.getObjectValue(field.getDataType()).toString();
+ String[] nodeSubPath = nodePaths.split("\\.");
+ nodes.add(nodeSubPath[nodeSubPath.length - 1]);
+ }
+ }
+ return Response.ok().entity(nodes).build();
+ }
}
diff --git a/server/src/main/java/org/apache/iotdb/db/protocol/rest/impl/GrafanaApiServiceImpl.java b/server/src/main/java/org/apache/iotdb/db/protocol/rest/impl/GrafanaApiServiceImpl.java
index ac024024c1..231ac63c12 100644
--- a/server/src/main/java/org/apache/iotdb/db/protocol/rest/impl/GrafanaApiServiceImpl.java
+++ b/server/src/main/java/org/apache/iotdb/db/protocol/rest/impl/GrafanaApiServiceImpl.java
@@ -20,6 +20,7 @@ package org.apache.iotdb.db.protocol.rest.impl;
import org.apache.iotdb.commons.conf.IoTDBConstant;
import org.apache.iotdb.db.conf.IoTDBDescriptor;
import org.apache.iotdb.db.exception.query.QueryProcessException;
+import org.apache.iotdb.db.metadata.path.PartialPath;
import org.apache.iotdb.db.protocol.rest.GrafanaApiService;
import org.apache.iotdb.db.protocol.rest.NotFoundException;
import org.apache.iotdb.db.protocol.rest.handler.AuthorizationHandler;
@@ -46,6 +47,7 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import java.time.ZoneId;
+import java.util.List;
public class GrafanaApiServiceImpl extends GrafanaApiService {
@@ -181,4 +183,43 @@ public class GrafanaApiServiceImpl extends GrafanaApiService {
.message(TSStatusCode.SUCCESS_STATUS.name()))
.build();
}
+
+ @Override
+ public Response node(List<String> requestBody, SecurityContext securityContext)
+ throws NotFoundException {
+ try {
+ if (requestBody != null && requestBody.size() > 0) {
+ PartialPath path = new PartialPath(Joiner.on(".").join(requestBody));
+ String sql = "show child paths " + path;
+ PhysicalPlan physicalPlan =
+ serviceProvider.getPlanner().parseSQLToGrafanaQueryPlan(sql, ZoneId.systemDefault());
+
+ Response response = authorizationHandler.checkAuthority(securityContext, physicalPlan);
+ if (response != null) {
+ return response;
+ }
+
+ final long queryId = ServiceProvider.SESSION_MANAGER.requestQueryId(true);
+ try {
+ QueryContext queryContext =
+ serviceProvider.genQueryContext(
+ queryId,
+ physicalPlan.isDebug(),
+ System.currentTimeMillis(),
+ sql,
+ IoTDBConstant.DEFAULT_CONNECTION_TIMEOUT_MS);
+ QueryDataSet queryDataSet =
+ serviceProvider.createQueryDataSet(
+ queryContext, physicalPlan, IoTDBConstant.DEFAULT_FETCH_SIZE);
+ return QueryDataSetHandler.fillGrafanaNodesResult(queryDataSet);
+ } finally {
+ ServiceProvider.SESSION_MANAGER.releaseQueryResourceNoExceptions(queryId);
+ }
+ } else {
+ return QueryDataSetHandler.fillGrafanaNodesResult(null);
+ }
+ } catch (Exception e) {
+ return Response.ok().entity(ExceptionHandler.tryCatchException(e)).build();
+ }
+ }
}