├── .dockerignore ├── .env.development ├── .env.production ├── .env.staging ├── .env.test ├── .eslintignore ├── .eslintrc-auto-import.json ├── .eslintrc.js ├── .github ├── dependabot.yml └── workflows │ ├── docker-manual-build.yml │ ├── docker-release.yml │ ├── node.js.yml │ └── release.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierignore ├── .prettierrc.js ├── .stylelintrc.js ├── .vscode └── settings.json ├── AWSCLIV2.pkg ├── LICENSE ├── README.md ├── babel.config.js ├── commitlint.config.js ├── config ├── plugin │ ├── arcoResolver.ts │ ├── compress.ts │ ├── imagemin.ts │ ├── styleImport.ts │ └── visualizer.ts ├── utils │ └── index.ts ├── vite.config.base.ts ├── vite.config.dev.ts └── vite.config.prod.ts ├── docker ├── Dockerfile ├── Dockerfile.runtimeonly ├── README.md ├── docker-compose.yml └── greptimedb.conf ├── index.html ├── package.json ├── pnpm-lock.yaml ├── screenshot1.png ├── screenshot2.png ├── shims.d.ts ├── src-tauri ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── build.rs ├── capabilities │ ├── default.json │ └── desktop.json ├── icons │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── 32x32.png │ ├── 64x64.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── StoreLogo.png │ ├── icon.icns │ ├── icon.ico │ └── icon.png ├── src │ ├── app_plugin.rs │ ├── lib.rs │ └── main.rs └── tauri.conf.json ├── src ├── App.vue ├── api │ ├── axios.d.ts │ ├── editor.ts │ ├── gist.ts │ ├── interceptor.ts │ ├── message.ts │ ├── pipeline.ts │ ├── playground.ts │ └── status.ts ├── assets │ ├── favicon.ico │ ├── fonts │ │ ├── Gilroy-ExtraBold.otf │ │ ├── Gilroy-Regular.otf │ │ ├── OpenSans-ExtraBold.ttf │ │ ├── OpenSans-Regular.ttf │ │ └── OpenSans-SemiBold.ttf │ ├── icons.js │ ├── images │ │ └── logo-text.png │ ├── style │ │ ├── breakpoint.less │ │ ├── button.less │ │ ├── custom-block.less │ │ ├── dataView.less │ │ ├── docs.less │ │ ├── editor.less │ │ ├── form.less │ │ ├── global.less │ │ ├── layout.less │ │ ├── list.less │ │ ├── log.less │ │ ├── new.less │ │ ├── select.less │ │ ├── tableList.less │ │ ├── tour.less │ │ ├── vars.less │ │ └── vp-doc.less │ ├── svg-list.ts │ └── world.json ├── components │ ├── breadcrumb │ │ └── index.vue │ ├── chart │ │ ├── chartTheme.json │ │ └── index.vue │ ├── empty-status.vue │ ├── footer │ │ └── index.vue │ ├── global-setting │ │ └── index.vue │ ├── guide-modal │ │ └── index.vue │ ├── index.ts │ ├── markdown-render │ │ ├── components │ │ │ ├── code-editor.vue │ │ │ ├── importPresets.vue │ │ │ └── utils.ts │ │ ├── composables │ │ │ └── codeGroups.ts │ │ ├── index.vue │ │ └── plugins │ │ │ ├── containerPlugin.ts │ │ │ ├── customButton.ts │ │ │ ├── customCode.ts │ │ │ ├── customComment.ts │ │ │ ├── customImage.ts │ │ │ └── index.ts │ ├── menu │ │ ├── index.vue │ │ └── use-menu-tree.ts │ ├── navbar │ │ └── index.vue │ ├── refresh-playground-modal │ │ └── index.vue │ ├── short-cut │ │ └── index.vue │ ├── simple-markdown │ │ ├── components │ │ │ └── simple-code-editor.vue │ │ ├── index.vue │ │ └── plugins │ │ │ ├── customCode.ts │ │ │ └── index.ts │ ├── social-link │ │ └── index.vue │ ├── status-bar │ │ └── status-list.vue │ ├── tab-bar │ │ ├── index.vue │ │ ├── readme.md │ │ └── tab-item.vue │ ├── table-menu │ │ └── index.vue │ ├── text-copyable.vue │ ├── time-select │ │ └── index.vue │ └── yml-editor.vue ├── config │ └── settings.json ├── directive │ ├── index.ts │ └── permission │ │ └── index.ts ├── env.d.ts ├── hooks │ ├── chart-option.ts │ ├── data-chart.ts │ ├── gist.ts │ ├── index.ts │ ├── loading.ts │ ├── locale.ts │ ├── log.ts │ ├── permission.ts │ ├── python-code.ts │ ├── query-code.ts │ ├── request.ts │ ├── responsive.ts │ ├── sider-tabs.ts │ ├── themes.ts │ ├── types.ts │ ├── user.ts │ └── visible.ts ├── layout │ ├── default-layout.vue │ └── page-layout.vue ├── locale │ ├── en-US.ts │ ├── en-US │ │ ├── dashboard.ts │ │ ├── logquery.ts │ │ ├── menu.ts │ │ ├── playground.ts │ │ └── settings.ts │ ├── index.ts │ ├── zh-CN.ts │ └── zh-CN │ │ ├── dashboard.ts │ │ ├── logquery.ts │ │ ├── menu.ts │ │ ├── playground.ts │ │ └── settings.ts ├── main.ts ├── router │ ├── app-menus │ │ └── index.ts │ ├── constants.ts │ ├── guard │ │ ├── index.ts │ │ ├── permission.ts │ │ └── userLoginInfo.ts │ ├── index.ts │ ├── routes │ │ ├── base.ts │ │ ├── client.ts │ │ ├── externalModules │ │ │ ├── arco.ts │ │ │ └── faq.ts │ │ ├── index.ts │ │ ├── modules │ │ │ └── dashboard.ts │ │ └── types.ts │ └── typings.d.ts ├── store │ ├── index.ts │ └── modules │ │ ├── app │ │ ├── index.ts │ │ └── types.ts │ │ ├── code-run │ │ ├── index.ts │ │ └── types.ts │ │ ├── database │ │ ├── index.ts │ │ └── types.ts │ │ ├── ingest │ │ └── index.ts │ │ ├── log │ │ ├── index.ts │ │ └── types.ts │ │ ├── logquery │ │ └── index.ts │ │ ├── status-bar │ │ └── index.ts │ │ ├── tab-bar │ │ ├── index.ts │ │ └── types.ts │ │ └── user │ │ ├── index.ts │ │ └── types.ts ├── tauri │ ├── about.vue │ ├── index.js │ ├── layout.vue │ └── menu.js ├── types │ ├── echarts.ts │ └── global.ts ├── utils │ ├── env.ts │ ├── event.ts │ ├── index.ts │ ├── is.ts │ ├── monitor.ts │ ├── route-listener.ts │ └── sql.ts └── views │ ├── dashboard │ ├── config.ts │ ├── ingest │ │ ├── doc.md │ │ ├── index.vue │ │ ├── influxdb │ │ │ ├── index.vue │ │ │ ├── input.vue │ │ │ └── upload.vue │ │ ├── panel-icon.vue │ │ └── top-bar.vue │ ├── logs │ │ ├── pipelines │ │ │ ├── PipeFileView.vue │ │ │ └── index.vue │ │ └── query │ │ │ ├── ChartContainer.vue │ │ │ ├── CountChart.vue │ │ │ ├── ExportLog.vue │ │ │ ├── FormView.vue │ │ │ ├── FunnelChart.vue │ │ │ ├── InputEditor.vue │ │ │ ├── JSONView.vue │ │ │ ├── LogDetail.vue │ │ │ ├── Pagination.vue │ │ │ ├── SQLBuilder.vue │ │ │ ├── SavedQuery.vue │ │ │ ├── TableData.vue │ │ │ ├── Toolbar.vue │ │ │ ├── index.vue │ │ │ ├── types.ts │ │ │ └── until.ts │ ├── modules │ │ ├── data-view │ │ │ ├── components │ │ │ │ ├── data-chart.vue │ │ │ │ └── data-grid.vue │ │ │ └── index.vue │ │ ├── explain │ │ │ ├── explain-chart │ │ │ │ ├── chart-controls.vue │ │ │ │ ├── index.vue │ │ │ │ ├── navigation-arrows.vue │ │ │ │ ├── plan-card.vue │ │ │ │ ├── tree-view.vue │ │ │ │ └── zoom-controls.vue │ │ │ ├── explain-grid.vue │ │ │ └── utils.ts │ │ ├── favorite.vue │ │ ├── list-tabs.vue │ │ ├── log.vue │ │ ├── logs-layout.vue │ │ ├── logs-new.vue │ │ ├── logs.vue │ │ ├── query-modal │ │ │ └── index.vue │ │ ├── scripts-list.vue │ │ ├── table-list.vue │ │ └── table-manager.vue │ ├── playground │ │ ├── docs │ │ │ ├── getting-started.md │ │ │ └── host-metrics-promql.md │ │ └── index.vue │ ├── query │ │ ├── editor.vue │ │ └── index.vue │ ├── scripts │ │ ├── index.vue │ │ └── py-editor.vue │ └── status │ │ └── index.vue │ ├── not-found │ └── index.vue │ └── redirect │ └── index.vue └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | # macOS trash 2 | .DS_Store 3 | 4 | # Visual Studio Code 5 | .vscode/ 6 | .devcontainer/ 7 | 8 | # Eclipse files 9 | .classpath 10 | .project 11 | .settings/** 12 | 13 | # Vim swap files 14 | *.swp 15 | 16 | # Files generated by JetBrains IDEs, e.g. IntelliJ IDEA 17 | .idea/ 18 | *.iml 19 | out/ 20 | 21 | # Rust 22 | target/ 23 | 24 | # Node 25 | node_modules/ 26 | 27 | # Git 28 | .git 29 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | VITE_CLOUD_URL='https://dev.greptime-cloud-frontend.pages.dev' 2 | VITE_API_BASE_URL='https://api-preview.greptime.cloud' 3 | VITE_ROLE='admin' 4 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | VITE_CLOUD_URL='https://dev.greptime-cloud-frontend.pages.dev' 2 | VITE_API_BASE_URL='https://api-preview.greptime.cloud' 3 | VITE_ROLE='admin' -------------------------------------------------------------------------------- /.env.staging: -------------------------------------------------------------------------------- 1 | VITE_CLOUD_URL='https://console.greptime.cloud' 2 | VITE_API_BASE_URL='https://api.greptime.cloud' 3 | VITE_ROLE='cloud' -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | VITE_CLOUD_URL='https://dev.greptime-cloud-frontend.pages.dev' 2 | VITE_API_BASE_URL='https://api-preview.greptime.cloud' 3 | VITE_ROLE='cloud' -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /*.json 2 | /*.js 3 | dist 4 | **/icons.js -------------------------------------------------------------------------------- /.eslintrc-auto-import.json: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "Component": true, 4 | "ComponentPublicInstance": true, 5 | "ComputedRef": true, 6 | "EffectScope": true, 7 | "InjectionKey": true, 8 | "PropType": true, 9 | "Ref": true, 10 | "VNode": true, 11 | "WritableComputedRef": true, 12 | "acceptHMRUpdate": true, 13 | "chartOption": true, 14 | "computed": true, 15 | "createApp": true, 16 | "createPinia": true, 17 | "customRef": true, 18 | "dataChart": true, 19 | "defineAsyncComponent": true, 20 | "defineComponent": true, 21 | "defineStore": true, 22 | "effectScope": true, 23 | "getActivePinia": true, 24 | "getCurrentInstance": true, 25 | "getCurrentScope": true, 26 | "gist": true, 27 | "h": true, 28 | "inject": true, 29 | "isProxy": true, 30 | "isReactive": true, 31 | "isReadonly": true, 32 | "isRef": true, 33 | "loading": true, 34 | "locale": true, 35 | "log": true, 36 | "mapActions": true, 37 | "mapGetters": true, 38 | "mapState": true, 39 | "mapStores": true, 40 | "mapWritableState": true, 41 | "markRaw": true, 42 | "nextTick": true, 43 | "onActivated": true, 44 | "onBeforeMount": true, 45 | "onBeforeRouteLeave": true, 46 | "onBeforeRouteUpdate": true, 47 | "onBeforeUnmount": true, 48 | "onBeforeUpdate": true, 49 | "onDeactivated": true, 50 | "onErrorCaptured": true, 51 | "onMounted": true, 52 | "onRenderTracked": true, 53 | "onRenderTriggered": true, 54 | "onScopeDispose": true, 55 | "onServerPrefetch": true, 56 | "onUnmounted": true, 57 | "onUpdated": true, 58 | "permission": true, 59 | "provide": true, 60 | "pythonCode": true, 61 | "queryCode": true, 62 | "reactive": true, 63 | "readonly": true, 64 | "ref": true, 65 | "request": true, 66 | "resolveComponent": true, 67 | "responsive": true, 68 | "setActivePinia": true, 69 | "setMapStoreSuffix": true, 70 | "shallowReactive": true, 71 | "shallowReadonly": true, 72 | "shallowRef": true, 73 | "siderTabs": true, 74 | "store": true, 75 | "storeToRefs": true, 76 | "stringType": true, 77 | "themes": true, 78 | "toRaw": true, 79 | "toRef": true, 80 | "toRefs": true, 81 | "toValue": true, 82 | "triggerRef": true, 83 | "unref": true, 84 | "useAppStore": true, 85 | "useAttrs": true, 86 | "useChartOption": true, 87 | "useCodeRunStore": true, 88 | "useCssModule": true, 89 | "useCssVars": true, 90 | "useDataBaseStore": true, 91 | "useGist": true, 92 | "useIngestStore": true, 93 | "useLink": true, 94 | "useLog": true, 95 | "useLogStore": true, 96 | "useQueryCode": true, 97 | "useRoute": true, 98 | "useRouter": true, 99 | "useSlots": true, 100 | "useStatusBarStore": true, 101 | "useTabBarStore": true, 102 | "useUserStore": true, 103 | "visible": true, 104 | "watch": true, 105 | "watchEffect": true, 106 | "watchPostEffect": true, 107 | "watchSyncEffect": true 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const path = require('path') 3 | 4 | module.exports = { 5 | root: true, 6 | parser: 'vue-eslint-parser', 7 | parserOptions: { 8 | // Parser that checks the content of the 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreptimeTeam/dashboard/5d6f0258e287a4640ea013f6dac60a73b50edada/screenshot1.png -------------------------------------------------------------------------------- /screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreptimeTeam/dashboard/5d6f0258e287a4640ea013f6dac60a73b50edada/screenshot2.png -------------------------------------------------------------------------------- /shims.d.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | declare global { 3 | interface Window { 4 | grecaptcha: any 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | /gen/schemas 5 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "app" 3 | version = "0.1.0" 4 | description = "A Tauri App" 5 | authors = ["you"] 6 | license = "" 7 | repository = "" 8 | edition = "2021" 9 | rust-version = "1.77.2" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [lib] 14 | name = "app_lib" 15 | crate-type = ["staticlib", "cdylib", "rlib"] 16 | 17 | [build-dependencies] 18 | tauri-build = { version = "2.0.4", features = [] } 19 | 20 | [dependencies] 21 | serde_json = "1.0" 22 | serde = { version = "1.0", features = ["derive"] } 23 | log = "0.4" 24 | tauri = { version = "2.2.3", features = ["tray-icon", "devtools"] } 25 | tauri-plugin-log = "2.0.0-rc" 26 | tauri-plugin-dialog = "2" 27 | tauri-plugin-process = "2" 28 | 29 | [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] 30 | tauri-plugin-updater = "2" 31 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/capabilities/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "default", 4 | "description": "enables the default permissions", 5 | "windows": ["main", "client"], 6 | "permissions": ["core:default", "updater:default","dialog:default","process:default", "core:webview:allow-create-webview-window"] 7 | } 8 | -------------------------------------------------------------------------------- /src-tauri/capabilities/desktop.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "desktop-capability", 3 | "platforms": [ 4 | "macOS", 5 | "windows", 6 | "linux" 7 | ], 8 | "windows": [ 9 | "main" 10 | ], 11 | "permissions": [ 12 | "updater:default" 13 | ] 14 | } -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreptimeTeam/dashboard/5d6f0258e287a4640ea013f6dac60a73b50edada/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreptimeTeam/dashboard/5d6f0258e287a4640ea013f6dac60a73b50edada/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreptimeTeam/dashboard/5d6f0258e287a4640ea013f6dac60a73b50edada/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreptimeTeam/dashboard/5d6f0258e287a4640ea013f6dac60a73b50edada/src-tauri/icons/64x64.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreptimeTeam/dashboard/5d6f0258e287a4640ea013f6dac60a73b50edada/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreptimeTeam/dashboard/5d6f0258e287a4640ea013f6dac60a73b50edada/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreptimeTeam/dashboard/5d6f0258e287a4640ea013f6dac60a73b50edada/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreptimeTeam/dashboard/5d6f0258e287a4640ea013f6dac60a73b50edada/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreptimeTeam/dashboard/5d6f0258e287a4640ea013f6dac60a73b50edada/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreptimeTeam/dashboard/5d6f0258e287a4640ea013f6dac60a73b50edada/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreptimeTeam/dashboard/5d6f0258e287a4640ea013f6dac60a73b50edada/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreptimeTeam/dashboard/5d6f0258e287a4640ea013f6dac60a73b50edada/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreptimeTeam/dashboard/5d6f0258e287a4640ea013f6dac60a73b50edada/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreptimeTeam/dashboard/5d6f0258e287a4640ea013f6dac60a73b50edada/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreptimeTeam/dashboard/5d6f0258e287a4640ea013f6dac60a73b50edada/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreptimeTeam/dashboard/5d6f0258e287a4640ea013f6dac60a73b50edada/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreptimeTeam/dashboard/5d6f0258e287a4640ea013f6dac60a73b50edada/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/src/app_plugin.rs: -------------------------------------------------------------------------------- 1 | use tauri::{ 2 | plugin::{Builder, TauriPlugin}, 3 | Runtime, 4 | }; 5 | 6 | #[tauri::command] 7 | fn is_running() -> bool { 8 | // Your logic here (return true if the app is running) 9 | true 10 | } 11 | 12 | pub fn init() -> TauriPlugin { 13 | Builder::new("app") // This must match `plugin:app|is_running` 14 | .invoke_handler(tauri::generate_handler![is_running]) 15 | .build() 16 | } 17 | -------------------------------------------------------------------------------- /src-tauri/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg_attr(mobile, tauri::mobile_entry_point)] 2 | pub mod app_plugin; 3 | 4 | pub fn run() { 5 | tauri::Builder::default() 6 | .plugin(tauri_plugin_process::init()) 7 | .plugin(tauri_plugin_dialog::init()) 8 | .plugin(tauri_plugin_updater::Builder::new().build()) 9 | .plugin(app_plugin::init()) 10 | .setup(|app| { 11 | if cfg!(debug_assertions) { 12 | app.handle().plugin( 13 | tauri_plugin_log::Builder::default() 14 | .level(log::LevelFilter::Info) 15 | .build(), 16 | )?; 17 | } 18 | Ok(()) 19 | }) 20 | .run(tauri::generate_context!()) 21 | .expect("error while running tauri application"); 22 | } 23 | 24 | // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ 25 | -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | fn main() { 5 | app_lib::run(); 6 | } 7 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", 3 | "productName": "Greptime Dashboard", 4 | "version": "0.8.21", 5 | "identifier": "com.greptime", 6 | "build": { 7 | "frontendDist": "../dist", 8 | "devUrl": "http://localhost:5177", 9 | "beforeDevCommand": "pnpm dev", 10 | "beforeBuildCommand": "pnpm build" 11 | }, 12 | "app": { 13 | "windows": [ 14 | { 15 | "title": "Greptime Dashboard", 16 | "width": 1200, 17 | "height": 800, 18 | "resizable": true, 19 | "fullscreen": false, 20 | "devtools": true 21 | } 22 | ], 23 | "security": { 24 | "csp": null 25 | } 26 | }, 27 | 28 | "bundle": { 29 | "active": true, 30 | "targets": "all", 31 | "createUpdaterArtifacts": true, 32 | "icon": [ 33 | "icons/32x32.png", 34 | "icons/128x128.png", 35 | "icons/128x128@2x.png", 36 | "icons/icon.icns", 37 | "icons/icon.ico" 38 | ], 39 | "macOS": { 40 | "entitlements": null, 41 | "exceptionDomain": "", 42 | "frameworks": [], 43 | "minimumSystemVersion": "10.13", 44 | "signingIdentity": "sun-code" 45 | } 46 | }, 47 | "plugins": { 48 | "updater": { 49 | "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDU2MzI1OTE1MUU0RjgzRjkKUldUNWcwOGVGVmt5VnIxOFpiMkxFK1l5dGsvczhzQ29YR1N0dXkzT0Z0TTB1OGY1eVlqTVQ2TXUK", 50 | "endpoints": ["https://github.com/GreptimeTeam/dashboard/releases/latest/download/latest.json"] 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 68 | -------------------------------------------------------------------------------- /src/api/axios.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-shadow */ 2 | import { AxiosRequestConfig } from 'axios' 3 | 4 | declare module 'axios' { 5 | export interface AxiosRequestConfig { 6 | traceTimeStart?: number 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/api/gist.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | /* eslint-disable import/prefer-default-export */ 4 | export const getGist = (gistId: string) => { 5 | return axios.get(`https://api.github.com/gists/${gistId}`) 6 | } 7 | -------------------------------------------------------------------------------- /src/api/message.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | export interface MessageRecord { 4 | id: number 5 | type: string 6 | title: string 7 | subTitle: string 8 | avatar?: string 9 | content: string 10 | time: string 11 | status: 0 | 1 12 | messageType?: number 13 | } 14 | export type MessageListType = MessageRecord[] 15 | 16 | export function queryMessageList() { 17 | return axios.post('/message/list') 18 | } 19 | 20 | interface MessageStatus { 21 | ids: number[] 22 | } 23 | 24 | export function setMessageStatus(data: MessageStatus) { 25 | return axios.post('/message/read', data) 26 | } 27 | 28 | export interface ChatRecord { 29 | id: number 30 | username: string 31 | content: string 32 | time: string 33 | isCollect: boolean 34 | } 35 | 36 | export function queryChatList() { 37 | return axios.post('/chat/list') 38 | } 39 | -------------------------------------------------------------------------------- /src/api/playground.ts: -------------------------------------------------------------------------------- 1 | import { importDeclaration } from '@babel/types' 2 | import axios, { AxiosRequestConfig } from 'axios' 3 | import qs from 'qs' 4 | 5 | const BASE_URL = import.meta.env.VITE_API_BASE_URL 6 | const PLAYGROUND_URL = `${BASE_URL}/playground/db` 7 | 8 | export const getPlaygroundInfo = (dbId: string) => { 9 | return axios.get(PLAYGROUND_URL, { 10 | params: { 11 | db_id: dbId, 12 | }, 13 | } as AxiosRequestConfig) 14 | } 15 | 16 | export const createPlayground = (token: string) => { 17 | return axios.post(PLAYGROUND_URL, {}, { 18 | params: { 19 | token, 20 | }, 21 | } as AxiosRequestConfig) 22 | } 23 | 24 | export const importPresets = (db: string, table: string, from: string) => { 25 | return axios.post('/v1/import-presets', {}, { 26 | params: { 27 | db, 28 | from, 29 | table, 30 | }, 31 | } as AxiosRequestConfig) 32 | } 33 | -------------------------------------------------------------------------------- /src/api/status.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | const statusUrl = `/status` 4 | 5 | /* eslint-disable import/prefer-default-export */ 6 | export const getStatus = () => { 7 | return axios.get(statusUrl) 8 | } 9 | -------------------------------------------------------------------------------- /src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreptimeTeam/dashboard/5d6f0258e287a4640ea013f6dac60a73b50edada/src/assets/favicon.ico -------------------------------------------------------------------------------- /src/assets/fonts/Gilroy-ExtraBold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreptimeTeam/dashboard/5d6f0258e287a4640ea013f6dac60a73b50edada/src/assets/fonts/Gilroy-ExtraBold.otf -------------------------------------------------------------------------------- /src/assets/fonts/Gilroy-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreptimeTeam/dashboard/5d6f0258e287a4640ea013f6dac60a73b50edada/src/assets/fonts/Gilroy-Regular.otf -------------------------------------------------------------------------------- /src/assets/fonts/OpenSans-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreptimeTeam/dashboard/5d6f0258e287a4640ea013f6dac60a73b50edada/src/assets/fonts/OpenSans-ExtraBold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreptimeTeam/dashboard/5d6f0258e287a4640ea013f6dac60a73b50edada/src/assets/fonts/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /src/assets/fonts/OpenSans-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreptimeTeam/dashboard/5d6f0258e287a4640ea013f6dac60a73b50edada/src/assets/fonts/OpenSans-SemiBold.ttf -------------------------------------------------------------------------------- /src/assets/images/logo-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreptimeTeam/dashboard/5d6f0258e287a4640ea013f6dac60a73b50edada/src/assets/images/logo-text.png -------------------------------------------------------------------------------- /src/assets/style/breakpoint.less: -------------------------------------------------------------------------------- 1 | // ==============breakpoint============ 2 | 3 | // Extra small screen / phone 4 | @screen-xs: 480px; 5 | 6 | // Small screen / tablet 7 | @screen-sm: 576px; 8 | 9 | // Medium screen / desktop 10 | @screen-md: 768px; 11 | 12 | // Large screen / wide desktop 13 | @screen-lg: 992px; 14 | 15 | // Extra large screen / full hd 16 | @screen-xl: 1200px; 17 | 18 | // Extra extra large screen / large desktop 19 | @screen-xxl: 1600px; 20 | -------------------------------------------------------------------------------- /src/assets/style/button.less: -------------------------------------------------------------------------------- 1 | .arco-btn { 2 | padding: 7px 14px; 3 | font-size: 13px; 4 | line-height: 16px; 5 | border-radius: 4px; 6 | } 7 | 8 | .arco-btn-primary[type='button'] { 9 | color: var(--white-font-color); 10 | background-color: var(--brand-color); 11 | } 12 | 13 | .arco-btn-primary[type='button']:hover { 14 | background-color: var(--hover-brand-color); 15 | } 16 | 17 | .arco-btn-primary[type='button'].arco-btn-loading { 18 | background-color: var(--brand-color); 19 | } 20 | 21 | .arco-btn-secondary[type='button'] { 22 | color: var(--brand-color); 23 | background-color: var(--light-brand-color); 24 | } 25 | 26 | .arco-btn-secondary[type='button'].arco-btn-loading { 27 | color: var(--brand-color); 28 | background-color: var(--light-brand-color); 29 | } 30 | 31 | .arco-btn-secondary[type='button']:hover { 32 | color: var(--brand-color); 33 | background-color: var(--light-brand-color); 34 | border: 1px solid var(--brand-color); 35 | } 36 | 37 | .arco-btn-secondary.arco-btn-status-danger { 38 | color: var(--danger-color); 39 | background: var(--danger-bg-color); 40 | } 41 | 42 | .arco-btn-secondary.arco-btn-status-danger:hover { 43 | color: var(--danger-color); 44 | background-color: var(--danger-bg-hover-color); 45 | border-color: transparent; 46 | } 47 | 48 | .arco-btn-text[type='button'] { 49 | color: var(--brand-color); 50 | } 51 | 52 | .arco-btn-text[type='button']:hover { 53 | color: var(--brand-color); 54 | background-color: var(--th-bg-color); 55 | } 56 | 57 | .arco-btn-outline, 58 | .arco-btn-outline[type='button'], 59 | .arco-btn-outline[type='submit'] { 60 | color: var(--main-font-color); 61 | border: 1px solid var(--border-color); 62 | } 63 | 64 | .arco-btn-outline:hover, 65 | .arco-btn-outline[type='button']:hover, 66 | .arco-btn-outline[type='submit']:hover, 67 | .arco-btn-outline.arco-btn-loading, 68 | .arco-btn-outline[type='button'].arco-btn-loading { 69 | color: var(--brand-color); 70 | border-color: var(--brand-color); 71 | } 72 | 73 | .arco-btn-primary.arco-btn-disabled, 74 | .arco-btn-primary[type='button'].arco-btn-disabled, 75 | .arco-btn-primary[type='submit'].arco-btn-disabled { 76 | color: rgb(156 156 156); 77 | background-color: rgb(240 240 240); 78 | } 79 | 80 | .arco-btn-secondary.arco-btn-disabled, 81 | .arco-btn-secondary[type='button'].arco-btn-disabled, 82 | .arco-btn-secondary[type='submit'].arco-btn-disabled { 83 | color: rgb(156 156 156); 84 | background-color: rgb(240 240 240); 85 | border: 1px solid transparent; 86 | } 87 | 88 | .arco-btn-outline.arco-btn-disabled, 89 | .arco-btn-outline[type='button'].arco-btn-disabled, 90 | .arco-btn-outline[type='submit'].arco-btn-disabled { 91 | color: rgb(156 156 156); 92 | background-color: rgb(240 240 240); 93 | border: 1px solid transparent; 94 | } 95 | 96 | .arco-btn-text.query-time-button, 97 | .arco-btn-text.query-time-button:hover { 98 | color: var(--small-font-color); 99 | background: var(--grey-bg-color); 100 | border: 1px solid transparent; 101 | padding: 0 10px; 102 | } 103 | 104 | .arco-btn-text.query-time-button:focus-within { 105 | background: transparent; 106 | border: 1px solid var(--hover-brand-color); 107 | } 108 | 109 | .arco-btn-size-medium:not(.arco-btn-only-icon) .arco-btn-icon { 110 | margin-right: 10px; 111 | } 112 | -------------------------------------------------------------------------------- /src/assets/style/docs.less: -------------------------------------------------------------------------------- 1 | .markdown-container { 2 | h1 { 3 | margin: 20px 0; 4 | color: var(--color-light-title-text); 5 | font-weight: bold; 6 | font-size: 36px; 7 | font-family: var(--vp-font-family-base); 8 | line-height: 47px; 9 | } 10 | 11 | h2 { 12 | margin: 60px 0 20px; 13 | padding-top: 30px; 14 | color: var(--color-light-title-text); 15 | font-weight: bold; 16 | font-size: 28px; 17 | font-family: var(--vp-font-family-base); 18 | line-height: 37px; 19 | } 20 | 21 | h3 { 22 | margin: 40px 0 20px; 23 | color: var(--color-light-title-text); 24 | font-weight: bold; 25 | font-size: 22px; 26 | font-family: var(--vp-font-family-base); 27 | line-height: 32px; 28 | } 29 | 30 | h4 { 31 | margin: 30px 0 20px; 32 | color: var(--color-light-title-text); 33 | font-weight: bold; 34 | font-size: 20px; 35 | line-height: 28px; 36 | opacity: 0.85; 37 | } 38 | 39 | p { 40 | margin: 20px 0; 41 | color: var(--color-p-text); 42 | font-size: 16px; 43 | line-height: 24px; 44 | } 45 | 46 | :not(pre) > code { 47 | color: var(--color-code); 48 | background: var(--color-code-bg); 49 | } 50 | 51 | a { 52 | color: var(--color-a-text); 53 | text-decoration: none; 54 | font-size: 16px; 55 | line-height: 24px; 56 | } 57 | 58 | a:hover { 59 | color: var(--color-a-hover); 60 | } 61 | 62 | ul { 63 | margin: 30px 0; 64 | } 65 | 66 | li { 67 | margin: 12px 0; 68 | color: var(--color-p-text); 69 | font-size: 16px; 70 | line-height: 24px; 71 | } 72 | 73 | pre { 74 | margin: 30px 0; 75 | padding: 20px; 76 | overflow: auto; 77 | color: var(--brand-color); 78 | font-size: 16px; 79 | line-height: 30px; 80 | background-color: var(--light-brand-color); 81 | border-radius: 4px; 82 | } 83 | 84 | code { 85 | padding: 0 4px; 86 | font-size: 16px; 87 | line-height: 30px; 88 | border-radius: 4px; 89 | } 90 | 91 | img { 92 | max-width: 100%; 93 | } 94 | 95 | div[class*='language-'] { 96 | background-color: var(--color-bg-code-table); 97 | border-radius: 4px; 98 | opacity: 0.9; 99 | 100 | pre { 101 | background-color: inherit; 102 | opacity: inherit; 103 | } 104 | 105 | code { 106 | font-size: 16px; 107 | line-height: 30px; 108 | background: inherit; 109 | opacity: inherit; 110 | } 111 | } 112 | } 113 | 114 | .vp-doc [class*='language-'] code &.wrap { 115 | white-space: break-spaces; 116 | } 117 | 118 | .playground-tabs { 119 | .arco-tabs-tab-active, 120 | .arco-tabs-tab-active:hover { 121 | color: var(--brand-color); 122 | } 123 | 124 | .arco-tabs-nav-ink { 125 | background-color: var(--brand-color); 126 | } 127 | 128 | .arco-tabs-nav { 129 | margin-top: 12px; 130 | } 131 | 132 | .arco-pagination-list { 133 | margin: 0; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/assets/style/editor.less: -------------------------------------------------------------------------------- 1 | .editor-card { 2 | .ͼo { 3 | background: var(--main-font-color); 4 | font-size: 12px; 5 | 6 | .cm-gutters { 7 | color: #fdfcff; 8 | background-color: transparent; 9 | border-right: 1px solid var(--hover-brand-color); 10 | opacity: 0.6; 11 | } 12 | 13 | .cm-activeLine { 14 | background-color: var(--editor-active-line-color); 15 | } 16 | } 17 | .ͼ1 .cm-content { 18 | padding: 10px 0 !important; 19 | width: calc(100% - 33px); 20 | white-space: pre-wrap; 21 | } 22 | 23 | .ͼ1 .cm-line { 24 | padding: 0 8px; 25 | } 26 | 27 | .ͼ1.cm-editor { 28 | border-radius: 6px; 29 | } 30 | 31 | .ͼ1.cm-editor.cm-focused { 32 | outline: 0; 33 | } 34 | } 35 | 36 | .space-between { 37 | display: flex; 38 | justify-content: space-between; 39 | } 40 | 41 | .pb-15 { 42 | padding-bottom: 15px; 43 | } 44 | 45 | .form-space { 46 | display: flex; 47 | padding-bottom: 16px; 48 | 49 | .arco-form-layout-inline .arco-form-item { 50 | margin-right: 0; 51 | margin-bottom: 0; 52 | } 53 | } 54 | 55 | .mr-4 { 56 | margin-right: 4px; 57 | } 58 | 59 | .script-form { 60 | padding-bottom: 8px; 61 | } 62 | 63 | .padding-16 { 64 | padding: 16px; 65 | } 66 | -------------------------------------------------------------------------------- /src/assets/style/layout.less: -------------------------------------------------------------------------------- 1 | .layout { 2 | height: 100%; 3 | padding: 16px 0 0 16px; 4 | 5 | > .arco-layout-sider-light { 6 | background: transparent; 7 | box-shadow: none; 8 | } 9 | 10 | > .arco-layout-content { 11 | padding: 0 16px; 12 | overflow-x: auto; 13 | 14 | .content-space { 15 | min-width: 920px; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/assets/style/list.less: -------------------------------------------------------------------------------- 1 | .arco-list-small .arco-list-content-wrapper { 2 | .arco-list-content > .arco-list-item { 3 | padding: 4px 0; 4 | } 5 | 6 | .arco-list-header { 7 | padding: 4px 0; 8 | color: var(--small-font-color); 9 | font-size: 14px; 10 | } 11 | } 12 | 13 | .arco-list { 14 | color: var(--small-font-color); 15 | font-size: 12px; 16 | } 17 | 18 | .arco-typography code { 19 | color: var(--danger-color); 20 | background-color: var(--light-brand-color); 21 | border: 0; 22 | border-radius: 4px; 23 | } 24 | 25 | .ml-4 { 26 | margin-left: 4px; 27 | } 28 | 29 | .ml-2 { 30 | margin-left: 2px; 31 | } 32 | -------------------------------------------------------------------------------- /src/assets/style/log.less: -------------------------------------------------------------------------------- 1 | .log-error { 2 | height: 100%; 3 | padding: 0 16px; 4 | overflow: hidden; 5 | white-space: nowrap; 6 | text-overflow: ellipsis; 7 | background-color: var(--danger-bg-color); 8 | border-radius: 4px; 9 | } 10 | 11 | .last-overflow { 12 | .arco-space-item:last-of-type { 13 | overflow: hidden; 14 | 15 | > div { 16 | position: relative; 17 | overflow: hidden; 18 | text-overflow: ellipsis; 19 | } 20 | } 21 | } 22 | 23 | .log-space { 24 | padding-left: 16px; 25 | 26 | .log-copy { 27 | .arco-typography-operation-copy, 28 | .arco-typography-operation-copied { 29 | margin-left: 0; 30 | padding: 0; 31 | } 32 | 33 | .arco-typography-operation-copy:hover { 34 | background-color: inherit; 35 | } 36 | } 37 | } 38 | 39 | .logs-tab { 40 | .arco-tabs-tab-active { 41 | cursor: default; 42 | } 43 | 44 | .arco-tabs-nav-type-rounded .arco-tabs-tab { 45 | padding: 10px 24px; 46 | } 47 | } 48 | 49 | .smaller-divider { 50 | .arco-divider-vertical { 51 | margin: 0 9px; 52 | } 53 | } 54 | 55 | .log-list { 56 | .arco-list-content { 57 | display: flex; 58 | flex-direction: column-reverse; 59 | } 60 | 61 | .arco-list { 62 | border-radius: 8px; 63 | } 64 | 65 | .arco-list-item-main { 66 | height: 100%; 67 | overflow: hidden; 68 | font-size: 14px; 69 | white-space: nowrap; 70 | text-overflow: ellipsis; 71 | 72 | .arco-list-item-content { 73 | height: 100%; 74 | line-height: 38px; 75 | } 76 | } 77 | 78 | .arco-list-item { 79 | align-items: center; 80 | height: 38px; 81 | padding: 0 !important; 82 | } 83 | 84 | .arco-list-item:nth-child(even) { 85 | background-color: inherit !important; 86 | } 87 | 88 | .arco-list-item:nth-child(odd) { 89 | background-color: var(--grey-bg-color); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/assets/style/new.less: -------------------------------------------------------------------------------- 1 | .new-layout { 2 | padding: 0; 3 | background: var(--card-bg-color); 4 | border-left: 1px solid var(--border-color); 5 | height: 100%; 6 | 7 | > .arco-layout-sider { 8 | box-shadow: none; 9 | min-width: 210px; 10 | max-width: 40vw; 11 | 12 | &.hide-sider { 13 | display: none; 14 | } 15 | } 16 | 17 | > .layout-content { 18 | height: 100%; 19 | 20 | > .arco-layout-header { 21 | height: var(--layout-header-height); 22 | display: flex; 23 | padding: 0 16px; 24 | } 25 | 26 | > .arco-layout-content { 27 | height: calc(100% - var(--layout-header-height)); 28 | } 29 | } 30 | 31 | .arco-layout-footer { 32 | display: flex; 33 | justify-content: space-between; 34 | padding-right: 20px; 35 | 36 | .arco-btn-size-mini.arco-btn-only-icon { 37 | width: 26px; 38 | height: 26px; 39 | padding: 0; 40 | } 41 | } 42 | 43 | .main-content { 44 | height: calc(100% - 58px); 45 | padding-right: 20px; 46 | } 47 | 48 | .layout-space { 49 | height: 100%; 50 | 51 | > .arco-space-item:first-of-type { 52 | flex: 1; 53 | overflow: auto; 54 | padding-left: 18px; 55 | } 56 | } 57 | 58 | .arco-tabs.arco-tabs-type-line.panel-tabs { 59 | display: flex; 60 | flex-direction: column; 61 | height: 100%; 62 | 63 | > .arco-tabs-nav { 64 | height: 28px; 65 | 66 | &:before { 67 | background-color: var(--border-color); 68 | } 69 | 70 | .arco-tabs-nav-tab { 71 | height: 100%; 72 | 73 | .arco-tabs-tab { 74 | border-radius: 0; 75 | font-size: 13px; 76 | height: 100%; 77 | margin: 0; 78 | padding: 0 12px; 79 | 80 | &.arco-tabs-tab-active { 81 | color: var(--main-font-color); 82 | font-weight: 600; 83 | } 84 | } 85 | } 86 | .arco-tabs-nav-ink { 87 | background: var(--brand-color); 88 | } 89 | } 90 | 91 | .arco-tabs-content { 92 | height: calc(100% - 28px); 93 | 94 | padding: 0; 95 | 96 | > .arco-tabs-content-list { 97 | height: 100%; 98 | > .arco-tabs-content-item { 99 | height: 100%; 100 | overflow: auto; 101 | > .arco-tabs-pane { 102 | height: 100%; 103 | } 104 | } 105 | } 106 | } 107 | } 108 | 109 | .has-panel { 110 | .layout-space { 111 | height: 100%; 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/assets/style/select.less: -------------------------------------------------------------------------------- 1 | .arco-select-view-single { 2 | padding-right: 8px; 3 | padding-left: 8px; 4 | background: var(--grey-bg-color); 5 | border-radius: 4px; 6 | } 7 | 8 | .arco-select-view-single:hover { 9 | background-color: var(--grey-bg-color); 10 | } 11 | 12 | .arco-select-view-single:focus-within, 13 | .arco-select-view-single.arco-select-view-focus { 14 | background-color: transparent; 15 | border-color: var(--hover-brand-color); 16 | } 17 | 18 | .arco-select-view-multiple { 19 | padding-right: 8px; 20 | padding-left: 8px; 21 | background: var(--grey-bg-color); 22 | border: 1px solid transparent; 23 | border-radius: 4px; 24 | 25 | .arco-select-view-inner .arco-select-view-tag { 26 | background: var(--grey-bg-color); 27 | border-color: transparent; 28 | border-radius: 6px; 29 | } 30 | } 31 | 32 | .arco-select-view-multiple:hover { 33 | background-color: var(--grey-bg-color); 34 | } 35 | 36 | .arco-select-view-multiple:focus-within, 37 | .arco-select-view-multiple.arco-select-view-focus { 38 | background-color: transparent; 39 | border-color: var(--hover-brand-color); 40 | } 41 | 42 | .arco-select-view-size-medium { 43 | color: var(--small-font-color) !important; 44 | } 45 | 46 | .query-select { 47 | .arco-select-view-single { 48 | padding-right: 16px; 49 | padding-left: 16px; 50 | background: var(--warning-bg-color); 51 | border: 1px solid var(--warning-color); 52 | .arco-select-view-value { 53 | font-size: 13px; 54 | } 55 | } 56 | } 57 | 58 | .arco-dropdown { 59 | padding: 4px 0; 60 | border-radius: 4px; 61 | box-shadow: 0 2px 10px 0 var(--box-shadow-color); 62 | .arco-dropdown-option { 63 | font-size: 13px; 64 | } 65 | } 66 | 67 | .arco-dropdown-option-content { 68 | width: 100%; 69 | height: 32px; 70 | line-height: 32px; 71 | border-radius: 0; 72 | font-size: 13px; 73 | } 74 | -------------------------------------------------------------------------------- /src/assets/style/tour.less: -------------------------------------------------------------------------------- 1 | .driver-popover.global { 2 | padding: 15px; 3 | border-radius: 4px; 4 | color: var(--main-font-color); 5 | min-width: 340px; 6 | max-width: 380px; 7 | .driver-popover-title { 8 | font-size: 14px; 9 | font-weight: 800; 10 | font-family: 'Gilroy'; 11 | } 12 | .driver-popover-close-btn { 13 | display: flex !important; 14 | top: 4px; 15 | right: -10px; 16 | color: var(--small-font-color); 17 | font-size: 18px; 18 | } 19 | .driver-popover-description { 20 | font-weight: 400; 21 | font-size: 12px; 22 | font-family: 'Open Sans'; 23 | } 24 | .driver-popover-footer { 25 | margin-top: 30px; 26 | } 27 | .driver-popover-navigation-btns button + button { 28 | border: none; 29 | margin-left: 4px; 30 | font-size: 13px; 31 | padding: 0; 32 | color: var(--brand-color); 33 | &:hover { 34 | background-color: transparent; 35 | color: var(--hover-brand-color); 36 | } 37 | } 38 | } 39 | 40 | .driver-popover.table-buttons { 41 | .title { 42 | font-family: 'Gilroy'; 43 | font-weight: 800; 44 | font-size: 14px; 45 | padding-bottom: 15px; 46 | } 47 | .row { 48 | display: flex; 49 | margin-bottom: 15px; 50 | span { 51 | margin-left: 10px; 52 | } 53 | } 54 | .driver-popover-footer { 55 | margin-top: 20px; 56 | } 57 | .icon-button { 58 | display: flex; 59 | align-items: center; 60 | padding: 4px; 61 | border-radius: 4px; 62 | height: 28px; 63 | background: var(--light-brand-color); 64 | width: 28px; 65 | justify-content: center; 66 | margin-right: 15px; 67 | } 68 | .hint-title { 69 | font-weight: 600; 70 | font-size: 13px; 71 | font-family: 'Open Sans'; 72 | } 73 | .hint-description { 74 | font-weight: 400; 75 | font-size: 12px; 76 | font-family: 'Open Sans'; 77 | margin-top: 4px; 78 | } 79 | } 80 | 81 | .driver-popover.workbench { 82 | border-radius: 4px; 83 | padding: 20px; 84 | color: var(--main-font-color); 85 | min-width: 300px; 86 | 87 | .driver-popover-close-btn { 88 | display: flex !important; 89 | top: 3px; 90 | right: -12px; 91 | } 92 | .driver-popover-description { 93 | font-weight: 600; 94 | font-size: 13px; 95 | font-family: 'Open Sans'; 96 | } 97 | .driver-popover-footer { 98 | margin-top: 40px; 99 | } 100 | .driver-popover-progress-text { 101 | font-size: 12px; 102 | color: var(--small-font-color); 103 | } 104 | .driver-popover-navigation-btns button + button { 105 | border: none; 106 | margin-left: 4px; 107 | font-size: 13px; 108 | padding: 0; 109 | color: var(--brand-color); 110 | &:hover { 111 | background-color: transparent; 112 | color: var(--hover-brand-color); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/components/breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 24 | 25 | 36 | -------------------------------------------------------------------------------- /src/components/chart/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/components/empty-status.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 20 | -------------------------------------------------------------------------------- /src/components/footer/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 35 | 36 | 73 | -------------------------------------------------------------------------------- /src/components/guide-modal/index.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 59 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'vue' 2 | import { use } from 'echarts/core' 3 | import { CanvasRenderer } from 'echarts/renderers' 4 | import { BarChart, LineChart, ScatterChart } from 'echarts/charts' 5 | import { 6 | GridComponent, 7 | TooltipComponent, 8 | LegendComponent, 9 | DataZoomComponent, 10 | GraphicComponent, 11 | } from 'echarts/components' 12 | import Chart from './chart/index.vue' 13 | import Breadcrumb from './breadcrumb/index.vue' 14 | import 'echarts/lib/component/dataset' 15 | import 'echarts/lib/component/transform' 16 | import YMLEditor from './yml-editor.vue' 17 | 18 | // Manually introduce ECharts modules to reduce packing size 19 | 20 | use([ 21 | CanvasRenderer, 22 | BarChart, 23 | LineChart, 24 | ScatterChart, 25 | GridComponent, 26 | TooltipComponent, 27 | LegendComponent, 28 | DataZoomComponent, 29 | GraphicComponent, 30 | ]) 31 | 32 | export default { 33 | install(Vue: App) { 34 | Vue.component('Chart', Chart) 35 | Vue.component('Breadcrumb', Breadcrumb) 36 | Vue.component('YMLEditorSimple', YMLEditor) 37 | }, 38 | } 39 | -------------------------------------------------------------------------------- /src/components/markdown-render/components/importPresets.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/components/markdown-render/components/utils.ts: -------------------------------------------------------------------------------- 1 | import { sql } from '@codemirror/lang-sql' 2 | import { python } from '@codemirror/lang-python' 3 | import { PromQLExtension } from '@prometheus-io/codemirror-promql' 4 | import { java } from '@codemirror/lang-java' 5 | import { rust } from '@codemirror/lang-rust' 6 | import { StreamLanguage } from '@codemirror/language' 7 | import { go } from '@codemirror/legacy-modes/mode/go' 8 | import { javascript } from '@codemirror/legacy-modes/mode/javascript' 9 | import { shell } from '@codemirror/legacy-modes/mode/shell' 10 | 11 | const mapLanguages = (value: string): any => { 12 | const mappedLanguages: any = { 13 | python, 14 | java, 15 | rust, 16 | sql, 17 | javascript: () => StreamLanguage.define(javascript), 18 | go: () => StreamLanguage.define(go), 19 | bash: () => StreamLanguage.define(shell), 20 | promql: () => new PromQLExtension().asExtension(), 21 | } 22 | return mappedLanguages[value.toLowerCase()] || mapLanguages('bash') 23 | } 24 | 25 | export default mapLanguages 26 | -------------------------------------------------------------------------------- /src/components/markdown-render/composables/codeGroups.ts: -------------------------------------------------------------------------------- 1 | export default function useCodeGroups() { 2 | document.querySelectorAll('.code-group > .blocks').forEach((el) => { 3 | Array.from(el.children).forEach((child) => { 4 | child.classList.remove('active') 5 | }) 6 | el.children[0].classList.add('active') 7 | }) 8 | 9 | window.addEventListener('click', (e) => { 10 | const el = e.target as HTMLInputElement 11 | 12 | if (el.matches('.code-group input')) { 13 | // input <- .tabs <- .vp-code-group 14 | const group = el.parentElement?.parentElement 15 | if (!group) return 16 | 17 | const i = Array.from(group.querySelectorAll('input')).indexOf(el) 18 | if (i < 0) return 19 | 20 | const blocks = group.querySelector('.blocks') 21 | if (!blocks) return 22 | 23 | const current = Array.from(blocks.children).find((child) => child.classList.contains('active')) 24 | if (!current) return 25 | 26 | const next = blocks.children[i] 27 | if (!next || current === next) return 28 | 29 | current.classList.remove('active') 30 | next.classList.add('active') 31 | 32 | const label = group?.querySelector(`label[for="${el.id}"]`) 33 | label?.scrollIntoView({ block: 'nearest' }) 34 | } 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /src/components/markdown-render/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 71 | -------------------------------------------------------------------------------- /src/components/markdown-render/plugins/containerPlugin.ts: -------------------------------------------------------------------------------- 1 | import type MarkdownIt from 'markdown-it' 2 | import container from 'markdown-it-container' 3 | import type { RenderRule } from 'markdown-it/lib/renderer' 4 | import { nanoid } from 'nanoid' 5 | import type Token from 'markdown-it/lib/token' 6 | 7 | type ContainerArgs = [typeof container, string, { render: RenderRule }] 8 | 9 | const extractLang = (info: string) => { 10 | return info 11 | .trim() 12 | .replace(/:(no-)?line-numbers$/, '') 13 | .replace(/(-vue|{| ).*$/, '') 14 | .replace(/^vue-html$/, 'template') 15 | } 16 | 17 | const extractTitle = (info: string) => { 18 | return info.match(/\[(.*)\]/)?.[1] || extractLang(info) || 'txt' 19 | } 20 | 21 | function createContainer(klass: string, defaultTitle: string, md: MarkdownIt): ContainerArgs { 22 | return [ 23 | container, 24 | klass, 25 | { 26 | render(tokens, idx) { 27 | const token = tokens[idx] 28 | const info = token.info.trim().slice(klass.length).trim() 29 | if (token.nesting === 1) { 30 | const title = md.renderInline(info || defaultTitle) 31 | if (klass === 'details') { 32 | return `
${title}\n` 33 | } 34 | return `

