Skip to content

Commit

Permalink
feat(manager): auto detect invalid schema
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Feb 12, 2022
1 parent f88d114 commit 242c91f
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 89 deletions.
29 changes: 28 additions & 1 deletion plugins/frontend/components/client/form/form.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
<template>
<k-comment v-if="!schema" type="warning">
此插件未声明配置项,这可能并非预期行为,请联系插件作者。
</k-comment>
<k-comment v-else-if="!isValid(schema)" type="warning">
部分配置项无法正常显示,这可能并非预期行为,请联系插件作者。
</k-comment>
<form class="k-form">
<h2 v-if="showHeader ?? isHeadless(schema)">基础设置</h2>
<slot name="header"></slot>
Expand Down Expand Up @@ -28,11 +34,32 @@ const config = computed({
function isHeadless(schema: Schema) {
if (!schema) return false
if (schema.type === 'object') return !schema.meta.description
if (schema.type === 'object') return Object.keys(schema.dict).length && !schema.meta.description
if (schema.type === 'intersect') return isHeadless(schema.list[0])
return true
}
const primitive = ['string', 'number', 'boolean']
const dynamic = ['function', 'transform']
function isValid(schema: Schema) {
if (!schema || schema.meta.hidden) return true
if (primitive.includes(schema.type)) {
return true
} else if (['array', 'dict'].includes(schema.type)) {
return primitive.includes(schema.inner.type)
} else if (schema.type === 'object') {
return Object.values(schema.dict).every(isValid)
} else if (schema.type === 'intersect') {
return schema.list.every(isValid)
} else if (schema.type === 'union') {
const choices = schema.list.filter(item => !dynamic.includes(item.type))
return choices.length === 1 && isValid(choices[0]) || choices.every(item => item.type === 'const')
} else {
return false
}
}
</script>

<style lang="scss">
Expand Down
2 changes: 1 addition & 1 deletion plugins/frontend/manager/client/settings/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<template #aside>
<plugin-select v-model="current"></plugin-select>
</template>
<plugin-settings :current="current"></plugin-settings>
<plugin-settings :key="current" :current="current"></plugin-settings>
</k-card-aside>
</template>

Expand Down
9 changes: 5 additions & 4 deletions plugins/frontend/manager/client/settings/select.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@
<k-tab-group
:data="packages" v-model="model"
:filter="data => data.id" #="data">
<span :class="{ readonly: isReadonly(data) }">{{ data.shortname }}</span>
<span>{{ data.shortname }}</span>
</k-tab-group>
<div class="k-tab-group-title">
未运行的插件
<k-hint placement="right" name="filter" v-model="filtered">
<template v-if="filtered">
<b>筛选:已开启</b><br>只显示可在线配置的插件
<b>筛选:已开启</b><br>只显示当前可启用的插件
</template>
<template v-else>
<b>筛选:已关闭</b><br>显示所有已安装的插件
<b>筛选:已关闭</b><br>显示所有已下载的插件
</template>
</k-hint>
</div>
Expand All @@ -41,6 +41,7 @@
import { ref, computed, onActivated, nextTick } from 'vue'
import { store } from '~/client'
import { envMap } from './utils'
const props = defineProps<{
modelValue: string
Expand All @@ -65,7 +66,7 @@ const filtered = ref(false)
const keyword = ref('')
function isReadonly(data: any) {
return !data.root && data.id
return envMap.value[data.name].invalid
}
onActivated(async () => {
Expand Down
86 changes: 3 additions & 83 deletions plugins/frontend/manager/client/settings/settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -69,104 +69,24 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { Dict } from 'koishi'
import { store, send } from '~/client'
import { MarketProvider, Package } from '@koishijs/plugin-manager'
import KDepLink from './dep-link.vue'
import { envMap } from './utils'
import { getMixedMeta } from '../utils'
import KDepLink from './dep-link.vue'
const props = defineProps<{
current: string
}>()
const data = computed(() => getMixedMeta(props.current))
const env = computed(() => envMap.value[props.current])
const version = ref('')
watch(data, (value) => {
version.value = value.version
}, { immediate: true })
function getKeywords(prefix: string, meta: Package.Meta = data.value) {
prefix += ':'
return meta.keywords
.filter(name => name.startsWith(prefix))
.map(name => name.slice(prefix.length))
}
interface DepInfo {
name: string
required: boolean
fulfilled: boolean
}
interface ServiceDepInfo extends DepInfo {
available?: string[]
}
interface PluginDepInfo extends DepInfo {
local?: boolean
}
interface EnvInfo {
impl: string[]
deps: Dict<PluginDepInfo>
using: Dict<ServiceDepInfo>
invalid?: boolean
console?: boolean
}
function isAvailable(name: string, remote: MarketProvider.Data) {
return getKeywords('impl', {
...remote.versions[0],
...store.packages[remote.name],
}).includes(name)
}
const env = computed(() => {
function setService(name: string, required: boolean) {
if (name === 'console') {
result.console = true
return
}
const fulfilled = name in store.services
if (required && !fulfilled) result.invalid = true
result.using[name] = { name, required, fulfilled }
if (!fulfilled) {
result.using[name].available = Object.values(store.market || {})
.filter(data => isAvailable(name, data))
.map(data => data.name)
}
}
const result: EnvInfo = { impl: [], using: {}, deps: {} }
for (const name of getKeywords('impl')) {
if (name === 'adapter') continue
result.impl.push(name)
if (name in store.services && !data.value.id) {
result.invalid = true
}
}
for (const name of getKeywords('required')) {
setService(name, true)
}
for (const name of getKeywords('optional')) {
setService(name, false)
}
for (const name of data.value.peerDeps) {
if (name === '@koishijs/plugin-console') continue
const available = name in store.packages
const fulfilled = !!store.packages[name]?.id
if (!fulfilled) result.invalid = true
result.deps[name] = { name, required: true, fulfilled, local: available }
for (const impl of getKeywords('impl', getMixedMeta(name))) {
delete result.using[impl]
}
}
return result
})
function execute(event: string) {
const { shortname, config } = data.value
send('manager/plugin-' + event, shortname, config)
Expand Down
96 changes: 96 additions & 0 deletions plugins/frontend/manager/client/settings/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Dict } from 'koishi'
import { computed } from 'vue'
import { MarketProvider, Package } from '@koishijs/plugin-manager'
import { store } from '~/client'
import { getMixedMeta } from '../utils'

interface DepInfo {
name: string
required: boolean
fulfilled: boolean
}

interface ServiceDepInfo extends DepInfo {
available?: string[]
}

interface PluginDepInfo extends DepInfo {
local?: boolean
}

export interface EnvInfo {
impl: string[]
deps: Dict<PluginDepInfo>
using: Dict<ServiceDepInfo>
invalid?: boolean
console?: boolean
}

function getKeywords(prefix: string, meta: Package.Meta) {
prefix += ':'
return meta.keywords
.filter(name => name.startsWith(prefix))
.map(name => name.slice(prefix.length))
}

function isAvailable(name: string, remote: MarketProvider.Data) {
return getKeywords('impl', {
...remote.versions[0],
...store.packages[remote.name],
}).includes(name)
}

function getEnvInfo(name: string) {
function setService(name: string, required: boolean) {
if (name === 'console') {
result.console = true
return
}

const fulfilled = name in store.services
if (required && !fulfilled) result.invalid = true
result.using[name] = { name, required, fulfilled }
if (!fulfilled) {
result.using[name].available = Object.values(store.market || {})
.filter(data => isAvailable(name, data))
.map(data => data.name)
}
}

// check implementations
const data = getMixedMeta(name)
const result: EnvInfo = { impl: [], using: {}, deps: {} }
for (const name of getKeywords('impl', data)) {
if (name === 'adapter') continue
result.impl.push(name)
if (name in store.services && !data.id) {
result.invalid = true
}
}

// check services
for (const name of getKeywords('required', data)) {
setService(name, true)
}
for (const name of getKeywords('optional', data)) {
setService(name, false)
}

// check dependencies
for (const name of data.peerDeps) {
if (name === '@koishijs/plugin-console') continue
const available = name in store.packages
const fulfilled = !!store.packages[name]?.id
if (!fulfilled) result.invalid = true
result.deps[name] = { name, required: true, fulfilled, local: available }
for (const impl of getKeywords('impl', getMixedMeta(name))) {
delete result.using[impl]
}
}

return result
}

export const envMap = computed(() => {
return Object.fromEntries(Object.keys(store.packages).map(name => [name, getEnvInfo(name)]))
})

0 comments on commit 242c91f

Please sign in to comment.