feat(site): save sync node ids

This commit is contained in:
Jacky 2024-10-25 18:10:45 +08:00
parent aa556767f2
commit 6c137e5229
No known key found for this signature in database
GPG key ID: 215C21B10DF38B4D
21 changed files with 381 additions and 327 deletions

0
.db
View file

View file

@ -6,6 +6,7 @@ func InitRouter(r *gin.RouterGroup) {
r.GET("domains", GetSiteList) r.GET("domains", GetSiteList)
r.GET("domains/:name", GetSite) r.GET("domains/:name", GetSite)
r.POST("domains/:name", SaveSite) r.POST("domains/:name", SaveSite)
r.PUT("domains", BatchUpdateSites)
r.POST("domains/:name/enable", EnableSite) r.POST("domains/:name/enable", EnableSite)
r.POST("domains/:name/disable", DisableSite) r.POST("domains/:name/disable", DisableSite)
r.POST("domains/:name/advance", DomainEditByAdvancedMode) r.POST("domains/:name/advance", DomainEditByAdvancedMode)

View file

@ -9,7 +9,9 @@ import (
"github.com/0xJacky/Nginx-UI/query" "github.com/0xJacky/Nginx-UI/query"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sashabaranov/go-openai" "github.com/sashabaranov/go-openai"
"github.com/uozi-tech/cosy"
"github.com/uozi-tech/cosy/logger" "github.com/uozi-tech/cosy/logger"
"gorm.io/gorm/clause"
"net/http" "net/http"
"os" "os"
) )
@ -125,10 +127,11 @@ func SaveSite(c *gin.Context) {
} }
var json struct { var json struct {
Name string `json:"name" binding:"required"` Name string `json:"name" binding:"required"`
Content string `json:"content" binding:"required"` Content string `json:"content" binding:"required"`
SiteCategoryID uint64 `json:"site_category_id"` SiteCategoryID uint64 `json:"site_category_id"`
Overwrite bool `json:"overwrite"` SyncNodeIDs []uint64 `json:"sync_node_ids"`
Overwrite bool `json:"overwrite"`
} }
if !api.BindAndValid(c, &json) { if !api.BindAndValid(c, &json) {
@ -152,7 +155,12 @@ func SaveSite(c *gin.Context) {
enabledConfigFilePath := nginx.GetConfPath("sites-enabled", name) enabledConfigFilePath := nginx.GetConfPath("sites-enabled", name)
s := query.Site s := query.Site
_, err = s.Where(s.Path.Eq(path)).Update(s.SiteCategoryID, json.SiteCategoryID) _, err = s.Where(s.Path.Eq(path)).
Select(s.SiteCategoryID, s.SyncNodeIDs).
Updates(&model.Site{
SiteCategoryID: json.SiteCategoryID,
SyncNodeIDs: json.SyncNodeIDs,
})
if err != nil { if err != nil {
api.ErrHandler(c, err) api.ErrHandler(c, err)
return return
@ -336,3 +344,29 @@ func DeleteSite(c *gin.Context) {
"message": "ok", "message": "ok",
}) })
} }
func BatchUpdateSites(c *gin.Context) {
cosy.Core[model.Site](c).SetValidRules(gin.H{
"site_category_id": "required",
}).SetItemKey("path").
BeforeExecuteHook(func(ctx *cosy.Ctx[model.Site]) {
effectedPath := make([]string, len(ctx.BatchEffectedIDs))
var sites []*model.Site
for i, name := range ctx.BatchEffectedIDs {
path := nginx.GetConfPath("sites-available", name)
effectedPath[i] = path
sites = append(sites, &model.Site{
Path: path,
})
}
s := query.Site
err := s.Clauses(clause.OnConflict{
DoNothing: true,
}).Create(sites...)
if err != nil {
ctx.AbortWithError(err)
return
}
ctx.BatchEffectedIDs = effectedPath
}).BatchModify()
}

View file

@ -1,95 +1,95 @@
export default { export default {
globals: { "globals": {
$gettext: true, "$gettext": true,
$ngettext: true, "$ngettext": true,
$npgettext: true, "$npgettext": true,
$pgettext: true, "$pgettext": true,
Component: true, "Component": true,
ComponentPublicInstance: true, "ComponentPublicInstance": true,
ComputedRef: true, "ComputedRef": true,
DirectiveBinding: true, "DirectiveBinding": true,
EffectScope: true, "EffectScope": true,
ExtractDefaultPropTypes: true, "ExtractDefaultPropTypes": true,
ExtractPropTypes: true, "ExtractPropTypes": true,
ExtractPublicPropTypes: true, "ExtractPublicPropTypes": true,
InjectionKey: true, "InjectionKey": true,
MaybeRef: true, "MaybeRef": true,
MaybeRefOrGetter: true, "MaybeRefOrGetter": true,
PropType: true, "PropType": true,
Ref: true, "Ref": true,
VNode: true, "VNode": true,
WritableComputedRef: true, "WritableComputedRef": true,
acceptHMRUpdate: true, "acceptHMRUpdate": true,
computed: true, "computed": true,
createApp: true, "createApp": true,
createPinia: true, "createPinia": true,
customRef: true, "customRef": true,
defineAsyncComponent: true, "defineAsyncComponent": true,
defineComponent: true, "defineComponent": true,
defineStore: true, "defineStore": true,
effectScope: true, "effectScope": true,
getActivePinia: true, "getActivePinia": true,
getCurrentInstance: true, "getCurrentInstance": true,
getCurrentScope: true, "getCurrentScope": true,
h: true, "h": true,
inject: true, "inject": true,
isProxy: true, "isProxy": true,
isReactive: true, "isReactive": true,
isReadonly: true, "isReadonly": true,
isRef: true, "isRef": true,
mapActions: true, "mapActions": true,
mapGetters: true, "mapGetters": true,
mapState: true, "mapState": true,
mapStores: true, "mapStores": true,
mapWritableState: true, "mapWritableState": true,
markRaw: true, "markRaw": true,
nextTick: true, "nextTick": true,
onActivated: true, "onActivated": true,
onBeforeMount: true, "onBeforeMount": true,
onBeforeRouteLeave: true, "onBeforeRouteLeave": true,
onBeforeRouteUpdate: true, "onBeforeRouteUpdate": true,
onBeforeUnmount: true, "onBeforeUnmount": true,
onBeforeUpdate: true, "onBeforeUpdate": true,
onDeactivated: true, "onDeactivated": true,
onErrorCaptured: true, "onErrorCaptured": true,
onMounted: true, "onMounted": true,
onRenderTracked: true, "onRenderTracked": true,
onRenderTriggered: true, "onRenderTriggered": true,
onScopeDispose: true, "onScopeDispose": true,
onServerPrefetch: true, "onServerPrefetch": true,
onUnmounted: true, "onUnmounted": true,
onUpdated: true, "onUpdated": true,
onWatcherCleanup: true, "onWatcherCleanup": true,
provide: true, "provide": true,
reactive: true, "reactive": true,
readonly: true, "readonly": true,
ref: true, "ref": true,
resolveComponent: true, "resolveComponent": true,
setActivePinia: true, "setActivePinia": true,
setMapStoreSuffix: true, "setMapStoreSuffix": true,
shallowReactive: true, "shallowReactive": true,
shallowReadonly: true, "shallowReadonly": true,
shallowRef: true, "shallowRef": true,
storeToRefs: true, "storeToRefs": true,
toRaw: true, "toRaw": true,
toRef: true, "toRef": true,
toRefs: true, "toRefs": true,
toValue: true, "toValue": true,
triggerRef: true, "triggerRef": true,
unref: true, "unref": true,
useAttrs: true, "useAttrs": true,
useCssModule: true, "useCssModule": true,
useCssVars: true, "useCssVars": true,
useId: true, "useId": true,
useLink: true, "useLink": true,
useModel: true, "useModel": true,
useRoute: true, "useRoute": true,
useRouter: true, "useRouter": true,
useSlots: true, "useSlots": true,
useTemplateRef: true, "useTemplateRef": true,
watch: true, "watch": true,
watchEffect: true, "watchEffect": true,
watchPostEffect: true, "watchPostEffect": true,
watchSyncEffect: true, "watchSyncEffect": true
}, }
} }

