Wizard: choose "create tables here" vs "connect your own database" per plugin
Answers the confusion of asking for a DB connection while also offering to create the tables. Each self-host-capable plugin (employees, usb) now shows a mode choice; the external connection fields appear only for "connect your own database". Default is self-hosted (create tables here) - the external path is the niche/our-site option. - provisioning_note gains mode_setting; employee_directory_mode + usb_directory_ mode settings (both default 'selfhosted'). - Wizard renders the radio, shows the note for self-hosted and the config fields for external, and saves the chosen mode. Employees works fully in both modes. USB self-hosted ROUTING is still TODO - the USB routes read the external cmmc_usb schema; wiring them to the app-owned tables is the remaining work (tracked). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -42,33 +42,43 @@
|
|||||||
<span class="plugin-name">{{ p.name }}</span>
|
<span class="plugin-name">{{ p.name }}</span>
|
||||||
<span class="plugin-desc">{{ p.description }}</span>
|
<span class="plugin-desc">{{ p.description }}</span>
|
||||||
</label>
|
</label>
|
||||||
<!-- Transparency note for plugins that provision extra tables -->
|
|
||||||
<div v-if="p.enabled && p.provisioning_note" class="provision-note">
|
<!-- Plugins that provision tables: choose to create them here or
|
||||||
<p>{{ p.provisioning_note.note }}</p>
|
connect an existing database -->
|
||||||
<p class="provision-tables">
|
<div v-if="p.enabled && p.provisioning_note && p.provisioning_note.mode_setting" class="plugin-mode">
|
||||||
Creates in shopdb:
|
<label class="mode-opt">
|
||||||
<code v-for="t in p.provisioning_note.tables" :key="t">{{ t }}</code>
|
<input type="radio" value="selfhosted" v-model="pluginModes[p.provisioning_note.mode_setting]" />
|
||||||
</p>
|
Create the tables here <span class="hint">(recommended)</span>
|
||||||
<p v-if="p.provisioning_note.docs" class="provision-docs">Schema: <code>{{ p.provisioning_note.docs }}</code></p>
|
</label>
|
||||||
|
<label class="mode-opt">
|
||||||
|
<input type="radio" value="external" v-model="pluginModes[p.provisioning_note.mode_setting]" />
|
||||||
|
Connect to your own database
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<!-- Self-hosted: transparency note -->
|
||||||
|
<div v-if="pluginModes[p.provisioning_note.mode_setting] !== 'external'" class="provision-note">
|
||||||
|
<p>{{ p.provisioning_note.note }}</p>
|
||||||
|
<p class="provision-tables">Creates in shopdb: <code v-for="t in p.provisioning_note.tables" :key="t">{{ t }}</code></p>
|
||||||
|
<p v-if="p.provisioning_note.docs" class="provision-docs">Schema: <code>{{ p.provisioning_note.docs }}</code></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- External: connection config -->
|
||||||
|
<div v-else class="config-block">
|
||||||
|
<div v-for="field in p.config_schema" :key="field.key" class="form-group">
|
||||||
|
<label>{{ field.label }}<span v-if="field.secret" class="secret-tag">secret</span></label>
|
||||||
|
<input
|
||||||
|
:type="field.type === 'password' ? 'password' : (field.type === 'number' ? 'number' : 'text')"
|
||||||
|
:placeholder="field.default || ''"
|
||||||
|
v-model="pluginConfig[field.key]"
|
||||||
|
class="form-control" />
|
||||||
|
<small v-if="field.help" class="hint">{{ field.help }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p v-else class="hint">No optional plugins found.</p>
|
<p v-else class="hint">No optional plugins found.</p>
|
||||||
|
|
||||||
<!-- Per-plugin config for enabled plugins that declare a schema -->
|
|
||||||
<div v-for="p in configurablePlugins" :key="`config-${p.name}`" class="config-block">
|
|
||||||
<h3 class="config-title">{{ p.name }} configuration</h3>
|
|
||||||
<div v-for="field in p.config_schema" :key="field.key" class="form-group">
|
|
||||||
<label>{{ field.label }}<span v-if="field.secret" class="secret-tag">secret</span></label>
|
|
||||||
<input
|
|
||||||
:type="field.type === 'password' ? 'password' : (field.type === 'number' ? 'number' : 'text')"
|
|
||||||
:placeholder="field.default || ''"
|
|
||||||
v-model="pluginConfig[field.key]"
|
|
||||||
class="form-control" />
|
|
||||||
<small v-if="field.help" class="hint">{{ field.help }}</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Secrets are never stored in the DB; emit .env lines to paste -->
|
<!-- Secrets are never stored in the DB; emit .env lines to paste -->
|
||||||
<div v-if="envLines" class="env-block">
|
<div v-if="envLines" class="env-block">
|
||||||
<p class="hint">Paste these into <code>.env</code> and restart the app (secrets are not stored in the database):</p>
|
<p class="hint">Paste these into <code>.env</code> and restart the app (secrets are not stored in the database):</p>
|
||||||
@@ -153,13 +163,21 @@ const form = ref({
|
|||||||
})
|
})
|
||||||
const plugins = ref([])
|
const plugins = ref([])
|
||||||
const pluginConfig = ref({}) // flat map: setting/field key -> value
|
const pluginConfig = ref({}) // flat map: setting/field key -> value
|
||||||
|
const pluginModes = ref({}) // mode_setting key -> 'selfhosted' | 'external'
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const seeding = ref(false)
|
const seeding = ref(false)
|
||||||
const seedResult = ref('')
|
const seedResult = ref('')
|
||||||
|
|
||||||
// Enabled plugins that declare config fields.
|
function modeOf(plugin) {
|
||||||
|
const key = plugin.provisioning_note?.mode_setting
|
||||||
|
return key ? (pluginModes.value[key] || 'selfhosted') : null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enabled plugins whose external connection config should be collected right
|
||||||
|
// now: those in external mode (or with config but no mode concept).
|
||||||
const configurablePlugins = computed(() =>
|
const configurablePlugins = computed(() =>
|
||||||
plugins.value.filter(p => p.enabled && (p.config_schema || []).length))
|
plugins.value.filter(p => p.enabled && (p.config_schema || []).length &&
|
||||||
|
(!p.provisioning_note?.mode_setting || modeOf(p) === 'external')))
|
||||||
|
|
||||||
// .env lines for secret fields that have a value entered.
|
// .env lines for secret fields that have a value entered.
|
||||||
const envLines = computed(() => {
|
const envLines = computed(() => {
|
||||||
@@ -196,8 +214,15 @@ onMounted(async () => {
|
|||||||
plugins.value = response.data.data?.plugins || response.data.data || []
|
plugins.value = response.data.data?.plugins || response.data.data || []
|
||||||
} catch (err) { /* ignore */ }
|
} catch (err) { /* ignore */ }
|
||||||
|
|
||||||
// Preload current values for non-secret plugin config fields.
|
// Preload directory mode (defaults self-hosted) + non-secret config fields.
|
||||||
for (const plugin of plugins.value) {
|
for (const plugin of plugins.value) {
|
||||||
|
const modeKey = plugin.provisioning_note?.mode_setting
|
||||||
|
if (modeKey && pluginModes.value[modeKey] === undefined) {
|
||||||
|
try {
|
||||||
|
const response = await settingsApi.get(modeKey)
|
||||||
|
pluginModes.value[modeKey] = response.data?.data?.value || 'selfhosted'
|
||||||
|
} catch (err) { pluginModes.value[modeKey] = 'selfhosted' }
|
||||||
|
}
|
||||||
for (const field of (plugin.config_schema || [])) {
|
for (const field of (plugin.config_schema || [])) {
|
||||||
if (field.secret) continue
|
if (field.secret) continue
|
||||||
try {
|
try {
|
||||||
@@ -210,8 +235,12 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function saveStep() {
|
async function saveStep() {
|
||||||
// Plugins step: persist non-secret config fields (secrets stay in .env).
|
// Plugins step: save each plugin's directory mode, plus non-secret external
|
||||||
|
// connection config (secrets stay in .env).
|
||||||
if (current.value.key === 'plugins') {
|
if (current.value.key === 'plugins') {
|
||||||
|
for (const [modeKey, modeValue] of Object.entries(pluginModes.value)) {
|
||||||
|
await settingsApi.update(modeKey, modeValue)
|
||||||
|
}
|
||||||
for (const plugin of configurablePlugins.value) {
|
for (const plugin of configurablePlugins.value) {
|
||||||
for (const field of plugin.config_schema) {
|
for (const field of plugin.config_schema) {
|
||||||
if (field.secret) continue
|
if (field.secret) continue
|
||||||
@@ -319,6 +348,10 @@ async function finish() {
|
|||||||
.plugin-row { display: grid; grid-template-columns: auto auto 1fr; gap: 0.6rem; align-items: baseline; padding: 0.5rem 0.6rem; background: var(--bg); border-radius: 6px; }
|
.plugin-row { display: grid; grid-template-columns: auto auto 1fr; gap: 0.6rem; align-items: baseline; padding: 0.5rem 0.6rem; background: var(--bg); border-radius: 6px; }
|
||||||
.plugin-name { font-weight: 600; text-transform: capitalize; }
|
.plugin-name { font-weight: 600; text-transform: capitalize; }
|
||||||
.plugin-desc { color: var(--text-light); font-size: 0.85rem; }
|
.plugin-desc { color: var(--text-light); font-size: 0.85rem; }
|
||||||
|
.plugin-mode { margin: 0.4rem 0 0.2rem 1.9rem; }
|
||||||
|
.mode-opt { display: block; font-size: 0.86rem; margin-bottom: 0.3rem; cursor: pointer; }
|
||||||
|
.mode-opt input { margin-right: 0.4rem; }
|
||||||
|
.plugin-mode .provision-note, .plugin-mode .config-block { margin-left: 0; }
|
||||||
.provision-note { margin: 0.3rem 0 0.2rem 1.9rem; padding: 0.6rem 0.75rem; background: var(--bg); border-left: 3px solid var(--warning); border-radius: 4px; font-size: 0.82rem; }
|
.provision-note { margin: 0.3rem 0 0.2rem 1.9rem; padding: 0.6rem 0.75rem; background: var(--bg); border-left: 3px solid var(--warning); border-radius: 4px; font-size: 0.82rem; }
|
||||||
.provision-note p { margin: 0 0 0.35rem; }
|
.provision-note p { margin: 0 0 0.35rem; }
|
||||||
.provision-note p:last-child { margin-bottom: 0; }
|
.provision-note p:last-child { margin-bottom: 0; }
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ class EmployeesPlugin(BasePlugin):
|
|||||||
|
|
||||||
def get_provisioning_note(self) -> Optional[Dict]:
|
def get_provisioning_note(self) -> Optional[Dict]:
|
||||||
return {
|
return {
|
||||||
|
'mode_setting': 'employee_directory_mode',
|
||||||
'tables': ['directoryemployees'],
|
'tables': ['directoryemployees'],
|
||||||
'note': ('Enabling this in self-hosted mode creates a '
|
'note': ('Enabling this in self-hosted mode creates a '
|
||||||
'"directoryemployees" table in the shopdb database (SSO, '
|
'"directoryemployees" table in the shopdb database (SSO, '
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ class USBPlugin(BasePlugin):
|
|||||||
|
|
||||||
def get_provisioning_note(self) -> Optional[Dict]:
|
def get_provisioning_note(self) -> Optional[Dict]:
|
||||||
return {
|
return {
|
||||||
|
'mode_setting': 'usb_directory_mode',
|
||||||
'tables': ['usbdevicetypes', 'usbdevices', 'usbcheckouts'],
|
'tables': ['usbdevicetypes', 'usbdevices', 'usbcheckouts'],
|
||||||
'note': ('Enabling this creates USB tracking tables in the shopdb '
|
'note': ('Enabling this creates USB tracking tables in the shopdb '
|
||||||
'database (usbdevicetypes, usbdevices, usbcheckouts) for CMMC '
|
'database (usbdevicetypes, usbdevices, usbcheckouts) for CMMC '
|
||||||
|
|||||||
@@ -265,10 +265,17 @@ def build_default_settings():
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
'key': 'employee_directory_mode',
|
'key': 'employee_directory_mode',
|
||||||
'value': 'external',
|
'value': 'selfhosted',
|
||||||
'valuetype': 'string',
|
'valuetype': 'string',
|
||||||
'category': 'site',
|
'category': 'site',
|
||||||
'description': "Employee directory source: 'external' (a separate HR database) or 'selfhosted' (managed in-app under Employees)"
|
'description': "Employee directory source: 'selfhosted' (tables in this app, default) or 'external' (a separate HR database)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'key': 'usb_directory_mode',
|
||||||
|
'value': 'selfhosted',
|
||||||
|
'valuetype': 'string',
|
||||||
|
'category': 'site',
|
||||||
|
'description': "USB check-in/out source: 'selfhosted' (tables in this app, default) or 'external' (a separate cmmc_usb database)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'key': 'site_base_url',
|
'key': 'site_base_url',
|
||||||
|
|||||||
Reference in New Issue
Block a user