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