Initial commit
This commit is contained in:
15
src/integrations.ts
Normal file
15
src/integrations.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { ModuleIntegrations } from "@KTXC/types/moduleTypes";
|
||||
|
||||
const integrations: ModuleIntegrations = {
|
||||
admin_settings_menu: [
|
||||
{
|
||||
id: 'module_manager',
|
||||
label: 'Modules',
|
||||
path: '/modules',
|
||||
icon: 'mdi-package-variant',
|
||||
priority: 100,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default integrations;
|
||||
14
src/main.ts
Normal file
14
src/main.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import routes from '@/routes'
|
||||
import integrations from '@/integrations'
|
||||
import type { App as Vue } from 'vue'
|
||||
|
||||
// CSS filename is injected by the vite plugin at build time
|
||||
export const css = ['__CSS_FILENAME_PLACEHOLDER__']
|
||||
|
||||
export { routes, integrations }
|
||||
|
||||
export default {
|
||||
install(app: Vue) {
|
||||
// Module-specific plugins can be registered here
|
||||
}
|
||||
}
|
||||
9
src/routes.ts
Normal file
9
src/routes.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
const routes = [
|
||||
{
|
||||
name: 'module_manager',
|
||||
path: '/modules',
|
||||
component: () => import('@/views/ModuleList.vue'),
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
40
src/services/moduleService.ts
Normal file
40
src/services/moduleService.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { Module, ModuleAction } from '@/types/module'
|
||||
|
||||
const API_BASE = '/modules'
|
||||
|
||||
export async function fetchModules(): Promise<Module[]> {
|
||||
const response = await fetch(`${API_BASE}/list`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch modules: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data.modules || []
|
||||
}
|
||||
|
||||
export async function manageModule(handle: string, action: ModuleAction): Promise<{ message?: string; error?: string }> {
|
||||
const formData = new FormData()
|
||||
formData.append('handle', handle)
|
||||
formData.append('action', action)
|
||||
|
||||
const response = await fetch(`${API_BASE}/manage`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || `Failed to ${action} module`)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
1
src/style.css
Normal file
1
src/style.css
Normal file
@@ -0,0 +1 @@
|
||||
/* module_manager module styles */
|
||||
11
src/types/module.ts
Normal file
11
src/types/module.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export interface Module {
|
||||
handle: string
|
||||
label: string
|
||||
author: string
|
||||
description: string
|
||||
version: string
|
||||
installed: boolean
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export type ModuleAction = 'install' | 'uninstall' | 'enable' | 'disable' | 'upgrade'
|
||||
302
src/views/ModuleList.vue
Normal file
302
src/views/ModuleList.vue
Normal file
@@ -0,0 +1,302 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { fetchModules, manageModule } from '@/services/moduleService'
|
||||
import type { Module, ModuleAction } from '@/types/module'
|
||||
import {
|
||||
mdiPackageVariant,
|
||||
mdiDownload,
|
||||
mdiTrashCan,
|
||||
mdiCheckCircle,
|
||||
mdiCloseCircle,
|
||||
mdiUpdate,
|
||||
mdiLoading,
|
||||
} from '@mdi/js'
|
||||
|
||||
const modules = ref<Module[]>([])
|
||||
const loading = ref(true)
|
||||
const error = ref<string | null>(null)
|
||||
const actionLoading = ref<string | null>(null)
|
||||
const snackbar = ref(false)
|
||||
const snackbarText = ref('')
|
||||
const snackbarColor = ref('success')
|
||||
const search = ref('')
|
||||
const filterStatus = ref<'all' | 'installed' | 'not-installed' | 'enabled' | 'disabled'>('all')
|
||||
|
||||
const filteredModules = computed(() => {
|
||||
let result = modules.value
|
||||
|
||||
// Filter by status
|
||||
if (filterStatus.value === 'installed') {
|
||||
result = result.filter(m => m.installed)
|
||||
} else if (filterStatus.value === 'not-installed') {
|
||||
result = result.filter(m => !m.installed)
|
||||
} else if (filterStatus.value === 'enabled') {
|
||||
result = result.filter(m => m.enabled)
|
||||
} else if (filterStatus.value === 'disabled') {
|
||||
result = result.filter(m => m.installed && !m.enabled)
|
||||
}
|
||||
|
||||
// Filter by search
|
||||
if (search.value) {
|
||||
const searchLower = search.value.toLowerCase()
|
||||
result = result.filter(
|
||||
m =>
|
||||
m.handle.toLowerCase().includes(searchLower) ||
|
||||
m.label.toLowerCase().includes(searchLower) ||
|
||||
m.description.toLowerCase().includes(searchLower) ||
|
||||
m.author.toLowerCase().includes(searchLower)
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
async function loadModules() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
modules.value = await fetchModules()
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : 'Failed to load modules'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAction(module: Module, action: ModuleAction) {
|
||||
const key = `${module.handle}-${action}`
|
||||
actionLoading.value = key
|
||||
|
||||
try {
|
||||
const result = await manageModule(module.handle, action)
|
||||
|
||||
snackbarText.value = result.message || `Module ${action}ed successfully`
|
||||
snackbarColor.value = 'success'
|
||||
snackbar.value = true
|
||||
|
||||
// Reload modules to get updated state
|
||||
await loadModules()
|
||||
} catch (e) {
|
||||
snackbarText.value = e instanceof Error ? e.message : `Failed to ${action} module`
|
||||
snackbarColor.value = 'error'
|
||||
snackbar.value = true
|
||||
} finally {
|
||||
actionLoading.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusColor(module: Module): string {
|
||||
if (!module.installed) return 'grey'
|
||||
if (module.enabled) return 'success'
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
function getStatusText(module: Module): string {
|
||||
if (!module.installed) return 'Not Installed'
|
||||
if (module.enabled) return 'Enabled'
|
||||
return 'Disabled'
|
||||
}
|
||||
|
||||
function isActionLoading(module: Module, action: string): boolean {
|
||||
return actionLoading.value === `${module.handle}-${action}`
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadModules()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon :icon="mdiPackageVariant" class="mr-2"></v-icon>
|
||||
<span>Module Manager</span>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="text"
|
||||
:loading="loading"
|
||||
@click="loadModules"
|
||||
>
|
||||
Refresh
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-row class="mb-4">
|
||||
<v-col cols="12" md="8">
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
label="Search modules"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
clearable
|
||||
hide-details
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-select
|
||||
v-model="filterStatus"
|
||||
:items="[
|
||||
{ title: 'All Modules', value: 'all' },
|
||||
{ title: 'Installed', value: 'installed' },
|
||||
{ title: 'Not Installed', value: 'not-installed' },
|
||||
{ title: 'Enabled', value: 'enabled' },
|
||||
{ title: 'Disabled', value: 'disabled' },
|
||||
]"
|
||||
label="Filter by status"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-alert v-if="error" type="error" class="mb-4">
|
||||
{{ error }}
|
||||
</v-alert>
|
||||
|
||||
<v-progress-linear v-if="loading" indeterminate color="primary"></v-progress-linear>
|
||||
|
||||
<v-row v-else>
|
||||
<v-col
|
||||
v-for="module in filteredModules"
|
||||
:key="module.handle"
|
||||
cols="12"
|
||||
md="6"
|
||||
lg="4"
|
||||
>
|
||||
<v-card elevation="2" class="module-card">
|
||||
<v-card-title class="d-flex align-center">
|
||||
<v-icon :icon="mdiPackageVariant" class="mr-2"></v-icon>
|
||||
<span>{{ module.label }}</span>
|
||||
<v-spacer></v-spacer>
|
||||
<v-chip
|
||||
:color="getStatusColor(module)"
|
||||
size="small"
|
||||
variant="flat"
|
||||
>
|
||||
{{ getStatusText(module) }}
|
||||
</v-chip>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-subtitle>
|
||||
<div class="text-caption">{{ module.handle }}</div>
|
||||
<div class="text-caption text-grey">by {{ module.author }} • v{{ module.version }}</div>
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-card-text>
|
||||
<p class="module-description">{{ module.description }}</p>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-btn
|
||||
v-if="!module.installed"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
:loading="isActionLoading(module, 'install')"
|
||||
@click="handleAction(module, 'install')"
|
||||
>
|
||||
<v-icon :icon="mdiDownload" start></v-icon>
|
||||
Install
|
||||
</v-btn>
|
||||
|
||||
<template v-else>
|
||||
<v-btn
|
||||
v-if="module.enabled"
|
||||
color="warning"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
:loading="isActionLoading(module, 'disable')"
|
||||
@click="handleAction(module, 'disable')"
|
||||
>
|
||||
<v-icon :icon="mdiCloseCircle" start></v-icon>
|
||||
Disable
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
v-else
|
||||
color="success"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
:loading="isActionLoading(module, 'enable')"
|
||||
@click="handleAction(module, 'enable')"
|
||||
>
|
||||
<v-icon :icon="mdiCheckCircle" start></v-icon>
|
||||
Enable
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
color="info"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
:loading="isActionLoading(module, 'upgrade')"
|
||||
@click="handleAction(module, 'upgrade')"
|
||||
>
|
||||
<v-icon :icon="mdiUpdate" start></v-icon>
|
||||
Upgrade
|
||||
</v-btn>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-btn
|
||||
color="error"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
:loading="isActionLoading(module, 'uninstall')"
|
||||
@click="handleAction(module, 'uninstall')"
|
||||
>
|
||||
<v-icon :icon="mdiTrashCan" start></v-icon>
|
||||
Uninstall
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col v-if="filteredModules.length === 0" cols="12">
|
||||
<v-alert type="info">
|
||||
No modules found matching your criteria.
|
||||
</v-alert>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-snackbar
|
||||
v-model="snackbar"
|
||||
:color="snackbarColor"
|
||||
:timeout="3000"
|
||||
>
|
||||
{{ snackbarText }}
|
||||
</v-snackbar>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.module-card {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.v-card-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.module-description {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-height: 4.5em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user