灾容datakeep项目推送
This commit is contained in:
14
frontend/Dockerfile
Normal file
14
frontend/Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
||||
# build stage
|
||||
FROM node:18-alpine AS build
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# run stage
|
||||
FROM nginx:1.25-alpine
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
|
||||
16
frontend/nginx.conf
Normal file
16
frontend/nginx.conf
Normal file
@@ -0,0 +1,16 @@
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://backend:8000/api/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
|
||||
20
frontend/package.json
Normal file
20
frontend/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "datakeep-ui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
"core-js": "^3.8.3",
|
||||
"element-ui": "^2.15.14",
|
||||
"vue": "^2.6.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-service": "~5.0.0",
|
||||
"vue-template-compiler": "^2.6.14"
|
||||
}
|
||||
}
|
||||
|
||||
216
frontend/src/App.vue
Normal file
216
frontend/src/App.vue
Normal file
@@ -0,0 +1,216 @@
|
||||
<template>
|
||||
<div id="app" style="padding: 20px; background-color: #f0f2f5; min-height: 100vh;">
|
||||
<el-card>
|
||||
<div slot="header" style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span style="font-size: 20px; font-weight: bold;">DataKeep 容灾备份系统</span>
|
||||
<el-button type="primary" icon="el-icon-plus" @click="showCreate">新增备份实例</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="instances" style="width: 100%">
|
||||
<el-table-column prop="id" label="实例ID" width="120"></el-table-column>
|
||||
<el-table-column prop="name" label="名称" width="180"></el-table-column>
|
||||
<el-table-column label="同步时间" width="150">
|
||||
<template slot-scope="scope">
|
||||
每天 {{ scope.row.sync_hour }}:00
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.enabled ? 'success' : 'info'">{{ scope.row.enabled ? '启用' : '禁用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="success" @click="runInstance(scope.row.id)">立即同步</el-button>
|
||||
<el-button size="mini" @click="showEdit(scope.row)">编辑</el-button>
|
||||
<el-button size="mini" type="info" @click="viewHistory(scope.row.id)">历史</el-button>
|
||||
<el-button size="mini" type="danger" @click="deleteInstance(scope.row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 编辑/新增对话框 -->
|
||||
<el-dialog :title="isEdit ? '编辑实例' : '新增实例'" :visible.sync="dialogVisible" width="70%">
|
||||
<el-form :model="form" label-width="120px" size="small">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="实例ID"><el-input v-model="form.id" :disabled="isEdit"></el-input></el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="名称"><el-input v-model="form.name"></el-input></el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="每天同步时间">
|
||||
<el-select v-model="form.sync_hour" placeholder="选择小时" style="width: 100%">
|
||||
<el-option v-for="h in 24" :key="h-1" :label="`${h-1}:00`" :value="h-1"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="是否启用">
|
||||
<el-switch v-model="form.enabled"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-tabs type="border-card" style="margin-top: 20px;">
|
||||
<el-tab-pane label="MySQL (必选)">
|
||||
<el-divider content-position="left">生产端 (源)</el-divider>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="16"><el-form-item label="Host"><el-input v-model="form.prod_mysql.host"></el-input></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="Port"><el-input-number v-model="form.prod_mysql.port" :controls="false" style="width: 100%"></el-input-number></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="8"><el-form-item label="User"><el-input v-model="form.prod_mysql.user"></el-input></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="Pass"><el-input v-model="form.prod_mysql.password" show-password></el-input></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="DB"><el-input v-model="form.prod_mysql.database"></el-input></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-divider content-position="left">备份端 (目标)</el-divider>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="16"><el-form-item label="Host"><el-input v-model="form.dr_mysql.host" placeholder="容器名: dr-mysql"></el-input></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="Port"><el-input-number v-model="form.dr_mysql.port" :controls="false" style="width: 100%"></el-input-number></el-form-item></el-col>
|
||||
</el-row>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="Redis (可选)">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-divider content-position="left">生产端 (源)</el-divider>
|
||||
<el-form-item label="Host"><el-input v-model="form.prod_redis.host"></el-input></el-form-item>
|
||||
<el-form-item label="Port"><el-input-number v-model="form.prod_redis.port" :controls="false" style="width: 100%"></el-input-number></el-form-item>
|
||||
<el-form-item label="Pass"><el-input v-model="form.prod_redis.password" show-password></el-input></el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-divider content-position="left">备份端 (目标)</el-divider>
|
||||
<el-form-item label="Host"><el-input v-model="form.dr_redis.host" placeholder="容器名: dr-redis"></el-input></el-form-item>
|
||||
<el-form-item label="Port"><el-input-number v-model="form.dr_redis.port" :controls="false" style="width: 100%"></el-input-number></el-form-item>
|
||||
<el-form-item label="Pass"><el-input v-model="form.dr_redis.password" show-password></el-input></el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="MinIO (可选)">
|
||||
<el-divider content-position="left">连接配置</el-divider>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12"><el-form-item label="生产端 Endpoint"><el-input v-model="form.prod_minio.endpoint"></el-input></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="备份端 Endpoint"><el-input v-model="form.dr_minio.endpoint" placeholder="容器名: dr-minio:9000"></el-input></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12"><el-form-item label="Buckets (逗号分隔)"><el-input v-model="bucketsStr" placeholder="bucket1,bucket2"></el-input></el-form-item></el-col>
|
||||
</el-row>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-form>
|
||||
<div slot="footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 历史记录 -->
|
||||
<el-drawer title="同步历史记录" :visible.sync="historyVisible" size="50%">
|
||||
<div style="padding: 20px;">
|
||||
<el-timeline>
|
||||
<el-timeline-item v-for="run in runHistory" :key="run.id" :timestamp="run.started_at" :type="run.ok ? 'success' : 'danger'">
|
||||
<el-card>
|
||||
<h4>结果: {{ run.ok ? '成功' : '失败' }} (耗时: {{ calcDuration(run) }})</h4>
|
||||
<div v-for="step in run.steps" :key="step.name">
|
||||
<p><b>{{ step.name }}:</b> {{ step.ok ? 'OK' : 'ERROR: ' + step.error }} {{ step.count ? '(数量: ' + step.count + ')' : '' }}</p>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
instances: [],
|
||||
dialogVisible: false,
|
||||
historyVisible: false,
|
||||
isEdit: false,
|
||||
runHistory: [],
|
||||
bucketsStr: '',
|
||||
form: {
|
||||
id: '', name: '', enabled: true, sync_hour: 2,
|
||||
prod_mysql: { host: '', port: 3306, user: '', password: '', database: '' },
|
||||
dr_mysql: { host: '', port: 3306, user: '', password: '', database: '' },
|
||||
prod_redis: { host: '', port: 6379, password: '' },
|
||||
dr_redis: { host: '', port: 6379, password: '' },
|
||||
prod_minio: { endpoint: '', access_key: '', secret_key: '', secure: false },
|
||||
dr_minio: { endpoint: '', access_key: '', secret_key: '', secure: false },
|
||||
minio_buckets: []
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() { this.refresh(); },
|
||||
methods: {
|
||||
async refresh() {
|
||||
const res = await this.$http.get('/instances');
|
||||
this.instances = res.data;
|
||||
},
|
||||
showCreate() {
|
||||
this.isEdit = false;
|
||||
this.resetForm();
|
||||
this.dialogVisible = true;
|
||||
},
|
||||
showEdit(row) {
|
||||
this.isEdit = true;
|
||||
this.form = JSON.parse(JSON.stringify(row));
|
||||
this.bucketsStr = (this.form.minio_buckets || []).join(',');
|
||||
this.dialogVisible = true;
|
||||
},
|
||||
resetForm() {
|
||||
this.form = {
|
||||
id: '', name: '', enabled: true, sync_hour: 2,
|
||||
prod_mysql: { host: '', port: 3306, user: '', password: '', database: '' },
|
||||
dr_mysql: { host: '', port: 3306, user: '', password: '', database: '' },
|
||||
prod_redis: { host: '', port: 6379, password: '' },
|
||||
dr_redis: { host: '', port: 6379, password: '' },
|
||||
prod_minio: { endpoint: '', access_key: '', secret_key: '', secure: false },
|
||||
dr_minio: { endpoint: '', access_key: '', secret_key: '', secure: false },
|
||||
minio_buckets: []
|
||||
};
|
||||
this.bucketsStr = '';
|
||||
},
|
||||
async submitForm() {
|
||||
this.form.minio_buckets = this.bucketsStr ? this.bucketsStr.split(',').map(s => s.trim()) : [];
|
||||
if (this.isEdit) {
|
||||
await this.$http.put(`/instances/${this.form.id}`, this.form);
|
||||
} else {
|
||||
await this.$http.post('/instances', this.form);
|
||||
}
|
||||
this.dialogVisible = false;
|
||||
this.refresh();
|
||||
this.$message.success('保存成功');
|
||||
},
|
||||
async runInstance(id) {
|
||||
await this.$http.post(`/instances/${id}/run`);
|
||||
this.$message.success('已触发同步任务');
|
||||
},
|
||||
async deleteInstance(id) {
|
||||
await this.$confirm('确定删除此实例吗?');
|
||||
await this.$http.delete(`/instances/${id}`);
|
||||
this.refresh();
|
||||
},
|
||||
async viewHistory(id) {
|
||||
const res = await this.$http.get(`/instances/${id}/runs`);
|
||||
this.runHistory = res.data;
|
||||
this.historyVisible = true;
|
||||
},
|
||||
calcDuration(run) {
|
||||
if (!run.finished_at) return '进行中';
|
||||
const d = new Date(run.finished_at) - new Date(run.started_at);
|
||||
return Math.round(d / 1000) + 's';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
21
frontend/src/main.js
Normal file
21
frontend/src/main.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import ElementUI from 'element-ui'
|
||||
import 'element-ui/lib/theme-chalk/index.css'
|
||||
import axios from 'axios'
|
||||
|
||||
Vue.use(ElementUI)
|
||||
Vue.config.productionTip = false
|
||||
|
||||
// 创建 axios 实例
|
||||
const http = axios.create({
|
||||
baseURL: process.env.VUE_APP_API_BASE || '/api',
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
Vue.prototype.$http = http
|
||||
|
||||
new Vue({
|
||||
render: h => h(App),
|
||||
}).$mount('#app')
|
||||
|
||||
12
frontend/vue.config.js
Normal file
12
frontend/vue.config.js
Normal file
@@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
devServer: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
},
|
||||
transpileDependencies: true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user