diff --git a/components.d.ts b/components.d.ts index 31e8e4c0..0157d51c 100644 --- a/components.d.ts +++ b/components.d.ts @@ -16,12 +16,10 @@ declare module 'vue' { NCard: typeof import('naive-ui')['NCard'] NCollapse: typeof import('naive-ui')['NCollapse'] NCollapseItem: typeof import('naive-ui')['NCollapseItem'] - NConfigProvider: typeof import('naive-ui')['NConfigProvider'] NDataTable: typeof import('naive-ui')['NDataTable'] - NDialogProvider: typeof import('naive-ui')['NDialogProvider'] NDivider: typeof import('naive-ui')['NDivider'] NDropdown: typeof import('naive-ui')['NDropdown'] - NEllipsis: typeof import('naive-ui')['NEllipsis'] + NEmpty: typeof import('naive-ui')['NEmpty'] NForm: typeof import('naive-ui')['NForm'] NFormItem: typeof import('naive-ui')['NFormItem'] NFormItemRow: typeof import('naive-ui')['NFormItemRow'] @@ -33,10 +31,9 @@ declare module 'vue' { NInputGroup: typeof import('naive-ui')['NInputGroup'] NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel'] NInputNumber: typeof import('naive-ui')['NInputNumber'] - NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider'] - NMessageProvider: typeof import('naive-ui')['NMessageProvider'] + NList: typeof import('naive-ui')['NList'] + NListItem: typeof import('naive-ui')['NListItem'] NModal: typeof import('naive-ui')['NModal'] - NNotificationProvider: typeof import('naive-ui')['NNotificationProvider'] NP: typeof import('naive-ui')['NP'] NPopover: typeof import('naive-ui')['NPopover'] NProgress: typeof import('naive-ui')['NProgress'] @@ -44,15 +41,15 @@ declare module 'vue' { NRadioGroup: typeof import('naive-ui')['NRadioGroup'] NScrollbar: typeof import('naive-ui')['NScrollbar'] NSelect: typeof import('naive-ui')['NSelect'] + NSpace: typeof import('naive-ui')['NSpace'] NSplit: typeof import('naive-ui')['NSplit'] NSwitch: typeof import('naive-ui')['NSwitch'] NTabPane: typeof import('naive-ui')['NTabPane'] NTabs: typeof import('naive-ui')['NTabs'] NTag: typeof import('naive-ui')['NTag'] NText: typeof import('naive-ui')['NText'] + NThing: typeof import('naive-ui')['NThing'] NTooltip: typeof import('naive-ui')['NTooltip'] - NUpload: typeof import('naive-ui')['NUpload'] - NUploadDragger: typeof import('naive-ui')['NUploadDragger'] PathBreadcrumb: typeof import('./src/components/PathBreadcrumb.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterMain: typeof import('./src/components/RouterMain.vue')['default'] diff --git a/package-lock.json b/package-lock.json index 0a5a1b86..2dfef338 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,6 +53,7 @@ "unplugin-auto-import": "^0.17.6", "unplugin-vue-components": "^0.27.1", "vite": "^5.3.1", + "vite-svg-loader": "^5.1.0", "vue-tsc": "^2.0.22" } }, @@ -2325,6 +2326,16 @@ "node": ">= 10" } }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2728,7 +2739,8 @@ "version": "0.12.0", "resolved": "https://registry.npmjs.org/@vicons/carbon/-/carbon-0.12.0.tgz", "integrity": "sha512-kCOgr/ZOhZzoiFLJ8pwxMa2TMxrkCUOA22qExPabus35F4+USqzcsxaPoYtqRd9ROOYiHrSqwapak/ywF0D9bg==", - "dev": true + "dev": true, + "license": "Apache 2.0" }, "node_modules/@vicons/fa": { "version": "0.12.0", @@ -3614,6 +3626,16 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/computeds": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz", @@ -3689,6 +3711,50 @@ "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==", "dev": true }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -3701,6 +3767,42 @@ "node": ">=4" } }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -3847,6 +3949,65 @@ "node": ">=8" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -6346,6 +6507,13 @@ "markdown-it": "bin/markdown-it.mjs" } }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/mdurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", @@ -7634,6 +7802,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svgo": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", + "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, "node_modules/synckit": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", @@ -7925,7 +8119,7 @@ "version": "5.5.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz", "integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==", - "devOptional": true, + "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8293,6 +8487,19 @@ } } }, + "node_modules/vite-svg-loader": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/vite-svg-loader/-/vite-svg-loader-5.1.0.tgz", + "integrity": "sha512-M/wqwtOEjgb956/+m5ZrYT/Iq6Hax0OakWbokj8+9PXOnB7b/4AxESHieEtnNEy7ZpjsjYW1/5nK8fATQMmRxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "svgo": "^3.0.2" + }, + "peerDependencies": { + "vue": ">=3.2.13" + } + }, "node_modules/vooks": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/vooks/-/vooks-0.2.12.tgz", diff --git a/package.json b/package.json index 3a83db0e..62227fb6 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "unplugin-auto-import": "^0.17.6", "unplugin-vue-components": "^0.27.1", "vite": "^5.3.1", + "vite-svg-loader": "^5.1.0", "vue-tsc": "^2.0.22" } } diff --git a/src/assets/svg/dynamoDB.svg b/src/assets/svg/dynamoDB.svg new file mode 100644 index 00000000..9ad4ae80 --- /dev/null +++ b/src/assets/svg/dynamoDB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/svg/elasticsearch.svg b/src/assets/svg/elasticsearch.svg index 6776cde8..1e0eea65 100644 --- a/src/assets/svg/elasticsearch.svg +++ b/src/assets/svg/elasticsearch.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/lang/enUS.ts b/src/lang/enUS.ts index 357e85eb..314226db 100644 --- a/src/lang/enUS.ts +++ b/src/lang/enUS.ts @@ -103,6 +103,8 @@ export const enUS = { targetIndex: 'Target Index', }, }, + emptyDynamodb: 'Still in development', + emptyNoConnection: 'Please establish connection first', }, connection: { new: 'New connection', @@ -115,14 +117,24 @@ export const enUS = { indexName: 'Index/Alias', queryParameters: 'query parameters', sslCertVerification: 'SSL Certificate Verification', - add: 'Add connection', - edit: 'Edit connection', - testSuccess: 'connect success', + connectSuccess: 'Connect success', + deleteSuccess: 'Delete success', + testSuccess: 'Test connect success', + saveSuccess: 'Save success', + edit: 'Edit configuration', + selectDatabase: 'Please select database type', + region: 'Region', + accessKeyId: 'Access Key ID', + secretAccessKey: 'Secret Access Key', + selectRegion: 'Please select region', formValidation: { nameRequired: 'Name is required', hostRequired: 'Host is required', portRequired: 'Port is required', sslCertOnlyHttps: 'SSL Certificate Verification can only be enabled under HTTPS connection', + regionRequired: 'Please select region', + accessKeyIdRequired: 'Please enter Access Key ID', + secretAccessKeyRequired: 'Please enter Secret Access Key', }, operations: { connect: 'Connect', @@ -133,6 +145,14 @@ export const enUS = { selectConnection: 'No Connection selected', validationFailed: 'Form validation failed!', unAuthorized: 'Authorization failed, ensure your username and password are correct', + emptyState: { + noConnections: 'No connections available', + pleaseSelect: 'Please select a connection' + }, + dynamodb: { + queryComingSoon: 'DynamoDB query interface coming soon...', + tablesComingSoon: 'DynamoDB tables management coming soon...' + } }, dialogOps: { warning: 'Warning', diff --git a/src/lang/zhCN.ts b/src/lang/zhCN.ts index dd65b20f..2991a555 100644 --- a/src/lang/zhCN.ts +++ b/src/lang/zhCN.ts @@ -103,6 +103,8 @@ export const zhCN = { targetIndex: '目标索引', }, }, + emptyNoConnection: '请先建立连接', + emptyDynamodb: '功能完善中', }, connection: { new: '新建连接', @@ -115,14 +117,24 @@ export const zhCN = { indexName: '索引/别名', queryParameters: '查询参数', sslCertVerification: 'SSL 证书验证', - add: '添加连接', - edit: '编辑连接', - testSuccess: '连接成功', + connectSuccess: '连接成功', + deleteSuccess: '删除成功', + testSuccess: '测试连接成功', + saveSuccess: '保存成功', + edit: '编辑配置', + selectDatabase: '请选择数据库类型', + region: '区域', + accessKeyId: 'Access Key ID', + secretAccessKey: 'Secret Access Key', + selectRegion: '请选择区域', formValidation: { nameRequired: '请输入连接名称', hostRequired: '请输入主机地址', portRequired: '请输入端口号', sslCertOnlyHttps: 'SSL 证书验证只能在 HTTPS 连接下开启', + regionRequired: '请选择区域', + accessKeyIdRequired: '请输入Access Key ID', + secretAccessKeyRequired: '请输入Secret Access Key', }, operations: { connect: '连接', @@ -133,6 +145,14 @@ export const zhCN = { selectConnection: '未选择连接', validationFailed: '表单验证失败!', unAuthorized: '认证失败,请输入正确的用户名和密码!', + emptyState: { + noConnections: '暂无可用连接', + pleaseSelect: '请选择一个连接' + }, + dynamodb: { + queryComingSoon: 'DynamoDB 查询功能开发中...', + tablesComingSoon: 'DynamoDB 表管理功能开发中...' + } }, dialogOps: { warning: '提示', @@ -177,7 +197,7 @@ export const zhCN = { backup: { backup: '备份', restore: '恢复', - restoreSourceDesc: '点击或拖动文件到此区域上传您的 JSON/CSV 文件', + restoreSourceDesc: '点击或拖动文件到此区域上传您的 JSON/CSV 文件', backupToFileSuccess: '成功备份到文件', restoreFromFileSuccess: '成功从文件恢复', backupForm: { diff --git a/src/main.ts b/src/main.ts index b7dbde43..56d3c8e2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,6 @@ import { createApp } from 'vue'; import App from './App.vue'; -import store from './store'; +import { createPinia } from 'pinia'; import { router } from './router'; import { lang } from './lang'; @@ -8,9 +8,10 @@ import './assets/styles/normalize.css'; import './assets/styles/theme.scss'; const app = createApp(App); +const pinia = createPinia(); +app.use(pinia); app.use(router); -app.use(store); app.use(lang); app.mount('#app'); diff --git a/src/store/backupRestoreStore.ts b/src/store/backupRestoreStore.ts index f1ecc4fb..68e16c3e 100644 --- a/src/store/backupRestoreStore.ts +++ b/src/store/backupRestoreStore.ts @@ -3,7 +3,7 @@ import { open } from '@tauri-apps/api/dialog'; import { defineStore } from 'pinia'; import { CustomError } from '../common'; import { get } from 'lodash'; -import { Connection } from './connectionStore.ts'; +import { Connection, DatabaseType } from './connectionStore.ts'; import { loadHttpClient, sourceFileApi } from '../datasources'; export type BackupInput = { @@ -80,7 +80,16 @@ export const useBackupRestoreStore = defineStore('backupRestoreStore', { } }, async backupToFile(input: BackupInput) { - const client = loadHttpClient(input.connection); + let client; + if (input.connection.type === DatabaseType.ELASTICSEARCH) { + client = loadHttpClient({ + host: input.connection.host, + port: input.connection.port, + sslCertVerification: input.connection.sslCertVerification, + }); + } else { + throw new CustomError(400, 'Unsupported connection type'); + } const filePath = `${input.backupFolder}/${input.backupFileName}.${input.backupFileType}`; let searchAfter: any[] | undefined = undefined; let hasMore = true; @@ -141,8 +150,17 @@ export const useBackupRestoreStore = defineStore('backupRestoreStore', { } }, async restoreFromFile(input: RestoreInput) { + let client; + if (input.connection.type === DatabaseType.ELASTICSEARCH) { + client = loadHttpClient({ + host: input.connection.host, + port: input.connection.port, + sslCertVerification: input.connection.sslCertVerification, + }); + } else { + throw new CustomError(400, 'Unsupported connection type'); + } const fileType = input.restoreFile.split('.').pop(); - const client = loadHttpClient(input.connection); const bulkSize = 1000; let data: string; try { diff --git a/src/store/clusterManageStore.ts b/src/store/clusterManageStore.ts index 72a708f3..9373414c 100644 --- a/src/store/clusterManageStore.ts +++ b/src/store/clusterManageStore.ts @@ -1,7 +1,7 @@ import { defineStore } from 'pinia'; import { loadHttpClient } from '../datasources'; import { lang } from '../lang'; -import { Connection, useConnectionStore } from './connectionStore.ts'; +import { DatabaseType, useConnectionStore } from './connectionStore.ts'; import { CustomError, optionalToNullableInt } from '../common'; export enum IndexHealth { @@ -290,13 +290,18 @@ export const useClusterManageStore = defineStore('clusterManageStore', { async fetchCluster() { const { established } = useConnectionStore(); if (!established) throw new Error(lang.global.t('connection.selectConnection')); - const client = loadHttpClient(established as Connection); - this.cluster = (await client.get('/_cluster/stats', 'format=json')) as RawClusterStats; + if (established.type === DatabaseType.ELASTICSEARCH) { + const client = loadHttpClient(established); + this.cluster = (await client.get('/_cluster/stats', 'format=json')) as RawClusterStats; + } else { + this.cluster = null; + } }, async fetchNodes() { const { established } = useConnectionStore(); if (!established) throw new Error(lang.global.t('connection.selectConnection')); - const client = loadHttpClient(established as Connection); + if (established.type === DatabaseType.ELASTICSEARCH) { + const client = loadHttpClient(established); try { const data = await client.get('/_cat/nodes', 'format=json'); this.nodes = data @@ -347,11 +352,14 @@ export const useClusterManageStore = defineStore('clusterManageStore', { ) .sort((a: SearchNode, b: SearchNode) => a.name.localeCompare(b.name)); } catch (err) {} + } + this.nodes = []; }, async fetchNodeState(nodeName: string) { const { established } = useConnectionStore(); if (!established) throw new Error(lang.global.t('connection.selectConnection')); - const client = loadHttpClient(established as Connection); + if (established.type === DatabaseType.ELASTICSEARCH) { + const client = loadHttpClient(established); const data = await client.get( `/_cat/nodes`, 'format=json&bytes=b&h=ip,id,name,heap.percent,heap.current,heap.max,ram.percent,ram.current,ram.max,node.role,master,cpu,load_1m,load_5m,load_15m,disk.used_percent,disk.used,disk.total,shard_stats.total_count,mappings.total_count&full_id=true', @@ -402,11 +410,15 @@ export const useClusterManageStore = defineStore('clusterManageStore', { }, })) .find(({ name }) => name === nodeName); + } else { + this.nodes = []; + } }, async fetchShards(includeHidden: boolean = false) { const { established } = useConnectionStore(); if (!established) throw new Error(lang.global.t('connection.selectConnection')); - const client = loadHttpClient(established as Connection); + if (established.type === DatabaseType.ELASTICSEARCH) { + const client = loadHttpClient(established); try { const data = ( await client.get( @@ -419,11 +431,14 @@ export const useClusterManageStore = defineStore('clusterManageStore', { ? data : data.filter((shard: Shard) => !shard.index.startsWith('.')); } catch (err) {} - }, + } + this.shards = []; + }, async getShardState(indexName: string) { const { established } = useConnectionStore(); if (!established) throw new Error(lang.global.t('connection.selectConnection')); - const client = loadHttpClient(established as Connection); + if (established.type === DatabaseType.ELASTICSEARCH) { + const client = loadHttpClient(established); try { const data = await client.get( `/_cat/shards/${indexName}`, @@ -600,10 +615,12 @@ export const useClusterManageStore = defineStore('clusterManageStore', { ); return { index: indexName, shards: result }; } catch (err) {} - }, + } + }, async fetchIndices() { const { established } = useConnectionStore(); if (!established) throw new Error(lang.global.t('connection.selectConnection')); + if (established.type === DatabaseType.ELASTICSEARCH) { const client = loadHttpClient(established); const data = (await client.get('/_cat/indices', 'format=json&s=index')) as Array<{ [key: string]: string; @@ -624,10 +641,13 @@ export const useClusterManageStore = defineStore('clusterManageStore', { deleted: parseInt(index['docs.deleted'], 10), }, })); - }, + } + this.indices = []; + }, async fetchAliases() { const { established } = useConnectionStore(); if (!established) throw new Error(lang.global.t('connection.selectConnection')); + if (established.type === DatabaseType.ELASTICSEARCH) { const client = loadHttpClient(established); const data = (await client.get('/_cat/aliases', 'format=json&s=alias')) as Array<{ [key: string]: string; @@ -642,10 +662,13 @@ export const useClusterManageStore = defineStore('clusterManageStore', { }, isWriteIndex: alias['is_write_index'] === 'true', })); - }, + } + this.aliases = []; + }, async fetchTemplates() { const { established } = useConnectionStore(); if (!established) throw new Error(lang.global.t('connection.selectConnection')); + if (established.type === DatabaseType.ELASTICSEARCH) { const client = loadHttpClient(established); const fetchIndexTemplates = async () => { const data = (await client.get('/_cat/templates', 'format=json')) as Array<{ @@ -683,7 +706,9 @@ export const useClusterManageStore = defineStore('clusterManageStore', { ]); this.templates = [...indexTemplates, ...componentTemplates]; - }, + } + this.templates = []; + }, async createIndex({ indexName, shards, @@ -703,6 +728,7 @@ export const useClusterManageStore = defineStore('clusterManageStore', { }) { const { established } = useConnectionStore(); if (!established) throw new Error(lang.global.t('connection.selectConnection')); + if (established.type === DatabaseType.ELASTICSEARCH) { const client = loadHttpClient(established); const queryParams = new URLSearchParams(); @@ -752,7 +778,9 @@ export const useClusterManageStore = defineStore('clusterManageStore', { err instanceof CustomError ? err.details : (err as Error).message, ); } - }, + } + return undefined; + }, async createAlias({ aliasName, indexName, @@ -776,6 +804,7 @@ export const useClusterManageStore = defineStore('clusterManageStore', { }) { const { established } = useConnectionStore(); if (!established) throw new Error(lang.global.t('connection.selectConnection')); + if (established.type === DatabaseType.ELASTICSEARCH) { const client = loadHttpClient(established); const queryParams = new URLSearchParams(); @@ -826,7 +855,9 @@ export const useClusterManageStore = defineStore('clusterManageStore', { } // refresh data Promise.all([this.fetchIndices(), this.fetchAliases()]).catch(); - }, + } + return undefined; + }, async createTemplate({ name, type, @@ -842,6 +873,7 @@ export const useClusterManageStore = defineStore('clusterManageStore', { }) { const { established } = useConnectionStore(); if (!established) throw new Error(lang.global.t('connection.selectConnection')); + if (established.type === DatabaseType.ELASTICSEARCH) { const client = loadHttpClient(established); const queryParams = new URLSearchParams(); [ @@ -874,12 +906,15 @@ export const useClusterManageStore = defineStore('clusterManageStore', { err instanceof CustomError ? err.details : (err as Error).message, ); } - }, + } + return undefined; + }, async deleteIndex(indexName: string) { // delete index const { established } = useConnectionStore(); if (!established) throw new Error(lang.global.t('connection.selectConnection')); + if (established.type === DatabaseType.ELASTICSEARCH) { const client = loadHttpClient(established); try { const response = await client.delete(`/${indexName}`); @@ -895,10 +930,13 @@ export const useClusterManageStore = defineStore('clusterManageStore', { err instanceof CustomError ? err.details : (err as Error).message, ); } - }, + } + return undefined; + }, async closeIndex(indexName: string) { const { established } = useConnectionStore(); if (!established) throw new Error(lang.global.t('connection.selectConnection')); + if (established.type === DatabaseType.ELASTICSEARCH) { const client = loadHttpClient(established); try { const response = await client.post(`/${indexName}/_close`); @@ -914,10 +952,13 @@ export const useClusterManageStore = defineStore('clusterManageStore', { err instanceof CustomError ? err.details : (err as Error).message, ); } - }, + } + return undefined; + }, async openIndex(indexName: string) { const { established } = useConnectionStore(); if (!established) throw new Error(lang.global.t('connection.selectConnection')); + if (established.type === DatabaseType.ELASTICSEARCH) { const client = loadHttpClient(established); try { const response = await client.post(`/${indexName}/_open`); @@ -933,10 +974,13 @@ export const useClusterManageStore = defineStore('clusterManageStore', { err instanceof CustomError ? err.details : (err as Error).message, ); } - }, + } + return undefined; + }, async removeAlias(indexName: string, aliasName: string) { const { established } = useConnectionStore(); if (!established) throw new Error(lang.global.t('connection.selectConnection')); + if (established.type === DatabaseType.ELASTICSEARCH) { const client = loadHttpClient(established); try { const response = await client.delete(`/${indexName}/_alias/${aliasName}`); @@ -952,10 +996,13 @@ export const useClusterManageStore = defineStore('clusterManageStore', { err instanceof CustomError ? err.details : (err as Error).message, ); } - }, + } + return undefined;; + }, async switchAlias(aliasName: string, sourceIndexName: string, targetIndexName: string) { const { established } = useConnectionStore(); if (!established) throw new Error(lang.global.t('connection.selectConnection')); + if (established.type === DatabaseType.ELASTICSEARCH) { const client = loadHttpClient(established); const payload = { actions: [ @@ -987,6 +1034,8 @@ export const useClusterManageStore = defineStore('clusterManageStore', { err instanceof CustomError ? err.details : (err as Error).message, ); } - }, + } + return undefined; +}, }, }); diff --git a/src/store/connectionStore.ts b/src/store/connectionStore.ts index 64577d88..d83ff805 100644 --- a/src/store/connectionStore.ts +++ b/src/store/connectionStore.ts @@ -1,11 +1,30 @@ import { defineStore } from 'pinia'; import { buildAuthHeader, buildURL, pureObject } from '../common'; -import { loadHttpClient, storeApi } from '../datasources'; import { SearchAction, transformToCurl } from '../common/monaco'; +import { loadHttpClient, storeApi } from '../datasources'; + +export enum DatabaseType { + ELASTICSEARCH = 'elasticsearch', + DYNAMODB = 'dynamodb' +} -export type Connection = { +export type BaseConnection = { id?: number; name: string; + type: DatabaseType; +} + +export type DynamoDBConnection = BaseConnection & { + type: DatabaseType.DYNAMODB; + region: string; + accessKeyId: string; + secretAccessKey: string; +} + +export type Connection = ElasticsearchConnection | DynamoDBConnection; + +export type ElasticsearchConnection = BaseConnection & { + type: DatabaseType.ELASTICSEARCH; host: string; port: number; username?: string; @@ -13,7 +32,8 @@ export type Connection = { password?: string; indexName?: string; queryParameters?: string; -}; +} + export type ConnectionIndex = { health: string; status: string; @@ -59,10 +79,12 @@ export const useConnectionStore = defineStore('connectionStore', { state: (): { connections: Connection[]; established: Established; + currentConnection: Connection | null; } => { return { connections: [], established: null, + currentConnection: null, }; }, getters: { @@ -78,56 +100,106 @@ export const useConnectionStore = defineStore('connectionStore', { }, actions: { async fetchConnections() { - this.connections = (await storeApi.get('connections', [])) as Connection[]; + try { + const connections = await storeApi.get('connections', []) as Connection[]; + this.connections = connections; + } catch (error) { + console.error('Error fetching connections:', error); + this.connections = []; + } }, - async testConnection(con: Connection) { + async testElasticsearchConnection(con: ElasticsearchConnection) { + if (con.type !== DatabaseType.ELASTICSEARCH) { + throw new Error('Unsupported connection type'); + } const client = loadHttpClient(con); return await client.get(con.indexName ?? undefined, 'format=json'); }, - async saveConnection(connection: Connection) { - const index = this.connections.findIndex(({ id }: Connection) => id === connection.id); - if (index >= 0) { - this.connections[index] = connection; - } else { - this.connections.push({ ...connection, id: this.connections.length + 1 }); + async saveConnection(connection: Connection): Promise<{ success: boolean; message: string }> { + try { + const newConnection = { + ...connection, + type: 'host' in connection ? DatabaseType.ELASTICSEARCH : DatabaseType.DYNAMODB, + id: connection.id || this.connections.length + 1 + } as Connection; + + if (connection.id) { + const index = this.connections.findIndex(c => c.id === connection.id); + if (index !== -1) { + this.connections[index] = newConnection; + if (this.established?.id === connection.id) { + this.established = null; + } + } + } else { + this.connections.push(newConnection); + this.established = null; + } + + await storeApi.set('connections', pureObject(this.connections)); + return { success: true, message: 'Connection saved successfully' }; + } catch (error) { + console.error('Error saving connection:', error); + return { success: false, message: error instanceof Error ? error.message : 'Unknown error' }; } - await storeApi.set('connections', pureObject(this.connections)); }, async removeConnection(connection: Connection) { - this.connections = this.connections.filter(({ id }: Connection) => id !== connection.id); - await storeApi.set('connections', pureObject(this.connections)); - }, - async establishConnection(connection: Connection) { try { - await this.testConnection(connection); - } catch (err) { - this.established = null; - throw err; + const isEstablishedConnection = this.established?.id === connection.id; + const updatedConnections = this.connections.filter(c => c.id !== connection.id); + this.connections = updatedConnections; + if (isEstablishedConnection) { + this.established = null; + } + try { + await storeApi.set('connections', pureObject(updatedConnections)); + } catch (error) { + console.warn('Failed to persist connections after removal:', error); + } + } catch (error) { + console.error('Error removing connection:', error); + throw error; } + }, + async establishConnection(connection: Connection) { + if (connection.type === DatabaseType.ELASTICSEARCH) { + try { + await this.testElasticsearchConnection(connection); + const client = loadHttpClient(connection); + let indices: ConnectionIndex[] = []; - const client = loadHttpClient(connection); - - try { - const data = (await client.get('/_cat/indices', 'format=json')) as Array<{ - [key: string]: string; - }>; - const indices = data.map((index: { [key: string]: string }) => ({ - ...index, - docs: { - count: parseInt(index['docs.count'], 10), - deleted: parseInt(index['docs.deleted'], 10), - }, - store: { size: index['store.size'] }, - })) as ConnectionIndex[]; - this.established = { ...connection, indices }; - } catch (err) { - this.established = { ...connection, indices: [] }; + try { + const data = (await client.get('/_cat/indices', 'format=json')) as Array<{ + [key: string]: string; + }>; + + indices = data.map((index) => ({ + ...index, + docs: { + count: parseInt(index['docs.count'], 10), + deleted: parseInt(index['docs.deleted'], 10), + }, + store: { size: index['store.size'] }, + })) as ConnectionIndex[]; + this.established = { ...connection, indices }; + } catch (err) { + this.established = { ...connection, indices: [] }; + } + } catch (err) { + console.warn('Failed to establish elasticsearch connection:', err); + this.established = { ...connection, indices: [] }; + } + } else if (connection.type === DatabaseType.DYNAMODB) { + this.established = {...connection, indices: [], activeIndex: undefined}; } }, async fetchIndices() { if (!this.established) throw new Error('no connection established'); - const client = loadHttpClient(this.established as Connection); + if (this.established.type !== DatabaseType.ELASTICSEARCH) { + throw new Error('Operation only supported for Elasticsearch connections'); + } + const client = loadHttpClient(this.established); const data = (await client.get('/_cat/indices', 'format=json')) as Array<{ [key: string]: string; }>; @@ -141,7 +213,7 @@ export const useConnectionStore = defineStore('connectionStore', { })) as ConnectionIndex[]; }, async selectIndex(indexName: string) { - const client = loadHttpClient(this.established as Connection); + const client = loadHttpClient(this.established as ElasticsearchConnection); // get the index mapping const mapping = await client.get(`/${indexName}/_mapping`, 'format=json'); @@ -167,6 +239,9 @@ export const useConnectionStore = defineStore('connectionStore', { qdsl?: string; }) { if (!this.established) throw new Error('no connection established'); + if (this.established.type !== DatabaseType.ELASTICSEARCH) { + throw new Error('Operation only supported for Elasticsearch connections'); + } const client = loadHttpClient(this.established); const queryParameters = queryParams ? `${queryParams}&format=json` : 'format=json'; // refresh the index mapping @@ -196,11 +271,15 @@ export const useConnectionStore = defineStore('connectionStore', { return dispatch[method](); }, queryToCurl({ method, path, index, qdsl, queryParams }: SearchAction) { + if (this.established?.type !== DatabaseType.ELASTICSEARCH) { + throw new Error('Operation only supported for Elasticsearch connections'); + } const { username, password, host, port, sslCertVerification } = this.established ?? { host: 'http://localhost', port: 9200, username: undefined, password: undefined, + sslCertVerification: false }; const params = queryParams ? `${queryParams}&format=json` : 'format=json'; const url = buildURL(host, port, buildPath(index, path, this.established), params); @@ -212,5 +291,27 @@ export const useConnectionStore = defineStore('connectionStore', { return transformToCurl({ method, headers, url, ssl: sslCertVerification, qdsl }); }, + async testDynamoDBConnection(connection: DynamoDBConnection) { + // test later, should send request to rust backend + console.log('test connect to ',connection.type) + return undefined; + + }, + validateConnection(connection: Connection): boolean { + if (connection.type === DatabaseType.ELASTICSEARCH) { + return !!( + connection.host && + connection.port && + typeof connection.sslCertVerification === 'boolean' + ); + } else if (connection.type === DatabaseType.DYNAMODB) { + return !!( + connection.region && + connection.accessKeyId && + connection.secretAccessKey + ); + } + return false; + } }, }); diff --git a/src/views/connect/components/connect-list.vue b/src/views/connect/components/connect-list.vue index 8a9e1197..88dcb09c 100644 --- a/src/views/connect/components/connect-list.vue +++ b/src/views/connect/components/connect-list.vue @@ -3,24 +3,30 @@ establishConnect(connection)" > - + - + + + + + + {{ connection.name }} + {{ getDatabaseTypeLabel(connection.type) }} - {{ con.name }} handleSelect(args, con)" + :options="getDropdownOptions(connection)" + placement="bottom-start" > - + @@ -33,10 +39,13 @@ \ No newline at end of file diff --git a/src/views/connect/index.vue b/src/views/connect/index.vue index f94c5ed4..204411db 100644 --- a/src/views/connect/index.vue +++ b/src/views/connect/index.vue @@ -1,67 +1,147 @@ - - - - - - - {{ $t('connection.new') }} + + + + - - - - - - - - - - - - {{ $t('editor.loadDefault') }} - - - - - + + + + + + + + + + + {{ established.name }} + + + + + + + + + + + + + + {{ $t('editor.loadDefault') }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ type.label }} + + + + + + + + - - + diff --git a/vite.config.ts b/vite.config.ts index 5f3bc9bf..970a6b1e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,11 +3,13 @@ import vue from '@vitejs/plugin-vue'; import AutoImport from 'unplugin-auto-import/vite'; import Components from 'unplugin-vue-components/vite'; import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'; +import svgLoader from 'vite-svg-loader'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [ vue(), + svgLoader(), AutoImport({ dts: './auto-import.d.ts', eslintrc: { @@ -58,4 +60,9 @@ export default defineConfig({ // produce sourcemaps for debug builds sourcemap: !!process.env.TAURI_DEBUG, }, + resolve: { + alias: { + '@': '/src' + } + } });