mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2025-05-11 10:25:52 +02:00
feat: add filter of category for sites list
This commit is contained in:
parent
207f80f858
commit
aa556767f2
10 changed files with 278 additions and 80 deletions
|
@ -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{
|
||||
|
|
|
@ -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)
|
||||
|
|
95
app/.eslint-auto-import.mjs
Normal file
95
app/.eslint-auto-import.mjs
Normal 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,
|
||||
},
|
||||
}
|
|
@ -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,
|
||||
{
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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'),
|
||||
},
|
||||
|
|
|
@ -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}`,
|
||||
})"
|
||||
>
|
62
app/src/views/site/site_list/columns.tsx
Normal file
62
app/src/views/site/site_list/columns.tsx
Normal 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
|
|
@ -56,6 +56,10 @@ export default defineConfig(({ mode }) => {
|
|||
},
|
||||
],
|
||||
vueTemplate: true,
|
||||
eslintrc: {
|
||||
enabled: true,
|
||||
filepath: '.eslint-auto-import.mjs',
|
||||
},
|
||||
}),
|
||||
DefineOptions(),
|
||||
],
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue