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