Showing preview only (262K chars total). Download the full file or copy to clipboard to get everything.
Repository: Bronya0/ES-King
Branch: wails
Commit: d53ffad3f0dc
Files: 44
Total size: 233.7 KB
Directory structure:
gitextract_q841ds1i/
├── .github/
│ └── workflows/
│ └── build.yaml
├── .gitignore
├── LICENSE
├── app/
│ ├── app.go
│ ├── backend/
│ │ ├── common/
│ │ │ └── vars.go
│ │ ├── config/
│ │ │ └── app.go
│ │ ├── service/
│ │ │ └── es.go
│ │ ├── system/
│ │ │ └── update.go
│ │ └── types/
│ │ └── resp.go
│ ├── build/
│ │ ├── README.md
│ │ ├── darwin/
│ │ │ ├── Info.dev.plist
│ │ │ └── Info.plist
│ │ └── windows/
│ │ ├── info.json
│ │ ├── installer/
│ │ │ ├── project.nsi
│ │ │ └── wails_tools.nsh
│ │ └── wails.exe.manifest
│ ├── dev.bat
│ ├── frontend/
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── App.vue
│ │ │ ├── assets/
│ │ │ │ └── fonts/
│ │ │ │ └── OFL.txt
│ │ │ ├── components/
│ │ │ │ ├── About.vue
│ │ │ │ ├── Aside.vue
│ │ │ │ ├── Conn.vue
│ │ │ │ ├── Core.vue
│ │ │ │ ├── Header.vue
│ │ │ │ ├── Health.vue
│ │ │ │ ├── Index.vue
│ │ │ │ ├── Nodes.vue
│ │ │ │ ├── Rest.vue
│ │ │ │ ├── Settings.vue
│ │ │ │ ├── Snapshot.vue
│ │ │ │ └── Task.vue
│ │ │ ├── main.js
│ │ │ ├── style.css
│ │ │ └── utils/
│ │ │ ├── common.js
│ │ │ └── eventBus.js
│ │ └── vite.config.js
│ ├── go.mod
│ ├── go.sum
│ ├── main.go
│ └── wails.json
├── readme-en.md
└── readme.md
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/build.yaml
================================================
name: Wails build
on:
release:
types: [ created ]
env:
NODE_OPTIONS: "--max-old-space-size=4096"
APP_NAME: 'ES-King'
APP_WORKING_DIRECTORY: 'app'
GO_VERSION: '1.24'
NODE_VERSION: "22.x"
jobs:
build-windows:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-latest] # amd64/x64
steps:
- name: Checkout
uses: actions/checkout@v2
with:
submodules: recursive
- name: Install 7-Zip
run: choco install 7zip
- name: GoLang
uses: actions/setup-go@v4
with:
check-latest: true
go-version: ${{ env.GO_VERSION }}
- name: NodeJS
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: Build & Compress
run: |
go install github.com/wailsapp/wails/v2/cmd/wails@latest
cd ${{ env.APP_WORKING_DIRECTORY }}
wails build -ldflags="-X 'app/backend/common.Version=${{ github.ref_name }}'" -webview2 download -o ${{ env.APP_NAME }}.exe
cd ..
copy readme.md app\build\bin\
copy LICENSE app\build\bin\
& "C:\Program Files\7-Zip\7z.exe" a -t7z "${{ env.APP_NAME }}-${{ github.ref_name }}-windows-x64.7z" ".\app\build\bin\*" -r
- name: Upload Release Asset
uses: softprops/action-gh-release@v1
with:
files: "*.7z"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build-macos:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest] # macos-13是amd64,macos-latest是m1芯片
steps:
- name: Checkout
uses: actions/checkout@v2
with:
submodules: recursive
- name: GoLang
uses: actions/setup-go@v4
with:
check-latest: true
go-version: ${{ env.GO_VERSION }}
- name: NodeJS
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: Build & Compress
shell: bash
run: |
go install github.com/wailsapp/wails/v2/cmd/wails@latest
cd ${{ env.APP_WORKING_DIRECTORY }}
wails build -ldflags "-X 'app/backend/common.Version=${{ github.ref_name }}'" -platform darwin/universal -o ${{ env.APP_NAME }}
chmod +x build/bin/*/Contents/MacOS/*
mkdir -p _build/ _dist/
cp -r ./build/bin/${{ env.APP_NAME }}.app _build/
brew install create-dmg
create-dmg \
--no-internet-enable \
--volname "${{ env.APP_NAME }}" \
--volicon "_build/${{ env.APP_NAME }}.app/Contents/Resources/iconfile.icns" \
--window-pos 400 400 \
--window-size 660 450 \
--icon "${{ env.APP_NAME }}.app" 180 180 \
--icon-size 100 \
--hide-extension "${{ env.APP_NAME }}.app" \
--app-drop-link 480 180 \
"_dist/${{ env.APP_NAME }}-${{ github.ref_name }}-macos.dmg" \
"_build/"
- name: Upload Release Assets
uses: softprops/action-gh-release@v1
with:
files: ${{ env.APP_WORKING_DIRECTORY }}/_dist/*
fail_on_unmatched_files: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build-linux:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-22.04]
steps:
- name: Checkout
uses: actions/checkout@v2
with:
submodules: recursive
- name: GoLang
uses: actions/setup-go@v4
with:
check-latest: true
go-version: ${{ env.GO_VERSION }}
- name: NodeJS
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: Build & Compress
run: |
ARCH=$(uname -m)
sudo apt-get update && sudo apt-get install libgtk-3-dev libwebkit2gtk-4.0-dev build-essential
go install github.com/wailsapp/wails/v2/cmd/wails@latest
cd ${{ env.APP_WORKING_DIRECTORY }}
wails build -ldflags="-X 'app/backend/common.Version=${{ github.ref_name }}'" -webview2 download -o ${{ env.APP_NAME }}
cd ..
mkdir _temp_dist
cp readme.md _temp_dist/
cp LICENSE _temp_dist/
cp -r ${{ env.APP_WORKING_DIRECTORY }}/build/bin/* _temp_dist/
chmod +x _temp_dist/*
cd _temp_dist/
tar -zcvf ${{ env.APP_NAME }}-${{ github.ref_name }}-ubuntu-$ARCH.tar.gz *
mv ${{ env.APP_NAME }}-${{ github.ref_name }}-ubuntu-$ARCH.tar.gz ..
- name: Upload Release Assets
uses: softprops/action-gh-release@v1
with:
files: "*.tar.gz"
fail_on_unmatched_files: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .gitignore
================================================
build/bin
.vscode
.idea
/app/frontend/node_modules
/app/frontend/wailsjs
/app/frontend/dist
/app/frontend/package.json.md5
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: app/app.go
================================================
/*
* Copyright 2025 Bronya0 <tangssst@163.com>.
* Author Github: https://github.com/Bronya0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package main
import (
"app/backend/common"
"context"
"crypto/tls"
"fmt"
"github.com/go-resty/resty/v2"
"log"
"runtime"
"runtime/debug"
)
// App struct
type App struct {
ctx context.Context
}
// NewApp creates a new App application struct
func NewApp() *App {
return &App{}
}
// Start is called at application startup
func (a *App) Start(ctx context.Context) {
a.ctx = ctx
log.Println("===注意,接下来前端执行onMounted,后端初始化必须在此处完成===")
}
// domReady is called after front-end resources have been loaded
func (a *App) domReady(ctx context.Context) {
log.Println("===最后一步,页面即将显示……可以执行后端异步任务===")
// 统计版本使用情况
go func() {
defer func() {
if err := recover(); err != nil {
fmt.Println(string(debug.Stack()))
}
}()
client := resty.New().SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true})
body := map[string]any{
"name": common.AppName,
"version": common.Version,
"platform": runtime.GOOS,
}
_, _ = client.R().SetBody(body).Post(common.PingUrl)
}()
}
// beforeClose is called when the application is about to quit,
// either by clicking the window close button or calling runtime.Quit.
// Returning true will cause the application to continue, false will continue shutdown as normal.
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
return false
}
// shutdown is called at application termination
func (a *App) shutdown(ctx context.Context) {
// Perform your teardown here
}
// Greet returns a greeting for the given name
func (a *App) Greet(name string) string {
return fmt.Sprintf("Hello %s, It's show time!", name)
}
================================================
FILE: app/backend/common/vars.go
================================================
/*
* Copyright 2025 Bronya0 <tangssst@163.com>.
* Author Github: https://github.com/Bronya0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package common
import "fmt"
var (
// Version 会在编译时注入 -ldflags="-X 'app/backend/common.Version=${{ github.event.release.tag_name }}'"
Version = ""
)
const (
AppName = "ES-King"
Width = 1600
Height = 870
Theme = "dark"
ConfigDir = ".es-king"
ConfigPath = "config.yaml"
HistoryPath = "history.yaml"
ErrLogPath = "error.log"
Language = "zh-CN"
PingUrl = "https://ysboke.cn/api/kingTool/ping"
)
var (
Project = "Bronya0/ES-King"
GITHUB_URL = fmt.Sprintf("https://github.com/%s", Project)
GITHUB_REPOS_URL = fmt.Sprintf("https://api.github.com/repos/%s", Project)
UPDATE_URL = fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", Project)
ISSUES_URL = fmt.Sprintf("https://github.com/%s/issues", Project)
ISSUES_API_URL = fmt.Sprintf("https://api.github.com/repos/%s/issues?state=open", Project)
)
================================================
FILE: app/backend/config/app.go
================================================
/*
* Copyright 2025 Bronya0 <tangssst@163.com>.
* Author Github: https://github.com/Bronya0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package config
import (
"app/backend/common"
"app/backend/types"
"context"
"fmt"
"github.com/wailsapp/wails/v2/pkg/runtime"
"gopkg.in/yaml.v3"
"log"
"os"
"path/filepath"
"sync"
)
type AppConfig struct {
ctx context.Context
mu sync.Mutex
}
func (a *AppConfig) Start(ctx context.Context) {
a.ctx = ctx
}
func (a *AppConfig) GetConfig() *types.Config {
var defaultConfig = &types.Config{
Width: common.Width,
Height: common.Height,
Language: common.Language,
Theme: common.Theme,
Connects: make([]types.Connect, 0),
}
configPath := a.getConfigPath()
data, err := os.ReadFile(configPath)
if err != nil {
return defaultConfig
}
err = yaml.Unmarshal(data, defaultConfig)
if err != nil {
return defaultConfig
}
return defaultConfig
}
func (a *AppConfig) SaveConfig(config *types.Config) string {
a.mu.Lock()
defer a.mu.Unlock()
configPath := a.getConfigPath()
fmt.Println(configPath)
data, err := yaml.Marshal(config)
if err != nil {
return err.Error()
}
err = os.WriteFile(configPath, data, 0644)
if err != nil {
return err.Error()
}
return ""
}
func (a *AppConfig) SaveTheme(theme string) string {
a.mu.Lock()
defer a.mu.Unlock()
config := a.GetConfig()
config.Theme = theme
data, err := yaml.Marshal(config)
if err != nil {
return err.Error()
}
configPath := a.getConfigPath()
err = os.WriteFile(configPath, data, 0644)
if err != nil {
return err.Error()
}
return ""
}
func (a *AppConfig) getConfigPath() string {
homeDir, err := os.UserHomeDir()
if err != nil {
log.Printf("os.UserHomeDir() error: %s", err.Error())
return common.ConfigPath
}
configDir := filepath.Join(homeDir, common.ConfigDir)
_, err = os.Stat(configDir)
if os.IsNotExist(err) {
err = os.Mkdir(configDir, os.ModePerm)
if err != nil {
log.Printf("create configDir %s error: %s", configDir, err.Error())
return common.ConfigPath
}
}
return filepath.Join(configDir, common.ConfigPath)
}
func (a *AppConfig) GetHistory() []*types.History {
historyPath := a.getHistoryPath()
data, err := os.ReadFile(historyPath)
history := make([]*types.History, 0, 200)
if err != nil {
return history
}
err = yaml.Unmarshal(data, &history)
if err != nil {
return history
}
return history
}
func (a *AppConfig) SaveHistory(histories []types.History) string {
a.mu.Lock()
defer a.mu.Unlock()
historyPath := a.getHistoryPath()
data, err := yaml.Marshal(histories)
if err != nil {
return err.Error()
}
err = os.WriteFile(historyPath, data, 0644)
if err != nil {
return err.Error()
}
return ""
}
func (a *AppConfig) getHistoryPath() string {
homeDir, err := os.UserHomeDir()
if err != nil {
log.Printf("os.UserHomeDir() error: %s", err.Error())
return common.HistoryPath
}
configDir := filepath.Join(homeDir, common.ConfigDir)
_, err = os.Stat(configDir)
if os.IsNotExist(err) {
err = os.Mkdir(configDir, os.ModePerm)
if err != nil {
log.Printf("create configDir %s error: %s", configDir, err.Error())
return common.HistoryPath
}
}
return filepath.Join(configDir, common.HistoryPath)
}
// GetVersion returns the application version
func (a *AppConfig) GetVersion() string {
return common.Version
}
func (a *AppConfig) GetAppName() string {
return common.AppName
}
func (a *AppConfig) OpenFileDialog(options runtime.OpenDialogOptions) (string, error) {
return runtime.OpenFileDialog(a.ctx, options)
}
func (a *AppConfig) LogErrToFile(message string) {
file, err := os.OpenFile(common.ErrLogPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Println("Failed to open log file:", err)
return
}
defer file.Close()
if _, err := file.WriteString(message); err != nil {
log.Println("Failed to write to log file:", err)
}
}
================================================
FILE: app/backend/service/es.go
================================================
/*
* Copyright 2025 Bronya0 <tangssst@163.com>.
* Author Github: https://github.com/Bronya0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package service
import (
"app/backend/types"
"bufio"
"crypto/tls"
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"os"
"strings"
"sync"
"time"
"github.com/go-resty/resty/v2"
)
const (
FORMAT = "?format=json&pretty"
StatsApi = "/_cluster/stats" + FORMAT
AddDoc = "/_doc"
HealthApi = "/_cluster/health"
NodesApi = "/_nodes/stats/indices,os,fs,process,jvm"
AllIndexApi = "/_cat/indices?format=json&pretty&bytes=b"
ClusterSettings = "/_cluster/settings"
ForceMerge = "/_forcemerge?wait_for_completion=false"
REFRESH = "/_refresh"
FLUSH = "/_flush"
CacheClear = "/_cache/clear"
TasksApi = "/_tasks" + FORMAT
CancelTasksApi = "/_tasks/%s/_cancel"
)
type ESService struct {
ConnectObj *types.Connect
Client *resty.Client
mu sync.RWMutex
}
func NewESService() *ESService {
client := resty.New()
client.SetTimeout(30 * time.Second)
client.SetRetryCount(0)
client.SetHeader("Content-Type", "application/json")
return &ESService{
Client: client,
ConnectObj: &types.Connect{},
}
}
func ConfigureSSL(UseSSL, SkipSSLVerify bool, client *resty.Client, CACert string) {
// Configure SSL
// CACert是证书内容
if UseSSL {
client.SetScheme("https")
if SkipSSLVerify {
client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true})
}
if CACert != "" {
client.SetRootCertificateFromString(CACert)
}
} else {
client.SetScheme("http")
}
}
func (es *ESService) SetConnect(key, host, username, password, CACert string, UseSSL, SkipSSLVerify bool) {
es.mu.Lock() // 加写锁
defer es.mu.Unlock() // 方法结束时解锁
es.ConnectObj = &types.Connect{
Name: key,
Host: host,
Username: username,
Password: password,
UseSSL: UseSSL,
SkipSSLVerify: SkipSSLVerify,
CACert: CACert,
}
if username != "" && password != "" {
es.Client.SetBasicAuth(username, password)
}
ConfigureSSL(UseSSL, SkipSSLVerify, es.Client, CACert)
fmt.Println("设置当前连接:", es.ConnectObj.Host)
}
func (es *ESService) TestClient(host, username, password, CACert string, UseSSL, SkipSSLVerify bool) string {
client := resty.New()
if username != "" && password != "" {
client.SetBasicAuth(username, password)
}
// Configure SSL
ConfigureSSL(UseSSL, SkipSSLVerify, client, CACert)
resp, err := client.R().Get(host + HealthApi)
if err != nil {
return err.Error()
}
if resp.StatusCode() != http.StatusOK {
return string(resp.Body())
}
return ""
}
// AddDocument 添加文档
func (es *ESService) AddDocument(index, doc string) *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
var result map[string]any
resp, err := es.Client.R().
SetBody(doc).
SetResult(&result).
Post(es.ConnectObj.Host + "/" + index + AddDoc)
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusCreated {
return &types.ResultResp{Err: string(resp.Body())}
}
return &types.ResultResp{Result: result}
}
func (es *ESService) GetNodes() *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
var result any
resp, err := es.Client.R().SetResult(&result).Get(es.ConnectObj.Host + NodesApi)
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultResp{Err: string(resp.Body())}
}
return &types.ResultResp{Result: result}
}
func (es *ESService) GetHealth() *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
var result map[string]any
resp, err := es.Client.R().SetResult(&result).Get(es.ConnectObj.Host + HealthApi)
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultResp{Err: string(resp.Body())}
}
return &types.ResultResp{Result: result}
}
func (es *ESService) GetStats() *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
var result map[string]any
resp, err := es.Client.R().SetResult(&result).Get(es.ConnectObj.Host + StatsApi)
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultResp{Err: string(resp.Body())}
}
return &types.ResultResp{Result: result}
}
func (es *ESService) GetIndexes(name string) *types.ResultsResp {
if es.ConnectObj.Host == "" {
return &types.ResultsResp{Err: "请先选择一个集群"}
}
newUrl := es.ConnectObj.Host + AllIndexApi
if name != "" {
newUrl += "&index=" + "*" + name + "*"
}
log.Println(newUrl)
var result []any
resp, err := es.Client.R().SetResult(&result).Get(newUrl)
if err != nil {
return &types.ResultsResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultsResp{Err: string(resp.Body())}
}
return &types.ResultsResp{Results: result}
}
func (es *ESService) CreateIndex(name string, numberOfShards, numberOfReplicas int, mapping string) *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
indexConfig := types.H{
"settings": types.H{
"number_of_shards": numberOfShards,
"number_of_replicas": numberOfReplicas,
},
}
if mapping != "" {
var mappings types.H
err := json.Unmarshal([]byte(mapping), &mappings)
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
indexConfig["mappings"] = mappings
}
resp, err := es.Client.R().
SetBody(indexConfig).
Put(es.ConnectObj.Host + "/" + name)
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultResp{Err: string(resp.Body())}
}
return &types.ResultResp{}
}
func (es *ESService) GetIndexInfo(indexName string) *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
var result map[string]any
resp, err := es.Client.R().SetResult(&result).Get(es.ConnectObj.Host + "/" + indexName)
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultResp{Err: string(resp.Body())}
}
return &types.ResultResp{Result: result}
}
func (es *ESService) DeleteIndex(indexName string) *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
var result map[string]any
resp, err := es.Client.R().SetResult(&result).Delete(es.ConnectObj.Host + "/" + indexName)
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultResp{Err: string(resp.Body())}
}
return &types.ResultResp{Result: result}
}
func (es *ESService) OpenCloseIndex(indexName, now string) *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
var result map[string]any
action, ok := map[string]string{
"open": "_close",
"close": "_open",
}[now]
if !ok {
return &types.ResultResp{Err: "无效的状态参数: " + now}
}
resp, err := es.Client.R().SetResult(&result).Post(es.ConnectObj.Host + "/" + indexName + "/" + action)
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultResp{Err: string(resp.Body())}
}
return &types.ResultResp{Result: result}
}
func (es *ESService) GetIndexMappings(indexName string) *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
var result map[string]any
resp, err := es.Client.R().SetResult(&result).Get(es.ConnectObj.Host + "/" + indexName)
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultResp{Err: string(resp.Body())}
}
return &types.ResultResp{Result: result}
}
func (es *ESService) MergeSegments(indexName string) *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
var result map[string]any
resp, err := es.Client.R().SetResult(&result).Post(es.ConnectObj.Host + "/" + indexName + ForceMerge)
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultResp{Err: string(resp.Body())}
}
return &types.ResultResp{Result: result}
}
func (es *ESService) Refresh(indexName string) *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
var result map[string]any
resp, err := es.Client.R().SetResult(&result).Post(es.ConnectObj.Host + "/" + indexName + REFRESH)
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultResp{Err: string(resp.Body())}
}
return &types.ResultResp{Result: result}
}
func (es *ESService) Flush(indexName string) *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
var result map[string]any
resp, err := es.Client.R().SetResult(&result).Post(es.ConnectObj.Host + "/" + indexName + FLUSH)
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultResp{Err: string(resp.Body())}
}
return &types.ResultResp{Result: result}
}
func (es *ESService) CacheClear(indexName string) *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
var result map[string]any
resp, err := es.Client.R().SetResult(&result).Post(es.ConnectObj.Host + "/" + indexName + CacheClear)
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultResp{Err: string(resp.Body())}
}
return &types.ResultResp{Result: result}
}
func (es *ESService) GetDoc10(indexName string) *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
body := map[string]any{
"query": map[string]any{
"query_string": map[string]any{
"query": "*",
},
},
"size": 10,
"from": 0,
"sort": []any{},
}
var result map[string]any
resp, err := es.Client.R().
SetBody(body).
SetResult(&result).
Post(es.ConnectObj.Host + "/" + indexName + "/_search")
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultResp{Err: string(resp.Body())}
}
return &types.ResultResp{Result: result}
}
func (es *ESService) Search(method, path string, body any) *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
var result any
req := es.Client.R().SetResult(&result)
if body != nil {
req = req.SetBody(body)
}
resp, err := req.Execute(method, es.ConnectObj.Host+path)
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultResp{Err: string(resp.Body())}
}
return &types.ResultResp{Result: result}
}
func (es *ESService) GetClusterSettings() *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
var result map[string]any
resp, err := es.Client.R().SetResult(&result).Get(es.ConnectObj.Host + ClusterSettings)
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultResp{Err: string(resp.Body())}
}
return &types.ResultResp{Result: result}
}
func (es *ESService) GetIndexSettings(indexName string) *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
var result map[string]any
resp, err := es.Client.R().SetResult(&result).Get(es.ConnectObj.Host + "/" + indexName)
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultResp{Err: string(resp.Body())}
}
return &types.ResultResp{Result: result}
}
func (es *ESService) GetIndexAliases(indexNameList []string) *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
var result map[string]any
indexNames := strings.Join(indexNameList, ",")
resp, err := es.Client.R().SetResult(&result).Get(es.ConnectObj.Host + "/" + indexNames + "/_alias")
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultResp{Err: string(resp.Body())}
}
alias := make(map[string]any)
for name, obj := range result {
if aliases, ok := obj.(map[string]any)["aliases"]; ok {
names := make([]string, 0)
aliases, ok := aliases.(map[string]any)
if !ok {
continue
}
for aliasName := range aliases {
names = append(names, aliasName)
}
if len(names) > 0 {
alias[name] = strings.Join(names, ",")
}
}
}
return &types.ResultResp{Result: alias}
}
func (es *ESService) GetIndexSegments(indexName string) *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
var result map[string]any
resp, err := es.Client.R().SetResult(&result).Get(es.ConnectObj.Host + "/" + indexName)
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultResp{Err: string(resp.Body())}
}
return &types.ResultResp{Result: result}
}
func (es *ESService) GetTasks() *types.ResultsResp {
if es.ConnectObj.Host == "" {
return &types.ResultsResp{Err: "请先选择一个集群"}
}
var result map[string]any
resp, err := es.Client.R().SetResult(&result).Get(es.ConnectObj.Host + TasksApi)
if err != nil {
return &types.ResultsResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultsResp{Err: string(resp.Body())}
}
nodes, ok := result["nodes"].(map[string]any)
if !ok {
return &types.ResultsResp{Err: "获取任务列表失败"}
}
var data []any
for _, nodeObj := range nodes {
nodeTasks, ok := nodeObj.(map[string]any)["tasks"].(map[string]any)
if !ok {
continue
}
for taskID, taskInfo := range nodeTasks {
taskInfoMap, ok := taskInfo.(map[string]any)
if !ok {
continue
}
nodeName, ok := nodeObj.(map[string]any)
if !ok {
continue
}
nodeIp, ok := nodeObj.(map[string]any)
if !ok {
continue
}
data = append(data, map[string]any{
"task_id": taskID,
"node_name": nodeName["name"],
"node_ip": nodeIp["ip"],
"type": taskInfoMap["type"],
"action": taskInfoMap["action"],
"start_time_in_millis": taskInfoMap["start_time_in_millis"],
"running_time_in_nanos": taskInfoMap["running_time_in_nanos"],
"cancellable": taskInfoMap["cancellable"],
"parent_task_id": taskInfoMap["parent_task_id"],
})
}
}
return &types.ResultsResp{Results: data}
}
func (es *ESService) CancelTasks(taskID string) *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
newUrl := fmt.Sprintf(es.ConnectObj.Host+CancelTasksApi, url.PathEscape(taskID))
var result map[string]any
resp, err := es.Client.R().SetResult(&result).Post(newUrl)
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultResp{Err: string(resp.Body())}
}
return &types.ResultResp{Result: result}
}
// GetSnapshots 获取ES快照列表
func (es *ESService) GetSnapshots() *types.ResultsResp {
if es.ConnectObj.Host == "" {
return &types.ResultsResp{Err: "请先选择一个集群"}
}
// 1. 首先获取所有仓库列表
var repositories map[string]interface{} // 修改目标类型
reposResp, err := es.Client.R().Get(es.ConnectObj.Host + "/_snapshot")
if err != nil {
return &types.ResultsResp{Err: err.Error()}
}
if reposResp.StatusCode() != http.StatusOK {
return &types.ResultsResp{Err: string(reposResp.Body())}
}
err = json.Unmarshal(reposResp.Body(), &repositories) // 直接解析到 map
if err != nil {
return &types.ResultsResp{Err: err.Error()}
}
// 2. 并发遍历每个仓库获取其快照
var (
mu sync.Mutex
allSnapshots []any
wg sync.WaitGroup
)
for repoName := range repositories {
wg.Add(1)
go func(repo string) {
defer wg.Done()
var repoResult map[string]interface{}
resp, err := es.Client.R().SetResult(&repoResult).Get(es.ConnectObj.Host + "/_snapshot/" + repo + "/_all")
if err != nil || resp.StatusCode() != http.StatusOK {
return
}
// 3. 处理每个快照的数据(带安全类型断言)
snapshots, ok := repoResult["snapshots"].([]interface{})
if !ok {
return
}
var items []any
for _, snap := range snapshots {
snapshot, ok := snap.(map[string]interface{})
if !ok {
continue
}
state, _ := snapshot["state"].(string)
items = append(items, map[string]interface{}{
"snapshot": snapshot["snapshot"],
"repository": repo,
"state": strings.ToUpper(state),
"start_time": snapshot["start_time"],
"end_time": snapshot["end_time"],
"indices": snapshot["indices"],
"total_shards": snapshot["shards_total"],
"successful_shards": snapshot["shards_successful"],
})
}
mu.Lock()
allSnapshots = append(allSnapshots, items...)
mu.Unlock()
}(repoName)
}
wg.Wait()
return &types.ResultsResp{Results: allSnapshots}
}
// GetSnapshotRepositories 获取所有快照仓库
func (es *ESService) GetSnapshotRepositories() *types.ResultsResp {
if es.ConnectObj.Host == "" {
return &types.ResultsResp{Err: "请先选择一个集群"}
}
var repositories map[string]any
resp, err := es.Client.R().Get(es.ConnectObj.Host + "/_snapshot")
if err != nil {
return &types.ResultsResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultsResp{Err: string(resp.Body())}
}
err = json.Unmarshal(resp.Body(), &repositories)
if err != nil {
return &types.ResultsResp{Err: err.Error()}
}
var data []any
for name, info := range repositories {
repoInfo, ok := info.(map[string]any)
if !ok {
continue
}
item := map[string]any{
"name": name,
"type": repoInfo["type"],
"settings": repoInfo["settings"],
}
data = append(data, item)
}
return &types.ResultsResp{Results: data}
}
// CreateSnapshotRepository 创建快照仓库
func (es *ESService) CreateSnapshotRepository(name, repoType, settings string) *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
if name == "" || repoType == "" {
return &types.ResultResp{Err: "仓库名称和类型不能为空"}
}
var settingsMap map[string]any
if settings != "" {
if err := json.Unmarshal([]byte(settings), &settingsMap); err != nil {
return &types.ResultResp{Err: "settings JSON 格式无效: " + err.Error()}
}
}
body := map[string]any{
"type": repoType,
"settings": settingsMap,
}
var result map[string]any
resp, err := es.Client.R().
SetBody(body).
SetResult(&result).
Put(es.ConnectObj.Host + "/_snapshot/" + url.PathEscape(name))
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultResp{Err: string(resp.Body())}
}
return &types.ResultResp{Result: result}
}
// DeleteSnapshotRepository 删除快照仓库
func (es *ESService) DeleteSnapshotRepository(name string) *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
if name == "" {
return &types.ResultResp{Err: "仓库名称不能为空"}
}
var result map[string]any
resp, err := es.Client.R().
SetResult(&result).
Delete(es.ConnectObj.Host + "/_snapshot/" + url.PathEscape(name))
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultResp{Err: string(resp.Body())}
}
return &types.ResultResp{Result: result}
}
// VerifySnapshotRepository 验证快照仓库
func (es *ESService) VerifySnapshotRepository(name string) *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
if name == "" {
return &types.ResultResp{Err: "仓库名称不能为空"}
}
var result map[string]any
resp, err := es.Client.R().
SetResult(&result).
Post(es.ConnectObj.Host + "/_snapshot/" + url.PathEscape(name) + "/_verify")
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultResp{Err: string(resp.Body())}
}
return &types.ResultResp{Result: result}
}
// CreateSnapshot 创建快照(异步)
func (es *ESService) CreateSnapshot(repository, snapshot, indices string, includeGlobalState bool) *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
if repository == "" || snapshot == "" {
return &types.ResultResp{Err: "仓库名称和快照名称不能为空"}
}
body := map[string]any{
"include_global_state": includeGlobalState,
}
if indices != "" {
body["indices"] = indices
}
var result map[string]any
resp, err := es.Client.R().
SetBody(body).
SetResult(&result).
Put(es.ConnectObj.Host + "/_snapshot/" + url.PathEscape(repository) + "/" + url.PathEscape(snapshot) + "?wait_for_completion=false")
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK && resp.StatusCode() != http.StatusAccepted {
return &types.ResultResp{Err: string(resp.Body())}
}
return &types.ResultResp{Result: result}
}
// DeleteSnapshot 删除快照
func (es *ESService) DeleteSnapshot(repository, snapshot string) *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
if repository == "" || snapshot == "" {
return &types.ResultResp{Err: "仓库名称和快照名称不能为空"}
}
var result map[string]any
resp, err := es.Client.R().
SetResult(&result).
Delete(es.ConnectObj.Host + "/_snapshot/" + url.PathEscape(repository) + "/" + url.PathEscape(snapshot))
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultResp{Err: string(resp.Body())}
}
return &types.ResultResp{Result: result}
}
// GetSnapshotDetail 获取快照详情
func (es *ESService) GetSnapshotDetail(repository, snapshot string) *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
if repository == "" || snapshot == "" {
return &types.ResultResp{Err: "仓库名称和快照名称不能为空"}
}
var result map[string]any
resp, err := es.Client.R().
SetResult(&result).
Get(es.ConnectObj.Host + "/_snapshot/" + url.PathEscape(repository) + "/" + url.PathEscape(snapshot))
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultResp{Err: string(resp.Body())}
}
return &types.ResultResp{Result: result}
}
// RestoreSnapshot 恢复快照(异步)
func (es *ESService) RestoreSnapshot(repository, snapshot, indices, renamePattern, renameReplacement string, includeGlobalState bool) *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
if repository == "" || snapshot == "" {
return &types.ResultResp{Err: "仓库名称和快照名称不能为空"}
}
body := map[string]any{
"include_global_state": includeGlobalState,
}
if indices != "" {
body["indices"] = indices
}
if renamePattern != "" {
body["rename_pattern"] = renamePattern
}
if renameReplacement != "" {
body["rename_replacement"] = renameReplacement
}
var result map[string]any
resp, err := es.Client.R().
SetBody(body).
SetResult(&result).
Post(es.ConnectObj.Host + "/_snapshot/" + url.PathEscape(repository) + "/" + url.PathEscape(snapshot) + "/_restore?wait_for_completion=false")
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK && resp.StatusCode() != http.StatusAccepted {
return &types.ResultResp{Err: string(resp.Body())}
}
return &types.ResultResp{Result: result}
}
// GetSnapshotRestoreStatus 获取快照恢复状态
func (es *ESService) GetSnapshotRestoreStatus() *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
var result map[string]any
resp, err := es.Client.R().
SetResult(&result).
Get(es.ConnectObj.Host + "/_recovery?active_only=true&format=json")
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultResp{Err: string(resp.Body())}
}
return &types.ResultResp{Result: result}
}
// GetSLMPolicies 获取所有SLM策略
func (es *ESService) GetSLMPolicies() *types.ResultsResp {
if es.ConnectObj.Host == "" {
return &types.ResultsResp{Err: "请先选择一个集群"}
}
var result []any
resp, err := es.Client.R().Get(es.ConnectObj.Host + "/_slm/policy")
if err != nil {
return &types.ResultsResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultsResp{Err: string(resp.Body())}
}
// SLM API 返回的是一个对象 {policyId: {...}, ...}
var policiesMap map[string]any
if err := json.Unmarshal(resp.Body(), &policiesMap); err != nil {
return &types.ResultsResp{Err: err.Error()}
}
for name, info := range policiesMap {
policyInfo, ok := info.(map[string]any)
if !ok {
continue
}
policyInfo["name"] = name
result = append(result, policyInfo)
}
return &types.ResultsResp{Results: result}
}
// CreateSLMPolicy 创建SLM策略
func (es *ESService) CreateSLMPolicy(policyId, name, schedule, repository, indices, expireAfter string, minCount, maxCount int) *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
if policyId == "" || schedule == "" || repository == "" {
return &types.ResultResp{Err: "策略ID、调度计划和仓库名称不能为空"}
}
body := map[string]any{
"schedule": schedule,
"name": name,
"repository": repository,
"config": map[string]any{
"indices": []string{"*"},
"include_global_state": false,
},
}
if indices != "" {
body["config"].(map[string]any)["indices"] = strings.Split(indices, ",")
}
retention := map[string]any{}
if expireAfter != "" {
retention["expire_after"] = expireAfter
}
if minCount > 0 {
retention["min_count"] = minCount
}
if maxCount > 0 {
retention["max_count"] = maxCount
}
if len(retention) > 0 {
body["retention"] = retention
}
var result map[string]any
resp, err := es.Client.R().
SetBody(body).
SetResult(&result).
Put(es.ConnectObj.Host + "/_slm/policy/" + url.PathEscape(policyId))
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultResp{Err: string(resp.Body())}
}
return &types.ResultResp{Result: result}
}
// DeleteSLMPolicy 删除SLM策略
func (es *ESService) DeleteSLMPolicy(policyId string) *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
if policyId == "" {
return &types.ResultResp{Err: "策略ID不能为空"}
}
var result map[string]any
resp, err := es.Client.R().
SetResult(&result).
Delete(es.ConnectObj.Host + "/_slm/policy/" + url.PathEscape(policyId))
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultResp{Err: string(resp.Body())}
}
return &types.ResultResp{Result: result}
}
// ExecuteSLMPolicy 手动执行SLM策略
func (es *ESService) ExecuteSLMPolicy(policyId string) *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
if policyId == "" {
return &types.ResultResp{Err: "策略ID不能为空"}
}
var result map[string]any
resp, err := es.Client.R().
SetResult(&result).
Post(es.ConnectObj.Host + "/_slm/policy/" + url.PathEscape(policyId) + "/_execute")
if err != nil {
return &types.ResultResp{Err: err.Error()}
}
if resp.StatusCode() != http.StatusOK {
return &types.ResultResp{Err: string(resp.Body())}
}
return &types.ResultResp{Result: result}
}
// SearchResponse 定义 ES 搜索响应的结构
type SearchResponse struct {
ScrollID string `json:"_scroll_id"`
Hits struct {
Total struct {
Value int `json:"value"`
} `json:"total"`
Hits []struct {
Source json.RawMessage `json:"_source"`
} `json:"hits"`
} `json:"hits"`
}
// DownloadESIndex 使用 Resty 客户端从 ES 下载指定索引的数据
func (es *ESService) DownloadESIndex(index string, queryDSL string, filePath string) *types.ResultResp {
if es.ConnectObj.Host == "" {
return &types.ResultResp{Err: "请先选择一个集群"}
}
res := &types.ResultResp{}
// 如果 queryDSL 为空,默认使用 match_all 查询
if queryDSL == "" {
queryDSL = `{"match_all": {}}`
}
// 创建本地文件
file, err := os.Create(filePath)
if err != nil {
res.Err = fmt.Sprintf("创建文件失败: %v", err)
return res
}
success := false
defer func() {
file.Close()
if !success {
os.Remove(filePath) // 下载失败时清理残缺文件
}
}()
// 构造初始搜索请求的 body,设置每批次大小为 10000
bodyStr := fmt.Sprintf(`{"size": 10000, "query": %s}`, queryDSL)
resp, err := es.Client.R().SetBody(bodyStr).Post(es.ConnectObj.Host + "/" + index + "/_search?scroll=3m")
if err != nil {
res.Err = fmt.Sprintf("初始搜索请求失败: %v", err)
return res
}
if resp.StatusCode() != 200 {
res.Err = "初始搜索请求返回非 200 状态码"
return res
}
// 解析初始响应
var searchResponse SearchResponse
err = json.Unmarshal(resp.Body(), &searchResponse)
if err != nil {
res.Err = fmt.Sprintf("解析初始响应失败: %v", err)
return res
}
// 使用 bufio.Writer 进行缓冲写入
writer := bufio.NewWriter(file)
defer writer.Flush() // 确保缓冲区数据在函数结束时写入文件
// 写入 JSON 数组的开头
_, _ = writer.WriteString("[")
// 标志变量,用于控制逗号分隔符
isFirst := true
// 循环处理滚动下载
for {
// 如果当前批次没有文档,则退出循环
if len(searchResponse.Hits.Hits) == 0 {
break
}
// 遍历当前批次的每个文档
for _, hit := range searchResponse.Hits.Hits {
if !isFirst {
// 除了第一个文档前,其他文档前添加逗号
_, _ = writer.WriteString(",")
}
isFirst = false
// 直接写入文档的 _source 字段(json.RawMessage 是 []byte 类型)
_, _ = writer.Write(hit.Source)
}
// 发送滚动请求获取下一批数据
scrollBody := map[string]interface{}{
"scroll": "3m", // 滚动上下文有效期 1 分钟
"scroll_id": searchResponse.ScrollID,
}
resp, err = es.Client.R().SetBody(scrollBody).Post(es.ConnectObj.Host + "/_search/scroll")
if err != nil {
res.Err = fmt.Sprintf("滚动请求失败: %v", err)
return res
}
if resp.StatusCode() != 200 {
res.Err = fmt.Sprintf("滚动请求返回非 200 状态码 %v", resp.StatusCode())
return res
}
// 解析滚动响应
err = json.Unmarshal(resp.Body(), &searchResponse)
if err != nil {
res.Err = fmt.Sprintf("解析滚动响应失败: %v", err)
return res
}
}
// 写入 JSON 数组的结尾
_, _ = writer.WriteString("]")
success = true
return res
}
================================================
FILE: app/backend/system/update.go
================================================
/*
* Copyright 2025 Bronya0 <tangssst@163.com>.
* Author Github: https://github.com/Bronya0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package system
import (
"app/backend/common"
"app/backend/types"
"context"
"fmt"
"github.com/go-resty/resty/v2"
"runtime"
"runtime/debug"
"strings"
"time"
)
type Update struct {
ctx context.Context
}
func (obj *Update) Start(ctx context.Context) {
obj.ctx = ctx
}
func (obj *Update) CheckUpdate() *types.Tag {
client := resty.New()
tag := &types.Tag{}
resp, err := client.R().SetResult(tag).Get(common.UPDATE_URL)
if err != nil || resp.StatusCode() != 200 {
return nil
}
tag.TagName = strings.TrimSpace(tag.TagName)
return tag
}
func (obj *Update) GetProcessInfo() string {
// 获取内存统计信息
var memStats runtime.MemStats
runtime.ReadMemStats(&memStats)
// 获取构建信息
var goVersion string
if buildInfo, ok := debug.ReadBuildInfo(); ok {
goVersion = buildInfo.GoVersion
} else {
goVersion = runtime.Version()
}
// 格式化输出详细信息
info := fmt.Sprintf(
"基本信息:\n"+
"- Go版本: %s\n"+
"- 操作系统: %s\n"+
"- 体系结构: %s\n"+
"- CPU数量: %d\n"+
"- 协程数量: %d\n"+
"- 当前时间戳: %s\n\n"+
"内存统计:\n"+
"- 已分配内存: %.2f MB\n"+
"- 总分配内存: %.2f MB\n"+
"- 系统内存: %.2f MB\n"+
"- 堆分配: %.2f MB\n"+
"- 堆系统内存: %.2f MB\n"+
"- 堆空闲: %.2f MB\n"+
"- 堆使用中: %.2f MB\n"+
"- 栈使用中: %.2f MB\n"+
"- 堆对象数量: %d\n"+
"- 内存分配次数: %d\n"+
"- 内存释放次数: %d\n\n"+
"垃圾回收统计:\n"+
"- 垃圾回收运行次数: %d\n"+
"- 上次垃圾回收时间: %s\n"+
"- 下次垃圾回收限制: %.2f MB\n"+
"- 垃圾回收CPU占比: %.4f%%\n"+
"- 垃圾回收总暂停时间: %v\n",
goVersion, // Go版本
runtime.GOOS, // 操作系统
runtime.GOARCH, // 体系结构
runtime.NumCPU(), // CPU数量
runtime.NumGoroutine(), // 协程数量
time.Now().Format(time.DateTime), // 当前时间戳
float64(memStats.Alloc)/1024/1024, // 已分配内存
float64(memStats.TotalAlloc)/1024/1024, // 总分配内存
float64(memStats.Sys)/1024/1024, // 系统内存
float64(memStats.HeapAlloc)/1024/1024, // 堆分配
float64(memStats.HeapSys)/1024/1024, // 堆系统内存
float64(memStats.HeapIdle)/1024/1024, // 堆空闲
float64(memStats.HeapInuse)/1024/1024, // 堆使用中
float64(memStats.StackInuse)/1024/1024, // 栈使用中
memStats.HeapObjects, // 堆对象数量
memStats.Mallocs, // 内存分配次数
memStats.Frees, // 内存释放次数
memStats.NumGC, // 垃圾回收运行次数
time.Unix(0, int64(memStats.LastGC)).Format(time.DateTime), // 上次垃圾回收时间
float64(memStats.NextGC)/1024/1024, // 下次垃圾回收限制
memStats.GCCPUFraction*100, // 垃圾回收CPU占比
time.Duration(memStats.PauseTotalNs), // 垃圾回收总暂停时间
)
return info
}
================================================
FILE: app/backend/types/resp.go
================================================
/*
* Copyright 2025 Bronya0 <tangssst@163.com>.
* Author Github: https://github.com/Bronya0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package types
type Tag struct {
Name string `json:"name"`
TagName string `json:"tag_name"`
Body string `json:"body"`
}
type Config struct {
Width int `json:"width"`
Height int `json:"height"`
Language string `json:"language"`
Theme string `json:"theme"`
Connects []Connect `json:"connects"`
}
type History struct {
Time int `json:"timestamp"`
Method string `json:"method"`
Path string `json:"path"`
DSL string `json:"dsl"`
}
type ResultsResp struct {
Results []any `json:"results"`
Err string `json:"err"`
}
type ResultResp struct {
Result any `json:"result"`
Err string `json:"err"`
}
type Connect struct {
Id int `json:"id"`
Name string `json:"name"`
Host string `json:"host"`
Username string `json:"username"`
Password string `json:"password"`
UseSSL bool `json:"useSSL"`
SkipSSLVerify bool `json:"skipSSLVerify"`
CACert string `json:"caCert"`
}
type H map[string]any
================================================
FILE: app/build/README.md
================================================
# Build Directory
The build directory is used to house all the build files and assets for your application.
The structure is:
* bin - Output directory
* darwin - macOS specific files
* windows - Windows specific files
## Mac
The `darwin` directory holds files specific to Mac builds.
These may be customised and used as part of the build. To return these files to the default state, simply delete them
and
build with `wails build`.
The directory contains the following files:
- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`.
- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`.
## Windows
The `windows` directory contains the manifest and rc files used when building with `wails build`.
These may be customised for your application. To return these files to the default state, simply delete them and
build with `wails build`.
- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to
use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file
will be created using the `appicon.png` file in the build directory.
- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`.
- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer,
as well as the application itself (right click the exe -> properties -> details)
- `wails.exe.manifest` - The main application manifest file.
================================================
FILE: app/build/darwin/Info.dev.plist
================================================
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>{{.Info.ProductName}}</string>
<key>CFBundleExecutable</key>
<string>{{.Name}}</string>
<key>CFBundleIdentifier</key>
<string>com.wails.{{.Name}}</string>
<key>CFBundleVersion</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleGetInfoString</key>
<string>{{.Info.Comments}}</string>
<key>CFBundleShortVersionString</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleIconFile</key>
<string>iconfile</string>
<key>LSMinimumSystemVersion</key>
<string>10.13.0</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>{{.Info.Copyright}}</string>
{{if .Info.FileAssociations}}
<key>CFBundleDocumentTypes</key>
<array>
{{range .Info.FileAssociations}}
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>{{.Ext}}</string>
</array>
<key>CFBundleTypeName</key>
<string>{{.Name}}</string>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
<key>CFBundleTypeIconFile</key>
<string>{{.IconName}}</string>
</dict>
{{end}}
</array>
{{end}}
{{if .Info.Protocols}}
<key>CFBundleURLTypes</key>
<array>
{{range .Info.Protocols}}
<dict>
<key>CFBundleURLName</key>
<string>com.wails.{{.Scheme}}</string>
<key>CFBundleURLSchemes</key>
<array>
<string>{{.Scheme}}</string>
</array>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
</dict>
{{end}}
</array>
{{end}}
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
</dict>
</plist>
================================================
FILE: app/build/darwin/Info.plist
================================================
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>{{.Info.ProductName}}</string>
<key>CFBundleExecutable</key>
<string>{{.Name}}</string>
<key>CFBundleIdentifier</key>
<string>com.github.Bronya0.{{.Name}}</string>
<key>CFBundleVersion</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleGetInfoString</key>
<string>{{.Info.Comments}}</string>
<key>CFBundleShortVersionString</key>
<string>{{.Info.ProductVersion}}</string>
<key>CFBundleIconFile</key>
<string>iconfile</string>
<key>LSMinimumSystemVersion</key>
<string>10.13.0</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>{{.Info.Copyright}}</string>
{{if .Info.FileAssociations}}
<key>CFBundleDocumentTypes</key>
<array>
{{range .Info.FileAssociations}}
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>{{.Ext}}</string>
</array>
<key>CFBundleTypeName</key>
<string>{{.Name}}</string>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
<key>CFBundleTypeIconFile</key>
<string>{{.IconName}}</string>
</dict>
{{end}}
</array>
{{end}}
{{if .Info.Protocols}}
<key>CFBundleURLTypes</key>
<array>
{{range .Info.Protocols}}
<dict>
<key>CFBundleURLName</key>
<string>com.wails.{{.Scheme}}</string>
<key>CFBundleURLSchemes</key>
<array>
<string>{{.Scheme}}</string>
</array>
<key>CFBundleTypeRole</key>
<string>{{.Role}}</string>
</dict>
{{end}}
</array>
{{end}}
</dict>
</plist>
================================================
FILE: app/build/windows/info.json
================================================
{
"fixed": {
"file_version": "{{.Info.ProductVersion}}"
},
"info": {
"0000": {
"ProductVersion": "{{.Info.ProductVersion}}",
"CompanyName": "{{.Info.CompanyName}}",
"FileDescription": "{{.Info.ProductName}}",
"LegalCopyright": "{{.Info.Copyright}}",
"ProductName": "{{.Info.ProductName}}",
"Comments": "{{.Info.Comments}}"
}
}
}
================================================
FILE: app/build/windows/installer/project.nsi
================================================
Unicode true
####
## Please note: Template replacements don't work in this file. They are provided with default defines like
## mentioned underneath.
## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo.
## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually
## from outside of Wails for debugging and development of the installer.
##
## For development first make a wails nsis build to populate the "wails_tools.nsh":
## > wails build --target windows/amd64 --nsis
## Then you can call makensis on this file with specifying the path to your binary:
## For a AMD64 only installer:
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
## For a ARM64 only installer:
## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
## For a installer with both architectures:
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
####
## The following information is taken from the ProjectInfo file, but they can be overwritten here.
####
## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}"
## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}"
## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}"
## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}"
## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}"
###
## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
####
## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
####
## Include the wails tools
####
!include "wails_tools.nsh"
# The version information for this two must consist of 4 parts
VIProductVersion "${INFO_PRODUCTVERSION}.0"
VIFileVersion "${INFO_PRODUCTVERSION}.0"
VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
ManifestDPIAware true
!include "MUI.nsh"
!define MUI_ICON "..\icon.ico"
!define MUI_UNICON "..\icon.ico"
# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
!insertmacro MUI_PAGE_INSTFILES # Installing page.
!insertmacro MUI_PAGE_FINISH # Finished installation page.
!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page
!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
#!uninstfinalize 'signtool --file "%1"'
#!finalize 'signtool --file "%1"'
Name "${INFO_PRODUCTNAME}"
OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
ShowInstDetails show # This will always show the installation details.
Function .onInit
!insertmacro wails.checkArchitecture
FunctionEnd
Section
!insertmacro wails.setShellContext
!insertmacro wails.webview2runtime
SetOutPath $INSTDIR
!insertmacro wails.files
CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
!insertmacro wails.associateFiles
!insertmacro wails.associateCustomProtocols
!insertmacro wails.writeUninstaller
SectionEnd
Section "uninstall"
!insertmacro wails.setShellContext
RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
RMDir /r $INSTDIR
Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
!insertmacro wails.unassociateFiles
!insertmacro wails.unassociateCustomProtocols
!insertmacro wails.deleteUninstaller
SectionEnd
================================================
FILE: app/build/windows/installer/wails_tools.nsh
================================================
# DO NOT EDIT - Generated automatically by `wails build`
!include "x64.nsh"
!include "WinVer.nsh"
!include "FileFunc.nsh"
!ifndef INFO_PROJECTNAME
!define INFO_PROJECTNAME "demo-app"
!endif
!ifndef INFO_COMPANYNAME
!define INFO_COMPANYNAME "demo-app"
!endif
!ifndef INFO_PRODUCTNAME
!define INFO_PRODUCTNAME "demo-app"
!endif
!ifndef INFO_PRODUCTVERSION
!define INFO_PRODUCTVERSION "1.0.0"
!endif
!ifndef INFO_COPYRIGHT
!define INFO_COPYRIGHT "Copyright........."
!endif
!ifndef PRODUCT_EXECUTABLE
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
!endif
!ifndef UNINST_KEY_NAME
!define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
!endif
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
!ifndef REQUEST_EXECUTION_LEVEL
!define REQUEST_EXECUTION_LEVEL "admin"
!endif
RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
!ifdef ARG_WAILS_AMD64_BINARY
!define SUPPORTS_AMD64
!endif
!ifdef ARG_WAILS_ARM64_BINARY
!define SUPPORTS_ARM64
!endif
!ifdef SUPPORTS_AMD64
!ifdef SUPPORTS_ARM64
!define ARCH "amd64_arm64"
!else
!define ARCH "amd64"
!endif
!else
!ifdef SUPPORTS_ARM64
!define ARCH "arm64"
!else
!error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
!endif
!endif
!macro wails.checkArchitecture
!ifndef WAILS_WIN10_REQUIRED
!define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
!endif
!ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
!define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
!endif
${If} ${AtLeastWin10}
!ifdef SUPPORTS_AMD64
${if} ${IsNativeAMD64}
Goto ok
${EndIf}
!endif
!ifdef SUPPORTS_ARM64
${if} ${IsNativeARM64}
Goto ok
${EndIf}
!endif
IfSilent silentArch notSilentArch
silentArch:
SetErrorLevel 65
Abort
notSilentArch:
MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
Quit
${else}
IfSilent silentWin notSilentWin
silentWin:
SetErrorLevel 64
Abort
notSilentWin:
MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
Quit
${EndIf}
ok:
!macroend
!macro wails.files
!ifdef SUPPORTS_AMD64
${if} ${IsNativeAMD64}
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
${EndIf}
!endif
!ifdef SUPPORTS_ARM64
${if} ${IsNativeARM64}
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
${EndIf}
!endif
!macroend
!macro wails.writeUninstaller
WriteUninstaller "$INSTDIR\uninstall.exe"
SetRegView 64
WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
IntFmt $0 "0x%08X" $0
WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
!macroend
!macro wails.deleteUninstaller
Delete "$INSTDIR\uninstall.exe"
SetRegView 64
DeleteRegKey HKLM "${UNINST_KEY}"
!macroend
!macro wails.setShellContext
${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
SetShellVarContext all
${else}
SetShellVarContext current
${EndIf}
!macroend
# Install webview2 by launching the bootstrapper
# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
!macro wails.webview2runtime
!ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
!define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
!endif
SetRegView 64
# If the admin key exists and is not empty then webview2 is already installed
ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
${If} $0 != ""
Goto ok
${EndIf}
${If} ${REQUEST_EXECUTION_LEVEL} == "user"
# If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
${If} $0 != ""
Goto ok
${EndIf}
${EndIf}
SetDetailsPrint both
DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
SetDetailsPrint listonly
InitPluginsDir
CreateDirectory "$pluginsdir\webview2bootstrapper"
SetOutPath "$pluginsdir\webview2bootstrapper"
File "tmp\MicrosoftEdgeWebview2Setup.exe"
ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
SetDetailsPrint both
ok:
!macroend
# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
; Backup the previously associated file class
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
!macroend
!macro APP_UNASSOCIATE EXT FILECLASS
; Backup the previously associated file class
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
!macroend
!macro wails.associateFiles
; Create file associations
!macroend
!macro wails.unassociateFiles
; Delete app associations
!macroend
!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
!macroend
!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
!macroend
!macro wails.associateCustomProtocols
; Create custom protocols associations
!macroend
!macro wails.unassociateCustomProtocols
; Delete app custom protocol associations
!macroend
================================================
FILE: app/build/windows/wails.exe.manifest
================================================
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity type="win32" name="com.wails.{{.Name}}" version="{{.Info.ProductVersion}}.0" processorArchitecture="*"/>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
</dependentAssembly>
</dependency>
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
</asmv3:windowsSettings>
</asmv3:application>
</assembly>
================================================
FILE: app/dev.bat
================================================
chcp 65001
wails dev
================================================
FILE: app/frontend/index.html
================================================
<!--
~ Copyright 2025 Bronya0 <tangssst@163.com>.
~ Author Github: https://github.com/Bronya0
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>wails-naive-demo</title>
<link href="./src/style.css" rel="stylesheet">
</head>
<body>
<div id="app"></div>
<script src="./src/main.js" type="module"></script>
</body>
</html>
================================================
FILE: app/frontend/package.json
================================================
{
"name": "app",
"version": "1.0.0",
"description": "",
"main": "",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"ace-builds": "1.4.12",
"highlight.js": "^11.10.0",
"jsoneditor": "^10.1.0",
"mitt": "^3.0.1",
"vue": "^3.5.8"
},
"devDependencies": {
"@vicons/material": "^0.12.0",
"@vitejs/plugin-vue": "^6.0.0",
"naive-ui": "^2.39.0",
"vite": "7.1.5"
},
"keywords": [],
"author": "bronya0"
}
================================================
FILE: app/frontend/src/App.vue
================================================
<!--
- Copyright 2025 Bronya0 <tangssst@163.com>.
- Author Github: https://github.com/Bronya0
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- https://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-->
<template>
<n-config-provider
:theme="Theme"
:hljs="hljs"
:locale="zhCN" :date-locale="dateZhCN"
>
<!--https://www.naiveui.com/zh-CN/os-theme/components/layout-->
<n-message-provider container-style=" word-break: break-all;">
<n-notification-provider placement="bottom-right" container-style="text-align: left;">
<n-dialog-provider>
<n-loading-bar-provider>
<n-layout has-sider position="absolute" style="height: 100vh;" :class="headerClass">
<!--header-->
<n-layout-header bordered style="height: 42px; bottom: 0; padding: 0; ">
<Header />
</n-layout-header>
<!--side + content-->
<n-layout has-sider position="absolute" style="top: 42px; bottom: 0;">
<n-layout-sider
bordered
collapse-mode="width"
:collapsed-width="60"
:collapsed="true"
style="--wails-draggable:drag"
>
<Aside
:collapsed-width="60"
:value="activeItem.key"
:options="sideMenuOptions"
/>
</n-layout-sider>
<n-layout-content style="padding: 0 16px;">
<keep-alive>
<component :is="activeItem.component"></component>
</keep-alive>
</n-layout-content>
</n-layout>
</n-layout>
</n-loading-bar-provider>
</n-dialog-provider>
</n-notification-provider>
</n-message-provider>
</n-config-provider>
</template>
<script setup>
import {onMounted, ref, shallowRef} from 'vue'
import {
darkTheme,
lightTheme,
NConfigProvider,
NLayout,
NLayoutContent,
NLayoutHeader,
NMessageProvider,
} from 'naive-ui'
import Header from './components/Header.vue'
import Settings from './components/Settings.vue'
import Health from './components/Health.vue'
import Core from './components/Core.vue'
import Nodes from './components/Nodes.vue'
import Index from './components/Index.vue'
import Rest from './components/Rest.vue'
import Conn from './components/Conn.vue'
import Task from './components/Task.vue'
import Snapshot from './components/Snapshot.vue'
import About from './components/About.vue'
import {GetConfig, SaveTheme} from "../wailsjs/go/config/AppConfig";
import {renderIcon} from "./utils/common";
import Aside from "./components/Aside.vue";
import emitter from "./utils/eventBus";
import {
FavoriteTwotone,
HiveOutlined,
SettingsSuggestOutlined, TaskAltFilled,
ApiOutlined, LibraryBooksOutlined, AllOutOutlined, BarChartOutlined, AddAPhotoTwotone, InfoOutlined
} from '@vicons/material'
import hljs from 'highlight.js/lib/core'
import json from 'highlight.js/lib/languages/json'
import { zhCN, dateZhCN } from 'naive-ui'
let headerClass = shallowRef('lightTheme')
let Theme = shallowRef(lightTheme)
hljs.registerLanguage('json', json)
onMounted(async () => {
// 从后端加载配置
const loadedConfig = await GetConfig()
// 设置主题
themeChange(loadedConfig.theme)
// 语言切换
// handleLanguageChange(loadedConfig.language)
// =====================注册事件监听=====================
// 主题切换
emitter.on('update_theme', themeChange)
// 菜单切换
emitter.on('menu_select', handleMenuSelect)
})
// 左侧菜单
const sideMenuOptions = [
{
label: '集群',
key: '集群',
icon: renderIcon(HiveOutlined),
component: Conn,
},
{
label: '节点',
key: '节点',
icon: renderIcon(AllOutOutlined),
component: Nodes,
},
{
label: '索引',
key: '索引',
icon: renderIcon(LibraryBooksOutlined),
component: Index,
},
{
label: 'REST',
key: 'REST',
icon: renderIcon(ApiOutlined),
component: Rest,
},
{
label: 'Task',
key: 'Task',
icon: renderIcon(TaskAltFilled),
component: Task,
},
{
label: '健康',
key: '健康',
icon: renderIcon(FavoriteTwotone),
component: Health,
},
{
label: '指标',
key: '指标',
icon: renderIcon(BarChartOutlined),
component: Core,
},
{
label: '快照',
key: '快照',
icon: renderIcon(AddAPhotoTwotone),
component: Snapshot,
},
{
label: '设置',
key: '设置',
icon: renderIcon(SettingsSuggestOutlined),
component: Settings
},
{
label: "关于",
key: "about",
icon: renderIcon(InfoOutlined),
component: About
},
]
const activeItem = shallowRef(sideMenuOptions[0])
// 切换菜单
function handleMenuSelect(key) {
// 根据key寻找item
activeItem.value = sideMenuOptions.find(item => item.key === key)
}
// 主题切换
function themeChange(newTheme) {
Theme.value = newTheme === lightTheme.name ? lightTheme : darkTheme
headerClass = newTheme === lightTheme.name ? "lightTheme" : "darkTheme"
}
</script>
<style>
body {
margin: 0;
font-family: sans-serif;
}
.lightTheme .n-layout-header {
background-color: #f7f7fa;
}
.lightTheme .n-layout-sider {
background-color: #f7f7fa !important;
}
</style>
================================================
FILE: app/frontend/src/assets/fonts/OFL.txt
================================================
Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
================================================
FILE: app/frontend/src/components/About.vue
================================================
<!--
- Copyright 2025 Bronya0 <tangssst@163.com>.
- Author Github: https://github.com/Bronya0
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- https://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-->
<template>
<n-flex vertical>
<n-flex align="center">
<h2>关于</h2>
</n-flex>
</n-flex>
<n-flex align="center">
<n-form label-placement="top" style="text-align: left;">
<n-form-item label="项目主页">
<n-button @click="BrowserOpenURL(home_url)" :render-icon="renderIcon(HouseTwotone)">ES-King</n-button>
</n-form-item>
<n-form-item label="推荐同款 Kafka 客户端">
<n-button @click="BrowserOpenURL(kafka_home_url)" :render-icon="renderIcon(HouseTwotone)">KafKa-King</n-button>
</n-form-item>
<n-form-item label="技术交流群">
<n-button :focusable="false" @click="openUrl(qq_url)">点我加群✨</n-button>
</n-form-item>
<n-form-item label="打赏是开源项目生存的动力,谢谢!">
<img src="../assets/images/wechat.png" alt="pay" style="width: 200px; height: 200px;">
</n-form-item>
</n-form>
</n-flex>
</template>
<script setup>
import {onMounted} from 'vue'
import {NButton, NForm, NFormItem, useMessage,} from 'naive-ui'
import {BrowserOpenURL} from "../../wailsjs/runtime";
import {openUrl, renderIcon} from "../utils/common";
import {HouseTwotone} from '@vicons/material'
const kafka_home_url = "https://github.com/Bronya0/kafka-King"
const qq_url = "https://qm.qq.com/cgi-bin/qm/qr?k=pDqlVFyLMYEEw8DPJlRSBN27lF8qHV2v&jump_from=webapi&authKey=Wle/K0ARM1YQWlpn6vvfiZuMedy2tT9BI73mUvXVvCuktvi0fNfmNR19Jhyrf2Nz"
const home_url = "https://github.com/Bronya0/ES-King"
onMounted(async () => {
})
</script>
================================================
FILE: app/frontend/src/components/Aside.vue
================================================
<!--
- Copyright 2025 Bronya0 <tangssst@163.com>.
- Author Github: https://github.com/Bronya0
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- https://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-->
<template>
<!-- https://www.naiveui.com/zh-CN/os-theme/components/menu-->
<n-menu
:mode="'vertical'"
:value="props.value"
@update:value="handleMenuSelect"
:options="props.options"
style="--wails-draggable:no-drag"
/>
</template>
<script setup>
import emitter from "../utils/eventBus";
const props = defineProps(['options', 'value']);
const handleMenuSelect = (key, item) => {
emitter.emit('menu_select', key)
}
</script>
<style>
</style>
================================================
FILE: app/frontend/src/components/Conn.vue
================================================
<!--
- Copyright 2025 Bronya0 <tangssst@163.com>.
- Author Github: https://github.com/Bronya0
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- https://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-->
<template>
<div>
<n-flex vertical>
<n-flex align="center">
<h2>集群</h2>
<n-text>共有 {{ esNodes.length }} 个</n-text>
<n-button @click="addNewNode" :render-icon="renderIcon(AddFilled)">添加集群</n-button>
</n-flex>
<n-spin :show="spin_loading" description="Connecting...">
<n-grid :x-gap="12" :y-gap="12" :cols="4">
<n-gi v-for="node in esNodes" :key="node.id">
<n-card :title="node.name" @click="selectNode(node)" hoverable class="conn_card">
<template #header-extra>
<n-space>
<n-button @click.stop="editNode(node)" size="small">
编辑
</n-button>
<n-popconfirm @positive-click="deleteNode(node.id)" negative-text="取消" positive-text="确定">
<template #trigger>
<n-button @click.stop size="small">
删除
</n-button>
</template>
确定删除吗?
</n-popconfirm>
</n-space>
</template>
<n-descriptions :column="1" label-placement="left">
<n-descriptions-item label="主机">
{{ node.host }}
</n-descriptions-item>
</n-descriptions>
</n-card>
</n-gi>
</n-grid>
</n-spin>
</n-flex>
<n-drawer v-model:show="showEditDrawer" style="width: 38.2%" placement="right">
<n-drawer-content :title="drawerTitle">
<n-form
ref="formRef"
:model="currentNode"
:rules="{
name: {required: true, message: '请输入昵称', trigger: 'blur'},
host: {required: true, message: '请输入主机地址', trigger: 'blur'},
port: {required: true, type: 'number', message: '请输入有效的端口号', trigger: 'blur'},
}"
label-placement="top"
style="text-align: left;"
>
<n-form-item label="昵称" path="name">
<n-input v-model:value="currentNode.name" placeholder="输入节点名称"/>
</n-form-item>
<n-form-item label="协议://主机:端口" path="host">
<n-input v-model:value="currentNode.host" placeholder="输入协议://主机:端口"/>
</n-form-item>
<n-form-item label="用户名" path="username">
<n-input v-model:value="currentNode.username" placeholder="输入用户名"/>
</n-form-item>
<n-form-item label="密码" path="password">
<n-input
v-model:value="currentNode.password"
type="password"
placeholder="输入密码"
/>
</n-form-item>
<n-form-item label="使用 SSL" path="useSSL">
<n-switch :round="false" v-model:value="currentNode.useSSL"/>
</n-form-item>
<n-form-item label="跳过 SSL 验证" path="skipSSLVerify">
<n-switch :round="false" v-model:value="currentNode.skipSSLVerify"/>
</n-form-item>
<n-form-item label="CA 证书" path="caCert">
<n-input v-model:value="currentNode.caCert" type="textarea" placeholder="输入 CA 证书内容"/>
</n-form-item>
</n-form>
<template #footer>
<n-space justify="end">
<n-button @click="test_connect" :loading="test_connect_loading">连接测试</n-button>
<n-button @click="showEditDrawer = false">取消</n-button>
<n-button type="primary" @click="saveNode">保存</n-button>
</n-space>
</template>
</n-drawer-content>
</n-drawer>
</div>
</template>
<script setup>
import {computed, onMounted, ref} from 'vue'
import {useMessage} from 'naive-ui'
import {renderIcon} from "../utils/common";
import {AddFilled} from "@vicons/material";
import emitter from "../utils/eventBus";
import {SetConnect, TestClient} from "../../wailsjs/go/service/ESService";
import {GetConfig, SaveConfig} from "../../wailsjs/go/config/AppConfig";
const message = useMessage()
const esNodes = ref([])
const showEditDrawer = ref(false)
const currentNode = ref({
name: '',
host: '',
port: 9200,
username: '',
password: '',
useSSL: false,
skipSSLVerify: false,
caCert: ''
})
const isEditing = ref(false)
const spin_loading = ref(false)
const test_connect_loading = ref(false)
const drawerTitle = computed(() => isEditing.value ? '编辑连接' : '添加连接')
const formRef = ref(null)
onMounted(() => {
refreshNodeList()
})
const refreshNodeList = async () => {
spin_loading.value = true
const config = await GetConfig()
esNodes.value = config.connects.filter(node => node.name !== null && node.name !== "")
spin_loading.value = false
}
function editNode(node) {
currentNode.value = {...node}
isEditing.value = true
showEditDrawer.value = true
}
const addNewNode = async () => {
currentNode.value = {}
isEditing.value = false
showEditDrawer.value = true
}
const saveNode = async () => {
formRef.value?.validate(async (errors) => {
if (!errors) {
const config = await GetConfig()
// edit
if (isEditing.value) {
const index = esNodes.value.findIndex(node => node.id === currentNode.value.id)
if (index !== -1) {
esNodes.value[index] = {...currentNode.value}
}
} else {
// add
const newId = Math.max(...esNodes.value.map(node => node.id), 0) + 1
esNodes.value.push({...currentNode.value, id: newId})
}
// 保存
config.connects = esNodes.value
await SaveConfig(config)
showEditDrawer.value = false
await refreshNodeList()
message.success('保存成功')
} else {
message.error('请填写所有必填字段')
}
})
}
const deleteNode = async (id) => {
console.log(esNodes.value)
console.log(id)
esNodes.value = esNodes.value.filter(node => node.id !== id)
console.log(esNodes.value)
const config = await GetConfig()
config.connects = esNodes.value
await SaveConfig(config)
await refreshNodeList()
message.success('删除成功')
}
const test_connect = async () => {
formRef.value?.validate(async (errors) => {
if (!errors) {
test_connect_loading.value = true
try {
const node = currentNode.value
const res = await TestClient(node.host, node.username, node.password, node.caCert, node.useSSL, node.skipSSLVerify)
if (res !== "") {
message.error("连接失败:" + res)
} else {
message.success('连接成功')
}
} catch (e) {
message.error(e.message)
}
test_connect_loading.value = false
} else {
message.error('请填写所有必填字段')
}
})
}
const selectNode = async (node) => {
// 这里实现切换菜单的逻辑
console.log('选中节点:', node)
spin_loading.value = true
try {
const res = await TestClient(node.host, node.username, node.password, node.caCert, node.useSSL, node.skipSSLVerify)
if (res !== "") {
message.error("连接失败:" + res)
} else {
await SetConnect(node.name, node.host, node.username, node.password, node.caCert, node.useSSL, node.skipSSLVerify)
message.success('连接成功')
emitter.emit('menu_select', "节点")
emitter.emit('selectNode', node)
}
} catch (e) {
message.error(e.message)
}
spin_loading.value = false
}
</script>
<style>
.lightTheme .conn_card {
background-color: #fafafc
}
</style>
================================================
FILE: app/frontend/src/components/Core.vue
================================================
<!--
- Copyright 2025 Bronya0 <tangssst@163.com>.
- Author Github: https://github.com/Bronya0
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- https://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-->
<template>
<n-flex vertical>
<n-flex align="center">
<h2>指标</h2>
<n-button @click="getData" text :render-icon="renderIcon(RefreshOutlined)">refresh</n-button>
</n-flex>
<n-spin :show="loading" description="Connecting...">
<n-collapse>
<n-collapse-item v-for="(item_v, item_k) in collapse_item" :name="item_v" :title="item_v">
<n-table :single-line="false" size="small">
<thead>
<tr>
<th>说明</th>
<th>值</th>
<th>key</th>
</tr>
</thead>
<tbody>
<tr v-for="(value, key) in filterByKey(data, item_k)" :key="key">
<td>
<n-tooltip placement="top" trigger="hover">
<template #trigger>{{ getLabel(key) }}</template>
{{ key }}
</n-tooltip>
</td>
<td>{{ value }}</td>
<td>{{ key }}</td>
</tr>
</tbody>
</n-table>
</n-collapse-item>
</n-collapse>
</n-spin>
</n-flex>
</template>
<script setup>
import {onMounted, ref} from "vue";
import emitter from "../utils/eventBus";
import {useMessage} from "naive-ui";
import {GetStats} from "../../wailsjs/go/service/ESService";
import {flattenObject, renderIcon} from "../utils/common";
import {RefreshOutlined} from "@vicons/material";
const loading = ref(false)
const data = ref({})
const message = useMessage()
const selectNode = async (node) => {
await getData()
}
onMounted(() => {
emitter.on('selectNode', selectNode)
getData()
})
const collapse_item = {
'node': '节点',
'memory': '内存',
'indices': '索引',
'doc': '文档',
'shard': '分片',
'store': '存储'
}
const getData = async () => {
loading.value = true
const res = await GetStats()
if (res.err !== "") {
message.error(res.err)
} else {
data.value = flattenObject(res.result)
}
console.log(data.value)
loading.value = false
}
// 方法,返回过滤后的数据
const filterByKey = (data, searchString) => {
const result = {};
for (const [key, value] of Object.entries(data)) {
if (key.includes(searchString)) {
result[key] = value;
}
}
return result;
};
const getLabel = (key) => {
const descriptions = {
"_nodes.failed": '报告中失败的节点数量',
"_nodes.successful": '报告中成功的节点数量',
"_nodes.total": '集群中报告的总节点数',
"cluster_name": '集群名称',
"cluster_uuid": '集群的唯一标识符',
"indices.analysis.analyzer_types": '分析器类型列表',
"indices.analysis.built_in_analyzers": '内置分析器列表',
"indices.analysis.built_in_char_filters": '内置字符过滤器列表',
"indices.analysis.built_in_filters": '内置过滤器列表',
"indices.analysis.built_in_tokenizers": '内置分词器列表',
"indices.analysis.char_filter_types": '字符过滤器类型列表',
"indices.analysis.filter_types": '过滤器类型列表',
"indices.analysis.tokenizer_types": '分词器类型列表',
"indices.completion.size_in_bytes": '完成建议字段使用的内存大小(字节)',
"indices.count": '索引总数',
"indices.docs.count": '文档总数',
"indices.docs.deleted": '已删除文档的数量',
"indices.fielddata.evictions": '字段数据缓存驱逐次数',
"indices.fielddata.memory_size_in_bytes": '字段数据缓存使用的内存大小(字节)',
"indices.mappings.field_types": '映射字段类型列表',
"indices.mappings.runtime_field_types": '运行时字段类型列表',
"indices.mappings.total_deduplicated_field_count": '去重后的字段计数',
"indices.mappings.total_deduplicated_mapping_size_in_bytes": '去重后的映射大小(字节)',
"indices.mappings.total_field_count": '字段计数',
"indices.query_cache.cache_count": '查询缓存条目数',
"indices.query_cache.cache_size": '查询缓存大小',
"indices.query_cache.evictions": '查询缓存驱逐次数',
"indices.query_cache.hit_count": '查询缓存命中次数',
"indices.query_cache.memory_size_in_bytes": '查询缓存使用的内存大小(字节)',
"indices.query_cache.miss_count": '查询缓存未命中次数',
"indices.query_cache.total_count": '总的查询缓存条目数',
"indices.segments.count": '段数',
"indices.segments.doc_values_memory_in_bytes": '文档值使用的内存大小(字节)',
"indices.segments.fixed_bit_set_memory_in_bytes": '固定位集使用的内存大小(字节)',
"indices.segments.index_writer_memory_in_bytes": '索引写入器使用的内存大小(字节)',
"indices.segments.max_unsafe_auto_id_timestamp": '最大不安全的自动 ID 时间戳',
"indices.segments.memory_in_bytes": '段使用的内存大小(字节)',
"indices.segments.norms_memory_in_bytes": '规范化因子使用的内存大小(字节)',
"indices.segments.points_memory_in_bytes": '点使用的内存大小(字节)',
"indices.segments.stored_fields_memory_in_bytes": '存储字段使用的内存大小(字节)',
"indices.segments.term_vectors_memory_in_bytes": '词条向量使用的内存大小(字节)',
"indices.segments.terms_memory_in_bytes": '词条使用的内存大小(字节)',
"indices.segments.version_map_memory_in_bytes": '版本映射使用的内存大小(字节)',
"indices.shards.index.primaries.avg": '主分片平均数量',
"indices.shards.index.primaries.max": '主分片最大数量',
"indices.shards.index.primaries.min": '主分片最小数量',
"indices.shards.index.replication.avg": '副本平均数量',
"indices.shards.index.replication.max": '副本最大数量',
"indices.shards.index.replication.min": '副本最小数量',
"indices.shards.index.shards.avg": '分片平均数量',
"indices.shards.index.shards.max": '分片最大数量',
"indices.shards.index.shards.min": '分片最小数量',
"indices.shards.primaries": '主分片总数',
"indices.shards.replication": '副本总数',
"indices.shards.total": '分片总数',
"indices.store.reserved_in_bytes": '保留的空间大小(字节)',
"indices.store.size_in_bytes": '存储大小(字节)',
"indices.store.total_data_set_size_in_bytes": '总数据集大小(字节)',
"indices.versions": '索引版本信息',
"nodes.count.coordinating_only": '仅协调节点的数量',
"nodes.count.data": '数据节点的数量',
"nodes.count.data_cold": '冷数据节点的数量',
"nodes.count.data_content": '数据内容节点的数量',
"nodes.count.data_frozen": '冻结数据节点的数量',
"nodes.count.data_hot": '热数据节点的数量',
"nodes.count.data_warm": '温数据节点的数量',
"nodes.count.ingest": '摄取节点的数量',
"nodes.count.master": '主节点的数量',
"nodes.count.ml": '机器学习节点的数量',
"nodes.count.remote_cluster_client": '远程集群客户端节点的数量',
"nodes.count.transform": '转换节点的数量',
"nodes.count.voting_only": '仅投票节点的数量',
"nodes.count.total": '节点总数',
"nodes.discovery_types.multi_node": '多节点发现类型数量',
"nodes.fs.available_in_bytes": '可用磁盘空间大小(字节)',
"nodes.fs.free_in_bytes": '空闲磁盘空间大小(字节)',
"nodes.fs.total_in_bytes": '总磁盘空间大小(字节)',
"nodes.indexing_pressure.memory.current.all_in_bytes": '当前所有索引压力内存大小(字节)',
"nodes.indexing_pressure.memory.current.combined_coordinating_and_primary_in_bytes": '当前组合协调和主要索引压力内存大小(字节)',
"nodes.indexing_pressure.memory.current.coordinating_in_bytes": '当前协调索引压力内存大小(字节)',
"nodes.indexing_pressure.memory.current.primary_in_bytes": '当前主要索引压力内存大小(字节)',
"nodes.indexing_pressure.memory.current.replica_in_bytes": '当前副本索引压力内存大小(字节)',
"nodes.indexing_pressure.memory.limit_in_bytes": '索引压力内存限制大小(字节)',
"nodes.indexing_pressure.memory.total.all_in_bytes": '总计所有索引压力内存大小(字节)',
"nodes.indexing_pressure.memory.total.combined_coordinating_and_primary_in_bytes": '总计组合协调和主要索引压力内存大小(字节)',
"nodes.indexing_pressure.memory.total.coordinating_in_bytes": '总计协调索引压力内存大小(字节)',
"nodes.indexing_pressure.memory.total.coordinating_rejections": '总计协调索引拒绝次数',
"nodes.indexing_pressure.memory.total.primary_in_bytes": '总计主要索引压力内存大小(字节)',
"nodes.indexing_pressure.memory.total.primary_rejections": '总计主要索引拒绝次数',
"nodes.indexing_pressure.memory.total.replica_in_bytes": '总计副本索引压力内存大小(字节)',
"nodes.indexing_pressure.memory.total.replica_rejections": '总计副本索引拒绝次数',
"nodes.ingest.number_of_pipelines": '摄取管道数量',
"nodes.jvm.max_uptime_in_millis": 'JVM 上线时间(毫秒)',
"nodes.jvm.mem.heap_max_in_bytes": '堆内存最大大小(字节)',
"nodes.jvm.mem.heap_used_in_bytes": '堆内存使用大小(字节)',
"nodes.jvm.threads": 'JVM 线程数',
"nodes.jvm.versions": 'JVM 版本信息',
"nodes.network_types.http_types.netty4": 'HTTP 类型为 Netty 4 的节点数量',
"nodes.network_types.transport_types.netty4": '传输类型为 Netty 4 的节点数量',
"nodes.os.allocated_processors": '分配的处理器数量',
"nodes.os.architectures": '操作系统架构信息',
"nodes.os.available_processors": '可用处理器数量',
"nodes.os.mem.adjusted_total_in_bytes": '调整后的总内存大小(字节)',
"nodes.os.mem.free_in_bytes": '空闲内存大小(字节)',
"nodes.os.mem.free_percent": '空闲内存百分比',
"nodes.os.mem.total_in_bytes": '总内存大小(字节)',
"nodes.os.mem.used_in_bytes": '使用内存大小(字节)',
"nodes.os.mem.used_percent": '使用内存百分比',
"nodes.os.names": '操作系统名称',
"nodes.os.pretty_names": '操作系统友好名称',
"nodes.packaging_types": '打包类型信息',
"nodes.plugins": '插件信息',
"nodes.process.cpu.percent": 'CPU 使用率',
"nodes.process.open_file_descriptors.avg": '平均打开的文件描述符数量',
"nodes.process.open_file_descriptors.max": '最大打开的文件描述符数量',
"nodes.process.open_file_descriptors.min": '最小打开的文件描述符数量',
"nodes.versions": '节点版本信息',
"status": '集群健康状态(绿色、黄色、红色)',
"timestamp": '报告的时间戳'
};
return descriptions[key] || '暂无说明'
}
</script>
<style scoped>
</style>
================================================
FILE: app/frontend/src/components/Header.vue
================================================
<!--
- Copyright 2025 Bronya0 <tangssst@163.com>.
- Author Github: https://github.com/Bronya0
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- https://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-->
<template>
<n-page-header style="padding: 4px;--wails-draggable:drag">
<template #avatar>
<n-avatar :src="icon"/>
</template>
<template #subtitle>
<n-tooltip>
<template #trigger>
<n-tag v-if="subtitle" :type=title_tag>{{ subtitle }}</n-tag>
<n-p v-else>{{ desc }}</n-p>
</template>
健康:{{ title_tag }}
</n-tooltip>
</template>
<template #title>
<div style="font-weight: 800">{{ app_name }}</div>
</template>
<template #extra>
<n-flex justify="flex-end" style="--wails-draggable:no-drag" class="right-section">
<n-button quaternary :focusable="false" @click="openUrl(qq_url)">技术交流群</n-button>
<!-- <n-button quaternary :focusable="false" @click="changeTheme" :render-icon="renderIcon(MoonOrSunnyOutline)"/>-->
<n-tooltip placement="bottom" trigger="hover">
<template #trigger>
<n-button :render-icon="renderIcon(HouseTwotone)" quaternary
@click="openUrl(update_url)"/>
</template>
<span>主页</span>
</n-tooltip>
<n-tooltip placement="bottom" trigger="hover">
<template #trigger>
<n-button quaternary :focusable="false" :loading="update_loading" @click="checkForUpdates"
:render-icon="renderIcon(SystemUpdateAltSharp)"/>
</template>
<span>检查版本:{{ version.tag_name }} {{ check_msg }}</span>
</n-tooltip>
<n-button quaternary :focusable="false" @click="minimizeWindow" :render-icon="renderIcon(RemoveOutlined)"/>
<n-button quaternary :focusable="false" @click="resizeWindow" :render-icon="renderIcon(MaxMinIcon)"/>
<n-button quaternary class="close-btn" style="font-size: 22px" :focusable="false" @click="closeWindow">
<n-icon>
<CloseFilled/>
</n-icon>
</n-button>
</n-flex>
</template>
</n-page-header>
</template>
<script setup>
import {NAvatar, NButton, NFlex, useMessage} from 'naive-ui'
import {
SystemUpdateAltSharp,
RemoveOutlined,
CloseFilled,
CropSquareFilled,
ContentCopyFilled, HouseTwotone
} from '@vicons/material'
import icon from '../assets/images/appicon.png'
import {h, onMounted, ref, shallowRef} from "vue";
import {BrowserOpenURL, Quit, WindowMaximise, WindowMinimise, WindowUnmaximise} from "../../wailsjs/runtime";
import {CheckUpdate} from '../../wailsjs/go/system/Update'
import {useNotification} from 'naive-ui'
import {openUrl, renderIcon} from "../utils/common";
import {GetVersion, GetAppName} from "../../wailsjs/go/config/AppConfig";
import emitter from "../utils/eventBus";
// defineProps(['options', 'value']);
// const MoonOrSunnyOutline = shallowRef(WbSunnyOutlined)
const isMaximized = ref(false);
const title_tag = ref("success");
const check_msg = ref("");
const app_name = ref("");
const MaxMinIcon = shallowRef(CropSquareFilled)
const update_url = "https://github.com/Bronya0/ES-King/releases"
const qq_url = "https://qm.qq.com/cgi-bin/qm/qr?k=pDqlVFyLMYEEw8DPJlRSBN27lF8qHV2v&jump_from=webapi&authKey=Wle/K0ARM1YQWlpn6vvfiZuMedy2tT9BI73mUvXVvCuktvi0fNfmNR19Jhyrf2Nz"
const update_loading = ref(false)
// let theme = lightTheme
let version = ref({
tag_name: "",
body: "",
})
const desc = "更人性化的ES GUI "
const subtitle = ref("")
const notification = useNotification()
const message = useMessage()
const checkForUpdates = async () => {
update_loading.value = true
try {
const v = await GetVersion()
const resp = await CheckUpdate()
if (!resp) {
message.error("无法连接github,检查更新失败")
} else if (resp.tag_name !== v) {
check_msg.value = '发现新版本 ' + resp.tag_name
version.value.body = resp.body
const n = notification.success({
title: '发现新版本: ' + resp.name,
description: resp.body,
action: () =>
h(NFlex, {justify: "flex-end"}, () => [
h(
NButton,
{
type: 'primary',
secondary: true,
onClick: () => BrowserOpenURL(update_url),
},
() => "立即下载",
),
h(
NButton,
{
secondary: true,
onClick: () => {
n.destroy()
},
},
() => "取消",
),
]),
onPositiveClick: () => BrowserOpenURL(update_url),
})
}
} finally {
update_loading.value = false
}
}
onMounted(async () => {
emitter.on('selectNode', selectNode)
emitter.on('changeTitleType', changeTitleType)
app_name.value = await GetAppName()
// const config = await GetConfig()
// MoonOrSunnyOutline.value = config.theme === lightTheme.name ? WbSunnyOutlined : NightlightRoundFilled
const v = await GetVersion()
version.value.tag_name = v
subtitle.value = desc + " " + v
await checkForUpdates()
})
const selectNode = (node) => {
subtitle.value = "当前集群:【" + node.name + "】"
}
// 动态修改title的类型
const changeTitleType = (type) => {
console.log(type)
title_tag.value = type
}
const minimizeWindow = () => {
WindowMinimise()
}
const resizeWindow = () => {
isMaximized.value = !isMaximized.value;
if (isMaximized.value) {
WindowMaximise();
MaxMinIcon.value = ContentCopyFilled;
} else {
WindowUnmaximise();
MaxMinIcon.value = CropSquareFilled;
}
console.log(isMaximized.value)
}
const closeWindow = () => {
Quit()
}
// const changeTheme = () => {
// MoonOrSunnyOutline.value = MoonOrSunnyOutline.value === NightlightRoundFilled ? WbSunnyOutlined : NightlightRoundFilled;
// theme = MoonOrSunnyOutline.value === NightlightRoundFilled ? darkTheme : lightTheme
// emitter.emit('update_theme', theme)
// }
</script>
<style scoped>
.close-btn:hover {
background-color: red;
}
.right-section .n-button {
padding: 0 8px;
}
</style>
================================================
FILE: app/frontend/src/components/Health.vue
================================================
<!--
- Copyright 2025 Bronya0 <tangssst@163.com>.
- Author Github: https://github.com/Bronya0
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- https://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-->
<template>
<n-flex vertical>
<n-flex align="center">
<h2>健康</h2>
<n-button :render-icon="renderIcon(RefreshOutlined)" text @click="getData">refresh</n-button>
</n-flex>
<n-spin :show="loading" description="Connecting...">
<n-table :bordered="false" :single-line="false">
<thead>
<tr>
<th>健康指标</th>
<th>值</th>
<th>完整键</th>
</tr>
</thead>
<tbody>
<tr v-for="(value, key) in data" :key="key">
<td>{{ getLabel(key) }}</td>
<td>
<n-tooltip placement="left" trigger="hover">
<template #trigger>
<n-tag :type="getTagType(key, value)">
{{ value }}
</n-tag>
</template>
{{ value }}
</n-tooltip>
</td>
<td>{{ key }}</td>
</tr>
</tbody>
</n-table>
</n-spin>
</n-flex>
</template>
<script setup>
import {onActivated, onMounted, ref} from "vue";
import emitter from "../utils/eventBus";
import {useMessage} from "naive-ui";
import {GetHealth} from "../../wailsjs/go/service/ESService";
import {renderIcon} from "../utils/common";
import {RefreshOutlined} from "@vicons/material";
const data = ref({})
const loading = ref(false)
const message = useMessage()
const selectNode = async (node) => {
await getData()
}
onMounted(() => {
emitter.on('selectNode', selectNode)
getData()
})
const getData = async () => {
loading.value = true
const res = await GetHealth()
if (res.err !== "") {
message.error(res.err)
} else {
data.value = res.result
emitter.emit('changeTitleType', getTagType("status", res.result['status']))
}
console.log(data.value)
loading.value = false
}
const getTagType = (key, value) => {
if (['cluster_name'].includes(key)) {
return 'success'
}
if (['unassigned_shards', 'delayed_unassigned_shards', 'initializing_shards'].includes(key)) {
return 'warning'
}
if (key === 'timed_out') {
return value === true ? 'error' : 'success'
}
if (key === 'status') {
if (value === 'green') {
return 'success'
} else {
return value === 'yellow' ? 'warning' : 'error'
}
}
return 'default'
}
const getLabel = (key) => {
const descriptions = {
cluster_name: '集群名称',
status: '集群健康状态(绿色、黄色、红色)',
timed_out: '请求是否超时',
number_of_nodes: '集群中的节点数',
number_of_data_nodes: '集群中的数据节点数',
active_primary_shards: '活跃的主分片数',
active_shards: '活跃的总分片数',
relocating_shards: '正在重新定位的分片数',
initializing_shards: '正在初始化的分片数',
unassigned_shards: '未分配的分片数',
delayed_unassigned_shards: '延迟未分配的分片数',
number_of_pending_tasks: '等待中的集群级任务数',
number_of_in_flight_fetch: '正在进行的分片数据获取数',
task_max_waiting_in_queue_millis: '任务在队列中的最长等待时间(毫秒)',
active_shards_percent_as_number: '活跃分片百分比'
}
return descriptions[key] || '暂无说明'
}
</script>
<style scoped>
</style>
================================================
FILE: app/frontend/src/components/Index.vue
================================================
<!--
- Copyright 2025 Bronya0 <tangssst@163.com>.
- Author Github: https://github.com/Bronya0
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- https://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-->
<template>
<n-flex vertical>
<n-flex align="center">
<h2>索引</h2>
<n-text>共计{{ data.length }}个</n-text>
</n-flex>
<n-flex align="center">
<n-input v-model:value="search_text" autosize placeholder="模糊搜索索引" style="min-width: 20%"
@keydown.enter="search"/>
<n-button :render-icon="renderIcon(SearchFilled)" @click="search"></n-button>
<n-button :render-icon="renderIcon(AddFilled)" @click="CreateIndexDrawerVisible = true">添加索引</n-button>
<n-button :render-icon="renderIcon(DriveFileMoveTwotone)" @click="downloadAllDataCsv">导出为csv</n-button>
<n-button :render-icon="renderIcon(AnnouncementOutlined)" @click="queryAlias">读取别名</n-button>
<n-button :loading="downloadIndexConfig.loading" :render-icon="renderIcon(DriveFileMoveTwotone)"
@click="downloadIndexConfig.show = true">
索引备份(预览)
</n-button>
</n-flex>
<n-spin :show="loading" description="Connecting...">
<n-data-table
ref="tableRef"
v-model:checked-row-keys="selectedRowKeys"
:bordered="false"
:columns="refColumns(columns)"
:data="data"
:pagination="pagination"
:row-key="rowKey"
size="small"
striped
/>
</n-spin>
<n-flex align="center">
<n-dropdown :options="bulk_options">
<n-button>批量操作</n-button>
</n-dropdown>
<n-text> 你选中了 {{ selectedRowKeys.length }} 行。</n-text>
</n-flex>
<n-drawer v-model:show="downloadIndexConfig.show" style="width: 38.2%">
<n-drawer-content style="text-align: left;" title="索引备份(预览)">
<n-flex vertical>
<n-p>【当前该功能还在测试中,使用请输入QQ群号,否则无法下载。如果遇到了bug,请反馈给群主。】</n-p>
输入要备份的索引名称
<n-input v-model:value="downloadIndexConfig.indexName"/>
输入查询dsl,不写默认查询所有
<n-input v-model:value="downloadIndexConfig.dsl" style="min-height: 300px; max-height: 800px;"
type="textarea"/>
测试码,也就是QQ群号,请在github上查看
<n-input v-model:value="downloadIndexConfig.code"/>
<n-flex align="center">
<n-button @click="downloadIndexConfig.show = false">取消</n-button>
<n-button :loading="downloadIndexConfig.loading" type="primary" @click="downloadIndex">开始下载</n-button>
</n-flex>
{{ downloadIndexConfig.msg }}
</n-flex>
</n-drawer-content>
</n-drawer>
<n-drawer v-model:show="drawerVisible" style="width: 38.2%">
<n-drawer-content :title="drawer_title" style="text-align: left;">
<n-code :code="json_data" language="json" show-line-numbers/>
</n-drawer-content>
</n-drawer>
<!-- 添加index-->
<n-drawer v-model:show="CreateIndexDrawerVisible" style="width: 38.2%">
<n-drawer-content style="text-align: left;" title="创建索引">
<n-form
ref="formRef"
:model="indexConfig"
:rules="{
name: {required: true, message: '请输入索引名称', trigger: 'blur'},
numberOfShards: {required: true, type: 'number', message: '请输入主分片', trigger: 'blur'},
numberOfReplicas: {required: true, type: 'number', message: '请输入副本数量', trigger: 'blur'},
}"
label-placement="top"
style="text-align: left;"
>
<n-form-item label="索引名称" path="name">
<n-input v-model:value="indexConfig.name"/>
</n-form-item>
<n-form-item label="主分片" path="numberOfShards">
<n-input-number v-model:value="indexConfig.numberOfShards"/>
</n-form-item>
<n-form-item label="副本数量" path="numberOfReplicas">
<n-input-number v-model:value="indexConfig.numberOfReplicas"/>
</n-form-item>
<n-p>mapping</n-p>
<n-form-item path="mapping">
<n-input v-model:value="indexConfig.mapping" placeholder='输入mapping的json,例如
{
"properties": {
"created_at": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss"
}
}
}' style="min-height: 300px; max-height: 800px;"
type="textarea"/>
</n-form-item>
</n-form>
<template #footer>
<n-space justify="end">
<n-button @click="CreateIndexDrawerVisible = false">取消</n-button>
<n-button :loading="addIndexLoading" type="primary" @click="addIndex">保存</n-button>
</n-space>
</template>
</n-drawer-content>
</n-drawer>
<!-- 添加doc-->
<n-drawer v-model:show="addDocDrawerVisible" style="width: 38.2%">
<n-drawer-content style="text-align: left;" title="添加文档">
<n-form
:model="docConfig"
:rules="{
doc: {required: true, message: '请输入文档内容', trigger: 'blur'},
}"
label-placement="top"
style="text-align: left;"
>
<n-form-item label="文档内容" path="doc">
<n-input v-model:value="docConfig.doc" placeholder='输入文档的json,例如
{
"field1": "value1",
"field2": "value2"
}' style="min-height: 300px; max-height: 800px;"
type="textarea"/>
</n-form-item>
</n-form>
<template #footer>
<n-space justify="end">
<n-button @click="addDocDrawerVisible = false">取消</n-button>
<n-button :loading="addDocLoading" type="primary" @click="addDocumentFunc">保存</n-button>
</n-space>
</template>
</n-drawer-content>
</n-drawer>
</n-flex>
</template>
<script setup>
import {h, onMounted, ref} from "vue";
import emitter from "../utils/eventBus";
import {NButton, NDataTable, NDropdown, NIcon, NTag, NText, useDialog, useMessage} from 'naive-ui'
import {
createCsvContent,
download_file,
formatBytes,
formattedJson,
isValidJson,
refColumns,
renderIcon
} from "../utils/common";
import {AddFilled, AnnouncementOutlined, DriveFileMoveTwotone, MoreVertFilled, SearchFilled} from "@vicons/material";
import {
AddDocument,
CacheClear,
CreateIndex,
DeleteIndex,
DownloadESIndex,
Flush,
GetDoc10,
GetIndexAliases,
GetIndexes,
GetIndexInfo,
MergeSegments,
OpenCloseIndex,
Refresh,
} from "../../wailsjs/go/service/ESService";
// 抽屉的可见性
const drawerVisible = ref(false)
const CreateIndexDrawerVisible = ref(false)
const json_data = ref({})
const drawer_title = ref("")
const loading = ref(false)
const tableRef = ref();
const formRef = ref();
const indexConfig = ref({
name: "",
numberOfShards: 1,
numberOfReplicas: 0,
mapping: "",
});
const data = ref([])
const message = useMessage()
const dialog = useDialog()
const search_text = ref("")
const selectedRowKeys = ref([]);
const rowKey = (row) => row.index
let aliases = {};
const downloadIndexConfig = ref({
indexName: "",
dsl: "",
loading: false,
show: false,
code: null,
msg: null,
});
const downloadIndex = async () => {
const indexName = downloadIndexConfig.value.indexName; // 或者从其他地方获取
const dsl = downloadIndexConfig.value.dsl; // 或者从其他地方获取
if (!indexName) {
message.error("请填写索引名");
return;
}
if (downloadIndexConfig.value.code !== "964440643") {
message.error("群号错误,请在github上查看");
return;
}
const file_path = `/${indexName}-${Math.floor(Date.now() / 1000)}.json`
message.info("开始下载,请不要退出...数据json位置:" + file_path);
downloadIndexConfig.value.msg = "开始下载,请不要退出...数据json位置:" + file_path;
downloadIndexConfig.value.loading = true;
try {
const res = await DownloadESIndex(indexName, dsl, file_path);
if (res.err !== "") {
message.error(res.err);
downloadIndexConfig.value.msg = res.err;
} else {
message.success("备份成功");
downloadIndexConfig.value.msg = "备份成功";
CreateIndexDrawerVisible.value = false;
}
} catch (e) {
message.error(e.message);
downloadIndexConfig.value.msg = e;
} finally {
downloadIndexConfig.value.loading = false;
}
};
const selectNode = (node) => {
data.value = []
selectedRowKeys.value = []
aliases = []
}
onMounted(() => {
emitter.on('selectNode', selectNode)
})
const search = async () => {
loading.value = true
try {
await getData(search_text.value)
} catch (e) {
message.error(e.message)
}
loading.value = false
}
const cacheData = (indexes) => {
// 缓存一下
const key = 'es_king_indexes';
let values = []
const stored = localStorage.getItem(key);
if (stored) {
values = JSON.parse(stored)
for (let v of indexes) {
if (!values.includes(v)) {
values.push(v);
}
}
}
if (values) {
localStorage.setItem(key, JSON.stringify(values.slice(-1000)))
}
}
const getData = async (value) => {
const res = await GetIndexes(value)
if (res.err !== "") {
message.error(res.err)
} else {
data.value = res.results
cacheData(res.results.map(item => item.index))
}
}
const pageKey = 'esKing:index:pageKey'
const pagination = ref({
page: 1,
pageSize: parseInt(localStorage.getItem(pageKey)) || 10,
showSizePicker: true,
pageSizes: [5, 10, 15, 20, 25, 30, 40],
onChange: (page) => {
pagination.value.page = page
},
onUpdatePageSize: (pageSize) => {
pagination.value.pageSize = pageSize
pagination.value.page = 1
localStorage.setItem(pageKey, pageSize.toString())
},
itemCount: data.value.length
})
const getType = (value) => {
const type = {
"green": "success",
"yellow": "warning",
"open": "success",
"close": "error",
}
return type[value] || 'error'
}
const columns = [
{
type: "selection",
},
{
title: '索引名',
key: 'index',
width: 200,
render: (row) => h(NText, {
type: 'info',
style: {cursor: 'pointer'},
onClick: () => viewIndexDocs(row)
},
{default: () => row['index']}
)
},
{title: '别名', key: 'alias', },
{
title: '健康',
key: 'health',
render: (row) => h(NTag, {type: getType(row['health'])}, {default: () => row['health']}),
},
{
title: '状态',
key: 'status',
render: (row) => h(NTag, {type: getType(row['status'])}, {default: () => row['status']}),
},
{
title: '主分片',
key: 'pri',
sorter: (a, b) => Number(a['pri']) - Number(b['pri'])
},
{
title: '副本',
key: 'rep',
sorter: (a, b) => Number(a['rep']) - Number(b['rep'])
},
{
title: '文档总数',
key: 'docs.count',
sorter: (a, b) => Number(a['docs.count']) - Number(b['docs.count'])
},
{
title: '软删除文档',
key: 'docs.deleted',
sorter: (a, b) => Number(a['docs.deleted']) - Number(b['docs.deleted'])
},
{
title: '占用存储',
key: 'store.size',
sorter: (a, b) => Number(a['store.size']) - Number(b['store.size']),
render(row) { // 这里要显示的是label,所以得转换一下
return h('span', formatBytes(row['store.size']))
}
},
{
title: '操作',
key: 'actions',
render: (row) => {
const options = [
{label: '添加文档', key: 'addDocument'},
{label: '查看索引构成', key: 'viewDetails'},
{label: '别名', key: 'viewAlias'},
{label: '查看10条文档', key: 'viewDocs'},
{label: '段合并', key: 'mergeSegments'},
{label: '删除索引', key: 'deleteIndex'},
{label: row.status === 'close' ? '打开索引' : '关闭索引', key: 'openCloseIndex'},
{label: 'Refresh', key: 'refresh'},
{label: 'Flush', key: 'flush'},
{label: '清理缓存', key: 'clearCache'},
]
return h(
NDropdown,
{
trigger: 'click',
options,
onSelect: (key) => handleMenuSelect(key, row)
},
{
default: () => h(
NButton,
{
strong: true,
secondary: true,
},
{default: () => '操作', icon: () => h(NIcon, null, {default: () => h(MoreVertFilled)})}
)
}
)
}
}
]
const handleMenuSelect = async (key, row) => {
const func = {
"addDocument": addDocument,
"viewDetails": viewIndexDetails,
"viewAlias": viewIndexAlias,
"viewDocs": viewIndexDocs,
"mergeSegments": mergeSegments,
"deleteIndex": deleteIndex,
"refresh": refreshIndex,
"openCloseIndex": openCloseIndex,
"flush": flushIndex,
"clearCache": clearCache,
}
loading.value = true
try {
await func[key](row)
} catch (e) {
message.error(e.message)
}
loading.value = false
}
// 定义各种操作函数
const addDocDrawerVisible = ref(false);
const addDocLoading = ref(false);
const docConfig = ref({
index: "",
doc: "",
});
const addDocument = async (row) => {
addDocDrawerVisible.value = true;
docConfig.value.index = row.index;
}
const addDocumentFunc = async () => {
if (!docConfig.value.doc) {
message.error("请输入文档内容")
return
}
if (!isValidJson(docConfig.value.doc)) {
message.error("文档内容不是合法的json")
return
}
addDocLoading.value = true;
try {
const res = await AddDocument(docConfig.value.index, docConfig.value.doc);
console.log(res);
if (res.err !== "") {
message.error(res.err);
} else {
message.success(`文档添加成功,id:` + res.result['_id']);
await search();
}
} catch (e) {
message.error(e.message);
} finally {
addDocLoading.value = false;
addDocDrawerVisible.value = false;
docConfig.value.index = "";
docConfig.value.doc = "";
}
}
const viewIndexDetails = async (row) => {
const res = await GetIndexInfo(row.index)
if (res.err !== "") {
message.error(res.err)
} else {
json_data.value = formattedJson(res.result)
drawer_title.value = row.index
drawerVisible.value = true
}
}
const viewIndexAlias = async (row) => {
const res = await GetIndexAliases([row.index])
if (res.err !== "") {
message.error(res.err)
} else {
json_data.value = formattedJson(res.result)
drawer_title.value = row.index
drawerVisible.value = true
}
}
const viewIndexDocs = async (row) => {
loading.value = true
try {
const res = await GetDoc10(row.index)
if (res.err !== "") {
message.error(res.err)
} else {
json_data.value = formattedJson(res.result)
drawer_title.value = row.index
drawerVisible.value = true
}
} catch (e) {
message.error(e.message)
}
loading.value = false
}
const mergeSegments = async (row) => {
dialog.info({
title: '警告',
content: `确定要对索引 ${row.index} 执行 段合并 吗?段合并非常耗资源,将提交给ES后台执行`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
await mergeSegmentsFunc(row)
}
})
}
const mergeSegmentsFunc = async (row) => {
const res = await MergeSegments(row.index)
if (res.err !== "") {
message.error(res.err)
} else {
json_data.value = formattedJson(res.result)
drawer_title.value = row.index
drawerVisible.value = true
message.success("已提交段合并请求,段合并是重IO任务,请注意集群负载")
await search()
}
}
const deleteIndex = async (row) => {
dialog.info({
title: '警告',
content: `确定要删除索引 ${row.index} 吗?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
await deleteIndexFunc(row)
}
})
}
const deleteIndexFunc = async (row) => {
const res = await DeleteIndex(row.index)
if (res.err !== "") {
message.error(res.err)
} else {
json_data.value = formattedJson(res.result)
drawer_title.value = row.index
drawerVisible.value = true
await search()
}
}
const openCloseIndex = async (row) => {
const res = await OpenCloseIndex(row.index, row.status)
if (res.err !== "") {
message.error(res.err)
} else {
json_data.value = formattedJson(res.result)
drawer_title.value = row.index
drawerVisible.value = true
await search()
}
}
const refreshIndex = async (row) => {
const res = await Refresh(row.index)
if (res.err !== "") {
message.error(res.err)
} else {
json_data.value = formattedJson(res.result)
drawer_title.value = row.index
drawerVisible.value = true
}
}
const flushIndex = async (row) => {
const res = await Flush(row.index)
if (res.err !== "") {
message.error(res.err)
} else {
json_data.value = formattedJson(res.result)
drawer_title.value = row.index
drawerVisible.value = true
}
}
const clearCache = async (row) => {
const res = await CacheClear(row.index)
if (res.err !== "") {
message.error(res.err)
} else {
json_data.value = formattedJson(res.result)
drawer_title.value = row.index
drawerVisible.value = true
await search()
}
}
const addIndexLoading = ref(false)
const addIndex = async () => {
formRef.value?.validate(async (errors) => {
if (!errors) {
// 测试mapping有的话,能不能json格式化
if (indexConfig.value.mapping) {
const err = isValidJson(indexConfig.value.mapping)
if (!err) {
message.error("mapping不是合法的json格式")
return
}
}
addIndexLoading.value = true
try {
const res = await CreateIndex(indexConfig.value.name, indexConfig.value.numberOfShards, indexConfig.value.numberOfReplicas, indexConfig.value.mapping)
if (res.err !== "") {
message.error(res.err)
} else {
message.success(`索引【${indexConfig.value.name}】创建成功`)
await search()
}
} catch (e) {
message.error(e.message)
} finally {
addIndexLoading.value = false
CreateIndexDrawerVisible.value = false
}
} else {
message.error('请填写所有必填字段')
}
})
}
// 下载所有数据的 CSV 文件
const downloadAllDataCsv = async () => {
const csvContent = createCsvContent(data.value, columns)
download_file(csvContent, '索引列表.csv', 'text/csv;charset=utf-8;')
}
const queryAlias = async () => {
loading.value = true
let name_lst = []
const start = (pagination.value.page - 1) * pagination.value.pageSize;
const end = start + pagination.value.pageSize;
let pagedData = data.value.slice(start, end);
for (const k in pagedData.value) {
name_lst.push(pagedData.value[k].index)
}
try {
const res = await GetIndexAliases(name_lst)
if (res.err !== "") {
loading.value = false
message.error(res.err)
return
}
// 合并别名缓存
// {
// "23": "xcx",
// }
aliases = {...aliases, ...res.result}
console.log(aliases)
for (const k in data.value) {
const alias = aliases[data.value[k].index]
if (alias) {
data.value[k].alias = alias
}
}
} catch (e) {
message.error(e.message)
}
loading.value = false
}
const bulk_delete = async () => {
loading.value = true
let success_count = 0
for (const Key in selectedRowKeys.value) {
const res = await DeleteIndex(selectedRowKeys.value[Key])
if (res.err !== "") {
message.error(res.err)
break
} else {
success_count += 1
}
}
// 重置
selectedRowKeys.value = []
// 提示删除了几个,失败了几个
message.success(`成功删除 ${success_count} 个索引`)
loading.value = false
await search()
}
const bulk_options = [
{
label: '批量删除',
key: 'bulk_delete',
props: {
onClick: async () => {
dialog.info({
title: '警告',
content: `确定要删除索引 ${selectedRowKeys.value} 吗?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
await bulk_delete()
}
})
}
}
},
]
</script>
<style scoped>
</style>
================================================
FILE: app/frontend/src/components/Nodes.vue
================================================
<!--
- Copyright 2025 Bronya0 <tangssst@163.com>.
- Author Github: https://github.com/Bronya0
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- https://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-->
<template>
<n-flex vertical>
<n-flex align="center">
<h2>节点</h2>
<n-button :render-icon="renderIcon(RefreshOutlined)" text @click="getData">refresh</n-button>
<n-text>共计{{ data.length }}个</n-text>
<n-button :render-icon="renderIcon(RefreshOutlined)" @click="getData">刷新</n-button>
<n-button :render-icon="renderIcon(DriveFileMoveTwotone)" @click="downloadAllDataCsv">导出为csv</n-button>
<n-text>集群名:{{cluster_name}}</n-text>
</n-flex>
<n-spin :show="loading" description="Connecting...">
<n-data-table
ref="tableRef"
:bordered="false"
:columns="refColumns(columns)"
:data="data"
:pagination="pagination"
size="small"
striped
/>
</n-spin>
</n-flex>
</template>
<script setup>
import {h, onMounted, ref} from "vue";
import emitter from "../utils/eventBus";
import {NButton, NDataTable, NFlex, NProgress, NTag, NText, NTooltip, useMessage} from 'naive-ui'
import {
createCsvContent,
download_file,
formatBytes,
formatMillis, formatMillisToDays,
formatNumber,
refColumns,
renderIcon
} from "../utils/common";
import {DriveFileMoveTwotone, RefreshOutlined} from "@vicons/material";
import {GetNodes} from "../../wailsjs/go/service/ESService";
const selectNode = async (node) => {
await getData()
}
onMounted(() => {
emitter.on('selectNode', selectNode)
getData()
})
const loading = ref(false)
const data = ref([])
const message = useMessage()
const tableRef = ref();
const cluster_name = ref("");
const getData = async () => {
loading.value = true;
const res = await GetNodes();
if (res.err !== "") {
message.error(res.err);
loading.value = false;
return;
}
cluster_name.value = res.result.cluster_name;
const nodesData = res.result.nodes || {};
const masterEligibleNodes = Object.keys(nodesData).filter(id => nodesData[id].roles?.includes('master'));
const masterNodeId = masterEligibleNodes.length > 0 ? masterEligibleNodes[0] : null;
const flattenedData = Object.values(nodesData).map(node => {
// --- 安全地访问嵌套属性 ---
const diskTotal = node.fs?.total?.total_in_bytes ?? 0;
const diskFree = node.fs?.total?.available_in_bytes ?? 0;
const diskUsedPercent = diskTotal > 0 ? Math.round(((diskTotal - diskFree) / diskTotal) * 100) : 0;
// 为每个可能缺失的嵌套对象提供一个空对象作为默认值,简化后续访问
const indices = node.indices ?? {};
const search = indices.search ?? {};
const merges = indices.merges ?? {};
const query_cache = indices.query_cache ?? {};
const fielddata = indices.fielddata ?? {};
const request_cache = indices.request_cache ?? {};
const segments = indices.segments ?? {};
const jvm = node.jvm ?? {};
const jvm_mem = jvm.mem ?? {};
const os_cpu = node.os?.cpu ?? {};
const process = node.process ?? {};
const fs_total = node.fs?.total ?? {};
const fs_io = node.fs?.io_stats?.total ?? {};
return {
// 基础信息
name: node.name,
ip: node.transport_address?.split(':')[0] ?? 'N/A',
roles: node.roles || [], // 确保 roles 是一个数组
is_master: node.name === nodesData[masterNodeId]?.name,
uptime: jvm.uptime_in_millis ?? 0,
// 指标
docs_count: indices.docs?.count ?? 0,
store_size: indices.store?.size_in_bytes ?? 0,
disk_total: diskTotal,
disk_percent: diskUsedPercent,
load_5m: os_cpu.load_average?.['5m'] ?? 0,
heap_used_in_bytes: jvm_mem.heap_used_in_bytes ?? 0,
heap_max_in_bytes: jvm_mem.heap_max_in_bytes ?? 0,
heap_percent: jvm_mem.heap_used_percent ?? 0,
segments_count: segments.count ?? 0,
// 用于 Tooltip 的原始对象
stats_search: {
"查询总数": formatNumber(search.query_total ?? 0),
"查询耗时": formatMillis(search.query_time_in_millis ?? 0),
"拉取总数": formatNumber(search.fetch_total ?? 0),
"拉取耗时": formatMillis(search.fetch_time_in_millis ?? 0),
"滚动总数": formatNumber(search.scroll_total ?? 0),
"滚动耗时": formatMillis(search.scroll_time_in_millis ?? 0),
},
stats_merges: {
"合并总数": formatNumber(merges.total ?? 0),
"合并总耗时": formatMillis(merges.total_time_in_millis ?? 0),
"合并文档总数": formatNumber(merges.total_docs ?? 0),
"合并总大小": formatBytes(merges.total_size_in_bytes ?? 0),
"当前合并数": merges.current ?? 0,
},
stats_cache: `查询缓存: ${formatBytes(query_cache.memory_size_in_bytes ?? 0)} |
Fielddata: ${formatBytes(fielddata.memory_size_in_bytes ?? 0)} |
请求缓存: ${formatBytes(request_cache.memory_size_in_bytes ?? 0)} |
段内存: ${formatBytes(segments.memory_in_bytes ?? 0)}`,
stats_cache_size: (query_cache.memory_size_in_bytes ?? 0) + (fielddata.memory_size_in_bytes ?? 0) + (request_cache.memory_size_in_bytes ?? 0)
+ (segments.memory_in_bytes ?? 0),
stats_segments: {
"段总数": formatNumber(segments.count ?? 0),
"总内存占用": formatBytes(segments.memory_in_bytes ?? 0),
"词项 (Terms)": formatBytes(segments.terms_memory_in_bytes ?? 0),
"存储字段 (Stored Fields)": formatBytes(segments.stored_fields_memory_in_bytes ?? 0),
"Doc Values": formatBytes(segments.doc_values_memory_in_bytes ?? 0),
"Norms": formatBytes(segments.norms_memory_in_bytes ?? 0),
},
stats_process: {
"打开文件描述符": `${formatNumber(process.open_file_descriptors ?? 0)} / ${formatNumber(process.max_file_descriptors ?? 0)}`,
"进程CPU使用率": `${process.cpu?.percent ?? 0}%`,
"虚拟内存占用": formatBytes(process.mem?.total_virtual_in_bytes ?? 0),
},
stats_fs: {
"总空间": formatBytes(fs_total.total_in_bytes ?? 0),
"可用空间": formatBytes(fs_total.available_in_bytes ?? 0),
"IO读操作": formatNumber(fs_io.read_operations ?? 0),
"IO写操作": formatNumber(fs_io.write_operations ?? 0),
"IO读流量": formatBytes((fs_io.read_kilobytes ?? 0) * 1024),
"IO写流量": formatBytes((fs_io.write_kilobytes ?? 0) * 1024),
}
};
});
flattenedData.sort((a, b) => (b.heap_percent ?? 0) - (a.heap_percent ?? 0));
data.value = flattenedData;
loading.value = false;
};
/**
* 创建一个通用的 Tooltip 内容 VNode
* @param {object} stats - 键值对对象
* @param {boolean} formatValueAsBytes - 是否将值格式化为字节
*/
function createTooltipContent(stats, formatValueAsBytes = false) {
return h(NFlex, { vertical: true, style: { maxWidth: '400px', textAlign: 'left'} }, () =>
Object.entries(stats).map(([key, value]) =>
h(NText, null, () => `${key}: ${formatValueAsBytes ? formatBytes(value) : value}`)
)
);
}
const getProgressType = (value) => {
const numValue = Number(value)
if (numValue < 60) return 'success'
if (numValue < 80) return 'warning'
return 'error'
}
const downloadAllDataCsv = () => {
if (!data.value || data.value.length === 0) {
message.warning("没有数据可以导出");
return;
}
// 1. 定义基础列和需要特殊处理的列
const exportColumns = [
{ title: '节点名', key: 'name' },
{ title: 'IP', key: 'ip' },
{ title: '角色', getCsvValue: (row) => row.roles.map(role => roleMap[role.charAt(0)] || role).join('/') },
{ title: 'CPU负载(5m)', key: 'load_5m' },
{ title: '运行时间', getCsvValue: (row) => formatMillisToDays(row.uptime) },
{ title: '总文档数', getCsvValue: (row) => formatNumber(row.docs_count) },
{ title: '段总数', getCsvValue: (row) => formatNumber(row.segments_count) },
{ title: '堆内存使用率(%)', key: 'heap_percent' },
{ title: '堆内存已用', getCsvValue: (row) => formatBytes(row.heap_used_in_bytes) },
{ title: '堆内存上限', getCsvValue: (row) => formatBytes(row.heap_max_in_bytes) },
{ title: '磁盘使用率(%)', key: 'disk_percent' },
{ title: '已用空间', getCsvValue: (row) => formatBytes(row.store_size) },
{ title: '总空间', getCsvValue: (row) => formatBytes(row.disk_total) },
{ title: '缓存总大小', getCsvValue: (row) => formatBytes(row.stats_cache_size) },
{
title: '缓存详情',
key: 'stats_cache',
// 清理字符串中的换行和多余空格,使其在CSV中保持单行
getCsvValue: (row) => (row.stats_cache || '').replace(/\s+/g, ' ').trim()
},
];
// 2. 动态地将所有嵌套的性能/系统指标对象平铺展开,作为新的列
const firstRow = data.value[0];
// 定义需要平铺展开的数据对象的键名和在CSV表头中使用的前缀
const statsToFlatten = {
'搜索': 'stats_search',
'合并': 'stats_merges',
'段指标': 'stats_segments',
'进程': 'stats_process',
'文件系统': 'stats_fs',
};
for (const [prefix, dataKey] of Object.entries(statsToFlatten)) {
// 确保数据源中存在这个对象
if (firstRow && typeof firstRow[dataKey] === 'object' && firstRow[dataKey] !== null) {
// 遍历对象中的每一个键(如 "查询总数", "查询耗时" 等)
for (const statKey in firstRow[dataKey]) {
exportColumns.push({
// CSV 表头,例如:"搜索 - 查询总数"
title: `${prefix} - ${statKey}`,
// 使用闭包来捕获正确的 dataKey 和 statKey,从每一行数据中提取对应的值
getCsvValue: (row) => row[dataKey]?.[statKey] ?? '',
});
}
}
}
// 3. 生成并下载 CSV 文件
try {
const csvContent = createCsvContent(data.value, exportColumns);
const fileName = `es-nodes-${cluster_name.value || 'export'}.csv`;
download_file(csvContent, fileName, 'text/csv;charset=utf-8;');
message.success("CSV 文件导出成功");
} catch (e) {
message.error("导出失败: " + e.message);
console.error("CSV export error:", e);
}
};
const pagination = ref({
page: 1,
pageSize: 10,
showSizePicker: true,
pageSizes: [5, 10, 20, 30, 40],
onChange: (page) => {
pagination.value.page = page
},
onUpdatePageSize: (pageSize) => {
pagination.value.pageSize = pageSize
pagination.value.page = 1
},
})
const roleMap = { m: '主', d: '数据', i: 'Ingest', l: 'ML', c: '协调', r: '远程', t: '转换', h: '热', w: '温', f: '冻', v: '投票' };
const columns = [
{ title: '节点名', key: 'name', width: 120,
fixed: 'left' // 可以选择固定列
},
{ title: 'IP', key: 'ip', width: 120 },
{
title: '角色', key: 'roles', width: 120,
render: (row) => row.roles.map(role => roleMap[role.charAt(0)] || role).join('/')
},
{ title: 'CPU负载', key: 'load_5m', width: 100 },
{
title: '堆内存使用率', key: 'heap_percent', width: 160,
render: (row) => h(NFlex, { vertical: true }, () => [
h(NText, null, () => `${formatBytes(row.heap_used_in_bytes)} / ${formatBytes(row.heap_max_in_bytes)}`),
h(NProgress, { type: "line", percentage: row.heap_percent, status: getProgressType(row.heap_percent), indicatorPlacement: 'inside', borderRadius: 4, })
]),
sorter: (a, b) => a.heap_percent - b.heap_percent
},
{
title: '磁盘使用率', key: 'disk_percent', width: 160,
render: (row) => h(NFlex, { vertical: true }, () => [
h(NText, null, () => `${formatBytes(row.store_size)} / ${formatBytes(row.disk_total)}`),
h(NProgress, { type: "line", percentage: row.disk_percent, status: getProgressType(row.disk_percent), indicatorPlacement: 'inside', borderRadius: 4, })
]),
sorter: (a, b) => a.disk_percent - b.disk_percent
},
{
title: '性能指标', key: 'perf', width: 160,
render: (row) => h(NFlex, null, () => [
h(NTooltip, null, { trigger: () => h(NTag, { type: 'primary' }, { default: () => '搜索' }), default: () => createTooltipContent(row.stats_search) }),
h(NTooltip, null, { trigger: () => h(NTag, { type: 'primary' }, { default: () => '合并' }), default: () => createTooltipContent(row.stats_merges) }),
h(NTooltip, null, { trigger: () => h(NTag, { type: 'primary' }, { default: () => '段' }), default: () => createTooltipContent(row.stats_segments) }),
])
},
{
title: '系统指标', key: 'sys', width: 120,
render: (row) => h(NFlex, null, () => [
h(NTooltip, null, { trigger: () => h(NTag, { type: 'info' }, { default: () => '进程' }), default: () => createTooltipContent(row.stats_process) }),
h(NTooltip, null, { trigger: () => h(NTag, { type: 'info' }, { default: () => 'FS' }), default: () => createTooltipContent(row.stats_fs) }),
])
},
{title: '缓存', key: 'cache', width: 100, render: (row) => row.stats_cache,
sorter: (a, b) => a.stats_cache_size - b.stats_cache_size
},
{title: '总文档数', key: 'docs_count', width: 100, render: (row) => formatNumber(row.docs_count),},
{ title: '段总数', key: 'segments_count', width: 100, render: (row) => formatNumber(row.segments_count)},
{ title: '运行时间', key: 'uptime', width: 90, render: (row) => formatMillisToDays(row.uptime) },
];
</script>
<style scoped>
</style>
================================================
FILE: app/frontend/src/components/Rest.vue
================================================
<!--
- Copyright 2025 Bronya0 <tangssst@163.com>.
- Author Github: https://github.com/Bronya0
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- https://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-->
<template>
<n-flex vertical>
<n-flex align="center">
<h2>REST</h2>
<n-text>一个 Restful 调试工具,支持格式化/DSL提示补全/示例/下载/历史缓存</n-text>
</n-flex>
<n-flex align="center">
<n-select v-model:value="method" :options="methodOptions" style="width: 120px;"/>
<div :id="ace_editorId" class="ace-editor" style="height:34px;min-width: 46.8%;text-align: left;
line-height: 34px;box-sizing: border-box;"/>
<n-button :loading="send_loading" :render-icon="renderIcon(SendSharp)" @click="sendRequest">Send</n-button>
<n-button :render-icon="renderIcon(HistoryOutlined)" @click="showHistoryDrawer = true">历史记录</n-button>
<n-button :render-icon="renderIcon(MenuBookTwotone)" @click="showDrawer = true">ES查询示例</n-button>
<n-button :render-icon="renderIcon(ArrowDownwardOutlined)" @click="exportJson">导出结果</n-button>
</n-flex>
<n-grid :cols="2" x-gap="20">
<n-grid-item>
<div id="json_editor" class="editarea"
style="white-space: pre-wrap; white-space-collapse: preserve; border: 0 !important;"
@paste="toTree"></div>
</n-grid-item>
<n-grid-item>
<div id="json_view" class="editarea"></div>
</n-grid-item>
</n-grid>
</n-flex>
<!-- 示例-->
<n-drawer v-model:show="showDrawer" placement="right" style="width: 38.2%">
<n-drawer-content style="text-align: left;" title="ES DSL查询示例">
<n-flex vertical>
<n-collapse>
<n-collapse-item name="1" title="1. Term查询">
<n-code :code="dslExamples.term" language="json"/>
</n-collapse-item>
<n-collapse-item name="2" title="2. Terms查询">
<n-code :code="dslExamples.terms" language="json"/>
</n-collapse-item>
<n-collapse-item name="3" title="3. Match查询">
<n-code :code="dslExamples.match" language="json"/>
</n-collapse-item>
<n-collapse-item name="4" title="4. Match Phrase查询">
<n-code :code="dslExamples.matchPhrase" language="json"/>
</n-collapse-item>
<n-collapse-item name="5" title="5. Range查询">
<n-code :code="dslExamples.range" language="json"/>
</n-collapse-item>
<n-collapse-item name="6" title="6. Bool复合查询">
<n-code :code="dslExamples.bool" language="json"/>
</n-collapse-item>
<n-collapse-item name="7" title="7. Terms Aggregation">
<n-code :code="dslExamples.termsAggs" language="json"/>
</n-collapse-item>
<n-collapse-item name="8" title="8. Date Histogram聚合">
<n-code :code="dslExamples.dateHistogram" language="json"/>
</n-collapse-item>
<n-collapse-item name="9" title="9. Nested查询">
<n-code :code="dslExamples.nested" language="json"/>
</n-collapse-item>
<n-collapse-item name="10" title="10. Exists查询">
<n-code :code="dslExamples.exists" language="json"/>
</n-collapse-item>
<n-collapse-item name="11" title="11. Multi-match查询">
<n-code :code="dslExamples.multiMatch" language="json"/>
</n-collapse-item>
<n-collapse-item name="12" title="12. Wildcard查询">
<n-code :code="dslExamples.wildcard" language="json"/>
</n-collapse-item>
<n-collapse-item name="13" title="13. Metrics聚合">
<n-code :code="dslExamples.metrics" language="json"/>
</n-collapse-item>
<n-collapse-item name="14" title="14. Cardinality聚合">
<n-code :code="dslExamples.cardinality" language="json"/>
</n-collapse-item>
<n-collapse-item name="15" title="15. Script查询">
<n-code :code="dslExamples.script" language="json"/>
</n-collapse-item>
</n-collapse>
</n-flex>
</n-drawer-content>
</n-drawer>
<!-- 历史记录抽屉 -->
<n-drawer v-model:show="showHistoryDrawer" style="width: 38.2%">
<n-drawer-content title="查询历史记录">
<!-- 搜索框 -->
<n-input
v-model:value="searchText"
clearable
placeholder="搜索历史记录,同时支持method、path、dsl"
style="margin-bottom: 12px"
>
<template #prefix>
<n-icon>
<SearchFilled/>
</n-icon>
</template>
</n-input>
<!-- 历史记录列表 -->
<n-list>
<n-pagination
v-model:page="currentPage"
:item-count="filteredHistory?.length"
:page-size="pageSize"
/>
<n-list-item v-for="item in currentPageData" :key="item.timestamp"
style="cursor: pointer;" @click="handleHistoryClick(item.method, item.path, item.dsl)">
<n-tooltip placement="left" style="max-height: 618px;overflow-y: auto" trigger="hover">
<template #trigger>
<div style="display: flex;font-size: 14px; justify-content: space-between;">
<n-tag :type="getMethodTagType(item.method)">
{{ item.method }}
</n-tag>
<n-text>{{ item.path }}</n-text>
<n-text depth="3">
{{ formatTimestamp(item.timestamp) }}
</n-text>
</div>
</template>
<n-code v-if="item.dsl !== ''" :code="formatDSL(item.dsl)" language="json" style="text-align: left;"/>
</n-tooltip>
</n-list-item>
</n-list>
</n-drawer-content>
</n-drawer>
</template>
<script setup>
import {NButton, NGrid, NGridItem, NInput, NSelect, useMessage} from 'naive-ui'
import {computed, nextTick, onMounted, ref} from "vue";
import {Search} from "../../wailsjs/go/service/ESService";
import {ArrowDownwardOutlined, HistoryOutlined, MenuBookTwotone, SearchFilled, SendSharp} from "@vicons/material";
import {formatTimestamp, renderIcon} from "../utils/common";
import {GetConfig, GetHistory, SaveHistory} from "../../wailsjs/go/config/AppConfig";
import emitter from "../utils/eventBus";
import JSONEditor from 'jsoneditor';
import '../assets/css/jsoneditor.min.css'
import 'jsoneditor/src/js/ace/theme-jsoneditor';
import 'ace-builds/src-noconflict/mode-text'
import 'ace-builds/src-noconflict/ext-language_tools'
import 'ace-builds/src-noconflict/theme-textmate'
import 'ace-builds/src-noconflict/theme-monokai'
import ace from 'ace-builds';
const message = useMessage()
const method = ref('POST')
const searchText = ref('')
const history = ref([])
const editor = ref();
const response = ref()
const send_loading = ref(false)
const showDrawer = ref(false)
const showHistoryDrawer = ref(false)
// 状态管理
const currentPage = ref(1)
const pageSize = ref(10)
const methodOptions = [
{label: 'GET', value: 'GET'},
{label: 'POST', value: 'POST'},
{label: 'PUT', value: 'PUT'},
{label: 'HEAD', value: 'HEAD'},
{label: 'PATCH', value: 'PATCH'},
{label: 'OPTIONS', value: 'OPTIONS'},
{label: 'DELETE', value: 'DELETE'}
]
// 自定义自动补全关键词
const keywords = [
{word: 'query', meta: 'keyword'}, // 查询入口
{word: 'bool', meta: 'keyword'}, // 布尔查询
{word: 'filter', meta: 'keyword'}, // 过滤条件
{word: 'must', meta: 'keyword'}, // 必须匹配
{word: 'should', meta: 'keyword'}, // 应该匹配
{word: 'must_not', meta: 'keyword'}, // 必须不匹配
{word: 'term', meta: 'keyword'}, // 精确匹配查询
{word: 'terms', meta: 'keyword'}, // 多值精确匹配查询
{word: 'match', meta: 'keyword'}, // 全文匹配查询
{word: 'match_phrase', meta: 'keyword'}, // 短语匹配查询
{word: 'multi_match', meta: 'keyword'}, // 多字段匹配查询
{word: 'range', meta: 'keyword'}, // 范围查询
{word: 'exists', meta: 'keyword'}, // 检查字段是否存在
{word: 'prefix', meta: 'keyword'}, // 前缀查询
{word: 'wildcard', meta: 'keyword'}, // 通配符查询
{word: 'regexp', meta: 'keyword'}, // 正则表达式查询
{word: 'aggs', meta: 'keyword'}, // 聚合入口
{word: 'aggregations', meta: 'keyword'}, // 聚合入口(aggs 的完整形式)
{word: 'terms', meta: 'aggregation'}, // 聚合中的 terms(按字段分组)
{word: 'sum', meta: 'aggregation'}, // 求和聚合
{word: 'avg', meta: 'aggregation'}, // 平均值聚合
{word: 'min', meta: 'aggregation'}, // 最小值聚合
{word: 'max', meta: 'aggregation'}, // 最大值聚合
{word: 'stats', meta: 'aggregation'}, // 统计聚合
{word: 'cardinality', meta: 'aggregation'}, // 去重计数聚合
{word: 'histogram', meta: 'aggregation'}, // 直方图聚合
{word: 'date_histogram', meta: 'aggregation'}, // 日期直方图聚合
{word: 'top_hits', meta: 'aggregation'}, // 返回顶部命中文档
{word: 'size', meta: 'keyword'}, // 返回结果数量
{word: 'from', meta: 'keyword'}, // 分页起始位置
{word: 'sort', meta: 'keyword'}, // 排序
{word: 'track_total_hits', meta: 'keyword'}, // 跟踪总命中数
{word: '_source', meta: 'keyword'}, // 控制返回的字段
{word: 'fields', meta: 'keyword'}, // 指定返回字段
{word: 'script', meta: 'keyword'}, // 脚本字段
{word: 'gte', meta: 'range'}, // 大于等于(范围查询)
{word: 'lte', meta: 'range'}, // 小于等于(范围查询)
{word: 'gt', meta: 'range'}, // 大于(范围查询)
{word: 'lt', meta: 'range'}, // 小于(范围查询)
{word: 'boost', meta: 'keyword'}, // 提升权重
{word: 'minimum_should_match', meta: 'keyword'}, // should 最小匹配数
{word: 'nested', meta: 'keyword'}, // 嵌套对象查询
{word: 'path', meta: 'keyword'}, // 嵌套路径
{word: 'score_mode', meta: 'keyword'}, // 嵌套查询评分模式
{word: 'bucket', meta: 'aggregation'}, // 聚合桶
{word: 'order', meta: 'keyword'}, // 排序顺序
{word: 'asc', meta: 'sort'}, // 升序
{word: 'desc', meta: 'sort'} // 降序
];
const selectNode = (node) => {
response.value.setText('{"tip": "响应结果,支持搜索"}')
send_loading.value = false
}
onMounted(async () => {
emitter.on('selectNode', selectNode)
emitter.on('update_theme', themeChange)
const loadedConfig = await GetConfig()
let theme = 'ace/theme/jsoneditor'
if (loadedConfig) {
if (loadedConfig.theme !== 'light') {
theme = 'ace/theme/monokai'
}
editor.value = new JSONEditor(document.getElementById('json_editor'), {
mode: 'code',
ace: ace,
theme: theme,
mainMenuBar: false,
statusBar: false,
showPrintMargin: false,
placeholder: '请求body'
});
response.value = new JSONEditor(document.getElementById('json_view'), {
mode: 'code',
ace: ace,
theme: theme,
mainMenuBar: false,
statusBar: false,
showPrintMargin: false,
});
editor.value.setText(null)
editor.value.aceEditor.setOptions({
enableBasicAutocompletion: true,
enableLiveAutocompletion: true
})
// 自定义补全器
const customCompleter = {
getCompletions: (editor, session, pos, prefix, callback) => {
// 根据前缀过滤关键词
const suggestions = keywords
.filter(k => k.word.startsWith(prefix))
.map(k => ({
caption: k.word,
value: k.word,
meta: k.meta
}));
callback(null, suggestions);
}
};
// 添加自定义补全器
editor.value.aceEditor.completers = [customCompleter];
response.value.setText('{"tip": "响应结果,支持搜索"}')
}
await read_history()
await nextTick()
initAce("输入rest api,以/开头;查询请用POST请求;GET不会携带body", loadedConfig.theme)
await setAceIndex()
});
// =============== ace编辑器 =================
const ace_editor = ref(null)
const ace_editorId = "ace-editor"
// 初始化 Ace 编辑器
const initAce = (defaultValue, theme) => {
ace.config.set('basePath', '/node_modules/ace-builds/src-noconflict')
ace_editor.value = ace.edit(document.getElementById(ace_editorId), {
mode: `ace/mode/text`,
theme: theme === 'light'? 'ace/theme/textmate': 'ace/theme/monokai',
placeholder: defaultValue,
fontSize: 14,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
enableSnippets: true,
showLineNumbers: false,
maxLines: 1,
minLines: 1,
showGutter: false,
showPrintMargin: false,
})
}
const getAceValue = () => {
return ace_editor.value?.getValue()
}
const setAceValue = (newValue) => {
ace_editor.value?.setValue(newValue, -1)
}
// 定义提示词数据
// const completions = [
// {
// caption: "console.log", // 用户看到的名称(可选,如果和 value 相同可以省略)
// value: "console.log(${1:value})", // 实际插入的值
// score: 100, // 权重(越高越靠前)
// meta: "JavaScript" // 分类(如 "JavaScript"、"CSS"、"Custom")
// },
// ];
const setAceCompleter = (completions) => {
const customCompleter = {
getCompletions: function (editor, session, pos, prefix, callback) {
callback(null, completions); // 返回提示词
}
};
// 添加到编辑器的补全器列表
ace_editor.value.completers = [customCompleter] // 覆盖默认补全器(不推荐)
// ace_editor.value.completers.push(customCompleter); // 追加自定义补全器(推荐)
}
// ================ ace编辑器 完结 =================
const setAceIndex = async () => {
const keywords = [
'_search', // 搜索API
'_cluster', // 集群API
'_cat', // Cat API
'_nodes', // 节点信息
'_doc', // 文档操作
'_tasks', // 任务管理
'_flush', // 刷新索引
'_refresh', // 刷新索引数据
'_mapping', // 获取/设置映射
'_settings', // 索引设置
'_stats', // 统计信息
'_bulk', // 批量操作
'_update', // 更新文档
'_msearch', // 多搜索
'_alias', // 别名操作
'_rollover', // 滚动索引
'_reindex', // 重新索引
'_snapshot', // 快照操作
'_forcemerge', // 强制合并段
'_indices', // 索引操作(补充)
'_count', // 计数API(补充)
'_validate', // 查询验证(补充)
'_explain', // 解释查询(补充)
'_field_caps', // 字段能力(补充)
'_search_shards', // 搜索分片信息(补充)
'_analyze', // 分析文本(补充)
'pretty', // 美化输出
'human', // 人类可读格式
'master_timeout', // 主节点超时
'ignore_unavailable', // 忽略不可用索引
'allow_no_indices', // 允许无索引
'expand_wildcards', // 通配符扩展
'wait_for_active_shards', // 等待活跃分片
'wait_for_completion', // 等待操作完成
'format=json', // 指定返回格式(通常是单独使用)
'size', // 返回文档数(补充)
'from', // 分页起始(补充)
'q', // 查询字符串(补充)
'scroll', // 滚动查询(补充)
'routing', // 路由值(补充)
'preference', // 查询偏好(补充)
'timeout', // 超时时间(补充)
'filter_path', // 过滤返回字段(补充)
];
let completions = [];
for (let k of keywords) {
completions.push({value: k});
}
const key = 'es_king_indexes';
const stored = localStorage.getItem(key);
if (stored) {
const values = JSON.parse(stored)
for (let v of values) {
completions.push({
value: v,
})
}
}
if (completions.length > 0) {
setAceCompleter(completions)
}
}
const read_history = async () => {
console.log("read_history")
try {
history.value = await GetHistory()
} catch (e) {
message.error(e.message)
}
}
const write_history = async () => {
console.log("write_history")
try {
// 从左侧插入history
history.value.unshift({
timestamp: Date.now(),
method: method.value,
path: getAceValue(),
dsl: editor.value.getText()
})
// 只保留100条
if (history.value.length > 100) {
history.value = history.value.slice(0, 100)
}
const res = await SaveHistory(history.value)
if (res !== "") {
message.error("保存查询失败:" + res)
}
} catch (e) {
message.error(e.message)
}
}
// 填充历史记录
function handleHistoryClick(m, p, d) {
method.value = m
setAceValue(p)
editor.value.setText(d)
showHistoryDrawer.value = false
}
function themeChange(newTheme) {
const new_editor_theme = newTheme.name === 'dark' ? 'ace/theme/monokai' : 'ace/theme/textmate'
editor.value.aceEditor.setTheme(new_editor_theme)
response.value.aceEditor.setTheme(new_editor_theme)
ace_editor.value?.setTheme(new_editor_theme)
}
const formatDSL = (dsl) => {
try {
return JSON.stringify(JSON.parse(dsl), null, 2)
} catch {
return dsl
}
}
const sendRequest = async () => {
send_loading.value = true
// 清空response
response.value.set({})
let path = getAceValue()
if (!path.startsWith('/')) {
setAceValue('/' + path);
}
try {
const res = await Search(method.value, path, editor.value.getText())
// 返回不是200也写入结果框
console.log(res)
if (res.err !== "") {
try {
response.value.set(JSON.parse(res.err))
} catch {
response.value.set(res.err)
}
} else {
response.value.set(res.result)
// 写入历史记录
await write_history()
}
} catch (e) {
message.error(e.message)
}
send_loading.value = false
}
const toTree = () => {
editor.value.format();
}
function exportJson() {
const blob = new Blob([response.value.getText()], {type: 'application/json'})
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = 'response.json'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}
// 过滤和分页逻辑
const filteredHistory = computed(() => {
if (!searchText.value) {
return history.value
} else {
return history.value.filter(item => {
return item.method.includes(searchText.value) ||
item.path.includes(searchText.value) ||
item.dsl.includes(searchText.value)
})
}
})
const currentPageData = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return filteredHistory.value.slice(start, end)
})
const getMethodTagType = (method) => {
const types = {
'GET': 'success',
'POST': 'info',
'PUT': 'warning',
'DELETE': 'error'
}
return types[method] || 'default'
}
const dslExamples = {
term: JSON.stringify({
"query": {
"term": {
"status": "active"
}
},
"size": 10,
"track_total_hits": true
}, null, 2),
terms: JSON.stringify({
"query": {
"terms": {
"user_id": [1, 2, 3, 4]
}
},
"size": 10
}, null, 2),
match: JSON.stringify({
"query": {
"match": {
"description": "quick brown fox"
}
},
"size": 20
}, null, 2),
matchPhrase: JSON.stringify({
"query": {
"match_phrase": {
"description": {
"query": "quick brown fox",
"slop": 1
}
}
}
}, null, 2),
range: JSON.stringify({
"query": {
"range": {
"age": {
"gte": 20,
"lte": 30
}
}
}
}, null, 2),
bool: JSON.stringify({
"query": {
"bool": {
"must": [
{"term": {"status": "active"}}
],
"must_not": [
{"term": {"type": "deleted"}}
],
"should": [
{"term": {"category": "electronics"}},
{"term": {"category": "computers"}}
],
"minimum_should_match": 1
}
}
}, null, 2),
termsAggs: JSON.stringify({
"aggs": {
"status_counts": {
"terms": {
"field": "status",
"missing": "N/A",
"size": 10
}
}
},
"size": 0
}, null, 2),
dateHistogram: JSON.stringify({
"aggs": {
"sales_over_time": {
"date_histogram": {
"field": "created_at",
"calendar_interval": "1d",
"format": "yyyy-MM-dd"
}
}
},
"size": 0
}, null, 2),
nested: JSON.stringify({
"query": {
"nested": {
"path": "comments",
"query": {
"bool": {
"must": [
{"match": {"comments.text": "great"}},
{"term": {"comments.rating": 5}}
]
}
}
}
}
}, null, 2),
exists: JSON.stringify({
"query": {
"exists": {
"field": "email"
}
}
}, null, 2),
multiMatch: JSON.stringify({
"query": {
"multi_match": {
"query": "quick brown fox",
"fields": ["title", "description^2"],
"type": "best_fields"
}
}
}, null, 2),
wildcard: JSON.stringify({
"query": {
"wildcard": {
"email": "*@gmail.com"
}
}
}, null, 2),
metrics: JSON.stringify({
"aggs": {
"avg_price": {"avg": {"field": "price"}},
"max_price": {"max": {"field": "price"}},
"min_price": {"min": {"field": "price"}},
"sum_quantity": {"sum": {"field": "quantity"}}
},
"size": 0
}, null, 2),
cardinality: JSON.stringify({
"aggs": {
"unique_users": {
"cardinality": {
"field": "user_id",
"precision_threshold": 100
}
}
},
"size": 0
}, null, 2),
script: JSON.stringify({
"query": {
"script_score": {
"query": {"match_all": {}},
"script": {
"source": "doc['price'].value * doc['rating'].value",
"lang": "painless"
}
}
}
}, null, 2)
}
</script>
<style>
.editarea, .json_view {
height: 72dvh;
}
/* 隐藏ace编辑器的脱离聚焦时携带的光标 */
.ace_editor:not(.ace_focus) .ace_cursor {
opacity: 0 !important;
}
/* 使主题支持placeholder */
.ace_editor .ace_placeholder {
position: absolute;
z-index: 10;
}
</style>
================================================
FILE: app/frontend/src/components/Settings.vue
================================================
<!--
- Copyright 2025 Bronya0 <tangssst@163.com>.
- Author Github: https://github.com/Bronya0
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- https://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-->
<template>
<!-- https://www.naiveui.com/zh-CN/os-theme/components/form -->
<n-flex justify="start" vertical>
<h2>设置</h2>
<n-form :model="config" label-placement="top" style="text-align: left;">
<n-form-item label="窗口宽度">
<n-input-number v-model:value="config.width" :max="1920" :min="800" :style="{ maxWidth: '120px' }"/>
</n-form-item>
<n-form-item label="窗口高度">
<n-input-number v-model:value="config.height" :max="1080" :min="600" :style="{ maxWidth: '120px' }"/>
</n-form-item>
<n-form-item label="语言">
<n-select v-model:value="config.language" :options="languageOptions" :style="{ maxWidth: '120px' }"/>
</n-form-item>
<n-form-item label="主题">
<n-switch
v-model:value="theme"
:checked-value="darkTheme.name"
:unchecked-value="lightTheme.name"
@update:value="changeTheme"
>
<template #checked-icon>
<n-icon :component="NightlightRoundFilled"/>
</template>
<template #unchecked-icon>
<n-icon :component="WbSunnyOutlined"/>
</template>
</n-switch>
</n-form-item>
<n-form-item>
<n-flex>
<n-button strong type="primary" @click="saveConfig">保存设置</n-button>
<n-tooltip>
<template #trigger>
<n-button style="width: 100px" @click="getSysInfo()">ProcessInfo</n-button>
</template>
<n-p style="white-space: pre-wrap; max-height: 400px; overflow: auto; text-align: left">{{ sys_info }}</n-p>
</n-tooltip>
</n-flex>
</n-form-item>
</n-form>
</n-flex>
</template>
<script setup>
import {onMounted, ref, shallowRef} from 'vue'
import {
darkTheme,
lightTheme,
NButton,
NForm,
NFormItem,
NInputNumber,
NSelect,
useMessage,
} from 'naive-ui'
import {WbSunnyOutlined, NightlightRoundFilled} from '@vicons/material'
import {GetConfig, SaveConfig} from '../../wailsjs/go/config/AppConfig'
import {GetProcessInfo} from '../../wailsjs/go/system/Update'
import {WindowSetSize} from "../../wailsjs/runtime";
import emitter from "../utils/eventBus";
const message = useMessage()
let theme = lightTheme.name
const sys_info = ref("")
const config = ref({
width: 1248,
height: 768,
language: 'zh-CN',
theme: theme,
})
const languageOptions = [
{label: '中文', value: 'zh-CN'},
{label: 'English', value: 'en-US'}
]
const getSysInfo = async () => {
sys_info.value = await GetProcessInfo()
}
onMounted(async () => {
console.info("初始化settings……")
// 从后端加载配置
const loadedConfig = await GetConfig()
console.log(loadedConfig)
if (loadedConfig) {
config.value = loadedConfig
theme = loadedConfig.theme
}
await getSysInfo()
})
const saveConfig = async () => {
config.value.theme = theme
const err = await SaveConfig(config.value)
if (err !== "") {
message.error("保存失败:" + err)
return
}
WindowSetSize(config.value.width, config.value.height)
emitter.emit('update_theme', theme)
// 可以添加保存成功的提示
message.success("保存成功")
config.value = await GetConfig()
}
const changeTheme = () => {
emitter.emit('update_theme', theme)
}
</script>
================================================
FILE: app/frontend/src/components/Snapshot.vue
================================================
<template>
<n-flex vertical>
<n-flex align="center">
<h2>快照管理</h2>
</n-flex>
<n-tabs type="line" animated>
<!-- ==================== Tab 1: 仓库管理 ==================== -->
<n-tab-pane name="repo" tab="仓库管理">
<n-flex vertical>
<n-flex align="center">
<n-button :render-icon="renderIcon(RefreshOutlined)" text @click="getRepos">刷新</n-button>
<n-button :render-icon="renderIcon(AddFilled)" @click="repoForm.show = true">创建仓库</n-button>
</n-flex>
<n-spin :show="repoLoading" description="加载中...">
<n-data-table
:bordered="false"
:columns="repoColumns"
:data="repoData"
:max-height="550"
size="small"
striped
/>
</n-spin>
</n-flex>
<!-- 创建仓库弹窗 -->
<n-modal v-model:show="repoForm.show" preset="dialog" title="创建快照仓库" positive-text="确认" negative-text="取消"
@positive-click="handleCreateRepo">
<n-form label-placement="left" label-width="auto">
<n-form-item label="仓库名称">
<n-input v-model:value="repoForm.name" placeholder="my_backup_repo"/>
</n-form-item>
<n-form-item label="仓库类型">
<n-select v-model:value="repoForm.type" :options="repoTypeOptions"/>
</n-form-item>
<n-form-item label="Settings (JSON)">
<n-input v-model:value="repoForm.settings" type="textarea" :autosize="{minRows: 3, maxRows: 8}"
placeholder='例如: {"location": "/mount/backups/my_backup"}' />
</n-form-item>
</n-form>
</n-modal>
</n-tab-pane>
<!-- ==================== Tab 2: 快照管理 ==================== -->
<n-tab-pane name="snapshot" tab="快照管理">
<n-flex vertical>
<n-flex align="center">
<n-button :render-icon="renderIcon(RefreshOutlined)" text @click="getSnapshots">刷新</n-button>
<n-button :render-icon="renderIcon(AddFilled)" @click="snapForm.show = true">创建快照</n-button>
</n-flex>
<n-spin :show="snapLoading" description="加载中...">
<n-data-table
:bordered="false"
:columns="snapColumns"
:data="snapData"
:max-height="550"
size="small"
striped
/>
</n-spin>
</n-flex>
<!-- 创建快照弹窗 -->
<n-modal v-model:show="snapForm.show" preset="dialog" title="创建快照" positive-text="确认" negative-text="取消"
@positive-click="handleCreateSnapshot">
<n-form label-placement="left" label-width="auto">
<n-form-item label="仓库">
<n-select v-model:value="snapForm.repository" :options="repoSelectOptions"
placeholder="选择目标仓库"/>
</n-form-item>
<n-form-item label="快照名称">
<n-input v-model:value="snapForm.snapshot" placeholder="snapshot_2026"/>
</n-form-item>
<n-form-item label="索引(逗号分隔)">
<n-input v-model:value="snapForm.indices" placeholder="留空表示所有索引"/>
</n-form-item>
<n-form-item label="包含全局状态">
<n-switch v-model:value="snapForm.includeGlobalState"/>
</n-form-item>
</n-form>
<n-text depth="3">快照将在后台异步创建,可在快照列表中查看进度。</n-text>
</n-modal>
<!-- 快照详情弹窗 -->
<n-modal v-model:show="snapDetail.show" preset="card" title="快照详情" style="width: 600px;">
<n-code :code="snapDetail.content" language="json" show-line-numbers/>
</n-modal>
</n-tab-pane>
<!-- ==================== Tab 3: 快照恢复 ==================== -->
<n-tab-pane name="restore" tab="快照恢复">
<n-flex vertical>
<n-flex align="center">
<n-button :render-icon="renderIcon(RefreshOutlined)" text @click="getRestoreStatus">刷新恢复状态
</n-button>
</n-flex>
<n-card title="恢复快照" size="small">
<n-form label-placement="left" label-width="auto">
<n-form-item label="仓库">
<n-select v-model:value="restoreForm.repository" :options="repoSelectOptions"
placeholder="选择仓库" @update:value="onRestoreRepoChange"/>
</n-form-item>
<n-form-item label="快照">
<n-select v-model:value="restoreForm.snapshot" :options="restoreSnapOptions"
placeholder="选择要恢复的快照"/>
</n-form-item>
<n-form-item label="索引(逗号分隔)">
<n-input v-model:value="restoreForm.indices" placeholder="留空表示恢复所有索引"/>
</n-form-item>
<n-form-item label="重命名模式">
<n-input v-model:value="restoreForm.renamePattern" placeholder="例如: index_(.+)"/>
</n-form-item>
<n-form-item label="重命名替换">
<n-input v-model:value="restoreForm.renameReplacement" placeholder="例如: restored_index_$1"/>
</n-form-item>
<n-form-item label="包含全局状态">
<n-switch v-model:value="restoreForm.includeGlobalState"/>
</n-form-item>
</n-form>
<n-flex>
<n-button type="warning" @click="handleRestore">执行恢复</n-button>
</n-flex>
<n-text depth="3">
警告:恢复操作会在集群中创建索引,如果同名索引已存在且未关闭,恢复将失败。建议使用重命名功能避免冲突。
</n-text>
</n-card>
<n-card title="恢复进度" size="small" style="margin-top: 12px;">
<n-spin :show="restoreStatusLoading">
<n-data-table
:bordered="false"
:columns="restoreColumns"
:data="restoreStatusData"
:max-height="300"
size="small"
striped
/>
</n-spin>
</n-card>
</n-flex>
</n-tab-pane>
<!-- ==================== Tab 4: 自动策略 (SLM) ==================== -->
<n-tab-pane name="slm" tab="自动策略 (SLM)">
<n-flex vertical>
<n-flex align="center">
<n-button :render-icon="renderIcon(RefreshOutlined)" text @click="getSLMPolicies">刷新</n-button>
<n-button :render-icon="renderIcon(AddFilled)" @click="slmForm.show = true">创建策略</n-button>
</n-flex>
<n-spin :show="slmLoading" description="加载中...">
<n-data-table
:bordered="false"
:columns="slmColumns"
:data="slmData"
:max-height="550"
size="small"
striped
/>
</n-spin>
</n-flex>
<!-- 创建SLM策略弹窗 -->
<n-modal v-model:show="slmForm.show" preset="dialog" title="创建自动快照策略" positive-text="确认"
negative-text="取消" @positive-click="handleCreateSLM" style="width: 520px;">
<n-form label-placement="left" label-width="auto">
<n-form-item label="策略ID">
<n-input v-model:value="slmForm.policyId" placeholder="daily-snapshots"/>
</n-form-item>
<n-form-item label="快照名称模板">
<n-input v-model:value="slmForm.name" placeholder="<daily-snap-{now/d}>"/>
</n-form-item>
<n-form-item label="Cron 调度">
<n-input v-model:value="slmForm.schedule" placeholder="0 30 1 * * ?(每天凌晨1:30)"/>
</n-form-item>
<n-form-item label="仓库">
<n-select v-model:value="slmForm.repository" :options="repoSelectOptions"
placeholder="目标仓库"/>
</n-form-item>
<n-form-item label="索引(逗号分隔)">
<n-input v-model:value="slmForm.indices" placeholder="留空表示所有索引"/>
</n-form-item>
<n-form-item label="过期时间">
<n-input v-model:value="slmForm.expireAfter" placeholder="例如: 30d"/>
</n-form-item>
<n-form-item label="最少保留数">
<n-input-number v-model:value="slmForm.minCount" :min="0" placeholder="5"/>
</n-form-item>
<n-form-item label="最多保留数">
<n-input-number v-model:value="slmForm.maxCount" :min="0" placeholder="50"/>
</n-form-item>
</n-form>
</n-modal>
</n-tab-pane>
</n-tabs>
</n-flex>
</template>
<script setup>
import {computed, h, onMounted, ref} from "vue";
import emitter from "../utils/eventBus";
import {NButton, NTag, useDialog, useMessage} from 'naive-ui'
import {renderIcon} from "../utils/common";
import {RefreshOutlined, AddFilled, DeleteOutlined, PlayArrowFilled, VisibilityOutlined, VerifiedOutlined} from "@vicons/material";
import {
GetSnapshots, GetSnapshotRepositories, CreateSnapshotRepository, DeleteSnapshotRepository,
VerifySnapshotRepository, CreateSnapshot, DeleteSnapshot, GetSnapshotDetail,
RestoreSnapshot, GetSnapshotRestoreStatus,
GetSLMPolicies, CreateSLMPolicy, DeleteSLMPolicy, ExecuteSLMPolicy,
} from "../../wailsjs/go/service/ESService";
const message = useMessage()
const dialog = useDialog()
// ==================== 仓库管理 ====================
const repoLoading = ref(false)
const repoData = ref([])
const repoForm = ref({
show: false,
name: '',
type: 'fs',
settings: '',
})
const repoTypeOptions = [
{label: 'fs(共享文件系统)', value: 'fs'},
{label: 's3(AWS S3)', value: 's3'},
{label: 'hdfs(Hadoop HDFS)', value: 'hdfs'},
{label: 'azure(Azure Blob)', value: 'azure'},
{label: 'gcs(Google Cloud Storage)', value: 'gcs'},
{label: 'url(只读URL)', value: 'url'},
]
const repoSelectOptions = computed(() => {
return repoData.value.map(r => ({label: r.name, value: r.name}))
})
const getRepos = async () => {
repoLoading.value = true
const res = await GetSnapshotRepositories()
if (res.err !== "") {
message.error(res.err)
} else {
repoData.value = res.results || []
}
repoLoading.value = false
}
const handleCreateRepo = async () => {
const res = await CreateSnapshotRepository(repoForm.value.name, repoForm.value.type, repoForm.value.settings)
if (res.err !== "") {
message.error(res.err)
return false
}
message.success("仓库创建成功")
repoForm.value = {show: false, name: '', type: 'fs', settings: ''}
await getRepos()
}
const handleDeleteRepo = (name) => {
dialog.warning({
title: '确认删除仓库',
content: `确定要删除快照仓库「${name}」吗?这不会删除仓库中已有的快照数据文件,但会从ES集群中移除此仓库的注册信息。`,
positiveText: '确认删除',
negativeText: '取消',
onPositiveClick: async () => {
const res = await DeleteSnapshotRepository(name)
if (res.err !== "") {
message.error(res.err)
} else {
message.success("仓库已删除")
await getRepos()
}
}
})
}
const handleVerifyRepo = async (name) => {
const res = await VerifySnapshotRepository(name)
if (res.err !== "") {
message.error("验证失败: " + res.err)
} else {
message.success("仓库验证通过,所有节点可访问")
}
}
const repoColumns = [
{title: '仓库名称', key: 'name', width: 180},
{title: '类型', key: 'type', width: 100},
{
title: 'Settings',
key: 'settings',
render: (row) => row.settings ? JSON.stringify(row.settings) : '-',
},
{
title: '操作',
key: 'actions',
width: 180,
render: (row) => h('div', {style: 'display: flex; gap: 8px;'}, [
h(NButton, {size: 'small', quaternary: true, type: 'info', onClick: () => handleVerifyRepo(row.name)},
{default: () => '验证'}),
h(NButton, {size: 'small', quaternary: true, type: 'error', onClick: () => handleDeleteRepo(row.name)},
{default: () => '删除'}),
])
}
]
// ==================== 快照管理 ====================
const snapLoading = ref(false)
const snapData = ref([])
const snapForm = ref({
show: false,
repository: null,
snapshot: '',
indices: '',
includeGlobalState: false,
})
const snapDetail = ref({
show: false,
content: '',
})
const getSnapshots = async () => {
snapLoading.value = true
const res = await GetSnapshots()
if (res.err !== "") {
message.error(res.err)
} else {
snapData.value = (res.results || []).sort((a, b) => {
return (b.start_time || '').localeCompare(a.start_time || '')
})
}
snapLoading.value = false
}
const handleCreateSnapshot = async () => {
const res = await CreateSnapshot(
snapForm.value.repository,
snapForm.value.snapshot,
snapForm.value.indices,
snapForm.value.includeGlobalState
)
if (res.err !== "") {
message.error(res.err)
return false
}
message.success("快照开始创建(异步),请稍后刷新查看状态")
snapForm.value = {show: false, repository: null, snapshot: '', indices: '', includeGlobalState: false}
await getSnapshots()
}
const handleDeleteSnapshot = (repository, snapshot) => {
dialog.warning({
title: '确认删除快照',
content: `确定要删除仓库「${repository}」中的快照「${snapshot}」吗?此操作不可逆。`,
positiveText: '确认删除',
negativeText: '取消',
onPositiveClick: async () => {
const res = await DeleteSnapshot(repository, snapshot)
if (res.err !== "") {
message.error(res.err)
} else {
message.success("快照已删除")
await getSnapshots()
}
}
})
}
const handleViewDetail = async (repository, snapshot) => {
const res = await GetSnapshotDetail(repository, snapshot)
if (res.err !== "") {
message.error(res.err)
} else {
snapDetail.value.content = JSON.stringify(res.result, null, 2)
snapDetail.value.show = true
}
}
const snapColumns = [
{title: '快照名称', key: 'snapshot', width: 160},
{title: '仓库', key: 'repository', width: 130},
{
title: '状态', key: 'state', width: 110,
render: (row) => h(NTag, {
size: 'small',
type
gitextract_q841ds1i/ ├── .github/ │ └── workflows/ │ └── build.yaml ├── .gitignore ├── LICENSE ├── app/ │ ├── app.go │ ├── backend/ │ │ ├── common/ │ │ │ └── vars.go │ │ ├── config/ │ │ │ └── app.go │ │ ├── service/ │ │ │ └── es.go │ │ ├── system/ │ │ │ └── update.go │ │ └── types/ │ │ └── resp.go │ ├── build/ │ │ ├── README.md │ │ ├── darwin/ │ │ │ ├── Info.dev.plist │ │ │ └── Info.plist │ │ └── windows/ │ │ ├── info.json │ │ ├── installer/ │ │ │ ├── project.nsi │ │ │ └── wails_tools.nsh │ │ └── wails.exe.manifest │ ├── dev.bat │ ├── frontend/ │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.vue │ │ │ ├── assets/ │ │ │ │ └── fonts/ │ │ │ │ └── OFL.txt │ │ │ ├── components/ │ │ │ │ ├── About.vue │ │ │ │ ├── Aside.vue │ │ │ │ ├── Conn.vue │ │ │ │ ├── Core.vue │ │ │ │ ├── Header.vue │ │ │ │ ├── Health.vue │ │ │ │ ├── Index.vue │ │ │ │ ├── Nodes.vue │ │ │ │ ├── Rest.vue │ │ │ │ ├── Settings.vue │ │ │ │ ├── Snapshot.vue │ │ │ │ └── Task.vue │ │ │ ├── main.js │ │ │ ├── style.css │ │ │ └── utils/ │ │ │ ├── common.js │ │ │ └── eventBus.js │ │ └── vite.config.js │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── wails.json ├── readme-en.md └── readme.md
SYMBOL INDEX (113 symbols across 8 files)
FILE: app/app.go
type App (line 32) | type App struct
method Start (line 42) | func (a *App) Start(ctx context.Context) {
method domReady (line 48) | func (a *App) domReady(ctx context.Context) {
method beforeClose (line 71) | func (a *App) beforeClose(ctx context.Context) (prevent bool) {
method shutdown (line 76) | func (a *App) shutdown(ctx context.Context) {
method Greet (line 81) | func (a *App) Greet(name string) string {
function NewApp (line 37) | func NewApp() *App {
FILE: app/backend/common/vars.go
constant AppName (line 28) | AppName = "ES-King"
constant Width (line 29) | Width = 1600
constant Height (line 30) | Height = 870
constant Theme (line 31) | Theme = "dark"
constant ConfigDir (line 32) | ConfigDir = ".es-king"
constant ConfigPath (line 33) | ConfigPath = "config.yaml"
constant HistoryPath (line 34) | HistoryPath = "history.yaml"
constant ErrLogPath (line 35) | ErrLogPath = "error.log"
constant Language (line 36) | Language = "zh-CN"
constant PingUrl (line 37) | PingUrl = "https://ysboke.cn/api/kingTool/ping"
FILE: app/backend/config/app.go
type AppConfig (line 33) | type AppConfig struct
method Start (line 38) | func (a *AppConfig) Start(ctx context.Context) {
method GetConfig (line 42) | func (a *AppConfig) GetConfig() *types.Config {
method SaveConfig (line 62) | func (a *AppConfig) SaveConfig(config *types.Config) string {
method SaveTheme (line 79) | func (a *AppConfig) SaveTheme(theme string) string {
method getConfigPath (line 96) | func (a *AppConfig) getConfigPath() string {
method GetHistory (line 114) | func (a *AppConfig) GetHistory() []*types.History {
method SaveHistory (line 127) | func (a *AppConfig) SaveHistory(histories []types.History) string {
method getHistoryPath (line 141) | func (a *AppConfig) getHistoryPath() string {
method GetVersion (line 160) | func (a *AppConfig) GetVersion() string {
method GetAppName (line 164) | func (a *AppConfig) GetAppName() string {
method OpenFileDialog (line 168) | func (a *AppConfig) OpenFileDialog(options runtime.OpenDialogOptions) ...
method LogErrToFile (line 172) | func (a *AppConfig) LogErrToFile(message string) {
FILE: app/backend/service/es.go
constant FORMAT (line 38) | FORMAT = "?format=json&pretty"
constant StatsApi (line 39) | StatsApi = "/_cluster/stats" + FORMAT
constant AddDoc (line 40) | AddDoc = "/_doc"
constant HealthApi (line 41) | HealthApi = "/_cluster/health"
constant NodesApi (line 42) | NodesApi = "/_nodes/stats/indices,os,fs,process,jvm"
constant AllIndexApi (line 43) | AllIndexApi = "/_cat/indices?format=json&pretty&bytes=b"
constant ClusterSettings (line 44) | ClusterSettings = "/_cluster/settings"
constant ForceMerge (line 45) | ForceMerge = "/_forcemerge?wait_for_completion=false"
constant REFRESH (line 46) | REFRESH = "/_refresh"
constant FLUSH (line 47) | FLUSH = "/_flush"
constant CacheClear (line 48) | CacheClear = "/_cache/clear"
constant TasksApi (line 49) | TasksApi = "/_tasks" + FORMAT
constant CancelTasksApi (line 50) | CancelTasksApi = "/_tasks/%s/_cancel"
type ESService (line 53) | type ESService struct
method SetConnect (line 86) | func (es *ESService) SetConnect(key, host, username, password, CACert ...
method TestClient (line 107) | func (es *ESService) TestClient(host, username, password, CACert strin...
method AddDocument (line 126) | func (es *ESService) AddDocument(index, doc string) *types.ResultResp {
method GetNodes (line 144) | func (es *ESService) GetNodes() *types.ResultResp {
method GetHealth (line 159) | func (es *ESService) GetHealth() *types.ResultResp {
method GetStats (line 174) | func (es *ESService) GetStats() *types.ResultResp {
method GetIndexes (line 190) | func (es *ESService) GetIndexes(name string) *types.ResultsResp {
method CreateIndex (line 213) | func (es *ESService) CreateIndex(name string, numberOfShards, numberOf...
method GetIndexInfo (line 245) | func (es *ESService) GetIndexInfo(indexName string) *types.ResultResp {
method DeleteIndex (line 261) | func (es *ESService) DeleteIndex(indexName string) *types.ResultResp {
method OpenCloseIndex (line 277) | func (es *ESService) OpenCloseIndex(indexName, now string) *types.Resu...
method GetIndexMappings (line 300) | func (es *ESService) GetIndexMappings(indexName string) *types.ResultR...
method MergeSegments (line 317) | func (es *ESService) MergeSegments(indexName string) *types.ResultResp {
method Refresh (line 334) | func (es *ESService) Refresh(indexName string) *types.ResultResp {
method Flush (line 350) | func (es *ESService) Flush(indexName string) *types.ResultResp {
method CacheClear (line 366) | func (es *ESService) CacheClear(indexName string) *types.ResultResp {
method GetDoc10 (line 383) | func (es *ESService) GetDoc10(indexName string) *types.ResultResp {
method Search (line 413) | func (es *ESService) Search(method, path string, body any) *types.Resu...
method GetClusterSettings (line 433) | func (es *ESService) GetClusterSettings() *types.ResultResp {
method GetIndexSettings (line 449) | func (es *ESService) GetIndexSettings(indexName string) *types.ResultR...
method GetIndexAliases (line 465) | func (es *ESService) GetIndexAliases(indexNameList []string) *types.Re...
method GetIndexSegments (line 499) | func (es *ESService) GetIndexSegments(indexName string) *types.ResultR...
method GetTasks (line 515) | func (es *ESService) GetTasks() *types.ResultsResp {
method CancelTasks (line 568) | func (es *ESService) CancelTasks(taskID string) *types.ResultResp {
method GetSnapshots (line 588) | func (es *ESService) GetSnapshots() *types.ResultsResp {
method GetSnapshotRepositories (line 657) | func (es *ESService) GetSnapshotRepositories() *types.ResultsResp {
method CreateSnapshotRepository (line 692) | func (es *ESService) CreateSnapshotRepository(name, repoType, settings...
method DeleteSnapshotRepository (line 727) | func (es *ESService) DeleteSnapshotRepository(name string) *types.Resu...
method VerifySnapshotRepository (line 749) | func (es *ESService) VerifySnapshotRepository(name string) *types.Resu...
method CreateSnapshot (line 771) | func (es *ESService) CreateSnapshot(repository, snapshot, indices stri...
method DeleteSnapshot (line 801) | func (es *ESService) DeleteSnapshot(repository, snapshot string) *type...
method GetSnapshotDetail (line 823) | func (es *ESService) GetSnapshotDetail(repository, snapshot string) *t...
method RestoreSnapshot (line 845) | func (es *ESService) RestoreSnapshot(repository, snapshot, indices, re...
method GetSnapshotRestoreStatus (line 881) | func (es *ESService) GetSnapshotRestoreStatus() *types.ResultResp {
method GetSLMPolicies (line 900) | func (es *ESService) GetSLMPolicies() *types.ResultsResp {
method CreateSLMPolicy (line 931) | func (es *ESService) CreateSLMPolicy(policyId, name, schedule, reposit...
method DeleteSLMPolicy (line 981) | func (es *ESService) DeleteSLMPolicy(policyId string) *types.ResultResp {
method ExecuteSLMPolicy (line 1003) | func (es *ESService) ExecuteSLMPolicy(policyId string) *types.ResultRe...
method DownloadESIndex (line 1038) | func (es *ESService) DownloadESIndex(index string, queryDSL string, fi...
function NewESService (line 59) | func NewESService() *ESService {
function ConfigureSSL (line 70) | func ConfigureSSL(UseSSL, SkipSSLVerify bool, client *resty.Client, CACe...
type SearchResponse (line 1025) | type SearchResponse struct
FILE: app/backend/system/update.go
type Update (line 32) | type Update struct
method Start (line 36) | func (obj *Update) Start(ctx context.Context) {
method CheckUpdate (line 39) | func (obj *Update) CheckUpdate() *types.Tag {
method GetProcessInfo (line 50) | func (obj *Update) GetProcessInfo() string {
FILE: app/backend/types/resp.go
type Tag (line 20) | type Tag struct
type Config (line 25) | type Config struct
type History (line 32) | type History struct
type ResultsResp (line 38) | type ResultsResp struct
type ResultResp (line 42) | type ResultResp struct
type Connect (line 46) | type Connect struct
type H (line 56) | type H
FILE: app/frontend/src/utils/common.js
function renderIcon (line 24) | function renderIcon(icon) {
function openUrl (line 29) | function openUrl(url) {
function flattenObject (line 34) | function flattenObject(obj, parentKey = '') {
function formattedJson (line 55) | function formattedJson(value) {
function isValidJson (line 61) | function isValidJson(jsonString) {
function formatBytes (line 73) | function formatBytes(bytes, decimals = 2) {
function createCsvContent (line 88) | function createCsvContent(allData, columns) {
function download_file (line 109) | function download_file(content, fileName, type) {
function formatDate (line 124) | function formatDate(date) {
function formatTimestamp (line 134) | function formatTimestamp(timestamp) {
function refColumns (line 148) | function refColumns(columns) {
function calculateWidthByTitle (line 191) | function calculateWidthByTitle(title) {
function formatMillis (line 202) | function formatMillis(ms) {
function formatMillisToDays (line 217) | function formatMillisToDays(ms){
function formatNumber (line 226) | function formatNumber(num) {
FILE: app/main.go
function main (line 44) | func main() {
Condensed preview — 44 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (270K chars).
[
{
"path": ".github/workflows/build.yaml",
"chars": 4829,
"preview": "name: Wails build\n\non:\n release:\n types: [ created ]\n\nenv:\n NODE_OPTIONS: \"--max-old-space-size=4096\"\n APP_NAME: '"
},
{
"path": ".gitignore",
"chars": 123,
"preview": "build/bin\n.vscode\n.idea\n/app/frontend/node_modules\n/app/frontend/wailsjs\n/app/frontend/dist\n/app/frontend/package.json.m"
},
{
"path": "LICENSE",
"chars": 11357,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "app/app.go",
"chars": 2246,
"preview": "/*\n * Copyright 2025 Bronya0 <tangssst@163.com>.\n * Author Github: https://github.com/Bronya0\n *\n * Licensed under the A"
},
{
"path": "app/backend/common/vars.go",
"chars": 1543,
"preview": "/*\n * Copyright 2025 Bronya0 <tangssst@163.com>.\n * Author Github: https://github.com/Bronya0\n *\n * Licensed under the A"
},
{
"path": "app/backend/config/app.go",
"chars": 4414,
"preview": "/*\n * Copyright 2025 Bronya0 <tangssst@163.com>.\n * Author Github: https://github.com/Bronya0\n *\n * Licensed under the A"
},
{
"path": "app/backend/service/es.go",
"chars": 30968,
"preview": "/*\n * Copyright 2025 Bronya0 <tangssst@163.com>.\n * Author Github: https://github.com/Bronya0\n *\n * Licensed under the A"
},
{
"path": "app/backend/system/update.go",
"chars": 3247,
"preview": "/*\n * Copyright 2025 Bronya0 <tangssst@163.com>.\n * Author Github: https://github.com/Bronya0\n *\n * Licensed under the A"
},
{
"path": "app/backend/types/resp.go",
"chars": 1667,
"preview": "/*\n * Copyright 2025 Bronya0 <tangssst@163.com>.\n * Author Github: https://github.com/Bronya0\n *\n * Licensed under the A"
},
{
"path": "app/build/README.md",
"chars": 1591,
"preview": "# Build Directory\n\nThe build directory is used to house all the build files and assets for your application. \n\nThe struc"
},
{
"path": "app/build/darwin/Info.dev.plist",
"chars": 2308,
"preview": "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1"
},
{
"path": "app/build/darwin/Info.plist",
"chars": 2177,
"preview": "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1"
},
{
"path": "app/build/windows/info.json",
"chars": 356,
"preview": "{\n\t\"fixed\": {\n\t\t\"file_version\": \"{{.Info.ProductVersion}}\"\n\t},\n\t\"info\": {\n\t\t\"0000\": {\n\t\t\t\"ProductVersion\": \"{{.Info.Prod"
},
{
"path": "app/build/windows/installer/project.nsi",
"chars": 4862,
"preview": "Unicode true\n\n####\n## Please note: Template replacements don't work in this file. They are provided with default defines"
},
{
"path": "app/build/windows/installer/wails_tools.nsh",
"chars": 7553,
"preview": "# DO NOT EDIT - Generated automatically by `wails build`\n\n!include \"x64.nsh\"\n!include \"WinVer.nsh\"\n!include \"FileFunc.ns"
},
{
"path": "app/build/windows/wails.exe.manifest",
"chars": 1036,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<assembly manifestVersion=\"1.0\" xmlns=\"urn:schemas-microsoft-com"
},
{
"path": "app/dev.bat",
"chars": 21,
"preview": "chcp 65001\n\nwails dev"
},
{
"path": "app/frontend/index.html",
"chars": 1012,
"preview": "<!--\n ~ Copyright 2025 Bronya0 <tangssst@163.com>.\n ~ Author Github: https://github.com/Bronya0\n ~\n ~ Licensed under"
},
{
"path": "app/frontend/package.json",
"chars": 516,
"preview": "{\n \"name\": \"app\",\n \"version\": \"1.0.0\",\n \"description\": \"\",\n \"main\": \"\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build"
},
{
"path": "app/frontend/src/App.vue",
"chars": 5740,
"preview": "<!--\n - Copyright 2025 Bronya0 <tangssst@163.com>.\n - Author Github: https://github.com/Bronya0\n -\n - Licensed under"
},
{
"path": "app/frontend/src/assets/fonts/OFL.txt",
"chars": 4371,
"preview": "Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),\n\nThis Font Software is licensed under the SIL Open F"
},
{
"path": "app/frontend/src/components/About.vue",
"chars": 2147,
"preview": "<!--\n - Copyright 2025 Bronya0 <tangssst@163.com>.\n - Author Github: https://github.com/Bronya0\n -\n - Licensed under"
},
{
"path": "app/frontend/src/components/Aside.vue",
"chars": 1161,
"preview": "<!--\n - Copyright 2025 Bronya0 <tangssst@163.com>.\n - Author Github: https://github.com/Bronya0\n -\n - Licensed under"
},
{
"path": "app/frontend/src/components/Conn.vue",
"chars": 8013,
"preview": "<!--\n - Copyright 2025 Bronya0 <tangssst@163.com>.\n - Author Github: https://github.com/Bronya0\n -\n - Licensed under"
},
{
"path": "app/frontend/src/components/Core.vue",
"chars": 9442,
"preview": "<!--\n - Copyright 2025 Bronya0 <tangssst@163.com>.\n - Author Github: https://github.com/Bronya0\n -\n - Licensed under"
},
{
"path": "app/frontend/src/components/Header.vue",
"chars": 6674,
"preview": "<!--\n - Copyright 2025 Bronya0 <tangssst@163.com>.\n - Author Github: https://github.com/Bronya0\n -\n - Licensed under"
},
{
"path": "app/frontend/src/components/Health.vue",
"chars": 3652,
"preview": "<!--\n - Copyright 2025 Bronya0 <tangssst@163.com>.\n - Author Github: https://github.com/Bronya0\n -\n - Licensed under"
},
{
"path": "app/frontend/src/components/Index.vue",
"chars": 20040,
"preview": "<!--\n - Copyright 2025 Bronya0 <tangssst@163.com>.\n - Author Github: https://github.com/Bronya0\n -\n - Licensed under"
},
{
"path": "app/frontend/src/components/Nodes.vue",
"chars": 12949,
"preview": "<!--\n - Copyright 2025 Bronya0 <tangssst@163.com>.\n - Author Github: https://github.com/Bronya0\n -\n - Licensed under"
},
{
"path": "app/frontend/src/components/Rest.vue",
"chars": 22005,
"preview": "<!--\n - Copyright 2025 Bronya0 <tangssst@163.com>.\n - Author Github: https://github.com/Bronya0\n -\n - Licensed under"
},
{
"path": "app/frontend/src/components/Settings.vue",
"chars": 3903,
"preview": "<!--\n - Copyright 2025 Bronya0 <tangssst@163.com>.\n - Author Github: https://github.com/Bronya0\n -\n - Licensed under"
},
{
"path": "app/frontend/src/components/Snapshot.vue",
"chars": 22050,
"preview": "<template>\n <n-flex vertical>\n <n-flex align=\"center\">\n <h2>快照管理</h2>\n </n-flex>\n <n-tabs type=\"line\" ani"
},
{
"path": "app/frontend/src/components/Task.vue",
"chars": 6246,
"preview": "<!--\n - Copyright 2025 Bronya0 <tangssst@163.com>.\n - Author Github: https://github.com/Bronya0\n -\n - Licensed under"
},
{
"path": "app/frontend/src/main.js",
"chars": 808,
"preview": "/*\n * Copyright 2025 Bronya0 <tangssst@163.com>.\n * Author Github: https://github.com/Bronya0\n *\n * Licensed under the A"
},
{
"path": "app/frontend/src/style.css",
"chars": 1240,
"preview": "/*\n * Copyright 2025 Bronya0 <tangssst@163.com>.\n * Author Github: https://github.com/Bronya0\n *\n * Licensed under the A"
},
{
"path": "app/frontend/src/utils/common.js",
"chars": 6491,
"preview": "/*\n * Copyright 2025 Bronya0 <tangssst@163.com>.\n * Author Github: https://github.com/Bronya0\n *\n * Licensed under the A"
},
{
"path": "app/frontend/src/utils/eventBus.js",
"chars": 742,
"preview": "/*\n * Copyright 2025 Bronya0 <tangssst@163.com>.\n * Author Github: https://github.com/Bronya0\n *\n * Licensed under the A"
},
{
"path": "app/frontend/vite.config.js",
"chars": 845,
"preview": "/*\n * Copyright 2025 Bronya0 <tangssst@163.com>.\n * Author Github: https://github.com/Bronya0\n *\n * Licensed under the A"
},
{
"path": "app/go.mod",
"chars": 1553,
"preview": "module app\n\ngo 1.22.0\n\ntoolchain go1.24.0\n\nrequire (\n\tgithub.com/go-resty/resty/v2 v2.16.2\n\tgithub.com/wailsapp/wails/v2"
},
{
"path": "app/go.sum",
"chars": 8166,
"preview": "github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=\ngithub.com/bep/debounce v1.2.1/go.mod h1:"
},
{
"path": "app/main.go",
"chars": 3336,
"preview": "/*\n * Copyright 2025 Bronya0 <tangssst@163.com>.\n * Author Github: https://github.com/Bronya0\n *\n * Licensed under the A"
},
{
"path": "app/wails.json",
"chars": 467,
"preview": "{\n \"name\": \"ES-King\",\n \"outputfilename\": \"ES-King\",\n \"frontend:install\": \"npm install\",\n \"frontend:build\": \"npm run "
},
{
"path": "readme-en.md",
"chars": 2988,
"preview": "\n\n<h1 align=\"center\">ES-King </h1>\n<h4 align=\"center\"><strong>简体中文</strong> | <a href=\"https://githu"
},
{
"path": "readme.md",
"chars": 2419,
"preview": "<p align=\"center\">\n <img src=\"app/build/appicon.png\" alt=\"图片标题\" width=\"200\">\n</p>\n<h1 align=\"center\">ES-King </h1>\n<h4 "
}
]
About this extraction
This page contains the full source code of the Bronya0/ES-King GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 44 files (233.7 KB), approximately 70.0k tokens, and a symbol index with 113 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.