feat: add filter of category for sites list

This commit is contained in:
Jacky 2024-10-25 11:24:09 +08:00
parent 207f80f858
commit aa556767f2
No known key found for this signature in database
GPG key ID: 215C21B10DF38B4D
10 changed files with 278 additions and 80 deletions

View file

@ -49,7 +49,7 @@ func GetSite(c *gin.Context) {
}
s := query.Site
site, err := s.Where(s.Path.Eq(path)).FirstOrInit()
site, err := s.Where(s.Path.Eq(path)).FirstOrCreate()
if err != nil {
api.ErrHandler(c, err)
return
@ -300,6 +300,14 @@ func DeleteSite(c *gin.Context) {
var err error
name := c.Param("name")
availablePath := nginx.GetConfPath("sites-available", name)
s := query.Site
_, err = s.Where(s.Path.Eq(availablePath)).Unscoped().Delete(&model.Site{})
if err != nil {
api.ErrHandler(c, err)
return
}
enabledPath := nginx.GetConfPath("sites-enabled", name)
if _, err = os.Stat(availablePath); os.IsNotExist(err) {
c.JSON(http.StatusNotFound, gin.H{

View file

@ -4,9 +4,14 @@ import (
"github.com/0xJacky/Nginx-UI/api"
"github.com/0xJacky/Nginx-UI/internal/config"
"github.com/0xJacky/Nginx-UI/internal/nginx"
"github.com/0xJacky/Nginx-UI/model"
"github.com/0xJacky/Nginx-UI/query"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
"github.com/spf13/cast"
"net/http"
"os"
"path/filepath"
"strings"
)
@ -15,6 +20,7 @@ func GetSiteList(c *gin.Context) {
enabled := c.Query("enabled")
orderBy := c.Query("order_by")
sort := c.DefaultQuery("sort", "desc")
querySiteCategoryId := cast.ToUint64(c.Query("site_category_id"))
configFiles, err := os.ReadDir(nginx.GetConfPath("sites-available"))
if err != nil {
@ -28,6 +34,20 @@ func GetSiteList(c *gin.Context) {
return
}
s := query.Site
sTx := s.Preload(s.SiteCategory)
if querySiteCategoryId != 0 {
sTx.Where(s.SiteCategoryID.Eq(querySiteCategoryId))
}
sites, err := sTx.Find()
if err != nil {
api.ErrHandler(c, err)
return
}
sitesMap := lo.SliceToMap(sites, func(item *model.Site) (string, *model.Site) {
return filepath.Base(item.Path), item
})
enabledConfigMap := make(map[string]bool)
for i := range enabledConfig {
enabledConfigMap[enabledConfig[i].Name()] = true
@ -38,28 +58,46 @@ func GetSiteList(c *gin.Context) {
for i := range configFiles {
file := configFiles[i]
fileInfo, _ := file.Info()
if !file.IsDir() {
// name filter
if name != "" && !strings.Contains(file.Name(), name) {
if file.IsDir() {
continue
}
// name filter
if name != "" && !strings.Contains(file.Name(), name) {
continue
}
// status filter
if enabled != "" {
if enabled == "true" && !enabledConfigMap[file.Name()] {
continue
}
// status filter
if enabled != "" {
if enabled == "true" && !enabledConfigMap[file.Name()] {
continue
}
if enabled == "false" && enabledConfigMap[file.Name()] {
continue
}
if enabled == "false" && enabledConfigMap[file.Name()] {
continue
}
configs = append(configs, config.Config{
Name: file.Name(),
ModifiedAt: fileInfo.ModTime(),
Size: fileInfo.Size(),
IsDir: fileInfo.IsDir(),
Enabled: enabledConfigMap[file.Name()],
})
}
var (
siteCategoryId uint64
siteCategory *model.SiteCategory
)
if site, ok := sitesMap[file.Name()]; ok {
siteCategoryId = site.SiteCategoryID
siteCategory = site.SiteCategory
}
// site category filter
if querySiteCategoryId != 0 && siteCategoryId != querySiteCategoryId {
continue
}
configs = append(configs, config.Config{
Name: file.Name(),
ModifiedAt: fileInfo.ModTime(),
Size: fileInfo.Size(),
IsDir: fileInfo.IsDir(),
Enabled: enabledConfigMap[file.Name()],
SiteCategoryID: siteCategoryId,
SiteCategory: siteCategory,
})
}
configs = config.Sort(orderBy, sort, configs)

View file

@ -0,0 +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,
},
}

View file

@ -1,10 +1,14 @@
import createConfig from '@antfu/eslint-config'
import sonarjs from 'eslint-plugin-sonarjs'
import autoImport from './.eslint-auto-import.mjs'
export default createConfig(
{
stylistic: true,
ignores: ['**/version.json', 'tsconfig.json', 'tsconfig.node.json'],
languageOptions: {
globals: autoImport.globals,
},
},
sonarjs.configs.recommended,
{

View file

@ -48,8 +48,10 @@ export function mask(maskObj: any): (args: CustomRenderProps) => JSX.Element {
export function arrayToTextRender(args: CustomRenderProps) {
return args.text?.join(', ')
}
export function actualValueRender(args: CustomRenderProps, actualDataIndex: string | string[]) {
return get(args.record, actualDataIndex)
export function actualValueRender(actualDataIndex: string | string[]) {
return (args: CustomRenderProps) => {
return get(args.record, actualDataIndex) || '/'
}
}
export function longTextWithEllipsis(len: number): (args: CustomRenderProps) => JSX.Element {

View file

@ -52,7 +52,7 @@ export const routes: RouteRecordRaw[] = [
children: [{
path: 'list',
name: 'Sites List',
component: () => import('@/views/site/SiteList.vue'),
component: () => import('@/views/site/site_list/SiteList.vue'),
meta: {
name: () => $gettext('Sites List'),
},

View file

@ -1,64 +1,44 @@
<script setup lang="tsx">
import type { CustomRenderProps } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
import type { Column, JSXElements } from '@/components/StdDesign/types'
import type { SiteCategory } from '@/api/site_category'
import domain from '@/api/domain'
import site_category from '@/api/site_category'
import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
import { datetime } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
import { input, select } from '@/components/StdDesign/StdDataEntry'
import InspectConfig from '@/views/config/InspectConfig.vue'
import SiteDuplicate from '@/views/site/components/SiteDuplicate.vue'
import { Badge, message } from 'ant-design-vue'
import columns from '@/views/site/site_list/columns'
import { message } from 'ant-design-vue'
const columns: Column[] = [{
title: () => $gettext('Name'),
dataIndex: 'name',
sorter: true,
pithy: true,
edit: {
type: input,
},
search: true,
}, {
title: () => $gettext('Status'),
dataIndex: 'enabled',
customRender: (args: CustomRenderProps) => {
const template: JSXElements = []
const { text } = args
if (text === true || text > 0) {
template.push(<Badge status="success" />)
template.push($gettext('Enabled'))
}
else {
template.push(<Badge status="warning" />)
template.push($gettext('Disabled'))
}
return h('div', template)
},
search: {
type: select,
mask: {
true: $gettext('Enabled'),
false: $gettext('Disabled'),
},
},
sorter: true,
pithy: true,
}, {
title: () => $gettext('Updated at'),
dataIndex: 'modified_at',
customRender: datetime,
sorter: true,
pithy: true,
}, {
title: () => $gettext('Action'),
dataIndex: 'action',
}]
const route = useRoute()
const router = useRouter()
const table = ref()
const inspect_config = ref()
const siteCategoryId = ref(Number.parseInt(route.query.site_category_id as string) || 0)
const siteCategories = ref([]) as Ref<SiteCategory[]>
watch(route, () => {
inspect_config.value?.test()
})
onMounted(async () => {
while (true) {
try {
const { data, pagination } = await site_category.get_list()
if (!data || !pagination)
return
siteCategories.value.push(...data)
if (data.length < pagination?.per_page) {
return
}
}
catch (e: any) {
message.error(e?.message ?? $gettext('Server error'))
return
}
}
})
function enable(name: string) {
domain.enable(name).then(() => {
message.success($gettext('Enabled successfully'))
@ -97,18 +77,17 @@ function handle_click_duplicate(name: string) {
show_duplicator.value = true
target.value = name
}
const route = useRoute()
watch(route, () => {
inspect_config.value?.test()
})
</script>
<template>
<ACard :title="$gettext('Manage Sites')">
<InspectConfig ref="inspect_config" />
<ATabs v-model:active-key="siteCategoryId">
<ATabPane :key="0" :tab="$gettext('All')" />
<ATabPane v-for="c in siteCategories" :key="c.id" :tab="c.name" />
</ATabs>
<StdTable
ref="table"
:api="domain"
@ -116,7 +95,10 @@ watch(route, () => {
row-key="name"
disable-delete
disable-view
@click-edit="r => $router.push({
:get-params="{
site_category_id: siteCategoryId,
}"
@click-edit="(r: string) => router.push({
path: `/sites/${r}`,
})"
>

View file

@ -0,0 +1,62 @@
import type { Column, JSXElements } from '@/components/StdDesign/types'
import {
actualValueRender,
type CustomRenderProps,
datetime,
} from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
import { input, select } from '@/components/StdDesign/StdDataEntry'
import { Badge } from 'ant-design-vue'
const columns: Column[] = [{
title: () => $gettext('Name'),
dataIndex: 'name',
sorter: true,
pithy: true,
edit: {
type: input,
},
search: true,
}, {
title: () => $gettext('Category'),
dataIndex: 'site_category_id',
customRender: actualValueRender('site_category.name'),
sorter: true,
pithy: true,
}, {
title: () => $gettext('Status'),
dataIndex: 'enabled',
customRender: (args: CustomRenderProps) => {
const template: JSXElements = []
const { text } = args
if (text === true || text > 0) {
template.push(<Badge status="success" />)
template.push($gettext('Enabled'))
}
else {
template.push(<Badge status="warning" />)
template.push($gettext('Disabled'))
}
return h('div', template)
},
search: {
type: select,
mask: {
true: $gettext('Enabled'),
false: $gettext('Disabled'),
},
},
sorter: true,
pithy: true,
}, {
title: () => $gettext('Updated at'),
dataIndex: 'modified_at',
customRender: datetime,
sorter: true,
pithy: true,
}, {
title: () => $gettext('Action'),
dataIndex: 'action',
}]
export default columns

View file

@ -56,6 +56,10 @@ export default defineConfig(({ mode }) => {
},
],
vueTemplate: true,
eslintrc: {
enabled: true,
filepath: '.eslint-auto-import.mjs',
},
}),
DefineOptions(),
],

View file

@ -1,6 +1,7 @@
package config
import (
"github.com/0xJacky/Nginx-UI/model"
"github.com/sashabaranov/go-openai"
"time"
)
@ -13,5 +14,7 @@ type Config struct {
ModifiedAt time.Time `json:"modified_at"`
Size int64 `json:"size,omitempty"`
IsDir bool `json:"is_dir"`
SiteCategoryID uint64 `json:"site_category_id"`
SiteCategory *model.SiteCategory `json:"site_category,omitempty"`
Enabled bool `json:"enabled"`
}