2
app/components.d.ts vendored
View file

@ -49,6 +49,8 @@ declare module 'vue' {
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm'] APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
APopover: typeof import('ant-design-vue/es')['Popover'] APopover: typeof import('ant-design-vue/es')['Popover']
AProgress: typeof import('ant-design-vue/es')['Progress'] AProgress: typeof import('ant-design-vue/es')['Progress']
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
AResult: typeof import('ant-design-vue/es')['Result'] AResult: typeof import('ant-design-vue/es')['Result']
ARow: typeof import('ant-design-vue/es')['Row'] ARow: typeof import('ant-design-vue/es')['Row']
ASelect: typeof import('ant-design-vue/es')['Select'] ASelect: typeof import('ant-design-vue/es')['Select']

View file

@ -5,7 +5,7 @@ import autoImport from './.eslint-auto-import.mjs'
export default createConfig( export default createConfig(
{ {
stylistic: true, stylistic: true,
ignores: ['**/version.json', 'tsconfig.json', 'tsconfig.node.json'], ignores: ['**/version.json', 'tsconfig.json', 'tsconfig.node.json', '.eslint-auto-import.mjs'],
languageOptions: { languageOptions: {
globals: autoImport.globals, globals: autoImport.globals,
}, },

View file

@ -19,6 +19,7 @@ export interface Site {
cert_info?: Record<number, CertificateInfo[]> cert_info?: Record<number, CertificateInfo[]>
site_category_id: number site_category_id: number
site_category?: SiteCategory site_category?: SiteCategory
sync_node_ids: number[]
} }
export interface AutoCertRequest { export interface AutoCertRequest {

View file

@ -1,48 +1,60 @@
<script setup lang="ts"> <script setup lang="ts">
import type Curd from '@/api/curd'
import type { Column } from '@/components/StdDesign/types'
import { getPithyColumns } from '@/components/StdDesign/StdDataDisplay/methods/columns'
import StdDataEntry from '@/components/StdDesign/StdDataEntry' import StdDataEntry from '@/components/StdDesign/StdDataEntry'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
const props = defineProps<{ const props = defineProps<{
// eslint-disable-next-line ts/no-explicit-any // eslint-disable-next-line ts/no-explicit-any
api: (ids: number[], data: any) => Promise<void> api: Curd<any>
beforeSave?: () => Promise<void> beforeSave?: () => Promise<void>
columns: Column[]
}>() }>()
const emit = defineEmits(['onSave']) const emit = defineEmits(['save'])
const batchColumns = ref([]) const batchColumns = ref<Column[]>([])
const selectedRowKeys = ref<(number | string)[]>([])
// eslint-disable-next-line ts/no-explicit-any
const selectedRows = ref<any[]>([])
const visible = ref(false) const visible = ref(false)
const data = ref({})
const error = ref({})
const loading = ref(false)
const selectedRowKeys = ref([])
// eslint-disable-next-line ts/no-explicit-any // eslint-disable-next-line ts/no-explicit-any
function showModal(c: any, rowKeys: any) { function showModal(c: Column[], rowKeys: (number | string)[], rows: any[]) {
data.value = {}
visible.value = true visible.value = true
selectedRowKeys.value = rowKeys selectedRowKeys.value = rowKeys
batchColumns.value = c batchColumns.value = c
selectedRows.value = rows
} }
defineExpose({ defineExpose({
showModal, showModal,
}) })
const data = reactive({})
const error = reactive({})
const loading = ref(false)
async function ok() { async function ok() {
loading.value = true loading.value = true
await props.beforeSave?.() await props.beforeSave?.()
await props.api(selectedRowKeys.value, data).then(async () => { await props.api.batch_save(selectedRowKeys.value, data.value)
message.success($gettext('Save successfully')) .then(async () => {
emit('onSave') message.success($gettext('Save successfully'))
}).catch(e => { emit('save')
message.error($gettext(e?.message) ?? $gettext('Server error')) visible.value = false
}).finally(() => { })
loading.value = false .catch(e => {
}) error.value = e.errors
message.error($gettext(e?.message) ?? $gettext('Server error'))
})
.finally(() => {
loading.value = false
})
} }
</script> </script>
@ -52,23 +64,31 @@ async function ok() {
class="std-curd-edit-modal" class="std-curd-edit-modal"
:mask="false" :mask="false"
:title="$gettext('Batch Modify')" :title="$gettext('Batch Modify')"
:cancel-text="$gettext('Cancel')" :cancel-text="$gettext('No')"
:ok-text="$gettext('OK')" :ok-text="$gettext('Save')"
:confirm-loading="loading" :confirm-loading="loading"
:width="600" :width="600"
destroy-on-close destroy-on-close
@ok="ok" @ok="ok"
> >
<p>{{ $gettext('Belows are selected items that you want to batch modify') }}</p>
<ATable
class="mb-4"
size="small"
:columns="getPithyColumns(columns)"
:data-source="selectedRows"
:pagination="{ showSizeChanger: false, pageSize: 5, size: 'small' }"
/>
<p>{{ $gettext('Leave blank if do not want to modify') }}</p>
<StdDataEntry <StdDataEntry
:data-list="batchColumns" :data-list="batchColumns"
:data-source="data" :data-source="data"
:error="error" :errors="error"
/> />
<slot name="extra" /> <slot name="extra" />
</AModal> </AModal>
</template> </template>
<style scoped> <style scoped></style>
</style>

View file

@ -1,8 +1,8 @@
<script setup lang="ts" generic="T=any"> <script setup lang="ts" generic="T=any">
import type { StdTableSlots } from '@/components/StdDesign/StdDataDisplay/types'
import type { Column } from '@/components/StdDesign/types' import type { Column } from '@/components/StdDesign/types'
import type { ComputedRef } from 'vue' import type { ComputedRef, Ref } from 'vue'
import type { StdTableProps } from './StdTable.vue' import type { StdTableProps } from './StdTable.vue'
import StdBatchEdit from '@/components/StdDesign/StdDataDisplay/StdBatchEdit.vue'
import StdCurdDetail from '@/components/StdDesign/StdDataDisplay/StdCurdDetail.vue' import StdCurdDetail from '@/components/StdDesign/StdDataDisplay/StdCurdDetail.vue'
import StdDataEntry from '@/components/StdDesign/StdDataEntry' import StdDataEntry from '@/components/StdDesign/StdDataEntry'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
@ -14,7 +14,7 @@ export interface StdCurdProps<T> extends StdTableProps<T> {
modalMask?: boolean modalMask?: boolean
exportExcel?: boolean exportExcel?: boolean
importExcel?: boolean importExcel?: boolean
disableTrash?: boolean
disableAdd?: boolean disableAdd?: boolean
onClickAdd?: () => void onClickAdd?: () => void
@ -24,6 +24,10 @@ export interface StdCurdProps<T> extends StdTableProps<T> {
} }
const props = defineProps<StdTableProps<T> & StdCurdProps<T>>() const props = defineProps<StdTableProps<T> & StdCurdProps<T>>()
const selectedRowKeys = ref<(string | number)[]>([])
const selectedRows: Ref<T[]> = ref([])
const visible = ref(false) const visible = ref(false)
// eslint-disable-next-line ts/no-explicit-any // eslint-disable-next-line ts/no-explicit-any
const data: any = reactive({ id: null }) const data: any = reactive({ id: null })
@ -61,24 +65,13 @@ function add(preset: any = undefined) {
if (preset) if (preset)
Object.assign(data, preset) Object.assign(data, preset)
clear_error() clearError()
visible.value = true visible.value = true
editMode.value = 'create' editMode.value = 'create'
modifyMode.value = true modifyMode.value = true
} }
const table = ref() const table = useTemplateRef('table')
// eslint-disable-next-line ts/no-explicit-any
const selectedRowKeys = defineModel<any[]>('selectedRowKeys', {
default: () => [],
})
// eslint-disable-next-line ts/no-explicit-any
const selectedRows = defineModel<any[]>('selectedRows', {
type: Array,
default: () => [],
})
const getParams = reactive({ const getParams = reactive({
trash: false, trash: false,
@ -101,20 +94,23 @@ defineExpose({
setParams, setParams,
}) })
function clear_error() { function clearError() {
Object.keys(error).forEach(v => { Object.keys(error).forEach(v => {
delete error[v] delete error[v]
}) })
} }
const stdEntryRef = ref() const stdEntryRef = useTemplateRef('stdEntryRef')
async function ok() { async function ok() {
if (!stdEntryRef.value)
return
const { formRef } = stdEntryRef.value const { formRef } = stdEntryRef.value
clear_error() clearError()
try { try {
await formRef.validateFields() await formRef?.validateFields()
props?.beforeSave?.(data) props?.beforeSave?.(data)
props props
.api!.save(data.id, { ...data, ...props.overwriteParams }, { params: { ...props.overwriteParams } }).then(r => { .api!.save(data.id, { ...data, ...props.overwriteParams }, { params: { ...props.overwriteParams } }).then(r => {
@ -135,7 +131,7 @@ async function ok() {
function cancel() { function cancel() {
visible.value = false visible.value = false
clear_error() clearError()
if (shouldRefetchList.value) { if (shouldRefetchList.value) {
get_list() get_list()
@ -159,7 +155,6 @@ function view(id: number | string) {
get(id).then(() => { get(id).then(() => {
visible.value = true visible.value = true
modifyMode.value = false modifyMode.value = false
editMode.value = 'modify'
}).catch(e => { }).catch(e => {
message.error($gettext(e?.message ?? 'Server error'), 5) message.error($gettext(e?.message ?? 'Server error'), 5)
}) })
@ -183,6 +178,17 @@ const modalTitle = computed(() => {
}) })
const localOverwriteParams = reactive(props.overwriteParams ?? {}) const localOverwriteParams = reactive(props.overwriteParams ?? {})
const stdBatchEditRef = useTemplateRef('stdBatchEditRef')
async function handleClickBatchEdit(batchColumns: Column[]) {
stdBatchEditRef.value?.showModal(batchColumns, selectedRowKeys.value, selectedRows.value)
}
function handleBatchUpdated() {
table.value?.get_list()
table.value?.resetSelection()
}
</script> </script>
<template> <template>
@ -202,7 +208,7 @@ const localOverwriteParams = reactive(props.overwriteParams ?? {})
@click="add" @click="add"
>{{ $gettext('Add') }}</a> >{{ $gettext('Add') }}</a>
<slot name="extra" /> <slot name="extra" />
<template v-if="!disableDelete && !disableTrash"> <template v-if="!disableDelete">
<a <a
v-if="!getParams.trash" v-if="!getParams.trash"
@click="getParams.trash = true" @click="getParams.trash = true"
@ -219,21 +225,23 @@ const localOverwriteParams = reactive(props.overwriteParams ?? {})
</ASpace> </ASpace>
</template> </template>
<slot name="beforeTable" />
<StdTable <StdTable
ref="table" ref="table"
v-model:selected-row-keys="selectedRowKeys"
v-model:selected-rows="selectedRows"
v-bind="{ v-bind="{
...props, ...props,
getParams, getParams,
overwriteParams: localOverwriteParams, overwriteParams: localOverwriteParams,
}" }"
v-model:selected-row-keys="selectedRowKeys"
v-model:selected-rows="selectedRows"
@click-edit="edit" @click-edit="edit"
@click-view="view" @click-view="view"
@selected="onSelect" @selected="onSelect"
@click-batch-modify="handleClickBatchEdit"
> >
<template <template
v-for="(_, key) in ($slots as unknown as StdTableSlots)" v-for="(_, key) in $slots"
:key="key" :key="key"
#[key]="slotProps" #[key]="slotProps"
> >
@ -295,10 +303,17 @@ const localOverwriteParams = reactive(props.overwriteParams ?? {})
<StdCurdDetail <StdCurdDetail
v-else v-else
:columns="columns" :columns
:data="data" :data
/> />
</AModal> </AModal>
<StdBatchEdit
ref="stdBatchEditRef"
:api
:columns
@save="handleBatchUpdated"
/>
</div> </div>
</template> </template>

View file

@ -8,9 +8,11 @@ import type { FilterValue } from 'ant-design-vue/es/table/interface'
import type { SorterResult, TablePaginationConfig } from 'ant-design-vue/lib/table/interface' import type { SorterResult, TablePaginationConfig } from 'ant-design-vue/lib/table/interface'
import type { ComputedRef, Ref } from 'vue' import type { ComputedRef, Ref } from 'vue'
import type { RouteParams } from 'vue-router' import type { RouteParams } from 'vue-router'
import { getPithyColumns } from '@/components/StdDesign/StdDataDisplay/methods/columns'
import useSortable from '@/components/StdDesign/StdDataDisplay/methods/sortable' import useSortable from '@/components/StdDesign/StdDataDisplay/methods/sortable'
import StdDataEntry from '@/components/StdDesign/StdDataEntry' import StdDataEntry from '@/components/StdDesign/StdDataEntry'
import { HolderOutlined } from '@ant-design/icons-vue' import { HolderOutlined } from '@ant-design/icons-vue'
import { watchPausable } from '@vueuse/core'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import _ from 'lodash' import _ from 'lodash'
import StdPagination from './StdPagination.vue' import StdPagination from './StdPagination.vue'
@ -20,6 +22,7 @@ export interface StdTableProps<T = any> {
title?: string title?: string
mode?: string mode?: string
rowKey?: string rowKey?: string
api: Curd<T> api: Curd<T>
columns: Column[] columns: Column[]
// eslint-disable-next-line ts/no-explicit-any // eslint-disable-next-line ts/no-explicit-any
@ -48,7 +51,20 @@ const props = withDefaults(defineProps<StdTableProps<T>>(), {
rowKey: 'id', rowKey: 'id',
}) })
const emit = defineEmits(['clickEdit', 'clickView', 'clickBatchModify', 'update:selectedRowKeys']) const emit = defineEmits([
'clickEdit',
'clickView',
'clickBatchModify',
])
const selectedRowKeys = defineModel<(number | string)[]>('selectedRowKeys', {
default: () => reactive([]),
})
const selectedRows = defineModel<T[]>('selectedRows', {
default: () => reactive([]),
})
const route = useRoute() const route = useRoute()
const dataSource: Ref<T[]> = ref([]) const dataSource: Ref<T[]> = ref([])
@ -93,17 +109,6 @@ const params = reactive({
...props.getParams, ...props.getParams,
}) })
// eslint-disable-next-line ts/no-explicit-any
const selectedRowKeys = defineModel<any[]>('selectedRowKeys', {
default: () => [],
})
// eslint-disable-next-line ts/no-explicit-any
const selectedRows = defineModel<any[]>('selectedRows', {
type: Array,
default: () => [],
})
onMounted(() => { onMounted(() => {
selectedRows.value.forEach(v => { selectedRows.value.forEach(v => {
selectedRecords.value[v[props.rowKey]] = v selectedRecords.value[v[props.rowKey]] = v
@ -122,7 +127,9 @@ const searchColumns = computed(() => {
}) })
} }
else { _searchColumns.push({ ...column }) } else {
_searchColumns.push({ ...column })
}
} }
}) })
@ -130,11 +137,8 @@ const searchColumns = computed(() => {
}) })
const pithyColumns = computed<Column[]>(() => { const pithyColumns = computed<Column[]>(() => {
if (props.pithy) { if (props.pithy)
return props.columns?.filter(c => { return getPithyColumns(props.columns)
return c.pithy === true && !c.hiddenInTable
})
}
return props.columns?.filter(c => { return props.columns?.filter(c => {
return !c.hiddenInTable return !c.hiddenInTable
@ -142,19 +146,12 @@ const pithyColumns = computed<Column[]>(() => {
}) })
const batchColumns = computed(() => { const batchColumns = computed(() => {
const batch: Column[] = [] return props.columns?.filter(column => column.batch) || []
props.columns?.forEach(column => {
if (column.batch)
batch.push(column)
})
return batch
}) })
const get_list = _.debounce(_get_list, 100, { const get_list = _.debounce(_get_list, 100, {
leading: false, leading: true,
trailing: true, trailing: false,
}) })
const filterParams = reactive({}) const filterParams = reactive({})
@ -184,15 +181,13 @@ onMounted(() => {
if (props.sortable) if (props.sortable)
initSortable() initSortable()
if (!selectedRowKeys.value?.length)
selectedRowKeys.value = []
init.value = true init.value = true
}) })
defineExpose({ defineExpose({
get_list, get_list,
pagination, pagination,
resetSelection,
}) })
function destroy(id: number | string) { function destroy(id: number | string) {
@ -229,13 +224,17 @@ function buildIndexMap(data: any, level: number = 0, index: number = 0, total: n
} }
} }
async function _get_list(page_num = null, page_size = 20) { async function _get_list(page_num: number | null = null, page_size = 20) {
dataSource.value = [] dataSource.value = []
loading.value = true loading.value = true
if (page_num) { if (page_num) {
params.page = page_num params.page = page_num
params.page_size = page_size params.page_size = page_size
} }
else {
params.page = 1
params.page_size = page_size
}
props.api?.get_list({ ...params, ...props.overwriteParams }).then(async r => { props.api?.get_list({ ...params, ...props.overwriteParams }).then(async r => {
dataSource.value = r.data dataSource.value = r.data
rowsKeyIndexMap.value = {} rowsKeyIndexMap.value = {}
@ -245,9 +244,7 @@ async function _get_list(page_num = null, page_size = 20) {
if (r.pagination) if (r.pagination)
Object.assign(pagination, r.pagination) Object.assign(pagination, r.pagination)
setTimeout(() => { loading.value = false
loading.value = false
}, 200)
}).catch(e => { }).catch(e => {
message.error(e?.message ?? $gettext('Server error')) message.error(e?.message ?? $gettext('Server error'))
}) })
@ -288,7 +285,8 @@ function expandedTable(keys: Key[]) {
// eslint-disable-next-line ts/no-explicit-any // eslint-disable-next-line ts/no-explicit-any
async function onSelect(record: any, selected: boolean, _selectedRows: any[]) { async function onSelect(record: any, selected: boolean, _selectedRows: any[]) {
if (props.selectionType === 'checkbox' || props.exportExcel) { // console.log('onSelect', record, selected, _selectedRows)
if (props.selectionType === 'checkbox' || batchColumns.value.length > 0 || props.exportExcel) {
if (selected) { if (selected) {
_selectedRows.forEach(v => { _selectedRows.forEach(v => {
if (v) { if (v) {
@ -300,20 +298,11 @@ async function onSelect(record: any, selected: boolean, _selectedRows: any[]) {
}) })
} }
else { else {
// eslint-disable-next-line ts/no-explicit-any selectedRowKeys.value.splice(selectedRowKeys.value.indexOf(record[props.rowKey]), 1)
selectedRowKeys.value = selectedRowKeys.value.filter((v: any) => v !== record[props.rowKey])
delete selectedRecords.value[record[props.rowKey]] delete selectedRecords.value[record[props.rowKey]]
} }
await nextTick()
await nextTick(async () => { selectedRows.value = [...selectedRowKeys.value.map(v => selectedRecords.value[v])]
// eslint-disable-next-line ts/no-explicit-any
const filteredRows: any[] = []
selectedRowKeys.value.forEach(v => {
filteredRows.push(selectedRecords.value[v])
})
selectedRows.value = filteredRows
})
} }
else if (selected) { else if (selected) {
selectedRowKeys.value = record[props.rowKey] selectedRowKeys.value = record[props.rowKey]
@ -327,7 +316,7 @@ async function onSelect(record: any, selected: boolean, _selectedRows: any[]) {
// eslint-disable-next-line ts/no-explicit-any // eslint-disable-next-line ts/no-explicit-any
async function onSelectAll(selected: boolean, _selectedRows: any[], changeRows: any[]) { async function onSelectAll(selected: boolean, _selectedRows: any[], changeRows: any[]) {
// console.log(selected, selectedRows, changeRows) // console.log('onSelectAll', selected, selectedRows, changeRows)
// eslint-disable-next-line ts/no-explicit-any // eslint-disable-next-line ts/no-explicit-any
changeRows.forEach((v: any) => { changeRows.forEach((v: any) => {
if (v) { if (v) {
@ -342,22 +331,19 @@ async function onSelectAll(selected: boolean, _selectedRows: any[], changeRows:
}) })
if (!selected) { if (!selected) {
selectedRowKeys.value = selectedRowKeys.value.filter(v => { selectedRowKeys.value.splice(0, selectedRowKeys.value.length, ...selectedRowKeys.value.filter(v => selectedRecords.value[v]))
return selectedRecords.value[v]
})
} }
// console.log(selectedRowKeysBuffer.value, selectedRecords.value) // console.log(selectedRowKeysBuffer.value, selectedRecords.value)
await nextTick(async () => { await nextTick()
// eslint-disable-next-line ts/no-explicit-any selectedRows.value.splice(0, selectedRows.value.length, ...selectedRowKeys.value.map(v => selectedRecords.value[v]))
const filteredRows: any[] = [] }
selectedRowKeys.value.forEach(v => { function resetSelection() {
filteredRows.push(selectedRecords.value[v]) selectedRowKeys.value = reactive([])
}) selectedRows.value = reactive([])
selectedRows.value = filteredRows selectedRecords.value = reactive({})
})
} }
const router = useRouter() const router = useRouter()
@ -381,7 +367,7 @@ async function resetSearch() {
updateFilter.value++ updateFilter.value++
} }
watch(params, v => { const { stop: stopWatchParams, resume: resumeWatchParams } = watchPausable(params, v => {
if (!init.value) if (!init.value)
return return
@ -393,7 +379,6 @@ watch(params, v => {
watch(() => route.query, async () => { watch(() => route.query, async () => {
params.trash = route.query.trash === 'true' params.trash = route.query.trash === 'true'
params.team_id = route.query.team_id
if (init.value) if (init.value)
await get_list() await get_list()
@ -425,14 +410,16 @@ if (props.overwriteParams) {
const rowSelection = computed(() => { const rowSelection = computed(() => {
if (batchColumns.value.length > 0 || props.selectionType || props.exportExcel) { if (batchColumns.value.length > 0 || props.selectionType || props.exportExcel) {
return { return {
selectedRowKeys: selectedRowKeys.value, selectedRowKeys: unref(selectedRowKeys),
onSelect, onSelect,
onSelectAll, onSelectAll,
getCheckboxProps: props?.getCheckboxProps, getCheckboxProps: props?.getCheckboxProps,
type: (batchColumns.value.length > 0 || props.exportExcel) ? 'checkbox' : props.selectionType, type: (batchColumns.value.length > 0 || props.exportExcel) ? 'checkbox' : props.selectionType,
} }
} }
else { return null } else {
return null
}
}) as ComputedRef<TableProps['rowSelection']> }) as ComputedRef<TableProps['rowSelection']>
const hasSelectedRow = computed(() => { const hasSelectedRow = computed(() => {
@ -440,18 +427,21 @@ const hasSelectedRow = computed(() => {
}) })
function clickBatchEdit() { function clickBatchEdit() {
emit('clickBatchModify', batchColumns.value, selectedRowKeys.value) emit('clickBatchModify', batchColumns.value, selectedRowKeys.value, selectedRows.value)
} }
function initSortable() { function initSortable() {
useSortable(props, randomId, dataSource, rowsKeyIndexMap, expandKeysList) useSortable(props, randomId, dataSource, rowsKeyIndexMap, expandKeysList)
} }
function changePage(page: number, page_size: number) { async function changePage(page: number, page_size: number) {
stopWatchParams()
Object.assign(params, { Object.assign(params, {
page, page,
page_size, page_size,
}) })
resumeWatchParams()
await get_list(page, page_size)
} }
const paginationSize = computed(() => { const paginationSize = computed(() => {
@ -529,10 +519,7 @@ const paginationSize = computed(() => {
> >
{{ $gettext('Modify') }} {{ $gettext('Modify') }}
</AButton> </AButton>
<ADivider <ADivider type="vertical" />
v-if="!props.disableDelete"
type="vertical"
/>
</template> </template>
<slot <slot
@ -569,6 +556,7 @@ const paginationSize = computed(() => {
{{ $gettext('Recover') }} {{ $gettext('Recover') }}
</AButton> </AButton>
</APopconfirm> </APopconfirm>
<ADivider type="vertical" />
<APopconfirm <APopconfirm
v-if="params.trash" v-if="params.trash"
:cancel-text="$gettext('No')" :cancel-text="$gettext('No')"
@ -601,8 +589,14 @@ const paginationSize = computed(() => {
.ant-table-scroll { .ant-table-scroll {
.ant-table-body { .ant-table-body {
overflow-x: auto !important; overflow-x: auto !important;
overflow-y: hidden !important;
} }
} }
.std-table {
overflow-x: hidden !important;
overflow-y: hidden !important;
}
</style> </style>
<style lang="less" scoped> <style lang="less" scoped>
@ -624,6 +618,12 @@ const paginationSize = computed(() => {
:deep(.ant-form-inline .ant-form-item) { :deep(.ant-form-inline .ant-form-item) {
margin-bottom: 10px; margin-bottom: 10px;
} }
.ant-divider {
&:last-child {
display: none;
}
}
</style> </style>
<style lang="less"> <style lang="less">

View file

@ -0,0 +1,7 @@
import type { Column } from '@/components/StdDesign/types'
export function getPithyColumns(columns: Column[]) {
return columns.filter(c => {
return c.pithy === true && !c.hiddenInTable
})
}

View file

@ -135,6 +135,7 @@ async function save() {
content: configText.value, content: configText.value,
overwrite: true, overwrite: true,
site_category_id: data.value.site_category_id, site_category_id: data.value.site_category_id,
sync_node_ids: data.value.sync_node_ids,
}).then(r => { }).then(r => {
handle_response(r) handle_response(r)
router.push({ router.push({

View file

@ -1,107 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Ref } from 'vue'
import domain from '@/api/domain'
import NodeSelector from '@/components/NodeSelector/NodeSelector.vue' import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
import { InfoCircleOutlined } from '@ant-design/icons-vue'
import { Modal, notification } from 'ant-design-vue'
const node_map = ref({}) const node_map = ref({})
const target = ref([]) const target = ref([])
const overwrite = ref(false)
const enabled = ref(false)
const name = inject('name') as Ref<string>
const [modal, ContextHolder] = Modal.useModal()
function deploy() {
modal.confirm({
title: () => $ngettext('Do you want to deploy this file to remote server?', 'Do you want to deploy this file to remote servers?', target.value.length),
mask: false,
centered: true,
okText: $gettext('OK'),
cancelText: $gettext('Cancel'),
onOk() {
target.value.forEach(id => {
const node_name = node_map.value[id]
// get source content
domain.get(name.value).then(r => {
domain.save(name.value, {
name: name.value,
content: r.config,
overwrite: overwrite.value,
}, { headers: { 'X-Node-ID': id } }).then(async () => {
notification.success({
message: $gettext('Deploy successfully'),
description:
$gettext('Deploy %{conf_name} to %{node_name} successfully', { conf_name: name.value, node_name }),
})
if (enabled.value) {
domain.enable(name.value, { headers: { 'X-Node-ID': id } }).then(() => {
notification.success({
message: $gettext('Enable successfully'),
description:
$gettext('Enable %{conf_name} in %{node_name} successfully', { conf_name: name.value, node_name }),
})
}).catch(e => {
notification.error({
message: $gettext('Enable %{conf_name} in %{node_name} failed', {
conf_name: name.value,
node_name,
}),
description: $gettext(e?.message ?? 'Server error'),
})
})
}
}).catch(e => {
notification.error({
message: $gettext('Deploy %{conf_name} to %{node_name} failed', {
conf_name: name.value,
node_name,
}),
description: $gettext(e?.message ?? 'Server error'),
})
})
})
})
},
})
}
</script> </script>
<template> <template>
<div> <NodeSelector
<ContextHolder /> v-model:target="target"
<NodeSelector v-model:map="node_map"
v-model:target="target" class="mb-4"
v-model:map="node_map" hidden-local
hidden-local />
/>
<div class="node-deploy-control">
<ACheckbox v-model:checked="enabled">
{{ $gettext('Enable') }}
</ACheckbox>
<div class="overwrite">
<ACheckbox v-model:checked="overwrite">
{{ $gettext('Overwrite') }}
</ACheckbox>
<ATooltip placement="bottom">
<template #title>
{{ $gettext('Overwrite exist file') }}
</template>
<InfoCircleOutlined />
</ATooltip>
</div>
<AButton
:disabled="target.length === 0"
type="primary"
ghost
@click="deploy"
>
{{ $gettext('Deploy') }}
</AButton>
</div>
</div>
</template> </template>
<style scoped lang="less"> <style scoped lang="less">

View file

@ -6,11 +6,12 @@ import type { Ref } from 'vue'
import domain from '@/api/domain' import domain from '@/api/domain'
import site_category from '@/api/site_category' import site_category from '@/api/site_category'
import ChatGPT from '@/components/ChatGPT/ChatGPT.vue' import ChatGPT from '@/components/ChatGPT/ChatGPT.vue'
import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
import StdSelector from '@/components/StdDesign/StdDataEntry/components/StdSelector.vue' import StdSelector from '@/components/StdDesign/StdDataEntry/components/StdSelector.vue'
import { formatDateTime } from '@/lib/helper' import { formatDateTime } from '@/lib/helper'
import { useSettingsStore } from '@/pinia' import { useSettingsStore } from '@/pinia'
import Deploy from '@/views/site/components/Deploy.vue'
import siteCategoryColumns from '@/views/site/site_category/columns' import siteCategoryColumns from '@/views/site/site_category/columns'
import { InfoCircleOutlined } from '@ant-design/icons-vue'
import { message, Modal } from 'ant-design-vue' import { message, Modal } from 'ant-design-vue'
const settings = useSettingsStore() const settings = useSettingsStore()
@ -71,6 +72,7 @@ function on_change_enabled(checked: CheckedType) {
<ACollapse <ACollapse
v-model:active-key="active_key" v-model:active-key="active_key"
ghost ghost
collapsible="header"
> >
<ACollapsePanel <ACollapsePanel
key="1" key="1"
@ -103,9 +105,33 @@ function on_change_enabled(checked: CheckedType) {
<ACollapsePanel <ACollapsePanel
v-if="!settings.is_remote" v-if="!settings.is_remote"
key="2" key="2"
:header="$gettext('Deploy')"
> >
<Deploy /> <template #header>
{{ $gettext('Synchronization') }}
</template>
<template #extra>
<APopover placement="bottomRight" :title="$gettext('Sync strategy')">
<template #content>
<div class="max-w-200px mb-2">
{{ $gettext('When you enable/disable, delete, or save this site, '
+ 'the nodes set in the site category and the nodes selected below will be synchronized.') }}
</div>
<div class="max-w-200px">
{{ $gettext('Note, if the configuration file include other configurations or certificates, '
+ 'please synchronize them to the remote nodes in advance.') }}
</div>
</template>
<div class="text-trueGray-600">
<InfoCircleOutlined class="mr-1" />
{{ $gettext('Sync strategy') }}
</div>
</APopover>
</template>
<NodeSelector
v-model:target="data.sync_node_ids"
class="mb-4"
hidden-local
/>
</ACollapsePanel> </ACollapsePanel>
<ACollapsePanel <ACollapsePanel
key="3" key="3"

View file

@ -1,5 +1,7 @@
<script setup lang="tsx"> <script setup lang="tsx">
import type { Site } from '@/api/domain'
import type { SiteCategory } from '@/api/site_category' import type { SiteCategory } from '@/api/site_category'
import type { Column } from '@/components/StdDesign/types'
import domain from '@/api/domain' import domain from '@/api/domain'
import site_category from '@/api/site_category' import site_category from '@/api/site_category'
import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue' import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
@ -7,6 +9,7 @@ import InspectConfig from '@/views/config/InspectConfig.vue'
import SiteDuplicate from '@/views/site/components/SiteDuplicate.vue' import SiteDuplicate from '@/views/site/components/SiteDuplicate.vue'
import columns from '@/views/site/site_list/columns' import columns from '@/views/site/site_list/columns'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import StdBatchEdit from '../../../components/StdDesign/StdDataDisplay/StdBatchEdit.vue'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@ -77,6 +80,17 @@ function handle_click_duplicate(name: string) {
show_duplicator.value = true show_duplicator.value = true
target.value = name target.value = name
} }
const stdBatchEditRef = useTemplateRef('stdBatchEditRef')
async function handleClickBatchEdit(batchColumns: Column[], selectedRowKeys: string[], selectedRows: Site[]) {
stdBatchEditRef.value?.showModal(batchColumns, selectedRowKeys, selectedRows)
}
function handleBatchUpdated() {
table.value?.get_list()
table.value?.resetSelection()
}
</script> </script>
<template> <template>
@ -101,9 +115,9 @@ function handle_click_duplicate(name: string) {
@click-edit="(r: string) => router.push({ @click-edit="(r: string) => router.push({
path: `/sites/${r}`, path: `/sites/${r}`,
})" })"
@click-batch-modify="handleClickBatchEdit"
> >
<template #actions="{ record }"> <template #actions="{ record }">
<ADivider type="vertical" />
<AButton <AButton
v-if="record.enabled" v-if="record.enabled"
type="link" type="link"
@ -146,6 +160,12 @@ function handle_click_duplicate(name: string) {
</APopconfirm> </APopconfirm>
</template> </template>
</StdTable> </StdTable>
<StdBatchEdit
ref="stdBatchEditRef"
:api="domain"
:columns
@save="handleBatchUpdated"
/>
<SiteDuplicate <SiteDuplicate
v-model:visible="show_duplicator" v-model:visible="show_duplicator"
:name="target" :name="target"

View file

@ -1,10 +1,12 @@
import type { Column, JSXElements } from '@/components/StdDesign/types' import type { Column, JSXElements } from '@/components/StdDesign/types'
import site_category from '@/api/site_category'
import { import {
actualValueRender, actualValueRender,
type CustomRenderProps, type CustomRenderProps,
datetime, datetime,
} from '@/components/StdDesign/StdDataDisplay/StdTableTransformer' } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
import { input, select } from '@/components/StdDesign/StdDataEntry' import { input, select, selector } from '@/components/StdDesign/StdDataEntry'
import siteCategoryColumns from '@/views/site/site_category/columns'
import { Badge } from 'ant-design-vue' import { Badge } from 'ant-design-vue'
const columns: Column[] = [{ const columns: Column[] = [{
@ -20,8 +22,18 @@ const columns: Column[] = [{
title: () => $gettext('Category'), title: () => $gettext('Category'),
dataIndex: 'site_category_id', dataIndex: 'site_category_id',
customRender: actualValueRender('site_category.name'), customRender: actualValueRender('site_category.name'),
edit: {
type: selector,
selector: {
api: site_category,
columns: siteCategoryColumns,
recordValueIndex: 'name',
selectionType: 'radio',
},
},
sorter: true, sorter: true,
pithy: true, pithy: true,
batch: true,
}, { }, {
title: () => $gettext('Status'), title: () => $gettext('Status'),
dataIndex: 'enabled', dataIndex: 'enabled',

View file

@ -135,7 +135,6 @@ function handleAddStream() {
})" })"
> >
<template #actions="{ record }"> <template #actions="{ record }">
<ADivider type="vertical" />
<AButton <AButton
v-if="record.enabled" v-if="record.enabled"
type="link" type="link"

View file

@ -4,7 +4,8 @@ import (
"flag" "flag"
"fmt" "fmt"
"github.com/0xJacky/Nginx-UI/model" "github.com/0xJacky/Nginx-UI/model"
"github.com/uozi-tech/cosy/settings" "github.com/0xJacky/Nginx-UI/settings"
cSettings "github.com/uozi-tech/cosy/settings"
"gorm.io/driver/sqlite" "gorm.io/driver/sqlite"
"gorm.io/gen" "gorm.io/gen"
"gorm.io/gorm" "gorm.io/gorm"
@ -39,8 +40,8 @@ func main() {
flag.StringVar(&confPath, "config", "app.ini", "Specify the configuration file") flag.StringVar(&confPath, "config", "app.ini", "Specify the configuration file")
flag.Parse() flag.Parse()
settings.Init(confPath) cSettings.Init(confPath)
dbPath := path.Join(path.Dir(confPath), fmt.Sprintf("%s.db", settings.DataBaseSettings.Name)) dbPath := path.Join(path.Dir(confPath), fmt.Sprintf("%s.db", settings.DatabaseSettings.Name))
var err error var err error
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{ db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{

View file

@ -2,8 +2,9 @@ package model
type Site struct { type Site struct {
Model Model
Path string `json:"path"` Path string `json:"path" gorm:"uniqueIndex"`
Advanced bool `json:"advanced"` Advanced bool `json:"advanced"`
SiteCategoryID uint64 `json:"site_category_id"` SiteCategoryID uint64 `json:"site_category_id"`
SiteCategory *SiteCategory `json:"site_category,omitempty"` SiteCategory *SiteCategory `json:"site_category,omitempty"`
SyncNodeIDs []uint64 `json:"sync_node_ids" gorm:"serializer:json"`
} }

View file

@ -35,6 +35,7 @@ func newSite(db *gorm.DB, opts ...gen.DOOption) site {
_site.Path = field.NewString(tableName, "path") _site.Path = field.NewString(tableName, "path")
_site.Advanced = field.NewBool(tableName, "advanced") _site.Advanced = field.NewBool(tableName, "advanced")
_site.SiteCategoryID = field.NewUint64(tableName, "site_category_id") _site.SiteCategoryID = field.NewUint64(tableName, "site_category_id")
_site.SyncNodeIDs = field.NewField(tableName, "sync_node_ids")
_site.SiteCategory = siteBelongsToSiteCategory{ _site.SiteCategory = siteBelongsToSiteCategory{
db: db.Session(&gorm.Session{}), db: db.Session(&gorm.Session{}),
@ -57,6 +58,7 @@ type site struct {
Path field.String Path field.String
Advanced field.Bool Advanced field.Bool
SiteCategoryID field.Uint64 SiteCategoryID field.Uint64
SyncNodeIDs field.Field
SiteCategory siteBelongsToSiteCategory SiteCategory siteBelongsToSiteCategory
fieldMap map[string]field.Expr fieldMap map[string]field.Expr
@ -81,6 +83,7 @@ func (s *site) updateTableName(table string) *site {
s.Path = field.NewString(table, "path") s.Path = field.NewString(table, "path")
s.Advanced = field.NewBool(table, "advanced") s.Advanced = field.NewBool(table, "advanced")
s.SiteCategoryID = field.NewUint64(table, "site_category_id") s.SiteCategoryID = field.NewUint64(table, "site_category_id")
s.SyncNodeIDs = field.NewField(table, "sync_node_ids")
s.fillFieldMap() s.fillFieldMap()
@ -97,7 +100,7 @@ func (s *site) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
} }
func (s *site) fillFieldMap() { func (s *site) fillFieldMap() {
s.fieldMap = make(map[string]field.Expr, 8) s.fieldMap = make(map[string]field.Expr, 9)
s.fieldMap["id"] = s.ID s.fieldMap["id"] = s.ID
s.fieldMap["created_at"] = s.CreatedAt s.fieldMap["created_at"] = s.CreatedAt
s.fieldMap["updated_at"] = s.UpdatedAt s.fieldMap["updated_at"] = s.UpdatedAt
@ -105,6 +108,7 @@ func (s *site) fillFieldMap() {
s.fieldMap["path"] = s.Path s.fieldMap["path"] = s.Path
s.fieldMap["advanced"] = s.Advanced s.fieldMap["advanced"] = s.Advanced
s.fieldMap["site_category_id"] = s.SiteCategoryID s.fieldMap["site_category_id"] = s.SiteCategoryID
s.fieldMap["sync_node_ids"] = s.SyncNodeIDs
} }