├── demo.png ├── i18n ├── index.ts └── dict.json ├── .vscode ├── extensions.json └── settings.template.json ├── link.ts ├── .gitignore ├── tsconfig.node.json ├── plugin.json ├── index.sass ├── dev.ts ├── tsconfig.json ├── package.json ├── logo.svg ├── .eslintrc.json ├── README.zh.md ├── README.md ├── LICENSE.txt ├── webpack.ts ├── plugin.schema.json ├── index.tsx ├── DdbCodeEditor.tsx └── ddb.svg /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dolphindb/grafana-datasource/HEAD/demo.png -------------------------------------------------------------------------------- /i18n/index.ts: -------------------------------------------------------------------------------- 1 | import { I18N } from 'xshell/i18n/index.js' 2 | 3 | import _dict from './dict.json' 4 | 5 | const i18n = new I18N(_dict) 6 | 7 | const { t, Trans, language } = i18n 8 | 9 | export { i18n, t, Trans, language } 10 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "MS-CEINTL.vscode-language-pack-zh-hans", 4 | "mhutchie.git-graph", 5 | "eamodio.gitlens", 6 | "syler.sass-indented", 7 | "dbaeumer.vscode-eslint" 8 | ], 9 | "unwantedRecommendations": [] 10 | } -------------------------------------------------------------------------------- /link.ts: -------------------------------------------------------------------------------- 1 | import { assert, flink, fmkdir } from 'xshell' 2 | 3 | import { fpd_out } from './webpack.js' 4 | 5 | await fmkdir(fpd_out) 6 | 7 | const fpd_plugins = process.argv[2] 8 | assert(fpd_plugins, 'pnpm run link 必须传入 grafana 插件目录的路径作为参数') 9 | 10 | await flink(fpd_out, `${fpd_plugins.fpd}dolphindb-datasource/`) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | /out/ 4 | /fonts 5 | 6 | .DS_Store 7 | 8 | /*.js 9 | *.map 10 | *.d.ts 11 | 12 | /docs.zh.json 13 | /docs.en.json 14 | 15 | /i18n/untranslateds.json 16 | /i18n/stats.json 17 | 18 | /.vscode/settings.json 19 | /.vscode/launch.json 20 | 21 | /dolphindb-datasource.*.zip 22 | /grafana.plugin.*.zip 23 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | 4 | "include": [ 5 | "./dev.ts", 6 | "./build.ts", 7 | "./link.ts" 8 | ], 9 | 10 | "exclude": [ 11 | "**/node_modules", 12 | ], 13 | 14 | "compilerOptions": { 15 | "module": "NodeNext", 16 | "moduleResolution": "NodeNext", 17 | "incremental": true, 18 | "tsBuildInfoFile": "./node_modules/.tsbuildinfo.node.json" 19 | }, 20 | } -------------------------------------------------------------------------------- /.vscode/settings.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "files.trimTrailingWhitespace": false, 4 | "editor.trimAutoWhitespace": false, 5 | "editor.formatOnSave": false, 6 | "editor.formatOnPaste": false, 7 | "editor.tabSize": 4, 8 | "editor.codeActionsOnSave": { 9 | "source.fixAll": true, 10 | "source.organizeImports": false 11 | }, 12 | "[typescript][typescriptreact]": { 13 | "editor.defaultFormatter": "vscode.typescript-language-features" 14 | }, 15 | "gitlens.integrations.enabled": true, 16 | "gitlens.remotes": [{ "domain": "dolphindb.net", "type": "GitLab" }], 17 | "git.pruneOnFetch": true, 18 | "git-graph.dialog.fetchRemote.prune": true 19 | } 20 | -------------------------------------------------------------------------------- /plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./plugin.schema.json", 3 | "type": "datasource", 4 | "name": "DolphinDB", 5 | "id": "dolphindb-datasource", 6 | "metrics": true, 7 | "streaming": true, 8 | "info": { 9 | "description": "DolphinDB Grafana Plugin", 10 | "author": { 11 | "name": "DolphinDB", 12 | "url": "https://dolphindb.com" 13 | }, 14 | "keywords": ["DolphinDB"], 15 | "logos": { 16 | "small": "logo.svg", 17 | "large": "logo.svg" 18 | }, 19 | "links": [ 20 | { 21 | "name": "GitHub", 22 | "url": "https://github.com/dolphindb/grafana-datasource" 23 | } 24 | ], 25 | "screenshots": [], 26 | "version": "0.0.1", 27 | "updated": "2022-05-30" 28 | }, 29 | "dependencies": { 30 | "grafanaDependency": ">=7.0.0", 31 | "plugins": [] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /index.sass: -------------------------------------------------------------------------------- 1 | .query-editor-nav-bar 2 | margin-bottom: 8px 3 | margin-top: 8px 4 | 5 | .query-editor 6 | .resizable 7 | > div:first-child 8 | height: 100% 9 | 10 | .monaco-editor 11 | .line-numbers 12 | color: #888888 13 | 14 | .active-line-number.line-numbers 15 | color: #444444 16 | 17 | .editor-tip 18 | color: #888888 19 | 20 | .streaming-editor-content 21 | button 22 | margin-top: 8px 23 | 24 | 25 | .query-editor-content-none 26 | display: none 27 | 28 | 29 | .variable-query-editor 30 | .query-bottom 31 | margin-top: 16px 32 | margin-bottom: 16px 33 | 34 | display: flex 35 | 36 | align-items: center 37 | 38 | .note 39 | flex: 1 40 | 41 | font-size: 13px 42 | 43 | color: #24292e 44 | 45 | 46 | .status 47 | justify-content: right 48 | 49 | display: flex 50 | 51 | align-items: center 52 | 53 | margin-right: 16px 54 | 55 | transition: opacity 0.2s cubic-bezier(0.4, 0.0, 0.2, 1) 56 | 57 | opacity: 0 58 | 59 | user-select: none 60 | 61 | &.visible 62 | opacity: 1 63 | 64 | .icon 65 | margin-right: 4px 66 | margin-top: 1px 67 | 68 | 69 | .button 70 | justify-content: right 71 | 72 | .theme-dark .variable-query-editor .query-bottom 73 | .note, .status 74 | color: #ccccdc 75 | 76 | .version 77 | color: #aaaaaa 78 | margin-top: 20px 79 | -------------------------------------------------------------------------------- /dev.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { fmkdir, Remote, type RemoteReconnectingOptions } from 'xshell' 4 | 5 | import { fpd_out, copy_files, webpack, ramdisk } from './webpack.js' 6 | 7 | await fmkdir(fpd_out) 8 | 9 | await Promise.all([ 10 | copy_files(), 11 | webpack.run(false) 12 | ]) 13 | 14 | 15 | let remote: Remote 16 | 17 | // 监听终端快捷键 18 | // https://stackoverflow.com/a/12506613/7609214 19 | 20 | let { stdin } = process 21 | 22 | stdin.setRawMode(true) 23 | 24 | stdin.resume() 25 | 26 | stdin.setEncoding('utf-8') 27 | 28 | // on any data into stdin 29 | stdin.on('data', async function (key: any) { 30 | // ctrl-c ( end of text ) 31 | if (key === '\u0003') 32 | process.exit() 33 | 34 | // write the key to stdout all normal like 35 | console.log(key) 36 | 37 | switch (key) { 38 | case 'r': 39 | await webpack.run(false) 40 | break 41 | 42 | case 'x': 43 | remote?.disconnect() 44 | await webpack.close() 45 | process.exit() 46 | break 47 | } 48 | }) 49 | 50 | 51 | if (ramdisk) { 52 | const reconnecting_options: RemoteReconnectingOptions = { 53 | func: 'register', 54 | args: ['ddb.gfn'], 55 | on_error (error: Error) { 56 | console.log(error.message) 57 | } 58 | } 59 | 60 | remote = new Remote({ 61 | url: 'ws://localhost', 62 | 63 | funcs: { 64 | async recompile () { 65 | await webpack.run(false) 66 | return [ ] 67 | }, 68 | 69 | async exit () { 70 | remote.disconnect() 71 | await webpack.close() 72 | process.exit() 73 | } 74 | }, 75 | 76 | on_error (error) { 77 | console.log(error.message) 78 | remote.start_reconnecting({ ...reconnecting_options, first_delay: 1000 }) 79 | } 80 | }) 81 | 82 | remote.start_reconnecting(reconnecting_options) 83 | } 84 | 85 | 86 | console.log( 87 | '编译器已启动,快捷键:\n' + 88 | 'r: 重新编译\n' + 89 | 'x: 退出编译器' 90 | ) 91 | 92 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./index.tsx", 4 | 5 | "./dev.ts", 6 | "./build.ts", 7 | "./link.ts", 8 | 9 | "./global.d.ts", 10 | ], 11 | 12 | "compilerOptions": { 13 | // 关闭绝对路径引入 14 | // "baseUrl": ".", 15 | 16 | // --- 用来控制输出文件结构 17 | // "rootDir": "./lib/", 18 | // "outDir": "./build/", 19 | // "outFile": "", 20 | 21 | 22 | // --- module 23 | "module": "ESNext", // none, CommonJS, amd, system, umd, es6, es2015, ESNext 24 | "moduleResolution": "Bundler", 25 | "allowSyntheticDefaultImports": true, 26 | "esModuleInterop": false, 27 | "resolveJsonModule": true, 28 | "isolatedModules": true, 29 | 30 | // --- build 31 | "target": "ESNext", 32 | "allowJs": false, 33 | "checkJs": false, 34 | "pretty": true, 35 | "newLine": "lf", 36 | "lib": ["ESNext", "DOM"], 37 | "importHelpers": true, 38 | "incremental": true, 39 | "tsBuildInfoFile": "./node_modules/.tsbuildinfo.json", 40 | 41 | // --- emit 42 | "declaration": false, 43 | "emitDeclarationOnly": false, 44 | "noEmitOnError": false, 45 | "listEmittedFiles": true, 46 | 47 | // --- source maps 48 | "sourceMap": true, 49 | "inlineSourceMap": false, 50 | "inlineSources": false, 51 | 52 | // --- features 53 | "experimentalDecorators": true, 54 | "emitDecoratorMetadata": true, 55 | "preserveSymlinks": false, 56 | "jsx": "react-jsx", 57 | "removeComments": false, 58 | "preserveConstEnums": true, 59 | "forceConsistentCasingInFileNames": true, 60 | 61 | 62 | // --- type checking 63 | "strict": false, 64 | "alwaysStrict": false, 65 | "noImplicitAny": false, 66 | "noImplicitReturns": false, 67 | "noImplicitThis": true, 68 | "noImplicitOverride": true, 69 | "noUnusedLocals": false, 70 | "noUnusedParameters": false, 71 | "skipLibCheck": true, 72 | } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dolphindb-datasource", 3 | "engines": { 4 | "node": ">=20.3.0" 5 | }, 6 | "type": "module", 7 | "scripts": { 8 | "link": "tsc --project ./tsconfig.node.json && node ./link.js", 9 | "dev": "tsc --project ./tsconfig.node.json && node ./dev.js", 10 | "typecheck": "tsc --noEmit", 11 | "build": "tsc --project ./tsconfig.node.json && node ./build.js", 12 | "scan": "i18n-scan --input \"**/*.{ts,tsx}\"", 13 | "lint": "eslint \"**/*.{ts,tsx}\"", 14 | "fix": "eslint --fix \"**/*.{ts,tsx}\"" 15 | }, 16 | "author": "Hongfei Shen (https://dolphindb.net/hongfeishen)", 17 | "homepage": "https://github.com/dolphindb/grafana-datasource", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/dolphindb/grafana-datasource.git" 21 | }, 22 | "devDependencies": { 23 | "@grafana/data": "^10.4.0", 24 | "@grafana/runtime": "^10.4.0", 25 | "@grafana/schema": "^10.4.0", 26 | "@grafana/ui": "^10.4.0", 27 | "@svgr/webpack": "^8.1.0", 28 | "@types/node": "^20.11.25", 29 | "@types/react": "^18.2.64", 30 | "@types/react-dom": "^18.2.21", 31 | "@types/sass-loader": "^8.0.8", 32 | "@types/webpack-sources": "^3.2.3", 33 | "@typescript-eslint/eslint-plugin": "^7.1.1", 34 | "@typescript-eslint/parser": "^7.1.1", 35 | "css-loader": "^6.10.0", 36 | "eslint": "^8.57.0", 37 | "eslint-plugin-react": "^7.34.0", 38 | "eslint-plugin-xlint": "^1.0.13", 39 | "license-webpack-plugin": "^4.0.2", 40 | "react": "^18.2.0", 41 | "react-dom": "^18.2.0", 42 | "sass": "^1.71.1", 43 | "sass-loader": "^14.1.1", 44 | "source-map": "^0.7.4", 45 | "source-map-loader": "^5.0.0", 46 | "style-loader": "^3.3.4", 47 | "ts-loader": "^9.5.1", 48 | "typescript": "^5.4.2", 49 | "webpack": "^5.90.3", 50 | "webpack-sources": "^3.2.3" 51 | }, 52 | "dependencies": { 53 | "dayjs": "^1.11.10", 54 | "dolphindb": "^2.0.1103", 55 | "monaco-editor": "^0.47.0", 56 | "re-resizable": "^6.9.11", 57 | "rxjs": "7.8.1", 58 | "tslib": "^2.6.2", 59 | "vscode-oniguruma": "^2.0.1", 60 | "vscode-textmate": "^9.0.0", 61 | "xshell": "^1.0.84" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /i18n/dict.json: -------------------------------------------------------------------------------- 1 | { 2 | "自动登录": { 3 | "en": "Autologin" 4 | }, 5 | "是否在建立连接后自动登录,默认 true": { 6 | "en": "Whether to automatically log in after the connection is established, the default is true" 7 | }, 8 | "用户名": { 9 | "en": "Username" 10 | }, 11 | "密码": { 12 | "en": "Password" 13 | }, 14 | "DolphinDB 登录用户名": { 15 | "en": "DolphinDB username" 16 | }, 17 | "DolphinDB 登录密码": { 18 | "en": "DolphinDB password" 19 | }, 20 | "使用 Python Parser 来解释执行脚本, 默认 false": { 21 | "en": "Use Python Parser to interpret and execute scripts, the default is false" 22 | }, 23 | "数据库连接地址 (WebSocket URL), 如: ws://127.0.0.1:8848, wss://dolphindb.com (HTTPS 加密)": { 24 | "en": "Database connection URL (WebSocket URL), e.g. ws://127.0.0.1:8848, wss://dolphindb.com (HTTPS encrypted)" 25 | }, 26 | "DolphinDB Grafana 插件已加载": { 27 | "en": "DolphinDB Grafana plugin loaded" 28 | }, 29 | "已连接到数据库": { 30 | "en": "Connected to database" 31 | }, 32 | "{{message}};\n无法通过 WebSocket 连接到 {{url}},请检查 url, DataSource 配置、网络连接状况 (是否配置代理,代理是否支持 WebSocket)、server 是否启动、server 版本不低于 1.30.16 或 2.00.4": { 33 | "en": "{{message}};\nUnable to connect to {{url}} through WebSocket, please check the url, DataSource configuration, network connection status (whether proxy is configured, whether the proxy supports WebSocket), whether the server is started, and the server version is not lower than 1.30.16 or 2.00.4" 34 | }, 35 | "Query 的返回值不是标量、向量或只含一个向量的表格": { 36 | "en": "Query return value is not a scalar, a vector, or a table containing only one vector" 37 | }, 38 | "通过执行脚本生成变量选项,脚本的最后一条语句应返回标量、向量、或者只含一个向量的表格": { 39 | "en": "Generate variable options by executing the script, the last statement of the script should return a scalar, a vector, or a table containing only one vector" 40 | }, 41 | "暂存查询并更新预览": { 42 | "en": "Stage the query and update the preview" 43 | }, 44 | "Query 代码的最后一条语句需要返回 table,实际返回的是: {{value}}": { 45 | "en": "The last statement of the Query code needs to return a table, the actual return is: {{value}}" 46 | }, 47 | "修改 Query 后请在编辑框内按 Ctrl + S 或点击右侧按钮暂存查询并更新预览": { 48 | "en": "Press Ctrl + S in the edit box or click the button to stage the query and update the preview" 49 | }, 50 | "已暂存查询并更新预览": { 51 | "en": "Query staged and preview updated" 52 | }, 53 | "table 不应该为空": { 54 | "en": "Table should not be empty" 55 | }, 56 | "检测到 ddb 连接已断开,尝试自动重连到:": { 57 | "en": "The DDB connection has been disconnected, and attempt to automatically connected to:" 58 | }, 59 | "在编辑器中按 Ctrl + S 可暂存查询并刷新结果": { 60 | "en": "Press Ctrl + S in the editor to temporarily store and refresh the result" 61 | }, 62 | "需要订阅的流数据表": { 63 | "en": "The streaming data table that needs to be subscribed" 64 | }, 65 | "流数据表": { 66 | "en": "streaming" 67 | }, 68 | "暂存": { 69 | "en": "Temporarily Store" 70 | }, 71 | "脚本": { 72 | "en": "script" 73 | }, 74 | "选择查询类型": { 75 | "en": "Select the query type" 76 | }, 77 | "类型": { 78 | "en": "Type" 79 | }, 80 | "函数文档 {{fname}} 已加载": { 81 | "en": "function documentation {{fname}} loaded" 82 | }, 83 | "暂存查询并更新预览:": { 84 | "en": "Stage the query and update the preview:" 85 | }, 86 | "向下复制行": { 87 | "en": "Copy row down" 88 | }, 89 | "删除行": { 90 | "en": "Delete row" 91 | }, 92 | "插件构建时间:": { 93 | "en": "Plugin Build Time:" 94 | }, 95 | "(需要 v2.10.0 以上的 DolphinDB Server) 使用 Python Parser 来解释执行脚本, 默认 false": { 96 | "en": "(DolphinDB Server version must be above v2.10.0) Use Python Parser to interpret and execute scripts, the default is false" 97 | }, 98 | "检测到 ddb 连接已断开,尝试建立新的连接到:": { 99 | "en": "A broken ddb connection was detected, trying to establish a new connection to:" 100 | }, 101 | "打印调试信息, 默认 false": { 102 | "en": "Print debug information, default false" 103 | }, 104 | "调试信息": { 105 | "en": "Debug Information" 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 资源 1 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | 4 | "ignorePatterns": ["**/*.d.ts"], 5 | 6 | "parser": "@typescript-eslint/parser", 7 | "parserOptions": { 8 | "ecmaVersion": "latest", 9 | "sourceType": "module", 10 | "project": "./tsconfig.json", 11 | "ecmaFeatures": { 12 | "jsx": true 13 | } 14 | }, 15 | 16 | "plugins": [ 17 | "@typescript-eslint", 18 | "react", 19 | "xlint" 20 | ], 21 | 22 | "settings": { 23 | "react": { 24 | "version": "detect" 25 | } 26 | }, 27 | 28 | "env": { 29 | "node": true, 30 | "browser": true 31 | }, 32 | 33 | "rules": { 34 | "xlint/fold-jsdoc-comments": "error", 35 | 36 | // 取代 nonblock-statement-body-position 37 | "xlint/nonblock-statement-body-position-with-indentation": "error", 38 | 39 | "xlint/empty-bracket-spacing": "error", 40 | 41 | // a + b**c 42 | "xlint/space-infix-ops-except-exponentiation": "error", 43 | 44 | "xlint/space-in-for-statement": "error", 45 | 46 | "xlint/jsx-no-redundant-parenthesis-in-return": "error", 47 | 48 | "xlint/keep-indent": "error", 49 | 50 | 51 | "@typescript-eslint/semi": ["error", "never"], 52 | "@typescript-eslint/no-extra-semi": "error", 53 | "semi-style": ["error", "first"], 54 | 55 | // 使用 === 56 | "eqeqeq": "error", 57 | 58 | // 父类尽量返回 this 类型 59 | "@typescript-eslint/prefer-return-this-type": "error", 60 | 61 | // 尽量使用 . 访问属性而不是 [] 62 | "@typescript-eslint/dot-notation": "error", 63 | 64 | // 必须 throw Error 65 | "@typescript-eslint/no-throw-literal": "error", 66 | 67 | // ------------ async 68 | // 返回 Promise 的函数一定要标记为 async 函数 69 | "@typescript-eslint/promise-function-async": "error", 70 | 71 | // 不要 return await promise, 直接 return promise, 除非外面有 try catch 72 | "@typescript-eslint/return-await": "error", 73 | 74 | // ------------ 括号 75 | 76 | // a => { } 而不是 (a) => { } 77 | "arrow-parens": ["error", "as-needed", { "requireForBlockBody": false }], 78 | 79 | // 不要多余的大括号 80 | // if (true) 81 | // console.log() 82 | "curly": ["error", "multi"], 83 | 84 | // 简单属性不要冗余的大括号 85 | "react/jsx-curly-brace-presence": ["error", "never"], 86 | 87 | // ------------ 空格 88 | 89 | // { a, b } 这样的对象,大括号里面要有空格 90 | "@typescript-eslint/object-curly-spacing": ["error", "always"], 91 | 92 | // [a, b, c] 93 | "@typescript-eslint/comma-spacing": "error", 94 | 95 | // foo() 96 | "@typescript-eslint/func-call-spacing": "error", 97 | 98 | // a => { } 中箭头左右两边空格 99 | "arrow-spacing": ["error"], 100 | 101 | // 注释双斜杠后面要有空格 102 | "spaced-comment": ["error", "always", { "markers": ["/"] }], 103 | 104 | // 函数声明中,名称后面要有空格 105 | "@typescript-eslint/space-before-function-paren": "error", 106 | 107 | // { return true } 这样的 block 大括号里面要有空格 108 | "block-spacing": ["error", "always"], 109 | 110 | // aaa: 123 111 | "@typescript-eslint/key-spacing": ["error", { "beforeColon": false, "afterColon": true, "mode": "minimum" }], 112 | 113 | // aaa: string 114 | "@typescript-eslint/type-annotation-spacing": "error", 115 | 116 | // if () 117 | "@typescript-eslint/keyword-spacing": ["error", { "before": true, "after": true }], 118 | 119 | // if (1) { } 120 | "@typescript-eslint/space-before-blocks": "error", 121 | 122 | // case 1: ... 123 | "switch-colon-spacing": "error", 124 | 125 | // 126 | "react/jsx-equals-spacing": ["error", "never"], 127 | 128 | // 不允许使用 tab 129 | "no-tabs": "error", 130 | 131 | // 使用 \n 换行 132 | "linebreak-style": ["error", "unix"], 133 | 134 | // 文件以 \n 结尾 135 | "eol-last": ["error", "always"], 136 | 137 | // ------------ 引号 138 | 139 | // 用单引号 140 | "jsx-quotes": ["error", "prefer-single"], 141 | 142 | // 用单引号 143 | "quotes": ["error", "single", { "avoidEscape": true, "allowTemplateLiterals": false }], 144 | 145 | // 不要冗余的引号包裹属性 146 | "quote-props": ["error", "as-needed", { "keywords": false, "unnecessary": true }], 147 | 148 | // ------------ 其它 149 | // boolean 属性不要冗余的 ={true} 150 | "react/jsx-boolean-value": ["error", "never"], 151 | 152 | // 没有 children 的 Component 写成闭合标签 153 | "react/self-closing-comp": "error", 154 | 155 | // 单行类型声明用 `,` 分割,多行类型声明结尾不要加分号 156 | "@typescript-eslint/member-delimiter-style": ["error", { 157 | "multiline": { 158 | "delimiter": "none", 159 | "requireLast": false 160 | }, 161 | "singleline": { 162 | "delimiter": "comma", 163 | "requireLast": false 164 | } 165 | }], 166 | 167 | "@typescript-eslint/prefer-includes": "error", 168 | 169 | "@typescript-eslint/prefer-regexp-exec": "error" 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | # DolphinDB Grafana DataSource Plugin 2 | 3 |

4 | DolphinDB Grafana DataSource 5 |

6 | 7 |

8 | 9 | vscode extension installs 10 | 11 |

12 | 13 | ## [English](./README.md) | 中文 14 | 15 | Grafana 是一个开源的数据可视化 Web 应用程序,擅长动态展示时序数据,支持多种数据源。用户通过配置连接的数据源,以及编写查询脚本,可在浏览器里显示数据图表 16 | 17 | DolphinDB 开发了 Grafana 数据源插件 (dolphindb-datasource),让用户在 Grafana 面板 (dashboard) 上通过编写查询脚本、订阅流数据表的方式,与 DolphinDB 进行交互 (基于 WebSocket),实现 DolphinDB 时序数据的可视化 18 | 19 | 20 | 21 | ## 安装方法 22 | #### 1. 安装 Grafana 23 | 前往 Grafana 官网: https://grafana.com/oss/grafana/ 下载并安装最新的开源版本 (OSS, Open-Source Software) 24 | 25 | #### 2. 安装 dolphindb-datasource 插件 26 | 在 [releases](https://github.com/dolphindb/grafana-datasource/releases) 中下载最新版本的插件压缩包,如 `dolphindb-datasource.v2.0.900.zip` 27 | 28 | 将压缩包中的 dolphindb-datasource 文件夹解压到以下路径: 29 | - Windows: `/data/plugins/` 30 | - Linux: `/var/lib/grafana/plugins/` 31 | 32 | 如果不存在 plugins 这一层目录,可以手动创建该文件夹 33 | 34 | #### 3. 修改 Grafana 配置文件,使其允许加载未签名的 dolphindb-datasource 插件 35 | 阅读 https://grafana.com/docs/grafana/latest/administration/configuration/#configuration-file-location 36 | 打开并编辑配置文件: 37 | 38 | 在 `[plugins]` 部分下面取消注释 `allow_loading_unsigned_plugins`,并配置为 `dolphindb-datasource`,即把下面的 39 | ```ini 40 | # Enter a comma-separated list of plugin identifiers to identify plugins to load even if they are unsigned. Plugins with modified signatures are never loaded. 41 | ;allow_loading_unsigned_plugins = 42 | ``` 43 | 改为 44 | ```ini 45 | # Enter a comma-separated list of plugin identifiers to identify plugins to load even if they are unsigned. Plugins with modified signatures are never loaded. 46 | allow_loading_unsigned_plugins = dolphindb-datasource 47 | ``` 48 | 49 | 注:每次修改配置项后,需重启 Grafana 50 | 51 | #### 4. 重启 Grafana 进程或服务 52 | 打开任务管理器 > 服务 > 找到 Grafana 服务 > 右键重启 53 | 54 | https://grafana.com/docs/grafana/latest/installation/restart-grafana/ 55 | 56 | 57 | ### 验证已加载插件 58 | 在 Grafana 启动日志中可以看到类似以下的日志 59 | ```log 60 | WARN [05-19|12:05:48] Permitting unsigned plugin. This is not recommended logger=plugin.signature.validator pluginID=dolphindb-datasource pluginDir=/data/plugins/dolphindb-datasource 61 | ``` 62 | 63 | 日志文件路径: 64 | - Windows: `/data/log/grafana.log` 65 | - Linux: `/var/log/grafana/grafana.log` 66 | 67 | 或者访问下面的链接,看到页面中 DolphinDB 插件是 Installed 状态: 68 | http://localhost:3000/admin/plugins?filterBy=all&filterByType=all&q=dolphindb 69 | 70 | 71 | 72 | ## 使用方法 73 | ### 1. 打开并登录 Grafana 74 | 打开 http://localhost:3000 75 | 初始登入名以及密码均为 admin 76 | 77 | ### 2. 新建 DolphinDB 数据源 78 | 打开 http://localhost:3000/datasources ,或点击左侧导航的 `Configuration > Data sources` 添加数据源,搜索并选择 dolphindb,配置数据源后点 `Save & Test` 保存数据源 79 | 80 | 注: 2022 年及之后的插件使用 WebSocket 协议与 DolphinDB 数据库通信,因此数据源配置中的 URL 需要以 `ws://` 或者 `wss://` 开头 81 | 82 | ### 3. 新建 Panel,通过编写查询脚本或订阅流数据表,可视化 DolphinDB 时序数据 83 | 打开或新建 Dashboard,编辑或新建 Panel,在 Panel 的 Data source 属性中选择上一步添加的数据源 84 | #### 3.1. 编写脚本执行查询,可视化返回的时序表格 85 | 1. 将 query 类型设置为 `脚本` 86 | 2. 编写查询脚本,代码的最后一条语句需要返回 table 87 | 3. 编写完成后按 `Ctrl + S` 保存,或者点击页面中的刷新按钮 (Refresh dashboard),可以将 Query 发到 DolphinDB 数据库运行并展示出图表 88 | 4. 代码编辑框的高度通过拖动底部边框进行调整 89 | 5. 点击右上角的保存 `Save` 按钮,保存 panel 配置 90 | 91 | dolphindb-datasource 插件支持变量,比如: 92 | - `$__timeFilter` 变量: 值为面板上方的时间轴区间,比如当前的时间轴区间是 `2022-02-15 00:00:00 - 2022.02.17 00:00:00` ,那么代码中的 `$__timeFilter` 会被替换为 `pair(2022.02.15 00:00:00.000, 2022.02.17 00:00:00.000)` 93 | - `$__interval` 和 `$__interval_ms` 变量: 值为 Grafana 根据时间轴区间长度和屏幕像素点自动计算的时间分组间隔。`$__interval` 会被替换为 DolphinDB 中对应的 DURATION 类型; `$__interval_ms` 会被替换为毫秒数 (整型) 94 | - query 变量: 通过 SQL 查询生成动态值或选项列表 95 | 96 | 更多变量请查看 https://grafana.com/docs/grafana/latest/variables/ 97 | 98 | 99 | 要查看代码中 `print('xxx')` 输出的消息,或者变量替换 (插值) 后的代码,可以按 `F12` 或 `Ctrl + Shift + I` 或 `右键 > 检查` 打开浏览器的开发者调试工具 (devtools), 切换到控制台 (Console) 面板中查看 100 | 101 | #### 3.2. 订阅并实时可视化 DolphinDB 中的流数据表 102 | 要求:DolphinDB server 版本不低于 2.00.9 或 1.30.21 103 | 1. 将 query 类型设置为 `流数据表` 104 | 2. 填写要订阅的流数据表表名 105 | 3. 点击暂存按钮 106 | 4. 将时间范围改成 `Last 5 minutes` (需要包含当前时间,如 Last x hour/minutes/seconds,而不是历史时间区间,否则看不到数据) 107 | 5. 点击右上角的保存 `Save` 按钮,保存 panel 配置 108 | 109 | ### 4. 参考文档学习 Grafana 使用 110 | https://grafana.com/docs/grafana/latest/ 111 | 112 | ### FAQ 113 | Q: 如何设置 dashboard 自动刷新间隔? 114 | A: 115 | 对于脚本类型,打开 dashboard, 在右上角刷新按钮右侧点击下拉框选择自动刷新间隔 116 | 对于流数据表类型,数据是实时的,无需设置。没有新的数据更新发生时,连接会关闭;如有新的数据更新发生,连接会重新建立。 117 | 118 | 如果需要自定义刷新间隔,可以打开 `dashboard settings > Time options > Auto refresh`, 输入自定义的间隔 119 | 如果需要定义比 5s 更小的刷新间隔,比如 1s,需要按下面的方法操作: 120 | 修改 Grafana 配置文件 121 | ```ini 122 | [dashboards] 123 | min_refresh_interval = 1s 124 | ``` 125 | 修改完后重启 Grafana 126 | (参考: https://community.grafana.com/t/how-to-change-refresh-rate-from-5s-to-1s/39008/2) 127 | 128 | 129 | ## 构建及开发方法 130 | ```shell 131 | # 安装最新版的 nodejs 132 | # https://nodejs.org/en/download/current/ 133 | 134 | # 安装 pnpm 包管理器 135 | corepack enable 136 | corepack prepare pnpm@latest --activate 137 | 138 | git clone https://github.com/dolphindb/grafana-datasource.git 139 | 140 | cd grafana-datasource 141 | 142 | # 安装项目依赖 143 | pnpm install 144 | 145 | # 将 .vscode/settings.template.json 复制为 .vscode/settings.json 146 | cp .vscode/settings.template.json .vscode/settings.json 147 | 148 | # 参考 package.json 中的 scripts 149 | 150 | # 链接项目构建后的输出文件夹到 grafana 的插件目录下 (clone 项目后链接一次即可) 151 | # 传入的参数为安装的 grafana 的插件目录 152 | # - Windows: `/data/plugins/` 153 | # - Linux: `/var/lib/grafana/plugins/` 154 | pnpm run link E:/sdk/grafana/data/plugins/ 155 | 156 | # 开发 157 | pnpm run dev 158 | 159 | # 重启 grafana 160 | 161 | # 扫描词条 162 | pnpm run scan 163 | # 手动补全未翻译词条 164 | # 再次运行扫描以更新词典文件 dict.json 165 | pnpm run scan 166 | 167 | # lint 168 | pnpm run lint 169 | 170 | # lint fix 171 | pnpm run fix 172 | 173 | # 构建 174 | npm run build 175 | # 完成后产物在 out 文件夹中。将 out 重命名为 dolphindb-datasource 后压缩为 .zip 即可 176 | ``` 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DolphinDB Grafana DataSource Plugin 2 | 3 |

4 | DolphinDB Grafana DataSource 5 |

6 | 7 |

8 | 9 | vscode extension installs 10 | 11 |

12 | 13 | ## English | [中文](./README.zh.md) 14 | 15 | Grafana is an open source data visualization web application that is good at dynamically displaying time series data and supports multiple data sources. Users can display data graphs in the browser through Grafana by configuring the connected data source and writing query scripts 16 | 17 | DolphindB has developed Grafana data source plug-in (DolphindB-Datasource), allowing users to interact with DolphinDB by writing query scripts and subscribing streaming data tables on the Grafana panel. 18 | 19 | 20 | 21 | ## Installation 22 | #### 1. Install Grafana 23 | Go to Grafana official website: https://grafana.com/oss/grafana/ , install the latest open source version (OSS, Open-Source Software) 24 | 25 | #### 2. Install the dolphindb-datasource plugin 26 | In Releases (https://github.com/dolphindb/grafana-datasource/releases) download the latest version of the plugin zip, such as `dolphindb-datasource.v2.0.900.zip` 27 | 28 | Unzip the dolphindb-datasource folder in the compressed package to the plugin directory of grafana: 29 | - Windows: `/data/plugins/` 30 | - Linux: 31 | - grafana is obtained by decompressing the zip archive: `/data/plugins/` 32 | - grafana is installed via the package manager: `/var/lib/grafana/plugins/` 33 | 34 | If the plugins level directory does not exist, you can manually create this folder 35 | 36 | #### 3. Modify the Grafana configuration file so that it allows to load the unsigned dolphindb-datasource plugin 37 | Read the following documents to open and edit configuration files 38 | https://grafana.com/docs/grafana/latest/administration/configuration/#configuration-file-location 39 | 40 | Uncomment `allow_loading_unsigned_plugins` under the `[plugins]` section and configure it as `dolphindb-datasource`, i.e. put the following 41 | ```ini 42 | # Enter a comma-separated list of plugin identifiers to identify plugins to load even if they are unsigned. Plugins with modified signatures are never loaded. 43 | ;allow_loading_unsigned_plugins = 44 | ``` 45 | changed to 46 | ```ini 47 | # Enter a comma-separated list of plugin identifiers to identify plugins to load even if they are unsigned. Plugins with modified signatures are never loaded. 48 | allow_loading_unsigned_plugins = dolphindb-datasource 49 | ``` 50 | 51 | Note: Grafana needs to be restarted every time a configuration item is modified 52 | 53 | ### 4. Restart the Grafana process or service 54 | Open Task Manager > Services > Find Grafana Service > Right Click Restart 55 | 56 | https://grafana.com/docs/grafana/latest/installation/restart-grafana/ 57 | 58 | 59 | ### Verify that the plugin is loaded 60 | You can see a log similar to the following in the grafana startup log 61 | ````log 62 | WARN [05-19|12:05:48] Permitting unsigned plugin. This is not recommended logger=plugin.signature.validator pluginID=dolphindb-datasource pluginDir=/data/plugins/dolphindb-datasource 63 | ```` 64 | 65 | The log file path might be: 66 | - Windows: `/data/log/grafana.log` 67 | - Linux: `/var/log/grafana/grafana.log` 68 | 69 | Or visit the link below, you can see that the DolphinDB plugin on the page is in the Installed state 70 | http://localhost:3000/admin/plugins?filterBy=all&filterByType=all&q=dolphindb 71 | 72 | 73 | 74 | ## Instructions 75 | ### 1. Open and log in to Grafana 76 | Open http://localhost:3000 77 | The initial login name and password are both admin 78 | 79 | ### 2. Create a new DolphinDB data source 80 | Open http://localhost:3000/datasources or click `Configuration > Data sources` in the left navigation to add a data source, filter and search for dolphindb, configure the data source and click `Save & Test` to save the data source 81 | 82 | Note: The new version of the plugin uses the WebSocket protocol to communicate with the DolphinDB database. The URL needs to start with `ws://` or `wss://` in the database configuration. Users upgrading from the old version of the plugin need to change the database URL from `http://` ` or `https://` to `ws://` or `wss://` 83 | 84 | ### 3. Create a new Panel, writing query scripts or subscribing streaming data tables to visualize DolphinDB's time-series data 85 | Open or create new Dashboard, edit or create a new Panel, select the data source added to the Panel's data source attribute 86 | #### 3.1. Write the script to execute the query to visulize the time-series table returned 87 | 1. Set the query type to `script` 88 | 2. Write a query script, the last statement of the code needs to return a table 89 | 3. After writing, press `Ctrl + S` to save, or click the refresh button on the page (Refresh dashboard), you can send the Query to the DolphinDB database to run and display the chart 90 | 4. The height of the code editing box can be adjusted by dragging the bottom 91 | 5. Click to save the `Save` button in the upper right corner to save the Panel configuration 92 | 93 | The dolphindb-datasource plugin supports variables, such as: 94 | - `$__timeFilter` variable: the value is the time axis interval above the panel. For example, the current time axis interval is `2022-02-15 00:00:00 - 2022.02.17 00:00:00` , then the ` $__timeFilter` will be replaced with `pair(2022.02.15 00:00:00.000, 2022.02.17 00:00:00.000)` 95 | - `$__interval` and `$__interval_ms` variables: the value is the time grouping interval automatically calculated by grafana based on the length of the time axis interval and the screen pixels. `$__interval` will be replaced with the corresponding duration type in DolphinDB; `$__interval_ms` will be replaced with the number of milliseconds (integer) 96 | - query variable: Generate dynamic value or list of options via SQL query 97 | 98 | For more variables see https://grafana.com/docs/grafana/latest/variables/ 99 | 100 | 101 | To view the message output by `print('xxx')` in the code, or the code after variable substitution (interpolation), you can press `F12` or `Ctrl + Shift + I` or `Right click > Inspect` to open the browser development Or debug tools (devtools), switch to the console (Console) panel to view 102 | 103 | #### 3.2. Subscribe to and visualize the streaming data table in DolphinDB 104 | Requirements: DolphinDB Server version is not less than 2.00.9 or 1.30.21 105 | 1. Set the query type to `streaming` 106 | 2. Fill in the streaming data table name to be subscribed to 107 | 3. Click the `Temporarily Store` button 108 | 4. Change the time range to `Last 5 Minutes` (need to include the current time, such as Last x Hour/Minutes/Seconds instead of the historical time interval, otherwise you will not see the data) 109 | 5. Click to save the `Save` button in the upper right corner to save the Panel configuration 110 | 111 | ### 4. Learn how to use Grafana by referring to the documentation 112 | https://grafana.com/docs/grafana/latest/ 113 | 114 | 115 | ### FAQ 116 | Q: How to set the automatic refresh interval of the dashboard? 117 | A: 118 | For the type of script, open Dashboard, and refresh the right side to the right side of the right corner to click the drop -down box to select the automatic refresh interval 119 | For stream data table types, the data is real-time, no settings are required. When no new data is updated, the connection will be closed; otherwise, the connection will be re-established. 120 | 121 | If you need to customize the refresh interval, you can open `dashboard settings > Time options > Auto refresh`, enter a custom interval 122 | 123 | If you need a refresh interval smaller than 5s, such as 1s, you need to do the following: 124 | Modify the grafana configuration file 125 | ````ini 126 | [dashboards] 127 | min_refresh_interval = 1s 128 | ```` 129 | Restart grafana after modification 130 | (Reference: https://community.grafana.com/t/how-to-change-refresh-rate-from-5s-to-1s/39008/2) 131 | 132 | 133 | ## Build and development 134 | ```shell 135 | # Install the latest version of nodejs 136 | # https://nodejs.org/en/download/current/ 137 | 138 | # Install the pnpm package manager 139 | corepack enable 140 | corepack prepare pnpm@latest --activate 141 | 142 | git clone https://github.com/dolphindb/grafana-datasource.git 143 | 144 | cd grafana-datasource 145 | 146 | # Install project dependencies 147 | pnpm install 148 | 149 | # Copy .vscode/settings.template.json to .vscode/settings.json 150 | cp .vscode/settings.template.json .vscode/settings.json 151 | 152 | # Refer to scripts in package.json 153 | 154 | # Link the output folder after the project is built to the plug-in directory of grafana (just link it once after cloning the project) 155 | # The parameter passed in is the plugin directory of the installed grafana 156 | # - Windows: `/data/plugins/` 157 | # - Linux: `/var/lib/grafana/plugins/` 158 | pnpm run link E:/sdk/grafana/data/plugins/ 159 | 160 | # development 161 | pnpm run dev 162 | 163 | # restart grafana 164 | 165 | # scan entries 166 | pnpm run scan 167 | # Manually complete untranslated entries 168 | # Run the scan again to update the dictionary file dict.json 169 | pnpm run scan 170 | 171 | #lint 172 | pnpm run lint 173 | 174 | #lint fix 175 | pnpm run fix 176 | 177 | # Construct 178 | npm run build 179 | # After completion, the product is in the out folder. Rename out to dolphindb-datasource and compress it to .zip 180 | ``` 181 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2016-2023 DolphinDB, Inc. 2 | 3 | 4 | Apache License 5 | Version 2.0, January 2004 6 | http://www.apache.org/licenses/ 7 | 8 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 9 | 10 | 1. Definitions. 11 | 12 | "License" shall mean the terms and conditions for use, reproduction, 13 | and distribution as defined by Sections 1 through 9 of this document. 14 | 15 | "Licensor" shall mean the copyright owner or entity authorized by 16 | the copyright owner that is granting the License. 17 | 18 | "Legal Entity" shall mean the union of the acting entity and all 19 | other entities that control, are controlled by, or are under common 20 | control with that entity. For the purposes of this definition, 21 | "control" means (i) the power, direct or indirect, to cause the 22 | direction or management of such entity, whether by contract or 23 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 24 | outstanding shares, or (iii) beneficial ownership of such entity. 25 | 26 | "You" (or "Your") shall mean an individual or Legal Entity 27 | exercising permissions granted by this License. 28 | 29 | "Source" form shall mean the preferred form for making modifications, 30 | including but not limited to software source code, documentation 31 | source, and configuration files. 32 | 33 | "Object" form shall mean any form resulting from mechanical 34 | transformation or translation of a Source form, including but 35 | not limited to compiled object code, generated documentation, 36 | and conversions to other media types. 37 | 38 | "Work" shall mean the work of authorship, whether in Source or 39 | Object form, made available under the License, as indicated by a 40 | copyright notice that is included in or attached to the work 41 | (an example is provided in the Appendix below). 42 | 43 | "Derivative Works" shall mean any work, whether in Source or Object 44 | form, that is based on (or derived from) the Work and for which the 45 | editorial revisions, annotations, elaborations, or other modifications 46 | represent, as a whole, an original work of authorship. For the purposes 47 | of this License, Derivative Works shall not include works that remain 48 | separable from, or merely link (or bind by name) to the interfaces of, 49 | the Work and Derivative Works thereof. 50 | 51 | "Contribution" shall mean any work of authorship, including 52 | the original version of the Work and any modifications or additions 53 | to that Work or Derivative Works thereof, that is intentionally 54 | submitted to Licensor for inclusion in the Work by the copyright owner 55 | or by an individual or Legal Entity authorized to submit on behalf of 56 | the copyright owner. For the purposes of this definition, "submitted" 57 | means any form of electronic, verbal, or written communication sent 58 | to the Licensor or its representatives, including but not limited to 59 | communication on electronic mailing lists, source code control systems, 60 | and issue tracking systems that are managed by, or on behalf of, the 61 | Licensor for the purpose of discussing and improving the Work, but 62 | excluding communication that is conspicuously marked or otherwise 63 | designated in writing by the copyright owner as "Not a Contribution." 64 | 65 | "Contributor" shall mean Licensor and any individual or Legal Entity 66 | on behalf of whom a Contribution has been received by Licensor and 67 | subsequently incorporated within the Work. 68 | 69 | 2. Grant of Copyright License. Subject to the terms and conditions of 70 | this License, each Contributor hereby grants to You a perpetual, 71 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 72 | copyright license to reproduce, prepare Derivative Works of, 73 | publicly display, publicly perform, sublicense, and distribute the 74 | Work and such Derivative Works in Source or Object form. 75 | 76 | 3. Grant of Patent License. Subject to the terms and conditions of 77 | this License, each Contributor hereby grants to You a perpetual, 78 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 79 | (except as stated in this section) patent license to make, have made, 80 | use, offer to sell, sell, import, and otherwise transfer the Work, 81 | where such license applies only to those patent claims licensable 82 | by such Contributor that are necessarily infringed by their 83 | Contribution(s) alone or by combination of their Contribution(s) 84 | with the Work to which such Contribution(s) was submitted. If You 85 | institute patent litigation against any entity (including a 86 | cross-claim or counterclaim in a lawsuit) alleging that the Work 87 | or a Contribution incorporated within the Work constitutes direct 88 | or contributory patent infringement, then any patent licenses 89 | granted to You under this License for that Work shall terminate 90 | as of the date such litigation is filed. 91 | 92 | 4. Redistribution. You may reproduce and distribute copies of the 93 | Work or Derivative Works thereof in any medium, with or without 94 | modifications, and in Source or Object form, provided that You 95 | meet the following conditions: 96 | 97 | (a) You must give any other recipients of the Work or 98 | Derivative Works a copy of this License; and 99 | 100 | (b) You must cause any modified files to carry prominent notices 101 | stating that You changed the files; and 102 | 103 | (c) You must retain, in the Source form of any Derivative Works 104 | that You distribute, all copyright, patent, trademark, and 105 | attribution notices from the Source form of the Work, 106 | excluding those notices that do not pertain to any part of 107 | the Derivative Works; and 108 | 109 | (d) If the Work includes a "NOTICE" text file as part of its 110 | distribution, then any Derivative Works that You distribute must 111 | include a readable copy of the attribution notices contained 112 | within such NOTICE file, excluding those notices that do not 113 | pertain to any part of the Derivative Works, in at least one 114 | of the following places: within a NOTICE text file distributed 115 | as part of the Derivative Works; within the Source form or 116 | documentation, if provided along with the Derivative Works; or, 117 | within a display generated by the Derivative Works, if and 118 | wherever such third-party notices normally appear. The contents 119 | of the NOTICE file are for informational purposes only and 120 | do not modify the License. You may add Your own attribution 121 | notices within Derivative Works that You distribute, alongside 122 | or as an addendum to the NOTICE text from the Work, provided 123 | that such additional attribution notices cannot be construed 124 | as modifying the License. 125 | 126 | You may add Your own copyright statement to Your modifications and 127 | may provide additional or different license terms and conditions 128 | for use, reproduction, or distribution of Your modifications, or 129 | for any such Derivative Works as a whole, provided Your use, 130 | reproduction, and distribution of the Work otherwise complies with 131 | the conditions stated in this License. 132 | 133 | 5. Submission of Contributions. Unless You explicitly state otherwise, 134 | any Contribution intentionally submitted for inclusion in the Work 135 | by You to the Licensor shall be under the terms and conditions of 136 | this License, without any additional terms or conditions. 137 | Notwithstanding the above, nothing herein shall supersede or modify 138 | the terms of any separate license agreement you may have executed 139 | with Licensor regarding such Contributions. 140 | 141 | 6. Trademarks. This License does not grant permission to use the trade 142 | names, trademarks, service marks, or product names of the Licensor, 143 | except as required for reasonable and customary use in describing the 144 | origin of the Work and reproducing the content of the NOTICE file. 145 | 146 | 7. Disclaimer of Warranty. Unless required by applicable law or 147 | agreed to in writing, Licensor provides the Work (and each 148 | Contributor provides its Contributions) on an "AS IS" BASIS, 149 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 150 | implied, including, without limitation, any warranties or conditions 151 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 152 | PARTICULAR PURPOSE. You are solely responsible for determining the 153 | appropriateness of using or redistributing the Work and assume any 154 | risks associated with Your exercise of permissions under this License. 155 | 156 | 8. Limitation of Liability. In no event and under no legal theory, 157 | whether in tort (including negligence), contract, or otherwise, 158 | unless required by applicable law (such as deliberate and grossly 159 | negligent acts) or agreed to in writing, shall any Contributor be 160 | liable to You for damages, including any direct, indirect, special, 161 | incidental, or consequential damages of any character arising as a 162 | result of this License or out of the use or inability to use the 163 | Work (including but not limited to damages for loss of goodwill, 164 | work stoppage, computer failure or malfunction, or any and all 165 | other commercial damages or losses), even if such Contributor 166 | has been advised of the possibility of such damages. 167 | 168 | 9. Accepting Warranty or Additional Liability. While redistributing 169 | the Work or Derivative Works thereof, You may choose to offer, 170 | and charge a fee for, acceptance of support, warranty, indemnity, 171 | or other liability obligations and/or rights consistent with this 172 | License. However, in accepting such obligations, You may act only 173 | on Your own behalf and on Your sole responsibility, not on behalf 174 | of any other Contributor, and only if You agree to indemnify, 175 | defend, and hold each Contributor harmless for any liability 176 | incurred by, or claims asserted against, such Contributor by reason 177 | of your accepting any such warranty or additional liability. 178 | 179 | END OF TERMS AND CONDITIONS 180 | 181 | APPENDIX: How to apply the Apache License to your work. 182 | 183 | To apply the Apache License to your work, attach the following 184 | boilerplate notice, with the fields enclosed by brackets "[]" 185 | replaced with your own identifying information. (Don't include 186 | the brackets!) The text should be enclosed in the appropriate 187 | comment syntax for the file format. We also recommend that a 188 | file or class name and description of purpose be included on the 189 | same "printed page" as the copyright notice for easier 190 | identification within third-party archives. 191 | 192 | Copyright 2016-2023 DolphinDB, Inc. 193 | 194 | Licensed under the Apache License, Version 2.0 (the "License"); 195 | you may not use this file except in compliance with the License. 196 | You may obtain a copy of the License at 197 | 198 | http://www.apache.org/licenses/LICENSE-2.0 199 | 200 | Unless required by applicable law or agreed to in writing, software 201 | distributed under the License is distributed on an "AS IS" BASIS, 202 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 203 | See the License for the specific language governing permissions and 204 | limitations under the License. 205 | -------------------------------------------------------------------------------- /webpack.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'url' 2 | 3 | import dayjs from 'dayjs' 4 | 5 | import { default as Webpack, type Compiler, type Configuration, type Stats } from 'webpack' 6 | 7 | import type { RawSourceMap } from 'source-map' 8 | 9 | // 需要分析 bundle 大小时开启 10 | // import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer' 11 | 12 | import type { Options as TSLoaderOptions } from 'ts-loader' 13 | 14 | import type { Options as SassOptions } from 'sass-loader' 15 | 16 | 17 | import { fcopy, fexists, fread, fwrite, Lock } from 'xshell' 18 | 19 | 20 | export const fpd_root = fileURLToPath(import.meta.url).fdir 21 | 22 | export const ramdisk = fexists('T:/TEMP/', { print: false }) 23 | export const fpd_ramdisk_root = 'T:/2/ddb/gfn/' 24 | 25 | export const fpd_out = `${ramdisk ? fpd_ramdisk_root : fpd_root}out/` 26 | 27 | 28 | export async function copy_files () { 29 | await Promise.all([ 30 | ... ([ 31 | 'plugin.json', 32 | 'logo.svg', 33 | 'demo.png', 34 | 'ddb.svg', 35 | 'README.zh.md' 36 | ] as const).map(async fname => 37 | fcopy(fpd_root + fname, fpd_out + fname) 38 | ), 39 | fwrite( 40 | `${fpd_out}README.md`, 41 | (await fread(`${fpd_root}README.md`)) 42 | .replaceAll('./README.zh.md', 'https://github.com/dolphindb/grafana-datasource/blob/main/README.zh.md') 43 | .replaceAll('./demo.png', '/public/plugins/dolphindb-datasource/demo.png') 44 | .replaceAll('./ddb.svg', '/public/plugins/dolphindb-datasource/ddb.svg') 45 | ), 46 | ... (['zh', 'en']).map(async language => 47 | fcopy(`${fpd_root}node_modules/dolphindb/docs.${language}.json`, `${fpd_out}docs.${language}.json`, { overwrite: true })), 48 | fcopy(`${fpd_root}node_modules/vscode-oniguruma/release/onig.wasm`, `${fpd_out}onig.wasm`), 49 | ]) 50 | } 51 | 52 | 53 | async function get_config (production: boolean): Promise { 54 | const sass = await import('sass') 55 | 56 | return { 57 | name: 'gfn', 58 | 59 | mode: production ? 'production' : 'development', 60 | 61 | devtool: 'source-map', 62 | 63 | entry: { 64 | 'module.js': './index.tsx', 65 | }, 66 | 67 | experiments: { 68 | outputModule: true, 69 | }, 70 | 71 | target: ['web', 'es2023'], 72 | 73 | output: { 74 | path: fpd_out, 75 | filename: '[name]', 76 | publicPath: '/', 77 | pathinfo: true, 78 | globalObject: 'globalThis', 79 | module: false, 80 | // grafana 插件会被 SystemJS 加载,最后需要编译生成 define(['依赖'], function (dep) { }) 这样的格式 81 | library: { 82 | type: 'amd' 83 | }, 84 | }, 85 | 86 | // externalsType: 'global', 87 | 88 | externals: [ 89 | 'react', 90 | 'react-dom', 91 | '@grafana/runtime', 92 | '@grafana/data', 93 | '@grafana/ui', 94 | ], 95 | 96 | 97 | resolve: { 98 | extensions: ['.js'], 99 | 100 | symlinks: true, 101 | 102 | plugins: [{ 103 | apply (resolver) { 104 | const target = resolver.ensureHook('file') 105 | 106 | for (const extension of ['.ts', '.tsx'] as const) 107 | resolver.getHook('raw-file').tapAsync('ResolveTypescriptPlugin', (request, ctx, callback) => { 108 | if ( 109 | typeof request.path !== 'string' || 110 | /(^|[\\/])node_modules($|[\\/])/.test(request.path) 111 | ) { 112 | callback() 113 | return 114 | } 115 | 116 | if (request.path.endsWith('.js')) { 117 | const path = request.path.slice(0, -3) + extension 118 | 119 | resolver.doResolve( 120 | target, 121 | { 122 | ...request, 123 | path, 124 | relativePath: request.relativePath?.replace(/\.js$/, extension) 125 | }, 126 | `using path: ${path}`, 127 | ctx, 128 | callback 129 | ) 130 | } else 131 | callback() 132 | }) 133 | } 134 | }] 135 | }, 136 | 137 | 138 | module: { 139 | rules: [ 140 | { 141 | test: /\.js$/, 142 | enforce: 'pre', 143 | use: ['source-map-loader'], 144 | }, 145 | { 146 | test: /\.tsx?$/, 147 | exclude: /node_modules/, 148 | loader: 'ts-loader', 149 | // https://github.com/TypeStrong/ts-loader 150 | options: { 151 | configFile: `${fpd_root}tsconfig.json`, 152 | onlyCompileBundledFiles: true, 153 | transpileOnly: true, 154 | } as Partial 155 | }, 156 | { 157 | test: /\.s[ac]ss$/, 158 | use: [ 159 | 'style-loader', 160 | { 161 | // https://github.com/webpack-contrib/css-loader 162 | loader: 'css-loader', 163 | options: { 164 | url: false, 165 | } 166 | }, 167 | { 168 | // https://webpack.js.org/loaders/sass-loader 169 | loader: 'sass-loader', 170 | options: { 171 | implementation: sass, 172 | // 解决 url(search.png) 打包出错的问题 173 | webpackImporter: false, 174 | sassOptions: { 175 | indentWidth: 4, 176 | }, 177 | } as SassOptions, 178 | } 179 | ] 180 | }, 181 | { 182 | test: /\.css$/, 183 | use: ['style-loader', 'css-loader'] 184 | }, 185 | { 186 | oneOf: [ 187 | { 188 | test: /\.icon\.svg$/, 189 | issuer: /\.[jt]sx?$/, 190 | loader: '@svgr/webpack', 191 | options: { icon: true } 192 | }, 193 | { 194 | test: /\.(svg|ico|png|jpe?g|gif|woff2?|ttf|eot|otf|mp4|webm|ogg|mp3|wav|flac|aac)$/, 195 | type: 'asset/inline', 196 | }, 197 | ] 198 | }, 199 | { 200 | test: /\.txt$/, 201 | type: 'asset/source', 202 | } 203 | ], 204 | }, 205 | 206 | 207 | plugins: [ 208 | new Webpack.DefinePlugin({ 209 | BUILD_TIME: dayjs().format('YYYY.MM.DD HH:mm:ss').quote() 210 | }), 211 | 212 | ... await (async () => { 213 | if (production) { 214 | const { LicenseWebpackPlugin } = await import('license-webpack-plugin') 215 | const ignoreds = new Set(['xshell', 'react-object-model', '@ant-design/icons-svg', '@ant-design/pro-layout', '@ant-design/pro-provider', 'toggle-selection']) 216 | return [ 217 | new LicenseWebpackPlugin({ 218 | perChunkOutput: false, 219 | outputFilename: 'ThirdPartyNotice.txt', 220 | excludedPackageTest: pkgname => ignoreds.has(pkgname), 221 | }) as any 222 | ] 223 | } else 224 | return [ ] 225 | })(), 226 | 227 | 228 | // new Webpack.DefinePlugin({ 229 | // process: { env: { }, argv: [] } 230 | // }) 231 | 232 | // 需要分析 bundle 大小时开启 233 | // new BundleAnalyzerPlugin({ analyzerPort: 8880, openAnalyzer: false }), 234 | ], 235 | 236 | 237 | optimization: { 238 | minimize: false, 239 | }, 240 | 241 | performance: { 242 | hints: false, 243 | }, 244 | 245 | cache: { 246 | type: 'filesystem', 247 | 248 | ... ramdisk ? { 249 | cacheDirectory: `${fpd_ramdisk_root}webpack/`, 250 | compression: false 251 | } : { 252 | compression: 'brotli', 253 | } 254 | }, 255 | 256 | ignoreWarnings: [ 257 | /Failed to parse source map/ 258 | ], 259 | 260 | stats: { 261 | colors: true, 262 | 263 | context: fpd_root, 264 | 265 | entrypoints: false, 266 | 267 | errors: true, 268 | errorDetails: true, 269 | 270 | hash: false, 271 | 272 | version: false, 273 | 274 | timings: true, 275 | 276 | children: false, 277 | 278 | assets: true, 279 | assetsSpace: 20, 280 | 281 | modules: false, 282 | modulesSpace: 20, 283 | 284 | cachedAssets: false, 285 | cachedModules: false, 286 | }, 287 | } 288 | } 289 | 290 | 291 | export let webpack = { 292 | lcompiler: new Lock(null), 293 | 294 | 295 | async init (production: boolean) { 296 | this.lcompiler.resource = Webpack(await get_config(production)) 297 | 298 | const { default: { SourceMapSource } } = await import('webpack-sources') 299 | 300 | // 删除 import 注释防止 SystemJS 加载模块失败 301 | // https://github.com/systemjs/systemjs/issues/1752 302 | this.lcompiler.resource.hooks.compilation.tap('PrepareRemoveImportCommentForCompilation', (compilation, params) => { 303 | compilation.hooks.processAssets.tap( 304 | { 305 | name: 'RemoveImportComment', 306 | stage: Webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_COMPATIBILITY, 307 | }, 308 | assets => { 309 | compilation.updateAsset( 310 | 'module.js', 311 | asset => { 312 | const { source, map } = asset.sourceAndMap() 313 | return new SourceMapSource( 314 | (source as string).replaceAll(/import dict from '\.\/dict\.json'.*/g, ''), 315 | 'module.js', 316 | map as RawSourceMap as any 317 | ) 318 | } 319 | ) 320 | }) 321 | }) 322 | }, 323 | 324 | 325 | async run (production: boolean) { 326 | return this.lcompiler.request(async compiler => { 327 | if (!compiler) { 328 | await this.init(production) 329 | compiler = this.lcompiler.resource 330 | } 331 | 332 | return new Promise((resolve, reject) => { 333 | compiler.run((error, stats) => { 334 | if (stats) 335 | console.log( 336 | stats.toString(compiler.options.stats) 337 | .replace(/\n\s*.*gfn.* compiled .*successfully.* in (.*)/, '\n编译成功,用时 $1'.green) 338 | ) 339 | 340 | if (error) 341 | reject(error) 342 | else if (stats.hasErrors()) 343 | reject(new Error('编译失败')) 344 | else 345 | resolve(stats) 346 | }) 347 | }) 348 | }) 349 | }, 350 | 351 | 352 | async close () { 353 | await this.lcompiler.request(async compiler => 354 | new Promise((resolve, reject) => { 355 | compiler.close(error => { 356 | if (error) 357 | reject(error) 358 | else 359 | resolve() 360 | }) 361 | }) 362 | ) 363 | }, 364 | 365 | 366 | async build () { 367 | await this.run(true) 368 | 369 | await this.close() 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /plugin.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "https://grafana.com/plugin-metadata", 3 | "$schema": "http://json-schema.org/draft-07/schema", 4 | "type": "object", 5 | "title": "plugin.json", 6 | "description": "The plugin.json file is required for all plugins. When Grafana starts, it scans the plugin folders and mounts every folder that contains a plugin.json file unless the folder contains a subfolder named dist. In that case, Grafana mounts the dist folder instead.", 7 | "required": ["type", "name", "id", "info", "dependencies"], 8 | "additionalProperties": false, 9 | "properties": { 10 | "id": { 11 | "type": "string", 12 | "description": "Unique name of the plugin. If the plugin is published on grafana.com, then the plugin `id` has to follow the naming conventions.", 13 | "pattern": "^[0-9a-z]+\\-([0-9a-z]+\\-)?(app|panel|datasource|secretsmanager)$" 14 | }, 15 | "type": { 16 | "type": "string", 17 | "description": "Plugin type.", 18 | "enum": ["app", "datasource", "panel", "renderer", "secretsmanager"] 19 | }, 20 | "info": { 21 | "type": "object", 22 | "description": "Metadata for the plugin. Some fields are used on the plugins page in Grafana and others on grafana.com if the plugin is published.", 23 | "required": ["logos", "version", "updated", "keywords"], 24 | "additionalProperties": false, 25 | "properties": { 26 | "author": { 27 | "type": "object", 28 | "description": "Information about the plugin author.", 29 | "additionalProperties": false, 30 | "properties": { 31 | "name": { 32 | "type": "string", 33 | "description": "Author's name." 34 | }, 35 | "email": { 36 | "type": "string", 37 | "description": "Author's name.", 38 | "format": "email" 39 | }, 40 | "url": { 41 | "type": "string", 42 | "description": "Link to author's website.", 43 | "format": "uri" 44 | } 45 | } 46 | }, 47 | "build": { 48 | "type": "object", 49 | "description": "Build information", 50 | "additionalProperties": false, 51 | "properties": { 52 | "time": { 53 | "type": "number", 54 | "description": "Time when the plugin was built, as a Unix timestamp." 55 | }, 56 | "repo": { 57 | "type": "string", 58 | "description": "" 59 | }, 60 | "branch": { 61 | "type": "string", 62 | "description": "Git branch the plugin was built from." 63 | }, 64 | "hash": { 65 | "type": "string", 66 | "description": "Git hash of the commit the plugin was built from" 67 | }, 68 | "number": { 69 | "type": "number", 70 | "description": "" 71 | }, 72 | "pr": { 73 | "type": "number", 74 | "description": "GitHub pull request the plugin was built from" 75 | } 76 | } 77 | }, 78 | "description": { 79 | "type": "string", 80 | "description": "Description of plugin. Used on the plugins page in Grafana and for search on grafana.com." 81 | }, 82 | "keywords": { 83 | "type": "array", 84 | "description": "Array of plugin keywords. Used for search on grafana.com.", 85 | "minItems": 1, 86 | "items": { 87 | "type": "string" 88 | } 89 | }, 90 | "links": { 91 | "type": "array", 92 | "description": "An array of link objects to be displayed on this plugin's project page in the form `{name: 'foo', url: 'http://example.com'}`", 93 | "items": { 94 | "type": "object", 95 | "additionalProperties": false, 96 | "properties": { 97 | "name": { 98 | "type": "string" 99 | }, 100 | "url": { 101 | "type": "string", 102 | "format": "uri" 103 | } 104 | } 105 | } 106 | }, 107 | "logos": { 108 | "type": "object", 109 | "description": "SVG images that are used as plugin icons.", 110 | "required": ["small", "large"], 111 | "additionalProperties": false, 112 | "properties": { 113 | "small": { 114 | "type": "string", 115 | "description": "Link to the \"small\" version of the plugin logo, which must be an SVG image. \"Large\" and \"small\" logos can be the same image." 116 | }, 117 | "large": { 118 | "type": "string", 119 | "description": "Link to the \"large\" version of the plugin logo, which must be an SVG image. \"Large\" and \"small\" logos can be the same image." 120 | } 121 | } 122 | }, 123 | "screenshots": { 124 | "type": "array", 125 | "description": "An array of screenshot objects in the form `{name: 'bar', path: 'img/screenshot.png'}`", 126 | "items": { 127 | "type": "object", 128 | "additionalProperties": false, 129 | "properties": { 130 | "name": { 131 | "type": "string" 132 | }, 133 | "path": { 134 | "type": "string" 135 | } 136 | } 137 | } 138 | }, 139 | "updated": { 140 | "type": "string", 141 | "description": "Date when this plugin was built.", 142 | "pattern": "^(\\d{4}-\\d{2}-\\d{2}|\\%TODAY\\%)$" 143 | }, 144 | "version": { 145 | "type": "string", 146 | "description": "Project version of this commit, e.g. `6.7.x`.", 147 | "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)|(\\%VERSION\\%)$" 148 | } 149 | } 150 | }, 151 | "name": { 152 | "type": "string", 153 | "description": "Human-readable name of the plugin that is shown to the user in the UI." 154 | }, 155 | "dependencies": { 156 | "type": "object", 157 | "description": "Dependency information related to Grafana and other plugins.", 158 | "required": ["grafanaDependency"], 159 | "additionalProperties": false, 160 | "properties": { 161 | "grafanaVersion": { 162 | "type": "string", 163 | "description": "(Deprecated) Required Grafana version for this plugin, e.g. `6.x.x 7.x.x` to denote plugin requires Grafana v6.x.x or v7.x.x.", 164 | "pattern": "^([0-9]+)(\\.[0-9x]+)?(\\.[0-9x])?$" 165 | }, 166 | "grafanaDependency": { 167 | "type": "string", 168 | "description": "Required Grafana version for this plugin. Validated using https://github.com/npm/node-semver.", 169 | "pattern": "^(<=|>=|<|>|=|~|\\^)?([0-9]+)(\\.[0-9x\\*]+)(\\.[0-9x\\*]+)?(\\s(<=|>=|<|=>)?([0-9]+)(\\.[0-9x]+)(\\.[0-9x]+))?(\\-[0-9]+)?$" 170 | }, 171 | "plugins": { 172 | "type": "array", 173 | "description": "An array of required plugins on which this plugin depends.", 174 | "additionalItems": false, 175 | "items": { 176 | "type": "object", 177 | "description": "Plugin dependency. Used to display information about plugin dependencies in the Grafana UI.", 178 | "required": ["id", "name", "type", "version"], 179 | "properties": { 180 | "id": { 181 | "type": "string", 182 | "pattern": "^[0-9a-z]+\\-([0-9a-z]+\\-)?(app|panel|datasource|secretsmanager)$" 183 | }, 184 | "type": { 185 | "type": "string", 186 | "enum": ["app", "datasource", "panel", "secretsmanager"] 187 | }, 188 | "name": { 189 | "type": "string" 190 | }, 191 | "version": { 192 | "type": "string" 193 | } 194 | } 195 | } 196 | } 197 | } 198 | }, 199 | "$schema": { 200 | "type": "string", 201 | "description": "Schema definition for the plugin.json file. Used primarily for schema validation." 202 | }, 203 | "alerting": { 204 | "type": "boolean", 205 | "description": "For data source plugins, if the plugin supports alerting. Requires `backend` to be set to `true`." 206 | }, 207 | "annotations": { 208 | "type": "boolean", 209 | "description": "For data source plugins, if the plugin supports annotation queries." 210 | }, 211 | "autoEnabled": { 212 | "type": "boolean", 213 | "description": "Set to true for app plugins that should be enabled and pinned to the navigation bar in all orgs." 214 | }, 215 | "backend": { 216 | "type": "boolean", 217 | "description": "If the plugin has a backend component." 218 | }, 219 | "builtIn": { 220 | "type": "boolean", 221 | "description": "[internal only] Indicates whether the plugin is developed and shipped as part of Grafana. Also known as a 'core plugin'." 222 | }, 223 | "category": { 224 | "type": "string", 225 | "description": "Plugin category used on the Add data source page.", 226 | "enum": ["tsdb", "logging", "cloud", "tracing", "profiling", "sql", "enterprise", "iot", "other"] 227 | }, 228 | "enterpriseFeatures": { 229 | "type": "object", 230 | "description": "Grafana Enterprise specific features", 231 | "additionalProperties": true, 232 | "properties": { 233 | "healthDiagnosticsErrors": { 234 | "type": "boolean", 235 | "description": "Enable/Disable health diagnostics errors. Requires Grafana >=7.5.5.", 236 | "default": false 237 | } 238 | } 239 | }, 240 | "executable": { 241 | "type": "string", 242 | "description": "The first part of the file name of the backend component executable. There can be multiple executables built for different operating system and architecture. Grafana will check for executables named `_<$GOOS>_<.exe for Windows>`, e.g. `plugin_linux_amd64`. Combination of $GOOS and $GOARCH can be found here: https://golang.org/doc/install/source#environment." 243 | }, 244 | "hideFromList": { 245 | "type": "boolean", 246 | "description": "[internal only] Excludes the plugin from listings in Grafana's UI. Only allowed for `builtIn` plugins." 247 | }, 248 | "includes": { 249 | "type": "array", 250 | "description": "Resources to include in plugin.", 251 | "items": { 252 | "type": "object", 253 | "additionalItems": false, 254 | "properties": { 255 | "uid": { 256 | "type": "string", 257 | "description": "Unique identifier of the included resource" 258 | }, 259 | "type": { 260 | "type": "string", 261 | "enum": ["dashboard", "page", "panel", "datasource", "secretsmanager"] 262 | }, 263 | "name": { 264 | "type": "string" 265 | }, 266 | "component": { 267 | "type": "string", 268 | "description": "(Legacy) The Angular component to use for a page." 269 | }, 270 | "role": { 271 | "type": "string", 272 | "description": "The minimum role a user must have to see this page in the navigation menu.", 273 | "enum": ["Admin", "Editor", "Viewer"] 274 | }, 275 | "path": { 276 | "type": "string", 277 | "description": "Used for app plugins." 278 | }, 279 | "addToNav": { 280 | "type": "boolean", 281 | "description": "Add the include to the navigation menu." 282 | }, 283 | "defaultNav": { 284 | "type": "boolean", 285 | "description": "Page or dashboard when user clicks the icon in the side menu." 286 | }, 287 | "icon": { 288 | "type": "string", 289 | "description": "Icon to use in the side menu. For information on available icon, refer to [Icons Overview](https://developers.grafana.com/ui/latest/index.html?path=/story/docs-overview-icon--icons-overview)." 290 | } 291 | } 292 | } 293 | }, 294 | "logs": { 295 | "type": "boolean", 296 | "description": "For data source plugins, if the plugin supports logs. It may be used to filter logs only features." 297 | }, 298 | "metrics": { 299 | "type": "boolean", 300 | "description": "For data source plugins, if the plugin supports metric queries. Used to enable the plugin in the panel editor." 301 | }, 302 | "pascalName": { 303 | "type": "string", 304 | "description": "[internal only] The PascalCase name for the plugin. Used for creating machine-friendly identifiers, typically in code generation. If not provided, defaults to name, but title-cased and sanitized (only alphabetical characters allowed).", 305 | "pattern": "^([A-Z][a-zA-Z]{1,62})$" 306 | }, 307 | "preload": { 308 | "type": "boolean", 309 | "description": "Initialize plugin on startup. By default, the plugin initializes on first use. Useful for app plugins that should load without user interaction." 310 | }, 311 | "queryOptions": { 312 | "type": "object", 313 | "description": "For data source plugins. There is a query options section in the plugin's query editor and these options can be turned on if needed.", 314 | "additionalProperties": false, 315 | "properties": { 316 | "maxDataPoints": { 317 | "type": "boolean", 318 | "description": "For data source plugins. If the `max data points` option should be shown in the query options section in the query editor." 319 | }, 320 | "minInterval": { 321 | "type": "boolean", 322 | "description": "For data source plugins. If the `min interval` option should be shown in the query options section in the query editor." 323 | }, 324 | "cacheTimeout": { 325 | "type": "boolean", 326 | "description": "For data source plugins. If the `cache timeout` option should be shown in the query options section in the query editor." 327 | } 328 | } 329 | }, 330 | "routes": { 331 | "type": "array", 332 | "description": "For data source plugins. Proxy routes used for plugin authentication and adding headers to HTTP requests made by the plugin. For more information, refer to [Authentication for data source plugins](/developers/plugin-tools/create-a-plugin/extend-a-plugin/add-authentication-for-data-source-plugins).", 333 | "items": { 334 | "type": "object", 335 | "description": "For data source plugins. Proxy routes used for plugin authentication and adding headers to HTTP requests made by the plugin. For more information, refer to [Authentication for data source plugins](/developers/plugin-tools/create-a-plugin/extend-a-plugin/add-authentication-for-data-source-plugins).", 336 | "additionalProperties": false, 337 | "properties": { 338 | "path": { 339 | "type": "string", 340 | "description": "For data source plugins. The route path that is replaced by the route URL field when proxying the call." 341 | }, 342 | "method": { 343 | "type": "string", 344 | "description": "For data source plugins. Route method matches the HTTP verb like GET or POST. Multiple methods can be provided as a comma-separated list." 345 | }, 346 | "url": { 347 | "type": "string", 348 | "description": "For data source plugins. Route URL is where the request is proxied to." 349 | }, 350 | "reqSignedIn": { 351 | "type": "boolean" 352 | }, 353 | "reqRole": { 354 | "type": "string" 355 | }, 356 | "headers": { 357 | "type": "array", 358 | "description": "For data source plugins. Route headers adds HTTP headers to the proxied request." 359 | }, 360 | "body": { 361 | "type": "object", 362 | "description": "For data source plugins. Route headers set the body content and length to the proxied request." 363 | }, 364 | "tokenAuth": { 365 | "type": "object", 366 | "description": "For data source plugins. Token authentication section used with an OAuth API.", 367 | "additionalProperties": false, 368 | "properties": { 369 | "url": { 370 | "type": "string", 371 | "description": "URL to fetch the authentication token." 372 | }, 373 | "scopes": { 374 | "type": "array", 375 | "description": "The list of scopes that your application should be granted access to.", 376 | "items": { 377 | "type": "string" 378 | } 379 | }, 380 | "params": { 381 | "type": "object", 382 | "description": "Parameters for the token authentication request.", 383 | "additionalProperties": true, 384 | "properties": { 385 | "grant_type": { 386 | "type": "string", 387 | "description": "OAuth grant type" 388 | }, 389 | "client_id": { 390 | "type": "string", 391 | "description": "OAuth client ID" 392 | }, 393 | "client_secret": { 394 | "type": "string", 395 | "description": "OAuth client secret. Usually populated by decrypting the secret from the SecureJson blob." 396 | }, 397 | "resource": { 398 | "type": "string", 399 | "description": "OAuth resource" 400 | } 401 | } 402 | } 403 | } 404 | }, 405 | "jwtTokenAuth": { 406 | "type": "object", 407 | "description": "For data source plugins. Token authentication section used with an JWT OAuth API.", 408 | "additionalProperties": true, 409 | "properties": { 410 | "url": { 411 | "type": "string", 412 | "description": "URL to fetch the JWT token.", 413 | "format": "uri" 414 | }, 415 | "scopes": { 416 | "type": "array", 417 | "description": "The list of scopes that your application should be granted access to.", 418 | "items": { 419 | "type": "string" 420 | } 421 | }, 422 | "params": { 423 | "type": "object", 424 | "description": "Parameters for the JWT token authentication request.", 425 | "additionalProperties": false, 426 | "properties": { 427 | "token_uri": { 428 | "type": "string", 429 | "description": "" 430 | }, 431 | "client_email": { 432 | "type": "string", 433 | "description": "" 434 | }, 435 | "private_key": { 436 | "type": "string", 437 | "description": "" 438 | } 439 | } 440 | } 441 | } 442 | }, 443 | "urlParams": { 444 | "type": "array", 445 | "description": "Add URL parameters to a proxy route", 446 | "items": { 447 | "type": "object", 448 | "additionalProperties": false, 449 | "properties": { 450 | "name": { 451 | "type": "string", 452 | "description": "Name of the URL parameter" 453 | }, 454 | "content": { 455 | "type": "string", 456 | "description": "Value of the URL parameter" 457 | } 458 | } 459 | } 460 | } 461 | } 462 | } 463 | }, 464 | "skipDataQuery": { 465 | "type": "boolean", 466 | "description": "For panel plugins. Hides the query editor." 467 | }, 468 | "state": { 469 | "type": "string", 470 | "description": "Marks a plugin as a pre-release.", 471 | "enum": ["alpha", "beta"] 472 | }, 473 | "streaming": { 474 | "type": "boolean", 475 | "description": "For data source plugins, if the plugin supports streaming. Used in Explore to start live streaming." 476 | }, 477 | "tracing": { 478 | "type": "boolean", 479 | "description": "For data source plugins, if the plugin supports tracing. Used for example to link logs (e.g. Loki logs) with tracing plugins." 480 | }, 481 | "iam": { 482 | "type": "object", 483 | "description": "Identity and Access Management.", 484 | "properties": { 485 | "permissions": { 486 | "type": "array", 487 | "description": "Permissions are the permissions that the plugin needs its associated service account to have", 488 | "items": { 489 | "type": "object", 490 | "additionalProperties": false, 491 | "properties": { 492 | "action": { 493 | "type": "string" 494 | }, 495 | "scope": { 496 | "type": "string" 497 | } 498 | } 499 | } 500 | }, 501 | "impersonation": { 502 | "type": "object", 503 | "description": "Impersonation describes the permissions that the plugin will be restricted to when acting on behalf of the user.", 504 | "properties": { 505 | "groups": { 506 | "type": "boolean", 507 | "description": "Groups allows the service to list the impersonated user's teams." 508 | }, 509 | "permissions": { 510 | "type": "array", 511 | "description": "Permissions are the permissions that the plugin needs when impersonating a user. The intersection of this set with the impersonated user's permission guarantees that the client will not gain more privileges than the impersonated user has.", 512 | "items": { 513 | "type": "object", 514 | "additionalProperties": false, 515 | "properties": { 516 | "action": { 517 | "type": "string" 518 | }, 519 | "scope": { 520 | "type": "string" 521 | } 522 | } 523 | } 524 | } 525 | } 526 | } 527 | } 528 | } 529 | } 530 | } 531 | -------------------------------------------------------------------------------- /index.tsx: -------------------------------------------------------------------------------- 1 | import './index.sass' 2 | 3 | import { default as React, useRef, useState, useEffect } from 'react' 4 | 5 | import { Observable, merge } from 'rxjs' 6 | 7 | 8 | import { getTemplateSrv } from '@grafana/runtime' 9 | import type { DataQuery } from '@grafana/schema' 10 | import { DataSourcePlugin, DataSourceApi, MutableDataFrame, FieldType, LoadingState, CircularDataFrame, 11 | SelectableValue, type DataQueryRequest, type DataSourcePluginOptionsEditorProps, 12 | type DataSourceInstanceSettings, type DataQueryResponse, type QueryEditorProps, 13 | type DataSourceJsonData, type MetricFindValue, type FieldDTO 14 | } from '@grafana/data' 15 | import { InlineField, Input, InlineSwitch, Button, Icon, Select } from '@grafana/ui' 16 | 17 | 18 | import { defer, delay } from 'xshell/utils.browser.js' 19 | 20 | 21 | import { 22 | DDB, DdbType, DdbForm, type DdbObj, format, formati, 23 | datetime2ms, month2ms, minute2ms, date2ms, datehour2ms, second2ms, time2ms, timestamp2ms, nanotime2ns, 24 | nanotimestamp2ns, nulls, type DdbVectorValue, type DdbVectorObj, type DdbTableObj, type DdbOptions 25 | } from 'dolphindb/browser.js' 26 | 27 | 28 | import { t } from './i18n/index.js' 29 | 30 | import { DdbCodeEditor } from './DdbCodeEditor.js' 31 | 32 | 33 | export const fpd_root = '/public/plugins/dolphindb-datasource/' as const 34 | 35 | 36 | console.log(t('DolphinDB Grafana 插件已加载')) 37 | 38 | 39 | 40 | export class DataSource extends DataSourceApi { 41 | settings: DataSourceInstanceSettings 42 | 43 | url: string 44 | 45 | options: DdbOptions 46 | 47 | ddb: DDB 48 | 49 | 50 | constructor (settings: DataSourceInstanceSettings) { 51 | super(settings) 52 | 53 | console.log('new DolphinDB.DataSource:', settings) 54 | 55 | this.settings = settings 56 | 57 | const { url, ...options } = settings.jsonData 58 | 59 | this.url = url 60 | this.options = options 61 | this.ddb = new DDB(url, options) 62 | } 63 | 64 | 65 | /** 调用后会确保和数据库的连接是正常的 (this.connected === true),否则自动尝试建立新的连接 66 | 这个方法是幂等的,首次调用建立实际的 WebSocket 连接到 URL 对应的 DolphinDB,然后执行自动登录, 67 | 后续调用检查上面的条件 */ 68 | async connect () { 69 | const { resource: websocket } = this.ddb.lwebsocket 70 | if (websocket && (websocket.readyState === WebSocket.CLOSING || websocket.readyState === WebSocket.CLOSED)) { 71 | console.log(t('检测到 ddb 连接已断开,尝试建立新的连接到:'), this.ddb.url) 72 | this.ddb = new DDB(this.url, this.options) 73 | } 74 | 75 | await this.ddb.connect() 76 | } 77 | 78 | 79 | override async testDatasource () { 80 | console.log('test datasource') 81 | 82 | try { 83 | await this.connect() 84 | return { 85 | status: 'success', 86 | message: t('已连接到数据库') 87 | } 88 | } catch (error) { 89 | console.error(error) 90 | error.message = t( 91 | '{{message}};\n无法通过 WebSocket 连接到 {{url}},请检查 url, DataSource 配置、网络连接状况 (是否配置代理,代理是否支持 WebSocket)、server 是否启动、server 版本不低于 1.30.16 或 2.00.4', 92 | { 93 | url: this.ddb.url, 94 | message: error.message 95 | } 96 | ) 97 | throw error 98 | } 99 | } 100 | 101 | 102 | override query (request: DataQueryRequest) { 103 | const { range: { from, to }, scopedVars } = request 104 | 105 | // 下面的 promises 是为了保证脚本类型下,所有 query 的数据都准备好后, 106 | // 再一起通过 subscriber.next 给 grafana,以解决不同时间添加数据导致图像线条闪烁的问题 107 | let pevals_ready = defer() 108 | let pevals: Promise[] = [ ] 109 | 110 | return merge(... request.targets.map(query => { 111 | const { refId, hide, is_streaming } = query 112 | const code = query.code || '' 113 | 114 | return new Observable(subscriber => { 115 | if (is_streaming) { 116 | const { streaming: { table, action } } = query 117 | 118 | let frame = new CircularDataFrame({ 119 | append: 'head', 120 | capacity: 10_0000 121 | }) 122 | 123 | if (!table) { 124 | subscriber.error(t('table 不应该为空')) 125 | return 126 | } 127 | 128 | const { url, ...options } = this.settings.jsonData 129 | 130 | const sddb = new DDB(url, { 131 | ...options, 132 | streaming: { 133 | table, 134 | action, 135 | handler: ({ data, colnames, error }) => { 136 | if (error) { 137 | console.error(error) 138 | subscriber.error(error) 139 | return 140 | } 141 | 142 | const fields = this.convert(data, colnames) 143 | 144 | if (fields.length !== 0) { 145 | if (frame.fields.length === 0) 146 | for (const field of fields) 147 | frame.addField(field) 148 | 149 | const nrows = fields[0].values.length 150 | for (let i = 0; i < nrows; i++) { 151 | let row = { } 152 | for (const field of fields) 153 | row[field.name] = field.values[i] 154 | frame.add(row) 155 | } 156 | 157 | subscriber.next({ 158 | data: [frame], 159 | key: query.refId, 160 | state: LoadingState.Streaming 161 | }) 162 | } 163 | } 164 | }, 165 | }) 166 | 167 | ;(async () => { 168 | try { 169 | await sddb.connect() 170 | 171 | subscriber.next({ 172 | data: [frame], 173 | key: refId, 174 | state: LoadingState.Streaming 175 | }) 176 | } catch (error) { 177 | subscriber.error(error) 178 | console.error(error) 179 | } 180 | })() 181 | 182 | return () => { 183 | sddb.disconnect() 184 | } 185 | } else 186 | if (hide || !code.trim()) 187 | subscriber.next({ 188 | data: [new MutableDataFrame({ refId, fields: [ ] })], 189 | key: refId, 190 | state: LoadingState.Done 191 | }) 192 | else 193 | (async () => { 194 | try { 195 | const tplsrv = getTemplateSrv() 196 | ;(from as any)._isUTC = false 197 | ;(to as any)._isUTC = false 198 | 199 | const code_ = tplsrv 200 | .replace( 201 | code 202 | .replaceAll( 203 | /\$(__)?timeFilter\b/g, 204 | () => 205 | 'pair(' + 206 | from.format('YYYY.MM.DD HH:mm:ss.SSS') + 207 | ', ' + 208 | to.format('YYYY.MM.DD HH:mm:ss.SSS') + 209 | ')' 210 | ).replaceAll( 211 | /\$__interval\b/g, 212 | () => 213 | tplsrv.replace('$__interval', scopedVars).replace(/h$/, 'H') 214 | ), 215 | scopedVars, 216 | var_formatter 217 | ) 218 | 219 | await this.connect() 220 | 221 | let peval = this.ddb.eval(code_) 222 | 223 | pevals.push(peval) 224 | if (pevals.length === request.targets.filter(({ is_streaming }) => !is_streaming).length) 225 | pevals_ready.resolve() 226 | 227 | const table = await peval 228 | 229 | if (table.form !== DdbForm.table) 230 | subscriber.error(t('Query 代码的最后一条语句需要返回 table,实际返回的是: {{value}}', { value: table.toString() })) 231 | 232 | await pevals_ready 233 | await Promise.allSettled(pevals) 234 | 235 | subscriber.next({ 236 | data: [ 237 | new MutableDataFrame({ 238 | refId, 239 | fields: this.convert(table) 240 | }) 241 | ], 242 | key: refId, 243 | state: LoadingState.Done 244 | }) 245 | } catch (error) { 246 | subscriber.error(error) 247 | } 248 | })() 249 | }) 250 | })) 251 | } 252 | 253 | 254 | override async metricFindQuery (query: string, options: any): Promise { 255 | console.log('metricFindQuery:', { query, options }) 256 | 257 | await this.connect() 258 | 259 | const result = await this.ddb.eval( 260 | getTemplateSrv() 261 | .replace(query, { }, var_formatter) 262 | ) 263 | 264 | // 标量直接返回含有该标量的数组 265 | // 向量返回对应数组 266 | // 含有一个向量的 table 取其中的向量映射为数组 267 | // 其它情况报错 268 | 269 | // expandable 是什么? 270 | 271 | switch (result.form) { 272 | case DdbForm.scalar: { 273 | const value = format(DdbType.char, result.value, result.le, { nullstr: false, quote: false }) 274 | return [{ text: value, value }] 275 | } 276 | 277 | case DdbForm.vector: 278 | case DdbForm.pair: 279 | case DdbForm.set: { 280 | let values = new Array(result.rows) 281 | 282 | for (let i = 0; i < result.rows; i++) { 283 | const text = formati(result as DdbVectorObj, i, { quote: false, nullstr: false }) 284 | values[i] = { text, value: text } 285 | } 286 | 287 | return values 288 | } 289 | 290 | case DdbForm.table: { 291 | if ((result as DdbTableObj).value.length === 1) { 292 | let values = new Array(result.value[0].rows) 293 | 294 | for (let i = 0; i < result.value[0].rows; i++) { 295 | const text = formati(result.value[0], i, { quote: false, nullstr: false }) 296 | values[i] = { 297 | text, 298 | value: text, 299 | expandable: true 300 | } 301 | } 302 | 303 | return values 304 | } else 305 | throw new Error(t('Query 的返回值不是标量、向量或只含一个向量的表格')) 306 | } 307 | 308 | default: 309 | throw new Error(t('Query 的返回值不是标量、向量或只含一个向量的表格')) 310 | } 311 | } 312 | 313 | 314 | convert (table: DdbObj[]>, colnames?: string[]): FieldDTO[] { 315 | return table.value.map((col, icol) => { 316 | const { type, value, rows, name = colnames[icol] } = col 317 | 318 | switch (type) { 319 | // --- boolean 320 | case DdbType.bool: 321 | return { 322 | name, 323 | type: FieldType.boolean, 324 | values: [...value as Uint8Array].map(x => x === nulls.int8 ? null : x) 325 | } 326 | 327 | 328 | // --- string 329 | case DdbType.string: 330 | case DdbType.symbol: 331 | return { 332 | name, 333 | type: FieldType.string, 334 | values: value 335 | } 336 | 337 | case DdbType.symbol_extended: 338 | case DdbType.char: 339 | case DdbType.uuid: 340 | case DdbType.int128: 341 | case DdbType.ipaddr: 342 | case DdbType.blob: 343 | case DdbType.complex: 344 | case DdbType.point: 345 | return { 346 | name, 347 | type: FieldType.string, 348 | values: (() => { 349 | let values = new Array(rows) 350 | 351 | for (let i = 0; i < rows; i++) 352 | values[i] = formati(col, i, { quote: false, nullstr: false }) 353 | 354 | return values 355 | })() 356 | } 357 | 358 | 359 | // --- time 360 | case DdbType.date: 361 | return { 362 | name, 363 | type: FieldType.time, 364 | values: [...value as Int32Array].map(x => date2ms(x)) 365 | } 366 | 367 | case DdbType.month: 368 | return { 369 | name, 370 | type: FieldType.time, 371 | values: [...value as Int32Array].map(x => month2ms(x)) 372 | } 373 | 374 | case DdbType.time: 375 | return { 376 | name, 377 | type: FieldType.time, 378 | values: [...value as Int32Array].map(x => time2ms(x)) 379 | } 380 | 381 | case DdbType.minute: 382 | return { 383 | name, 384 | type: FieldType.time, 385 | values: [...value as Int32Array].map(x => minute2ms(x)) 386 | } 387 | 388 | case DdbType.second: 389 | return { 390 | name, 391 | type: FieldType.time, 392 | values: [...value as Int32Array].map(x => second2ms(x)) 393 | } 394 | 395 | case DdbType.datetime: 396 | return { 397 | name, 398 | type: FieldType.time, 399 | values: [...value as Int32Array].map(x => datetime2ms(x)) 400 | } 401 | 402 | case DdbType.timestamp: 403 | return { 404 | name, 405 | type: FieldType.time, 406 | values: [...value as BigInt64Array].map(x => timestamp2ms(x)) 407 | } 408 | 409 | case DdbType.nanotime: 410 | return { 411 | name, 412 | type: FieldType.time, 413 | values: [...value as BigInt64Array].map(x => Number(nanotime2ns(x)) / 1000000) 414 | } 415 | 416 | case DdbType.nanotimestamp: 417 | return { 418 | name, 419 | type: FieldType.time, 420 | values: [...value as BigInt64Array].map(x => Number(nanotimestamp2ns(x)) / 1000000) 421 | } 422 | 423 | case DdbType.datehour: 424 | return { 425 | name, 426 | type: FieldType.time, 427 | values: [...value as Int32Array].map(x => datehour2ms(x)) 428 | } 429 | 430 | 431 | // --- number 432 | case DdbType.short: 433 | return { 434 | name, 435 | type: FieldType.number, 436 | values: [...value as Int16Array].map(x => x === nulls.int16 ? null : x) 437 | } 438 | 439 | case DdbType.int: 440 | return { 441 | name, 442 | type: FieldType.number, 443 | values: [...value as Int32Array].map(x => x === nulls.int32 ? null : x) 444 | } 445 | 446 | case DdbType.float: 447 | return { 448 | name, 449 | type: FieldType.number, 450 | values: [...value as Float32Array].map(x => x === nulls.float32 ? null : x) 451 | } 452 | 453 | case DdbType.double: 454 | return { 455 | name, 456 | type: FieldType.number, 457 | values: [...value as Float64Array].map(x => x === nulls.double ? null : x) 458 | } 459 | 460 | case DdbType.long: 461 | return { 462 | name, 463 | type: FieldType.number, 464 | values: [...(value as BigInt64Array)].map(x => x === nulls.int64 ? null : Number(x)) 465 | } 466 | 467 | 468 | // --- other 469 | default: 470 | return { 471 | name, 472 | type: FieldType.other, 473 | values: value 474 | } 475 | } 476 | }) as FieldDTO[] 477 | } 478 | } 479 | 480 | 481 | /** DDB constructor 所需参数 */ 482 | interface DataSourceConfig extends DataSourceJsonData { 483 | url?: string 484 | autologin?: boolean 485 | username?: string 486 | password?: string 487 | python?: boolean 488 | verbose?: boolean 489 | } 490 | 491 | export interface DdbDataQuery extends DataQuery { 492 | is_streaming: boolean 493 | code?: string 494 | streaming?: { 495 | table: string 496 | action?: string 497 | } 498 | } 499 | 500 | 501 | function ConfigEditor ({ 502 | options, 503 | onOptionsChange 504 | }: DataSourcePluginOptionsEditorProps) { 505 | let { jsonData } = options 506 | 507 | jsonData.url ??= 'ws://127.0.0.1:8848' 508 | jsonData.autologin ??= true 509 | jsonData.username ??= 'admin' 510 | jsonData.password ??= '123456' 511 | jsonData.python ??= false 512 | jsonData.verbose ??= false 513 | 514 | function on_change (option: keyof DataSourceConfig, checked?: boolean) { 515 | return (event: React.FormEvent) => { 516 | onOptionsChange({ 517 | ...options, 518 | jsonData: { 519 | ...options.jsonData, 520 | [option]: checked ? event.currentTarget.checked : event.currentTarget.value 521 | } 522 | }) 523 | } 524 | } 525 | 526 | 527 | return
528 | 533 | 537 | 538 |
539 | 540 | 541 | 545 | 546 |
547 | 548 | {(options.jsonData.autologin || options.jsonData.autologin === undefined) && <> 549 | 550 | 554 | 555 |
556 | } 557 | 558 | {(options.jsonData.autologin || options.jsonData.autologin === undefined) && <> 559 | 560 | 565 | 566 |
567 | } 568 | 569 | 570 | 574 | 575 |
576 | 577 | 578 | 582 | 583 | 584 |
({t('插件构建时间:')} {BUILD_TIME})
585 | 586 | {/*
587 | { JSON.stringify(options) } 588 |
*/} 589 |
590 | } 591 | 592 | 593 | function QueryEditor ( 594 | { 595 | query, 596 | onChange, 597 | onRunQuery, 598 | datasource 599 | }: QueryEditorProps & { height?: number } 600 | ) { 601 | const script_type = { label: t('脚本'), value: 'script' as const } 602 | const streaming_type = { label: t('流数据表'), value: 'streaming' as const } 603 | 604 | const [type, set_type] = useState>(script_type) 605 | 606 | useEffect(() => { 607 | set_type(query.is_streaming ? streaming_type : script_type) 608 | }, [ ]) 609 | 610 | const { 611 | is_streaming, 612 | code, 613 | refId, 614 | streaming 615 | } = query 616 | 617 | return
618 |
619 | 620 | { 659 | const { value } = event.currentTarget 660 | onChange({ 661 | refId, 662 | is_streaming, 663 | code, 664 | streaming: { 665 | table: value || streaming?.table, 666 | } 667 | }) 668 | }} /> 669 | 670 |
671 | 672 |
673 | 674 | 675 | 676 | } 677 | 678 | 679 | /** 创建 query 变量时的编辑器 */ 680 | function VariableEditor ({ 681 | query, 682 | onChange, 683 | }: { 684 | query: string 685 | onChange (query: string, definition: string): void 686 | }) { 687 | const rquery = useRef(query) 688 | const rtrigger = useRef(() => { }) 689 | 690 | function save () { 691 | const { current: query } = rquery 692 | console.log(t('暂存查询并更新预览:')) 693 | console.log(query) 694 | onChange(query, query) 695 | rtrigger.current() 696 | } 697 | 698 | return
699 | 700 | <> 701 | {/* @ts-ignore */} 702 | { 706 | rquery.current = code 707 | }} 708 | onRunQuery={save} 709 | tip={false} 710 | /> 711 | 712 | 713 | 714 |
715 | } 716 | 717 | 718 | function VariableEditorBottom ({ 719 | save, 720 | rtrigger 721 | }: { 722 | save? (): void 723 | rtrigger: React.MutableRefObject<() => void> 724 | }) { 725 | const [visible, set_visible] = useState(false) 726 | 727 | rtrigger.current = async function trigger () { 728 | set_visible(true) 729 | await delay(500) 730 | set_visible(false) 731 | } 732 | 733 | return
734 |
{t('修改 Query 后请在编辑框内按 Ctrl + S 或点击右侧按钮暂存查询并更新预览')}
735 |
736 | 737 | {t('已暂存查询并更新预览')} 738 |
739 | 747 |
748 | } 749 | 750 | 751 | function var_formatter (value: string | string[], variable: any, default_formatter: Function) { 752 | if (typeof value === 'string') 753 | return value 754 | 755 | if (Array.isArray(variable)) 756 | return JSON.stringify(variable) 757 | 758 | return default_formatter(value, 'json', variable) 759 | } 760 | 761 | 762 | // ------------ 注册插件 763 | export const plugin = new DataSourcePlugin(DataSource) 764 | .setConfigEditor(ConfigEditor) 765 | .setQueryEditor(QueryEditor) 766 | .setVariableQueryEditor(VariableEditor) 767 | -------------------------------------------------------------------------------- /DdbCodeEditor.tsx: -------------------------------------------------------------------------------- 1 | import './index.sass' 2 | 3 | import { useRef } from 'react' 4 | 5 | 6 | import { Resizable } from 're-resizable' 7 | 8 | import { request_json } from 'xshell/net.browser.js' 9 | 10 | import { 11 | type QueryEditorProps, 12 | type DataSourceJsonData, 13 | } from '@grafana/data' 14 | import { 15 | CodeEditor, 16 | useTheme2, 17 | } from '@grafana/ui' 18 | 19 | import type * as monacoapi from 'monaco-editor/esm/vs/editor/editor.api.js' 20 | 21 | import { generateTokensCSSForColorMap } from 'monaco-editor/esm/vs/editor/common/languages/supports/tokenization.js' 22 | import { Color } from 'monaco-editor/esm/vs/base/common/color.js' 23 | 24 | 25 | import { 26 | INITIAL, 27 | Registry, 28 | parseRawGrammar, 29 | type IGrammar, 30 | type StateStack, 31 | } from 'vscode-textmate' 32 | import type { IRawGrammar } from 'vscode-textmate/release/rawGrammar.js' 33 | 34 | import { createOnigScanner, createOnigString, loadWASM } from 'vscode-oniguruma' 35 | 36 | 37 | import { keywords, constants, tm_language } from 'dolphindb/language.js' 38 | 39 | import { theme_light } from 'dolphindb/theme.light.js' 40 | import { theme_dark } from 'dolphindb/theme.dark.js' 41 | 42 | 43 | 44 | import { t, language } from './i18n/index.js' 45 | import { fpd_root, type DataSource, type DdbDataQuery } from './index.js' 46 | 47 | 48 | const constants_lower = constants.map(constant => constant.toLowerCase()) 49 | 50 | let docs = { } 51 | 52 | let funcs: string[] = [ ] 53 | let funcs_lower: string[] = [ ] 54 | 55 | 56 | export function DdbCodeEditor ( 57 | { 58 | height = 260, 59 | query: { 60 | code, 61 | refId, 62 | is_streaming, 63 | streaming 64 | }, 65 | onChange, 66 | onRunQuery, 67 | tip = true, 68 | }: QueryEditorProps & { height?: number, tip?: boolean } 69 | ) { 70 | const { isDark } = useTheme2() 71 | 72 | const rpinit = useRef>() 73 | 74 | 75 | return
76 | {/*
77 | 78 | 79 |
*/} 80 | 81 | 86 | { 183 | rpinit.current = (async () => { 184 | if ((monaco as any).inited) 185 | return 186 | 187 | (monaco as any).inited = true 188 | 189 | let { languages, editor } = monaco 190 | 191 | const { CompletionItemKind } = languages 192 | 193 | 194 | ;(async () => { 195 | const fname = `docs.${ language === 'zh' ? 'zh' : 'en' }.json` 196 | 197 | docs = await request_json(fpd_root + fname) 198 | 199 | funcs = Object.keys(docs) 200 | funcs_lower = funcs.map(func => 201 | func.toLowerCase()) 202 | 203 | console.log(t('函数文档 {{fname}} 已加载', { fname })) 204 | })() 205 | 206 | // Using the response directly only works if the server sets the MIME type 'application/wasm'. 207 | // Otherwise, a TypeError is thrown when using the streaming compiler. 208 | // We therefore use the non-streaming compiler :(. 209 | await loadWASM(await fetch(`${fpd_root}/onig.wasm`)) 210 | 211 | 212 | languages.register({ 213 | id: 'dolphindb', 214 | // configuration: '' 215 | }) 216 | 217 | 218 | languages.setTokensProvider( 219 | 'dolphindb', 220 | 221 | await new TokensProviderCache(registry) 222 | .createEncodedTokensProvider( 223 | 'source.dolphindb', 224 | languages.getEncodedLanguageId('dolphindb') 225 | ) 226 | ) 227 | 228 | 229 | languages.setLanguageConfiguration('dolphindb', { 230 | comments: { 231 | // symbol used for single line comment. Remove this entry if your language does not support line comments 232 | lineComment: '//', 233 | 234 | // symbols used for start and end a block comment. Remove this entry if your language does not support block comments 235 | blockComment: ['/*', '*/'] 236 | }, 237 | 238 | // symbols used as brackets 239 | brackets: [ 240 | ['{', '}'], 241 | ['[', ']'], 242 | ['(', ')'] 243 | ], 244 | 245 | // symbols that are auto closed when typing 246 | autoClosingPairs: [ 247 | { open: '{', close: '}' }, 248 | { open: '[', close: ']' }, 249 | { open: '(', close: ')' }, 250 | { open: '"', close: '"', notIn: ['string'] }, 251 | { open: "'", close: "'", notIn: ['string'] }, 252 | { open: '/**', close: ' */', notIn: ['string'] }, 253 | { open: '/*', close: ' */', notIn: ['string'] } 254 | ], 255 | 256 | // symbols that that can be used to surround a selection 257 | surroundingPairs: [ 258 | { open: '{', close: '}' }, 259 | { open: '[', close: ']' }, 260 | { open: '(', close: ')' }, 261 | { open: '"', close: '"' }, 262 | { open: "'", close: "'" }, 263 | { open: '<', close: '>' }, 264 | ], 265 | 266 | folding: { 267 | markers: { 268 | start: new RegExp('^\\s*//\\s*#?region\\b'), 269 | end: new RegExp('^\\s*//\\s*#?endregion\\b') 270 | } 271 | }, 272 | 273 | wordPattern: new RegExp('(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\#\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\\'\\"\\,\\.\\<\\>\\/\\?\\s]+)'), 274 | 275 | indentationRules: { 276 | increaseIndentPattern: new RegExp('^((?!\\/\\/).)*(\\{[^}"\'`]*|\\([^)"\'`]*|\\[[^\\]"\'`]*)$'), 277 | decreaseIndentPattern: new RegExp('^((?!.*?\\/\\*).*\\*/)?\\s*[\\}\\]].*$') 278 | } 279 | }) 280 | 281 | languages.registerCompletionItemProvider('dolphindb', { 282 | // @ts-ignore 283 | provideCompletionItems (doc, pos, ctx, canceller) { 284 | if (canceller.isCancellationRequested) 285 | return 286 | 287 | const keyword = doc.getWordAtPosition(pos).word 288 | 289 | 290 | let fns: string[] 291 | let _constants: string[] 292 | 293 | if (keyword.length === 1) { 294 | const c = keyword[0].toLowerCase() 295 | fns = funcs.filter((func, i) => 296 | funcs_lower[i].startsWith(c) 297 | ) 298 | _constants = constants.filter((constant, i) => 299 | constants_lower[i].startsWith(c) 300 | ) 301 | } else { 302 | const keyword_lower = keyword.toLowerCase() 303 | 304 | fns = funcs.filter((func, i) => { 305 | const func_lower = funcs_lower[i] 306 | let j = 0 307 | for (const c of keyword_lower) { 308 | j = func_lower.indexOf(c, j) + 1 309 | if (!j) // 找不到则 j === 0 310 | return false 311 | } 312 | 313 | return true 314 | }) 315 | 316 | _constants = constants.filter((constant, i) => { 317 | const constant_lower = constants_lower[i] 318 | let j = 0 319 | for (const c of keyword_lower) { 320 | j = constant_lower.indexOf(c, j) + 1 321 | if (!j) // 找不到则 j === 0 322 | return false 323 | } 324 | 325 | return true 326 | }) 327 | } 328 | 329 | return { 330 | suggestions: [ 331 | ...keywords.filter(kw => 332 | kw.startsWith(keyword) 333 | ).map(kw => ({ 334 | label: kw, 335 | insertText: kw, 336 | kind: CompletionItemKind.Keyword, 337 | }) as monacoapi.languages.CompletionItem), 338 | ... _constants.map(constant => ({ 339 | label: constant, 340 | insertText: constant, 341 | kind: CompletionItemKind.Constant 342 | }) as monacoapi.languages.CompletionItem), 343 | ...fns.map(fn => ({ 344 | label: fn, 345 | insertText: fn, 346 | kind: CompletionItemKind.Function, 347 | }) as monacoapi.languages.CompletionItem), 348 | ] 349 | } 350 | }, 351 | 352 | resolveCompletionItem (item, canceller) { 353 | if (canceller.isCancellationRequested) 354 | return 355 | 356 | // @ts-ignore 357 | item.documentation = get_func_md(item.label as string) 358 | 359 | return item 360 | } 361 | }) 362 | 363 | languages.registerHoverProvider('dolphindb', { 364 | // @ts-ignore 365 | provideHover (doc, pos, canceller) { 366 | if (canceller.isCancellationRequested) 367 | return 368 | 369 | const word = doc.getWordAtPosition(pos) 370 | 371 | if (!word) 372 | return 373 | 374 | const md = get_func_md(word.word) 375 | 376 | if (!md) 377 | return 378 | 379 | return { 380 | contents: [md] 381 | } 382 | } 383 | }) 384 | 385 | languages.registerSignatureHelpProvider('dolphindb', { 386 | signatureHelpTriggerCharacters: ['(', ','], 387 | 388 | // @ts-ignore 389 | provideSignatureHelp (doc, pos, canceller, ctx) { 390 | if (canceller.isCancellationRequested) 391 | return 392 | 393 | // @ts-ignore 394 | const { func_name, param_search_pos } = find_func_start(doc, pos) 395 | if (param_search_pos === -1) 396 | return 397 | 398 | // @ts-ignore 399 | const index = find_active_param_index(doc, pos, param_search_pos) 400 | if (index === -1) 401 | return 402 | 403 | const signature_and_params = get_signature_and_params(func_name) 404 | if (!signature_and_params) 405 | return 406 | 407 | const { signature, params } = signature_and_params 408 | 409 | return { 410 | dispose () { }, 411 | 412 | value: { 413 | activeParameter: index > params.length - 1 ? params.length - 1 : index, 414 | signatures: [{ 415 | label: signature, 416 | documentation: get_func_md(func_name), 417 | parameters: params.map(param => ({ 418 | label: param 419 | })) 420 | }], 421 | activeSignature: 0, 422 | } 423 | } 424 | } 425 | }) 426 | 427 | 428 | await document.fonts.ready 429 | })() 430 | }} 431 | 432 | onEditorDidMount={async (editor, monaco) => { 433 | await rpinit.current 434 | 435 | editor.setValue(code || '') 436 | 437 | editor.getModel().onDidChangeContent(event => { 438 | onChange({ 439 | refId, 440 | is_streaming, 441 | code: editor.getValue().replaceAll('\r\n', '\n'), 442 | streaming 443 | }) 444 | }) 445 | 446 | 447 | editor.addAction({ 448 | id: 'duplicate_line', 449 | 450 | keybindings: [ 451 | monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyD 452 | ], 453 | 454 | label: t('向下复制行'), 455 | 456 | // @ts-ignore 457 | async run (editor: monacoapi.editor.IStandaloneCodeEditor) { 458 | await editor.getAction('editor.action.copyLinesDownAction').run() 459 | } 460 | }) 461 | 462 | editor.addAction({ 463 | id: 'delete_lines', 464 | 465 | keybindings: [ 466 | monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyY 467 | ], 468 | 469 | label: t('删除行'), 470 | 471 | // @ts-ignore 472 | async run (editor: monacoapi.editor.IStandaloneCodeEditor) { 473 | await editor.getAction('editor.action.deleteLines').run() 474 | } 475 | }) 476 | 477 | 478 | let { widget } = editor.getContribution('editor.contrib.suggestController') as any 479 | 480 | if (widget) { 481 | const { value: suggest_widget } = widget 482 | suggest_widget._setDetailsVisible(true) 483 | // suggest_widget._persistedSize.store({ 484 | // width: 200, 485 | // height: 256 486 | // }) 487 | } 488 | 489 | 490 | registry.setTheme(isDark ? theme_dark : theme_light) 491 | 492 | inject_css() 493 | }} 494 | /> 495 | 496 | 497 | { tip &&
{t('在编辑器中按 Ctrl + S 可暂存查询并刷新结果')}
} 498 |
499 | } 500 | 501 | 502 | 503 | // ------------ tokenizer 504 | // 方法来自: https://github.com/bolinfest/monaco-tm/ 505 | 506 | interface ScopeNameInfo { 507 | /** 508 | If set, this is the id of an ILanguageExtensionPoint. This establishes the 509 | mapping from a MonacoLanguage to a TextMate grammar. 510 | */ 511 | language?: string 512 | 513 | /** 514 | Scopes that are injected *into* this scope. For example, the 515 | `text.html.markdown` scope likely has a number of injections to support 516 | fenced code blocks. 517 | */ 518 | injections?: string[] 519 | } 520 | 521 | 522 | interface DemoScopeNameInfo extends ScopeNameInfo { 523 | path: string 524 | } 525 | 526 | 527 | const grammars: { 528 | [scopeName: string]: DemoScopeNameInfo 529 | } = { 530 | 'source.dolphindb': { 531 | language: 'dolphindb', 532 | path: 'dolphindb.tmLanguage.json' 533 | } 534 | } 535 | 536 | 537 | let registry = new Registry({ 538 | onigLib: Promise.resolve({ createOnigScanner, createOnigString }), 539 | 540 | async loadGrammar (scopeName: string): Promise { 541 | const scopeNameInfo = grammars[scopeName] 542 | // eslint-disable-next-line 543 | if (scopeNameInfo == null) 544 | return null 545 | 546 | const grammar_text: string = JSON.stringify(tm_language) 547 | 548 | // If this is a JSON grammar, filePath must be specified with a `.json` 549 | // file extension or else parseRawGrammar() will assume it is a PLIST 550 | // grammar. 551 | return parseRawGrammar(grammar_text, 'dolphindb.json') 552 | }, 553 | 554 | /** 555 | For the given scope, returns a list of additional grammars that should be 556 | "injected into" it (i.e., a list of grammars that want to extend the 557 | specified `scopeName`). The most common example is other grammars that 558 | want to "inject themselves" into the `text.html.markdown` scope so they 559 | can be used with fenced code blocks. 560 | 561 | In the manifest of a VS Code extension, grammar signals that it wants 562 | to do this via the "injectTo" property: 563 | https://code.visualstudio.com/api/language-extensions/syntax-highlight-guide#injection-grammars 564 | */ 565 | getInjections (scopeName: string): string[] | undefined { 566 | const grammar = grammars[scopeName] 567 | return grammar ? grammar.injections : undefined 568 | }, 569 | }) 570 | 571 | 572 | class TokensProviderCache { 573 | private scopeNameToGrammar: Map> = new Map() 574 | 575 | constructor (private registry: Registry) { } 576 | 577 | async createEncodedTokensProvider (scopeName: string, encodedLanguageId: number): Promise { 578 | const grammar = await this.getGrammar(scopeName, encodedLanguageId) 579 | 580 | return { 581 | getInitialState () { 582 | return INITIAL 583 | }, 584 | 585 | tokenizeEncoded (line: string, state: monacoapi.languages.IState): monacoapi.languages.IEncodedLineTokens { 586 | const tokenizeLineResult2 = grammar.tokenizeLine2(line, state as StateStack) 587 | const { tokens, ruleStack: endState } = tokenizeLineResult2 588 | return { tokens, endState } 589 | } 590 | } 591 | } 592 | 593 | async getGrammar (scopeName: string, encodedLanguageId: number): Promise { 594 | const grammar = this.scopeNameToGrammar.get(scopeName) 595 | // eslint-disable-next-line 596 | if (grammar != null) 597 | return grammar 598 | 599 | 600 | // This is defined in vscode-textmate and has optional embeddedLanguages 601 | // and tokenTypes fields that might be useful/necessary to take advantage of 602 | // at some point. 603 | const grammarConfiguration = { } 604 | 605 | // We use loadGrammarWithConfiguration() rather than loadGrammar() because 606 | // we discovered that if the numeric LanguageId is not specified, then it 607 | // does not get encoded in the TokenMetadata. 608 | // 609 | // Failure to do so means that the LanguageId cannot be read back later, 610 | // which can cause other Monaco features, such as "Toggle Line Comment", 611 | // to fail. 612 | const promise = this.registry 613 | .loadGrammarWithConfiguration(scopeName, encodedLanguageId, grammarConfiguration) 614 | .then((grammar: IGrammar | null) => { 615 | if (grammar) 616 | return grammar 617 | else 618 | throw Error(`failed to load grammar for ${scopeName}`) 619 | }) 620 | this.scopeNameToGrammar.set(scopeName, promise) 621 | return promise 622 | } 623 | } 624 | 625 | 626 | function create_style_element_for_colors_css (): HTMLStyleElement { 627 | // We want to ensure that our --------------------------------------------------------------------------------