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/:name", GetSite)
r.POST("domains/:name", SaveSite)
r.PUT("domains", BatchUpdateSites)
r.POST("domains/:name/enable", EnableSite)
r.POST("domains/:name/disable", DisableSite)
r.POST("domains/:name/advance", DomainEditByAdvancedMode)

View file

@ -9,7 +9,9 @@ import (
"github.com/0xJacky/Nginx-UI/query"
"github.com/gin-gonic/gin"
"github.com/sashabaranov/go-openai"
"github.com/uozi-tech/cosy"
"github.com/uozi-tech/cosy/logger"
"gorm.io/gorm/clause"
"net/http"
"os"
)
@ -125,10 +127,11 @@ func SaveSite(c *gin.Context) {
}
var json struct {
Name string `json:"name" binding:"required"`
Content string `json:"content" binding:"required"`
SiteCategoryID uint64 `json:"site_category_id"`
Overwrite bool `json:"overwrite"`
Name string `json:"name" binding:"required"`
Content string `json:"content" binding:"required"`
SiteCategoryID uint64 `json:"site_category_id"`
SyncNodeIDs []uint64 `json:"sync_node_ids"`
Overwrite bool `json:"overwrite"`
}
if !api.BindAndValid(c, &json) {
@ -152,7 +155,12 @@ func SaveSite(c *gin.Context) {
enabledConfigFilePath := nginx.GetConfPath("sites-enabled", name)
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 {
api.ErrHandler(c, err)
return
@ -336,3 +344,29 @@ func DeleteSite(c *gin.Context) {
"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 {
globals: {
$gettext: true,
$ngettext: true,
$npgettext: true,
$pgettext: true,
Component: true,
ComponentPublicInstance: true,
ComputedRef: true,
DirectiveBinding: true,
EffectScope: true,
ExtractDefaultPropTypes: true,
ExtractPropTypes: true,
ExtractPublicPropTypes: true,
InjectionKey: true,
MaybeRef: true,
MaybeRefOrGetter: true,
PropType: true,
Ref: true,
VNode: true,
WritableComputedRef: true,
acceptHMRUpdate: true,
computed: true,
createApp: true,
createPinia: true,
customRef: true,
defineAsyncComponent: true,
defineComponent: true,
defineStore: true,
effectScope: true,
getActivePinia: true,
getCurrentInstance: true,
getCurrentScope: true,
h: true,
inject: true,
isProxy: true,
isReactive: true,
isReadonly: true,
isRef: true,
mapActions: true,
mapGetters: true,
mapState: true,
mapStores: true,
mapWritableState: true,
markRaw: true,
nextTick: true,
onActivated: true,
onBeforeMount: true,
onBeforeRouteLeave: true,
onBeforeRouteUpdate: true,
onBeforeUnmount: true,
onBeforeUpdate: true,
onDeactivated: true,
onErrorCaptured: true,
onMounted: true,
onRenderTracked: true,
onRenderTriggered: true,
onScopeDispose: true,
onServerPrefetch: true,
onUnmounted: true,
onUpdated: true,
onWatcherCleanup: true,
provide: true,
reactive: true,
readonly: true,
ref: true,
resolveComponent: true,
setActivePinia: true,
setMapStoreSuffix: true,
shallowReactive: true,
shallowReadonly: true,
shallowRef: true,
storeToRefs: true,
toRaw: true,
toRef: true,
toRefs: true,
toValue: true,
triggerRef: true,
unref: true,
useAttrs: true,
useCssModule: true,
useCssVars: true,
useId: true,
useLink: true,
useModel: true,
useRoute: true,
useRouter: true,
useSlots: true,
useTemplateRef: true,
watch: true,
watchEffect: true,
watchPostEffect: true,
watchSyncEffect: true,
},
"globals": {
"$gettext": true,
"$ngettext": true,
"$npgettext": true,
"$pgettext": true,
"Component": true,
"ComponentPublicInstance": true,
"ComputedRef": true,
"DirectiveBinding": true,
"EffectScope": true,
"ExtractDefaultPropTypes": true,
"ExtractPropTypes": true,
"ExtractPublicPropTypes": true,
"InjectionKey": true,
"MaybeRef": true,
"MaybeRefOrGetter": true,
"PropType": true,
"Ref": true,
"VNode": true,
"WritableComputedRef": true,
"acceptHMRUpdate": true,
"computed": true,
"createApp": true,
"createPinia": true,
"customRef": true,
"defineAsyncComponent": true,
"defineComponent": true,
"defineStore": true,
"effectScope": true,
"getActivePinia": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"h": true,
"inject": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"mapActions": true,
"mapGetters": true,
"mapState": true,
"mapStores": true,
"mapWritableState": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onBeforeMount": true,
"onBeforeRouteLeave": true,
"onBeforeRouteUpdate": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onDeactivated": true,
"onErrorCaptured": true,
"onMounted": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onUnmounted": true,
"onUpdated": true,
"onWatcherCleanup": true,
"provide": true,
"reactive": true,
"readonly": true,
"ref": true,
"resolveComponent": true,
"setActivePinia": true,
"setMapStoreSuffix": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"storeToRefs": true,
"toRaw": true,
"toRef": true,
"toRefs": true,
"toValue": true,
"triggerRef": true,
"unref": true,
"useAttrs": true,
"useCssModule": true,
"useCssVars": true,
"useId": true,
"useLink": true,
"useModel": true,
"useRoute": true,
"useRouter": true,
"useSlots": true,
"useTemplateRef": true,
"watch": true,
"watchEffect": true,
"watchPostEffect": 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']
APopover: typeof import('ant-design-vue/es')['Popover']
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']
ARow: typeof import('ant-design-vue/es')['Row']
ASelect: typeof import('ant-design-vue/es')['Select']

View file

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

View file

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

View file

@ -1,48 +1,60 @@
<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 { message } from 'ant-design-vue'
const props = defineProps<{
// eslint-disable-next-line ts/no-explicit-any
api: (ids: number[], data: any) => Promise<void>
api: Curd<any>
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 data = ref({})
const error = ref({})
const loading = ref(false)
const selectedRowKeys = ref([])
// 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
selectedRowKeys.value = rowKeys
batchColumns.value = c
selectedRows.value = rows
}
defineExpose({
showModal,
})
const data = reactive({})
const error = reactive({})
const loading = ref(false)
async function ok() {
loading.value = true
await props.beforeSave?.()
await props.api(selectedRowKeys.value, data).then(async () => {
message.success($gettext('Save successfully'))
emit('onSave')
}).catch(e => {
message.error($gettext(e?.message) ?? $gettext('Server error'))
}).finally(() => {
loading.value = false
})
await props.api.batch_save(selectedRowKeys.value, data.value)
.then(async () => {
message.success($gettext('Save successfully'))
emit('save')
visible.value = false
})
.catch(e => {
error.value = e.errors
message.error($gettext(e?.message) ?? $gettext('Server error'))
})
.finally(() => {
loading.value = false
})
}
</script>
@ -52,23 +64,31 @@ async function ok() {
class="std-curd-edit-modal"
:mask="false"
:title="$gettext('Batch Modify')"
:cancel-text="$gettext('Cancel')"
:ok-text="$gettext('OK')"
:cancel-text="$gettext('No')"
:ok-text="$gettext('Save')"
:confirm-loading="loading"
:width="600"
destroy-on-close
@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
:data-list="batchColumns"
:data-source="data"
:error="error"
:errors="error"
/>
<slot name="extra" />
</AModal>
</template>
<style scoped>
</style>
<style scoped></style>

View file

@ -1,8 +1,8 @@
<script setup lang="ts" generic="T=any">
import type { StdTableSlots } from '@/components/StdDesign/StdDataDisplay/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 StdBatchEdit from '@/components/StdDesign/StdDataDisplay/StdBatchEdit.vue'
import StdCurdDetail from '@/components/StdDesign/StdDataDisplay/StdCurdDetail.vue'
import StdDataEntry from '@/components/StdDesign/StdDataEntry'
import { message } from 'ant-design-vue'
@ -14,7 +14,7 @@ export interface StdCurdProps<T> extends StdTableProps<T> {
modalMask?: boolean
exportExcel?: boolean
importExcel?: boolean
disableTrash?: boolean
disableAdd?: boolean
onClickAdd?: () => void
@ -24,6 +24,10 @@ export interface StdCurdProps<T> extends StdTableProps<T> {
}
const props = defineProps<StdTableProps<T> & StdCurdProps<T>>()
const selectedRowKeys = ref<(string | number)[]>([])
const selectedRows: Ref<T[]> = ref([])
const visible = ref(false)
// eslint-disable-next-line ts/no-explicit-any
const data: any = reactive({ id: null })
@ -61,24 +65,13 @@ function add(preset: any = undefined) {
if (preset)
Object.assign(data, preset)
clear_error()
clearError()
visible.value = true
editMode.value = 'create'
modifyMode.value = true
}
const table = ref()
// 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 table = useTemplateRef('table')
const getParams = reactive({
trash: false,
@ -101,20 +94,23 @@ defineExpose({
setParams,
})
function clear_error() {
function clearError() {
Object.keys(error).forEach(v => {
delete error[v]
})
}
const stdEntryRef = ref()
const stdEntryRef = useTemplateRef('stdEntryRef')
async function ok() {
if (!stdEntryRef.value)
return
const { formRef } = stdEntryRef.value
clear_error()
clearError()
try {
await formRef.validateFields()
await formRef?.validateFields()
props?.beforeSave?.(data)
props
.api!.save(data.id, { ...data, ...props.overwriteParams }, { params: { ...props.overwriteParams } }).then(r => {
@ -135,7 +131,7 @@ async function ok() {
function cancel() {
visible.value = false
clear_error()
clearError()
if (shouldRefetchList.value) {
get_list()
@ -159,7 +155,6 @@ function view(id: number | string) {
get(id).then(() => {
visible.value = true
modifyMode.value = false
editMode.value = 'modify'
}).catch(e => {
message.error($gettext(e?.message ?? 'Server error'), 5)
})
@ -183,6 +178,17 @@ const modalTitle = computed(() => {
})
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>
<template>
@ -202,7 +208,7 @@ const localOverwriteParams = reactive(props.overwriteParams ?? {})
@click="add"
>{{ $gettext('Add') }}</a>
<slot name="extra" />
<template v-if="!disableDelete && !disableTrash">
<template v-if="!disableDelete">
<a
v-if="!getParams.trash"
@click="getParams.trash = true"
@ -219,21 +225,23 @@ const localOverwriteParams = reactive(props.overwriteParams ?? {})
</ASpace>
</template>
<slot name="beforeTable" />
<StdTable
ref="table"
v-model:selected-row-keys="selectedRowKeys"
v-model:selected-rows="selectedRows"
v-bind="{
...props,
getParams,
overwriteParams: localOverwriteParams,
}"
v-model:selected-row-keys="selectedRowKeys"
v-model:selected-rows="selectedRows"
@click-edit="edit"
@click-view="view"
@selected="onSelect"
@click-batch-modify="handleClickBatchEdit"
>
<template
v-for="(_, key) in ($slots as unknown as StdTableSlots)"
v-for="(_, key) in $slots"
:key="key"
#[key]="slotProps"
>
@ -295,10 +303,17 @@ const localOverwriteParams = reactive(props.overwriteParams ?? {})
<StdCurdDetail
v-else
:columns="columns"
:data="data"
:columns
:data
/>
</AModal>
<StdBatchEdit
ref="stdBatchEditRef"
:api
:columns
@save="handleBatchUpdated"
/>
</div>
</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 { ComputedRef, Ref } from 'vue'
import type { RouteParams } from 'vue-router'
import { getPithyColumns } from '@/components/StdDesign/StdDataDisplay/methods/columns'
import useSortable from '@/components/StdDesign/StdDataDisplay/methods/sortable'
import StdDataEntry from '@/components/StdDesign/StdDataEntry'
import { HolderOutlined } from '@ant-design/icons-vue'
import { watchPausable } from '@vueuse/core'
import { message } from 'ant-design-vue'
import _ from 'lodash'
import StdPagination from './StdPagination.vue'
@ -20,6 +22,7 @@ export interface StdTableProps<T = any> {
title?: string
mode?: string
rowKey?: string
api: Curd<T>
columns: Column[]
// eslint-disable-next-line ts/no-explicit-any
@ -48,7 +51,20 @@ const props = withDefaults(defineProps<StdTableProps<T>>(), {
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 dataSource: Ref<T[]> = ref([])
@ -93,17 +109,6 @@ const params = reactive({
...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(() => {
selectedRows.value.forEach(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[]>(() => {
if (props.pithy) {
return props.columns?.filter(c => {
return c.pithy === true && !c.hiddenInTable
})
}
if (props.pithy)
return getPithyColumns(props.columns)
return props.columns?.filter(c => {
return !c.hiddenInTable
@ -142,19 +146,12 @@ const pithyColumns = computed<Column[]>(() => {
})
const batchColumns = computed(() => {
const batch: Column[] = []
props.columns?.forEach(column => {
if (column.batch)
batch.push(column)
})
return batch
return props.columns?.filter(column => column.batch) || []
})
const get_list = _.debounce(_get_list, 100, {
leading: false,
trailing: true,
leading: true,
trailing: false,
})
const filterParams = reactive({})
@ -184,15 +181,13 @@ onMounted(() => {
if (props.sortable)
initSortable()
if (!selectedRowKeys.value?.length)
selectedRowKeys.value = []
init.value = true
})
defineExpose({
get_list,
pagination,
resetSelection,
})
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 = []
loading.value = true
if (page_num) {
params.page = page_num
params.page_size = page_size
}
else {
params.page = 1
params.page_size = page_size
}
props.api?.get_list({ ...params, ...props.overwriteParams }).then(async r => {
dataSource.value = r.data
rowsKeyIndexMap.value = {}
@ -245,9 +244,7 @@ async function _get_list(page_num = null, page_size = 20) {
if (r.pagination)
Object.assign(pagination, r.pagination)
setTimeout(() => {
loading.value = false
}, 200)
loading.value = false
}).catch(e => {
message.error(e?.message ?? $gettext('Server error'))
})
@ -288,7 +285,8 @@ function expandedTable(keys: Key[]) {
// eslint-disable-next-line ts/no-explicit-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) {
_selectedRows.forEach(v => {
if (v) {
@ -300,20 +298,11 @@ async function onSelect(record: any, selected: boolean, _selectedRows: any[]) {
})
}
else {
// eslint-disable-next-line ts/no-explicit-any
selectedRowKeys.value = selectedRowKeys.value.filter((v: any) => v !== record[props.rowKey])
selectedRowKeys.value.splice(selectedRowKeys.value.indexOf(record[props.rowKey]), 1)
delete selectedRecords.value[record[props.rowKey]]
}
await nextTick(async () => {
// eslint-disable-next-line ts/no-explicit-any
const filteredRows: any[] = []
selectedRowKeys.value.forEach(v => {
filteredRows.push(selectedRecords.value[v])
})
selectedRows.value = filteredRows
})
await nextTick()
selectedRows.value = [...selectedRowKeys.value.map(v => selectedRecords.value[v])]
}
else if (selected) {
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
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
changeRows.forEach((v: any) => {
if (v) {
@ -342,22 +331,19 @@ async function onSelectAll(selected: boolean, _selectedRows: any[], changeRows:
})
if (!selected) {
selectedRowKeys.value = selectedRowKeys.value.filter(v => {
return selectedRecords.value[v]
})
selectedRowKeys.value.splice(0, selectedRowKeys.value.length, ...selectedRowKeys.value.filter(v => selectedRecords.value[v]))
}
// console.log(selectedRowKeysBuffer.value, selectedRecords.value)
await nextTick(async () => {
// eslint-disable-next-line ts/no-explicit-any
const filteredRows: any[] = []
await nextTick()
selectedRows.value.splice(0, selectedRows.value.length, ...selectedRowKeys.value.map(v => selectedRecords.value[v]))
}
selectedRowKeys.value.forEach(v => {
filteredRows.push(selectedRecords.value[v])
})
selectedRows.value = filteredRows
})
function resetSelection() {
selectedRowKeys.value = reactive([])
selectedRows.value = reactive([])
selectedRecords.value = reactive({})
}
const router = useRouter()
@ -381,7 +367,7 @@ async function resetSearch() {
updateFilter.value++
}
watch(params, v => {
const { stop: stopWatchParams, resume: resumeWatchParams } = watchPausable(params, v => {
if (!init.value)
return
@ -393,7 +379,6 @@ watch(params, v => {
watch(() => route.query, async () => {
params.trash = route.query.trash === 'true'
params.team_id = route.query.team_id
if (init.value)
await get_list()
@ -425,14 +410,16 @@ if (props.overwriteParams) {
const rowSelection = computed(() => {
if (batchColumns.value.length > 0 || props.selectionType || props.exportExcel) {
return {
selectedRowKeys: selectedRowKeys.value,
selectedRowKeys: unref(selectedRowKeys),
onSelect,
onSelectAll,
getCheckboxProps: props?.getCheckboxProps,
type: (batchColumns.value.length > 0 || props.exportExcel) ? 'checkbox' : props.selectionType,
}
}
else { return null }
else {
return null
}
}) as ComputedRef<TableProps['rowSelection']>
const hasSelectedRow = computed(() => {
@ -440,18 +427,21 @@ const hasSelectedRow = computed(() => {
})
function clickBatchEdit() {
emit('clickBatchModify', batchColumns.value, selectedRowKeys.value)
emit('clickBatchModify', batchColumns.value, selectedRowKeys.value, selectedRows.value)
}
function initSortable() {
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, {
page,
page_size,
})
resumeWatchParams()
await get_list(page, page_size)
}
const paginationSize = computed(() => {
@ -529,10 +519,7 @@ const paginationSize = computed(() => {
>
{{ $gettext('Modify') }}
</AButton>
<ADivider
v-if="!props.disableDelete"
type="vertical"
/>
<ADivider type="vertical" />
</template>
<slot
@ -569,6 +556,7 @@ const paginationSize = computed(() => {
{{ $gettext('Recover') }}
</AButton>
</APopconfirm>
<ADivider type="vertical" />
<APopconfirm
v-if="params.trash"
:cancel-text="$gettext('No')"
@ -601,8 +589,14 @@ const paginationSize = computed(() => {
.ant-table-scroll {
.ant-table-body {
overflow-x: auto !important;
overflow-y: hidden !important;
}
}
.std-table {
overflow-x: hidden !important;
overflow-y: hidden !important;
}
</style>
<style lang="less" scoped>
@ -624,6 +618,12 @@ const paginationSize = computed(() => {
:deep(.ant-form-inline .ant-form-item) {
margin-bottom: 10px;
}
.ant-divider {
&:last-child {
display: none;
}
}
</style>
<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,
overwrite: true,
site_category_id: data.value.site_category_id,
sync_node_ids: data.value.sync_node_ids,
}).then(r => {
handle_response(r)
router.push({

View file

@ -1,107 +1,17 @@
<script setup lang="ts">
import type { Ref } from 'vue'
import domain from '@/api/domain'
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 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>
<template>
<div>
<ContextHolder />
<NodeSelector
v-model:target="target"
v-model:map="node_map"
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>
<NodeSelector
v-model:target="target"
v-model:map="node_map"
class="mb-4"
hidden-local
/>
</template>
<style scoped lang="less">

View file

@ -6,11 +6,12 @@ import type { Ref } from 'vue'
import domain from '@/api/domain'
import site_category from '@/api/site_category'
import ChatGPT from '@/components/ChatGPT/ChatGPT.vue'
import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
import StdSelector from '@/components/StdDesign/StdDataEntry/components/StdSelector.vue'
import { formatDateTime } from '@/lib/helper'
import { useSettingsStore } from '@/pinia'
import Deploy from '@/views/site/components/Deploy.vue'
import siteCategoryColumns from '@/views/site/site_category/columns'
import { InfoCircleOutlined } from '@ant-design/icons-vue'
import { message, Modal } from 'ant-design-vue'
const settings = useSettingsStore()
@ -71,6 +72,7 @@ function on_change_enabled(checked: CheckedType) {
<ACollapse
v-model:active-key="active_key"
ghost
collapsible="header"
>
<ACollapsePanel
key="1"
@ -103,9 +105,33 @@ function on_change_enabled(checked: CheckedType) {
<ACollapsePanel
v-if="!settings.is_remote"
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
key="3"

View file

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

View file

@ -1,10 +1,12 @@
import type { Column, JSXElements } from '@/components/StdDesign/types'
import site_category from '@/api/site_category'
import {
actualValueRender,
type CustomRenderProps,
datetime,
} 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'
const columns: Column[] = [{
@ -20,8 +22,18 @@ const columns: Column[] = [{
title: () => $gettext('Category'),
dataIndex: 'site_category_id',
customRender: actualValueRender('site_category.name'),
edit: {
type: selector,
selector: {
api: site_category,
columns: siteCategoryColumns,
recordValueIndex: 'name',
selectionType: 'radio',
},
},
sorter: true,
pithy: true,
batch: true,
}, {
title: () => $gettext('Status'),
dataIndex: 'enabled',

View file

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

View file

@ -4,7 +4,8 @@ import (
"flag"
"fmt"
"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/gen"
"gorm.io/gorm"
@ -39,8 +40,8 @@ func main() {
flag.StringVar(&confPath, "config", "app.ini", "Specify the configuration file")
flag.Parse()
settings.Init(confPath)
dbPath := path.Join(path.Dir(confPath), fmt.Sprintf("%s.db", settings.DataBaseSettings.Name))
cSettings.Init(confPath)
dbPath := path.Join(path.Dir(confPath), fmt.Sprintf("%s.db", settings.DatabaseSettings.Name))
var err error
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{

View file

@ -2,8 +2,9 @@ package model
type Site struct {
Model
Path string `json:"path"`
Path string `json:"path" gorm:"uniqueIndex"`
Advanced bool `json:"advanced"`
SiteCategoryID uint64 `json:"site_category_id"`
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.Advanced = field.NewBool(tableName, "advanced")
_site.SiteCategoryID = field.NewUint64(tableName, "site_category_id")
_site.SyncNodeIDs = field.NewField(tableName, "sync_node_ids")
_site.SiteCategory = siteBelongsToSiteCategory{
db: db.Session(&gorm.Session{}),
@ -57,6 +58,7 @@ type site struct {
Path field.String
Advanced field.Bool
SiteCategoryID field.Uint64
SyncNodeIDs field.Field
SiteCategory siteBelongsToSiteCategory
fieldMap map[string]field.Expr
@ -81,6 +83,7 @@ func (s *site) updateTableName(table string) *site {
s.Path = field.NewString(table, "path")
s.Advanced = field.NewBool(table, "advanced")
s.SiteCategoryID = field.NewUint64(table, "site_category_id")
s.SyncNodeIDs = field.NewField(table, "sync_node_ids")
s.fillFieldMap()
@ -97,7 +100,7 @@ func (s *site) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
}
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["created_at"] = s.CreatedAt
s.fieldMap["updated_at"] = s.UpdatedAt
@ -105,6 +108,7 @@ func (s *site) fillFieldMap() {
s.fieldMap["path"] = s.Path
s.fieldMap["advanced"] = s.Advanced
s.fieldMap["site_category_id"] = s.SiteCategoryID
s.fieldMap["sync_node_ids"] = s.SyncNodeIDs
}