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 3ef6bab  [IOTDB-2233] Grafana plugin: add `control` field for the 
`expression` panel (#4662)
3ef6bab is described below

commit 3ef6babe5388e0bef47ac4218c62f7e0bf71f4a5
Author: CloudWise-Lukemiao 
<[email protected]>
AuthorDate: Fri Dec 31 15:58:12 2021 +0800

    [IOTDB-2233] Grafana plugin: add `control` field for the `expression` panel 
(#4662)
    
    Co-authored-by: Steve Yurong Su <[email protected]>
---
 .../Ecosystem Integration/Grafana Plugin.md        |  16 +-
 grafana-plugin/src/QueryEditor.tsx                 |  15 +-
 .../src/{types.ts => componments/ControlValue.tsx} |  44 ++--
 grafana-plugin/src/datasource.ts                   |   3 +
 grafana-plugin/src/types.ts                        |   1 +
 openapi/src/main/openapi3/iotdb-rest.yaml          |   2 +
 .../protocol/rest/impl/GrafanaApiServiceImpl.java  |   3 +
 .../db/protocol/rest/GrafanaApiServiceIT.java      | 293 +++++++++++++++++++++
 8 files changed, 351 insertions(+), 26 deletions(-)

diff --git a/docs/zh/UserGuide/Ecosystem Integration/Grafana Plugin.md 
b/docs/zh/UserGuide/Ecosystem Integration/Grafana Plugin.md
index 0be61a7..fb308fa 100644
--- a/docs/zh/UserGuide/Ecosystem Integration/Grafana Plugin.md 
+++ b/docs/zh/UserGuide/Ecosystem Integration/Grafana Plugin.md 
@@ -191,7 +191,7 @@ Ip 为您的 IoTDB 服务器所在的宿主机 IP,port 为 REST 服务的运
 
 <img style="width:100%; max-width:800px; max-height:600px; margin-left:auto; 
margin-right:auto; display:block;" 
src="https://github.com/apache/iotdb-bin-resources/blob/main/docs/UserGuide/Ecosystem%20Integration/Grafana-plugin/add%20empty%20panel.png?raw=true";>
 
-在 SELECT 输入框、FROM 输入框、WHERE 输入框输入内容,其中 WHERE 输入框为非必填。
+在 SELECT 输入框、FROM 输入框、WHERE输入框、CONTROL输入框中输入内容,其中 WHERE 和 CONTROL 输入框为非必填。
 
 如果一个查询涉及多个表达式,我们可以点击 SELECT 输入框右侧的 `+` 来添加 SELECT 子句中的表达式,也可以点击 FROM 输入框右侧的 
`+` 来添加路径前缀,如下图所示:
 
@@ -206,6 +206,20 @@ SELECT 输入框中的内容可以是时间序列的后缀,可以是函数或
 *  `sin(s1) + cos(s1 + s2)` 
 *  `udf(s1) as "中文别名"`
 
+FROM 输入框中的内容必须是时间序列的前缀路径,比如 `root.sg.d`。
+
+WHERE 输入框为非必须填写项目,填写内容应当是查询的过滤条件,比如 `time > 0`  或者 `s1 < 1024 and s2 > 1024`。
+
+CONTROL 输入框为非必须填写项目,填写内容应当是控制查询类型、输出格式的特殊子句,下面是 CONTROL 输入框中一些合法的输入举例:
+
+*  `group by ([2017-11-01T00:00:00, 2017-11-07T23:00:00), 1d)`
+*  `group by ([2017-11-01 00:00:00, 2017-11-07 23:00:00), 3h, 1d)`
+*  `GROUP BY([2017-11-07T23:50:00, 2017-11-07T23:59:00), 1m) FILL 
(PREVIOUSUNTILLAST)` 
+*  `GROUP BY([2017-11-07T23:50:00, 2017-11-07T23:59:00), 1m) FILL (PREVIOUS, 
1m)`
+*  `GROUP BY([2017-11-07T23:50:00, 2017-11-07T23:59:00), 1m) FILL (LINEAR, 5m, 
5m)`
+*  `group by ((2017-11-01T00:00:00, 2017-11-07T23:00:00], 1d), level=1`
+*  `group by ([0, 20), 2ms, 3ms), level=1`
+
 
 
 #### 变量与模板功能的支持
diff --git a/grafana-plugin/src/QueryEditor.tsx 
b/grafana-plugin/src/QueryEditor.tsx
index 5b8567e..851edb4 100644
--- a/grafana-plugin/src/QueryEditor.tsx
+++ b/grafana-plugin/src/QueryEditor.tsx
@@ -23,11 +23,13 @@ import { QueryInlineField } from './componments/Form';
 import { SelectValue } from 'componments/SelectValue';
 import { FromValue } from 'componments/FromValue';
 import { WhereValue } from 'componments/WhereValue';
+import { ControlValue } from 'componments/ControlValue';
 
 interface State {
   expression: string[];
   prefixPath: string[];
   condition: string;
+  control: string;
 }
 
 const paths = [''];
@@ -39,6 +41,7 @@ export class QueryEditor extends PureComponent<Props, State> {
     expression: expressions,
     prefixPath: paths,
     condition: '',
+    control: '',
   };
 
   onSelectValueChange = (exp: string[]) => {
@@ -58,6 +61,11 @@ export class QueryEditor extends PureComponent<Props, State> 
{
     onChange({ ...query, condition: c });
     this.setState({ condition: c });
   };
+  onControlValueChange = (c: string) => {
+    const { onChange, query } = this.props;
+    onChange({ ...query, control: c });
+    this.setState({ control: c });
+  };
 
   onQueryTextChange = (event: ChangeEvent<HTMLInputElement>) => {
     const { onChange, query } = this.props;
@@ -66,7 +74,7 @@ export class QueryEditor extends PureComponent<Props, State> {
 
   render() {
     const query = defaults(this.props.query);
-    const { expression, prefixPath, condition } = query;
+    const { expression, prefixPath, condition, control } = query;
 
     return (
       <>
@@ -93,6 +101,11 @@ export class QueryEditor extends PureComponent<Props, 
State> {
                 <WhereValue condition={condition} 
onChange={this.onWhereValueChange} />
               </QueryInlineField>
             </div>
+            <div className="gf-form">
+              <QueryInlineField label={'CONTROL'}>
+                <ControlValue control={control} 
onChange={this.onControlValueChange} />
+              </QueryInlineField>
+            </div>
           </>
         }
       </>
diff --git a/grafana-plugin/src/types.ts 
b/grafana-plugin/src/componments/ControlValue.tsx
similarity index 57%
copy from grafana-plugin/src/types.ts
copy to grafana-plugin/src/componments/ControlValue.tsx
index 972a24d..b77b024 100644
--- a/grafana-plugin/src/types.ts
+++ b/grafana-plugin/src/componments/ControlValue.tsx
@@ -14,30 +14,26 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { DataQuery, DataSourceJsonData } from '@grafana/data';
+import { FunctionComponent } from 'react';
+import { SegmentInput } from '@grafana/ui';
+import React from 'react';
 
-export interface IoTDBQuery extends DataQuery {
-  startTime: number;
-  endTime: number;
-  expression: string[];
-  prefixPath: string[];
-  condition: string;
-  queryText?: string;
-  constant: number;
+export interface Props {
+  control: string;
+  onChange: (controlStr: string) => void;
 }
 
-/**
- * These are options configured for each DataSource instance
- */
-export interface IoTDBOptions extends DataSourceJsonData {
-  url: string;
-  password: string;
-  username: string;
-}
-
-/**
- * Value that is used in the backend, but never sent over HTTP to the frontend
- */
-export interface IoTDBSecureJsonData {
-  apiKey?: string;
-}
+export const ControlValue: FunctionComponent<Props> = ({ control, onChange }) 
=> (
+  <>
+    {
+      <>
+        <SegmentInput
+          className="min-width-8"
+          placeholder="(optional)"
+          value={control}
+          onChange={string => onChange(string.toString())}
+        />
+      </>
+    }
+  </>
+);
diff --git a/grafana-plugin/src/datasource.ts b/grafana-plugin/src/datasource.ts
index 64b50c9..27a279a 100644
--- a/grafana-plugin/src/datasource.ts
+++ b/grafana-plugin/src/datasource.ts
@@ -56,6 +56,9 @@ export class DataSource extends DataSourceApi<IoTDBQuery, 
IoTDBOptions> {
         if (target.condition) {
           target.condition = getTemplateSrv().replace(target.condition, 
options.scopedVars);
         }
+        if (target.control) {
+          target.control = getTemplateSrv().replace(target.control, 
options.scopedVars);
+        }
       }
       //target.paths = ['root', ...target.paths];
       return this.doRequest(target);
diff --git a/grafana-plugin/src/types.ts b/grafana-plugin/src/types.ts
index 972a24d..fa60779 100644
--- a/grafana-plugin/src/types.ts
+++ b/grafana-plugin/src/types.ts
@@ -24,6 +24,7 @@ export interface IoTDBQuery extends DataQuery {
   condition: string;
   queryText?: string;
   constant: number;
+  control: string;
 }
 
 /**
diff --git a/openapi/src/main/openapi3/iotdb-rest.yaml 
b/openapi/src/main/openapi3/iotdb-rest.yaml
index daa382e..85397c3 100644
--- a/openapi/src/main/openapi3/iotdb-rest.yaml
+++ b/openapi/src/main/openapi3/iotdb-rest.yaml
@@ -206,6 +206,8 @@ components:
             type: string
         condition:
           type: string
+        control:
+           type: string
         startTime:
           type: number
         endTime:
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 6ab8f9f..bd725a0 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
@@ -135,6 +135,9 @@ public class GrafanaApiServiceImpl extends 
GrafanaApiService {
       if (StringUtils.isNotEmpty(expressionRequest.getCondition())) {
         sql += " and " + expressionRequest.getCondition();
       }
+      if (StringUtils.isNotEmpty(expressionRequest.getControl())) {
+        sql += " " + expressionRequest.getControl();
+      }
 
       PhysicalPlan physicalPlan = 
basicServiceProvider.getPlanner().parseSQLToPhysicalPlan(sql);
 
diff --git 
a/server/src/test/java/org/apache/iotdb/db/protocol/rest/GrafanaApiServiceIT.java
 
b/server/src/test/java/org/apache/iotdb/db/protocol/rest/GrafanaApiServiceIT.java
new file mode 100644
index 0000000..6cba346
--- /dev/null
+++ 
b/server/src/test/java/org/apache/iotdb/db/protocol/rest/GrafanaApiServiceIT.java
@@ -0,0 +1,293 @@
+/*
+ * 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.
+ */
+package org.apache.iotdb.db.protocol.rest;
+
+import org.apache.iotdb.db.utils.EnvironmentUtils;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import org.apache.http.HttpEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.util.EntityUtils;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class GrafanaApiServiceIT {
+  @Before
+  public void setUp() throws Exception {
+    EnvironmentUtils.closeStatMonitor();
+    EnvironmentUtils.envSetUp();
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    EnvironmentUtils.cleanEnv();
+  }
+
+  private String getAuthorization(String username, String password) {
+    return Base64.getEncoder()
+        .encodeToString((username + ":" + 
password).getBytes(StandardCharsets.UTF_8));
+  }
+
+  private HttpPost getHttpPost(String url) {
+    HttpPost httpPost = new HttpPost(url);
+    httpPost.addHeader("Content-type", "application/json; charset=utf-8");
+    httpPost.setHeader("Accept", "application/json");
+    String authorization = getAuthorization("root", "root");
+    httpPost.setHeader("Authorization", authorization);
+    return httpPost;
+  }
+
+  public void rightInsertTablet(CloseableHttpClient httpClient) {
+    CloseableHttpResponse response = null;
+    try {
+      HttpPost httpPost = 
getHttpPost("http://127.0.0.1:18080/rest/v1/insertTablet";);
+      String json =
+          
"{\"timestamps\":[1635232143960,1635232153960],\"measurements\":[\"s4\",\"s5\"],\"dataTypes\":[\"INT32\",\"INT32\"],\"values\":[[11,2],[15,13]],\"isAligned\":false,\"deviceId\":\"root.sg25\"}";
+      httpPost.setEntity(new StringEntity(json, Charset.defaultCharset()));
+      response = httpClient.execute(httpPost);
+      HttpEntity responseEntity = response.getEntity();
+      String message = EntityUtils.toString(responseEntity, "utf-8");
+      JsonObject result = JsonParser.parseString(message).getAsJsonObject();
+      assertEquals(200, Integer.parseInt(result.get("code").toString()));
+    } catch (IOException e) {
+      e.printStackTrace();
+      fail(e.getMessage());
+    } finally {
+      try {
+        if (response != null) {
+          response.close();
+        }
+      } catch (IOException e) {
+        e.printStackTrace();
+        fail(e.getMessage());
+      }
+    }
+  }
+
+  public void expression(CloseableHttpClient httpClient) {
+    CloseableHttpResponse response = null;
+    try {
+      HttpPost httpPost = 
getHttpPost("http://127.0.0.1:18080/grafana/v1/query/expression";);
+      String sql =
+          
"{\"expression\":[\"s4\",\"s5\"],\"prefixPath\":[\"root.sg25\"],\"startTime\":1635232133960,\"endTime\":1635232163960}";
+      httpPost.setEntity(new StringEntity(sql, Charset.defaultCharset()));
+      response = httpClient.execute(httpPost);
+      HttpEntity responseEntity = response.getEntity();
+      String message = EntityUtils.toString(responseEntity, "utf-8");
+      ObjectMapper mapper = new ObjectMapper();
+      Map<String, List> map = mapper.readValue(message, Map.class);
+      String[] expressionsResult = {"root.sg25.s4", "root.sg25.s5"};
+      Long[] timestamps = {1635232143960L, 1635232153960L};
+      Object[] values1 = {11, 2};
+      Object[] values2 = {15, 13};
+      Assert.assertArrayEquals(
+          expressionsResult, (map.get("expressions")).toArray(new String[] 
{}));
+      Assert.assertArrayEquals(timestamps, (map.get("timestamps")).toArray(new 
Long[] {}));
+      Assert.assertArrayEquals(
+          values1, ((List) (map.get("values")).get(0)).toArray(new Object[] 
{}));
+      Assert.assertArrayEquals(
+          values2, ((List) (map.get("values")).get(1)).toArray(new Object[] 
{}));
+    } catch (IOException e) {
+      e.printStackTrace();
+      fail(e.getMessage());
+    } finally {
+      try {
+        if (response != null) {
+          response.close();
+        }
+      } catch (IOException e) {
+        e.printStackTrace();
+        fail(e.getMessage());
+      }
+    }
+  }
+
+  public void expressionWithControl(CloseableHttpClient httpClient) {
+    CloseableHttpResponse response = null;
+    try {
+      HttpPost httpPost = 
getHttpPost("http://127.0.0.1:18080/grafana/v1/query/expression";);
+      String sql =
+          
"{\"expression\":[\"sum(s4)\",\"avg(s5)\"],\"prefixPath\":[\"root.sg25\"],\"startTime\":1635232133960,\"endTime\":1635232163960,\"control\":\"group
 by([1635232133960,1635232163960),20s)\"}";
+      httpPost.setEntity(new StringEntity(sql, Charset.defaultCharset()));
+      response = httpClient.execute(httpPost);
+      HttpEntity responseEntity = response.getEntity();
+      String message = EntityUtils.toString(responseEntity, "utf-8");
+      ObjectMapper mapper = new ObjectMapper();
+      Map<String, List> map = mapper.readValue(message, Map.class);
+      String[] expressionsResult = {"sum(root.sg25.s4)", "avg(root.sg25.s5)"};
+      Long[] timestamps = {1635232133960L, 1635232153960L};
+      Object[] values1 = {11.0, 2.0};
+      Object[] values2 = {15.0, 13.0};
+      Assert.assertArrayEquals(
+          expressionsResult, (map.get("expressions")).toArray(new String[] 
{}));
+      Assert.assertArrayEquals(timestamps, (map.get("timestamps")).toArray(new 
Long[] {}));
+      Assert.assertArrayEquals(
+          values1, ((List) (map.get("values")).get(0)).toArray(new Object[] 
{}));
+      Assert.assertArrayEquals(
+          values2, ((List) (map.get("values")).get(1)).toArray(new Object[] 
{}));
+    } catch (IOException e) {
+      e.printStackTrace();
+      fail(e.getMessage());
+    } finally {
+      try {
+        if (response != null) {
+          response.close();
+        }
+      } catch (IOException e) {
+        e.printStackTrace();
+        fail(e.getMessage());
+      }
+    }
+  }
+
+  public void expressionWithConditionControl(CloseableHttpClient httpClient) {
+    CloseableHttpResponse response = null;
+    try {
+      HttpPost httpPost = 
getHttpPost("http://127.0.0.1:18080/grafana/v1/query/expression";);
+      String sql =
+          
"{\"expression\":[\"sum(s4)\",\"avg(s5)\"],\"prefixPath\":[\"root.sg25\"],\"condition\":\"timestamp=1635232143960\",\"startTime\":1635232133960,\"endTime\":1635232163960,\"control\":\"group
 by([1635232133960,1635232163960),20s)\"}";
+      httpPost.setEntity(new StringEntity(sql, Charset.defaultCharset()));
+      response = httpClient.execute(httpPost);
+      HttpEntity responseEntity = response.getEntity();
+      String message = EntityUtils.toString(responseEntity, "utf-8");
+      ObjectMapper mapper = new ObjectMapper();
+      Map<String, List> map = mapper.readValue(message, Map.class);
+      String[] expressionsResult = {"sum(root.sg25.s4)", "avg(root.sg25.s5)"};
+      Long[] timestamps = {1635232133960L, 1635232153960L};
+      Object[] values1 = {11.0, null};
+      Object[] values2 = {15.0, null};
+      Assert.assertArrayEquals(expressionsResult, 
map.get("expressions").toArray(new String[] {}));
+      Assert.assertArrayEquals(timestamps, (map.get("timestamps")).toArray(new 
Long[] {}));
+      Assert.assertArrayEquals(
+          values1, ((List) (map.get("values")).get(0)).toArray(new Object[] 
{}));
+      Assert.assertArrayEquals(
+          values2, ((List) (map.get("values")).get(1)).toArray(new Object[] 
{}));
+    } catch (IOException e) {
+      e.printStackTrace();
+      fail(e.getMessage());
+    } finally {
+      try {
+        if (response != null) {
+          response.close();
+        }
+      } catch (IOException e) {
+        e.printStackTrace();
+        fail(e.getMessage());
+      }
+    }
+  }
+
+  public void variable(CloseableHttpClient httpClient) {
+    CloseableHttpResponse response = null;
+    try {
+      HttpPost httpPost = 
getHttpPost("http://127.0.0.1:18080/grafana/v1/variable";);
+      String sql = "{\"sql\":\"show child paths root.sg25\"}";
+      httpPost.setEntity(new StringEntity(sql, Charset.defaultCharset()));
+      response = httpClient.execute(httpPost);
+      HttpEntity responseEntity = response.getEntity();
+      String message = EntityUtils.toString(responseEntity, "utf-8");
+      ObjectMapper mapper = new ObjectMapper();
+      List list = mapper.readValue(message, List.class);
+      String[] expectedResult = {"s4", "s5"};
+      Assert.assertArrayEquals(expectedResult, list.toArray(new String[] {}));
+    } catch (IOException e) {
+      e.printStackTrace();
+      fail(e.getMessage());
+    } finally {
+      try {
+        if (response != null) {
+          response.close();
+        }
+      } catch (IOException e) {
+        e.printStackTrace();
+        fail(e.getMessage());
+      }
+    }
+  }
+
+  @Test
+  public void expressionWithConditionControlTest() {
+    CloseableHttpClient httpClient = HttpClientBuilder.create().build();
+    rightInsertTablet(httpClient);
+    expressionWithConditionControl(httpClient);
+    try {
+      httpClient.close();
+    } catch (IOException e) {
+      e.printStackTrace();
+      fail(e.getMessage());
+    }
+  }
+
+  @Test
+  public void expressionTest() {
+    CloseableHttpClient httpClient = HttpClientBuilder.create().build();
+    rightInsertTablet(httpClient);
+    expression(httpClient);
+    try {
+      httpClient.close();
+    } catch (IOException e) {
+      e.printStackTrace();
+      fail(e.getMessage());
+    }
+  }
+
+  @Test
+  public void expressionWithControlTest() {
+    CloseableHttpClient httpClient = HttpClientBuilder.create().build();
+    rightInsertTablet(httpClient);
+    expressionWithControl(httpClient);
+    try {
+      httpClient.close();
+    } catch (IOException e) {
+      e.printStackTrace();
+      fail(e.getMessage());
+    }
+  }
+
+  @Test
+  public void variableTest() {
+    CloseableHttpClient httpClient = HttpClientBuilder.create().build();
+    rightInsertTablet(httpClient);
+    variable(httpClient);
+    try {
+      httpClient.close();
+    } catch (IOException e) {
+      e.printStackTrace();
+      fail(e.getMessage());
+    }
+  }
+}

Reply via email to