${title}

\n` 35 | } 36 | return klass === 'details' ? `
\n` : `\n` 37 | }, 38 | }, 39 | ] 40 | } 41 | function createCodeGroup(): ContainerArgs { 42 | return [ 43 | container, 44 | 'code-group', 45 | { 46 | render(tokens, idx) { 47 | if (tokens[idx].nesting === 1) { 48 | const name = nanoid(5) 49 | let tabs = '' 50 | let checked = 'checked="checked"' 51 | 52 | for ( 53 | let i = idx + 1; 54 | !(tokens[i].nesting === -1 && tokens[i].type === 'container_code-group_close'); 55 | i += 1 56 | ) { 57 | if (tokens[i].type === 'fence' && tokens[i].tag === 'code') { 58 | const title = extractTitle(tokens[i].info) 59 | const id = nanoid(7) 60 | tabs += `` 61 | 62 | if (checked) { 63 | tokens[i].info += ' active' 64 | checked = '' 65 | } 66 | } 67 | } 68 | 69 | return `
${tabs}
\n` 70 | } 71 | return `
\n` 72 | }, 73 | }, 74 | ] 75 | } 76 | export default function containerPlugin(md: MarkdownIt) { 77 | md.use(...createContainer('tip', 'TIP', md)) 78 | .use(...createContainer('info', 'INFO', md)) 79 | .use(...createContainer('warning', 'WARNING', md)) 80 | .use(...createContainer('danger', 'DANGER', md)) 81 | .use(...createContainer('details', 'Details', md)) 82 | // explicitly escape Vue syntax 83 | .use(container, 'v-pre', { 84 | render: (tokens: Token[], idx: number) => (tokens[idx].nesting === 1 ? `
\n` : `
\n`), 85 | }) 86 | .use(container, 'raw', { 87 | render: (tokens: Token[], idx: number) => (tokens[idx].nesting === 1 ? `
\n` : `
\n`), 88 | }) 89 | .use(...createCodeGroup()) 90 | } 91 | -------------------------------------------------------------------------------- /src/components/markdown-render/plugins/customButton.ts: -------------------------------------------------------------------------------- 1 | import type MarkdownIt from 'markdown-it' 2 | import type { RenderRule } from 'markdown-it/lib/renderer' 3 | 4 | export default function customButton(md: MarkdownIt) { 5 | const codeInline = md.renderer.rules.code_inline as RenderRule 6 | md.renderer.rules.code_inline = (...args) => { 7 | const res = codeInline(...args) 8 | 9 | if (/@button:.*=.*/.test(res)) { 10 | const [tokens, idx] = args 11 | const token = tokens[idx] 12 | 13 | const [_, action, configStr] = token.content.match(/@button:(.*)=(.*)/) || [] 14 | const config = configStr ? JSON.parse(configStr) : {} 15 | 16 | switch (action) { 17 | case 'import': 18 | return ` 19 | ${config.label || 'Import'} 20 | ` 21 | 22 | default: 23 | break 24 | } 25 | } 26 | 27 | return res 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/markdown-render/plugins/customCode.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import { ChartFormType, PromForm, SchemaType } from '@/store/modules/code-run/types' 3 | import type MarkdownIt from 'markdown-it' 4 | import type { RenderRule } from 'markdown-it/lib/renderer' 5 | 6 | export default function customCode(md: MarkdownIt) { 7 | const fence = md.renderer.rules.fence as RenderRule 8 | md.renderer.rules.fence = (...args) => { 9 | const rawCode = fence(...args) 10 | 11 | const [tokens, idx] = args 12 | const token = tokens[idx] 13 | 14 | const params = 15 | token.info 16 | .match(/\((.*)\)/)?.[1] 17 | .split('|') 18 | .map((item) => item.split(',')) || [] 19 | 20 | const chartParams: ChartFormType = { 21 | chartType: params[0]?.[0] || 'line', 22 | selectedYTypes: params[1] || [], 23 | xAxisType: (params[2]?.[0] ? { name: params[2]?.[0] } : {}) as SchemaType, 24 | groupBySelectedTypes: params[3] || [], 25 | } 26 | 27 | const promParams: PromForm = { 28 | // eslint-disable-next-line no-nested-ternary 29 | time: params.length === 0 ? 5 : params[0]?.length === 1 ? +params[0][0] : 0, 30 | range: 31 | params[0]?.length === 2 32 | ? params[0] 33 | : [dayjs().subtract(5, 'minute').unix().toString(), dayjs().unix().toString()], 34 | step: params[1]?.[0] || '30s', 35 | } 36 | 37 | const res = rawCode?.replace( 38 | /
([\s\S]*)<\/code><\/pre>/,
39 |       ($1: string, $2: string) => {
40 |         const disabled = /sql|promql/.test($2.toLowerCase()) ? '' : 'disabled'
41 |         return `${$1}`
44 |       }
45 |     )
46 |     return `${res}`
47 |   }
48 | }
49 | 


--------------------------------------------------------------------------------
/src/components/markdown-render/plugins/customComment.ts:
--------------------------------------------------------------------------------
 1 | import type MarkdownIt from 'markdown-it'
 2 | import type { RenderRule } from 'markdown-it/lib/renderer'
 3 | 
 4 | export default function customComment(md: MarkdownIt) {
 5 |   const defaultRender = md.renderer.rules.text as RenderRule
 6 |   md.renderer.rules.text = (tokens, idx, options, env, self) => {
 7 |     if (tokens[idx].content.startsWith('')) {
 8 |       return ''
 9 |     }
10 |     return defaultRender(tokens, idx, options, env, self)
11 |   }
12 | }
13 | 


--------------------------------------------------------------------------------
/src/components/markdown-render/plugins/customImage.ts:
--------------------------------------------------------------------------------
 1 | import type MarkdownIt from 'markdown-it'
 2 | import type { RenderRule } from 'markdown-it/lib/renderer'
 3 | 
 4 | export default function customImage(md: MarkdownIt) {
 5 |   const defaultRender = md.renderer.rules.image as RenderRule
 6 |   const isDigital = (str: string) => /^\d+$/.test(str)
 7 |   const hasDigital = (str: string) => /\d+/.test(str)
 8 | 
 9 |   md.renderer.rules.image = (tokens, idx, options, env, self) => {
10 |     const token = tokens[idx]
11 |     const altInfo = token.content || ''
12 |     const src = token.attrGet('src') || ''
13 | 
14 |     const [alt, size, klass] = altInfo.split('|').map((item) => item.trim())
15 |     let [width, height] = size?.split(/[xX*]/) || []
16 | 
17 |     if (isDigital(width)) width += 'px'
18 |     if (isDigital(height)) height += 'px'
19 |     if (!height) height = width
20 | 
21 |     return `
22 | ${alt} 23 |
` 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/markdown-render/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import containerPlugin from './containerPlugin' 2 | import customCode from './customCode' 3 | import customImage from './customImage' 4 | import customComment from './customComment' 5 | import customButton from './customButton' 6 | 7 | export default { 8 | containerPlugin, 9 | customCode, 10 | customImage, 11 | customComment, 12 | customButton, 13 | } 14 | -------------------------------------------------------------------------------- /src/components/menu/use-menu-tree.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue' 2 | import { RouteRecordRaw, RouteRecordNormalized } from 'vue-router' 3 | import usePermission from '@/hooks/permission' 4 | import { useAppStore } from '@/store' 5 | import appClientMenus from '@/router/app-menus' 6 | 7 | export default function useMenuTree() { 8 | const permission = usePermission() 9 | const appStore = useAppStore() 10 | const appRoute = computed(() => { 11 | if (appStore.menuFromServer) { 12 | return appStore.appAsyncMenus 13 | } 14 | return appClientMenus 15 | }) 16 | const menuTree = computed(() => { 17 | const copyRouter = JSON.parse(JSON.stringify(appRoute.value)) 18 | copyRouter.sort((a: RouteRecordNormalized, b: RouteRecordNormalized) => { 19 | return (a.meta.order || 0) - (b.meta.order || 0) 20 | }) 21 | function travel(_routes: RouteRecordRaw[], layer: number) { 22 | if (!_routes) return null 23 | 24 | const collector: any = _routes.map((element) => { 25 | // no access 26 | if (!permission.accessRouter(element)) { 27 | return null 28 | } 29 | 30 | // leaf node 31 | if (element.meta?.hideChildrenInMenu || !element.children) { 32 | element.children = [] 33 | return element 34 | } 35 | 36 | // route filter hideInMenu true 37 | element.children = element.children.filter((x) => x.meta?.hideInMenu !== true) 38 | 39 | // Associated child node 40 | const subItem = travel(element.children, layer + 1) 41 | 42 | if (subItem.length) { 43 | element.children = subItem 44 | return element 45 | } 46 | // the else logic 47 | if (layer > 1) { 48 | element.children = subItem 49 | return element 50 | } 51 | 52 | if (element.meta?.hideInMenu === false) { 53 | return element 54 | } 55 | 56 | return null 57 | }) 58 | return collector.filter(Boolean) 59 | } 60 | return travel(copyRouter, 0) 61 | }) 62 | 63 | return { 64 | menuTree, 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/components/refresh-playground-modal/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 39 | -------------------------------------------------------------------------------- /src/components/simple-markdown/components/simple-code-editor.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 47 | 48 | 135 | -------------------------------------------------------------------------------- /src/components/simple-markdown/index.vue: -------------------------------------------------------------------------------- 1 | 56 | -------------------------------------------------------------------------------- /src/components/simple-markdown/plugins/customCode.ts: -------------------------------------------------------------------------------- 1 | import type MarkdownIt from 'markdown-it' 2 | import type { RenderRule } from 'markdown-it/lib/renderer' 3 | 4 | export default function customCode(md: MarkdownIt) { 5 | const fence = md.renderer.rules.fence as RenderRule 6 | md.renderer.rules.fence = (...args) => { 7 | const rawCode = fence(...args) 8 | 9 | const res = rawCode 10 | ?.replace(/([\s\S]*)<\/code><\/pre>/, ($1: string, $2: string) => { 12 | const disabled = /sql|promql/.test($2.toLowerCase()) ? '' : 'disabled' 13 | return `${$1}` 14 | }) 15 | return `${res}` 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/simple-markdown/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import customCode from './customCode' 2 | 3 | export default { 4 | customCode, 5 | } 6 | -------------------------------------------------------------------------------- /src/components/social-link/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 19 | 20 | 41 | -------------------------------------------------------------------------------- /src/components/status-bar/status-list.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | 22 | 29 | -------------------------------------------------------------------------------- /src/components/tab-bar/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 50 | 51 | 91 | -------------------------------------------------------------------------------- /src/components/tab-bar/readme.md: -------------------------------------------------------------------------------- 1 | ## Component description 2 | 3 | The component unofficial final design specification exists as a separate component. 4 | 5 | At the same time, only the most basic functions are provided, and subsequent optimizations and changes will be made. 6 | -------------------------------------------------------------------------------- /src/components/text-copyable.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 49 | 50 | 71 | -------------------------------------------------------------------------------- /src/components/yml-editor.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 38 | 39 | 44 | -------------------------------------------------------------------------------- /src/config/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme": "dark", 3 | "colorWeak": false, 4 | "navbar": true, 5 | "menu": false, 6 | "hideMenu": false, 7 | "menuCollapse": false, 8 | "footer": true, 9 | "themeColor": "#165DFF", 10 | "menuWidth": 220, 11 | "globalSettings": false, 12 | "device": "desktop", 13 | "tabBar": false, 14 | "menuFromServer": false, 15 | "host": "", 16 | "database": "public", 17 | "databaseList": [], 18 | "username": "", 19 | "password": "", 20 | "dbId": "", 21 | "serverMenu": [], 22 | "guideModalVisible": false, 23 | "lifetime": "long", 24 | "menuSelectedKey": "tables", 25 | "userTimezone": "", 26 | "isFullScreen": false, 27 | "authHeader": "Authorization" 28 | } 29 | -------------------------------------------------------------------------------- /src/directive/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'vue' 2 | import permission from './permission' 3 | 4 | export default { 5 | install(Vue: App) { 6 | Vue.directive('permission', permission) 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /src/directive/permission/index.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveBinding } from 'vue' 2 | import { useUserStore } from '@/store' 3 | 4 | function checkPermission(el: HTMLElement, binding: DirectiveBinding) { 5 | const { value } = binding 6 | const { role } = useUserStore() 7 | 8 | if (Array.isArray(value)) { 9 | if (value.length > 0) { 10 | const permissionValues = value 11 | 12 | const hasPermission = permissionValues.includes(role) 13 | if (!hasPermission && el.parentNode) { 14 | el.parentNode.removeChild(el) 15 | } 16 | } 17 | } else { 18 | throw new Error(`need roles! Like v-permission="['admin','user']"`) 19 | } 20 | } 21 | 22 | export default { 23 | mounted(el: HTMLElement, binding: DirectiveBinding) { 24 | checkPermission(el, binding) 25 | }, 26 | updated(el: HTMLElement, binding: DirectiveBinding) { 27 | checkPermission(el, binding) 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import { DefineComponent } from 'vue' 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any> 7 | export default component 8 | } 9 | interface ImportMetaEnv { 10 | readonly VITE_API_BASE_URL: string 11 | readonly VITE_CLOUD_URL: string 12 | } 13 | -------------------------------------------------------------------------------- /src/hooks/chart-option.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue' 2 | import { EChartsOption } from 'echarts' 3 | import { useAppStore } from '@/store' 4 | 5 | // for code hints 6 | // import { SeriesOption } from 'echarts'; 7 | // Because there are so many configuration items, this provides a relatively convenient code hint. 8 | // When using vue, pay attention to the reactive issues. It is necessary to ensure that corresponding functions can be triggered, TypeScript does not report errors, and code writing is convenient. 9 | interface optionsFn { 10 | (isDark: boolean): EChartsOption 11 | } 12 | 13 | export default function useChartOption(sourceOption: optionsFn) { 14 | const appStore = useAppStore() 15 | const isDark = computed(() => { 16 | return appStore.theme === 'dark' 17 | }) 18 | 19 | const chartOption = computed(() => { 20 | return sourceOption(isDark.value) 21 | }) 22 | return { 23 | chartOption, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/hooks/data-chart.ts: -------------------------------------------------------------------------------- 1 | import { ChartFormType, ResultType, SchemaType } from '../store/modules/code-run/types' 2 | import { dateTypes, numberTypes } from '../views/dashboard/config' 3 | 4 | export default function useDataChart(data: ResultType) { 5 | const chartForm: ChartFormType = reactive({ 6 | chartType: 'line', 7 | selectedYTypes: [''], 8 | groupBySelectedTypes: [] as string[], 9 | xAxisType: {} as SchemaType, 10 | }) 11 | const hasTimestamp = data.dimensionsAndXName.xAxis !== '' 12 | const schemaInRecords = data.records.schema 13 | 14 | const yOptions = computed(() => { 15 | if (!schemaInRecords || !hasTimestamp) return [] 16 | return schemaInRecords.column_schemas 17 | .filter((item: SchemaType) => numberTypes.find((type: string) => type === item.data_type)) 18 | .map((item: SchemaType) => item.name) 19 | }) 20 | 21 | const groupByOptions = computed(() => { 22 | return schemaInRecords.column_schemas 23 | .map((item: SchemaType, index: number) => ({ 24 | ...item, 25 | index, 26 | })) 27 | .filter( 28 | (item: SchemaType) => 29 | !dateTypes.find((type: string) => type === item.data_type) && item.name !== chartForm.selectedYTypes[0] 30 | ) 31 | }) 32 | 33 | const xOptions = computed(() => { 34 | return schemaInRecords.column_schemas.filter( 35 | (item: SchemaType) => 36 | dateTypes.find((type: string) => type === item.data_type) && item.name !== chartForm.selectedYTypes[0] 37 | ) 38 | }) 39 | 40 | const hasChart = computed(() => { 41 | return yOptions.value.length > 0 42 | }) 43 | 44 | return { 45 | chartForm, 46 | yOptions, 47 | groupByOptions, 48 | hasChart, 49 | xOptions, 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/hooks/gist.ts: -------------------------------------------------------------------------------- 1 | import { getGist } from '@/api/gist' 2 | 3 | export default function useGist() { 4 | const getGistFiles = async (gistId: string) => { 5 | const res: any = await getGist(gistId) 6 | 7 | return Object.values(res.files) 8 | } 9 | 10 | return { 11 | getGistFiles, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | import useQueryCode from './query-code' 2 | import useChartOption from './chart-option' 3 | import useLog from './log' 4 | import useGist from './gist' 5 | 6 | export { useQueryCode, useChartOption, useLog, useGist } 7 | -------------------------------------------------------------------------------- /src/hooks/loading.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | export default function useLoading(initValue = false) { 4 | const loading = ref(initValue) 5 | const setLoading = (value: boolean) => { 6 | loading.value = value 7 | } 8 | const toggle = () => { 9 | loading.value = !loading.value 10 | } 11 | return { 12 | loading, 13 | setLoading, 14 | toggle, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/hooks/locale.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue' 2 | import { useI18n } from 'vue-i18n' 3 | import { Message } from '@arco-design/web-vue' 4 | 5 | export default function useLocale() { 6 | const i18 = useI18n() 7 | const currentLocale = computed(() => { 8 | return i18.locale.value 9 | }) 10 | const changeLocale = (value: string) => { 11 | i18.locale.value = value 12 | localStorage.setItem('arco-locale', value) 13 | Message.success(i18.t('navbar.action.locale')) 14 | } 15 | return { 16 | currentLocale, 17 | changeLocale, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/hooks/log.ts: -------------------------------------------------------------------------------- 1 | import { Log } from '@/store/modules/log/types' 2 | 3 | export default function useLog(route?: any) { 4 | const { push, clear } = useLogStore() 5 | 6 | const pushLog = (log: Log, type: string) => { 7 | push(log, type || (route?.name as string)) 8 | } 9 | const clearLogs = (type = route?.name as string | string[]) => { 10 | clear(type) 11 | } 12 | 13 | return { 14 | pushLog, 15 | clearLogs, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/hooks/permission.ts: -------------------------------------------------------------------------------- 1 | import { RouteLocationNormalized, RouteRecordRaw } from 'vue-router' 2 | import { useUserStore } from '@/store' 3 | 4 | export default function usePermission() { 5 | const userStore = useUserStore() 6 | return { 7 | // has access: 8 | // 1. requiresAuth === 'false' 9 | // 2. no specific roles 10 | // 3. roles is * 11 | // 4. roles include current userRole 12 | accessRouter(route: RouteLocationNormalized | RouteRecordRaw) { 13 | return ( 14 | !route.meta?.requiresAuth || 15 | !route.meta?.roles || 16 | route.meta?.roles?.includes('*') || 17 | route.meta?.roles?.includes(userStore.role) 18 | ) 19 | }, 20 | findFirstPermissionRoute(_routers: any, role = 'admin') { 21 | const cloneRouters = [..._routers] 22 | while (cloneRouters.length) { 23 | const firstElement = cloneRouters.shift() 24 | if ( 25 | firstElement?.meta?.roles?.find((el: string[]) => { 26 | return el.includes('*') || el.includes(role) 27 | }) 28 | ) 29 | return { name: firstElement.name } 30 | if (firstElement?.children) { 31 | cloneRouters.push(...firstElement.children) 32 | } 33 | } 34 | return null 35 | }, 36 | // You can add any rules you want 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/hooks/python-code.ts: -------------------------------------------------------------------------------- 1 | import { Log } from '@/store/modules/log/types' 2 | import { Md5 } from 'ts-md5' 3 | import { Message } from '@arco-design/web-vue' 4 | import i18n from '@/locale' 5 | 6 | const { saveScript } = useCodeRunStore() 7 | 8 | const pythonCode = ref('') 9 | const lastSavedCode = ref('') 10 | 11 | const scriptSelectedKeys = ref>([]) 12 | const lastSelectedKey = ref>([]) 13 | 14 | const cursorAt = ref>([]) 15 | const scriptName = ref('') 16 | const isNewScript = ref(true) 17 | const modelVisible = ref(false) 18 | const creating = ref(false) 19 | const scriptSaving = ref(false) 20 | const scriptRunning = ref(false) 21 | const isChanged = computed(() => Md5.hashStr(pythonCode.value) !== Md5.hashStr(lastSavedCode.value)) 22 | 23 | export default function usePythonCode() { 24 | const { pushLog } = useLog() 25 | const insertNameToPyCode = (name: any) => { 26 | pythonCode.value = 27 | pythonCode.value.substring(0, cursorAt.value[0]) + name + pythonCode.value.substring(cursorAt.value[1]) 28 | } 29 | 30 | const overwriteCode = (script: any) => { 31 | scriptName.value = script.key 32 | isNewScript.value = false 33 | pythonCode.value = script.code 34 | lastSelectedKey.value = [script.key] 35 | lastSavedCode.value = pythonCode.value 36 | } 37 | 38 | const resetScript = () => { 39 | scriptName.value = '' 40 | isNewScript.value = true 41 | pythonCode.value = '' 42 | scriptSelectedKeys.value = [] 43 | lastSelectedKey.value = [] 44 | lastSavedCode.value = '' 45 | creating.value = false 46 | } 47 | 48 | const createNewScript = () => { 49 | creating.value = true 50 | if (!isChanged.value) { 51 | resetScript() 52 | } else { 53 | modelVisible.value = true 54 | } 55 | } 56 | 57 | const selectAfterSave = (name: string) => { 58 | scriptSelectedKeys.value = [name] 59 | isNewScript.value = false 60 | lastSavedCode.value = pythonCode.value 61 | } 62 | 63 | const save = async (name: string, code: string) => { 64 | try { 65 | const res = await saveScript(name, code) 66 | Message.success({ 67 | content: i18n.global.t('dashboard.saveSuccess'), 68 | duration: 2 * 1000, 69 | }) 70 | pushLog(res, 'python') 71 | } catch (err: any) { 72 | throw pushLog(JSON.parse(err.message) as Log, 'python') 73 | } 74 | } 75 | 76 | const isButtonDisabled = computed(() => { 77 | if ( 78 | scriptRunning.value === true || 79 | scriptSaving.value === true || 80 | scriptName.value.trim().length === 0 || 81 | pythonCode.value.trim().length === 0 82 | ) 83 | return true 84 | return false 85 | }) 86 | 87 | return { 88 | insertNameToPyCode, 89 | overwriteCode, 90 | createNewScript, 91 | selectAfterSave, 92 | resetScript, 93 | pythonCode, 94 | lastSavedCode, 95 | cursorAt, 96 | scriptName, 97 | isNewScript, 98 | scriptSelectedKeys, 99 | lastSelectedKey, 100 | isChanged, 101 | modelVisible, 102 | creating, 103 | isButtonDisabled, 104 | scriptSaving, 105 | scriptRunning, 106 | save, 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/hooks/request.ts: -------------------------------------------------------------------------------- 1 | import { ref, UnwrapRef } from 'vue' 2 | import { AxiosResponse } from 'axios' 3 | import { HttpResponse } from '@/api/interceptor' 4 | import useLoading from './loading' 5 | 6 | // use to fetch list 7 | // Don't use async function. It doesn't work in async function. 8 | // Use the bind function to add parameters 9 | // example: useRequest(api.bind(null, {})) 10 | 11 | export default function useRequest( 12 | api: () => Promise>, 13 | defaultValue = [] as unknown as T, 14 | isLoading = true 15 | ) { 16 | const { loading, setLoading } = useLoading(isLoading) 17 | const response = ref(defaultValue) 18 | api() 19 | .then((res) => { 20 | response.value = res.data as unknown as UnwrapRef 21 | }) 22 | .finally(() => { 23 | setLoading(false) 24 | }) 25 | return { loading, response } 26 | } 27 | -------------------------------------------------------------------------------- /src/hooks/responsive.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, onBeforeMount, onBeforeUnmount } from 'vue' 2 | import { useDebounceFn } from '@vueuse/core' 3 | import { useAppStore } from '@/store' 4 | import { addEventListen, removeEventListen } from '@/utils/event' 5 | 6 | const WIDTH = 992 // https://arco.design/vue/component/grid#responsivevalue 7 | 8 | function queryDevice() { 9 | const rect = document.body.getBoundingClientRect() 10 | return rect.width - 1 < WIDTH 11 | } 12 | 13 | export default function useResponsive(immediate?: boolean) { 14 | const appStore = useAppStore() 15 | function resizeHandler() { 16 | if (!document.hidden) { 17 | const isMobile = queryDevice() 18 | appStore.toggleDevice(isMobile ? 'mobile' : 'desktop') 19 | appStore.toggleMenu(isMobile) 20 | } 21 | } 22 | const debounceFn = useDebounceFn(resizeHandler, 100) 23 | onMounted(() => { 24 | if (immediate) debounceFn() 25 | }) 26 | onBeforeMount(() => { 27 | addEventListen(window, 'resize', debounceFn) 28 | }) 29 | onBeforeUnmount(() => { 30 | removeEventListen(window, 'resize', debounceFn) 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /src/hooks/themes.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue' 2 | import { useAppStore } from '@/store' 3 | 4 | export default function useThemes() { 5 | const appStore = useAppStore() 6 | const isDark = computed(() => { 7 | return appStore.theme === 'dark' 8 | }) 9 | return { 10 | isDark, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/hooks/types.ts: -------------------------------------------------------------------------------- 1 | export interface stringType { 2 | [key: string]: string 3 | } 4 | -------------------------------------------------------------------------------- /src/hooks/user.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreptimeTeam/dashboard/5d6f0258e287a4640ea013f6dac60a73b50edada/src/hooks/user.ts -------------------------------------------------------------------------------- /src/hooks/visible.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | export default function useVisible(initValue = false) { 4 | const visible = ref(initValue) 5 | const setVisible = (value: boolean) => { 6 | visible.value = value 7 | } 8 | const toggle = () => { 9 | visible.value = !visible.value 10 | } 11 | return { 12 | visible, 13 | setVisible, 14 | toggle, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/layout/default-layout.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 42 | 43 | 70 | -------------------------------------------------------------------------------- /src/layout/page-layout.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/locale/en-US.ts: -------------------------------------------------------------------------------- 1 | import dashboard from './en-US/dashboard' 2 | import settings from './en-US/settings' 3 | import menu from './en-US/menu' 4 | import playground from './en-US/playground' 5 | import logquery from './en-US/logquery' 6 | 7 | export default { 8 | 'navbar.action.locale': 'Switch to English', 9 | 'navbar.docs': 'Docs', 10 | 'copied': 'Copied', 11 | 'guide.confirm': 'Confirm', 12 | 'guide.welcome': 'Welcome!', 13 | ...dashboard, 14 | ...settings, 15 | ...menu, 16 | ...playground, 17 | ...logquery, 18 | } 19 | -------------------------------------------------------------------------------- /src/locale/en-US/logquery.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'logquery.run': 'Run', 3 | 'logquery.saveSql': 'Save SQL', 4 | 'logquery.savedSql': 'Saved SQL', 5 | 'logquery.showTables': 'Show tables', 6 | 'logquery.results': 'Results', 7 | 'logquery.hideStatChart': 'Hide chart', 8 | 'logquery.showStatChart': 'Show chart', 9 | 'logquery.live': 'Live', 10 | 'logquery.columns': 'Columns', 11 | 'logquery.wrapLines': 'Wrap lines', 12 | 'logquery.nodata': 'No Data', 13 | 'logquery.newer': 'Newer', 14 | 'logquery.older': 'Older', 15 | } 16 | -------------------------------------------------------------------------------- /src/locale/en-US/menu.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.dashboard.playground': 'Playground', 3 | 'menu.dashboard.query': 'Query', 4 | 'menu.dashboard.tables': 'Tables', 5 | 'menu.dashboard.scripts': 'Scripts', 6 | 'menu.dashboard.status': 'Status', 7 | 'menu.dashboard.ingest': 'Ingest', 8 | 'menu.dashboard.influxdb': 'InfluxDB Line Protocol', 9 | 'menu.dashboard.write': 'Write', 10 | 'menu.dashboard.upload': 'Upload', 11 | 'menu.dashboard.input': 'Input', 12 | 'menu.dashboard': 'Dashboard', 13 | 'menu.exception': 'Exception', 14 | 'menu.faq': 'FAQ', 15 | 'menu.form': 'Form', 16 | 'menu.list': 'List', 17 | 'menu.profile': 'Profile', 18 | 'menu.result': 'Result', 19 | 'menu.server.dashboard': 'Dashboard-Server', 20 | 'menu.server.monitor': 'Monitor-Server', 21 | 'menu.server.workplace': 'Workplace-Server', 22 | 'menu.user': 'User Center', 23 | 'menu.visualization': 'Data Visualization', 24 | 'menu.newQuery': 'New Query', 25 | 'menu.tour.query': 26 | 'Check full list of tables and their metadata of your instance. Place SQL and PromQL query in our query editor.', 27 | 'menu.tour.tables': 'Full list of tables and their metadata of your instance.', 28 | 'menu.tour.ingest': 'Ingest time-series data from the Ingest UI.', 29 | 'menu.tour.workbench': 'Build advanced dashboard using UI builder and YAML based configuration, all managed by git.', 30 | 'menu.dashboard.logquery': 'Log Query', 31 | 'menu.dashboard.logpipeline': 'Log Pipelines', 32 | } 33 | -------------------------------------------------------------------------------- /src/locale/en-US/playground.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'playground.create': 'OK', 3 | 'playground.import': 'Import {lines} lines in {time}ms', 4 | 'playground.refreshNote': 'The Playground has been reclaimed due to timeout. Click OK to re-create the playground', 5 | 'playground.refreshTitle': 'Warning', 6 | 'playground.reset': 'Reset', 7 | 'playground.run': 'Run', 8 | 'playground.preImport': 'Preparing to import {nums} groups of data', 9 | } 10 | -------------------------------------------------------------------------------- /src/locale/en-US/settings.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'settings.auth': 'Auth', 3 | 'settings.database': 'Database', 4 | 'settings.host': 'Host', 5 | 'settings.password': 'Password', 6 | 'settings.save': 'Save and Test', 7 | 'settings.title': 'Settings', 8 | 'settings.username': 'Username', 9 | 'settings.timezone': 'Timezone', 10 | 'settings.saveTip': 'Authentication failed. Please check your settings.', 11 | 'settings.saveSuccess': 'Success!', 12 | 'settings.authHeader': 'Authentication Header Name', 13 | 'settings.authHeaderTip': 'The name of the HTTP header that is used for authentication.', 14 | } 15 | -------------------------------------------------------------------------------- /src/locale/index.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n' 2 | import en from './en-US' 3 | import cn from './zh-CN' 4 | 5 | export const LOCALE_OPTIONS = [ 6 | { label: '中文', value: 'zh-CN' }, 7 | { label: 'English', value: 'en-US' }, 8 | ] 9 | const defaultLocale = localStorage.getItem('arco-locale') || import.meta.env.VITE_LANG || 'en-US' 10 | 11 | const i18n = createI18n({ 12 | locale: defaultLocale, 13 | fallbackLocale: 'en-US', 14 | allowComposition: true, 15 | messages: { 16 | 'en-US': en, 17 | 'zh-CN': cn, 18 | }, 19 | }) 20 | 21 | export default i18n 22 | -------------------------------------------------------------------------------- /src/locale/zh-CN.ts: -------------------------------------------------------------------------------- 1 | import dashboard from './zh-CN/dashboard' 2 | import menu from './zh-CN/menu' 3 | import playground from './zh-CN/playground' 4 | import settings from './zh-CN/settings' 5 | import logquery from './zh-CN/logquery' 6 | 7 | export default { 8 | 'navbar.action.locale': '切换到中文', 9 | 'navbar.docs': '文档', 10 | 'copied': '已复制', 11 | 'guide.confirm': '确定', 12 | 'guide.welcome': '欢迎!', 13 | ...dashboard, 14 | ...menu, 15 | ...playground, 16 | ...settings, 17 | } 18 | -------------------------------------------------------------------------------- /src/locale/zh-CN/logquery.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'logquery.run': '查询', 3 | 'logquery.saveSql': '保存为常用 SQL', 4 | 'logquery.savedSql': '常用 SQL', 5 | 'logquery.showTables': '所有表格', 6 | 'logquery.results': '结果', 7 | 'logquery.hideStatChart': '隐藏图表', 8 | 'logquery.showStatChart': '显示图表', 9 | 'logquery.live': '实时', 10 | 'logquery.columns': '设置列', 11 | 'logquery.wrapLines': '自动换行', 12 | 'logquery.nodata': '暂无数据', 13 | 'logquery.newer': '较新', 14 | 'logquery.older': '较旧', 15 | } 16 | -------------------------------------------------------------------------------- /src/locale/zh-CN/menu.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.dashboard.playground': 'Playground', 3 | 'menu.dashboard.query': 'Query', 4 | 'menu.dashboard.tables': 'Tables', 5 | 'menu.dashboard.scripts': 'Scripts', 6 | 'menu.dashboard.status': 'Status', 7 | 'menu.dashboard.ingest': 'Ingest', 8 | 'menu.dashboard.influxdb': 'InfluxDB Line Protocol', 9 | 'menu.dashboard.write': 'Write', 10 | 'menu.dashboard.upload': 'Upload', 11 | 'menu.dashboard.input': 'Input', 12 | 'menu.dashboard': '仪表盘', 13 | 'menu.server.dashboard': '仪表盘-服务端', 14 | 'menu.server.workplace': '工作台-服务端', 15 | 'menu.server.monitor': '实时监控-服务端', 16 | 'menu.list': '列表页', 17 | 'menu.result': '结果页', 18 | 'menu.exception': '异常页', 19 | 'menu.form': '表单页', 20 | 'menu.profile': '详情页', 21 | 'menu.visualization': '数据可视化', 22 | 'menu.user': '个人中心', 23 | 'menu.faq': '常见问题', 24 | 'menu.newQuery': '新查询', 25 | 'menu.tour.query': '查看实例的完整表列表及其元数据。将 SQL 和 PromQL 查询放入我们的查询编辑器。', 26 | 'menu.tour.ingest': '从 Ingest UI 中摄取时间序列数据。', 27 | 'menu.tour.workbench': '使用 UI 构建器和基于 YAML 的配置构建高级仪表板,所有这些都由 git 管理。', 28 | 'menu.dashboard.logquery': '日志查询', 29 | 'menu.dashboard.logpipeline': '日志 Pipelines', 30 | } 31 | -------------------------------------------------------------------------------- /src/locale/zh-CN/playground.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'playground.reset': '重置', 3 | 'playground.run': '运行', 4 | 'playground.create': '确定', 5 | 'playground.refreshTitle': '提醒', 6 | 'playground.refreshNote': '这个 Playground 已经被回收了,点击确定重新创建', 7 | 'playground.import': '导入 {lines} 行数据,耗时 {time}ms', 8 | 'playground.preImport': '准备导入 {nums} 组数据', 9 | } 10 | -------------------------------------------------------------------------------- /src/locale/zh-CN/settings.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'settings.host': '主机', 3 | 'settings.title': '设置', 4 | 'settings.auth': '认证', 5 | 'settings.username': '用户名', 6 | 'settings.password': '密码', 7 | 'settings.database': '数据库', 8 | 'settings.save': '保存', 9 | 'settings.timezone': '时区', 10 | 'settings.saveTip': '认证失败,请检查您的设置。', 11 | 'settings.saveSuccess': '保存成功!', 12 | 'settings.authHeader': '认证头名称', 13 | 'settings.authHeaderTip': '用于认证的HTTP头的名称。', 14 | } 15 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { App, createApp } from 'vue' 2 | import ArcoVue from '@arco-design/web-vue' 3 | import ArcoVueIcon from '@arco-design/web-vue/es/icon' 4 | import globalComponents from '@/components' 5 | import router from './router' 6 | import store from './store' 7 | import i18n from './locale' 8 | import directive from './directive' 9 | import Apps from './App.vue' 10 | import '@arco-design/web-vue/dist/arco.css' 11 | import '@/assets/style/global.less' 12 | import '@/api/interceptor' 13 | 14 | const app: App = createApp(Apps) 15 | 16 | app.config.errorHandler = (err, vm, info) => { 17 | console.error(err, info) 18 | // Optionally show an error message to users 19 | } 20 | 21 | app.use(ArcoVue, {}) 22 | app.use(ArcoVueIcon) 23 | 24 | app.use(router) 25 | app.use(store) 26 | app.use(i18n) 27 | app.use(globalComponents) 28 | app.use(directive) 29 | 30 | app.mount('#app') 31 | -------------------------------------------------------------------------------- /src/router/app-menus/index.ts: -------------------------------------------------------------------------------- 1 | import { appRoutes, appExternalRoutes } from '../routes' 2 | 3 | const mixinRoutes = [...appRoutes, ...appExternalRoutes] 4 | 5 | const appClientMenus = mixinRoutes.map((el) => { 6 | const { name, path, meta, redirect, children } = el 7 | return { 8 | name, 9 | path, 10 | meta, 11 | redirect, 12 | children, 13 | } 14 | }) 15 | 16 | export default appClientMenus 17 | -------------------------------------------------------------------------------- /src/router/constants.ts: -------------------------------------------------------------------------------- 1 | export const WHITE_LIST = [{ name: 'notFound', children: [] }] 2 | 3 | export const NOT_FOUND = { 4 | name: 'notFound', 5 | } 6 | 7 | export const REDIRECT_ROUTE_NAME = 'Redirect' 8 | 9 | export const DEFAULT_ROUTE_NAME = 'Dashboard' 10 | 11 | export const DEFAULT_ROUTE = { 12 | title: 'menu.dashboard', 13 | name: DEFAULT_ROUTE_NAME, 14 | fullPath: '/dashboard/tables', 15 | } 16 | -------------------------------------------------------------------------------- /src/router/guard/index.ts: -------------------------------------------------------------------------------- 1 | import type { Router } from 'vue-router' 2 | import { setRouteEmitter } from '@/utils/route-listener' 3 | import setupPermissionGuard from './permission' 4 | import setupUserLoginInfoGuard from './userLoginInfo' 5 | 6 | function setupPageGuard(router: Router) { 7 | router.beforeEach(async (to) => { 8 | // emit route change 9 | setRouteEmitter(to) 10 | }) 11 | } 12 | 13 | export default function createRouteGuard(router: Router) { 14 | setupPageGuard(router) 15 | setupUserLoginInfoGuard(router) 16 | setupPermissionGuard(router) 17 | } 18 | -------------------------------------------------------------------------------- /src/router/guard/permission.ts: -------------------------------------------------------------------------------- 1 | import type { Router, RouteRecordNormalized } from 'vue-router' 2 | import NProgress from 'nprogress' // progress bar 3 | 4 | import usePermission from '@/hooks/permission' 5 | import { useUserStore, useAppStore } from '@/store' 6 | import { appRoutes } from '../routes' 7 | import { WHITE_LIST, NOT_FOUND } from '../constants' 8 | 9 | export default function setupPermissionGuard(router: Router) { 10 | router.beforeEach(async (to, from, next) => { 11 | const appStore = useAppStore() 12 | const userStore = useUserStore() 13 | const Permission = usePermission() 14 | const permissionsAllow = Permission.accessRouter(to) 15 | if (appStore.menuFromServer) { 16 | // Handle routing configuration from the server 17 | 18 | // Refine the permission logic from the server's menu configuration as needed 19 | if (!appStore.appAsyncMenus.length && !WHITE_LIST.find((el) => el.name === to.name)) { 20 | await appStore.fetchServerMenuConfig() 21 | } 22 | const serverMenuConfig = [...appStore.appAsyncMenus, ...WHITE_LIST] 23 | 24 | let exist = false 25 | while (serverMenuConfig.length && !exist) { 26 | const element = serverMenuConfig.shift() 27 | if (element?.name === to.name) exist = true 28 | 29 | if (element?.children) { 30 | serverMenuConfig.push(...(element.children as unknown as RouteRecordNormalized[])) 31 | } 32 | } 33 | if (exist && permissionsAllow) { 34 | next() 35 | } else next(NOT_FOUND) 36 | } else { 37 | // eslint-disable-next-line no-lonely-if 38 | if (permissionsAllow) next() 39 | else { 40 | const destination = Permission.findFirstPermissionRoute(appRoutes, userStore.role) || NOT_FOUND 41 | next(destination) 42 | } 43 | } 44 | NProgress.done() 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /src/router/guard/userLoginInfo.ts: -------------------------------------------------------------------------------- 1 | import type { Router } from 'vue-router' 2 | import NProgress from 'nprogress' // progress bar 3 | import { useStorage } from '@vueuse/core' 4 | 5 | export default function setupUserLoginInfoGuard(router: Router) { 6 | router.beforeEach(async (to, from, next) => { 7 | NProgress.start() 8 | 9 | if (from.matched?.[0]?.path === '/dashboard') { 10 | return next() 11 | } 12 | try { 13 | const appStore = useAppStore() 14 | // If there is info in URL (direct from cloud) 15 | if (to.query.info) { 16 | const config = JSON.parse(atob(to.query.info as string)) 17 | useStorage('config', config, localStorage, { 18 | mergeDefaults: (storageValue, defaults) => { 19 | return { 20 | ...storageValue, 21 | ...defaults, 22 | } 23 | }, 24 | }) 25 | // Update settings with config from URL 26 | appStore.updateSettings(config) 27 | delete to.query.info 28 | return next({ path: to.path, query: to.query }) 29 | } 30 | 31 | // Update settings with config from local storage 32 | appStore.updateSettings(useStorage('config', {}).value) 33 | const { role } = storeToRefs(useUserStore()) 34 | 35 | const { guideModalVisible } = storeToRefs(useAppStore()) 36 | if (role.value === 'cloud') { 37 | guideModalVisible.value = true 38 | } 39 | } catch (error) { 40 | // error 41 | } 42 | return next() 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router' 2 | import NProgress from 'nprogress' // progress bar 3 | import 'nprogress/nprogress.css' 4 | 5 | import { appRoutes } from './routes' 6 | import { REDIRECT_MAIN, NOT_FOUND_ROUTE } from './routes/base' 7 | import createRouteGuard from './guard' 8 | import client from './routes/client' 9 | 10 | NProgress.configure({ showSpinner: false }) // NProgress Configuration 11 | 12 | const extractContextPath = (path: string): string => { 13 | // Find the index of 'dashboard' 14 | const dashboardIndex = path.lastIndexOf('/dashboard') 15 | 16 | if (dashboardIndex === -1) { 17 | return '' // Return empty string if 'dashboard' is not found 18 | } 19 | 20 | // Extract the substring up to 'dashboard' 21 | const contextPath = path.substring(0, dashboardIndex + 1) 22 | 23 | return contextPath 24 | } 25 | 26 | const router = createRouter({ 27 | history: createWebHashHistory(extractContextPath(window.location.pathname)), 28 | routes: [ 29 | { 30 | path: '/', 31 | redirect: '/dashboard/query', 32 | }, 33 | ...appRoutes, 34 | client, 35 | REDIRECT_MAIN, 36 | NOT_FOUND_ROUTE, 37 | ], 38 | scrollBehavior() { 39 | return { top: 0 } 40 | }, 41 | }) 42 | 43 | createRouteGuard(router) 44 | 45 | export default router 46 | -------------------------------------------------------------------------------- /src/router/routes/base.ts: -------------------------------------------------------------------------------- 1 | import { REDIRECT_ROUTE_NAME } from '@/router/constants' 2 | import { RouteRecordRaw } from 'vue-router' 3 | 4 | export const DEFAULT_LAYOUT = () => import('@/layout/default-layout.vue') 5 | 6 | export const REDIRECT_MAIN: RouteRecordRaw = { 7 | path: '/redirect', 8 | name: 'redirectWrapper', 9 | component: DEFAULT_LAYOUT, 10 | meta: { 11 | requiresAuth: true, 12 | hideInMenu: true, 13 | }, 14 | children: [ 15 | { 16 | path: '/redirect/:path', 17 | name: REDIRECT_ROUTE_NAME, 18 | component: () => import('@/views/redirect/index.vue'), 19 | meta: { 20 | requiresAuth: true, 21 | hideInMenu: true, 22 | }, 23 | }, 24 | ], 25 | } 26 | 27 | export const NOT_FOUND_ROUTE = { 28 | path: '/:pathMatch(.*)*', 29 | name: 'notFound', 30 | component: () => import('@/views/not-found/index.vue'), 31 | } 32 | -------------------------------------------------------------------------------- /src/router/routes/client.ts: -------------------------------------------------------------------------------- 1 | import ClientLayout from '@/tauri/layout.vue' 2 | import { AppRouteRecordRaw } from './types' 3 | 4 | export default { 5 | path: '/client', 6 | name: 'ClientComponent', 7 | meta: { 8 | requiresAuth: false, 9 | }, 10 | component: ClientLayout, 11 | children: [ 12 | { 13 | path: 'about', 14 | name: 'clientAbout', 15 | component: () => import('@/tauri/about.vue'), 16 | meta: { 17 | requiresAuth: false, 18 | }, 19 | }, 20 | ], 21 | } as AppRouteRecordRaw 22 | -------------------------------------------------------------------------------- /src/router/routes/externalModules/arco.ts: -------------------------------------------------------------------------------- 1 | // export default { 2 | // path: 'https://arco.design', 3 | // name: 'arcoWebsite', 4 | // meta: { 5 | // locale: 'menu.arcoWebsite', 6 | // icon: 'icon-link', 7 | // requiresAuth: true, 8 | // order: 8, 9 | // }, 10 | // } 11 | -------------------------------------------------------------------------------- /src/router/routes/externalModules/faq.ts: -------------------------------------------------------------------------------- 1 | // export default { 2 | // path: 'https://arco.design/vue/docs/pro/faq', 3 | // name: 'faq', 4 | // meta: { 5 | // locale: 'menu.faq', 6 | // icon: 'icon-question-circle', 7 | // requiresAuth: true, 8 | // order: 9, 9 | // }, 10 | // } 11 | -------------------------------------------------------------------------------- /src/router/routes/index.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordNormalized } from 'vue-router' 2 | 3 | const modules = import.meta.glob('./modules/*.ts', { eager: true }) 4 | const externalModules = import.meta.glob('./externalModules/*.ts', { 5 | eager: true, 6 | }) 7 | 8 | function formatModules(_modules: any, result: RouteRecordNormalized[]) { 9 | Object.keys(_modules).forEach((key) => { 10 | const defaultModule = _modules[key].default 11 | if (!defaultModule) return 12 | const moduleList = Array.isArray(defaultModule) ? [...defaultModule] : [defaultModule] 13 | result.push(...moduleList) 14 | }) 15 | return result 16 | } 17 | 18 | export const appRoutes: RouteRecordNormalized[] = formatModules(modules, []) 19 | 20 | export const appExternalRoutes: RouteRecordNormalized[] = formatModules(externalModules, []) 21 | -------------------------------------------------------------------------------- /src/router/routes/types.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue' 2 | import type { RouteMeta, NavigationGuard } from 'vue-router' 3 | 4 | export type Component = 5 | | ReturnType 6 | | (() => Promise) 7 | | (() => Promise) 8 | 9 | export interface AppRouteRecordRaw { 10 | path: string 11 | name?: string | symbol 12 | meta?: RouteMeta 13 | redirect?: string 14 | component: Component | string 15 | children?: AppRouteRecordRaw[] 16 | alias?: string | string[] 17 | props?: Record 18 | beforeEnter?: NavigationGuard | NavigationGuard[] 19 | fullPath?: string 20 | } 21 | -------------------------------------------------------------------------------- /src/router/typings.d.ts: -------------------------------------------------------------------------------- 1 | import 'vue-router' 2 | 3 | declare module 'vue-router' { 4 | interface RouteMeta { 5 | roles?: string[] // Controls roles that have access to the page 6 | requiresAuth: boolean // Whether login is required to access the current page (every route must declare) 7 | icon?: string // The icon show in the side menu 8 | locale?: string // The locale name show in side menu and breadcrumb 9 | hideInMenu?: boolean // If true, it is not displayed in the side menu 10 | hideChildrenInMenu?: boolean // if set true, the children are not displayed in the side menu 11 | activeMenu?: string // if set name, the menu will be highlighted according to the name you set 12 | order?: number // Sort routing menu items. If set key, the higher the value, the more forward it is 13 | noAffix?: boolean // if set true, the tag will not affix in the tab-bar 14 | ignoreCache?: boolean // if set true, the page will not be cached 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia' 2 | import useAppStore from './modules/app' 3 | import useUserStore from './modules/user' 4 | import useTabBarStore from './modules/tab-bar' 5 | import useDataBaseStore from './modules/database' 6 | import useCodeRunStore from './modules/code-run' 7 | import useLogStore from './modules/log' 8 | import useIngestStore from './modules/ingest' 9 | import { useStatusBarStore } from './modules/status-bar' 10 | 11 | const pinia = createPinia() 12 | 13 | export { 14 | useAppStore, 15 | useUserStore, 16 | useTabBarStore, 17 | useDataBaseStore, 18 | useCodeRunStore, 19 | useLogStore, 20 | useIngestStore, 21 | useStatusBarStore, 22 | } 23 | export default pinia 24 | -------------------------------------------------------------------------------- /src/store/modules/app/types.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordNormalized } from 'vue-router' 2 | 3 | export interface AppState { 4 | theme: string 5 | colorWeak: boolean 6 | navbar: boolean 7 | menu: boolean 8 | hideMenu: boolean 9 | menuCollapse: boolean 10 | footer: boolean 11 | themeColor: string 12 | menuWidth: number 13 | globalSettings: boolean 14 | device: string 15 | tabBar: boolean 16 | menuFromServer: boolean 17 | serverMenu: RouteRecordNormalized[] 18 | database: string 19 | host: string 20 | databaseList: Array 21 | guideModalVisible: boolean 22 | username: string 23 | password: string 24 | dbId: string 25 | lifetime: string 26 | menuSelectedKey: string 27 | userTimezone: string 28 | isFullScreen: boolean 29 | authHeader: string 30 | [key: string]: unknown 31 | } 32 | -------------------------------------------------------------------------------- /src/store/modules/code-run/types.ts: -------------------------------------------------------------------------------- 1 | export interface SchemaType { 2 | name: string 3 | data_type: string 4 | } 5 | 6 | export interface ChartFormType { 7 | chartType: string 8 | selectedYTypes: string[] 9 | groupBySelectedTypes: string[] 10 | xAxisType: SchemaType 11 | } 12 | 13 | export interface RecordsType { 14 | rows: [][] 15 | schema: { column_schemas: SchemaType[] } 16 | } 17 | 18 | export interface OutputType { 19 | records?: RecordsType 20 | affectedrows?: any 21 | } 22 | 23 | export interface DimensionType { 24 | name: string 25 | } 26 | 27 | export interface ResultType { 28 | records: RecordsType 29 | dimensionsAndXName: { dimensions: DimensionType[]; xAxis: string } 30 | key: number | string 31 | type: string 32 | name?: string 33 | executionTime?: number 34 | } 35 | 36 | export interface SeriesType { 37 | name: string 38 | type: string 39 | smooth: boolean 40 | encode: { x: string; y: string; label?: string[] } 41 | symbolSize: number 42 | datasetIndex?: number 43 | } 44 | 45 | export interface datasetType { 46 | dimensions?: DimensionType[] 47 | source?: Array 48 | transform?: any 49 | fromDatasetIndex?: number 50 | } 51 | 52 | export interface PromForm { 53 | time: number 54 | range: Array 55 | step: string 56 | } 57 | -------------------------------------------------------------------------------- /src/store/modules/database/types.ts: -------------------------------------------------------------------------------- 1 | export interface tableState { 2 | columns: Array 3 | tableList: Array 4 | count: number 5 | } 6 | 7 | export interface TreeChild { 8 | key: string | number 9 | title: string 10 | isLeaf?: boolean 11 | } 12 | 13 | export interface TreeData extends TreeChild { 14 | children: TreeChild[] 15 | } 16 | 17 | export interface TableTreeChild extends TreeChild { 18 | key: string 19 | dataType: string 20 | iconType: string 21 | parentKey: number 22 | } 23 | 24 | export interface TableDetail extends TreeChild { 25 | key: string 26 | parentKey: number 27 | tableName: string 28 | info: any 29 | class: string 30 | } 31 | 32 | export interface TableTreeParent extends TreeData { 33 | key: number 34 | childrenType: 'columns' | 'details' 35 | timeIndexName: string 36 | children: TableTreeChild[] | TableDetail[] 37 | columns: TableTreeChild[] 38 | details: TableDetail[] 39 | tableType?: string 40 | } 41 | 42 | export interface ScriptTreeData extends TreeData { 43 | code: string 44 | } 45 | -------------------------------------------------------------------------------- /src/store/modules/ingest/index.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { defineStore } from 'pinia' 3 | 4 | const useIngestStore = defineStore('ingest', () => { 5 | const activeTab = ref('influxdb-input') 6 | const precision = ref('ns') 7 | const footer = ref<{ [key: string]: boolean }>({ 8 | 'influxdb-input': true, 9 | 'influxdb-upload': true, 10 | }) 11 | 12 | return { 13 | activeTab, 14 | precision, 15 | footer, 16 | } 17 | }) 18 | export default useIngestStore 19 | -------------------------------------------------------------------------------- /src/store/modules/log/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import type { Ref } from 'vue' 3 | import { Log } from './types' 4 | 5 | const useLogStore = defineStore('log', () => { 6 | const logs: Ref = ref([]) 7 | 8 | function push(log: Log, type: string) { 9 | logs.value.push({ 10 | ...log, 11 | type, 12 | }) 13 | } 14 | function clear(type: string | string[]) { 15 | const types = Array.isArray(type) ? type : [type] 16 | if (!type) { 17 | logs.value = [] 18 | } 19 | logs.value = logs.value.filter((log) => !types.includes(log.type)) 20 | } 21 | 22 | return { 23 | logs, 24 | push, 25 | clear, 26 | } 27 | }) 28 | export default useLogStore 29 | -------------------------------------------------------------------------------- /src/store/modules/log/types.ts: -------------------------------------------------------------------------------- 1 | export interface ResultInLog { 2 | type: string 3 | rowCount: number 4 | } 5 | 6 | export interface Log { 7 | sql?: string 8 | error?: string 9 | name?: string 10 | type: string 11 | promInfo?: object 12 | codeInfo: string 13 | codeTooltip?: string 14 | message: string 15 | startTime?: string 16 | networkTime?: number 17 | } 18 | -------------------------------------------------------------------------------- /src/store/modules/status-bar/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { VNode } from 'vue' 3 | 4 | export interface StatusContentSimple { 5 | text?: string 6 | icon?: Component | string 7 | onClick?: (item: any, evt: PointerEvent) => void 8 | } 9 | 10 | export type StatusContentType = StatusContentSimple | VNode 11 | 12 | export type StatusItem = { 13 | id: number 14 | pos?: 'left' | 'right' 15 | timeout?: number 16 | content: StatusContentType 17 | } 18 | type StatusConfig = { 19 | pos?: 'left' | 'right' 20 | timeout?: number 21 | } 22 | export const useStatusBarStore = defineStore('statusBarStore', () => { 23 | let idNum = 0 24 | const status = ref([]) 25 | const statusLeft = computed(() => { 26 | return status.value.filter((item) => item.pos === 'left').map((item) => item.content) 27 | }) 28 | 29 | const statusRight = computed(() => { 30 | return status.value.filter((item) => item.pos !== 'left').map((item) => item.content) 31 | }) 32 | 33 | function remove(id: number) { 34 | for (let index = 0; index < status.value.length; index += 1) { 35 | if (id === status.value[index].id) { 36 | status.value.splice(index, 1) 37 | break 38 | } 39 | } 40 | } 41 | 42 | function add(item: StatusContentType, conf = {} as StatusConfig) { 43 | idNum += 1 44 | const id = idNum 45 | status.value.push({ content: item, ...conf, id }) 46 | if (conf.timeout) { 47 | setTimeout(() => { 48 | remove(id) 49 | }, conf.timeout) 50 | } 51 | return id 52 | } 53 | 54 | function update(id: number, item: StatusContentType, conf = {} as StatusConfig) { 55 | for (let index = 0; index < status.value.length; index += 1) { 56 | if (id === status.value[index].id) { 57 | status.value[index] = { 58 | id, 59 | content: item, 60 | ...conf, 61 | } 62 | break 63 | } 64 | } 65 | } 66 | 67 | return { 68 | status, 69 | add, 70 | remove, 71 | update, 72 | statusLeft, 73 | statusRight, 74 | } 75 | }) 76 | -------------------------------------------------------------------------------- /src/store/modules/tab-bar/index.ts: -------------------------------------------------------------------------------- 1 | import type { RouteLocationNormalized } from 'vue-router' 2 | import { defineStore } from 'pinia' 3 | import { DEFAULT_ROUTE, DEFAULT_ROUTE_NAME, REDIRECT_ROUTE_NAME } from '@/router/constants' 4 | import { isString } from '@/utils/is' 5 | import { TabBarState, TagProps } from './types' 6 | 7 | const formatTag = (route: RouteLocationNormalized): TagProps => { 8 | const { name, meta, fullPath, query } = route 9 | return { 10 | title: meta.locale || '', 11 | name: String(name), 12 | fullPath, 13 | query, 14 | ignoreCache: meta.ignoreCache, 15 | } 16 | } 17 | 18 | const BAN_LIST = [REDIRECT_ROUTE_NAME] 19 | 20 | const useTabBarStore = defineStore('tabBar', { 21 | state: (): TabBarState => ({ 22 | cacheTabList: new Set([DEFAULT_ROUTE_NAME]), 23 | tagList: [DEFAULT_ROUTE], 24 | }), 25 | 26 | getters: { 27 | getTabList(): TagProps[] { 28 | return this.tagList 29 | }, 30 | getCacheList(): string[] { 31 | return Array.from(this.cacheTabList) 32 | }, 33 | }, 34 | 35 | actions: { 36 | updateTabList(route: RouteLocationNormalized) { 37 | if (BAN_LIST.includes(route.name as string)) return 38 | this.tagList.push(formatTag(route)) 39 | if (!route.meta.ignoreCache) { 40 | this.cacheTabList.add(route.name as string) 41 | } 42 | }, 43 | deleteTag(idx: number, tag: TagProps) { 44 | this.tagList.splice(idx, 1) 45 | this.cacheTabList.delete(tag.name) 46 | }, 47 | addCache(name: string) { 48 | if (isString(name) && name !== '') this.cacheTabList.add(name) 49 | }, 50 | deleteCache(tag: TagProps) { 51 | this.cacheTabList.delete(tag.name) 52 | }, 53 | freshTabList(tags: TagProps[]) { 54 | this.tagList = tags 55 | this.cacheTabList.clear() 56 | this.tagList 57 | .filter((el) => !el.ignoreCache) 58 | .map((el) => el.name) 59 | .forEach((x) => this.cacheTabList.add(x)) 60 | }, 61 | resetTabList() { 62 | this.tagList = [DEFAULT_ROUTE] 63 | this.cacheTabList.clear() 64 | this.cacheTabList.add(DEFAULT_ROUTE_NAME) 65 | }, 66 | }, 67 | }) 68 | 69 | export default useTabBarStore 70 | -------------------------------------------------------------------------------- /src/store/modules/tab-bar/types.ts: -------------------------------------------------------------------------------- 1 | export interface TagProps { 2 | title: string 3 | name: string 4 | fullPath: string 5 | query?: any 6 | ignoreCache?: boolean 7 | } 8 | 9 | export interface TabBarState { 10 | tagList: TagProps[] 11 | cacheTabList: Set 12 | } 13 | -------------------------------------------------------------------------------- /src/store/modules/user/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import type { Ref } from 'vue' 3 | 4 | const useUserStore = defineStore('user', () => { 5 | const role: Ref = ref('admin') 6 | const dataStatusMap = ref<{ [key: string]: boolean }>({ 7 | tables: false, 8 | scripts: false, 9 | }) 10 | const dataNames = ['tables', 'scripts'] 11 | 12 | function setRole(r: string) { 13 | role.value = r 14 | } 15 | 16 | const updateDataStatus = (name: string, status: boolean) => { 17 | dataStatusMap.value[name] = status 18 | } 19 | 20 | const resetDataStatus = () => { 21 | dataNames.forEach((name: string) => { 22 | updateDataStatus(name, false) 23 | }) 24 | } 25 | 26 | return { 27 | role, 28 | dataStatusMap, 29 | setRole, 30 | updateDataStatus, 31 | resetDataStatus, 32 | } 33 | }) 34 | 35 | export default useUserStore 36 | -------------------------------------------------------------------------------- /src/store/modules/user/types.ts: -------------------------------------------------------------------------------- 1 | export type RoleType = '' | '*' | 'admin' | 'user' 2 | export interface UserState { 3 | name?: string 4 | avatar?: string 5 | job?: string 6 | organization?: string 7 | location?: string 8 | email?: string 9 | introduction?: string 10 | personalWebsite?: string 11 | jobName?: string 12 | organizationName?: string 13 | locationName?: string 14 | phone?: string 15 | registrationDate?: string 16 | accountId?: string 17 | certification?: number 18 | role: RoleType 19 | } 20 | -------------------------------------------------------------------------------- /src/tauri/index.js: -------------------------------------------------------------------------------- 1 | import './menu' 2 | -------------------------------------------------------------------------------- /src/tauri/layout.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/tauri/menu.js: -------------------------------------------------------------------------------- 1 | import { Menu, MenuItem, Submenu, PredefinedMenuItem } from '@tauri-apps/api/menu' 2 | import { WebviewWindow } from '@tauri-apps/api/webviewWindow' 3 | import { getVersion } from '@tauri-apps/api/app' 4 | 5 | async function setAppMenu() { 6 | try { 7 | const quitItem = await PredefinedMenuItem.new({ 8 | text: 'Quit', 9 | item: 'Quit', 10 | }) 11 | const version = await getVersion() 12 | 13 | const aboutItem = await MenuItem.new({ 14 | id: 'info', 15 | text: `Version : ${version}`, 16 | action: () => { 17 | const webview = new WebviewWindow('client', { 18 | url: '/#/client/about', 19 | title: 'About Greptime Dashboard', 20 | alwaysOnTop: true, 21 | center: true, 22 | height: 300, 23 | width: 780, 24 | }) 25 | webview.once('tauri://created', function () { 26 | console.log('webview created') 27 | }) 28 | }, 29 | }) 30 | // const checkItem = await MenuItem.new({ 31 | // id: 'about', 32 | // text: 'Check Version', 33 | // action: () => { 34 | // checkForUpdates() 35 | // }, 36 | // }) 37 | 38 | const submenu = await Submenu.new({ 39 | text: 'App', 40 | items: [aboutItem, quitItem], 41 | }) 42 | const menu = await Menu.new({ 43 | text: 'Greptime Dashboard', 44 | items: [submenu], 45 | }) 46 | await menu.setAsAppMenu() 47 | } catch (error) { 48 | console.error('❌ Failed to set application menu:', error) 49 | if (error instanceof Error) { 50 | console.error('Error details:', error.message, error.stack) 51 | } else { 52 | console.error('Unknown error:', error) 53 | } 54 | } 55 | } 56 | 57 | setAppMenu() 58 | -------------------------------------------------------------------------------- /src/types/echarts.ts: -------------------------------------------------------------------------------- 1 | import { CallbackDataParams } from 'echarts/types/dist/shared' 2 | 3 | export interface ToolTipFormatterParams extends CallbackDataParams { 4 | axisDim: string 5 | axisIndex: number 6 | axisType: string 7 | axisId: string 8 | axisValue: string 9 | axisValueLabel: string 10 | } 11 | -------------------------------------------------------------------------------- /src/types/global.ts: -------------------------------------------------------------------------------- 1 | export interface AnyObject { 2 | [key: string]: any 3 | } 4 | 5 | export interface StringObject { 6 | [key: string]: string 7 | } 8 | 9 | export interface NumberObject { 10 | [key: string]: number 11 | } 12 | 13 | export interface OptionsType { 14 | value: unknown 15 | label: string 16 | } 17 | 18 | export interface NodeOptions extends OptionsType { 19 | children?: NodeOptions[] 20 | } 21 | 22 | export interface GetParams { 23 | body: null 24 | type: string 25 | url: string 26 | } 27 | 28 | export interface PostData { 29 | body: string 30 | type: string 31 | url: string 32 | } 33 | 34 | export interface Pagination { 35 | current: number 36 | pageSize: number 37 | total?: number 38 | } 39 | 40 | export type TimeRanger = [string, string] 41 | 42 | export interface GeneralChart { 43 | xAxis: string[] 44 | data: Array<{ name: string; value: number[] }> 45 | } 46 | -------------------------------------------------------------------------------- /src/utils/env.ts: -------------------------------------------------------------------------------- 1 | const debug = process.env.NODE_ENV !== 'production' 2 | 3 | export default debug 4 | -------------------------------------------------------------------------------- /src/utils/event.ts: -------------------------------------------------------------------------------- 1 | export function addEventListen( 2 | target: Window | HTMLElement, 3 | event: string, 4 | handler: EventListenerOrEventListenerObject, 5 | capture = false 6 | ) { 7 | if (target.addEventListener && typeof target.addEventListener === 'function') { 8 | target.addEventListener(event, handler, capture) 9 | } 10 | } 11 | 12 | export function removeEventListen( 13 | target: Window | HTMLElement, 14 | event: string, 15 | handler: EventListenerOrEventListenerObject, 16 | capture = false 17 | ) { 18 | if (target.removeEventListener && typeof target.removeEventListener === 'function') { 19 | target.removeEventListener(event, handler, capture) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | 3 | type TargetContext = '_self' | '_parent' | '_blank' | '_top' 4 | 5 | const units = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'] 6 | const PLACES = 15 7 | const FIXED = 2 8 | 9 | export const openWindow = (url: string, opts?: { target?: TargetContext; [key: string]: any }) => { 10 | const { target = '_blank', ...others } = opts || {} 11 | window.open( 12 | url, 13 | target, 14 | Object.entries(others) 15 | .reduce((preValue: string[], curValue) => { 16 | const [key, value] = curValue 17 | return [...preValue, `${key}=${value}`] 18 | }, []) 19 | .join(',') 20 | ) 21 | } 22 | 23 | export const importFiles = (file: any) => { 24 | return new Promise((resolve, reject) => { 25 | const script = document.createElement('script') 26 | script.src = file 27 | script.type = 'text/javascript' 28 | script.defer = true 29 | document.getElementsByTagName('head').item(0)?.appendChild(script) 30 | 31 | script.onload = () => { 32 | resolve(script) 33 | } 34 | script.onerror = () => { 35 | reject() 36 | } 37 | }) 38 | } 39 | export const regexUrl = new RegExp( 40 | '^(?!mailto:)(?:(?:http|https|ftp)://)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$', 41 | 'i' 42 | ) 43 | 44 | export const dateFormatter = (dataType: string, value: number | null) => { 45 | switch (dataType) { 46 | case 'Date': 47 | return value && dayjs(0).add(value, 'day').format('YYYY-MM-DD HH:mm:ss') 48 | case 'TimestampSecond': 49 | return value && dayjs.unix(value).format('YYYY-MM-DD HH:mm:ss') 50 | case 'DateTime': 51 | case 'TimestampMillisecond': 52 | return value && dayjs(value).format('YYYY-MM-DD HH:mm:ss') 53 | case 'TimestampMicrosecond': 54 | return value && dayjs(value / 1000).format('YYYY-MM-DD HH:mm:ss') 55 | case 'TimestampNanosecond': 56 | return value && dayjs(value / 1000000).format('YYYY-MM-DD HH:mm:ss') 57 | default: 58 | return null 59 | } 60 | } 61 | 62 | // TODO: perhaps a better function 63 | export const groupByToMap = (array: T[], predicate: (value: T, index: number, array2: T[]) => Q) => 64 | array.reduce((map, value, index, array2) => { 65 | const key = predicate(value, index, array2) 66 | const collection = map.get(key) 67 | if (!collection) { 68 | map.set(key, [value]) 69 | } else { 70 | collection.push(value) 71 | } 72 | return map 73 | }, new Map()) 74 | 75 | export const getIconUrl = (iconName: string) => new URL(`/src/assets/images/${iconName}.png`, import.meta.url).href 76 | 77 | export default null 78 | -------------------------------------------------------------------------------- /src/utils/is.ts: -------------------------------------------------------------------------------- 1 | const opt = Object.prototype.toString 2 | 3 | export function isArray(obj: any): obj is any[] { 4 | return opt.call(obj) === '[object Array]' 5 | } 6 | 7 | export function isObject(obj: any): obj is { [key: string]: any } { 8 | return opt.call(obj) === '[object Object]' 9 | } 10 | 11 | export function isString(obj: any): obj is string { 12 | return opt.call(obj) === '[object String]' 13 | } 14 | 15 | export function isNumber(obj: any): obj is number { 16 | return opt.call(obj) === '[object Number]' && obj === obj // eslint-disable-line 17 | } 18 | 19 | export function isRegExp(obj: any) { 20 | return opt.call(obj) === '[object RegExp]' 21 | } 22 | 23 | export function isFile(obj: any): obj is File { 24 | return opt.call(obj) === '[object File]' 25 | } 26 | 27 | export function isBlob(obj: any): obj is Blob { 28 | return opt.call(obj) === '[object Blob]' 29 | } 30 | 31 | export function isUndefined(obj: any): obj is undefined { 32 | return obj === undefined 33 | } 34 | 35 | export function isNull(obj: any): obj is null { 36 | return obj === null 37 | } 38 | 39 | export function isFunction(obj: any): obj is (...args: any[]) => any { 40 | return typeof obj === 'function' 41 | } 42 | 43 | export function isEmptyObject(obj: any): boolean { 44 | return isObject(obj) && Object.keys(obj).length === 0 45 | } 46 | 47 | export function isExist(obj: any): boolean { 48 | return obj || obj === 0 49 | } 50 | 51 | export function isWindow(el: any): el is Window { 52 | return el === window 53 | } 54 | -------------------------------------------------------------------------------- /src/utils/monitor.ts: -------------------------------------------------------------------------------- 1 | import { App, ComponentPublicInstance } from 'vue' 2 | import axios from 'axios' 3 | 4 | export default function handleError(Vue: App, baseUrl: string) { 5 | if (!baseUrl) { 6 | return 7 | } 8 | Vue.config.errorHandler = (err: unknown, instance: ComponentPublicInstance | null, info: string) => { 9 | // send error info 10 | axios.post(`${baseUrl}/report-error`, { 11 | err, 12 | instance, 13 | info, 14 | // location: window.location.href, 15 | // message: err.message, 16 | // stack: err.stack, 17 | // browserInfo: getBrowserInfo(), 18 | // user info 19 | // dom info 20 | // url info 21 | // ... 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/route-listener.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Listening to routes alone would waste rendering performance. Use the publish-subscribe model for distribution management 3 | */ 4 | import mitt, { Handler } from 'mitt' 5 | import type { RouteLocationNormalized } from 'vue-router' 6 | 7 | const emitter = mitt() 8 | 9 | const key = Symbol('ROUTE_CHANGE') 10 | 11 | let latestRoute: RouteLocationNormalized 12 | 13 | export function setRouteEmitter(to: RouteLocationNormalized) { 14 | emitter.emit(key, to) 15 | latestRoute = to 16 | } 17 | 18 | export function listenerRouteChange(handler: (route: RouteLocationNormalized) => void, immediate = true) { 19 | emitter.on(key, handler as Handler) 20 | if (immediate && latestRoute) { 21 | handler(latestRoute) 22 | } 23 | } 24 | 25 | export function removeRouteListener() { 26 | emitter.off(key) 27 | } 28 | -------------------------------------------------------------------------------- /src/views/dashboard/ingest/doc.md: -------------------------------------------------------------------------------- 1 | ### About InfluxDB Line Protocol 2 | 3 | The general syntax of an InfluxDB Line Protocol data point is as follows: 4 | 5 | ``` 6 | [,=[,...]] =[,=[,...]] [timestamp] 7 | ``` 8 | 9 | - `
`: The name of the Table. 10 | - `=`: Tags provide metadata for the data point. They are optional but useful for filtering and grouping data. 11 | - `=`: Fields represent the actual data values associated with the table. 12 | - `[timestamp]`: Optional. If not provided, the server's current time will be used. 13 | 14 | ### Example 15 | 16 | Let's say we want to write a data point representing CPU usage: 17 | 18 | - Table: `cpu_usage` 19 | - Tags: `host=server1,region=us-west` 20 | - Fields: `usage_user=80,usage_system=10` 21 | 22 | The Line Protocol for this data point would look like: 23 | 24 | ``` 25 | cpu_usage,host=server1,region=us-west usage_user=80,usage_system=10 26 | ``` 27 | 28 | If you want to include a timestamp (e.g., 1621401600000000000, which represents June 1, 2021, at 12:00:00 AM UTC), you can add it at the end: 29 | 30 | ``` 31 | cpu_usage,host=server1,region=us-west usage_user=80,usage_system=10 1621401600000000000 32 | ``` 33 | -------------------------------------------------------------------------------- /src/views/dashboard/ingest/influxdb/index.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/views/dashboard/ingest/influxdb/input.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 62 | 63 | 113 | -------------------------------------------------------------------------------- /src/views/dashboard/ingest/panel-icon.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 15 | 16 | 23 | -------------------------------------------------------------------------------- /src/views/dashboard/logs/query/ChartContainer.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 48 | 49 | 55 | -------------------------------------------------------------------------------- /src/views/dashboard/logs/query/ExportLog.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/views/dashboard/logs/query/FormView.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 35 | 36 | 41 | -------------------------------------------------------------------------------- /src/views/dashboard/logs/query/FunnelChart.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /src/views/dashboard/logs/query/JSONView.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 31 | 32 | 37 | -------------------------------------------------------------------------------- /src/views/dashboard/logs/query/LogDetail.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 54 | -------------------------------------------------------------------------------- /src/views/dashboard/logs/query/SavedQuery.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/views/dashboard/logs/query/types.ts: -------------------------------------------------------------------------------- 1 | export type ColumnType = { 2 | name: string 3 | data_type: string 4 | label: string 5 | semantic_type: string 6 | } 7 | 8 | export type Condition = { 9 | field: ColumnType 10 | op: string 11 | value: string 12 | rel: 'and' | 'or' 13 | } 14 | 15 | export type TSColumn = ColumnType & { 16 | multiple: number 17 | } 18 | -------------------------------------------------------------------------------- /src/views/dashboard/modules/explain/explain-chart/chart-controls.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 82 | 83 | 107 | -------------------------------------------------------------------------------- /src/views/dashboard/modules/explain/explain-chart/navigation-arrows.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 42 | 43 | 66 | -------------------------------------------------------------------------------- /src/views/dashboard/modules/explain/explain-chart/zoom-controls.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 19 | -------------------------------------------------------------------------------- /src/views/dashboard/modules/explain/utils.ts: -------------------------------------------------------------------------------- 1 | export const CARD_DIMENSIONS = { 2 | width: 200, 3 | minHeight: 40, 4 | progressBarHeight: 30, 5 | singleMetricHeight: 20, 6 | expandedBaseHeight: 25, 7 | metricLineHeight: 18, 8 | padding: 20, // Padding between nodes 9 | horizontalPadding: 60, // Add this new parameter for horizontal spacing 10 | } 11 | 12 | export const NODE_INDEX_CARD = { 13 | width: 70, 14 | height: 30, 15 | fontSize: 12, 16 | } 17 | 18 | export function formatNumber(value) { 19 | return new Intl.NumberFormat().format(value) 20 | } 21 | 22 | export function formatTimeValue(nanoseconds) { 23 | if (nanoseconds === undefined || nanoseconds === null) return '0' 24 | 25 | if (nanoseconds < 1000) return `${nanoseconds}ns` 26 | if (nanoseconds < 1000000) return `${(nanoseconds / 1000).toFixed(2)}μs` 27 | if (nanoseconds < 1000000000) return `${(nanoseconds / 1000000).toFixed(2)}ms` 28 | return `${(nanoseconds / 1000000000).toFixed(2)}s` 29 | } 30 | 31 | export function formatMetricValue(key, value) { 32 | if (typeof value === 'number') { 33 | if (key.includes('elapsed_compute')) { 34 | return formatTimeValue(value) 35 | } 36 | 37 | return formatNumber(value) 38 | } 39 | return value 40 | } 41 | 42 | export function getProgressColor(percentage) { 43 | if (percentage < 20) return 'var(--success-color)' 44 | if (percentage < 80) return 'var(--warning-color)' 45 | return 'var(--danger-color)' 46 | } 47 | 48 | export function formatMetricName(key: string): string { 49 | const map: Record = { 50 | output_rows: 'Rows', 51 | elapsed_compute: 'Duration', 52 | } 53 | return ( 54 | map[key] || 55 | key 56 | .split('_') 57 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) 58 | .join(' ') 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /src/views/dashboard/modules/favorite.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/views/dashboard/modules/list-tabs.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 32 | 33 | 49 | 50 | 65 | -------------------------------------------------------------------------------- /src/views/dashboard/modules/logs-layout.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 45 | 46 | 116 | -------------------------------------------------------------------------------- /src/views/dashboard/modules/logs.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 35 | 36 | 61 | -------------------------------------------------------------------------------- /src/views/dashboard/playground/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 76 | 77 | 89 | -------------------------------------------------------------------------------- /src/views/dashboard/scripts/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 37 | -------------------------------------------------------------------------------- /src/views/dashboard/status/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 46 | 47 | 74 | -------------------------------------------------------------------------------- /src/views/not-found/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | 20 | 31 | -------------------------------------------------------------------------------- /src/views/redirect/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ES2020", 5 | "allowJs": true, 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "esModuleInterop": true, 12 | "baseUrl": ".", 13 | "noImplicitAny": false, 14 | "strictNullChecks": false, 15 | "paths": { 16 | "@/*": ["src/*"] 17 | }, 18 | "types": ["node"], 19 | "lib": ["es2020", "dom"], 20 | "skipLibCheck": true, 21 | "noUnusedLocals": false 22 | }, 23 | "include": ["src/**/*", "src/**/*.vue", "*.ts"], 24 | "exclude": ["node_modules"] 25 | } 26 | --------------------------------------------------------------------------------