516 lines
18 KiB
Vue

<script setup>
definePageMeta({
title: "Upload Plugin",
middleware: ["auth"],
requiresAuth: true,
});
const nuxtApp = useNuxtApp();
// Upload states
const isUploading = ref(false);
const uploadProgress = ref(0);
const uploadedFile = ref(null);
const installationLog = ref([]);
// Form data
const uploadForm = ref({
file: null,
agreeToTerms: false,
overwriteExisting: false
});
// Upload methods
const handleFileUpload = (files) => {
if (files && files.length > 0) {
uploadedFile.value = files[0];
}
};
const simulateUpload = async () => {
if (!uploadedFile.value || !uploadForm.value.agreeToTerms) {
nuxtApp.$swal.fire({
title: "Error",
text: "Please select a file and agree to terms",
icon: "error",
});
return;
}
isUploading.value = true;
uploadProgress.value = 0;
installationLog.value = [];
// Simulate upload progress
const steps = [
{ progress: 20, message: "Uploading plugin package..." },
{ progress: 40, message: "Validating plugin manifest..." },
{ progress: 60, message: "Checking dependencies..." },
{ progress: 80, message: "Installing plugin files..." },
{ progress: 100, message: "Plugin installed successfully!" }
];
for (const step of steps) {
await new Promise(resolve => setTimeout(resolve, 1000));
uploadProgress.value = step.progress;
installationLog.value.push({
message: step.message,
timestamp: new Date().toLocaleTimeString(),
type: step.progress === 100 ? 'success' : 'info'
});
}
isUploading.value = false;
nuxtApp.$swal.fire({
title: "Success",
text: "Plugin uploaded and installed successfully!",
icon: "success",
timer: 3000,
showConfirmButton: false,
}).then(() => {
navigateTo('/devtool/plugin-manager/installed');
});
};
const resetUpload = () => {
uploadedFile.value = null;
uploadProgress.value = 0;
isUploading.value = false;
installationLog.value = [];
uploadForm.value = {
file: null,
agreeToTerms: false,
overwriteExisting: false
};
};
const formatFileSize = (bytes) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
// app.config.js example code
const appConfigExample = ref(`export default {
// Application Identity
name: 'notification-management',
version: '1.2.0',
displayName: 'Notification Management System',
description: 'Complete notification system with templates, scheduling, and analytics',
// Authentik Integration
authentik: {
applicationSlug: 'notification-management',
requiredScopes: ['notifications:read', 'notifications:write', 'notifications:admin'],
groups: ['notification-users', 'notification-admins']
},
// CORRAD Menu Integration
menuStructure: {
title: 'Notifications',
icon: 'bell',
permission: 'notifications:read',
order: 100,
children: [
{
title: 'Dashboard',
route: '/notifications/dashboard',
icon: 'dashboard',
permission: 'notifications:read'
},
{
title: 'Send Notification',
route: '/notifications/send',
icon: 'send',
permission: 'notifications:write'
},
{
title: 'Templates',
route: '/notifications/templates',
icon: 'template',
permission: 'notifications:write'
},
{
title: 'History',
route: '/notifications/history',
icon: 'history',
permission: 'notifications:read'
},
{
title: 'Settings',
route: '/notifications/settings',
icon: 'settings',
permission: 'notifications:admin'
},
{
title: 'Reports',
route: '/notifications/reports',
icon: 'chart',
permission: 'notifications:admin'
}
]
},
// Database Schema
migrations: [
'migrations/001_create_notifications.sql',
'migrations/002_create_templates.sql',
'migrations/003_create_schedules.sql'
],
// API Endpoints
apiRoutes: [
{
path: '/api/notifications',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
permissions: {
'GET': 'notifications:read',
'POST': 'notifications:write',
'PUT': 'notifications:write',
'DELETE': 'notifications:admin'
}
},
{
path: '/api/notifications/templates',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
permissions: {
'GET': 'notifications:read',
'POST': 'notifications:write',
'PUT': 'notifications:write',
'DELETE': 'notifications:admin'
}
}
],
// Dependencies
dependencies: {
corrad: '^2.0.0',
authentik: '^2023.10.0'
},
// Installation hooks
hooks: {
preInstall: 'scripts/pre-install.js',
postInstall: 'scripts/post-install.js',
preUninstall: 'scripts/pre-uninstall.js'
}
}`);
</script>
<template>
<div>
<LayoutsBreadcrumb />
<!-- Header -->
<rs-card class="mb-6">
<template #body>
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900 mb-2">Upload Plugin</h1>
<p class="text-gray-600">
Install custom plugin packages to extend your application
</p>
</div>
<div class="hidden md:block">
<Icon name="mdi:cloud-upload" size="64" class="text-primary/20" />
</div>
</div>
</template>
</rs-card>
<!-- Security Warning -->
<rs-card class="mb-6">
<template #body>
<div class="flex items-start space-x-3">
<Icon name="mdi:security" class="text-orange-500 mt-1" size="20" />
<div>
<h3 class="font-medium text-orange-800 mb-2">Plugin Installation Guidelines</h3>
<p class="text-orange-700 text-sm mb-3">
Upload plugin packages for CORRAD+ applications. Each plugin should include app.config.js and proper folder structure.
Supported plugins: RBAC, Notification Management, Business Process Maker, Queue Management, Report Management, Audit Trail, and EDMS.
</p>
<div class="grid grid-cols-3 md:grid-cols-7 gap-2">
<div class="bg-green-100 rounded px-2 py-1 text-xs text-center">
<Icon name="mdi:shield-account" class="text-green-600 mb-1" size="12" />
<div class="text-green-800">RBAC</div>
</div>
<div class="bg-orange-100 rounded px-2 py-1 text-xs text-center">
<Icon name="mdi:bell-ring" class="text-orange-600 mb-1" size="12" />
<div class="text-orange-800">Notify</div>
</div>
<div class="bg-blue-100 rounded px-2 py-1 text-xs text-center">
<Icon name="mdi:flowchart" class="text-blue-600 mb-1" size="12" />
<div class="text-blue-800">BPM</div>
</div>
<div class="bg-indigo-100 rounded px-2 py-1 text-xs text-center">
<Icon name="mdi:format-list-numbered" class="text-indigo-600 mb-1" size="12" />
<div class="text-indigo-800">Queue</div>
</div>
<div class="bg-blue-100 rounded px-2 py-1 text-xs text-center">
<Icon name="mdi:chart-line" class="text-blue-600 mb-1" size="12" />
<div class="text-blue-800">Reports</div>
</div>
<div class="bg-purple-100 rounded px-2 py-1 text-xs text-center">
<Icon name="mdi:file-document-alert" class="text-purple-600 mb-1" size="12" />
<div class="text-purple-800">Audit</div>
</div>
<div class="bg-red-100 rounded px-2 py-1 text-xs text-center">
<Icon name="mdi:file-document-multiple" class="text-red-600 mb-1" size="12" />
<div class="text-red-800">EDMS</div>
</div>
</div>
</div>
</div>
</template>
</rs-card>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Upload Form -->
<rs-card>
<template #header>
<div class="flex items-center">
<Icon name="mdi:upload" class="mr-2" />
Upload Plugin Package
</div>
</template>
<template #body>
<FormKit type="form" :actions="false" @submit="simulateUpload">
<!-- File Upload -->
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">
Plugin Package (.zip)
</label>
<div
class="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center hover:border-primary transition-colors"
:class="{ 'border-primary bg-primary/5': uploadedFile }"
>
<div v-if="!uploadedFile">
<Icon name="mdi:cloud-upload" class="mx-auto text-gray-400 mb-4" size="48" />
<p class="text-gray-600 mb-2">Drag and drop your plugin file here, or</p>
<FormKit
type="file"
accept=".zip"
@change="handleFileUpload"
wrapper-class="inline-block"
outer-class="mb-0"
input-class="hidden"
>
<template #label>
<rs-button variant="outline" size="sm">
Browse Files
</rs-button>
</template>
</FormKit>
<p class="text-xs text-gray-500 mt-2">Maximum file size: 50MB</p>
</div>
<div v-else class="space-y-2">
<Icon name="mdi:file-check" class="mx-auto text-green-500 mb-2" size="32" />
<p class="font-medium text-gray-900">{{ uploadedFile.name }}</p>
<p class="text-sm text-gray-500">{{ formatFileSize(uploadedFile.size) }}</p>
<rs-button variant="outline" size="sm" @click="resetUpload">
Choose Different File
</rs-button>
</div>
</div>
</div>
<!-- Options -->
<div class="space-y-4 mb-6">
<FormKit
type="checkbox"
v-model="uploadForm.overwriteExisting"
label="Overwrite existing plugin if it exists"
help="This will replace any existing plugin with the same name"
/>
<FormKit
type="checkbox"
v-model="uploadForm.agreeToTerms"
label="I understand that this plugin will be installed as a complete CORRAD+ application"
validation="required"
:validation-messages="{ required: 'You must confirm to continue' }"
/>
</div>
<!-- Upload Button -->
<div class="flex gap-3">
<rs-button
btnType="submit"
class="flex-1"
:disabled="!uploadedFile || isUploading"
>
<Icon v-if="!isUploading" name="mdi:upload" class="mr-2" size="16" />
<Icon v-else name="mdi:loading" class="mr-2 animate-spin" size="16" />
{{ isUploading ? 'Installing...' : 'Upload & Install' }}
</rs-button>
<rs-button variant="outline" @click="resetUpload" :disabled="isUploading">
Reset
</rs-button>
</div>
</FormKit>
</template>
</rs-card>
<!-- Upload Progress & Instructions -->
<div class="space-y-6">
<!-- Progress -->
<rs-card v-if="isUploading || uploadProgress > 0">
<template #header>
<div class="flex items-center">
<Icon name="mdi:progress-upload" class="mr-2" />
Installation Progress
</div>
</template>
<template #body>
<div class="space-y-4">
<!-- Progress Bar -->
<div>
<div class="flex justify-between text-sm mb-1">
<span>Installing plugin...</span>
<span>{{ uploadProgress }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div
class="bg-primary h-2 rounded-full transition-all duration-300"
:style="{ width: uploadProgress + '%' }"
></div>
</div>
</div>
<!-- Installation Log -->
<div class="space-y-2 max-h-40 overflow-y-auto">
<div
v-for="(log, index) in installationLog"
:key="index"
class="flex items-start space-x-2 text-sm"
>
<Icon
:name="log.type === 'success' ? 'mdi:check-circle' : 'mdi:information'"
:class="log.type === 'success' ? 'text-green-500' : 'text-blue-500'"
size="16"
class="mt-0.5"
/>
<div class="flex-1">
<span class="text-gray-900">{{ log.message }}</span>
<span class="text-gray-500 ml-2">{{ log.timestamp }}</span>
</div>
</div>
</div>
</div>
</template>
</rs-card>
<!-- Instructions -->
<rs-card>
<template #header>
<div class="flex items-center">
<Icon name="mdi:information-outline" class="mr-2" />
Plugin Requirements
</div>
</template>
<template #body>
<div class="space-y-4 text-sm">
<div>
<h4 class="font-medium text-gray-900 mb-2">Plugin Structure Requirements</h4>
<ul class="list-disc list-inside text-gray-600 space-y-1">
<li>Must be a valid .zip file</li>
<li>Include app.config.js with complete application metadata</li>
<li>Authentik integration configuration (scopes, groups, permissions)</li>
<li>CORRAD menu structure with proper routing and permissions</li>
<li>Database migrations with SQL files</li>
<li>API routes with method-specific permissions</li>
<li>Installation hooks for pre/post install actions</li>
</ul>
</div>
<div>
<h4 class="font-medium text-gray-900 mb-2">app.config.js Example</h4>
<RsCodeMirror
:model-value="appConfigExample"
mode="javascript"
height="300px"
:disabled="true"
/>
</div>
<div>
<h4 class="font-medium text-gray-900 mb-2">Complete Folder Structure</h4>
<div class="bg-gray-100 p-3 rounded text-xs font-mono">
<pre>notification-management/
app.config.js # Complete application config
pages/
dashboard/
send/
templates/
history/
settings/
reports/
components/
NotificationSender.vue
NotificationList.vue
NotificationStats.vue
server/
api/
notifications/
templates/
composables/
useNotifications.js
migrations/
001_create_notifications.sql
002_create_templates.sql
003_create_schedules.sql
scripts/
pre-install.js
post-install.js
pre-uninstall.js</pre>
</div>
</div>
</div>
</template>
</rs-card>
<!-- Recent Uploads -->
<rs-card>
<template #header>
<div class="flex items-center">
<Icon name="mdi:history" class="mr-2" />
Recent Uploads
</div>
</template>
<template #body>
<div class="space-y-3">
<div class="flex items-center justify-between py-2 border-b">
<div class="flex items-center space-x-3">
<Icon name="mdi:check-circle" class="text-green-500" size="16" />
<div>
<p class="text-sm font-medium">Report Management System</p>
<p class="text-xs text-gray-500">Uploaded 3 days ago</p>
</div>
</div>
<rs-badge variant="success" size="sm">Success</rs-badge>
</div>
<div class="flex items-center justify-between py-2 border-b">
<div class="flex items-center space-x-3">
<Icon name="mdi:check-circle" class="text-green-500" size="16" />
<div>
<p class="text-sm font-medium">Notification Management System</p>
<p class="text-xs text-gray-500">Uploaded 1 week ago</p>
</div>
</div>
<rs-badge variant="success" size="sm">Success</rs-badge>
</div>
<div class="text-center py-4 text-gray-500 text-sm">
Ready to upload your next CORRAD+ application
</div>
</div>
</template>
</rs-card>
</div>
</div>
</div>
</template>