show changes feature

This commit is contained in:
Alireza Ahmadi
2024-06-08 17:49:03 +02:00
parent 1b9d5e9378
commit 12fe21906e
12 changed files with 254 additions and 6 deletions
+6
View File
@@ -157,6 +157,12 @@ func (a *APIHandler) getHandler(c *gin.Context) {
level := c.Query("l")
logs := a.ServerService.GetLogs(service, count, level)
jsonObj(c, logs, nil)
case "changes":
actor := c.Query("a")
chngKey := c.Query("k")
count := c.Query("c")
changes := a.ConfigService.GetChanges(actor, chngKey, count)
jsonObj(c, changes, nil)
default:
jsonMsg(c, "API call", nil)
}
+12
View File
@@ -28,6 +28,13 @@ func migrateDb() {
}()
fmt.Println("Start migrating database...")
err = migrateClientSchema(tx)
if err != nil {
log.Fatal(err)
}
err = changesObj(tx)
if err != nil {
log.Fatal(err)
}
fmt.Println("Migration done!")
}
@@ -81,5 +88,10 @@ func migrateClientSchema(db *gorm.DB) error {
}
}
}
db.AutoMigrate(model.Client{})
return nil
}
func changesObj(db *gorm.DB) error {
return db.Exec("UPDATE changes SET obj = CAST('\"' || CAST(obj AS TEXT) || '\"' AS BLOB) WHERE actor = ? and obj not like ?", "DepleteJob", "\"%\"").Error
}
+1 -1
View File
@@ -70,7 +70,7 @@ func (s *ClientService) DepleteClients() ([]string, []string, error) {
Actor: "DepleteJob",
Key: "clients",
Action: "disable",
Obj: json.RawMessage(client.Name),
Obj: json.RawMessage("\"" + client.Name + "\""),
})
}
+19
View File
@@ -6,6 +6,7 @@ import (
"s-ui/config"
"s-ui/database"
"s-ui/database/model"
"s-ui/logger"
"s-ui/singbox"
"strconv"
"time"
@@ -342,3 +343,21 @@ func (s *ConfigService) contains(slice []string, item string) bool {
}
return false
}
func (s *ConfigService) GetChanges(actor string, chngKey string, count string) []model.Changes {
c, _ := strconv.Atoi(count)
whereString := "`id`>0"
if len(actor) > 0 {
whereString += " and `actor`='" + actor + "'"
}
if len(chngKey) > 0 {
whereString += " and `key`='" + chngKey + "'"
}
db := database.GetDB()
var chngs []model.Changes
err := db.Model(model.Changes{}).Where(whereString).Order("`id` desc").Limit(c).Scan(&chngs).Error
if err != nil {
logger.Warning(err)
}
return chngs
}
+140
View File
@@ -0,0 +1,140 @@
<template>
<v-dialog transition="dialog-bottom-transition" width="90%" max-width="800" :loading="loading">
<v-card class="rounded-lg">
<v-card-title>
<v-row>
<v-col>{{ $t('admin.changes') }}</v-col>
<v-spacer></v-spacer>
<v-col cols="auto">
<v-icon icon="mdi-close" @click="$emit('close')" />
</v-col>
</v-row>
</v-card-title>
<v-divider></v-divider>
<v-card-text>
<v-row>
<v-col cols="12" sm="4" md="3">
<v-select
hide-details
:label="$t('admin.actor')"
:items="['', 'DepleteJob', ...admins]"
v-model="user"
@update:model-value="loadData">
</v-select>
</v-col>
<v-col cols="12" sm="4" md="3">
<v-select
hide-details
:label="$t('admin.key')"
:items="['', 'inbounds', 'outbounds', 'clients', 'route', 'tls', 'experimental']"
v-model="key"
@update:model-value="loadData">
</v-select>
</v-col>
<v-col cols="6" sm="4" md="3">
<v-select
hide-details
:label="$t('count')"
:items="[10,20,30,50,100]"
v-model.number="chngCount"
@update:model-value="loadData">
</v-select>
</v-col>
<v-col cols="auto" align="center" justify="center">
<v-btn
icon="mdi-refresh"
variant="tonal"
:loading="loading"
@click="loadData">
<v-icon />
</v-btn>
</v-col>
</v-row>
<v-data-table
:headers="changesHeaders"
:items="changes"
item-value="id"
density="compact"
show-expand
items-per-page="10"
>
<template v-slot:item.dateTime="{ value }">
<v-chip variant="text" dir="ltr" density="compact">
{{ dateFormatted(value) }}
</v-chip>
</template>
<template v-slot:item.action="{ value }">
<v-chip density="compact">
{{ $t('actions.' + value) }}
</v-chip>
</template>
<template v-slot:expanded-row="{ columns, item }">
<tr>
<td :colspan="columns.length">
<v-card dir="ltr" v-if="item.index>0">Index: {{ item.index }}</v-card>
<v-card style="background-color: background" dir="ltr"><pre>{{ item.obj }}</pre></v-card>
</td>
</tr>
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-dialog>
</template>
<script lang="ts">
import { i18n } from '@/locales'
import HttpUtils from '@/plugins/httputil'
export default {
props: ['admins', 'actor', 'visible'],
data() {
return {
loading: false,
changes: <any[]>[],
user: '',
key: '',
chngCount: 10,
expanded: [],
changesHeaders: [
{ title: 'ID', key: 'id' },
{ title: i18n.global.t('admin.date') + '-' + i18n.global.t('admin.time'), key: 'dateTime' },
{ title: i18n.global.t('admin.actor'), key: 'Actor' },
{ title: i18n.global.t('admin.key'), key: 'key' },
{ title: i18n.global.t('admin.action'), key: 'action' },
],
}
},
methods: {
async loadData() {
this.loading = true
const data = await HttpUtils.get('api/changes',{ a: this.user, k: this.key, c: this.chngCount })
if (data.success) {
this.changes = data.obj?? []
this.loading = false
}
},
dateFormatted(dt: number): string {
const date = new Date(dt*1000)
return date.toLocaleString(this.locale)
},
},
computed: {
locale() {
const l = i18n.global.locale.value
return l.replace('zh', 'zh-')
},
},
watch: {
visible(newValue) {
this.changes = []
this.user = this.$props.actor
this.key = ''
this.chngCount = 10
if (newValue) {
this.loadData()
}
},
},
}
</script>
+8
View File
@@ -23,6 +23,7 @@ export default {
version: "Version",
email: "Email",
commaSeparated: "(comma separated)",
count: "Count",
error: {
dplData: "Duplicate Data",
},
@@ -85,11 +86,14 @@ export default {
actions: {
action: "Action",
add: "Add",
new: "Add",
edit: "Edit",
del: "Delete",
save: "Save",
update: "Update",
submit: "Submit",
set: "Set",
disable: "Disable",
close: "Close",
restartApp: "Restart App",
},
@@ -111,6 +115,10 @@ export default {
lastLogin: "Last login",
date: "Date",
time: "Time",
changes: "Changes",
actor: "Actor",
key: "Key",
action: "Action",
},
setting: {
interface: "Interface",
+8
View File
@@ -23,6 +23,7 @@ export default {
version: "نسخه",
email: "ایمیل",
commaSeparated: "(جداشده با کاما)",
count: "تعداد",
error: {
dplData: "داده تکراری",
},
@@ -84,11 +85,14 @@ export default {
actions: {
action: "فرمان",
add: "ایجاد",
new: "ایجاد",
edit: "ویرایش",
del: "حذف",
save: "ذخیره",
update: "بروزرسانی",
submit: "ارسال",
set: "تنظیم",
disable: "غیرفعال",
close: "بستن",
restartApp: "ریستارت پنل",
},
@@ -110,6 +114,10 @@ export default {
lastLogin: "آخرین ورود",
date: "تاریخ",
time: "ساعت",
changes: "تغییرات",
actor: "مجری",
key: "کلید",
action: "عمل",
},
setting: {
interface: "نما",
+8
View File
@@ -23,6 +23,7 @@ export default {
version: "Phiên bản",
email: "Email",
commaSeparated: "(được phân tách bằng dấu phẩy)",
count: "Đếm",
error: {
dplData: "Dữ liệu trùng lặp",
},
@@ -85,11 +86,14 @@ export default {
actions: {
action: "Hành động",
add: "Thêm",
new: "Thêm",
edit: "Chỉnh sửa",
del: "Xóa",
save: "Lưu",
update: "Cập nhật",
submit: "Gửi",
set: "Đặt",
disable: "Vô hiệu hóa",
close: "Đóng",
restartApp: "Khởi động lại ứng dụng",
},
@@ -111,6 +115,10 @@ export default {
lastLogin: "Lân đăng nhập cuôi",
date: "Ngày",
time: "Thời gian",
changes: "Thay đổi",
actor: "Diễn viên",
key: "Khóa",
action: "Hành động",
},
setting: {
interface: "Giao diện",
+8
View File
@@ -23,6 +23,7 @@ export default {
version: "版本",
email: "电子邮件",
commaSeparated: "(逗号分隔)",
count: "计数",
error: {
dplData: "重复数据",
},
@@ -85,11 +86,14 @@ export default {
actions: {
action: "操作",
add: "添加",
new: "添加",
edit: "编辑",
del: "删除",
save: "保存",
update: "更新",
submit: "提交",
set: "设置",
disable: "禁用",
close: "关闭",
restartApp: "重启面板",
},
@@ -111,6 +115,10 @@ export default {
lastLogin: "上次登录",
date: "日期",
time: "时间",
changes: "更改",
actor: "执行者",
key: "键",
action: "操作",
},
setting: {
interface: "界面",
+8
View File
@@ -24,6 +24,7 @@ export default {
version: "版本",
email: "電子郵件",
commaSeparated: "(逗號分隔)",
count: "計數",
error: {
dplData: "重複數據",
},
@@ -86,11 +87,14 @@ export default {
actions: {
action: "操作",
add: "添加",
new: "添加",
edit: "編輯",
del: "刪除",
save: "保存",
update: "更新",
submit: "提交",
set: "設置",
disable: "禁用",
close: "關閉",
restartApp: "重啟面板",
},
@@ -112,6 +116,10 @@ export default {
lastLogin: "上次登入",
date: "日期",
time: "時間",
changes: "更改",
actor: "執行者",
key: "鍵",
action: "操作",
},
setting: {
interface: "界面",
+2 -2
View File
@@ -9,7 +9,7 @@ import '@mdi/font/css/materialdesignicons.css'
import 'vuetify/styles'
import colors from 'vuetify/util/colors'
import { fa, en } from 'vuetify/locale'
import { fa, en, vi, zhHans as zhcn, zhHant as zhtw } from 'vuetify/locale'
// Composables
import { createVuetify } from 'vuetify'
@@ -53,6 +53,6 @@ export default createVuetify({
locale: {
locale: localStorage.getItem("locale") ?? 'en',
fallback: 'en',
messages: { en, fa },
messages: { en, fa, vi, zhcn, zhtw },
},
})
+34 -3
View File
@@ -6,6 +6,18 @@
@close="closeEditModal"
@save="saveEditModal"
/>
<ChngModal
v-model="changesModal.visible"
:visible="changesModal.visible"
:admins="users.map((u:any) => u.username)"
:actor="changesModal.actor"
@close="closeChangesModal"
/>
<v-row>
<v-col cols="12" justify="center" align="center">
<v-btn color="primary" @click="showChangesModal('')">{{ $t('admin.changes') }}</v-btn>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="4" md="3" lg="2" v-for="(item, index) in <any[]>users" :key="item.id">
<v-card rounded="xl" elevation="5" min-width="200" :title="item.username">
@@ -38,6 +50,10 @@
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('actions.edit')"></v-tooltip>
</v-btn>
<v-btn icon="mdi-list-box-outline" @click="showChangesModal(item.username)">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('admin.changes')"></v-tooltip>
</v-btn>
</v-card-actions>
</v-card>
</v-col>
@@ -45,9 +61,10 @@
</template>
<script lang="ts" setup>
import AdminModal from '@/layouts/modals/Admin.vue';
import HttpUtils from '@/plugins/httputil';
import { Ref, ref, inject, onMounted } from 'vue';
import AdminModal from '@/layouts/modals/Admin.vue'
import ChngModal from '@/layouts/modals/Changes.vue'
import HttpUtils from '@/plugins/httputil'
import { Ref, ref, inject, onMounted } from 'vue'
const loading:Ref = inject('loading')?? ref(false)
@@ -75,6 +92,7 @@ const showEditModal = (user: any) => {
}
const closeEditModal = () => {
editModal.value.visible = false
editModal.value.user = {}
}
const saveEditModal = async (data:any) => {
loading.value=true
@@ -88,4 +106,17 @@ const saveEditModal = async (data:any) => {
loading.value=false
}
}
const changesModal = ref({
visible: false,
actor: '',
})
const showChangesModal = (actor: string) => {
changesModal.value.actor = actor
changesModal.value.visible = true
}
const closeChangesModal = () => {
changesModal.value.visible = false
changesModal.value.actor = ''
}
</script>