├── .gitignore
├── .prettierrc.js
├── CHANGELOG.md
├── LICENSE
├── Magefile.go
├── README.md
├── README_EN.md
├── dist
├── CHANGELOG.md
├── LICENSE
├── README.md
├── gpx_log-service-datasource_darwin_amd64
├── gpx_log-service-datasource_darwin_arm64
├── gpx_log-service-datasource_linux_amd64
├── gpx_log-service-datasource_linux_arm
├── gpx_log-service-datasource_linux_arm64
├── gpx_log-service-datasource_windows_amd64.exe
├── img
│ └── sls_logo.jpg
├── index.html
├── module.js
├── module.js.LICENSE.txt
├── module.js.map
└── plugin.json
├── go.mod
├── go.sum
├── img
├── 2.33
│ ├── config_editor_2.33.png
│ └── query_editor_2.33.png
├── 2.34
│ └── totalLogs_2.34.png
├── 2.36
│ ├── custom.jpg
│ ├── region_2.36.png
│ └── variable_2.36.jpg
├── demo1.png
├── demo2.png
├── demo3.png
└── demo4.png
├── jest.config.js
├── makefile
├── old_README.md
├── old_README_CN.md
├── package.json
├── pkg
├── main.go
├── models.go
├── ram.go
├── resource.go
├── sls-plugin.go
└── sts.go
├── src
├── ConfigEditor.tsx
├── QueryEditor.tsx
├── SLS-monaco-editor
│ ├── MonacoQueryField.tsx
│ ├── MonacoQueryFieldOld.tsx
│ ├── MonacoQueryFieldProps.ts
│ ├── getOverrideServices.ts
│ └── language-definition
│ │ ├── definition.ts
│ │ ├── sls.ts
│ │ └── slsterms.ts
├── SelectTips.tsx
├── VariableQueryEditor.tsx
├── components
│ ├── Collapse.tsx
│ ├── IconButton.tsx
│ ├── QueryEditor
│ │ ├── EditorField.tsx
│ │ ├── EditorRow.tsx
│ │ ├── Field.tsx
│ │ ├── FieldValidationMessage.tsx
│ │ ├── InlineField.tsx
│ │ ├── Label.tsx
│ │ ├── QueryOptionGroup.tsx
│ │ ├── SecretInput.tsx
│ │ ├── Space.tsx
│ │ └── Stack.tsx
│ ├── RadioButtonGroup
│ │ ├── RadioButton.tsx
│ │ └── RadioButtonGroup.tsx
│ ├── Switch
│ │ └── switch.tsx
│ ├── configSection.tsx
│ ├── description.tsx
│ └── style.css
├── const.ts
├── custom-header
│ ├── custom-header.tsx
│ └── custom-headers.tsx
├── datasource.ts
├── img
│ └── sls_logo.jpg
├── module.ts
├── plugin.json
└── types.ts
├── tsconfig.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea/
2 | /node_modules/
3 | .DS_Store
4 | /coverage/
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | ...require('@grafana/toolkit/src/config/prettier.plugin.config.json'),
3 | };
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | # 2.38 (2025-03-03)
4 | - 修复时序库跳转 SLS 路径问题。
5 | - 日志输出级别整理,减少无用输出。
6 | - 支持‘增强Sql’查询,参考 [开启SQL独享版](https://help.aliyun.com/zh/sls/user-guide/enable-dedicated-sql/?spm=a2c4g.11186623.0.0.2c7771dclfhQou)
7 |
8 | # 2.37 (2025-01-13)
9 |
10 | - 修复 grafana 内部初始化 currentPage 的问题。
11 | - 修复自定义 logstore 在日志库列表展示不回显的问题;修复 gotoSLS 跳转到 defaultLogstore 的问题。
12 |
13 | # 2.36 (2024-11-25)
14 |
15 | - 支持 v4 签名,提供更高的安全性。
16 | - 数据源配置页面新增 Region 字段,兼容非受信环境独立签名。
17 | 
18 | - 变量替换支持自定义'日志库列表'选项。
19 | - Variables 设置页面选择 'Custom' 类型。'name' 字段作为独立标识,必须包含 logstore 字符串,不区分大小写。'Custom options' 手动填写可选变量,以逗号区分。
20 | 
21 | - Panel 界面修改'日志库列表'选项为自定义Variable,修改相应变量并刷新仪表盘,即可获得最新结果。
22 | 
23 |
24 |
25 | # 2.35 (2024-10-21)
26 |
27 | 优化查询分析接口性能。
28 |
29 | # 2.34 (2024-09-23)
30 |
31 | - 优化 table/log 视图
32 | - 支持当存在纳秒字段时, time 按纳秒精度排序。
33 | - 支持修改总查询记录数 totalLogs, 默认值为 100, 最小值为 1, 最大值为 5000, 仅对 Query 查询语句生效, 分析语句无效
34 | 
35 | - 修复 gotoSLS 仅可跳转至数据源中设置的默认 Logstore 的问题
36 | - 修复 gotoSLS 跳转后变量替换为具体值的问题
37 |
38 | # 2.33 (2024-09-01)
39 |
40 | - 插件配置界面优化,必填项为 `Endpoint Project AccessKeyID AccessKeySecret`, 不再强制要求填写 `LogStore`;如果不填写 `LogStore`, 请确保你的填写的 Ak 具备当前 Project 的 ListProject 权限,参考 [ListLogStores - 列出 LogStore](https://help.aliyun.com/zh/sls/developer-reference/api-sls-2020-12-30-listlogstores)
41 | - 插件支持自定义 Headers, 但仅在数据源类型为`MetricStore(PrmQL)` 生效
42 | 
43 | - 插件编辑界面优化,支持切换数据源类型和在编辑界面下拉选择具体[日志库(Logstore)](https://help.aliyun.com/zh/sls/product-overview/logstore)或[时序库(MetricStore)](https://help.aliyun.com/zh/sls/product-overview/metricstore?spm=a2c4g.11186623.0.0.2e8b60d1YQznSr)
44 | 
45 |
46 | - 数据源类型主要是两种语法区别`SQL` 和 `PromQL`,再加上存储库的类型不同,有四种类型可选: `ALL(SQL)`、`Logstore(SQL)`、`MetricStore(SQL)`、`MetricStore(PromQL)` ;
47 | - 日志库(Logstore)支持`SQL`[日志库(Logstore)查询分析](https://help.aliyun.com/zh/sls/user-guide/search-and-analysis);
48 | - 时序库(MetricStore)支持`SQL + PromQL`[时序库(MetricStore)查询分析](https://help.aliyun.com/zh/sls/user-guide/search-and-analysis);
49 | - `MetricStore(PromQL)`支持添加 `custom Headers`,具体在该数据源的配置界面进行添加;
50 |
51 | # 2.32 (2023-04-16)
52 |
53 | - 支持时序图断点展示,通过 time_series(`时间列`, `补全窗口`, `%Y-%m-%d %H:%i:%s`, `null`) 补 null 值进行断点展示
54 | - time*series 参考:[time_series 时间补全函数说明](https://help.aliyun.com/zh/sls/user-guide/date-and-time-functions-1?spm=a2c4g.11186623.0.i10#section-wsz-wt2-4fb)
55 | 
56 |
57 | # 2.31 (2023-10-18)
58 |
59 | - 支持日志图在 Grafana v10 的展示,日志图可以指定想展示的字段,ycol 格式为`字段1,字段2`
60 | - 流图支持多条线,ycol 格式为`#:#指标1,指标2`
61 | - 修改表格区域的名字为 ycol,使多个表格在同一个图表时,可以区分。
62 |
63 | 
64 |
65 | 
66 |
67 | - Supports the display of log graphs in Grafana v10. Log graphs can specify the fields you want to display. The ycol format is `Field 1, Field 2`
68 | - The flow graph supports multiple lines, and the ycol format is `#:#Indicator 1,Indicator 2`
69 | - Modify the name of the table area to ycol so that multiple tables can be distinguished when they are in the same chart.
70 |
71 | # 2.30 (2023-07-25)
72 |
73 | - 优化 SLS Grafana 插件后端结构,现支持加入自定义 Resource API 功能。
74 | - 引入 gotoSLS 功能,用户可以方便地跳转到 SLS 控制台进行查询,和体验 SLS 控制台更强大的功能,跳转附带当前 Grafana 的 query、时间信息。在 DataSource 界面配置 roleArn 可实现 STS 跳转,若不配置,则按照正常直接访问逻辑跳转(需要登录控制台)。
75 | - **注意:若配置 STS 跳转,为权限安全考虑,需要满足以下两个条件**:
76 | - **配置 DataSource 的 accessKey 对应的用户,需要有**`**AliyunRAMReadOnlyAccess**`**权限**
77 | - **配置 DataSource 的 roleArn,里面的权限策略,必须有且只有**`**AliyunLogReadOnlyAccess**`
78 | - 原理参考:[控制台内嵌及分享](https://help.aliyun.com/document_detail/74971.html)
79 | - 优化`xcol`的表现形式,现以下拉框的形式规范推荐输入。兼顾兼容性与自定义输入。
80 | - Variable 编辑页面,SLS DataSource 同样引入 Monaco Editor,且自动识别 grafana 版本切换新老显示。
81 | - 修复`xcol`与`ycol`输入框末尾 Tips 在 Grafana 8.3.x 及以下无法显示的问题。
82 | - 修复部分适配问题。
83 |
84 |  
85 |
86 | - Optimize the back-end structure of the SLS Grafana plug-in, and now support the addition of custom Resource API functions.
87 | - Introducing the gotoSLS function, users can easily jump to the SLS console to query and experience more powerful functions of the SLS console, and the jump includes the current Grafana query and time information. Configuring roleArn on the DataSource interface can realize STS jumping. If not configured, it will jump according to the normal direct access logic (login to the console is required).
88 | - **Note: If STS redirection is configured, the following two conditions need to be met for permission security considerations:**
89 | - **To configure the user corresponding to the accessKey of the DataSource, `AliyunRAMReadOnlyAccess` permission is required**
90 | - **Configure the roleArn of DataSource, the permission policy inside must have and only `AliyunLogReadOnlyAccess`**
91 | - Principle reference: [Console embedding and sharing](https://help.aliyun.com/document_detail/74971.html)
92 | - Optimize the expression form of `xcol`, and now standardize the recommended input in the form of a drop-down box. Take into account compatibility and custom input.
93 | - On the Variable editing page, SLS DataSource also introduces Monaco Editor, and automatically recognizes the grafana version to switch between the new and old displays.
94 | - Fix the problem that Tips at the end of the `xcol` and `ycol` input boxes cannot be displayed in Grafana 8.3.x and below.
95 | - Fix some adaptation problems.
96 |
97 | # 2.29 (2023-06-29)
98 |
99 | - 全新 SLS Query 编写体验,引入 Monaco Editor,支持语法高亮显示,关键字、函数自动提示等功能。
100 | - SLS Query 框支持多行自动换行和高度扩展。
101 | - `xcol`与`ycol`的输入使用 Grafana 的 Input 组件,带来更一致的体验。
102 | - `xcol`与`ycol`输入框末尾增加 Tips,介绍常见写法,方便初学者上手。
103 | - 修复一系列版本问题。
104 |
105 |
106 |
107 | - Brand-new SLS Query writing experience, introduces Monaco Editor, supports syntax highlighting, keyword, function automatic prompting and other functions.
108 | - The SLS Query box supports multi-line word wrapping and height expansion.
109 | - The input of xcol and ycol uses Grafana's Input component to bring a more consistent experience.
110 | - Tips are added at the end of the xcol and ycol input boxes to introduce common writing methods, which is convenient for beginners.
111 | - Fix a series of version issues.
112 |
113 |  
114 |
115 | # 2.28
116 |
117 | - 适配 Grafana 9.4 及以上版本的 Datasource 数据源结构。
118 |
119 | - Adapt to the Datasource data source structure of Grafana 9.4 and above.
120 |
121 | # 2.27
122 |
123 | - 加了 legacy_compatible 设置,适配旧版本的返回结构(带 response)
124 |
125 | - Added legacy_compatible to adapt to the return structure of the old version (with response)
126 |
127 | # 2.0.0 (released)
128 |
129 | Initial release.
130 |
131 | Support Grafana 8.0.
132 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Grafana
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Magefile.go:
--------------------------------------------------------------------------------
1 | //+build mage
2 |
3 | package main
4 |
5 | import (
6 | "fmt"
7 | // mage:import
8 | build "github.com/grafana/grafana-plugin-sdk-go/build"
9 | )
10 |
11 | // Hello prints a message (shows that you can define custom Mage targets).
12 | func Hello() {
13 | fmt.Println("hello plugin developer!")
14 | }
15 |
16 | // Default configures the default target.
17 | var Default = build.BuildAll
18 |
--------------------------------------------------------------------------------
/dist/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | # 2.38 (2025-03-03)
4 | - 修复时序库跳转 SLS 路径问题。
5 | - 日志输出级别整理,减少无用输出。
6 | - 支持‘增强Sql’查询,参考 [开启SQL独享版](https://help.aliyun.com/zh/sls/user-guide/enable-dedicated-sql/?spm=a2c4g.11186623.0.0.2c7771dclfhQou)
7 |
8 | # 2.37 (2025-01-13)
9 |
10 | - 修复 grafana 内部初始化 currentPage 的问题。
11 | - 修复自定义 logstore 在日志库列表展示不回显的问题;修复 gotoSLS 跳转到 defaultLogstore 的问题。
12 |
13 | # 2.36 (2024-11-25)
14 |
15 | - 支持 v4 签名,提供更高的安全性。
16 | - 数据源配置页面新增 Region 字段,兼容非受信环境独立签名。
17 | 
18 | - 变量替换支持自定义'日志库列表'选项。
19 | - Variables 设置页面选择 'Custom' 类型。'name' 字段作为独立标识,必须包含 logstore 字符串,不区分大小写。'Custom options' 手动填写可选变量,以逗号区分。
20 | 
21 | - Panel 界面修改'日志库列表'选项为自定义Variable,修改相应变量并刷新仪表盘,即可获得最新结果。
22 | 
23 |
24 |
25 | # 2.35 (2024-10-21)
26 |
27 | 优化查询分析接口性能。
28 |
29 | # 2.34 (2024-09-23)
30 |
31 | - 优化 table/log 视图
32 | - 支持当存在纳秒字段时, time 按纳秒精度排序。
33 | - 支持修改总查询记录数 totalLogs, 默认值为 100, 最小值为 1, 最大值为 5000, 仅对 Query 查询语句生效, 分析语句无效
34 | 
35 | - 修复 gotoSLS 仅可跳转至数据源中设置的默认 Logstore 的问题
36 | - 修复 gotoSLS 跳转后变量替换为具体值的问题
37 |
38 | # 2.33 (2024-09-01)
39 |
40 | - 插件配置界面优化,必填项为 `Endpoint Project AccessKeyID AccessKeySecret`, 不再强制要求填写 `LogStore`;如果不填写 `LogStore`, 请确保你的填写的 Ak 具备当前 Project 的 ListProject 权限,参考 [ListLogStores - 列出 LogStore](https://help.aliyun.com/zh/sls/developer-reference/api-sls-2020-12-30-listlogstores)
41 | - 插件支持自定义 Headers, 但仅在数据源类型为`MetricStore(PrmQL)` 生效
42 | 
43 | - 插件编辑界面优化,支持切换数据源类型和在编辑界面下拉选择具体[日志库(Logstore)](https://help.aliyun.com/zh/sls/product-overview/logstore)或[时序库(MetricStore)](https://help.aliyun.com/zh/sls/product-overview/metricstore?spm=a2c4g.11186623.0.0.2e8b60d1YQznSr)
44 | 
45 |
46 | - 数据源类型主要是两种语法区别`SQL` 和 `PromQL`,再加上存储库的类型不同,有四种类型可选: `ALL(SQL)`、`Logstore(SQL)`、`MetricStore(SQL)`、`MetricStore(PromQL)` ;
47 | - 日志库(Logstore)支持`SQL`[日志库(Logstore)查询分析](https://help.aliyun.com/zh/sls/user-guide/search-and-analysis);
48 | - 时序库(MetricStore)支持`SQL + PromQL`[时序库(MetricStore)查询分析](https://help.aliyun.com/zh/sls/user-guide/search-and-analysis);
49 | - `MetricStore(PromQL)`支持添加 `custom Headers`,具体在该数据源的配置界面进行添加;
50 |
51 | # 2.32 (2023-04-16)
52 |
53 | - 支持时序图断点展示,通过 time_series(`时间列`, `补全窗口`, `%Y-%m-%d %H:%i:%s`, `null`) 补 null 值进行断点展示
54 | - time*series 参考:[time_series 时间补全函数说明](https://help.aliyun.com/zh/sls/user-guide/date-and-time-functions-1?spm=a2c4g.11186623.0.i10#section-wsz-wt2-4fb)
55 | 
56 |
57 | # 2.31 (2023-10-18)
58 |
59 | - 支持日志图在 Grafana v10 的展示,日志图可以指定想展示的字段,ycol 格式为`字段1,字段2`
60 | - 流图支持多条线,ycol 格式为`#:#指标1,指标2`
61 | - 修改表格区域的名字为 ycol,使多个表格在同一个图表时,可以区分。
62 |
63 | 
64 |
65 | 
66 |
67 | - Supports the display of log graphs in Grafana v10. Log graphs can specify the fields you want to display. The ycol format is `Field 1, Field 2`
68 | - The flow graph supports multiple lines, and the ycol format is `#:#Indicator 1,Indicator 2`
69 | - Modify the name of the table area to ycol so that multiple tables can be distinguished when they are in the same chart.
70 |
71 | # 2.30 (2023-07-25)
72 |
73 | - 优化 SLS Grafana 插件后端结构,现支持加入自定义 Resource API 功能。
74 | - 引入 gotoSLS 功能,用户可以方便地跳转到 SLS 控制台进行查询,和体验 SLS 控制台更强大的功能,跳转附带当前 Grafana 的 query、时间信息。在 DataSource 界面配置 roleArn 可实现 STS 跳转,若不配置,则按照正常直接访问逻辑跳转(需要登录控制台)。
75 | - **注意:若配置 STS 跳转,为权限安全考虑,需要满足以下两个条件**:
76 | - **配置 DataSource 的 accessKey 对应的用户,需要有**`**AliyunRAMReadOnlyAccess**`**权限**
77 | - **配置 DataSource 的 roleArn,里面的权限策略,必须有且只有**`**AliyunLogReadOnlyAccess**`
78 | - 原理参考:[控制台内嵌及分享](https://help.aliyun.com/document_detail/74971.html)
79 | - 优化`xcol`的表现形式,现以下拉框的形式规范推荐输入。兼顾兼容性与自定义输入。
80 | - Variable 编辑页面,SLS DataSource 同样引入 Monaco Editor,且自动识别 grafana 版本切换新老显示。
81 | - 修复`xcol`与`ycol`输入框末尾 Tips 在 Grafana 8.3.x 及以下无法显示的问题。
82 | - 修复部分适配问题。
83 |
84 |  
85 |
86 | - Optimize the back-end structure of the SLS Grafana plug-in, and now support the addition of custom Resource API functions.
87 | - Introducing the gotoSLS function, users can easily jump to the SLS console to query and experience more powerful functions of the SLS console, and the jump includes the current Grafana query and time information. Configuring roleArn on the DataSource interface can realize STS jumping. If not configured, it will jump according to the normal direct access logic (login to the console is required).
88 | - **Note: If STS redirection is configured, the following two conditions need to be met for permission security considerations:**
89 | - **To configure the user corresponding to the accessKey of the DataSource, `AliyunRAMReadOnlyAccess` permission is required**
90 | - **Configure the roleArn of DataSource, the permission policy inside must have and only `AliyunLogReadOnlyAccess`**
91 | - Principle reference: [Console embedding and sharing](https://help.aliyun.com/document_detail/74971.html)
92 | - Optimize the expression form of `xcol`, and now standardize the recommended input in the form of a drop-down box. Take into account compatibility and custom input.
93 | - On the Variable editing page, SLS DataSource also introduces Monaco Editor, and automatically recognizes the grafana version to switch between the new and old displays.
94 | - Fix the problem that Tips at the end of the `xcol` and `ycol` input boxes cannot be displayed in Grafana 8.3.x and below.
95 | - Fix some adaptation problems.
96 |
97 | # 2.29 (2023-06-29)
98 |
99 | - 全新 SLS Query 编写体验,引入 Monaco Editor,支持语法高亮显示,关键字、函数自动提示等功能。
100 | - SLS Query 框支持多行自动换行和高度扩展。
101 | - `xcol`与`ycol`的输入使用 Grafana 的 Input 组件,带来更一致的体验。
102 | - `xcol`与`ycol`输入框末尾增加 Tips,介绍常见写法,方便初学者上手。
103 | - 修复一系列版本问题。
104 |
105 |
106 |
107 | - Brand-new SLS Query writing experience, introduces Monaco Editor, supports syntax highlighting, keyword, function automatic prompting and other functions.
108 | - The SLS Query box supports multi-line word wrapping and height expansion.
109 | - The input of xcol and ycol uses Grafana's Input component to bring a more consistent experience.
110 | - Tips are added at the end of the xcol and ycol input boxes to introduce common writing methods, which is convenient for beginners.
111 | - Fix a series of version issues.
112 |
113 |  
114 |
115 | # 2.28
116 |
117 | - 适配 Grafana 9.4 及以上版本的 Datasource 数据源结构。
118 |
119 | - Adapt to the Datasource data source structure of Grafana 9.4 and above.
120 |
121 | # 2.27
122 |
123 | - 加了 legacy_compatible 设置,适配旧版本的返回结构(带 response)
124 |
125 | - Added legacy_compatible to adapt to the return structure of the old version (with response)
126 |
127 | # 2.0.0 (released)
128 |
129 | Initial release.
130 |
131 | Support Grafana 8.0.
132 |
--------------------------------------------------------------------------------
/dist/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Grafana
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/dist/gpx_log-service-datasource_darwin_amd64:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/dist/gpx_log-service-datasource_darwin_amd64
--------------------------------------------------------------------------------
/dist/gpx_log-service-datasource_darwin_arm64:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/dist/gpx_log-service-datasource_darwin_arm64
--------------------------------------------------------------------------------
/dist/gpx_log-service-datasource_linux_amd64:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/dist/gpx_log-service-datasource_linux_amd64
--------------------------------------------------------------------------------
/dist/gpx_log-service-datasource_linux_arm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/dist/gpx_log-service-datasource_linux_arm
--------------------------------------------------------------------------------
/dist/gpx_log-service-datasource_linux_arm64:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/dist/gpx_log-service-datasource_linux_arm64
--------------------------------------------------------------------------------
/dist/gpx_log-service-datasource_windows_amd64.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/dist/gpx_log-service-datasource_windows_amd64.exe
--------------------------------------------------------------------------------
/dist/img/sls_logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/dist/img/sls_logo.jpg
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Webpack App
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/dist/module.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
2 |
--------------------------------------------------------------------------------
/dist/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://raw.githubusercontent.com/grafana/grafana/master/docs/sources/developers/plugins/plugin.schema.json",
3 | "type": "datasource",
4 | "name": "log-service-datasource",
5 | "id": "aliyun-log-service-datasource",
6 | "metrics": true,
7 | "backend": true,
8 | "logs": true,
9 | "alerting": true,
10 | "executable": "gpx_log-service-datasource",
11 | "flow_chart_max_points": "6000000",
12 | "legacy_compatible": false,
13 | "info": {
14 | "description": "Aliyun log service datasource (backend version)",
15 | "author": {
16 | "name": "aliyun-log",
17 | "url": "https://aliyun.com/product/sls"
18 | },
19 | "keywords": [
20 | "sls",
21 | "aliyun",
22 | "log",
23 | "日志服务"
24 | ],
25 | "logos": {
26 | "small": "img/sls_logo.jpg",
27 | "large": "img/sls_logo.jpg"
28 | },
29 | "links": [
30 | {
31 | "name": "Website",
32 | "url": "https://github.com/aliyun/aliyun-log-grafana-datasource-plugin"
33 | },
34 | {
35 | "name": "License",
36 | "url": "https://github.com/aliyun/aliyun-log-grafana-datasource-plugin/blob/master/LICENSE"
37 | }
38 | ],
39 | "screenshots": [],
40 | "version": "2.38",
41 | "updated": "2025-03-05"
42 | },
43 | "dependencies": {
44 | "grafanaDependency": ">=7.0.0",
45 | "plugins": []
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/grafana/grafana-starter-datasource-backend
2 |
3 | go 1.21
4 |
5 | toolchain go1.23.1
6 |
7 | require (
8 | github.com/aliyun/alibaba-cloud-sdk-go v1.62.453
9 | github.com/aliyun/aliyun-log-go-sdk v0.1.83
10 | github.com/grafana/grafana-plugin-sdk-go v0.102.0
11 | github.com/satori/go.uuid v1.2.0
12 | )
13 |
14 | require (
15 | cloud.google.com/go v0.65.0 // indirect
16 | cloud.google.com/go/bigquery v1.8.0 // indirect
17 | cloud.google.com/go/datastore v1.1.0 // indirect
18 | cloud.google.com/go/pubsub v1.3.1 // indirect
19 | cloud.google.com/go/storage v1.10.0 // indirect
20 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 // indirect
21 | github.com/BurntSushi/toml v1.2.1 // indirect
22 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 // indirect
23 | github.com/DataDog/datadog-go v3.2.0+incompatible // indirect
24 | github.com/DataDog/zstd v1.5.5 // indirect
25 | github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect
26 | github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect
27 | github.com/Netflix/go-env v0.0.0-20220526054621-78278af1949d // indirect
28 | github.com/OneOfOne/xxhash v1.2.2 // indirect
29 | github.com/Shopify/sarama v1.19.0 // indirect
30 | github.com/Shopify/toxiproxy v2.1.4+incompatible // indirect
31 | github.com/VividCortex/gohistogram v1.0.0 // indirect
32 | github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 // indirect
33 | github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af // indirect
34 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
35 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect
36 | github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect
37 | github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.4 // indirect
38 | github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 // indirect
39 | github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect
40 | github.com/alibabacloud-go/openapi-util v0.1.0 // indirect
41 | github.com/alibabacloud-go/sts-20150401/v2 v2.0.1 // indirect
42 | github.com/alibabacloud-go/tea v1.1.19 // indirect
43 | github.com/alibabacloud-go/tea-utils v1.3.1 // indirect
44 | github.com/alibabacloud-go/tea-utils/v2 v2.0.1 // indirect
45 | github.com/alibabacloud-go/tea-xml v1.1.2 // indirect
46 | github.com/aliyun/credentials-go v1.1.2 // indirect
47 | github.com/antihax/optional v1.0.0 // indirect
48 | github.com/apache/arrow/go/arrow v0.0.0-20210223225224-5bea62493d91 // indirect
49 | github.com/apache/thrift v0.13.0 // indirect
50 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e // indirect
51 | github.com/armon/go-metrics v0.4.0 // indirect
52 | github.com/armon/go-radix v1.0.0 // indirect
53 | github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a // indirect
54 | github.com/aws/aws-lambda-go v1.13.3 // indirect
55 | github.com/aws/aws-sdk-go v1.40.45 // indirect
56 | github.com/aws/aws-sdk-go-v2 v1.9.1 // indirect
57 | github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1 // indirect
58 | github.com/aws/smithy-go v1.8.0 // indirect
59 | github.com/benbjohnson/clock v1.1.0 // indirect
60 | github.com/beorn7/perks v1.0.1 // indirect
61 | github.com/bgentry/speakeasy v0.1.0 // indirect
62 | github.com/casbin/casbin/v2 v2.37.0 // indirect
63 | github.com/cenkalti/backoff v2.2.1+incompatible // indirect
64 | github.com/cenkalti/backoff/v4 v4.1.1 // indirect
65 | github.com/census-instrumentation/opencensus-proto v0.2.1 // indirect
66 | github.com/cespare/xxhash v1.1.0 // indirect
67 | github.com/cespare/xxhash/v2 v2.1.2 // indirect
68 | github.com/cheekybits/genny v1.0.0 // indirect
69 | github.com/chzyer/logex v1.1.10 // indirect
70 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
71 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
72 | github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible // indirect
73 | github.com/circonus-labs/circonusllhist v0.1.3 // indirect
74 | github.com/clbanning/mxj v1.8.4 // indirect
75 | github.com/clbanning/mxj/v2 v2.5.5 // indirect
76 | github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec // indirect
77 | github.com/client9/misspell v0.3.4 // indirect
78 | github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 // indirect
79 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403 // indirect
80 | github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed // indirect
81 | github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa // indirect
82 | github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd // indirect
83 | github.com/coreos/go-semver v0.3.0 // indirect
84 | github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7 // indirect
85 | github.com/coreos/go-systemd/v22 v22.3.2 // indirect
86 | github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf // indirect
87 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect
88 | github.com/creack/pty v1.1.9 // indirect
89 | github.com/davecgh/go-spew v1.1.1 // indirect
90 | github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
91 | github.com/duke-git/lancet v1.3.7 // indirect
92 | github.com/dustin/go-humanize v1.0.0 // indirect
93 | github.com/eapache/go-resiliency v1.1.0 // indirect
94 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 // indirect
95 | github.com/eapache/queue v1.1.0 // indirect
96 | github.com/edsrzf/mmap-go v1.0.0 // indirect
97 | github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0 // indirect
98 | github.com/envoyproxy/protoc-gen-validate v0.1.0 // indirect
99 | github.com/fatih/color v1.13.0 // indirect
100 | github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90 // indirect
101 | github.com/franela/goblin v0.0.0-20210519012713-85d372ac71e2 // indirect
102 | github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8 // indirect
103 | github.com/frankban/quicktest v1.10.2 // indirect
104 | github.com/fsnotify/fsnotify v1.4.9 // indirect
105 | github.com/ghodss/yaml v1.0.0 // indirect
106 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 // indirect
107 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 // indirect
108 | github.com/go-kit/kit v0.13.0 // indirect
109 | github.com/go-kit/log v0.2.1 // indirect
110 | github.com/go-logfmt/logfmt v0.6.0 // indirect
111 | github.com/go-sql-driver/mysql v1.4.0 // indirect
112 | github.com/go-stack/stack v1.8.0 // indirect
113 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
114 | github.com/go-zookeeper/zk v1.0.2 // indirect
115 | github.com/godbus/dbus/v5 v5.0.4 // indirect
116 | github.com/gogo/googleapis v1.1.0 // indirect
117 | github.com/gogo/protobuf v1.3.2 // indirect
118 | github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d // indirect
119 | github.com/golang-jwt/jwt/v4 v4.0.0 // indirect
120 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
121 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
122 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
123 | github.com/golang/mock v1.4.4 // indirect
124 | github.com/golang/protobuf v1.5.4 // indirect
125 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect
126 | github.com/google/btree v1.0.0 // indirect
127 | github.com/google/flatbuffers v1.11.0 // indirect
128 | github.com/google/go-cmp v0.6.0 // indirect
129 | github.com/google/gofuzz v1.0.0 // indirect
130 | github.com/google/martian v2.1.0+incompatible // indirect
131 | github.com/google/martian/v3 v3.0.0 // indirect
132 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99 // indirect
133 | github.com/google/renameio v0.1.0 // indirect
134 | github.com/google/uuid v1.1.2 // indirect
135 | github.com/googleapis/gax-go/v2 v2.0.5 // indirect
136 | github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect
137 | github.com/gorilla/context v1.1.1 // indirect
138 | github.com/gorilla/mux v1.8.0 // indirect
139 | github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c // indirect
140 | github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
141 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
142 | github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
143 | github.com/hashicorp/consul/api v1.14.0 // indirect
144 | github.com/hashicorp/consul/sdk v0.10.0 // indirect
145 | github.com/hashicorp/errwrap v1.0.0 // indirect
146 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
147 | github.com/hashicorp/go-hclog v1.2.2 // indirect
148 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
149 | github.com/hashicorp/go-msgpack v0.5.5 // indirect
150 | github.com/hashicorp/go-multierror v1.1.1 // indirect
151 | github.com/hashicorp/go-plugin v1.2.2 // indirect
152 | github.com/hashicorp/go-retryablehttp v0.5.3 // indirect
153 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect
154 | github.com/hashicorp/go-sockaddr v1.0.2 // indirect
155 | github.com/hashicorp/go-syslog v1.0.0 // indirect
156 | github.com/hashicorp/go-uuid v1.0.2 // indirect
157 | github.com/hashicorp/go-version v1.2.0 // indirect
158 | github.com/hashicorp/go.net v0.0.1 // indirect
159 | github.com/hashicorp/golang-lru v0.5.4 // indirect
160 | github.com/hashicorp/logutils v1.0.0 // indirect
161 | github.com/hashicorp/mdns v1.0.4 // indirect
162 | github.com/hashicorp/memberlist v0.4.0 // indirect
163 | github.com/hashicorp/serf v0.10.0 // indirect
164 | github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect
165 | github.com/hpcloud/tail v1.0.0 // indirect
166 | github.com/hudl/fargo v1.4.0 // indirect
167 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 // indirect
168 | github.com/inconshreveable/mousetrap v1.0.0 // indirect
169 | github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab // indirect
170 | github.com/jhump/protoreflect v1.6.0 // indirect
171 | github.com/jmespath/go-jmespath v0.4.0 // indirect
172 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 // indirect
173 | github.com/jonboulle/clockwork v0.1.0 // indirect
174 | github.com/jpillora/backoff v1.0.0 // indirect
175 | github.com/json-iterator/go v1.1.12 // indirect
176 | github.com/jstemmer/go-junit-report v0.9.1 // indirect
177 | github.com/jtolds/gls v4.20.0+incompatible // indirect
178 | github.com/julienschmidt/httprouter v1.3.0 // indirect
179 | github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5 // indirect
180 | github.com/kisielk/errcheck v1.5.0 // indirect
181 | github.com/kisielk/gotool v1.0.0 // indirect
182 | github.com/klauspost/compress v1.17.11 // indirect
183 | github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect
184 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 // indirect
185 | github.com/kr/pretty v0.2.1 // indirect
186 | github.com/kr/pty v1.1.1 // indirect
187 | github.com/kr/text v0.2.0 // indirect
188 | github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743 // indirect
189 | github.com/lightstep/lightstep-tracer-go v0.18.1 // indirect
190 | github.com/lyft/protoc-gen-validate v0.0.13 // indirect
191 | github.com/magefile/mage v1.11.0 // indirect
192 | github.com/mattetti/filebuffer v1.0.1 // indirect
193 | github.com/mattn/go-colorable v0.1.13 // indirect
194 | github.com/mattn/go-isatty v0.0.16 // indirect
195 | github.com/mattn/go-runewidth v0.0.9 // indirect
196 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
197 | github.com/miekg/dns v1.1.43 // indirect
198 | github.com/minio/highwayhash v1.0.2 // indirect
199 | github.com/mitchellh/cli v1.1.0 // indirect
200 | github.com/mitchellh/go-homedir v1.1.0 // indirect
201 | github.com/mitchellh/go-testing-interface v1.0.0 // indirect
202 | github.com/mitchellh/go-wordwrap v1.0.0 // indirect
203 | github.com/mitchellh/gox v0.4.0 // indirect
204 | github.com/mitchellh/iochan v1.0.0 // indirect
205 | github.com/mitchellh/mapstructure v1.5.0 // indirect
206 | github.com/mitchellh/reflectwalk v1.0.2 // indirect
207 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
208 | github.com/modern-go/reflect2 v1.0.2 // indirect
209 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
210 | github.com/nats-io/jwt v0.3.2 // indirect
211 | github.com/nats-io/jwt/v2 v2.2.1-0.20220330180145-442af02fd36a // indirect
212 | github.com/nats-io/nats-server/v2 v2.8.4 // indirect
213 | github.com/nats-io/nats.go v1.15.0 // indirect
214 | github.com/nats-io/nkeys v0.3.0 // indirect
215 | github.com/nats-io/nuid v1.0.1 // indirect
216 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
217 | github.com/nxadm/tail v1.4.8 // indirect
218 | github.com/oklog/oklog v0.3.2 // indirect
219 | github.com/oklog/run v1.0.0 // indirect
220 | github.com/olekukonko/tablewriter v0.0.5 // indirect
221 | github.com/onsi/ginkgo v1.16.2 // indirect
222 | github.com/onsi/gomega v1.13.0 // indirect
223 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 // indirect
224 | github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492 // indirect
225 | github.com/opentracing/basictracer-go v1.0.0 // indirect
226 | github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
227 | github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5 // indirect
228 | github.com/openzipkin/zipkin-go v0.2.5 // indirect
229 | github.com/pact-foundation/pact-go v1.0.4 // indirect
230 | github.com/pascaldekloe/goe v0.1.0 // indirect
231 | github.com/pborman/uuid v1.2.0 // indirect
232 | github.com/performancecopilot/speed v3.0.0+incompatible // indirect
233 | github.com/performancecopilot/speed/v4 v4.0.0 // indirect
234 | github.com/pierrec/lz4 v2.6.1+incompatible // indirect
235 | github.com/pkg/errors v0.9.1 // indirect
236 | github.com/pkg/profile v1.2.1 // indirect
237 | github.com/pmezard/go-difflib v1.0.0 // indirect
238 | github.com/posener/complete v1.2.3 // indirect
239 | github.com/prometheus/client_golang v1.11.1 // indirect
240 | github.com/prometheus/client_model v0.2.0 // indirect
241 | github.com/prometheus/common v0.30.0 // indirect
242 | github.com/prometheus/procfs v0.7.3 // indirect
243 | github.com/rabbitmq/amqp091-go v1.2.0 // indirect
244 | github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a // indirect
245 | github.com/rogpeppe/fastuuid v1.2.0 // indirect
246 | github.com/rogpeppe/go-internal v1.3.0 // indirect
247 | github.com/russross/blackfriday/v2 v2.0.1 // indirect
248 | github.com/ryanuber/columnize v2.1.0+incompatible // indirect
249 | github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da // indirect
250 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
251 | github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
252 | github.com/sirupsen/logrus v1.8.1 // indirect
253 | github.com/smartystreets/assertions v1.1.0 // indirect
254 | github.com/smartystreets/goconvey v1.6.4 // indirect
255 | github.com/soheilhy/cmux v0.1.4 // indirect
256 | github.com/sony/gobreaker v0.4.1 // indirect
257 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 // indirect
258 | github.com/spf13/cobra v0.0.3 // indirect
259 | github.com/spf13/pflag v1.0.1 // indirect
260 | github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271 // indirect
261 | github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e // indirect
262 | github.com/stretchr/objx v0.4.0 // indirect
263 | github.com/stretchr/testify v1.8.0 // indirect
264 | github.com/tjfoc/gmsm v1.3.2 // indirect
265 | github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8 // indirect
266 | github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 // indirect
267 | github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect
268 | github.com/uber/jaeger-lib v2.4.1+incompatible // indirect
269 | github.com/urfave/cli v1.22.1 // indirect
270 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
271 | github.com/yuin/goldmark v1.4.13 // indirect
272 | gitlab.alibaba-inc.com/rapt/go-security-utils v0.1.4 // indirect
273 | go.etcd.io/bbolt v1.3.3 // indirect
274 | go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738 // indirect
275 | go.etcd.io/etcd/api/v3 v3.5.0 // indirect
276 | go.etcd.io/etcd/client/pkg/v3 v3.5.0 // indirect
277 | go.etcd.io/etcd/client/v2 v2.305.0 // indirect
278 | go.etcd.io/etcd/client/v3 v3.5.0 // indirect
279 | go.opencensus.io v0.23.0 // indirect
280 | go.opentelemetry.io/proto/otlp v0.7.0 // indirect
281 | go.uber.org/atomic v1.11.0 // indirect
282 | go.uber.org/goleak v1.1.11 // indirect
283 | go.uber.org/multierr v1.7.0 // indirect
284 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee // indirect
285 | go.uber.org/zap v1.24.0 // indirect
286 | golang.org/x/crypto v0.28.0 // indirect
287 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 // indirect
288 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b // indirect
289 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect
290 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 // indirect
291 | golang.org/x/mod v0.17.0 // indirect
292 | golang.org/x/net v0.30.0 // indirect
293 | golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c // indirect
294 | golang.org/x/sync v0.8.0 // indirect
295 | golang.org/x/sys v0.26.0 // indirect
296 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 // indirect
297 | golang.org/x/term v0.25.0 // indirect
298 | golang.org/x/text v0.19.0 // indirect
299 | golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
300 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
301 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
302 | gonum.org/v1/gonum v0.8.2 // indirect
303 | gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 // indirect
304 | gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b // indirect
305 | google.golang.org/api v0.30.0 // indirect
306 | google.golang.org/appengine v1.6.6 // indirect
307 | google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4 // indirect
308 | google.golang.org/grpc v1.40.0 // indirect
309 | google.golang.org/grpc/cmd/protoc-gen-go-grpc v0.0.0-20200910201057-6591123024b3 // indirect
310 | google.golang.org/protobuf v1.35.1 // indirect
311 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect
312 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
313 | gopkg.in/cheggaaa/pb.v1 v1.0.25 // indirect
314 | gopkg.in/errgo.v2 v2.1.0 // indirect
315 | gopkg.in/fsnotify.v1 v1.4.7 // indirect
316 | gopkg.in/gcfg.v1 v1.2.3 // indirect
317 | gopkg.in/ini.v1 v1.66.2 // indirect
318 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
319 | gopkg.in/resty.v1 v1.12.0 // indirect
320 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
321 | gopkg.in/warnings.v0 v0.1.2 // indirect
322 | gopkg.in/yaml.v2 v2.4.0 // indirect
323 | gopkg.in/yaml.v3 v3.0.1 // indirect
324 | honnef.co/go/tools v0.0.1-2020.1.4 // indirect
325 | rsc.io/binaryregexp v0.2.0 // indirect
326 | rsc.io/pdf v0.1.1 // indirect
327 | rsc.io/quote/v3 v3.1.0 // indirect
328 | rsc.io/sampler v1.3.0 // indirect
329 | sigs.k8s.io/yaml v1.2.0 // indirect
330 | sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0 // indirect
331 | )
332 |
--------------------------------------------------------------------------------
/img/2.33/config_editor_2.33.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/img/2.33/config_editor_2.33.png
--------------------------------------------------------------------------------
/img/2.33/query_editor_2.33.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/img/2.33/query_editor_2.33.png
--------------------------------------------------------------------------------
/img/2.34/totalLogs_2.34.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/img/2.34/totalLogs_2.34.png
--------------------------------------------------------------------------------
/img/2.36/custom.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/img/2.36/custom.jpg
--------------------------------------------------------------------------------
/img/2.36/region_2.36.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/img/2.36/region_2.36.png
--------------------------------------------------------------------------------
/img/2.36/variable_2.36.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/img/2.36/variable_2.36.jpg
--------------------------------------------------------------------------------
/img/demo1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/img/demo1.png
--------------------------------------------------------------------------------
/img/demo2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/img/demo2.png
--------------------------------------------------------------------------------
/img/demo3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/img/demo3.png
--------------------------------------------------------------------------------
/img/demo4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/img/demo4.png
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | // This file is needed because it is used by vscode and other tools that
2 | // call `jest` directly. However, unless you are doing anything special
3 | // do not edit this file
4 |
5 | const standard = require('@grafana/toolkit/src/config/jest.plugin.config');
6 |
7 | // This process will use the same config that `yarn test` is using
8 | module.exports = standard.jestConfig();
9 |
--------------------------------------------------------------------------------
/makefile:
--------------------------------------------------------------------------------
1 | darwin:
2 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o dist/gpx_log-service-datasource_darwin_amd64 pkg/*
3 | CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o dist/gpx_log-service-datasource_darwin_arm64 pkg/*
4 | linux:
5 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o dist/gpx_log-service-datasource_linux_amd64 pkg/*
6 | CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -o dist/gpx_log-service-datasource_linux_arm pkg/*
7 | CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o dist/gpx_log-service-datasource_linux_arm64 pkg/*
8 | windows:
9 | CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o dist/gpx_log-service-datasource_windows_amd64.exe pkg/*
10 | clean:
11 | rm -f dist/gpx_log-service-datasource_*
12 | all: clean darwin linux windows
13 | #mage build
--------------------------------------------------------------------------------
/old_README.md:
--------------------------------------------------------------------------------
1 | ## Aliyun log service Datasource
2 |
3 |
4 | More documentation about datasource plugins can be found in the [Docs](https://github.com/grafana/grafana/blob/master/docs/sources/plugins/developing/datasources.md).
5 |
6 |
7 | [**中文文档**](README_CN.md)
8 |
9 |
10 | ## Install
11 |
12 |
13 | Clone this project into grafana plugin directory , then restart grafana.
14 |
15 | In mac the plugin directory is /usr/local/var/lib/grafana/plugins.
16 |
17 | After install the plugin ,restart grafana
18 |
19 | ```
20 | brew services start grafana
21 | ```
22 |
23 | ## Add datasource
24 |
25 | In datasource management panel, add a datasource with the type "LogService".
26 |
27 | In Http settings, set Url = http://${log\_service\_endpoint} . e.g. Your projectName is accesslog in qingdao region, then the url is http://cn-qingdao.log.aliyuncs.com.
28 |
29 | Access : select `Server(Default)`
30 |
31 | log service details:
32 |
33 | set Project and logstore
34 |
35 | AccessId and AccessKey : it is better to use a sub user accessId and accessKey.
36 |
37 | To ensure data security, AK is saved and cleared without echo
38 |
39 |
40 | ## Add dashboard
41 |
42 |
43 | Add a panel, in the datasource option, choose the log service datasource that is just created.
44 |
45 | In the query : insert your query , e.g.
46 |
47 | ```
48 | *|select count(1) as c,count(1)/2 as c1, __time__- __time__%60 as t group by t limit 10000
49 | ```
50 |
51 | The X column ,insert t (**Second timestamp**)
52 |
53 | The Y column , insert c,c1 (**Multiple columns are separated by commas**)
54 |
55 | Save the dashboard
56 |
57 | ## Usage
58 |
59 | ### Variables
60 |
61 | In the top right corner of the dashboard panel, click dashboard Settings and select Variables.
62 |
63 | Reference variables `$VariableName`
64 |
65 | ### Flow graph
66 |
67 | The X-axis is set to the time column
68 |
69 | The Y-axis is set to the format `col1#:#col2`, where col1 is the aggregate column and col2 is the other columns
70 |
71 | The Query sample is set to
72 | ```
73 | * | select to_unixtime(time) as time,status,count from (select time_series(__time__, '1m', '%Y-%m-%d %H:%i', '0') as time,status,count(*) as count from log group by status,time order by time limit 10000)
74 | ```
75 |
76 | 
77 |
78 | ### Pie
79 |
80 | The X-axis is set to `pie`
81 |
82 | The Y-axis is set to categories and numeric columns (example `method,pv`)
83 |
84 | The Query sample is set to
85 | ```
86 | $hostname | select count(1) as pv ,method group by method
87 | ```
88 |
89 | 
90 |
91 | ### Table
92 |
93 | The X-axis is set to `table` or null
94 |
95 | The Y-axis is set to columns
96 |
97 | ### World map penel
98 |
99 | The X-axis is set to `map`
100 |
101 | The Y-axis is set to `country,geo,pv`
102 |
103 | The Query sample is set to
104 | ```
105 | * | select count(1) as pv ,geohash(ip_to_geo(arbitrary(remote_addr))) as geo,ip_to_country(remote_addr) as country from log group by country having geo <>'' limit 1000
106 | ```
107 |
108 | Location Data : `geohash`
109 |
110 | Location Name Field : `country`
111 |
112 | Geo_point/Geohash Field :" `geo`
113 |
114 | Metric Field : `pv`
115 |
116 | The query:
117 |
118 | 
119 |
120 | Parameter Settings:
121 |
122 | 
123 |
124 | ### Alert
125 |
126 | #### Mode of notification
127 |
128 | In the alert notification panel, select New channel to add
129 |
130 | #### Alert
131 |
132 | **Attention** :Dashboard alert only, not plug-in alert
133 |
134 | A sample of:
135 |
136 | 
137 |
138 | Add the alert panel:
139 |
140 | 
141 |
142 | - The red line on the chart represents the set threshold. Click on the right side and drag it up and down.
143 | - Evaluate every `1m` for `5m`, Is the result calculated every minute, and the threshold is exceeded for five consecutive minutes.
144 | - After setting for, if the state exceeds the threshold value and changes from Ok to Pending, the alarm will not be triggered. After continuously exceeding the threshold value for a period of time, the alarm will be sent. If the state changes from Pending to Alerting, the alarm will only be notified once.
145 | - WHEN `avg ()` OF `query (B, 5m, now)` IS ABOVE `89`, That means line B has an average of more than 89 alarms in the last five minutes.
146 | - Add notification mode and notification information under Notifications.
147 |
148 |
149 | ## grafana 7.0
150 |
151 | [Backend plugins: Unsigned external plugins should not be loaded by default #24027](https://github.com/grafana/grafana/issues/24027)
152 |
153 |
154 | ## Contributors
155 |
156 | [@WPH95](https://github.com/WPH95) made a great contribution to this project.
157 |
158 | Thanks for the excellent work by [@WPH95](https://github.com/WPH95).
159 |
--------------------------------------------------------------------------------
/old_README_CN.md:
--------------------------------------------------------------------------------
1 | ## 阿里云日志服务数据源
2 |
3 | ## 编译
4 |
5 | ```
6 | yarn build
7 | ```
8 |
9 | ## 安装
10 |
11 | 依赖 `Grafana 8.0` 及以上版本 , `Grafana 8.0` 以下请使用 [1.0版本](https://github.com/aliyun/aliyun-log-grafana-datasource-plugin/releases/tag/1.0)
12 |
13 | 下载本插件到grafana插件目录下 , 然后重启grafana
14 |
15 | 在 mac 插件目录是 /usr/local/var/lib/grafana/plugins
16 |
17 | 重启命令为
18 |
19 | ```
20 | brew services restart grafana
21 | ```
22 |
23 | ## 添加数据源
24 |
25 | 在数据源管理面板, 添加 `LogService` 数据源
26 |
27 | 在 settings 面板, 设置 Url 为您日志服务 project 的 endpoint ( endpoint 在 project 的概览页可以看到).
28 |
29 | 例如你的 project 在 qingdao region, Url 可以填 `http://cn-qingdao.log.aliyuncs.com`
30 |
31 | Access 设置为 `Server(Default)`
32 |
33 |
34 | 设置 Project 和 logstore
35 |
36 | 设置 AccessId 和 AccessKeySecret , 最好配置为子账号的AK
37 |
38 | 为保证数据安全 , AK保存后清空 , 且不会回显
39 |
40 |
41 | ## 添加仪表盘
42 |
43 |
44 | 添加一个面板, 在 datasource 选项, 选择刚创建的日志服务数据源.
45 |
46 | 在 query 输入查询语句, 查询语法与日志服务控制台相同.
47 |
48 | ```
49 | *|select count(1) as c,count(1)/2 as c1, __time__- __time__%60 as t group by t limit 10000
50 | ```
51 |
52 | X轴设置为`t` (**秒级时间戳**)
53 |
54 | Y轴设置为`c,c1` (**多列用逗号分隔**)
55 |
56 | 保存仪表盘
57 |
58 | ## 使用
59 |
60 | ### 设置变量
61 |
62 | 在 dashboard 面板右上角点击 Dashboard settings, 选择 Variables
63 |
64 | 引用变量 `$VariableName`
65 |
66 | 变量查询语句中有双引号,需要转义成```\"```
67 |
68 | Query 类型变量
69 |
70 | 如果开启 Multi-value 可以设置多选查询
71 |
72 | **注:多选实现为用OR连接,如果语句有多个变量AND,变量两边需要加括号($VariableName),否则OR和AND会混一起**
73 |
74 | 如果 Name 和 Label 一致 可以按字段索引查询
75 |
76 | ### 设置Logs
77 |
78 | 每页最多展示100条
79 |
80 | ### 设置单值图
81 |
82 | 选择Stat
83 |
84 | X轴 设置为`stat`
85 |
86 | Y轴 设置为查询结果的列名
87 |
88 | ### 设置流图
89 |
90 | X轴 设置为时间列
91 |
92 | Y轴 设置为 `col1#:#col2` 这种格式, 其中 col1 为 聚合列, col2 为其他列
93 |
94 | Query 设置样例为
95 | ```
96 | * | select to_unixtime(time) as time,status,count from (select time_series(__time__, '1m', '%Y-%m-%d %H:%i', '0') as time,status,count(*) as count from log group by status,time order by time limit 10000)
97 | ```
98 |
99 | 
100 |
101 | ### 设置饼图
102 |
103 | X轴 设置为`pie`
104 |
105 | Y轴 设置为类别和数字列 (样例为`method,pv`)
106 |
107 | Query 设置样例为
108 | ```
109 | $hostname | select count(1) as pv ,method group by method
110 | ```
111 |
112 | 
113 |
114 | ### 设置表格
115 |
116 | X轴 设置为`table` 或空
117 |
118 | Y轴 设置为列
119 |
120 | ### 设置柱形图
121 |
122 | 选择 `Bar charts`
123 |
124 | X轴 设置为`bar`
125 |
126 | Y轴 第一个值为分组列,后面的值为数字列
127 |
128 | 
129 |
130 | ### 设置地图
131 |
132 | X轴 设置为`map`
133 |
134 | Y轴 设置为 `country,geo,pv`
135 |
136 | Query 设置样例为
137 | ```
138 | * | select count(1) as pv ,geohash(ip_to_geo(arbitrary(remote_addr))) as geo,ip_to_country(remote_addr) as country from log group by country having geo <>'' limit 1000
139 | ```
140 |
141 | Location Data 设置为 `geohash`
142 |
143 | Location Name Field 设置为 `country`
144 |
145 | geo_point/geohash Field 设置为 `geo`
146 |
147 | Metric Field 设置为 `pv`
148 |
149 | 查询语句:
150 |
151 | 
152 |
153 | 参数设置:
154 |
155 | 
156 |
157 | ### 设置Trace
158 |
159 | [**Trace数据格式**](https://help.aliyun.com/document_detail/208891.html)
160 |
161 | 在 Explore 面板
162 |
163 | X轴 设置为`trace`
164 |
165 | 
166 |
167 | ### 设置告警
168 |
169 | #### 通知方式
170 |
171 | 在告警通知方式面板, 选择 New channel 添加
172 |
173 | **注意** :选择dingding告警, 在钉钉机器人的安全设置里选自定义关键词, 添加 `Alerting`
174 |
175 | #### 添加告警
176 |
177 | **注意** :只支持dashboard告警, 不支持插件告警
178 |
179 | 样例如下:
180 |
181 | 
182 |
183 | 添加告警面板:
184 |
185 | 
186 |
187 | - 其中图表上红线代表设置的阈值, 点击右侧可以上下拖动
188 | - Evaluate every `1m` for `5m`, 代表计算每分钟的结果, 连续五分钟超过阈值告警
189 | - 设置for后, 如果超过阈值状态由Ok转为Pending, 不会触发告警, 连续超过阈值一段时候后发送告警, 状态由Pending转为Alerting, 告警只会通知一次
190 | - WHEN `avg ()` OF `query (B, 5m, now)` IS ABOVE `89`, 代表线条B最近五分钟的均值超过89告警
191 | - 在Notifications下添加通知方式及通知信息
192 |
193 |
194 | ## 常见问题
195 |
196 |
197 | ### 错误诊断
198 |
199 | 查看grafana 日志
200 |
201 | 在 mac 日志目录是 /usr/local/var/log/grafana
202 |
203 | 在 linux 日志目录是 /var/log/grafana
204 |
205 | - aliyun-log-plugin_linux_amd64: permission denied , 需要授予插件目录下dist/aliyun-log-plugin_linux_amd64执行权限
206 |
207 |
208 | ### grafana 7.0
209 |
210 | 参考 [Backend plugins: Unsigned external plugins should not be loaded by default #24027](https://github.com/grafana/grafana/issues/24027)
211 |
212 | 修改grafana配置文件
213 |
214 | 在mac上一般为 `/usr/local/etc/grafana/grafana.ini`
215 |
216 | 在linux上一般为 `/etc/grafana/grafana.ini`
217 |
218 | 在`[plugins]`标签下设置参数
219 |
220 | `allow_loading_unsigned_plugins = aliyun-log-service-datasource`
221 |
222 |
223 | ### 添加source变量
224 |
225 | `* | select distinct __source__ as source`
226 |
227 | __source__字段默认会被过滤掉,需要用as起别名
228 |
229 | ### provision 配置
230 |
231 | ```yml
232 | apiVersion: 1
233 |
234 | datasources:
235 | - name: LogService
236 | type: aliyun-log-service-datasource
237 | url: http://cn-hangzhou.log.aliyuncs.com
238 | jsonData:
239 | project: xxxxx
240 | logstore: xxxxx
241 | secureJsonData:
242 | accessKeyId: xxxx
243 | accessKeySecret: xxxxx
244 | editable: true
245 | ```
246 |
247 |
248 | ### 钉钉群
249 |
250 |
251 |
252 |
253 |
254 |
255 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "aliyun-log-service-datasource",
3 | "version": "2.38",
4 | "description": "Aliyun log service datasource (backend version)",
5 | "scripts": {
6 | "build": "grafana-toolkit plugin:build && mage",
7 | "build-front": "grafana-toolkit plugin:build",
8 | "build-back": "mage",
9 | "dev-back": "mage; launchctl stop com.aliyun.sls.autoGrafana; launchctl start com.aliyun.sls.autoGrafana",
10 | "test": "grafana-toolkit plugin:test",
11 | "dev": "grafana-toolkit plugin:dev",
12 | "watch": "grafana-toolkit plugin:dev --watch",
13 | "sign": "grafana-toolkit plugin:sign",
14 | "start": "yarn watch"
15 | },
16 | "author": "aliyun-log",
17 | "license": "Apache-2.0",
18 | "devDependencies": {
19 | "@grafana/data": "9.1.2",
20 | "@grafana/runtime": "9.1.2",
21 | "@grafana/toolkit": "9.1.2",
22 | "@grafana/experimental": "1.8.0",
23 | "@grafana/ui": "9.1.2",
24 | "@types/lodash": "latest",
25 | "@types/uuid": "9.0.1"
26 | },
27 | "resolutions": {
28 | "rxjs": "6.6.3"
29 | },
30 | "engines": {
31 | "node": ">=14"
32 | },
33 | "dependencies": {
34 | "js-base64": "^3.7.5",
35 | "urijs": "^1.19.11"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/pkg/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "io/ioutil"
6 | _ "net/http/pprof"
7 | "os"
8 | "path/filepath"
9 | "strconv"
10 |
11 | "github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
12 | "github.com/grafana/grafana-plugin-sdk-go/backend/log"
13 | )
14 |
15 | func main() {
16 | // Start listening to requests sent from Grafana. This call is blocking so
17 | // it won't finish until Grafana shuts down the process or the plugin choose
18 | // to exit by itself using os.Exit. Manage automatically manages life cycle
19 | // of datasource instances. It accepts datasource instance factory as first
20 | // argument. This factory will be automatically called on incoming request
21 | // from Grafana to create different instances of SampleDatasource (per datasource
22 | // ID). When datasource configuration changed Dispose method will be called and
23 | // new datasource instance created using NewSampleDatasource factory.
24 |
25 | loadConfig()
26 | // if err := datasource.Manage("aliyun-log-backend-datasource", NewSLSDatasource, datasource.ManageOpts{}); err != nil {
27 | // log.DefaultLogger.Error(err.Error())
28 | // os.Exit(1)
29 | // }
30 | err := datasource.Serve(NewSLSDatasource())
31 | grafanaVersion := os.Getenv("GF_VERSION")
32 | log.DefaultLogger.Info("GF_VERSION", grafanaVersion)
33 |
34 | if err != nil {
35 | log.DefaultLogger.Error(err.Error())
36 | os.Exit(1)
37 | }
38 | }
39 |
40 | var maxPointsLimit = 6000000
41 | var compatible = false
42 |
43 | func loadConfig() {
44 | ex, err := os.Executable()
45 | if err != nil {
46 | log.DefaultLogger.Info("", err)
47 | return
48 | }
49 | exPath := filepath.Dir(ex)
50 | b, err := ioutil.ReadFile(exPath + "/plugin.json")
51 | if err != nil {
52 | log.DefaultLogger.Info("", err)
53 | return
54 | }
55 | m := map[string]interface{}{}
56 | err = json.Unmarshal(b, &m)
57 | if err != nil {
58 | log.DefaultLogger.Info("", err)
59 | return
60 | }
61 | maxPointsI := m["flow_chart_max_points"]
62 | if maxPointsI == nil {
63 | return
64 | }
65 | if _, ok := maxPointsI.(string); !ok {
66 | return
67 | }
68 | parseInt, err := strconv.ParseInt(maxPointsI.(string), 0, 0)
69 | if err != nil {
70 | log.DefaultLogger.Info("", err)
71 | return
72 | }
73 | maxPointsLimit = int(parseInt)
74 |
75 | compatibleI := m["legacy_compatible"]
76 | if compatibleI == nil {
77 | return
78 | }
79 | if _, ok := compatibleI.(bool); ok {
80 | compatible = compatibleI.(bool)
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/pkg/models.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 |
7 | "github.com/grafana/grafana-plugin-sdk-go/backend"
8 | )
9 |
10 | type Header struct {
11 | Name string `json:"name"`
12 | Value string `json:"value"`
13 | }
14 |
15 | type LogSource struct {
16 | Endpoint string
17 | Project string `json:"project"`
18 | LogStore string `json:"logstore"`
19 | RoleArn string `json:"roleArn"`
20 | Region string
21 | AccessKeyId string
22 | AccessKeySecret string
23 | Headers []Header `json:"headers"`
24 | }
25 |
26 | type QueryInfo struct {
27 | Type string `json:"type"`
28 | QueryMode string `json:"mode"`
29 | Query string `json:"query"`
30 | Xcol string `json:"xcol"`
31 | Ycol string `json:"ycol"`
32 | LogsPerPage int64 `json:"logsPerPage"`
33 | CurrentPage int64 `json:"currentPage"`
34 | LogStore string `json:"logStore"`
35 | LegendFormat string `json:"legendFormat"`
36 | QueryType string `json:"queryType"`
37 | Step string `json:"step"`
38 | IntervalMs int64 `json:"intervalMs"`
39 | TotalLogs int64 `json:"totalLogs"`
40 | PowerSql bool `json:"powerSql"`
41 | }
42 |
43 | type Result struct {
44 | refId string
45 | dataResponse backend.DataResponse
46 | }
47 |
48 | // Contents {"keys":["c","c1","t"],"terms":[["*",""]],"limited":"100"}
49 | type Contents struct {
50 | Keys []string `json:"keys"`
51 | Terms [][]string `json:"terms"`
52 | Limited string `json:"limited"`
53 | }
54 |
55 | type ResultItem struct {
56 | Metric map[string]string `json:"metric"`
57 | Values [][]interface{} `json:"values"`
58 | Value []interface{} `json:"value"`
59 | }
60 | type MetricData struct {
61 | ResultType string `json:"resultType"`
62 | Result []ResultItem `json:"result"`
63 | }
64 | type MetricLogs struct {
65 | Status string `json:"status"`
66 | Data MetricData `json:"data"`
67 | }
68 |
69 | func LoadSettings(ctx backend.PluginContext) (*LogSource, error) {
70 | model := &LogSource{}
71 |
72 | settings := ctx.DataSourceInstanceSettings
73 | err := json.Unmarshal(settings.JSONData, &model)
74 | if err != nil {
75 | return nil, fmt.Errorf("error reading settings: %s", err.Error())
76 | }
77 | model.Endpoint = settings.URL
78 | model.AccessKeyId = settings.DecryptedSecureJSONData["accessKeyId"]
79 | model.AccessKeySecret = settings.DecryptedSecureJSONData["accessKeySecret"]
80 |
81 | return model, nil
82 | }
83 |
--------------------------------------------------------------------------------
/pkg/ram.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "strings"
6 |
7 | "github.com/aliyun/alibaba-cloud-sdk-go/sdk"
8 | "github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials"
9 | ram "github.com/aliyun/alibaba-cloud-sdk-go/services/ram"
10 | )
11 |
12 | var POLICY_LEN_ERROR = "policy length should be 1. Role policy should ONLY have AliyunLogReadOnlyAccess."
13 | var RAM_NO_PERMISSION_ERROR = "configured user should have AliyunRAMReadOnlyAccess."
14 | var POLYCY_NOT_MATCH_ERROR = "role policy should only have AliyunLogReadOnlyAccess."
15 |
16 | func roleCheck(ak string, sk string, roleName string) ([]ram.Policy, error) {
17 | config := sdk.NewConfig()
18 | credential := credentials.NewAccessKeyCredential(ak, sk)
19 | // log.DefaultLogger.Info("roleName", roleName)
20 | client, err := ram.NewClientWithOptions("cn-hangzhou", config, credential)
21 | if err != nil {
22 | return nil, err
23 | }
24 | request := ram.CreateListPoliciesForRoleRequest()
25 | request.Scheme = "https"
26 | request.RoleName = roleName
27 |
28 | response, err := client.ListPoliciesForRole(request)
29 | if err != nil {
30 | s := err.Error()
31 | if strings.Contains(s, "NoPermission") {
32 | return nil, errors.New(RAM_NO_PERMISSION_ERROR)
33 | }
34 | return nil, err
35 | }
36 | policyList := response.Policies.Policy
37 | len := len(policyList)
38 | if len != 1 {
39 | return nil, errors.New(POLICY_LEN_ERROR)
40 | }
41 | if policyList[0].PolicyName != "AliyunLogReadOnlyAccess" {
42 | return nil, errors.New(POLYCY_NOT_MATCH_ERROR)
43 | }
44 | return policyList, nil
45 | }
46 |
--------------------------------------------------------------------------------
/pkg/resource.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "io/ioutil"
6 | "net/http"
7 | "net/url"
8 | "regexp"
9 | "strings"
10 |
11 | sls "github.com/aliyun/aliyun-log-go-sdk"
12 | "gitlab.alibaba-inc.com/rapt/go-security-utils/network"
13 |
14 | "github.com/grafana/grafana-plugin-sdk-go/backend"
15 | "github.com/grafana/grafana-plugin-sdk-go/backend/log"
16 | "github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
17 | )
18 |
19 | func newResourceHandler(ds *SlsDatasource) backend.CallResourceHandler {
20 | mux := http.NewServeMux()
21 |
22 | // register route
23 | mux.HandleFunc("/api/gotoSLS", ds.gotoSLS)
24 | mux.HandleFunc("/api/version", ds.serveVersion)
25 | mux.HandleFunc("/api/getLogstoreList", ds.getLogstoreList)
26 |
27 | return httpadapter.New(mux)
28 | }
29 |
30 | func (ds *SlsDatasource) serveVersion(w http.ResponseWriter, r *http.Request) {
31 | // Handle query request...
32 | }
33 |
34 | type ListLogstoresData struct {
35 | Project string
36 | TelemetryType string
37 | }
38 |
39 | func (ds *SlsDatasource) getLogstoreList(w http.ResponseWriter, r *http.Request) {
40 | response := map[string]interface{}{
41 | "data": []map[string]interface{}{}, // 定义 data 为一个任意类型的对象数组
42 | "res": nil,
43 | "message": "",
44 | }
45 |
46 | config, err := LoadSettings(httpadapter.PluginConfigFromContext(r.Context()))
47 | if err != nil {
48 | response["message"] = err.Error()
49 | http.Error(w, err.Error(), http.StatusBadRequest)
50 | return
51 | }
52 |
53 | provider := sls.NewStaticCredentialsProvider(config.AccessKeyId, config.AccessKeySecret, "")
54 | client := sls.CreateNormalInterfaceV2(config.Endpoint, provider)
55 | client.SetUserAgent("grafana-go")
56 |
57 | if config.Region != "" {
58 | client.SetAuthVersion(sls.AuthV4)
59 | client.SetRegion(config.Region)
60 | }
61 |
62 | body, err := ioutil.ReadAll(r.Body)
63 | if err != nil {
64 | response["message"] = err.Error()
65 | http.Error(w, err.Error(), http.StatusBadRequest)
66 | return
67 | }
68 |
69 | // 解析request JSON 数据
70 | var data ListLogstoresData
71 | if err := json.Unmarshal(body, &data); err != nil {
72 | response["message"] = err.Error()
73 | http.Error(w, err.Error(), http.StatusBadRequest)
74 | return
75 | }
76 |
77 | log.DefaultLogger.Debug("getBODY", "body", body, "bodyData", data)
78 |
79 | project, err := client.GetProject(data.Project)
80 | if err != nil {
81 | response["message"] = err.Error()
82 | http.Error(w, err.Error(), http.StatusBadRequest)
83 | return
84 | }
85 |
86 | // 拿当前 Project 的信息
87 | list, err := project.ListLogStoreV2(0, 500, data.TelemetryType)
88 | if err != nil {
89 | response["message"] = err.Error()
90 | http.Error(w, err.Error(), http.StatusBadRequest)
91 | return
92 | }
93 |
94 | response["data"] = list
95 | response["res"] = list
96 |
97 | w.Header().Set("Content-Type", "application/json")
98 | w.WriteHeader(http.StatusOK)
99 | json.NewEncoder(w).Encode(response)
100 | log.DefaultLogger.Debug("get logstore success.")
101 | }
102 |
103 | type Data struct {
104 | Encoding string `json:"encoding"`
105 | Logstore string `json:"logstore"`
106 | Type string `json:"type"`
107 | }
108 |
109 | func (ds *SlsDatasource) gotoSLS(w http.ResponseWriter, r *http.Request) {
110 |
111 | response := map[string]interface{}{
112 | "message": "",
113 | "err": "",
114 | "url": "",
115 | // "policy": "",
116 | }
117 |
118 | config, err := LoadSettings(httpadapter.PluginConfigFromContext(r.Context()))
119 | if err != nil {
120 | http.Error(w, err.Error(), http.StatusBadRequest)
121 | return
122 | }
123 |
124 | ak := config.AccessKeyId
125 | sk := config.AccessKeySecret
126 | arn := config.RoleArn
127 | prj := config.Project
128 | logstore := config.LogStore
129 |
130 | body, err := ioutil.ReadAll(r.Body)
131 | if err != nil {
132 | http.Error(w, err.Error(), http.StatusBadRequest)
133 | return
134 | }
135 |
136 | // 解析request JSON 数据
137 | var data Data
138 | if err := json.Unmarshal(body, &data); err != nil {
139 | http.Error(w, err.Error(), http.StatusBadRequest)
140 | return
141 | }
142 |
143 | logstoreType := "/logsearch/"
144 |
145 | if data.Type == "metricsql" || data.Type == "metricstore" {
146 | logstoreType = "/metric/"
147 | }
148 |
149 | if data.Logstore != "" {
150 | logstore = data.Logstore
151 | }
152 |
153 | pattern := `^acs:ram::\d+:role\/[^\/]+$`
154 | regex, err := regexp.Compile(pattern)
155 | if err != nil {
156 | return
157 | }
158 |
159 | normalJump := false
160 |
161 | if len(arn) == 0 {
162 | normalJump = true
163 | } else {
164 | if !regex.MatchString(arn) {
165 | response["err"] = "regexCheckError"
166 | response["message"] = "roleArn 不符合格式,请检查。"
167 | normalJump = true
168 | }
169 | }
170 |
171 | if !normalJump {
172 | roleName := strings.Split(arn, "/")[1]
173 | _, err2 := roleCheck(ak, sk, roleName)
174 | if err2 != nil {
175 | response["err"] = "roleCheckError"
176 | response["message"] = err2.Error()
177 | // http.Error(w, err2.Error(), http.StatusBadRequest)
178 | // return
179 | normalJump = true
180 | }
181 | // response["policy"] = p
182 | }
183 |
184 | if !normalJump {
185 | client := NewClient(ak, sk, arn, "default")
186 | stsResp, err := client.AssumeRole(900)
187 | if err != nil {
188 | http.Error(w, err.Error(), http.StatusInternalServerError)
189 | log.DefaultLogger.Error(err.Error())
190 | // response["err"] = err.Error()
191 | // response["message"] = err.Error()
192 | // w.Header().Set("Content-Type", "application/json")
193 | // w.WriteHeader(http.StatusInternalServerError)
194 | // json.NewEncoder(w).Encode(response)
195 | return
196 | }
197 | id := stsResp.Credentials.AccessKeyId
198 | secret := stsResp.Credentials.AccessKeySecret
199 | token := stsResp.Credentials.SecurityToken
200 |
201 | // 使用STS Token换取控制台Signin Token
202 | SigninResp, err := getSigninToken(id, secret, token)
203 | if err != nil {
204 | panic(err)
205 | }
206 | signinToken := SigninResp.SigninToken
207 |
208 | // 生成登录链接
209 | loginUrl := "http://www.aliyun.com"
210 | // destination := "http://sls4service.console.aliyun.com"
211 | destination := "http://sls4service.console.aliyun.com/lognext/project/" + prj + logstoreType + logstore + "?isShare=true&hideTopbar=true&hideSidebar=true&ignoreTabLocalStorage=true&" + data.Encoding
212 | url, err := genSigninUrl(signinToken, loginUrl, destination)
213 | if err != nil {
214 | http.Error(w, err.Error(), http.StatusInternalServerError)
215 | log.DefaultLogger.Error(err.Error())
216 | return
217 | }
218 |
219 | response["url"] = url
220 | w.Header().Set("Content-Type", "application/json")
221 | w.WriteHeader(http.StatusOK)
222 | json.NewEncoder(w).Encode(response)
223 | log.DefaultLogger.Debug("Goto SLS with STS success.", url)
224 | return
225 | }
226 | url := "https://sls.console.aliyun.com/lognext/project/" + prj + logstoreType + logstore + "?" + data.Encoding
227 | response["url"] = url
228 | w.Header().Set("Content-Type", "application/json")
229 | w.WriteHeader(http.StatusOK)
230 | json.NewEncoder(w).Encode(response)
231 | log.DefaultLogger.Debug("Goto SLS with Normal jump success.", url)
232 | }
233 |
234 | func getSigninToken(id string, secret string, token string) (*SigninResponse, error) {
235 | urlStr := "http://signin.aliyun.com/federation?Action=GetSigninToken"
236 | urlStr += "&AccessKeyId=" + id
237 | urlStr += "&AccessKeySecret=" + secret
238 | urlStr += "&SecurityToken=" + url.QueryEscape(token)
239 | urlStr += "&TicketType=mini"
240 |
241 | transport := http.DefaultTransport.(*http.Transport).Clone()
242 | transport.DialContext = network.DefaultNetworkFilter.FilterHttpDialContext(transport.DialContext)
243 | client := &http.Client{
244 | Transport: transport,
245 | }
246 |
247 | res, err := client.Get(urlStr)
248 | if err != nil {
249 | return nil, err
250 | }
251 | body, err := ioutil.ReadAll(res.Body)
252 | if err != nil {
253 | return nil, err
254 | }
255 | // fmt.Println("SigninToken json:", string(body))
256 | resp := SigninResponse{}
257 | err = json.Unmarshal(body, &resp)
258 | if err != nil {
259 | return nil, err
260 | }
261 | return &resp, nil
262 | }
263 |
264 | func genSigninUrl(signinToken string, loginUrl string, destination string) (string, error) {
265 | urlStr := "http://signin.aliyun.com/federation?Action=Login"
266 | urlStr += "&LoginUrl=" + url.QueryEscape(loginUrl)
267 | urlStr += "&Destination=" + url.QueryEscape(destination)
268 | urlStr += "&SigninToken=" + url.QueryEscape(signinToken)
269 | client := &http.Client{
270 | CheckRedirect: func(req *http.Request, via []*http.Request) error {
271 | return http.ErrUseLastResponse
272 | },
273 | }
274 | res, err := client.Get(urlStr)
275 | if err != nil {
276 | return "", err
277 | }
278 | location, err := res.Location()
279 | if err != nil {
280 | return "", err
281 | }
282 | locationUrl := location.String()
283 | return locationUrl, nil
284 | }
285 |
286 | type SigninResponse struct {
287 | SigninToken string
288 | }
289 |
--------------------------------------------------------------------------------
/pkg/sts.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "crypto/hmac"
5 | "crypto/sha1"
6 | "crypto/tls"
7 | "encoding/base64"
8 | "encoding/json"
9 | "fmt"
10 | "io/ioutil"
11 | "net/http"
12 | "net/url"
13 | "strconv"
14 | "time"
15 |
16 | uuid "github.com/satori/go.uuid"
17 | )
18 |
19 | // Client sts client
20 | type Client struct {
21 | AccessKeyId string
22 | AccessKeySecret string
23 | RoleArn string
24 | SessionName string
25 | }
26 |
27 | // ServiceError sts service error
28 | type ServiceError struct {
29 | Code string
30 | Message string
31 | RequestId string
32 | HostId string
33 | RawMessage string
34 | StatusCode int
35 | }
36 |
37 | // Credentials the credentials obtained by AssumedRole,
38 | // used for the peration of Alibaba Cloud service.
39 | type Credentials struct {
40 | AccessKeyId string
41 | AccessKeySecret string
42 | Expiration time.Time
43 | SecurityToken string
44 | }
45 |
46 | // AssumedRoleUser the user to AssumedRole
47 | type AssumedRoleUser struct {
48 | Arn string
49 | AssumedRoleId string
50 | }
51 |
52 | // Response the response of AssumeRole
53 | type Response struct {
54 | Credentials Credentials
55 | AssumedRoleUser AssumedRoleUser
56 | RequestId string
57 | }
58 |
59 | // Error implement interface error
60 | func (e *ServiceError) Error() string {
61 | return fmt.Sprintf("oss: service returned error: StatusCode=%d, ErrorCode=%s, ErrorMessage=%s, RequestId=%s",
62 | e.StatusCode, e.Code, e.Message, e.RequestId)
63 | }
64 |
65 | // NewClient New STS Client
66 | func NewClient(accessKeyId, accessKeySecret, roleArn, sessionName string) *Client {
67 | return &Client{
68 | AccessKeyId: accessKeyId,
69 | AccessKeySecret: accessKeySecret,
70 | RoleArn: roleArn,
71 | SessionName: sessionName,
72 | }
73 | }
74 |
75 | const (
76 | // StsSignVersion sts sign version
77 | StsSignVersion = "1.0"
78 | // StsAPIVersion sts api version
79 | StsAPIVersion = "2015-04-01"
80 | // StsHost sts host
81 | StsHost = "https://sts.aliyuncs.com/"
82 | // TimeFormat time fomrat
83 | TimeFormat = "2006-01-02T15:04:05Z"
84 | // RespBodyFormat respone body format
85 | RespBodyFormat = "JSON"
86 | // PercentEncode '/'
87 | PercentEncode = "%2F"
88 | // HTTPGet http get method
89 | HTTPGet = "GET"
90 | )
91 |
92 | // AssumeRole assume role
93 | func (c *Client) AssumeRole(expiredTime uint) (*Response, error) {
94 | url, err := c.generateSignedURL(expiredTime)
95 | if err != nil {
96 | return nil, err
97 | }
98 |
99 | body, status, err := c.sendRequest(url)
100 | if err != nil {
101 | return nil, err
102 | }
103 |
104 | return c.handleResponse(body, status)
105 | }
106 |
107 | // Private function
108 | func (c *Client) generateSignedURL(expiredTime uint) (string, error) {
109 | uid := uuid.NewV4()
110 | rst := uid.String()
111 | queryStr := "SignatureVersion=" + StsSignVersion
112 | queryStr += "&Format=" + RespBodyFormat
113 | queryStr += "&Timestamp=" + url.QueryEscape(time.Now().UTC().Format(TimeFormat))
114 | queryStr += "&RoleArn=" + url.QueryEscape(c.RoleArn)
115 | queryStr += "&RoleSessionName=" + c.SessionName
116 | queryStr += "&AccessKeyId=" + c.AccessKeyId
117 | queryStr += "&SignatureMethod=HMAC-SHA1"
118 | queryStr += "&Version=" + StsAPIVersion
119 | queryStr += "&Action=AssumeRole"
120 | queryStr += "&SignatureNonce=" + rst
121 | queryStr += "&DurationSeconds=" + strconv.FormatUint((uint64)(expiredTime), 10)
122 |
123 | // Sort query string
124 | queryParams, err := url.ParseQuery(queryStr)
125 | if err != nil {
126 | return "", err
127 | }
128 | result := queryParams.Encode()
129 |
130 | strToSign := HTTPGet + "&" + PercentEncode + "&" + url.QueryEscape(result)
131 |
132 | // Generate signature
133 | hashSign := hmac.New(sha1.New, []byte(c.AccessKeySecret+"&"))
134 | hashSign.Write([]byte(strToSign))
135 | signature := base64.StdEncoding.EncodeToString(hashSign.Sum(nil))
136 |
137 | // Build url
138 | assumeURL := StsHost + "?" + queryStr + "&Signature=" + url.QueryEscape(signature)
139 |
140 | return assumeURL, nil
141 | }
142 |
143 | func (c *Client) sendRequest(url string) ([]byte, int, error) {
144 | tr := &http.Transport{
145 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
146 | }
147 | client := &http.Client{Transport: tr}
148 |
149 | resp, err := client.Get(url)
150 | if err != nil {
151 | return nil, -1, err
152 | }
153 | defer resp.Body.Close()
154 |
155 | body, err := ioutil.ReadAll(resp.Body)
156 | return body, resp.StatusCode, err
157 | }
158 |
159 | func (c *Client) handleResponse(responseBody []byte, statusCode int) (*Response, error) {
160 | if statusCode != http.StatusOK {
161 | se := ServiceError{StatusCode: statusCode, RawMessage: string(responseBody)}
162 | err := json.Unmarshal(responseBody, &se)
163 | if err != nil {
164 | return nil, err
165 | }
166 | return nil, &se
167 | }
168 |
169 | resp := Response{}
170 | err := json.Unmarshal(responseBody, &resp)
171 | if err != nil {
172 | return nil, err
173 | }
174 | return &resp, nil
175 | }
176 |
--------------------------------------------------------------------------------
/src/ConfigEditor.tsx:
--------------------------------------------------------------------------------
1 | import React, { ChangeEvent, PureComponent } from 'react';
2 | import { Input, InlineField } from '@grafana/ui';
3 | import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
4 | import { HeaderWithValue, SLSDataSourceOptions, SLSSecureJsonData } from './types';
5 | import CustomHeaders from './custom-header/custom-headers';
6 | import { ConfigSection } from './components/configSection';
7 | import { DataSourceDescription } from './components/description';
8 | import { SecretInput } from './components/QueryEditor/SecretInput';
9 |
10 | interface Props extends DataSourcePluginOptionsEditorProps {}
11 |
12 | interface State {}
13 |
14 | export class SLSConfigEditor extends PureComponent {
15 | onEndpointChange = (event: ChangeEvent) => {
16 | const { onOptionsChange, options } = this.props;
17 | options.url = event.target.value;
18 | onOptionsChange({ ...options });
19 | };
20 |
21 | onProjectChange = (event: ChangeEvent) => {
22 | const { onOptionsChange, options } = this.props;
23 | const jsonData = {
24 | ...options.jsonData,
25 | project: event.target.value,
26 | };
27 | onOptionsChange({ ...options, jsonData });
28 | };
29 |
30 | onLogStoreChange = (event: ChangeEvent) => {
31 | const { onOptionsChange, options } = this.props;
32 | const jsonData = {
33 | ...options.jsonData,
34 | logstore: event.target.value,
35 | };
36 | onOptionsChange({ ...options, jsonData });
37 | };
38 |
39 | onRegionChange = (event: ChangeEvent) => {
40 | const { onOptionsChange, options } = this.props;
41 | const jsonData = {
42 | ...options.jsonData,
43 | region: event.target.value,
44 | };
45 | onOptionsChange({ ...options, jsonData });
46 | };
47 | onRoleArnChange = (event: ChangeEvent) => {
48 | const { onOptionsChange, options } = this.props;
49 | const jsonData = {
50 | ...options.jsonData,
51 | roleArn: event.target.value,
52 | };
53 | onOptionsChange({ ...options, jsonData });
54 | };
55 |
56 | // Secure field (only sent to the backend)
57 | onAKIDChange = (event: ChangeEvent) => {
58 | const { onOptionsChange, options } = this.props;
59 | var accessKeySecret = '';
60 | if (options.secureJsonData !== undefined) {
61 | if (options.secureJsonData.hasOwnProperty('accessKeySecret')) {
62 | // @ts-ignore
63 | accessKeySecret = options.secureJsonData['accessKeySecret'];
64 | }
65 | }
66 | onOptionsChange({
67 | ...options,
68 | secureJsonData: {
69 | accessKeyId: event.target.value,
70 | accessKeySecret: accessKeySecret,
71 | },
72 | });
73 | };
74 |
75 | onAKSecretChange = (event: ChangeEvent) => {
76 | const { onOptionsChange, options } = this.props;
77 | var accessKeyId = '';
78 | if (options.secureJsonData !== undefined) {
79 | if (options.secureJsonData.hasOwnProperty('accessKeyId')) {
80 | // @ts-ignore
81 | accessKeyId = options.secureJsonData['accessKeyId'];
82 | }
83 | }
84 | onOptionsChange({
85 | ...options,
86 | secureJsonData: {
87 | accessKeyId: accessKeyId,
88 | accessKeySecret: event.target.value,
89 | },
90 | });
91 | };
92 |
93 | onResetAKID = () => {
94 | const { onOptionsChange, options } = this.props;
95 | onOptionsChange({
96 | ...options,
97 | secureJsonFields: {
98 | ...options.secureJsonFields,
99 | accessKeyId: false,
100 | },
101 | secureJsonData: {
102 | ...options.secureJsonData,
103 | accessKeyId: '',
104 | },
105 | });
106 | };
107 | onResetAKSecret = () => {
108 | const { onOptionsChange, options } = this.props;
109 | onOptionsChange({
110 | ...options,
111 | secureJsonFields: {
112 | ...options.secureJsonFields,
113 | accessKeySecret: false,
114 | },
115 | secureJsonData: {
116 | ...options.secureJsonData,
117 | accessKeySecret: '',
118 | },
119 | });
120 | };
121 |
122 | onHeadersChange = (headers: HeaderWithValue[]) => {
123 | const { onOptionsChange, options } = this.props;
124 | const jsonData = {
125 | ...options.jsonData,
126 | headers,
127 | };
128 | onOptionsChange({ ...options, jsonData });
129 | };
130 |
131 | render() {
132 | const { options } = this.props;
133 | const { jsonData, secureJsonFields, url } = options;
134 | const secureJsonData = (options.secureJsonData || {}) as SLSSecureJsonData;
135 |
136 | return (
137 | <>
138 |
142 |
143 |
144 | {/* EndPonit */}
145 |
151 | Specify a complete Endpoint URL
152 |
153 | (for example: cn-hangzhou.log.aliyuncs.com)
154 | >
155 | }
156 | grow
157 | interactive
158 | >
159 |
166 |
167 |
168 | {/* Project */}
169 |
177 |
184 |
185 |
186 | {/* AccessKeyID */}
187 |
195 |
203 |
204 |
205 | {/* AccessKeySecret */}
206 |
214 |
222 |
223 |
224 | {/* Other */}
225 |
232 | Region
233 |
234 | (for example: cn-hangzhou)
235 | >
236 | }
237 | grow
238 | interactive
239 | >
240 |
247 |
248 |
249 |
250 | {/* Other */}
251 |
252 |
253 |
262 |
269 |
270 |
271 | {/* roleArn */}
272 |
280 |
287 |
288 |
289 | {/* headers */}
290 |
291 |
292 | >
293 | );
294 | }
295 | }
296 |
--------------------------------------------------------------------------------
/src/SLS-monaco-editor/MonacoQueryField.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/css';
2 | import { debounce } from 'lodash';
3 | import { slsLanguageDefinition } from './language-definition/definition';
4 | import React, { useRef, useEffect } from 'react';
5 | import { useLatest } from 'react-use';
6 | import { v4 as uuidv4 } from 'uuid';
7 |
8 | import { GrafanaTheme2 } from '@grafana/data';
9 | import { selectors } from '@grafana/e2e-selectors';
10 | import { useTheme2, ReactMonacoEditor, Monaco, monacoTypes } from '@grafana/ui';
11 |
12 | import { Props } from './MonacoQueryFieldProps';
13 | import { getOverrideServices } from './getOverrideServices';
14 | import { completionItemProvider, language, languageConfiguration } from './language-definition/sls';
15 |
16 | const options: monacoTypes.editor.IStandaloneEditorConstructionOptions = {
17 | codeLens: false,
18 | contextmenu: false,
19 | fixedOverflowWidgets: true,
20 | folding: false,
21 | fontSize: 14,
22 | lineDecorationsWidth: 8,
23 | lineNumbers: 'off',
24 | minimap: { enabled: false },
25 | overviewRulerBorder: false,
26 | overviewRulerLanes: 0,
27 | padding: {
28 | top: 4,
29 | bottom: 5,
30 | },
31 | renderLineHighlight: 'none',
32 | scrollbar: {
33 | vertical: 'hidden',
34 | verticalScrollbarSize: 8,
35 | horizontal: 'hidden',
36 | horizontalScrollbarSize: 0,
37 | },
38 | scrollBeyondLastLine: false,
39 | // suggest: getSuggestOptions(),
40 | suggestFontSize: 12,
41 | wordWrap: 'on',
42 | };
43 |
44 | // single line and multiline
45 | const EDITOR_HEIGHT_OFFSET = 2;
46 | const SLS_LANG_ID = slsLanguageDefinition.id;
47 |
48 | let SLS_SETUP_STARTED = false;
49 |
50 | function ensureSLSQL(monaco: Monaco) {
51 | if (SLS_SETUP_STARTED === false) {
52 | SLS_SETUP_STARTED = true;
53 | const { aliases, extensions, mimetypes } = slsLanguageDefinition;
54 | monaco.languages.register({ id: SLS_LANG_ID, aliases, extensions, mimetypes });
55 | monaco.languages.setMonarchTokensProvider(SLS_LANG_ID, language);
56 | monaco.languages.setLanguageConfiguration(SLS_LANG_ID, languageConfiguration);
57 | // loader().then((mod) => {
58 | // monaco.languages.setMonarchTokensProvider(SLS_LANG_ID, mod.language);
59 | // monaco.languages.setLanguageConfiguration(SLS_LANG_ID, mod.languageConfiguration);
60 | // });
61 | }
62 | }
63 |
64 | const getStyles = (theme: GrafanaTheme2, placeholder: string) => {
65 | return {
66 | container: css`
67 | border-radius: ${theme.shape.borderRadius()};
68 | border: 1px solid ${theme.components.input.borderColor};
69 | `,
70 | placeholder: css`
71 | ::after {
72 | content: '${placeholder}';
73 | font-family: ${theme.typography.fontFamilyMonospace};
74 | opacity: 0.3;
75 | }
76 | `,
77 | };
78 | };
79 |
80 | const MonacoQueryField = (props: Props) => {
81 | const id = uuidv4();
82 |
83 | const overrideServicesRef = useRef(getOverrideServices());
84 | const containerRef = useRef(null);
85 | const { onBlur, onRunQuery, initialValue, placeholder, onChange } = props;
86 |
87 | // const lpRef = useLatest(languageProvider);
88 | // const historyRef = useLatest(history);
89 | const onRunQueryRef = useLatest(onRunQuery);
90 | const onBlurRef = useLatest(onBlur);
91 | const onChangeRef = useLatest(onChange);
92 |
93 | const autocompleteDisposeFun = useRef<(() => void) | null>(null);
94 |
95 | const theme = useTheme2();
96 | const styles = getStyles(theme, placeholder);
97 |
98 | useEffect(() => {
99 | return () => {
100 | autocompleteDisposeFun.current?.();
101 | };
102 | }, []);
103 |
104 | return (
105 |
110 | {
116 | ensureSLSQL(monaco);
117 | }}
118 | onMount={(editor, monaco) => {
119 | const isEditorFocused = editor.createContextKey('isEditorFocused' + id, false);
120 |
121 | editor.onDidBlurEditorWidget(() => {
122 | isEditorFocused.set(false);
123 | onBlurRef.current(editor.getValue());
124 | });
125 | editor.onDidFocusEditorText(() => {
126 | isEditorFocused.set(true);
127 | });
128 |
129 | const completionProvider = completionItemProvider
130 |
131 | const filteringCompletionProvider: monacoTypes.languages.CompletionItemProvider = {
132 | ...completionProvider,
133 | provideCompletionItems: (model, position, context, token) => {
134 | if (editor.getModel()?.id !== model.id) {
135 | return { suggestions: [] };
136 | }
137 | return completionProvider.provideCompletionItems(model, position, context, token);
138 | },
139 | };
140 |
141 | const { dispose } = monaco.languages.registerCompletionItemProvider(
142 | SLS_LANG_ID,
143 | filteringCompletionProvider
144 | );
145 |
146 | autocompleteDisposeFun.current = dispose;
147 |
148 | const updateElementHeight = () => {
149 | const containerDiv = containerRef.current;
150 | if (containerDiv !== null) {
151 | const pixelHeight = editor.getContentHeight();
152 | containerDiv.style.height = `${pixelHeight + EDITOR_HEIGHT_OFFSET}px`;
153 | containerDiv.style.width = '100%';
154 | const pixelWidth = containerDiv.clientWidth;
155 | editor.layout({ width: pixelWidth, height: pixelHeight });
156 | }
157 | };
158 |
159 | editor.onDidContentSizeChange(updateElementHeight);
160 | updateElementHeight();
161 |
162 |
163 | const updateCurrentEditorValue = debounce(() => {
164 | const editorValue = editor.getValue();
165 | onChangeRef.current(editorValue);
166 | }, 300);
167 |
168 | editor.getModel()?.onDidChangeContent(() => {
169 | updateCurrentEditorValue();
170 | });
171 |
172 | editor.addCommand(
173 | monaco.KeyMod.Shift | monaco.KeyCode.Enter,
174 | () => {
175 | onRunQueryRef.current(editor.getValue());
176 | },
177 | 'isEditorFocused' + id
178 | );
179 |
180 | if (placeholder) {
181 | const placeholderDecorators = [
182 | {
183 | range: new monaco.Range(1, 1, 1, 1),
184 | options: {
185 | className: styles.placeholder,
186 | isWholeLine: true,
187 | },
188 | },
189 | ];
190 |
191 | let decorators: string[] = [];
192 |
193 | const checkDecorators: () => void = () => {
194 | const model = editor.getModel();
195 |
196 | if (!model) {
197 | return;
198 | }
199 |
200 | const newDecorators = model.getValueLength() === 0 ? placeholderDecorators : [];
201 | decorators = model.deltaDecorations(decorators, newDecorators);
202 | };
203 |
204 | checkDecorators();
205 | editor.onDidChangeModelContent(checkDecorators);
206 | }
207 | }}
208 | />
209 |
210 | );
211 | };
212 |
213 |
214 | export default MonacoQueryField;
215 |
--------------------------------------------------------------------------------
/src/SLS-monaco-editor/MonacoQueryFieldOld.tsx:
--------------------------------------------------------------------------------
1 | import { Button, CodeEditor, Monaco, MonacoEditor } from '@grafana/ui';
2 | import React, { useCallback, useRef, useState } from 'react';
3 | import { Props } from './MonacoQueryFieldProps';
4 |
5 | export class Deferred {
6 | resolve?: (reason?: T | PromiseLike) => void;
7 | reject?: (reason?: any) => void;
8 | promise: Promise;
9 |
10 | constructor() {
11 | this.resolve = undefined;
12 | this.reject = undefined;
13 |
14 | this.promise = new Promise((resolve, reject) => {
15 | this.resolve = resolve as any;
16 | this.reject = reject;
17 | });
18 | Object.freeze(this);
19 | }
20 | }
21 |
22 | interface MonacoPromise {
23 | editor: MonacoEditor;
24 | monaco: Monaco;
25 | }
26 |
27 | const QueryField: React.FC = ({
28 | initialValue: query,
29 | onChange,
30 | disableMultiLine,
31 | }) => {
32 | const monacoPromiseRef = useRef>();
33 | const [queryHeight, setQueryHeight] = useState(30);
34 | const handleEditorMount = useCallback((editor: MonacoEditor, monaco: Monaco) => {
35 | monacoPromiseRef.current?.resolve?.({ editor, monaco });
36 | }, []);
37 |
38 | return (
39 |
40 |
41 |
52 |
53 | {!disableMultiLine && (
54 |
55 | {
61 | setQueryHeight(queryHeight - 30);
62 | }}
63 | />
64 | {
70 | setQueryHeight(queryHeight + 30);
71 | }}
72 | />
73 |
74 | )}
75 |
76 | );
77 | };
78 |
79 | export default QueryField;
80 |
--------------------------------------------------------------------------------
/src/SLS-monaco-editor/MonacoQueryFieldProps.ts:
--------------------------------------------------------------------------------
1 | import { HistoryItem } from '@grafana/data';
2 |
3 | export type Props = {
4 | initialValue: string;
5 | languageProvider: any;
6 | history: Array>;
7 | placeholder: string;
8 | onRunQuery: (value: string) => void;
9 | onBlur: (value: string) => void;
10 | onChange: (value: string) => void;
11 | };
12 |
--------------------------------------------------------------------------------
/src/SLS-monaco-editor/getOverrideServices.ts:
--------------------------------------------------------------------------------
1 | import { monacoTypes } from '@grafana/ui';
2 |
3 | function makeStorageService() {
4 | const strings = new Map();
5 |
6 | strings.set('expandSuggestionDocs', true.toString());
7 |
8 | return {
9 | onDidChangeValue: (data: unknown): void => undefined,
10 | onDidChangeTarget: (data: unknown): void => undefined,
11 | onWillSaveState: (data: unknown): void => undefined,
12 |
13 | get: (key: string, scope: unknown, fallbackValue?: string): string | undefined => {
14 | return strings.get(key) ?? fallbackValue;
15 | },
16 |
17 | getBoolean: (key: string, scope: unknown, fallbackValue?: boolean): boolean | undefined => {
18 | const val = strings.get(key);
19 | if (val !== undefined) {
20 | return val === 'true';
21 | } else {
22 | return fallbackValue;
23 | }
24 | },
25 |
26 | getNumber: (key: string, scope: unknown, fallbackValue?: number): number | undefined => {
27 | const val = strings.get(key);
28 | if (val !== undefined) {
29 | return parseInt(val, 10);
30 | } else {
31 | return fallbackValue;
32 | }
33 | },
34 |
35 | store: (
36 | key: string,
37 | value: string | boolean | number | undefined | null,
38 | scope: unknown,
39 | target: unknown
40 | ): void => {
41 | if (value === null || value === undefined) {
42 | strings.delete(key);
43 | } else {
44 | strings.set(key, value.toString());
45 | }
46 | },
47 |
48 | remove: (key: string, scope: unknown): void => {
49 | strings.delete(key);
50 | },
51 |
52 | keys: (scope: unknown, target: unknown): string[] => {
53 | return Array.from(strings.keys());
54 | },
55 |
56 | logStorage: (): void => {
57 | console.log('logStorage: not implemented');
58 | },
59 |
60 | migrate: (): Promise => {
61 | return Promise.resolve(undefined);
62 | },
63 |
64 | isNew: (scope: unknown): boolean => {
65 | return true;
66 | },
67 |
68 | flush: (reason?: unknown): Promise => {
69 | return Promise.resolve(undefined);
70 | },
71 | };
72 | }
73 |
74 | let overrideServices: monacoTypes.editor.IEditorOverrideServices | null = null;
75 |
76 | export function getOverrideServices(): monacoTypes.editor.IEditorOverrideServices {
77 | if (overrideServices === null) {
78 | overrideServices = {
79 | storageService: makeStorageService(),
80 | };
81 | }
82 |
83 | return overrideServices;
84 | }
85 |
--------------------------------------------------------------------------------
/src/SLS-monaco-editor/language-definition/definition.ts:
--------------------------------------------------------------------------------
1 | export const slsLanguageDefinition = {
2 | id: 'sls',
3 | extensions: [ '.sls' ],
4 | aliases: [ 'sls', 'SLS', 'SLSQuery' ],
5 | mimetypes: []
6 | };
7 |
--------------------------------------------------------------------------------
/src/SLS-monaco-editor/language-definition/sls.ts:
--------------------------------------------------------------------------------
1 | import type { monacoTypes } from '@grafana/ui';
2 | import { functions, keywords, operators } from './slsterms';
3 | import _ from 'lodash';
4 |
5 | export const languageConfiguration: monacoTypes.languages.LanguageConfiguration = {
6 | wordPattern: /(-?\d*\.\d\w*)|([^`~!#%^&*()\-=+\[{\]}\\|;:'",.<>\/?\s]+)/g,
7 | comments: {
8 | lineComment: '#',
9 | },
10 | brackets: [
11 | ['{', '}'],
12 | ['[', ']'],
13 | ['(', ')'],
14 | ],
15 | autoClosingPairs: [
16 | { open: '{', close: '}' },
17 | { open: '[', close: ']' },
18 | { open: '(', close: ')' },
19 | { open: '"', close: '"' },
20 | { open: "'", close: "'" },
21 | ],
22 | surroundingPairs: [
23 | { open: '{', close: '}' },
24 | { open: '[', close: ']' },
25 | { open: '(', close: ')' },
26 | { open: '"', close: '"' },
27 | { open: "'", close: "'" },
28 | { open: '<', close: '>' },
29 | ],
30 | folding: {},
31 | };
32 |
33 | // const sls_functions = functions
34 |
35 | const slsFunctionsList = Object.keys(functions);
36 | const slsFuncitonsSuggest = slsFunctionsList.map((value) => {
37 | return {
38 | label: value,
39 | type: _.get(functions, value).type,
40 | };
41 | });
42 |
43 | const slsKeywordsList = keywords.map((value) => {
44 | return value.label;
45 | });
46 | const slsoperatorList = operators.map((value) => {
47 | return value.label;
48 | });
49 | const allSuggestions = slsFuncitonsSuggest.concat(keywords).concat(operators);
50 |
51 | export const language = {
52 | ignoreCase: true,
53 | defaultToken: '',
54 | tokenPostfix: '.sls',
55 | vectorMatching: '',
56 |
57 | symbols: /[=>=!%&+\-*/|~^]/, 'operator'],
95 | ],
96 | whitespace: [[/\s+/, 'white']],
97 | comments: [
98 | [/--+.*/, 'comment'],
99 | [/\/\*/, { token: 'comment.quote', next: '@comment' }],
100 | ],
101 | comment: [
102 | [/[^*/]+/, 'comment'],
103 | [/\*\//, { token: 'comment.quote', next: '@pop' }],
104 | [/./, 'comment'],
105 | ],
106 | pseudoColumns: [
107 | [
108 | /[$][A-Za-z_][\w@#$]*/,
109 | {
110 | cases: {
111 | '@pseudoColumns': 'predefined',
112 | '@default': 'identifier',
113 | },
114 | },
115 | ],
116 | ],
117 | numbers: [
118 | [/0[xX][0-9a-fA-F]*/, 'number'],
119 | [/[$][+-]*\d*(\.\d*)?/, 'number'],
120 | [/((\d+(\.\d*)?)|(\.\d+))([eE][\-+]?\d+)?/, 'number'],
121 | ],
122 | strings: [
123 | [/N'/, { token: 'string', next: '@string' }],
124 | [/'/, { token: 'string', next: '@string' }],
125 | ],
126 | string: [
127 | [/[^']+/, 'string'],
128 | [/''/, 'string'],
129 | [/'/, { token: 'string', next: '@pop' }],
130 | ],
131 | complexIdentifiers: [
132 | [/\[/, { token: 'identifier.quote', next: '@bracketedIdentifier' }],
133 | [/"/, { token: 'identifier.quote', next: '@quotedIdentifier' }],
134 | ],
135 | bracketedIdentifier: [
136 | [/[^\]]+/, 'identifier'],
137 | [/]]/, 'identifier'],
138 | [/]/, { token: 'identifier.quote', next: '@pop' }],
139 | ],
140 | quotedIdentifier: [
141 | [/[^"]+/, 'identifier'],
142 | [/""/, 'identifier'],
143 | [/"/, { token: 'identifier.quote', next: '@pop' }],
144 | ],
145 | scopes: [
146 | [/BEGIN\s+(DISTRIBUTED\s+)?TRAN(SACTION)?\b/i, 'keyword'],
147 | [/BEGIN\s+TRY\b/i, { token: 'keyword.try' }],
148 | [/END\s+TRY\b/i, { token: 'keyword.try' }],
149 | [/BEGIN\s+CATCH\b/i, { token: 'keyword.catch' }],
150 | [/END\s+CATCH\b/i, { token: 'keyword.catch' }],
151 | [/(BEGIN|CASE)\b/i, { token: 'keyword.block' }],
152 | [/END\b/i, { token: 'keyword.block' }],
153 | [/WHEN\b/i, { token: 'keyword.choice' }],
154 | [/THEN\b/i, { token: 'keyword.choice' }],
155 | ],
156 | },
157 | } as monacoTypes.languages.IMonarchLanguage;
158 |
159 | export const completionItemProvider: monacoTypes.languages.CompletionItemProvider = {
160 | provideCompletionItems: () => {
161 | const suggestions = allSuggestions.map((value) => {
162 | return {
163 | label: value,
164 | kind: value.type,
165 | insertText: value.label,
166 | detail: value.label,
167 | insertTextRules: 4 as monacoTypes.languages.CompletionItemInsertTextRule.InsertAsSnippet,
168 | } as unknown as monacoTypes.languages.CompletionItem;
169 | });
170 | return { suggestions } as monacoTypes.languages.ProviderResult;
171 | },
172 | };
173 |
--------------------------------------------------------------------------------
/src/SLS-monaco-editor/language-definition/slsterms.ts:
--------------------------------------------------------------------------------
1 | // type
2 | export enum CompletionItemKind {
3 | Method = 0,
4 | Function = 1,
5 | Constructor = 2,
6 | Field = 3,
7 | Variable = 4,
8 | Class = 5,
9 | Struct = 6,
10 | Interface = 7,
11 | Module = 8,
12 | Property = 9,
13 | Event = 10,
14 | Operator = 11,
15 | Unit = 12,
16 | Value = 13,
17 | Constant = 14,
18 | Enum = 15,
19 | EnumMember = 16,
20 | Keyword = 17,
21 | Text = 18,
22 | Color = 19,
23 | File = 20,
24 | Reference = 21,
25 | Customcolor = 22,
26 | Folder = 23,
27 | TypeParameter = 24,
28 | User = 25,
29 | Issue = 26,
30 | Snippet = 27,
31 | }
32 |
33 | export const keywords = [
34 | {
35 | label: 'SELECT',
36 | type: CompletionItemKind.Keyword,
37 | },
38 | {
39 | label: 'FROM',
40 | type: CompletionItemKind.Keyword,
41 | },
42 | {
43 | label: 'AS',
44 | type: CompletionItemKind.Keyword,
45 | },
46 | {
47 | label: 'DISTINCT',
48 | type: CompletionItemKind.Keyword,
49 | },
50 | {
51 | label: 'GROUP',
52 | type: CompletionItemKind.Keyword,
53 | },
54 | {
55 | label: 'WHERE',
56 | type: CompletionItemKind.Keyword,
57 | },
58 | {
59 | label: 'BY',
60 | type: CompletionItemKind.Keyword,
61 | },
62 | {
63 | label: 'HAVING',
64 | type: CompletionItemKind.Keyword,
65 | },
66 | {
67 | label: 'ORDER',
68 | type: CompletionItemKind.Keyword,
69 | },
70 | {
71 | label: 'LIMIT',
72 | type: CompletionItemKind.Keyword,
73 | },
74 | {
75 | label: 'OR',
76 | type: CompletionItemKind.Keyword,
77 | },
78 | {
79 | label: 'AND',
80 | type: CompletionItemKind.Keyword,
81 | },
82 | {
83 | label: 'IN',
84 | type: CompletionItemKind.Keyword,
85 | },
86 | {
87 | label: 'NOT',
88 | type: CompletionItemKind.Keyword,
89 | },
90 | {
91 | label: 'BETWEEN',
92 | type: CompletionItemKind.Keyword,
93 | },
94 | {
95 | label: 'LIKE',
96 | type: CompletionItemKind.Keyword,
97 | },
98 | {
99 | label: 'IS',
100 | type: CompletionItemKind.Keyword,
101 | },
102 | {
103 | label: 'ASC',
104 | type: CompletionItemKind.Keyword,
105 | },
106 | {
107 | label: 'DESC',
108 | type: CompletionItemKind.Keyword,
109 | },
110 | {
111 | label: 'SUBSTRING',
112 | type: CompletionItemKind.Keyword,
113 | },
114 | {
115 | label: 'CASE',
116 | type: CompletionItemKind.Keyword,
117 | },
118 | {
119 | label: 'WHEN',
120 | type: CompletionItemKind.Keyword,
121 | },
122 | {
123 | label: 'THEN',
124 | type: CompletionItemKind.Keyword,
125 | },
126 | {
127 | label: 'ELSE',
128 | type: CompletionItemKind.Keyword,
129 | },
130 | {
131 | label: 'END',
132 | type: CompletionItemKind.Keyword,
133 | },
134 | {
135 | label: 'JOIN',
136 | type: CompletionItemKind.Keyword,
137 | },
138 | {
139 | label: 'INNER',
140 | type: CompletionItemKind.Keyword,
141 | },
142 | {
143 | label: 'LEFT',
144 | type: CompletionItemKind.Keyword,
145 | },
146 | {
147 | label: 'RIGHT',
148 | type: CompletionItemKind.Keyword,
149 | },
150 | {
151 | label: 'OUTER',
152 | type: CompletionItemKind.Keyword,
153 | },
154 | {
155 | label: 'ON',
156 | type: CompletionItemKind.Keyword,
157 | },
158 | {
159 | label: 'UNION',
160 | type: CompletionItemKind.Keyword,
161 | },
162 | ];
163 |
164 | export const operators = [
165 | // Logical
166 | {
167 | label: 'ALL',
168 | type: CompletionItemKind.Operator,
169 | },
170 | {
171 | label: 'AND',
172 | type: CompletionItemKind.Operator,
173 | },
174 | {
175 | label: 'ANY',
176 | type: CompletionItemKind.Operator,
177 | },
178 | {
179 | label: 'BETWEEN',
180 | type: CompletionItemKind.Operator,
181 | },
182 | {
183 | label: 'IN',
184 | type: CompletionItemKind.Operator,
185 | },
186 | {
187 | label: 'LIKE',
188 | type: CompletionItemKind.Operator,
189 | },
190 | {
191 | label: 'NOT',
192 | type: CompletionItemKind.Operator,
193 | },
194 | {
195 | label: 'OR',
196 | type: CompletionItemKind.Operator,
197 | },
198 | {
199 | label: 'SOME',
200 | type: CompletionItemKind.Operator,
201 | },
202 | // Set
203 | {
204 | label: 'EXCEPT',
205 | type: CompletionItemKind.Operator,
206 | },
207 | {
208 | label: 'INTERSECT',
209 | type: CompletionItemKind.Operator,
210 | },
211 | {
212 | label: 'UNION',
213 | type: CompletionItemKind.Operator,
214 | },
215 | // Join
216 | {
217 | label: 'APPLY',
218 | type: CompletionItemKind.Operator,
219 | },
220 | {
221 | label: 'CROSS',
222 | type: CompletionItemKind.Operator,
223 | },
224 | {
225 | label: 'FULL',
226 | type: CompletionItemKind.Operator,
227 | },
228 | {
229 | label: 'INNER',
230 | type: CompletionItemKind.Operator,
231 | },
232 | {
233 | label: 'JOIN',
234 | type: CompletionItemKind.Operator,
235 | },
236 | {
237 | label: 'LEFT',
238 | type: CompletionItemKind.Operator,
239 | },
240 | {
241 | label: 'OUTER',
242 | type: CompletionItemKind.Operator,
243 | },
244 | {
245 | label: 'RIGHT',
246 | type: CompletionItemKind.Operator,
247 | },
248 | // Predicates
249 | {
250 | label: 'CONTAINS',
251 | type: CompletionItemKind.Operator,
252 | },
253 | {
254 | label: 'FREETEXT',
255 | type: CompletionItemKind.Operator,
256 | },
257 | {
258 | label: 'IS',
259 | type: CompletionItemKind.Operator,
260 | },
261 | {
262 | label: 'NULL',
263 | type: CompletionItemKind.Operator,
264 | },
265 | // Pivoting
266 | {
267 | label: 'PIVOT',
268 | type: CompletionItemKind.Operator,
269 | },
270 | {
271 | label: 'UNPIVOT',
272 | type: CompletionItemKind.Operator,
273 | },
274 | // Merging
275 | {
276 | label: 'MATCHED',
277 | type: CompletionItemKind.Operator,
278 | },
279 | ];
280 |
281 | export const functions = {
282 | // 通用聚合函数
283 | arbitrary: {
284 | type: CompletionItemKind.Field,
285 | },
286 | avg: {
287 | type: CompletionItemKind.Field,
288 | },
289 | checksum: {
290 | type: CompletionItemKind.Field,
291 | },
292 | COUNT: {
293 | type: CompletionItemKind.Field,
294 | },
295 | count_if: {
296 | type: CompletionItemKind.Field,
297 | },
298 | geometric_mean: {
299 | type: CompletionItemKind.Field,
300 | },
301 | max_by: {
302 | type: CompletionItemKind.Field,
303 | },
304 | min_by: {
305 | type: CompletionItemKind.Field,
306 | },
307 | max: {
308 | type: CompletionItemKind.Field,
309 | },
310 | min: {
311 | type: CompletionItemKind.Field,
312 | },
313 | sum: {
314 | type: CompletionItemKind.Field,
315 | },
316 | bitwise_and_agg: {
317 | type: CompletionItemKind.Field,
318 | },
319 | bitwise_or_agg: {
320 | type: CompletionItemKind.Field,
321 | },
322 | // 映射函数
323 | histogram: {
324 | type: CompletionItemKind.Method,
325 | },
326 | map_agg: {
327 | type: CompletionItemKind.Method,
328 | },
329 | multimap_agg: {
330 | type: CompletionItemKind.Method,
331 | },
332 | cardinality: {
333 | type: CompletionItemKind.Method,
334 | },
335 | element_at: {
336 | type: CompletionItemKind.Method,
337 | },
338 | map: {
339 | type: CompletionItemKind.Method,
340 | },
341 | map_from_entries: {
342 | type: CompletionItemKind.Method,
343 | },
344 | map_entries: {
345 | type: CompletionItemKind.Method,
346 | },
347 | map_concat: {
348 | type: CompletionItemKind.Method,
349 | },
350 | map_filter: {
351 | type: CompletionItemKind.Method,
352 | },
353 | transform_keys: {
354 | type: CompletionItemKind.Method,
355 | },
356 | transform_values: {
357 | type: CompletionItemKind.Method,
358 | },
359 | map_keys: {
360 | type: CompletionItemKind.Method,
361 | },
362 | map_values: {
363 | type: CompletionItemKind.Method,
364 | },
365 | map_zip_with: {
366 | type: CompletionItemKind.Method,
367 | },
368 | // 估算函数
369 | approx_distinct: {
370 | type: CompletionItemKind.Method,
371 | },
372 | approx_percentile: {
373 | type: CompletionItemKind.Method,
374 | },
375 | numeric_histogram: {
376 | type: CompletionItemKind.Method,
377 | },
378 | // 数学统计函数
379 | corr: {
380 | type: CompletionItemKind.Function,
381 | },
382 | covar_pop: {
383 | type: CompletionItemKind.Function,
384 | },
385 | covar_samp: {
386 | type: CompletionItemKind.Function,
387 | },
388 | regr_intercept: {
389 | type: CompletionItemKind.Function,
390 | },
391 | regr_slope: {
392 | type: CompletionItemKind.Function,
393 | },
394 | stddev: {
395 | type: CompletionItemKind.Function,
396 | },
397 | stddev_samp: {
398 | type: CompletionItemKind.Function,
399 | },
400 | stddev_pop: {
401 | type: CompletionItemKind.Function,
402 | },
403 | variance: {
404 | type: CompletionItemKind.Function,
405 | },
406 | var_samp: {
407 | type: CompletionItemKind.Function,
408 | },
409 | var_pop: {
410 | type: CompletionItemKind.Function,
411 | },
412 | // 数学计算函数
413 | abs: {
414 | type: CompletionItemKind.Function,
415 | },
416 | cbrt: {
417 | type: CompletionItemKind.Function,
418 | },
419 | ceiling: {
420 | type: CompletionItemKind.Function,
421 | },
422 | cosine_similarity: {
423 | type: CompletionItemKind.Function,
424 | },
425 | degrees: {
426 | type: CompletionItemKind.Function,
427 | },
428 | e: {
429 | type: CompletionItemKind.Function,
430 | },
431 | exp: {
432 | type: CompletionItemKind.Function,
433 | },
434 | floor: {
435 | type: CompletionItemKind.Function,
436 | },
437 | from_base: {
438 | type: CompletionItemKind.Function,
439 | },
440 | ln: {
441 | type: CompletionItemKind.Function,
442 | },
443 | log2: {
444 | type: CompletionItemKind.Function,
445 | },
446 | log10: {
447 | type: CompletionItemKind.Function,
448 | },
449 | log: {
450 | type: CompletionItemKind.Function,
451 | },
452 | pi: {
453 | type: CompletionItemKind.Function,
454 | },
455 | pow: {
456 | type: CompletionItemKind.Function,
457 | },
458 | radians: {
459 | type: CompletionItemKind.Function,
460 | },
461 | rand: {
462 | type: CompletionItemKind.Function,
463 | },
464 | random: {
465 | type: CompletionItemKind.Function,
466 | },
467 | round: {
468 | type: CompletionItemKind.Function,
469 | },
470 | sqrt: {
471 | type: CompletionItemKind.Function,
472 | },
473 | to_base: {
474 | type: CompletionItemKind.Function,
475 | },
476 | truncate: {
477 | type: CompletionItemKind.Function,
478 | },
479 | acos: {
480 | type: CompletionItemKind.Function,
481 | },
482 | asin: {
483 | type: CompletionItemKind.Function,
484 | },
485 | atan: {
486 | type: CompletionItemKind.Function,
487 | },
488 | atan2: {
489 | type: CompletionItemKind.Function,
490 | },
491 | cos: {
492 | type: CompletionItemKind.Function,
493 | },
494 | sin: {
495 | type: CompletionItemKind.Function,
496 | },
497 | cosh: {
498 | type: CompletionItemKind.Function,
499 | },
500 | tan: {
501 | type: CompletionItemKind.Function,
502 | },
503 | tanh: {
504 | type: CompletionItemKind.Function,
505 | },
506 | infinity: {
507 | type: CompletionItemKind.Function,
508 | },
509 | is_infinity: {
510 | type: CompletionItemKind.Function,
511 | },
512 | is_finity: {
513 | type: CompletionItemKind.Function,
514 | },
515 | is_nan: {
516 | type: CompletionItemKind.Function,
517 | },
518 | // 字符串函数
519 | chr: {
520 | type: CompletionItemKind.Function,
521 | },
522 | length: {
523 | type: CompletionItemKind.Function,
524 | },
525 | levenshtein_distance: {
526 | type: CompletionItemKind.Function,
527 | },
528 | lpad: {
529 | type: CompletionItemKind.Function,
530 | },
531 | rpad: {
532 | type: CompletionItemKind.Function,
533 | },
534 | ltrim: {
535 | type: CompletionItemKind.Function,
536 | },
537 | replace: {
538 | type: CompletionItemKind.Function,
539 | },
540 | reverse: {
541 | type: CompletionItemKind.Function,
542 | },
543 | rtrim: {
544 | type: CompletionItemKind.Function,
545 | },
546 | split: {
547 | type: CompletionItemKind.Function,
548 | },
549 | split_part: {
550 | type: CompletionItemKind.Function,
551 | },
552 | split_to_map: {
553 | type: CompletionItemKind.Function,
554 | },
555 | position: {
556 | type: CompletionItemKind.Function,
557 | },
558 | strpos: {
559 | type: CompletionItemKind.Function,
560 | },
561 | substr: {
562 | type: CompletionItemKind.Function,
563 | },
564 | trim: {
565 | type: CompletionItemKind.Function,
566 | },
567 | upper: {
568 | type: CompletionItemKind.Function,
569 | },
570 | concat: {
571 | type: CompletionItemKind.Function,
572 | },
573 | hamming_distance: {
574 | type: CompletionItemKind.Function,
575 | },
576 | // 日期函数
577 | current_date: {
578 | type: CompletionItemKind.Function,
579 | },
580 | current_time: {
581 | type: CompletionItemKind.Function,
582 | },
583 | current_timestamp: {
584 | type: CompletionItemKind.Function,
585 | },
586 | current_timezone: {
587 | type: CompletionItemKind.Function,
588 | },
589 | from_iso8601_timestamp: {
590 | type: CompletionItemKind.Function,
591 | },
592 | from_iso8601_date: {
593 | type: CompletionItemKind.Function,
594 | },
595 | from_unixtime: {
596 | type: CompletionItemKind.Function,
597 | },
598 | localtime: {
599 | type: CompletionItemKind.Function,
600 | },
601 | localtimestamp: {
602 | type: CompletionItemKind.Function,
603 | },
604 | now: {
605 | type: CompletionItemKind.Function,
606 | },
607 | to_unixtime: {
608 | type: CompletionItemKind.Function,
609 | },
610 | date_trunc: {
611 | type: CompletionItemKind.Function,
612 | },
613 | date_format: {
614 | type: CompletionItemKind.Function,
615 | },
616 | date_parse: {
617 | type: CompletionItemKind.Function,
618 | },
619 | date_add: {
620 | type: CompletionItemKind.Function,
621 | },
622 | date_diff: {
623 | type: CompletionItemKind.Function,
624 | },
625 | time_series: {
626 | type: CompletionItemKind.Function,
627 | },
628 | // URL函数
629 | url_extract_fragment: {
630 | type: CompletionItemKind.Function,
631 | },
632 | url_extract_host: {
633 | type: CompletionItemKind.Function,
634 | },
635 | url_extract_parameter: {
636 | type: CompletionItemKind.Function,
637 | },
638 | url_extract_path: {
639 | type: CompletionItemKind.Function,
640 | },
641 | url_extract_port: {
642 | type: CompletionItemKind.Function,
643 | },
644 | url_extract_protocol: {
645 | type: CompletionItemKind.Function,
646 | },
647 | url_extract_query: {
648 | type: CompletionItemKind.Function,
649 | },
650 | url_encode: {
651 | type: CompletionItemKind.Function,
652 | },
653 | url_decode: {
654 | type: CompletionItemKind.Function,
655 | },
656 | // 正则式函数
657 | regexp_extract_all: {
658 | type: CompletionItemKind.Function,
659 | },
660 | regexp_extract: {
661 | type: CompletionItemKind.Function,
662 | },
663 | regexp_like: {
664 | type: CompletionItemKind.Function,
665 | },
666 | regexp_replace: {
667 | type: CompletionItemKind.Function,
668 | },
669 | regexp_split: {
670 | type: CompletionItemKind.Function,
671 | },
672 | // JSON函数
673 | json_parse: {
674 | type: CompletionItemKind.Function,
675 | },
676 | json_format: {
677 | type: CompletionItemKind.Function,
678 | },
679 | json_array_contains: {
680 | type: CompletionItemKind.Function,
681 | },
682 | json_array_get: {
683 | type: CompletionItemKind.Function,
684 | },
685 | json_array_length: {
686 | type: CompletionItemKind.Function,
687 | },
688 | json_extract: {
689 | type: CompletionItemKind.Function,
690 | },
691 | json_extract_scalar: {
692 | type: CompletionItemKind.Function,
693 | },
694 | json_size: {
695 | type: CompletionItemKind.Function,
696 | },
697 | // 类型转换函数
698 | cast: {
699 | type: CompletionItemKind.Method,
700 | },
701 | try_cast: {
702 | type: CompletionItemKind.Method,
703 | },
704 | // IP地理函数
705 | ip_to_domain: {
706 | type: CompletionItemKind.Function,
707 | },
708 | ip_to_country: {
709 | type: CompletionItemKind.Function,
710 | },
711 | ip_to_province: {
712 | type: CompletionItemKind.Function,
713 | },
714 | ip_to_city: {
715 | type: CompletionItemKind.Function,
716 | },
717 | ip_to_geo: {
718 | type: CompletionItemKind.Function,
719 | },
720 | ip_to_city_geo: {
721 | type: CompletionItemKind.Function,
722 | },
723 | ip_to_provider: {
724 | type: CompletionItemKind.Function,
725 | },
726 | ip_to_country_code: {
727 | type: CompletionItemKind.Function,
728 | },
729 | // 同比和环比函数
730 | compare: {
731 | type: CompletionItemKind.Function,
732 | },
733 | // 机器学习函数
734 | ts_smooth_simple: {
735 | type: CompletionItemKind.Interface,
736 | },
737 | ts_smooth_fir: {
738 | type: CompletionItemKind.Interface,
739 | },
740 | ts_smooth_iir: {
741 | type: CompletionItemKind.Interface,
742 | },
743 | ts_period_detect: {
744 | type: CompletionItemKind.Interface,
745 | },
746 | ts_cp_detect: {
747 | type: CompletionItemKind.Interface,
748 | },
749 | ts_breakout_detect: {
750 | type: CompletionItemKind.Interface,
751 | },
752 | ts_predicate_simple: {
753 | type: CompletionItemKind.Interface,
754 | },
755 | ts_predicate_ar: {
756 | type: CompletionItemKind.Interface,
757 | },
758 | ts_predicate_arma: {
759 | type: CompletionItemKind.Interface,
760 | },
761 | ts_predicate_arima: {
762 | type: CompletionItemKind.Interface,
763 | },
764 | ts_regression_predict: {
765 | type: CompletionItemKind.Interface,
766 | },
767 | ts_decompose: {
768 | type: CompletionItemKind.Interface,
769 | },
770 | ts_density_cluster: {
771 | type: CompletionItemKind.Interface,
772 | },
773 | ts_hierarchical_cluster: {
774 | type: CompletionItemKind.Interface,
775 | },
776 | ts_similar_instance: {
777 | type: CompletionItemKind.Interface,
778 | },
779 | pattern_stat: {
780 | type: CompletionItemKind.Interface,
781 | },
782 | pattern_diff: {
783 | type: CompletionItemKind.Interface,
784 | },
785 | ts_find_peaks: {
786 | type: CompletionItemKind.Interface,
787 | },
788 | // PromQL函数
789 | promql_query: {
790 | type: CompletionItemKind.Struct,
791 | },
792 | promql_query_range: {
793 | type: CompletionItemKind.Struct,
794 | },
795 | promql_labels: {
796 | type: CompletionItemKind.Struct,
797 | },
798 | promql_label_values: {
799 | type: CompletionItemKind.Struct,
800 | },
801 | promql_series: {
802 | type: CompletionItemKind.Struct,
803 | },
804 | };
805 |
--------------------------------------------------------------------------------
/src/SelectTips.tsx:
--------------------------------------------------------------------------------
1 | import { Card, SeriesTable, SeriesTableRowProps } from '@grafana/ui';
2 | import { version, xColInfoSeries, yColInfoSeries, totalLogsInfoSeries } from 'const';
3 | import React from 'react';
4 |
5 | export function SelectTips(props: { type: string }) {
6 | const isOld =
7 | version === '' ||
8 | version.startsWith('8.0') ||
9 | version.startsWith('8.1') ||
10 | version.startsWith('8.2') ||
11 | version.startsWith('8.3') ||
12 | version.startsWith('7');
13 | let series = [] as SeriesTableRowProps[]
14 | switch(props.type) {
15 | case 'ycol':
16 | series = yColInfoSeries
17 | break
18 | case 'xcol':
19 | series = xColInfoSeries
20 | break
21 | case 'totalLogs':
22 | series = totalLogsInfoSeries
23 | break
24 | default:
25 | break
26 | }
27 | return isOld ? (
28 |
29 | {series.map((v, i) => {
30 | return (
31 |
32 | {`${i + 1}.`}
33 | {v.label}
34 | {v.value}
35 |
36 | );
37 | })}
38 |
39 | ) : (
40 |
41 | {`${props.type} 简介 Introduction`}
42 |
43 |
44 |
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/src/VariableQueryEditor.tsx:
--------------------------------------------------------------------------------
1 | import React, { ChangeEvent, PureComponent } from 'react';
2 | import { SLSDataSourceOptions, SLSQuery } from './types';
3 |
4 | import { InlineFormLabel } from '@grafana/ui';
5 | import { QueryEditorProps } from '@grafana/data';
6 | import { SLSDataSource } from './datasource';
7 | import MonacoQueryField from 'SLS-monaco-editor/MonacoQueryField';
8 | import MonacoQueryFieldOld from 'SLS-monaco-editor/MonacoQueryFieldOld';
9 | import { version } from 'const';
10 | import { SLSQueryEditor } from 'QueryEditor';
11 | // const { FormField } = LegacyForms;
12 |
13 | type Props = QueryEditorProps;
14 |
15 | export class SLSVariableQueryEditor extends PureComponent {
16 | onQueryTextChange = (event: ChangeEvent) => {
17 | const { onChange, query } = this.props;
18 | onChange({ ...query, query: event.target.value });
19 | };
20 |
21 | onQueryTextChangeString = (value: string) => {
22 | const { onChange, query } = this.props;
23 | onChange({ ...query, query: value });
24 | };
25 |
26 | onQueryTextChangeWithRunQuery = (value: string) => {
27 | const { onChange, query, onRunQuery } = this.props;
28 | onChange({ ...query, query: value });
29 | // executes the query
30 | onRunQuery();
31 | };
32 |
33 | render() {
34 | const { query } = this.props.query;
35 |
36 | return (
37 | <>
38 |
39 | {/*
*/}
47 |
48 | Query
49 |
50 | {version === '' ||
51 | version.startsWith('8.0') ||
52 | version.startsWith('8.1') ||
53 | version.startsWith('8.2') ||
54 | version.startsWith('7') ? (
55 | //
62 |
63 |
73 |
74 | ) : (
75 |
84 | )}
85 |
86 | >
87 | );
88 | }
89 | }
90 |
91 |
92 |
93 |
94 | export function SLSVariableQueryEditorWapper (props: Props) {
95 | return
96 | }
97 |
--------------------------------------------------------------------------------
/src/components/Collapse.tsx:
--------------------------------------------------------------------------------
1 | import { css, cx } from '@emotion/css';
2 | import React, { Component } from 'react';
3 |
4 | import { GrafanaTheme2 } from '@grafana/data';
5 |
6 | import { Icon, withTheme2 } from '@grafana/ui';
7 |
8 | const getStyles = (theme: GrafanaTheme2) => ({
9 | collapse: css`
10 | label: collapse;
11 | margin-bottom: ${theme.spacing(1)};
12 | `,
13 | collapseBody: css`
14 | label: collapse__body;
15 | padding: ${theme.spacing(theme.components.panel.padding)};
16 | padding-top: 0;
17 | flex: 1;
18 | overflow: hidden;
19 | display: flex;
20 | flex-direction: column;
21 | `,
22 | bodyContentWrapper: css`
23 | label: bodyContentWrapper;
24 | flex: 1;
25 | overflow: hidden;
26 | `,
27 | loader: css`
28 | label: collapse__loader;
29 | height: 2px;
30 | position: relative;
31 | overflow: hidden;
32 | background: none;
33 | margin: ${theme.spacing(0.5)};
34 | `,
35 | loaderActive: css`
36 | label: collapse__loader_active;
37 | &:after {
38 | content: ' ';
39 | display: block;
40 | width: 25%;
41 | top: 0;
42 | top: -50%;
43 | height: 250%;
44 | position: absolute;
45 | animation: loader 2s cubic-bezier(0.17, 0.67, 0.83, 0.67) 500ms;
46 | animation-iteration-count: 100;
47 | left: -25%;
48 | background: ${theme.colors.primary.main};
49 | }
50 | @keyframes loader {
51 | from {
52 | left: -25%;
53 | opacity: 0.1;
54 | }
55 | to {
56 | left: 100%;
57 | opacity: 1;
58 | }
59 | }
60 | `,
61 | header: css`
62 | label: collapse__header;
63 | padding: ${theme.spacing(1, 2, 1, 2)};
64 | display: flex;
65 | cursor: inherit;
66 | transition: all 0.1s linear;
67 | cursor: pointer;
68 | `,
69 | headerCollapsed: css`
70 | label: collapse__header--collapsed;
71 | padding: ${theme.spacing(1, 2, 1, 2)};
72 | `,
73 | headerLabel: css`
74 | label: collapse__header-label;
75 | font-weight: ${theme.typography.fontWeightMedium};
76 | margin-right: ${theme.spacing(1)};
77 | font-size: ${theme.typography.size.md};
78 | `,
79 | icon: css`
80 | label: collapse__icon;
81 | margin: ${theme.spacing(0, 1, 0, -1)};
82 | `,
83 | });
84 |
85 | export interface Props {
86 | /** Expand or collapse the content */
87 | isOpen?: boolean;
88 | /** Element or text for the Collapse header */
89 | label: React.ReactNode;
90 | /** Indicates loading state of the content */
91 | loading?: boolean;
92 | /** Toggle collapsed header icon */
93 | collapsible?: boolean;
94 | /** Callback for the toggle functionality */
95 | onToggle?: (isOpen: boolean) => void;
96 | /** Additional class name for the root element */
97 | className?: string;
98 |
99 | theme: GrafanaTheme2;
100 | }
101 |
102 | class Collapse extends Component {
103 | static defaultProps = {
104 | isOpen: false,
105 | collapsible: true,
106 | loading: false,
107 | };
108 |
109 | onClickToggle = () => {
110 | const { onToggle, isOpen } = this.props;
111 | if (onToggle) {
112 | onToggle(!isOpen);
113 | }
114 | };
115 |
116 | render() {
117 | const { isOpen, label, loading, collapsible, className, children, theme } = this.props;
118 | const styles = getStyles(theme);
119 |
120 | const panelClass = cx([styles.collapse, 'panel-container', className]);
121 | const loaderClass = loading ? cx([styles.loader, styles.loaderActive]) : cx([styles.loader]);
122 | const headerClass = collapsible ? cx([styles.header]) : cx([styles.headerCollapsed]);
123 |
124 | return (
125 |
126 |
127 | {collapsible &&
}
128 |
{label}
129 |
130 | {isOpen && (
131 |
132 |
133 |
{children}
134 |
135 | )}
136 |
137 | );
138 | }
139 | }
140 |
141 | const CollapseWithTheme = withTheme2(Collapse);
142 |
143 |
144 | export { CollapseWithTheme as Collapse };
145 |
146 |
--------------------------------------------------------------------------------
/src/components/IconButton.tsx:
--------------------------------------------------------------------------------
1 | import { css, cx } from '@emotion/css';
2 | import React, { Component } from 'react';
3 |
4 | import { GrafanaTheme2, colorManipulator } from '@grafana/data';
5 | import { getFocusStyles, getMouseFocusStyles } from './RadioButtonGroup/RadioButton';
6 | import { Icon, IconName, IconSize, IconType, PopoverContent, Tooltip, withTheme2 } from '@grafana/ui';
7 |
8 | export type IconButtonVariant = 'primary' | 'secondary' | 'destructive';
9 |
10 | export interface Props extends React.ButtonHTMLAttributes {
11 | /** Name of the icon **/
12 | name: IconName;
13 | /** Icon size */
14 | size?: IconSize;
15 | /** Type of the icon - mono or default */
16 | iconType?: IconType;
17 | /** Tooltip content to display on hover */
18 | tooltip?: PopoverContent;
19 | /** Position of the tooltip */
20 | tooltipPlacement?: TooltipPlacement;
21 | /** Variant to change the color of the Icon */
22 | variant?: IconButtonVariant;
23 | /** Text available only for screen readers. Will use tooltip text as fallback. */
24 | ariaLabel?: string;
25 | /** Theme object injected by withTheme2 HOC */
26 | theme: GrafanaTheme2;
27 | innerRef?: React.Ref;
28 | }
29 |
30 | class IconButton_ extends Component {
31 | static defaultProps = {
32 | size: 'md' as any,
33 | variant: 'secondary' as any,
34 | };
35 |
36 | getStyles = () => {
37 | const { theme, size, variant } = this.props;
38 | const pixelSize = getSvgSize(size || 'md');
39 | const hoverSize = Math.max(pixelSize / 3, 8);
40 | let iconColor = theme.colors.text.primary;
41 |
42 | if (variant === 'primary') {
43 | iconColor = theme.colors.primary.text;
44 | } else if (variant === 'destructive') {
45 | iconColor = theme.colors.error.text;
46 | }
47 |
48 | return {
49 | button: css`
50 | width: ${pixelSize}px;
51 | height: ${pixelSize}px;
52 | background: transparent;
53 | border: none;
54 | color: ${iconColor};
55 | padding: 0;
56 | margin: 0;
57 | outline: none;
58 | box-shadow: none;
59 | display: inline-flex;
60 | align-items: center;
61 | justify-content: center;
62 | position: relative;
63 | border-radius: ${theme.shape.borderRadius()};
64 | z-index: 0;
65 | margin-right: ${theme.spacing(0.5)};
66 |
67 | &[disabled],
68 | &:disabled {
69 | cursor: not-allowed;
70 | color: ${theme.colors.action.disabledText};
71 | opacity: 0.65;
72 | box-shadow: none;
73 | }
74 |
75 | &:before {
76 | content: '';
77 | display: block;
78 | opacity: 1;
79 | position: absolute;
80 | transition-duration: 0.2s;
81 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
82 | z-index: -1;
83 | bottom: -${hoverSize}px;
84 | left: -${hoverSize}px;
85 | right: -${hoverSize}px;
86 | top: -${hoverSize}px;
87 | background: none;
88 | border-radius: 50%;
89 | box-sizing: border-box;
90 | transform: scale(0);
91 | transition-property: transform, opacity;
92 | }
93 |
94 | &:focus,
95 | &:focus-visible {
96 | ${getFocusStyles(theme)}
97 | }
98 |
99 | &:focus:not(:focus-visible) {
100 | ${getMouseFocusStyles(theme)}
101 | }
102 |
103 | &:hover {
104 | color: ${iconColor};
105 |
106 | &:before {
107 | background-color: ${variant === 'secondary'
108 | ? theme.colors.action.hover
109 | : colorManipulator.alpha(iconColor, 0.12)};
110 | border: none;
111 | box-shadow: none;
112 | opacity: 1;
113 | transform: scale(0.8);
114 | }
115 | }
116 | `,
117 | icon: css`
118 | vertical-align: baseline;
119 | display: flex;
120 | `,
121 | };
122 | };
123 |
124 | render() {
125 | const {
126 | name,
127 | size = 'md',
128 | iconType,
129 | tooltip,
130 | tooltipPlacement,
131 | ariaLabel,
132 | className,
133 | variant = 'secondary',
134 | theme,
135 | ...restProps
136 | } = this.props;
137 |
138 | const styles = this.getStyles();
139 | const tooltipString = typeof tooltip === 'string' ? tooltip : '';
140 |
141 | const button = (
142 |
143 |
144 |
145 | );
146 |
147 | if (tooltip) {
148 | return (
149 |
150 | {button}
151 |
152 | );
153 | }
154 |
155 | return button;
156 | }
157 | }
158 |
159 |
160 |
161 | export const IconButton = withTheme2(IconButton_);
162 |
163 | /* Transform string with px to number and add 2 pxs as path in svg is 2px smaller */
164 | export function getSvgSize(size: IconSize) {
165 | switch (size) {
166 | case 'xs':
167 | return 12;
168 | case 'sm':
169 | return 14;
170 | case 'md':
171 | return 16;
172 | case 'lg':
173 | return 18;
174 | case 'xl':
175 | return 24;
176 | case 'xxl':
177 | return 36;
178 | case 'xxxl':
179 | return 48;
180 | }
181 | }
182 |
183 | export type TooltipPlacement =
184 | | 'auto-start'
185 | | 'auto'
186 | | 'auto-end'
187 | | 'top-start'
188 | | 'top'
189 | | 'top-end'
190 | | 'right-start'
191 | | 'right'
192 | | 'right-end'
193 | | 'bottom-end'
194 | | 'bottom'
195 | | 'bottom-start'
196 | | 'left-end'
197 | | 'left'
198 | | 'left-start';
199 |
--------------------------------------------------------------------------------
/src/components/QueryEditor/EditorField.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/css';
2 | import React, { Component, ComponentProps } from 'react';
3 | import { GrafanaTheme2 } from '@grafana/data';
4 | import Space from './Space';
5 | import { Icon, PopoverContent, ReactUtils, Tooltip, withTheme2 } from '@grafana/ui';
6 | import Field from './Field'
7 |
8 | interface EditorFieldProps extends ComponentProps {
9 | label: string;
10 | children: React.ReactElement;
11 | width?: number | string;
12 | optional?: boolean;
13 | tooltip?: PopoverContent;
14 | tooltipInteractive?: boolean;
15 | theme: GrafanaTheme2; // Add theme to props for withTheme2 HOC
16 | }
17 |
18 | class EditorField extends Component {
19 | getStyles = () => {
20 | const { theme, width } = this.props;
21 | return {
22 | root: css({
23 | minWidth: theme.spacing(width ?? 0),
24 | }),
25 | label: css({
26 | fontSize: 12,
27 | fontWeight: theme.typography.fontWeightMedium,
28 | }),
29 | optional: css({
30 | fontStyle: 'italic',
31 | color: theme.colors.text.secondary,
32 | }),
33 | field: css({
34 | marginBottom: 0, // GrafanaUI/Field has a bottom margin which we must remove
35 | }),
36 | icon: css({
37 | color: theme.colors.text.secondary,
38 | marginLeft: theme.spacing(1),
39 | ':hover': {
40 | color: theme.colors.text.primary,
41 | },
42 | }),
43 | };
44 | };
45 |
46 | render() {
47 | const { label, optional, tooltip, tooltipInteractive, children, width, theme, ...fieldProps } = this.props;
48 |
49 | const styles = this.getStyles();
50 |
51 | // Null check for backward compatibility
52 | const childInputId = fieldProps?.htmlFor || ReactUtils?.getChildId?.(children);
53 |
54 | const labelEl = (
55 | <>
56 |
57 | {label}
58 | {optional && - optional }
59 | {tooltip && (
60 |
61 |
62 |
63 | )}
64 |
65 |
66 | >
67 | );
68 |
69 | return (
70 |
71 |
72 | {children}
73 |
74 |
75 | );
76 | }
77 | }
78 |
79 | export default withTheme2(EditorField);
80 |
--------------------------------------------------------------------------------
/src/components/QueryEditor/EditorRow.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/css';
2 | import React, { Component } from 'react';
3 | import { withTheme2 } from '@grafana/ui'; // Assuming you export GrafanaTheme2 and withTheme2 from @grafana/ui
4 | import { GrafanaTheme2 } from '@grafana/data';
5 |
6 | import Stack from './Stack';
7 |
8 | interface EditorRowProps {
9 | theme: GrafanaTheme2; // Add theme to props for withTheme2 HOC
10 | }
11 |
12 | interface EditorRowState {}
13 |
14 | class EditorRow extends Component, EditorRowState> {
15 | getStyles = () => {
16 | const { theme } = this.props;
17 |
18 | return {
19 | root: css({
20 | backgroundColor: theme.colors.background.secondary,
21 | borderRadius: theme.shape.borderRadius(1),
22 | }),
23 | };
24 | };
25 |
26 | render() {
27 | const { children } = this.props;
28 | const styles = this.getStyles();
29 |
30 | return (
31 |
32 | {children}
33 |
34 | );
35 | }
36 | }
37 |
38 | export default withTheme2(EditorRow);
39 |
--------------------------------------------------------------------------------
/src/components/QueryEditor/Field.tsx:
--------------------------------------------------------------------------------
1 | import { css, cx } from '@emotion/css';
2 | import React, { Component, HTMLAttributes } from 'react';
3 | import { withTheme2, ReactUtils } from '@grafana/ui'; // Assuming you export GrafanaTheme2 and withTheme2 from @grafana/ui
4 | import { GrafanaTheme2 } from '@grafana/data';
5 |
6 | import FieldValidationMessage from './FieldValidationMessage';
7 | import Label from './Label';
8 |
9 | export interface FieldProps extends HTMLAttributes {
10 | /** Form input element, i.e Input or Switch */
11 | children: React.ReactElement;
12 | /** Label for the field */
13 | label?: React.ReactNode;
14 | /** Description of the field */
15 | description?: React.ReactNode;
16 | /** Indicates if field is in invalid state */
17 | invalid?: boolean;
18 | /** Indicates if field is in loading state */
19 | loading?: boolean;
20 | /** Indicates if field is disabled */
21 | disabled?: boolean;
22 | /** Indicates if field is required */
23 | required?: boolean;
24 | /** Error message to display */
25 | error?: React.ReactNode;
26 | /** Indicates horizontal layout of the field */
27 | horizontal?: boolean;
28 | /** make validation message overflow horizontally. Prevents pushing out adjacent inline components */
29 | validationMessageHorizontalOverflow?: boolean;
30 |
31 | className?: string;
32 | /**
33 | * A unique id that associates the label of the Field component with the control with the unique id.
34 | * If the `htmlFor` property is missing the `htmlFor` will be inferred from the `id` or `inputId` property of the first child.
35 | * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label#attr-for
36 | */
37 | htmlFor?: string;
38 | theme: GrafanaTheme2; // Add theme to props for withTheme2 HOC
39 | }
40 |
41 | class Field extends Component {
42 | static defaultProps = {
43 | validationMessageHorizontalOverflow: false,
44 | };
45 |
46 | getStyles = () => {
47 | const { theme } = this.props;
48 | return {
49 | field: css`
50 | display: flex;
51 | flex-direction: column;
52 | margin-bottom: ${theme.spacing(2)};
53 | `,
54 | fieldHorizontal: css`
55 | flex-direction: row;
56 | justify-content: space-between;
57 | flex-wrap: wrap;
58 | `,
59 | fieldValidationWrapper: css`
60 | margin-top: ${theme.spacing(0.5)};
61 | `,
62 | fieldValidationWrapperHorizontal: css`
63 | flex: 1 1 100%;
64 | `,
65 | validationMessageHorizontalOverflow: css`
66 | width: 0;
67 | overflow-x: visible;
68 |
69 | & > * {
70 | white-space: nowrap;
71 | }
72 | `,
73 | };
74 | };
75 |
76 | deleteUndefinedProps = (obj: T): Partial => {
77 | for (const key in obj) {
78 | if (obj[key] === undefined) {
79 | delete obj[key];
80 | }
81 | }
82 | return obj;
83 | };
84 |
85 | render() {
86 | const {
87 | label,
88 | description,
89 | horizontal,
90 | invalid,
91 | loading,
92 | disabled,
93 | required,
94 | error,
95 | children,
96 | className,
97 | validationMessageHorizontalOverflow,
98 | htmlFor,
99 | theme,
100 | ...otherProps
101 | } = this.props;
102 |
103 | const styles = this.getStyles();
104 | const inputId = htmlFor ?? ReactUtils?.getChildId?.(children);
105 |
106 | const labelElement =
107 | typeof label === 'string' ? (
108 |
109 | {`${label}${required ? ' *' : ''}`}
110 |
111 | ) : (
112 | label
113 | );
114 |
115 | const childProps = this.deleteUndefinedProps({ invalid, disabled, loading });
116 |
117 | return (
118 |
119 | {labelElement}
120 |
121 | {React.cloneElement(children, childProps)}
122 | {invalid && error && !horizontal && (
123 |
128 | {error}
129 |
130 | )}
131 |
132 |
133 | {invalid && error && horizontal && (
134 |
139 | {error}
140 |
141 | )}
142 |
143 | );
144 | }
145 | }
146 |
147 | export default withTheme2(Field);
148 |
--------------------------------------------------------------------------------
/src/components/QueryEditor/FieldValidationMessage.tsx:
--------------------------------------------------------------------------------
1 | import { css, cx } from '@emotion/css';
2 | import React, { Component } from 'react';
3 | import { GrafanaTheme2 } from '@grafana/data';
4 |
5 | import { stylesFactory, Icon, withTheme2 } from '@grafana/ui';
6 |
7 | export interface FieldValidationMessageProps {
8 | /** Override component style */
9 | className?: string;
10 | horizontal?: boolean;
11 | theme: GrafanaTheme2; // Add theme to props for withTheme2 HOC
12 | }
13 |
14 | const getFieldValidationMessageStyles = stylesFactory((theme: GrafanaTheme2) => {
15 | const baseStyle = `
16 | font-size: ${theme.typography.size.sm};
17 | font-weight: ${theme.typography.fontWeightMedium};
18 | padding: ${theme.spacing(0.5, 1)};
19 | color: ${theme.colors.error.contrastText};
20 | background: ${theme.colors.error.main};
21 | border-radius: ${theme.shape.borderRadius()};
22 | position: relative;
23 | display: inline-block;
24 |
25 | a {
26 | color: ${theme.colors.error.contrastText};
27 | text-decoration: underline;
28 | }
29 |
30 | a:hover {
31 | text-decoration: none;
32 | }
33 | `;
34 |
35 | return {
36 | vertical: css`
37 | ${baseStyle}
38 | margin: ${theme.spacing(0.5, 0, 0, 0)};
39 |
40 | &:before {
41 | content: '';
42 | position: absolute;
43 | left: 9px;
44 | top: -5px;
45 | width: 0;
46 | height: 0;
47 | border-width: 0 4px 5px 4px;
48 | border-color: transparent transparent ${theme.colors.error.main} transparent;
49 | border-style: solid;
50 | }
51 | `,
52 | horizontal: css`
53 | ${baseStyle}
54 | margin-left: 10px;
55 |
56 | &:before {
57 | content: '';
58 | position: absolute;
59 | left: -5px;
60 | top: 9px;
61 | width: 0;
62 | height: 0;
63 | border-width: 4px 5px 4px 0;
64 | border-color: transparent #e02f44 transparent transparent;
65 | border-style: solid;
66 | }
67 | `,
68 | fieldValidationMessageIcon: css`
69 | margin-right: ${theme.spacing()};
70 | `,
71 | };
72 | });
73 |
74 | class FieldValidationMessage extends Component {
75 | render() {
76 | const { children, horizontal, className, theme } = this.props;
77 | const styles = getFieldValidationMessageStyles(theme);
78 | const cssName = cx(horizontal ? styles.horizontal : styles.vertical, className);
79 |
80 | return (
81 |
82 |
83 | {children}
84 |
85 | );
86 | }
87 | }
88 |
89 | export default withTheme2(FieldValidationMessage);
90 |
--------------------------------------------------------------------------------
/src/components/QueryEditor/InlineField.tsx:
--------------------------------------------------------------------------------
1 | import { cx, css } from '@emotion/css';
2 | import React, { Component, HTMLAttributes } from 'react';
3 |
4 | import { withTheme2, ReactUtils, PopoverContent, InlineLabel } from '@grafana/ui'; // Assuming you export withTheme2 from @grafana/ui
5 | import { GrafanaTheme2 } from '@grafana/data';
6 | import FieldValidationMessage from './FieldValidationMessage';
7 |
8 | export interface FieldProps extends HTMLAttributes {
9 | /** Form input element, i.e Input or Switch */
10 | children: React.ReactElement;
11 | /** Label for the field */
12 | label?: React.ReactNode;
13 | /** Description of the field */
14 | description?: React.ReactNode;
15 | /** Indicates if field is in invalid state */
16 | invalid?: boolean;
17 | /** Indicates if field is in loading state */
18 | loading?: boolean;
19 | /** Indicates if field is disabled */
20 | disabled?: boolean;
21 | /** Indicates if field is required */
22 | required?: boolean;
23 | /** Error message to display */
24 | error?: React.ReactNode;
25 | /** Indicates horizontal layout of the field */
26 | horizontal?: boolean;
27 | /** make validation message overflow horizontally. Prevents pushing out adjacent inline components */
28 | validationMessageHorizontalOverflow?: boolean;
29 |
30 | className?: string;
31 | /**
32 | * A unique id that associates the label of the Field component with the control with the unique id.
33 | * If the `htmlFor` property is missing the `htmlFor` will be inferred from the `id` or `inputId` property of the first child.
34 | * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label#attr-for
35 | */
36 | htmlFor?: string;
37 | }
38 |
39 | export interface Props extends Omit {
40 | /** Content for the label's tooltip */
41 | tooltip?: PopoverContent;
42 | /** Custom width for the label as a multiple of 8px */
43 | labelWidth?: number | 'auto';
44 | /** Make the field's child to fill the width of the row. Equivalent to setting `flex-grow:1` on the field */
45 | grow?: boolean;
46 | /** Make the field's child shrink with width of the row. Equivalent to setting `flex-shrink:1` on the field */
47 | shrink?: boolean;
48 | /** Make field's background transparent */
49 | transparent?: boolean;
50 | /** Error message to display */
51 | error?: string | null;
52 | htmlFor?: string;
53 | /** Make tooltip interactive */
54 | interactive?: boolean;
55 | theme: GrafanaTheme2; // Add theme to props for withTheme2 HOC
56 | }
57 |
58 | class InlineField extends Component {
59 | static defaultProps = {
60 | labelWidth: 'auto' as any,
61 | };
62 |
63 | getStyles = () => {
64 | const { theme, grow, shrink } = this.props;
65 | return {
66 | container: css`
67 | display: flex;
68 | flex-direction: row;
69 | align-items: flex-start;
70 | text-align: left;
71 | position: relative;
72 | flex: ${grow ? 1 : 0} ${shrink ? 1 : 0} auto;
73 | margin: 0 ${theme.spacing(0.5)} ${theme.spacing(0.5)} 0;
74 | `,
75 | childContainer: css`
76 | flex: ${grow ? 1 : 0} ${shrink ? 1 : 0} auto;
77 | `,
78 | fieldValidationWrapper: css`
79 | margin-top: ${theme.spacing(0.5)};
80 | `,
81 | };
82 | };
83 |
84 | render() {
85 | const {
86 | children,
87 | label,
88 | tooltip,
89 | labelWidth,
90 | invalid,
91 | loading,
92 | disabled,
93 | className,
94 | htmlFor,
95 | grow,
96 | shrink,
97 | error,
98 | transparent,
99 | interactive,
100 | ...htmlProps
101 | } = this.props;
102 |
103 | const styles = this.getStyles();
104 | const inputId = htmlFor ?? ReactUtils?.getChildId?.(children);
105 |
106 | const labelElement =
107 | typeof label === 'string' ? (
108 |
115 | {label}
116 |
117 | ) : (
118 | label
119 | );
120 |
121 | return (
122 |
123 | {labelElement}
124 |
125 | {React.cloneElement(children, { invalid, disabled, loading })}
126 | {invalid && error && (
127 |
128 | {error}
129 |
130 | )}
131 |
132 |
133 | );
134 | }
135 | }
136 |
137 | export default withTheme2(InlineField);
138 |
--------------------------------------------------------------------------------
/src/components/QueryEditor/Label.tsx:
--------------------------------------------------------------------------------
1 | import { css, cx } from '@emotion/css';
2 | import React, { Component } from 'react';
3 | import { withTheme2, Icon } from '@grafana/ui'; // Assuming you export GrafanaTheme2 and withTheme2 from @grafana/ui
4 | import { GrafanaTheme2 } from '@grafana/data';
5 |
6 | export interface LabelProps extends React.LabelHTMLAttributes {
7 | children: React.ReactNode;
8 | description?: React.ReactNode;
9 | category?: React.ReactNode[];
10 | theme: GrafanaTheme2; // Add theme to props for withTheme2 HOC
11 | }
12 |
13 | const getLabelStyles = (theme: GrafanaTheme2) => {
14 | return {
15 | label: css`
16 | label: Label;
17 | font-size: ${theme.typography.size.sm};
18 | font-weight: ${theme.typography.fontWeightMedium};
19 | line-height: 1.25;
20 | margin-bottom: ${theme.spacing(0.5)};
21 | color: ${theme.colors.text.primary};
22 | max-width: 480px;
23 | `,
24 | labelContent: css`
25 | display: flex;
26 | align-items: center;
27 | `,
28 | description: css`
29 | label: Label-description;
30 | color: ${theme.colors.text.secondary};
31 | font-size: ${theme.typography.size.sm};
32 | font-weight: ${theme.typography.fontWeightRegular};
33 | margin-top: ${theme.spacing(0.25)};
34 | display: block;
35 | `,
36 | categories: css`
37 | label: Label-categories;
38 | display: inline-flex;
39 | align-items: center;
40 | `,
41 | chevron: css`
42 | margin: 0 ${theme.spacing(0.25)};
43 | `,
44 | };
45 | };
46 |
47 | class Label extends Component {
48 | render() {
49 | const { children, description, className, category, theme, ...labelProps } = this.props;
50 | const styles = getLabelStyles(theme);
51 | const categories = category?.map((c, i) => {
52 | return (
53 |
54 | {c}
55 |
56 |
57 | );
58 | });
59 |
60 | return (
61 |
62 |
63 |
64 | {categories}
65 | {children}
66 |
67 | {description && {description} }
68 |
69 |
70 | );
71 | }
72 | }
73 |
74 | export default withTheme2(Label);
75 |
--------------------------------------------------------------------------------
/src/components/QueryEditor/QueryOptionGroup.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/css';
2 | import React, { Component } from 'react';
3 | import { getValueFormat, GrafanaTheme2 } from '@grafana/data';
4 | import { config } from '@grafana/runtime';
5 | import { Icon, Tooltip, withTheme2 } from '@grafana/ui';
6 | import { Collapse } from '../Collapse';
7 |
8 | import Stack from './Stack';
9 |
10 | export type QueryStats = {
11 | bytes: number;
12 | // The error message displayed in the UI when we cant estimate the size of the query.
13 | message?: string;
14 | };
15 |
16 | interface Props {
17 | title: string;
18 | collapsedInfo: string[];
19 | queryStats?: QueryStats | null;
20 | children: React.ReactNode;
21 | theme: GrafanaTheme2; // Add theme to props for withTheme2 HOC
22 | }
23 |
24 | interface State {
25 | isOpen: boolean;
26 | }
27 |
28 | class QueryOptionGroup extends Component {
29 | constructor(props: Props) {
30 | super(props);
31 | this.state = {
32 | isOpen: false,
33 | };
34 | }
35 |
36 | toggleOpen = () => {
37 | this.setState((prevState) => ({ isOpen: !prevState.isOpen }));
38 | };
39 |
40 | getStyles = () => {
41 | const { theme } = this.props;
42 | return {
43 | collapse: css({
44 | backgroundColor: 'unset',
45 | border: 'unset',
46 | marginBottom: 0,
47 |
48 | ['> button']: {
49 | padding: theme.spacing(0, 1),
50 | },
51 | }),
52 | wrapper: css({
53 | width: '100%',
54 | display: 'flex',
55 | justifyContent: 'space-between',
56 | alignItems: 'baseline',
57 | }),
58 | title: css({
59 | flexGrow: 1,
60 | overflow: 'hidden',
61 | fontSize: theme.typography.bodySmall.fontSize,
62 | fontWeight: theme.typography.fontWeightMedium,
63 | margin: 0,
64 | }),
65 | description: css({
66 | color: theme.colors.text.secondary,
67 | fontSize: theme.typography.bodySmall.fontSize,
68 | fontWeight: theme.typography.bodySmall.fontWeight,
69 | paddingLeft: theme.spacing(2),
70 | gap: theme.spacing(2),
71 | display: 'flex',
72 | }),
73 | body: css({
74 | display: 'flex',
75 | gap: theme.spacing(2),
76 | flexWrap: 'wrap',
77 | }),
78 | stats: css({
79 | margin: '0px',
80 | color: theme.colors.text.secondary,
81 | fontSize: theme.typography.bodySmall.fontSize,
82 | }),
83 | tooltip: css({
84 | marginRight: theme.spacing(0.25),
85 | }),
86 | };
87 | };
88 |
89 | render() {
90 | const { title, children, collapsedInfo, queryStats } = this.props;
91 | const { isOpen } = this.state;
92 | const styles = this.getStyles();
93 |
94 | return (
95 |
96 |
103 | {title}
104 | {!isOpen && (
105 |
106 | {collapsedInfo.map((x, i) => (
107 | {x}
108 | ))}
109 |
110 | )}
111 |
112 | }
113 | >
114 | {children}
115 |
116 | {/**TODO: This is Loki logic that should eventually be moved to Loki */}
117 | {queryStats && config.featureToggles.lokiQuerySplitting && (
118 |
119 |
120 |
121 | )}
122 |
123 | {queryStats &&
{generateQueryStats(queryStats)}
}
124 |
125 | );
126 | }
127 | }
128 |
129 | export default withTheme2(QueryOptionGroup);
130 |
131 | const generateQueryStats = (queryStats: QueryStats) => {
132 | if (queryStats.message) {
133 | return queryStats.message;
134 | }
135 |
136 | return `This query will process approximately ${convertUnits(queryStats)}.`;
137 | };
138 |
139 | const convertUnits = (queryStats: QueryStats): string => {
140 | const { text, suffix } = getValueFormat('bytes')(queryStats.bytes, 1);
141 | return text + suffix;
142 | };
143 |
--------------------------------------------------------------------------------
/src/components/QueryEditor/SecretInput.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Button, Input } from '@grafana/ui';
3 | import Stack from './Stack';
4 |
5 | export type Props = React.ComponentProps & {
6 | /** TRUE if the secret was already configured. (It is needed as often the backend doesn't send back the actual secret, only the information that it was configured) */
7 | isConfigured: boolean;
8 | /** Called when the user clicks on the "Reset" button in order to clear the secret */
9 | onReset: () => void;
10 | };
11 |
12 | export const CONFIGURED_TEXT = 'configured';
13 | export const RESET_BUTTON_TEXT = 'Reset';
14 |
15 | export const SecretInput = ({ isConfigured, onReset, ...props }: Props) => (
16 |
17 | {!isConfigured && }
18 | {isConfigured && }
19 | {isConfigured && (
20 |
21 | {RESET_BUTTON_TEXT}
22 |
23 | )}
24 |
25 | );
26 |
--------------------------------------------------------------------------------
/src/components/QueryEditor/Space.tsx:
--------------------------------------------------------------------------------
1 | import { css, cx } from '@emotion/css';
2 | import React, { Component } from 'react';
3 | import { GrafanaTheme2 } from '@grafana/data';
4 | import { withTheme2 } from '@grafana/ui';
5 |
6 | export interface SpaceProps {
7 | v?: number;
8 | h?: number;
9 | layout?: 'block' | 'inline';
10 | theme: GrafanaTheme2; // Add theme to props for withTheme2 HOC
11 | }
12 |
13 | /**
14 | * @deprecated use the Space component from @grafana/ui instead. Available starting from @grafana/ui@10.4.0
15 | */
16 | class Space extends Component {
17 | static defaultProps = {
18 | v: 0,
19 | h: 0,
20 | layout: 'block' as any,
21 | };
22 |
23 | getStyles = () => {
24 | const { theme, v, h, layout } = this.props;
25 | return {
26 | wrapper: css([
27 | {
28 | paddingRight: theme.spacing(h ?? 0),
29 | paddingBottom: theme.spacing(v ?? 0),
30 | },
31 | layout === 'inline' && {
32 | display: 'inline-block',
33 | },
34 | layout === 'block' && {
35 | display: 'block',
36 | },
37 | ]),
38 | };
39 | };
40 |
41 | render() {
42 | const styles = this.getStyles();
43 | return ;
44 | }
45 | }
46 |
47 | export default withTheme2(Space);
48 |
--------------------------------------------------------------------------------
/src/components/QueryEditor/Stack.tsx:
--------------------------------------------------------------------------------
1 | import { css } from '@emotion/css';
2 | import React, { Component, CSSProperties } from 'react';
3 | import { withTheme2 } from '@grafana/ui';
4 | import {GrafanaTheme2} from '@grafana/data'
5 |
6 | interface StackProps {
7 | direction?: CSSProperties['flexDirection'];
8 | alignItems?: CSSProperties['alignItems'];
9 | wrap?: boolean;
10 | gap?: number;
11 | flexGrow?: CSSProperties['flexGrow'];
12 | theme: GrafanaTheme2; // Add theme to props for withTheme2 HOC
13 | }
14 |
15 | /**
16 | * @deprecated use the Stack component from @grafana/ui instead. Available starting from @grafana/ui@10.2.3
17 | */
18 | class Stack extends Component> {
19 | static defaultProps = {
20 | direction: 'row' as any,
21 | wrap: true,
22 | gap: 2,
23 | };
24 |
25 | getStyles = () => {
26 | const { theme, direction = 'row', wrap = true, alignItems, gap = 2, flexGrow } = this.props;
27 | return {
28 | root: css({
29 | display: 'flex',
30 | flexDirection: direction,
31 | flexWrap: wrap ? 'wrap' : undefined,
32 | alignItems: alignItems,
33 | gap: theme.spacing(gap),
34 | flexGrow: flexGrow,
35 | width: '100%'
36 | }),
37 | };
38 | };
39 |
40 | render() {
41 | const styles = this.getStyles();
42 | const { children } = this.props;
43 |
44 | return {children}
;
45 | }
46 | }
47 |
48 | export default withTheme2(Stack);
49 |
50 |
--------------------------------------------------------------------------------
/src/components/RadioButtonGroup/RadioButton.tsx:
--------------------------------------------------------------------------------
1 | import { css, CSSObject } from '@emotion/css';
2 | import React, { Component } from 'react';
3 | import { withTheme2 } from '@grafana/ui'; // Assuming you export GrafanaTheme2 and withTheme2 from @grafana/ui
4 | import { GrafanaTheme2 } from '@grafana/data';
5 |
6 | import { StringSelector } from '@grafana/e2e-selectors';
7 |
8 | interface RadioButtonProps {
9 | size?: RadioButtonSize;
10 | disabled?: boolean;
11 | name?: string;
12 | description?: string;
13 | active: boolean;
14 | id: string;
15 | onChange: () => void;
16 | onClick: () => void;
17 | fullWidth?: boolean;
18 | 'aria-label'?: StringSelector;
19 | children?: React.ReactNode;
20 | theme: GrafanaTheme2; // Add theme to props for withTheme2 HOC
21 | inputRef?: React.RefObject;
22 | }
23 |
24 | export type RadioButtonSize = 'sm' | 'md';
25 |
26 | class RadioButton_ extends Component {
27 | // inputRef = React.createRef();
28 |
29 | getStyles = () => {
30 | const { theme, size, fullWidth } = this.props;
31 | const { fontSize, height, padding } = getPropertiesForButtonSize(size || 'md', theme);
32 |
33 | const textColor = theme.colors.text.secondary;
34 | const textColorHover = theme.colors.text.primary;
35 | // remove the group inner padding (set on RadioButtonGroup)
36 | const labelHeight = height * theme.spacing.gridSize - 4 - 2;
37 |
38 | return {
39 | radio: css`
40 | position: absolute;
41 | opacity: 0;
42 | z-index: -1000;
43 |
44 | &:checked + label {
45 | color: ${theme.colors.text.primary};
46 | font-weight: ${theme.typography.fontWeightMedium};
47 | background: ${theme.colors.action.selected};
48 | z-index: 3;
49 | }
50 |
51 | &:focus + label,
52 | &:focus-visible + label {
53 | ${getFocusStyles(theme)};
54 | }
55 |
56 | &:focus:not(:focus-visible) + label {
57 | ${getMouseFocusStyles(theme)}
58 | }
59 |
60 | &:disabled + label {
61 | color: ${theme.colors.text.disabled};
62 | cursor: not-allowed;
63 | }
64 | `,
65 | radioLabel: css`
66 | display: inline-block;
67 | position: relative;
68 | font-size: ${fontSize};
69 | height: ${labelHeight}px;
70 | // Deduct border from line-height for perfect vertical centering on windows and linux
71 | line-height: ${labelHeight}px;
72 | color: ${textColor};
73 | padding: ${theme.spacing(0, padding)};
74 | border-radius: ${theme.shape.borderRadius()};
75 | background: ${theme.colors.background.primary};
76 | cursor: pointer;
77 | z-index: 1;
78 | flex: ${fullWidth ? `1 0 0` : 'none'};
79 | text-align: center;
80 | user-select: none;
81 | white-space: nowrap;
82 |
83 | &:hover {
84 | color: ${textColorHover};
85 | }
86 | `,
87 | };
88 | };
89 |
90 | render() {
91 | const {
92 | children,
93 | active,
94 | disabled,
95 | onChange,
96 | onClick,
97 | id,
98 | name,
99 | description,
100 | inputRef,
101 | 'aria-label': ariaLabel,
102 | } = this.props;
103 |
104 | const styles = this.getStyles();
105 |
106 | return (
107 | <>
108 |
120 |
121 | {children}
122 |
123 | >
124 | );
125 | }
126 | }
127 |
128 | export const RadioButton = withTheme2(RadioButton_);
129 |
130 | // Utility functions
131 |
132 | export function getMouseFocusStyles(theme: GrafanaTheme2): CSSObject {
133 | return {
134 | outline: 'none',
135 | boxShadow: `none`,
136 | };
137 | }
138 |
139 | export function getFocusStyles(theme: GrafanaTheme2): CSSObject {
140 | return {
141 | outline: '2px dotted transparent',
142 | outlineOffset: '2px',
143 | boxShadow: `0 0 0 2px ${theme.colors.background.canvas}, 0 0 0px 4px ${theme.colors.primary.main}`,
144 | transitionTimingFunction: `cubic-bezier(0.19, 1, 0.22, 1)`,
145 | transitionDuration: '0.2s',
146 | transitionProperty: 'outline, outline-offset, box-shadow',
147 | };
148 | }
149 |
150 | function getPropertiesForButtonSize(size: any, theme: GrafanaTheme2) {
151 | switch (size) {
152 | case 'sm':
153 | return {
154 | padding: 1,
155 | fontSize: theme.typography.size.sm,
156 | height: theme.components.height.sm,
157 | };
158 |
159 | case 'lg':
160 | return {
161 | padding: 3,
162 | fontSize: theme.typography.size.lg,
163 | height: theme.components.height.lg,
164 | };
165 | case 'md':
166 | default:
167 | return {
168 | padding: 2,
169 | fontSize: theme.typography.size.md,
170 | height: theme.components.height.md,
171 | };
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/src/components/RadioButtonGroup/RadioButtonGroup.tsx:
--------------------------------------------------------------------------------
1 | import { css, cx } from '@emotion/css';
2 | import { uniqueId } from 'lodash';
3 | import React, { Component, createRef } from 'react';
4 |
5 | import { GrafanaTheme2, SelectableValue } from '@grafana/data';
6 | import { Icon, withTheme2 } from '@grafana/ui'; // Assuming you export GrafanaTheme2 and withTheme2 from @grafana/ui
7 |
8 | import { RadioButtonSize, RadioButton } from './RadioButton';
9 |
10 | interface RadioButtonGroupProps {
11 | value?: T;
12 | id?: string;
13 | disabled?: boolean;
14 | disabledOptions?: T[];
15 | options: Array>;
16 | onChange?: (value: T) => void;
17 | onClick?: (value: T) => void;
18 | size?: RadioButtonSize;
19 | fullWidth?: boolean;
20 | className?: string;
21 | autoFocus?: boolean;
22 | theme: GrafanaTheme2; // Add theme to props for withTheme2 HOC
23 | }
24 |
25 | class RadioButtonGroup extends Component> {
26 | static defaultProps = {
27 | size: 'md' as any,
28 | fullWidth: false,
29 | autoFocus: false,
30 | };
31 |
32 | activeButtonRef = createRef();
33 |
34 | handleOnChange = (option: SelectableValue) => {
35 | return () => {
36 | if (this.props.onChange) {
37 | this.props.onChange(option.value);
38 | }
39 | };
40 | };
41 |
42 | handleOnClick = (option: SelectableValue) => {
43 | return () => {
44 | if (this.props.onClick) {
45 | this.props.onClick(option.value);
46 | }
47 | };
48 | };
49 |
50 | componentDidMount() {
51 | if (this.props.autoFocus && this.activeButtonRef.current) {
52 | this.activeButtonRef.current.focus();
53 | }
54 | }
55 |
56 | getStyles = () => {
57 | const { theme } = this.props;
58 | return {
59 | radioGroup: css({
60 | display: 'inline-flex',
61 | flexDirection: 'row',
62 | flexWrap: 'nowrap',
63 | border: `1px solid ${theme.components.input.borderColor}`,
64 | borderRadius: theme.shape.borderRadius(),
65 | padding: '2px',
66 | }),
67 | fullWidth: css({
68 | display: 'flex',
69 | }),
70 | icon: css`
71 | margin-right: 6px;
72 | `,
73 | img: css`
74 | width: ${theme.spacing(2)};
75 | height: ${theme.spacing(2)};
76 | margin-right: ${theme.spacing(1)};
77 | `,
78 | };
79 | };
80 |
81 | render() {
82 | const {
83 | options,
84 | value,
85 | disabled,
86 | disabledOptions,
87 | size,
88 | id,
89 | className,
90 | fullWidth,
91 | } = this.props;
92 |
93 | const internalId = id ?? uniqueId('radiogroup-');
94 | const groupName = internalId;
95 | const styles = this.getStyles();
96 |
97 | return (
98 |
99 | {options.map((o, i) => {
100 | const isItemDisabled = disabledOptions && o.value && disabledOptions.includes(o.value);
101 | return (
102 |
116 | {o.icon && }
117 | {o.imgUrl && }
118 | {o.label} {o.component ? : null}
119 |
120 | );
121 | })}
122 |
123 | );
124 | }
125 | }
126 |
127 | export default withTheme2(RadioButtonGroup);
128 |
--------------------------------------------------------------------------------
/src/components/Switch/switch.tsx:
--------------------------------------------------------------------------------
1 | import React, { HTMLProps, useRef } from 'react';
2 | import { css, cx } from '@emotion/css';
3 | import { uniqueId } from 'lodash';
4 | import { GrafanaTheme2, deprecationWarning } from '@grafana/data';
5 | import { stylesFactory, useTheme2 } from '@grafana/ui';
6 |
7 | import { getFocusStyles, getMouseFocusStyles } from '../RadioButtonGroup/RadioButton';
8 |
9 | export interface Props extends Omit, 'value'> {
10 | value?: boolean;
11 | /** Make switch's background and border transparent */
12 | transparent?: boolean;
13 | }
14 |
15 | export const Switch = React.forwardRef(
16 | ({ value, checked, disabled, onChange, id, ...inputProps }, ref) => {
17 | if (checked) {
18 | deprecationWarning('Switch', 'checked prop', 'value');
19 | }
20 |
21 | const theme = useTheme2();
22 | const styles = getSwitchStyles(theme);
23 | const switchIdRef = useRef(id ? id : uniqueId('switch-'));
24 |
25 | return (
26 |
27 | {
32 | console.log(event)
33 | onChange?.(event);
34 | }}
35 | id={switchIdRef.current}
36 | {...inputProps}
37 | ref={ref}
38 | />
39 |
40 |
41 | );
42 | }
43 | );
44 |
45 | Switch.displayName = 'Switch';
46 |
47 | export interface InlineSwitchProps extends Props {
48 | showLabel?: boolean;
49 | }
50 |
51 | export const InlineSwitch = React.forwardRef(
52 | ({ transparent, showLabel, label, value, id, onChange, ...props }, ref) => {
53 | const theme = useTheme2();
54 | const styles = getSwitchStyles(theme, transparent);
55 | console.log(value)
56 |
57 | return (
58 |
59 | {showLabel && (
60 |
64 | {label}
65 |
66 | )}
67 |
68 |
69 | );
70 | }
71 | );
72 |
73 | InlineSwitch.displayName = 'Switch';
74 |
75 | const getSwitchStyles = stylesFactory((theme: GrafanaTheme2, transparent?: boolean) => {
76 | return {
77 | switch: css`
78 | width: 32px;
79 | height: 16px;
80 | position: relative;
81 |
82 | input {
83 | opacity: 0;
84 | left: -100vw;
85 | z-index: -1000;
86 | position: absolute;
87 |
88 | &:disabled + label {
89 | background: ${theme.colors.action.disabledBackground};
90 | cursor: not-allowed;
91 | }
92 |
93 | &:checked + label {
94 | background: ${theme.colors.primary.main};
95 | border-color: ${theme.colors.primary.main};
96 |
97 | &:hover {
98 | background: ${theme.colors.primary.shade};
99 | }
100 |
101 | &::after {
102 | transform: translate3d(18px, -50%, 0);
103 | background: ${theme.colors.primary.contrastText};
104 | }
105 | }
106 |
107 | &:focus + label,
108 | &:focus-visible + label {
109 | ${getFocusStyles(theme)}
110 | }
111 |
112 | &:focus:not(:focus-visible) + label {
113 | ${getMouseFocusStyles(theme)}
114 | }
115 | }
116 |
117 | label {
118 | width: 100%;
119 | height: 100%;
120 | cursor: pointer;
121 | border: none;
122 | border-radius: 50px;
123 | background: ${theme.components.input.background};
124 | border: 1px solid ${theme.components.input.borderColor};
125 | transition: all 0.3s ease;
126 | position: absolute;
127 |
128 | &:hover {
129 | border-color: ${theme.components.input.borderHover};
130 | }
131 |
132 | &::after {
133 | position: absolute;
134 | display: block;
135 | content: '';
136 | width: 12px;
137 | height: 12px;
138 | border-radius: 6px;
139 | background: ${theme.colors.text.secondary};
140 | box-shadow: ${theme.shadows.z1};
141 | top: 50%;
142 | transform: translate3d(2px, -50%, 0);
143 | transition: transform 0.2s cubic-bezier(0.19, 1, 0.22, 1);
144 | }
145 | }
146 | `,
147 | inlineContainer: css`
148 | padding: ${theme.spacing(0, 1)};
149 | height: ${theme.spacing(theme.components.height.md)};
150 | display: inline-flex;
151 | align-items: center;
152 | background: ${transparent ? 'transparent' : theme.components.input.background};
153 | border: 1px solid ${transparent ? 'transparent' : theme.components.input.borderColor};
154 | border-radius: ${theme.shape.borderRadius()};
155 |
156 | &:hover {
157 | border: 1px solid ${transparent ? 'transparent' : theme.components.input.borderHover};
158 |
159 | .inline-switch-label {
160 | color: ${theme.colors.text.primary};
161 | }
162 | }
163 | `,
164 | inlineLabel: css`
165 | cursor: pointer;
166 | padding-right: ${theme.spacing(1)};
167 | color: ${theme.colors.text.secondary};
168 | white-space: nowrap;
169 | `,
170 | inlineLabelEnabled: css`
171 | color: ${theme.colors.text.primary};
172 | `,
173 | };
174 | });
175 |
--------------------------------------------------------------------------------
/src/components/configSection.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, PureComponent } from 'react';
2 | import { IconName } from '@grafana/ui';
3 | import { IconButton } from './IconButton';
4 | import './style.css';
5 |
6 | export type Props = {
7 | title: string;
8 | description?: ReactNode;
9 | isCollapsible?: boolean;
10 | isInitiallyOpen?: boolean;
11 | kind?: 'section' | 'sub-section';
12 | className?: string;
13 | children: ReactNode;
14 | };
15 |
16 | export class ConfigSection extends PureComponent {
17 | state = {
18 | isOpen: true,
19 | };
20 | render() {
21 | const { children, title, description, isCollapsible = false, kind = 'section' } = this.props;
22 | const { isOpen } = this.state;
23 | const iconName: IconName = isOpen ? 'angle-up' : 'angle-down';
24 | const isSubSection = kind === 'sub-section';
25 | const collapsibleButtonAriaLabel = `${isOpen ? 'Collapse' : 'Expand'} section ${title}`;
26 |
27 | return (
28 |
29 |
30 | {kind === 'section' ?
{title} : {title} }
31 | {isCollapsible && (
32 | this.setState({ isOpen: !isOpen })}
35 | type="button"
36 | size="xl"
37 | aria-label={collapsibleButtonAriaLabel}
38 | />
39 | )}
40 |
41 | {description && (
42 |
50 | {description}
51 |
52 | )}
53 | {isOpen &&
{children}
}
54 |
55 | );
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/description.tsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import './style.css';
3 |
4 | type Props = {
5 | dataSourceName: string;
6 | docsLink: string;
7 | hasRequiredFields?: boolean;
8 | className?: string;
9 | };
10 |
11 | export class DataSourceDescription extends PureComponent {
12 | render() {
13 | const { dataSourceName, docsLink, hasRequiredFields = true } = this.props;
14 |
15 | return (
16 |
17 |
18 | Before you can use the {dataSourceName} data source, you must configure it below or in the config file. For
19 | detailed instructions,
20 |
21 | view the documentation
22 |
23 | .
24 |
25 | {hasRequiredFields && (
26 |
27 | Fields marked with * are required
28 |
29 | )}
30 |
31 | );
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/style.css:
--------------------------------------------------------------------------------
1 |
2 | .description-container {
3 | p{
4 | margin: 0;
5 | }
6 |
7 | p+p{
8 | margin-top: 16px;
9 | }
10 |
11 | .description-text {
12 | font-weight: 400;
13 | font-family: Inter, Helvetica, Arial, sans-serif;
14 | }
15 |
16 | a {
17 | color: #6E9FFF;
18 | text-decoration: underline;
19 | }
20 | a::hover {
21 | text-decoration: none;
22 | }
23 | }
24 |
25 | .config-section {
26 | .header {
27 | display: flex;
28 | justify-content: space-between;
29 | align-items: center;
30 | }
31 | .title {
32 | margin: 0,
33 | }
34 |
35 | .subtitle {
36 | margin: 0;
37 | font-weight: 400
38 | }
39 | }
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/const.ts:
--------------------------------------------------------------------------------
1 | export const xColInfoSeries = [
2 | {
3 | color: '#ff006e',
4 | label: '空 empty',
5 | value: '表格或日志 table or log',
6 | },
7 | {
8 | color: '#80ed99',
9 | label: '【时间列 Time colomn】',
10 | value: '时序数据 Timeseries',
11 | },
12 | {
13 | color: '#0077b6',
14 | label: 'stat',
15 | value: '单值图格式 gauge/stat graph',
16 | },
17 | {
18 | color: '#0096c7',
19 | label: 'pie',
20 | value: '饼图格式 pie graph',
21 | },
22 | {
23 | color: '#00b4d8',
24 | label: 'bar',
25 | value: '柱状图格式 bar graph',
26 | },
27 | {
28 | color: '#48cae4',
29 | label: 'trace',
30 | value: 'Trace格式 Trace graph',
31 | },
32 | {
33 | color: '#90e0ef',
34 | label: 'map',
35 | value: '地图格式 map graph',
36 | },
37 | ];
38 |
39 | export const yColInfoSeries = [
40 | {
41 | color: '#ffca3a',
42 | label: '【无需填写】 此情况适用于Log、Trace以及表格全列展示的情况',
43 | value: '[No need to fill in] This situation applies to Log, Trace and the full column of the table',
44 | },
45 | {
46 | color: '#8ac926',
47 | label: '【col1,col2,col3】 最常用的一种形式,在图表中展示若干数值列',
48 | value: '[col1,col2,col3] The most common form, displaying several numeric columns in the chart',
49 | },
50 | {
51 | color: '#1982c4',
52 | label: '【col1#:#col2】 适用于bar和SLS时序库展示,col1为聚合列,col2为其他列',
53 | value: '[col1#:#col2] Applicable to bar and SLS metricStore, col1 is the aggregation column, col2 is other columns',
54 | },
55 | ];
56 |
57 | export const totalLogsInfoSeries = [
58 | {
59 | color: '#ffca3a',
60 | label: '只对 Query 查询语句生效,分析语句无效',
61 | value: 'Only effective for search statements, not applicable to analytic statements',
62 | },
63 | {
64 | color: '#8ac926',
65 | label: '最小值为 1 条',
66 | value: 'Minimum value is 1',
67 | },
68 | {
69 | color: '#1982c4',
70 | label: '最大值为 5000 条',
71 | value: 'Maximum value is 5000',
72 | },
73 | ];
74 |
75 | export const xSelectOptions = [
76 | {
77 | label: 'TimeSeries / Custom',
78 | value: 'custom',
79 | description: '时序数据 Timeseries 自定义【时间列 Time colomn】,或自定义x轴输入',
80 | },
81 | { label: 'Table / Log', value: '', description: '表格或日志 table or log' },
82 | { label: 'Stat / Gauge', value: 'stat', description: '单值图格式 gauge/stat graph' },
83 | { label: 'Pie', value: 'pie', description: '饼图格式 pie graph' },
84 | { label: 'Bar', value: 'bar', description: '柱状图格式 bar graph' },
85 | { label: 'Trace', value: 'trace', description: 'Trace格式 Trace graph' },
86 | { label: 'Map', value: 'map', description: '地图格式 map graph' },
87 | // {
88 | // label: 'Option with description and image',
89 | // value: 2,
90 | // description: 'This is a very elaborate description, describing all the wonders in the world.',
91 | // imgUrl: 'https://placekitten.com/40/40',
92 | // },
93 | ];
94 |
95 | export const dataSourceType = [
96 | { label: 'ALL(SQL)', value: 'all' },
97 | {
98 | label: 'Logstore(SQL)',
99 | value: 'logstore',
100 | },
101 | { label: 'Metricstore(SQL)', value: 'metricsql' },
102 | { label: 'Metricstore(PromQL)', value: 'metricstore' },
103 | ];
104 |
105 | export const version = (window as any)?.grafanaBootData?.settings?.buildInfo?.version ?? '';
106 |
--------------------------------------------------------------------------------
/src/custom-header/custom-header.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { css, cx } from '@emotion/css';
3 | import { InlineFieldRow, InlineField, Input, IconButton, withTheme2 } from '@grafana/ui';
4 | import type { LocalHeader } from '../types';
5 |
6 | export type Props = {
7 | header: LocalHeader;
8 | onChange: (header: LocalHeader) => void;
9 | onBlur: () => void;
10 | onDelete: () => void;
11 | readOnly: boolean;
12 | theme: any; // Add theme to props for withTheme2 HOC
13 | };
14 |
15 | const getCommonStyles = () => {
16 | return {
17 | inlineFieldNoMarginRight: css({
18 | marginRight: 0,
19 | }),
20 | // This is dirty hack to make configured secret input grow
21 | inlineFieldWithSecret: css({
22 | '[class$="layoutChildrenWrapper"]:first-child': {
23 | flexGrow: 1,
24 | },
25 | }),
26 | };
27 | };
28 |
29 | class CustomHeader extends Component {
30 | handleInputChange = (field: 'name' | 'value') => (event: React.ChangeEvent) => {
31 | const { header, onChange } = this.props;
32 | onChange({ ...header, [field]: event.currentTarget.value });
33 | };
34 |
35 | handleReset = () => {
36 | const { header, onChange } = this.props;
37 | onChange({ ...header, configured: false, value: '' });
38 | };
39 |
40 | render() {
41 | const { header, onBlur, onDelete, readOnly, theme } = this.props;
42 | const { spacing } = theme;
43 | const commonStyles = getCommonStyles();
44 |
45 | const styles = {
46 | container: css({
47 | alignItems: 'center',
48 | }),
49 | headerNameField: css({
50 | width: '40%',
51 | marginRight: 0,
52 | paddingRight: spacing(1),
53 | }),
54 | headerValueField: css({
55 | width: '45%',
56 | marginRight: 0,
57 | }),
58 | removeHeaderBtn: css({
59 | margin: `0 0 3px 10px`,
60 | }),
61 | };
62 |
63 | return (
64 |
65 |
73 |
80 |
81 |
89 | {} : this.handleReset}
95 | onBlur={onBlur}
96 | />
97 |
98 |
107 |
108 | );
109 | }
110 | }
111 |
112 | export default withTheme2(CustomHeader);
113 |
--------------------------------------------------------------------------------
/src/custom-header/custom-headers.tsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { css } from '@emotion/css';
3 | import { Button, withTheme2 } from '@grafana/ui';
4 | import CustomHeader from './custom-header';
5 | import type { HeaderWithValue, LocalHeader } from '../types';
6 | import { ConfigSection } from '../components/configSection';
7 |
8 | export type Props = {
9 | headers: HeaderWithValue[];
10 | onChange: (headers: HeaderWithValue[]) => void;
11 | readOnly: boolean;
12 | theme: any; // Add theme to props for withTheme2 HOC
13 | };
14 |
15 | type State = {
16 | headers: LocalHeader[];
17 | };
18 |
19 | class CustomHeaders extends Component {
20 | constructor(props: Props) {
21 | super(props);
22 | this.state = {
23 | headers: props.headers.map((header) => ({
24 | ...header,
25 | id: uniqueId(),
26 | })),
27 | };
28 | }
29 |
30 | componentDidUpdate(prevProps: Props) {
31 | if (this.props.headers !== prevProps.headers) {
32 | this.setState((prevState) => {
33 | let changed = false;
34 | const newHeaders = prevState.headers.map((header) => {
35 | const configured = this.props.headers.find((h) => h.name === header.name)?.configured;
36 | if (typeof configured !== 'undefined' && header.configured !== configured) {
37 | changed = true;
38 | return { ...header, configured };
39 | }
40 | return header;
41 | });
42 |
43 | if (changed) {
44 | return { headers: newHeaders };
45 | }
46 |
47 | return prevState;
48 | });
49 | }
50 | }
51 |
52 | onHeaderAdd = (e: React.MouseEvent) => {
53 | e.preventDefault()
54 | this.setState(
55 | (prevState) => ({
56 | headers: [...prevState.headers, { id: uniqueId(), name: '', value: '', configured: false }],
57 | }),
58 | this.triggerChange
59 | );
60 | };
61 |
62 | onHeaderChange = (id: string, header: LocalHeader) => {
63 | this.setState(
64 | (prevState) => ({
65 | headers: prevState.headers.map((h) => (h.id === id ? { ...header } : h)),
66 | }),
67 | this.triggerChange
68 | );
69 | };
70 |
71 | onHeaderDelete = (id: string) => {
72 | this.setState(
73 | (prevState) => {
74 | const index = prevState.headers.findIndex((h) => h.id === id);
75 | if (index === -1) {
76 | return prevState;
77 | }
78 | const newHeaders = [...prevState.headers];
79 | newHeaders.splice(index, 1);
80 | return { headers: newHeaders };
81 | },
82 | this.triggerChange
83 | );
84 | };
85 |
86 | onBlur = () => {
87 | this.triggerChange();
88 | };
89 |
90 | triggerChange = () => {
91 | const { headers } = this.state;
92 | this.props.onChange(
93 | headers.map(({ name, value, configured }) => ({
94 | name,
95 | value,
96 | configured,
97 | }))
98 | );
99 | };
100 |
101 | render() {
102 | const { readOnly, theme } = this.props;
103 | const { headers } = this.state;
104 | const { spacing } = theme;
105 |
106 | const styles = {
107 | container: css({
108 | marginTop: spacing(3),
109 | }),
110 | addHeaderButton: css({
111 | marginTop: spacing(1.5),
112 | }),
113 | };
114 |
115 | return (
116 |
117 |
124 |
125 | {headers.map((header) => (
126 | this.onHeaderChange(header.id, header)}
130 | onDelete={() => this.onHeaderDelete(header.id)}
131 | onBlur={this.onBlur}
132 | readOnly={readOnly}
133 | />
134 | ))}
135 |
136 |
137 |
138 | {headers.length === 0 ? 'Add header' : 'Add another header'}
139 |
140 |
141 |
142 |
143 | );
144 | }
145 | }
146 |
147 | export default withTheme2(CustomHeaders);
148 |
149 | function uniqueId(): string {
150 | return Math.random().toString(16).slice(2);
151 | }
152 |
--------------------------------------------------------------------------------
/src/datasource.ts:
--------------------------------------------------------------------------------
1 | import {
2 | TraceLog,
3 | TraceKeyValuePair,
4 | TraceSpanRow,
5 | DataFrame,
6 | DataQueryRequest,
7 | DataQueryResponse,
8 | DataSourceInstanceSettings,
9 | FieldType,
10 | MutableDataFrame,
11 | Vector,
12 | MetricFindValue,
13 | } from '@grafana/data';
14 | import { defaultQuery, SLSDataSourceOptions, SLSQuery } from './types';
15 | import { DataSourceWithBackend, getBackendSrv, getTemplateSrv } from '@grafana/runtime';
16 | import _ from 'lodash';
17 | import { map } from 'rxjs/operators';
18 |
19 | export class SLSDataSource extends DataSourceWithBackend {
20 | constructor(instanceSettings: DataSourceInstanceSettings) {
21 | super(instanceSettings);
22 | }
23 |
24 | query(options: DataQueryRequest) {
25 | // console.log("into query.")
26 | options.targets.forEach((q: SLSQuery) => {
27 | q.query = replaceQueryParameters(q, options);
28 | if(/__custom__@/.test(q?.logstore ?? '')){
29 | const currentLogstore: any = getTemplateSrv().getVariables().find((item) => item.name === q?.logstore?.split('@')[1]);
30 | const value = currentLogstore?.current?.value;
31 | q.logstore = value;
32 | }
33 | });
34 | if (options.targets[0].xcol === 'trace') {
35 | return super.query(options).pipe(map(responseToDataQueryResponse));
36 | }
37 | return super.query(options);
38 | }
39 |
40 | metricFindQuery(query: SLSQuery | string, options?: any): Promise {
41 | const _defaultQuery = { ...defaultQuery, ...(typeof query === 'string' ? { query } : query)}
42 | const { type, logstore, queryType, legendFormat, step } = _defaultQuery;
43 | const data = {
44 | from: options.range.from.valueOf().toString(),
45 | to: options.range.to.valueOf().toString(),
46 | queries: [
47 | {
48 | // datasource: this.name,
49 | datasource: { type: this.type, uid: this.uid },
50 | datasourceId: this.id,
51 | query: replaceQueryParameters(_defaultQuery.query || '', options),
52 | type,
53 | logstore,
54 | queryType,
55 | legendFormat,
56 | step,
57 | },
58 | ],
59 | };
60 |
61 | return new Promise(async (resolve) => {
62 | let values: any[] = [];
63 |
64 | try {
65 | const response = await getBackendSrv().post('/api/ds/query', data);
66 | values = response.results.A.frames ?? []
67 | } catch (error) {
68 | console.log('error', error)
69 | }
70 | const res = mapToTextValueNew(values);
71 | resolve(res);
72 | });
73 | }
74 | }
75 |
76 | function valueToTag(key: any, value: any): TraceKeyValuePair {
77 | return {
78 | key,
79 | value,
80 | };
81 | }
82 |
83 | function transServiceTags(result: Map, i: number): TraceKeyValuePair[] {
84 | const resource = JSON.parse(result.get('resource')?.get(i));
85 | const resourceArray = Array.from(Object.entries(resource), ([name, value]) => valueToTag(name, value));
86 | resourceArray.push(valueToTag('host', result.get('host')?.get(i)));
87 | return resourceArray;
88 | }
89 |
90 | function transTags(result: Map, i: number): TraceKeyValuePair[] {
91 | const attribute = JSON.parse(result.get('attribute')?.get(i));
92 | const resourceArray = Array.from(Object.entries(attribute), ([name, value]) => valueToTag(name, value));
93 | resourceArray.push(valueToTag('statusCode', result.get('statusCode')?.get(i)));
94 | resourceArray.push(valueToTag('statusMessage', result.get('statusMessage')?.get(i)));
95 | return resourceArray;
96 | }
97 |
98 | function transLogs(result: Map, i: number): TraceLog[] {
99 | let traceLogs: TraceLog[] = [];
100 | const slsLogs = JSON.parse(result.get('logs')?.get(i)) as Object[];
101 | for (const slsLog of slsLogs) {
102 | const logMap = new Map(Object.entries(slsLog));
103 | const attributeArray = Array.from(Object.entries(slsLog), ([name, value]) => valueToTag(name, value));
104 | let time = logMap.get('time');
105 | if (time !== undefined) {
106 | traceLogs.push({
107 | timestamp: Number(time) / 1000000,
108 | fields: attributeArray,
109 | });
110 | }
111 | }
112 | return traceLogs;
113 | }
114 |
115 | function transformSpan(df: DataFrame): TraceSpanRow[] {
116 | let traceSpanRows: TraceSpanRow[] = [];
117 | const fields = df.fields;
118 | const result = new Map(fields.map((key) => [key.name, key.values]));
119 | for (let i = 0; i < df.length; i++) {
120 | const tsd = {
121 | traceID: result.get('traceID')?.get(i),
122 | spanID: result.get('spanID')?.get(i),
123 | parentSpanID: result.get('parentSpanID')?.get(i),
124 | operationName: result.get('operationName')?.get(i),
125 | serviceName: result.get('serviceName')?.get(i),
126 | serviceTags: transServiceTags(result, i),
127 | startTime: result.get('startTime')?.get(i),
128 | duration: result.get('duration')?.get(i),
129 | tags: transTags(result, i),
130 | errorIconColor: result.get('statusCode')?.get(i) === 'ERROR' ? '#f00' : '',
131 | logs: transLogs(result, i),
132 | };
133 | traceSpanRows.push(tsd);
134 | }
135 | return traceSpanRows;
136 | }
137 |
138 | export function transformResponse(df: DataFrame): DataFrame {
139 | const spanRows = transformSpan(df);
140 | const frame = new MutableDataFrame({
141 | fields: [
142 | { name: 'traceID', type: FieldType.string },
143 | { name: 'spanID', type: FieldType.string },
144 | { name: 'parentSpanID', type: FieldType.string },
145 | { name: 'operationName', type: FieldType.string },
146 | { name: 'serviceName', type: FieldType.string },
147 | { name: 'serviceTags', type: FieldType.other },
148 | { name: 'startTime', type: FieldType.number },
149 | { name: 'duration', type: FieldType.number },
150 | { name: 'logs', type: FieldType.other },
151 | { name: 'tags', type: FieldType.other },
152 | { name: 'errorIconColor', type: FieldType.string },
153 | ],
154 | meta: {
155 | preferredVisualisationType: 'trace',
156 | },
157 | });
158 |
159 | for (const span of spanRows) {
160 | frame.add(span);
161 | }
162 |
163 | return frame;
164 | }
165 |
166 | function responseToDataQueryResponse(response: DataQueryResponse): DataQueryResponse {
167 | return {
168 | data: [transformResponse(response.data[0])],
169 | };
170 | }
171 |
172 | export function mapToTextValue(result: any) {
173 | if (Array.isArray(result) && result.length === 2) {
174 | return _.map(result[0], (d, i) => {
175 | return { text: d, value: result[1][i] };
176 | });
177 | }
178 | return _.map(result[0], (d, i) => {
179 | if (d && d.text && d.value) {
180 | return { text: d.text, value: d.value };
181 | } else if (_.isObject(d)) {
182 | return { text: d, value: i };
183 | }
184 | return { text: d, value: d };
185 | });
186 | }
187 |
188 | export function mapToTextValueNew(frames: any): MetricFindValue[] {
189 |
190 | if(!frames || !Array.isArray(frames) || frames?.length === 0 ) {
191 | return []
192 | }
193 |
194 | const TextValues: MetricFindValue[] = [];
195 | for(const frame of frames) {
196 | if(frame.data?.values?.length > 0) {
197 | const fieldsValues = frame.data.values
198 | // 如果只有两列 默认第一列为text
199 | if(fieldsValues.length === 2) {
200 | for(let i = 0; i < fieldsValues[0].length; i++) {
201 | TextValues.push({ text: fieldsValues[0][i], value: fieldsValues[1][i] });
202 | }
203 | } else {
204 | for(let rowIndx = 0; rowIndx < fieldsValues[0].length; rowIndx++) {
205 |
206 | // 保存之前的逻辑
207 | const columnBaseItem = fieldsValues[0][rowIndx] // 默认都是用第一项来处理
208 | if(columnBaseItem && columnBaseItem.text && columnBaseItem.value) {
209 | TextValues.push({ text: columnBaseItem.text, value: columnBaseItem.value });
210 | } else if (_.isObject(columnBaseItem)){
211 | TextValues.push({ text: safeJsonStringify(columnBaseItem), value: rowIndx });
212 | } else {
213 | // 把每一项的值拼接起来
214 | const resValue = fieldsValues.reduce((str: string, field: any[]) => str += field[rowIndx] + " " , '')
215 | TextValues.push({ text: resValue?.trim?.(), value: resValue?.trim?.() });
216 | }
217 |
218 | }
219 | }
220 | }
221 | }
222 |
223 | return TextValues
224 | }
225 |
226 | function safeJsonStringify(obj: any) {
227 | try {
228 | return JSON.stringify(obj);
229 | } catch (error) {
230 | return obj;
231 | }
232 | }
233 |
234 | export function replaceFormat(
235 | value: { forEach: (arg0: (v: string) => void) => void; join: (arg0: string) => void },
236 | variable: { multi: any; includeAll: any; name: string; label: any; description: string }
237 | ) {
238 | if (typeof value === 'object' && (variable.multi || variable.includeAll)) {
239 | const a: string[] = [];
240 | value.forEach(function (v: string) {
241 | if (variable.name === variable.label || (variable.description && variable.description.indexOf('field_search') >= 0)) {
242 | a.push('"' + variable.name + '":"' + v + '"');
243 | } else {
244 | a.push(v);
245 | }
246 | });
247 | return a.join(' OR ');
248 | }
249 | if (_.isArray(value)) {
250 | return value.join(' OR ');
251 | }
252 | return value;
253 | }
254 |
255 |
256 | export function replaceQueryParameters(q: SLSQuery | string, options: DataQueryRequest) {
257 | if (typeof q !== 'string' && q.hide) {
258 | return;
259 | }
260 | let varQuery;
261 | if (typeof q === 'string') {
262 | varQuery = q;
263 | } else {
264 | varQuery = q.query;
265 | }
266 | let query = getTemplateSrv().replace(
267 | varQuery,
268 | options.scopedVars,
269 | replaceFormat
270 | );
271 |
272 | const re = /\$([0-9]+)([dmhs])/g;
273 | const reArray = query.match(re);
274 | _(reArray).forEach(function (col) {
275 | const old = col;
276 | col = col.replace('$', '');
277 | let sec = 1;
278 | if (col.indexOf('s') !== -1) {
279 | sec = 1;
280 | } else if (col.indexOf('m') !== -1) {
281 | sec = 60;
282 | } else if (col.indexOf('h') !== -1) {
283 | sec = 3600;
284 | } else if (col.indexOf('d') !== -1) {
285 | sec = 3600 * 24;
286 | }
287 | col = col.replace(/[smhd]/g, '');
288 | let v = parseInt(col, 10);
289 | v = v * sec;
290 | console.log(old, v, col, sec, query);
291 | query = query.replace(old, String(v));
292 | });
293 | if (query.indexOf('#time_end') !== -1) {
294 | query = query.replace('#time_end', String(options.range.to.unix() / 1000));
295 | }
296 | if (query.indexOf('#time_begin') !== -1) {
297 | query = query.replace('#time_begin', String(options.range.from.unix() / 1000));
298 | }
299 | return query;
300 | }
301 |
--------------------------------------------------------------------------------
/src/img/sls_logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aliyun/aliyun-log-grafana-datasource-plugin/1f7b025901a1c9eb9f38facbe075d0ef9cbd13d3/src/img/sls_logo.jpg
--------------------------------------------------------------------------------
/src/module.ts:
--------------------------------------------------------------------------------
1 | import { DataSourcePlugin } from '@grafana/data';
2 | import { SLSDataSource } from './datasource';
3 | import { SLSConfigEditor } from './ConfigEditor';
4 | import { SLSQueryEditor } from './QueryEditor';
5 | import { SLSQuery, SLSDataSourceOptions } from './types';
6 | import { SLSVariableQueryEditorWapper } from './VariableQueryEditor';
7 |
8 | export const plugin = new DataSourcePlugin(SLSDataSource)
9 | .setConfigEditor(SLSConfigEditor)
10 | .setQueryEditor(SLSQueryEditor)
11 | .setVariableQueryEditor(SLSVariableQueryEditorWapper);
12 |
--------------------------------------------------------------------------------
/src/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://raw.githubusercontent.com/grafana/grafana/master/docs/sources/developers/plugins/plugin.schema.json",
3 | "type": "datasource",
4 | "name": "log-service-datasource",
5 | "id": "aliyun-log-service-datasource",
6 | "metrics": true,
7 | "backend": true,
8 | "logs": true,
9 | "alerting": true,
10 | "executable": "gpx_log-service-datasource",
11 | "flow_chart_max_points": "6000000",
12 | "legacy_compatible": false,
13 | "info": {
14 | "description": "Aliyun log service datasource (backend version)",
15 | "author": {
16 | "name": "aliyun-log",
17 | "url": "https://aliyun.com/product/sls"
18 | },
19 | "keywords": [
20 | "sls",
21 | "aliyun",
22 | "log",
23 | "日志服务"
24 | ],
25 | "logos": {
26 | "small": "img/sls_logo.jpg",
27 | "large": "img/sls_logo.jpg"
28 | },
29 | "links": [
30 | {
31 | "name": "Website",
32 | "url": "https://github.com/aliyun/aliyun-log-grafana-datasource-plugin"
33 | },
34 | {
35 | "name": "License",
36 | "url": "https://github.com/aliyun/aliyun-log-grafana-datasource-plugin/blob/master/LICENSE"
37 | }
38 | ],
39 | "screenshots": [],
40 | "version": "%VERSION%",
41 | "updated": "%TODAY%"
42 | },
43 | "dependencies": {
44 | "grafanaDependency": ">=7.0.0",
45 | "plugins": []
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import { DataQuery, DataSourceJsonData } from '@grafana/data';
2 |
3 | export interface SLSQuery extends DataQuery {
4 | query?: string;
5 | xcol?: string;
6 | ycol?: string;
7 | logsPerPage?: number;
8 | currentPage?: number;
9 | type?: string;
10 | logstore?: string
11 | legendFormat?: string // 图例 format
12 | step?: string // promql step
13 | queryType?: 'range' | 'instant'; // 查询类型
14 | totalLogs?: number
15 | powerSql?: boolean
16 | }
17 |
18 | export const defaultQuery: Partial = {
19 | type: 'all',
20 | query: '* | select count(*) as c, __time__-__time__%60 as t group by t',
21 | xcol: 't',
22 | ycol: '',
23 | logsPerPage: 100,
24 | currentPage: 1,
25 | totalLogs: 100,
26 | powerSql: false
27 | };
28 | export const defaultEidtorQuery: Partial = {
29 | type: 'all',
30 | query: '',
31 | xcol: '',
32 | ycol: '',
33 | logsPerPage: 100,
34 | currentPage: 1,
35 | totalLogs: 100,
36 | powerSql: false
37 | };
38 |
39 | /**
40 | * These are options configured for each DataSource instance
41 | */
42 | export interface SLSDataSourceOptions extends DataSourceJsonData {
43 | endpoint?: string;
44 | project?: string;
45 | logstore?: string;
46 | roleArn?: string;
47 | region?: string;
48 | headers?: HeaderWithValue[]
49 | }
50 |
51 | /**
52 | * Value that is used in the backend, but never sent over HTTP to the frontend
53 | */
54 | export interface SLSSecureJsonData {
55 | accessKeyId?: string;
56 | accessKeySecret?: string;
57 | }
58 |
59 | // export declare type SlsLog = {
60 | // time: number;
61 | // attribute: Map;
62 | // name: string;
63 | // };
64 |
65 | export type Header = {
66 | name: string;
67 | configured: boolean;
68 | };
69 |
70 | export type HeaderWithValue = Header & { value: string };
71 |
72 | export type LocalHeader = HeaderWithValue & { id: string };
73 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@grafana/toolkit/src/config/tsconfig.plugin.json",
3 | "include": ["src", "types"],
4 | "compilerOptions": {
5 | "jsx": "react",
6 | "rootDir": "./src",
7 | "baseUrl": "./src",
8 | "typeRoots": ["./node_modules/@types"]
9 | }
10 | }
--------------------------------------------------------------------------------