Add web UI to enable/disable plugins
GET /api/plugins now lists all discovered plugins (enabled or not) with status; PUT /api/plugins/<name> toggles enabled (persists to plugins.json). New Settings > Plugins admin page with per-plugin toggles. Route changes apply on next restart. Replaces CLI-only enable/disable for self-serve admin. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -674,6 +674,15 @@ export const employeesApi = {
|
||||
export const businessUnitsApi = businessunitsApi
|
||||
|
||||
// System Settings API
|
||||
export const pluginsApi = {
|
||||
list() {
|
||||
return api.get('/plugins')
|
||||
},
|
||||
setEnabled(name, enabled) {
|
||||
return api.put(`/plugins/${name}`, { enabled })
|
||||
}
|
||||
}
|
||||
|
||||
export const settingsApi = {
|
||||
list(params = {}) {
|
||||
return api.get('/settings', { params })
|
||||
|
||||
@@ -92,6 +92,12 @@ export default [
|
||||
component: () => import('../../views/settings/SystemSettings.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
},
|
||||
{
|
||||
path: 'settings/plugins',
|
||||
name: 'plugins',
|
||||
component: () => import('../../views/settings/PluginsList.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
},
|
||||
{
|
||||
path: 'settings/auditlogs',
|
||||
name: 'audit-logs',
|
||||
|
||||
140
frontend/src/views/settings/PluginsList.vue
Normal file
140
frontend/src/views/settings/PluginsList.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>Plugins</h2>
|
||||
<span v-if="contractVersion" class="contract-badge">contract v{{ contractVersion }}</span>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div v-if="loading" class="loading">Loading...</div>
|
||||
|
||||
<template v-else>
|
||||
<p class="hint">
|
||||
Disabling a plugin removes its pages and API on the next app restart.
|
||||
</p>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Plugin</th>
|
||||
<th>Version</th>
|
||||
<th>Description</th>
|
||||
<th>Enabled</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="p in plugins" :key="p.name">
|
||||
<td>{{ p.name }}</td>
|
||||
<td class="mono">{{ p.version }}</td>
|
||||
<td class="cell-truncate" :title="p.description">{{ p.description || '-' }}</td>
|
||||
<td>
|
||||
<button
|
||||
class="toggle-btn"
|
||||
:class="{ active: p.enabled }"
|
||||
:disabled="saving"
|
||||
@click="toggle(p)"
|
||||
>
|
||||
<span class="toggle-slider"></span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-if="error" class="error-message">{{ error }}</div>
|
||||
<div v-if="message" class="success-message">{{ message }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { pluginsApi } from '../../api'
|
||||
|
||||
const plugins = ref([])
|
||||
const contractVersion = ref('')
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const error = ref('')
|
||||
const message = ref('')
|
||||
|
||||
onMounted(() => load())
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await pluginsApi.list()
|
||||
plugins.value = response.data.data.plugins || []
|
||||
contractVersion.value = response.data.data.contract_version || ''
|
||||
} catch (err) {
|
||||
error.value = 'Failed to load plugins'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function toggle(p) {
|
||||
error.value = ''
|
||||
message.value = ''
|
||||
saving.value = true
|
||||
try {
|
||||
const response = await pluginsApi.setEnabled(p.name, !p.enabled)
|
||||
p.enabled = !p.enabled
|
||||
message.value = response.data.message || 'Updated'
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.data?.error?.message || 'Failed to update plugin'
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mono {
|
||||
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: var(--text-light);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.contract-badge {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-light);
|
||||
background: var(--bg);
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
width: 44px;
|
||||
height: 24px;
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
background: var(--border);
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.toggle-btn.active {
|
||||
background: var(--success);
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.toggle-btn.active .toggle-slider {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
</style>
|
||||
@@ -75,6 +75,12 @@
|
||||
<p>Configure integrations and system options</p>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/settings/plugins" class="settings-card">
|
||||
<div class="card-icon"><Puzzle :size="28" /></div>
|
||||
<h3>Plugins</h3>
|
||||
<p>Enable or disable installed plugins</p>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/settings/auditlogs" class="settings-card">
|
||||
<div class="card-icon"><FileText :size="28" /></div>
|
||||
<h3>Audit Logs</h3>
|
||||
@@ -91,7 +97,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Factory, MapPin, Tag, Package, Droplets, Monitor, Laptop, Cog, Building, Globe, Link, Settings, FileText, Users } from 'lucide-vue-next'
|
||||
import { Factory, MapPin, Tag, Package, Droplets, Monitor, Laptop, Cog, Building, Globe, Link, Settings, FileText, Users, Puzzle } from 'lucide-vue-next'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user