mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2025-05-12 10:55:51 +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
|
s := query.Site
|
||||||
site, err := s.Where(s.Path.Eq(path)).FirstOrInit()
|
site, err := s.Where(s.Path.Eq(path)).FirstOrCreate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.ErrHandler(c, err)
|
api.ErrHandler(c, err)
|
||||||
return
|
return
|
||||||
|
@ -300,6 +300,14 @@ func DeleteSite(c *gin.Context) {
|
||||||
var err error
|
var err error
|
||||||
name := c.Param("name")
|
name := c.Param("name")
|
||||||
availablePath := nginx.GetConfPath("sites-available", 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)
|
enabledPath := nginx.GetConfPath("sites-enabled", name)
|
||||||
if _, err = os.Stat(availablePath); os.IsNotExist(err) {
|
if _, err = os.Stat(availablePath); os.IsNotExist(err) {
|
||||||
c.JSON(http.StatusNotFound, gin.H{
|
c.JSON(http.StatusNotFound, gin.H{
|
||||||
|
|
|
@ -4,9 +4,14 @@ import (
|
||||||
"github.com/0xJacky/Nginx-UI/api"
|
"github.com/0xJacky/Nginx-UI/api"
|
||||||
"github.com/0xJacky/Nginx-UI/internal/config"
|
"github.com/0xJacky/Nginx-UI/internal/config"
|
||||||
"github.com/0xJacky/Nginx-UI/internal/nginx"
|
"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/gin-gonic/gin"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
"github.com/spf13/cast"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -15,6 +20,7 @@ func GetSiteList(c *gin.Context) {
|
||||||
enabled := c.Query("enabled")
|
enabled := c.Query("enabled")
|
||||||
orderBy := c.Query("order_by")
|
orderBy := c.Query("order_by")
|
||||||
sort := c.DefaultQuery("sort", "desc")
|
sort := c.DefaultQuery("sort", "desc")
|
||||||
|
querySiteCategoryId := cast.ToUint64(c.Query("site_category_id"))
|
||||||
|
|
||||||
configFiles, err := os.ReadDir(nginx.GetConfPath("sites-available"))
|
configFiles, err := os.ReadDir(nginx.GetConfPath("sites-available"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -28,6 +34,20 @@ func GetSiteList(c *gin.Context) {
|
||||||
return
|
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)
|
enabledConfigMap := make(map[string]bool)
|
||||||
for i := range enabledConfig {
|
for i := range enabledConfig {
|
||||||
enabledConfigMap[enabledConfig[i].Name()] = true
|
enabledConfigMap[enabledConfig[i].Name()] = true
|
||||||
|
@ -38,7 +58,9 @@ func GetSiteList(c *gin.Context) {
|
||||||
for i := range configFiles {
|
for i := range configFiles {
|
||||||
file := configFiles[i]
|
file := configFiles[i]
|
||||||
fileInfo, _ := file.Info()
|
fileInfo, _ := file.Info()
|
||||||
if !file.IsDir() {
|
if file.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
// name filter
|
// name filter
|
||||||
if name != "" && !strings.Contains(file.Name(), name) {
|
if name != "" && !strings.Contains(file.Name(), name) {
|
||||||
continue
|
continue
|
||||||
|
@ -52,15 +74,31 @@ func GetSiteList(c *gin.Context) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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{
|
configs = append(configs, config.Config{
|
||||||
Name: file.Name(),
|
Name: file.Name(),
|
||||||
ModifiedAt: fileInfo.ModTime(),
|
ModifiedAt: fileInfo.ModTime(),
|
||||||
Size: fileInfo.Size(),
|
Size: fileInfo.Size(),
|
||||||
IsDir: fileInfo.IsDir(),
|
IsDir: fileInfo.IsDir(),
|
||||||
Enabled: enabledConfigMap[file.Name()],
|
Enabled: enabledConfigMap[file.Name()],
|
||||||
|
SiteCategoryID: siteCategoryId,
|
||||||
|
SiteCategory: siteCategory,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
configs = config.Sort(orderBy, sort, configs)
|
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 createConfig from '@antfu/eslint-config'
|
||||||
import sonarjs from 'eslint-plugin-sonarjs'
|
import sonarjs from 'eslint-plugin-sonarjs'
|
||||||
|
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'],
|
||||||
|
languageOptions: {
|
||||||
|
globals: autoImport.globals,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
sonarjs.configs.recommended,
|
sonarjs.configs.recommended,
|
||||||
{
|
{
|
||||||
|
|
|
@ -48,8 +48,10 @@ export function mask(maskObj: any): (args: CustomRenderProps) => JSX.Element {
|
||||||
export function arrayToTextRender(args: CustomRenderProps) {
|
export function arrayToTextRender(args: CustomRenderProps) {
|
||||||
return args.text?.join(', ')
|
return args.text?.join(', ')
|
||||||
}
|
}
|
||||||
export function actualValueRender(args: CustomRenderProps, actualDataIndex: string | string[]) {
|
export function actualValueRender(actualDataIndex: string | string[]) {
|
||||||
return get(args.record, actualDataIndex)
|
return (args: CustomRenderProps) => {
|
||||||
|
return get(args.record, actualDataIndex) || '/'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function longTextWithEllipsis(len: number): (args: CustomRenderProps) => JSX.Element {
|
export function longTextWithEllipsis(len: number): (args: CustomRenderProps) => JSX.Element {
|
||||||
|
|
|
@ -52,7 +52,7 @@ export const routes: RouteRecordRaw[] = [
|
||||||
children: [{
|
children: [{
|
||||||
path: 'list',
|
path: 'list',
|
||||||
name: 'Sites List',
|
name: 'Sites List',
|
||||||
component: () => import('@/views/site/SiteList.vue'),
|
component: () => import('@/views/site/site_list/SiteList.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
name: () => $gettext('Sites List'),
|
name: () => $gettext('Sites List'),
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,64 +1,44 @@
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import type { CustomRenderProps } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
|
import type { SiteCategory } from '@/api/site_category'
|
||||||
import type { Column, JSXElements } from '@/components/StdDesign/types'
|
|
||||||
import domain from '@/api/domain'
|
import domain from '@/api/domain'
|
||||||
|
import site_category from '@/api/site_category'
|
||||||
import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
|
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 InspectConfig from '@/views/config/InspectConfig.vue'
|
||||||
import SiteDuplicate from '@/views/site/components/SiteDuplicate.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[] = [{
|
const route = useRoute()
|
||||||
title: () => $gettext('Name'),
|
const router = useRouter()
|
||||||
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 table = ref()
|
const table = ref()
|
||||||
|
|
||||||
const inspect_config = 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) {
|
function enable(name: string) {
|
||||||
domain.enable(name).then(() => {
|
domain.enable(name).then(() => {
|
||||||
message.success($gettext('Enabled successfully'))
|
message.success($gettext('Enabled successfully'))
|
||||||
|
@ -97,18 +77,17 @@ function handle_click_duplicate(name: string) {
|
||||||
show_duplicator.value = true
|
show_duplicator.value = true
|
||||||
target.value = name
|
target.value = name
|
||||||
}
|
}
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
|
|
||||||
watch(route, () => {
|
|
||||||
inspect_config.value?.test()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ACard :title="$gettext('Manage Sites')">
|
<ACard :title="$gettext('Manage Sites')">
|
||||||
<InspectConfig ref="inspect_config" />
|
<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
|
<StdTable
|
||||||
ref="table"
|
ref="table"
|
||||||
:api="domain"
|
:api="domain"
|
||||||
|
@ -116,7 +95,10 @@ watch(route, () => {
|
||||||
row-key="name"
|
row-key="name"
|
||||||
disable-delete
|
disable-delete
|
||||||
disable-view
|
disable-view
|
||||||
@click-edit="r => $router.push({
|
:get-params="{
|
||||||
|
site_category_id: siteCategoryId,
|
||||||
|
}"
|
||||||
|
@click-edit="(r: string) => router.push({
|
||||||
path: `/sites/${r}`,
|
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,
|
vueTemplate: true,
|
||||||
|
eslintrc: {
|
||||||
|
enabled: true,
|
||||||
|
filepath: '.eslint-auto-import.mjs',
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
DefineOptions(),
|
DefineOptions(),
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/0xJacky/Nginx-UI/model"
|
||||||
"github.com/sashabaranov/go-openai"
|
"github.com/sashabaranov/go-openai"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
@ -13,5 +14,7 @@ type Config struct {
|
||||||
ModifiedAt time.Time `json:"modified_at"`
|
ModifiedAt time.Time `json:"modified_at"`
|
||||||
Size int64 `json:"size,omitempty"`
|
Size int64 `json:"size,omitempty"`
|
||||||
IsDir bool `json:"is_dir"`
|
IsDir bool `json:"is_dir"`
|
||||||
|
SiteCategoryID uint64 `json:"site_category_id"`
|
||||||
|
SiteCategory *model.SiteCategory `json:"site_category,omitempty"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue