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 
<[email protected]>
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();
+    }
+  }
 }

Reply via email to