Compare commits

..

1 Commits

8 changed files with 2894 additions and 14 deletions

View File

@ -25,11 +25,11 @@ const props = defineProps({
// Slots // Slots
const slots = useSlots(); const slots = useSlots();
const tabs = ref(slots.default().map((tab) => tab.props)); const tabs = ref(slots.default().map((tab) => tab.props).filter(Boolean));
const selectedTitle = ref(tabs.value[0]["title"]); const selectedTitle = ref(tabs.value.length > 0 && tabs.value[0] ? tabs.value[0]["title"] : "");
tabs.value.forEach((tab) => { tabs.value.forEach((tab) => {
if (typeof tab.active !== "undefined") { if (tab && typeof tab.active !== "undefined") {
selectedTitle.value = tab.title; selectedTitle.value = tab.title;
} }
}); });

View File

@ -75,6 +75,28 @@ export default [
"path": "/devtool/api-editor", "path": "/devtool/api-editor",
"icon": "material-symbols:api-rounded", "icon": "material-symbols:api-rounded",
"child": [] "child": []
},
{
"title": "Plugin Manager",
"icon": "mdi:puzzle",
"child": [
{
"title": "Dashboard",
"path": "/devtool/plugin-manager"
},
{
"title": "Plugin Store",
"path": "/devtool/plugin-manager/store"
},
{
"title": "My Plugins",
"path": "/devtool/plugin-manager/installed"
},
{
"title": "Upload Plugin",
"path": "/devtool/plugin-manager/upload"
}
]
} }
], ],
"meta": { "meta": {

View File

@ -0,0 +1,396 @@
<script setup>
definePageMeta({
title: "Plugin Manager",
middleware: ["auth"],
requiresAuth: true,
});
// Quick stats for dashboard
const pluginStats = ref({
installed: 3,
active: 3,
available: 4,
updates: 2
});
// Quick actions
const quickActions = ref([
{
title: "Browse Store",
description: "Discover new plugins and applications",
icon: "mdi:store",
color: "blue",
path: "/devtool/plugin-manager/store"
},
{
title: "My Plugins",
description: "Manage installed plugins",
icon: "mdi:puzzle",
color: "green",
path: "/devtool/plugin-manager/installed"
},
{
title: "Upload Plugin",
description: "Install custom plugin packages",
icon: "mdi:cloud-upload",
color: "purple",
path: "/devtool/plugin-manager/upload"
},
{
title: "Plugin Settings",
description: "Configure plugin system settings",
icon: "mdi:cog",
color: "gray",
path: "/devtool/plugin-manager/settings"
}
]);
// Recently installed plugins
const recentPlugins = ref([
{
id: 1,
name: "notification-management",
displayName: "Notification Management System",
version: "1.2.0",
description: "Complete notification system with templates, scheduling, and analytics",
status: "active",
author: "CORRAD Team",
installDate: "2024-01-15",
icon: "mdi:bell-ring",
color: "orange",
authentik: {
applicationSlug: "notification-management",
requiredScopes: ["notifications:read", "notifications:write", "notifications:admin"],
groups: ["notification-users", "notification-admins"]
},
menuItems: 6,
apiEndpoints: 2,
migrations: 3
},
{
id: 2,
name: "report-management",
displayName: "Report Management System",
version: "1.0.0",
description: "Advanced reporting system integrated with Metabase for analytics and dashboards",
status: "active",
author: "CORRAD Team",
installDate: "2024-01-12",
icon: "mdi:chart-line",
color: "blue",
authentik: {
applicationSlug: "report-management",
requiredScopes: ["reports:read", "reports:write", "reports:admin"],
groups: ["report-users", "report-admins"]
},
menuItems: 5,
apiEndpoints: 3,
migrations: 2
},
{
id: 3,
name: "rbac",
displayName: "RBAC System",
version: "1.0.0",
description: "Role-Based Access Control system integrated with Authentik",
status: "active",
author: "CORRAD Team",
installDate: "2024-01-10",
icon: "mdi:shield-account",
color: "green",
authentik: {
applicationSlug: "rbac-system",
requiredScopes: ["rbac:read", "rbac:write", "rbac:admin"],
groups: ["rbac-users", "rbac-admins"]
},
menuItems: 4,
apiEndpoints: 4,
migrations: 1
}
]);
// Plugin updates available
const availableUpdates = ref([
{
id: 4,
name: "audit-trail",
displayName: "Audit Trail System",
currentVersion: "1.0.0",
newVersion: "1.1.0",
updateSize: "2.1 MB",
icon: "mdi:file-document-alert",
color: "purple"
},
{
id: 5,
name: "queue-management",
displayName: "Queue Management System",
currentVersion: "1.0.0",
newVersion: "1.0.1",
updateSize: "800 KB",
icon: "mdi:format-list-numbered",
color: "indigo"
}
]);
const getColorClasses = (color, type = 'bg') => {
const colorMap = {
bg: {
blue: 'bg-blue-100 text-blue-600',
green: 'bg-green-100 text-green-600',
purple: 'bg-purple-100 text-purple-600',
orange: 'bg-orange-100 text-orange-600',
indigo: 'bg-indigo-100 text-indigo-600',
red: 'bg-red-100 text-red-600',
gray: 'bg-gray-100 text-gray-600'
},
hover: {
blue: 'hover:bg-blue-200',
green: 'hover:bg-green-200',
purple: 'hover:bg-purple-200',
orange: 'hover:bg-orange-200',
indigo: 'hover:bg-indigo-200',
red: 'hover:bg-red-200',
gray: 'hover:bg-gray-200'
}
};
return colorMap[type][color] || colorMap[type]['gray'];
};
const navigateToAction = (path) => {
navigateTo(path);
};
const getStatusBadgeVariant = (status) => {
return status === 'active' ? 'success' : 'secondary';
};
</script>
<template>
<div>
<LayoutsBreadcrumb />
<!-- Welcome 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">Plugin Manager</h1>
<p class="text-gray-600">
Manage your application ecosystem. Install, configure, and organize plugins to extend functionality.
</p>
</div>
<div class="hidden md:block">
<Icon name="mdi:puzzle" size="64" class="text-primary/20" />
</div>
</div>
</template>
</rs-card>
<!-- Stats Overview -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<rs-card class="text-center">
<template #body>
<div class="p-4">
<div class="flex items-center justify-center mb-2">
<div class="bg-blue-100 text-blue-600 p-3 rounded-full">
<Icon name="mdi:puzzle" size="24" />
</div>
</div>
<h3 class="text-2xl font-bold text-gray-900">{{ pluginStats.installed }}</h3>
<p class="text-sm text-gray-600">Installed</p>
</div>
</template>
</rs-card>
<rs-card class="text-center">
<template #body>
<div class="p-4">
<div class="flex items-center justify-center mb-2">
<div class="bg-green-100 text-green-600 p-3 rounded-full">
<Icon name="mdi:check-circle" size="24" />
</div>
</div>
<h3 class="text-2xl font-bold text-gray-900">{{ pluginStats.active }}</h3>
<p class="text-sm text-gray-600">Active</p>
</div>
</template>
</rs-card>
<rs-card class="text-center">
<template #body>
<div class="p-4">
<div class="flex items-center justify-center mb-2">
<div class="bg-purple-100 text-purple-600 p-3 rounded-full">
<Icon name="mdi:store" size="24" />
</div>
</div>
<h3 class="text-2xl font-bold text-gray-900">{{ pluginStats.available }}</h3>
<p class="text-sm text-gray-600">Available</p>
</div>
</template>
</rs-card>
<rs-card class="text-center">
<template #body>
<div class="p-4">
<div class="flex items-center justify-center mb-2">
<div class="bg-orange-100 text-orange-600 p-3 rounded-full">
<Icon name="mdi:update" size="24" />
</div>
</div>
<h3 class="text-2xl font-bold text-gray-900">{{ pluginStats.updates }}</h3>
<p class="text-sm text-gray-600">Updates</p>
</div>
</template>
</rs-card>
</div>
<!-- Quick Actions -->
<rs-card class="mb-6">
<template #header>
<div class="flex items-center">
<Icon name="mdi:lightning-bolt" class="mr-2" />
Quick Actions
</div>
</template>
<template #body>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div
v-for="action in quickActions"
:key="action.title"
class="group p-6 rounded-lg border-2 border-gray-200 hover:border-primary cursor-pointer transition-all duration-200 hover:shadow-md"
@click="navigateToAction(action.path)"
>
<div class="text-center">
<div
:class="[getColorClasses(action.color), getColorClasses(action.color, 'hover')]"
class="w-16 h-16 mx-auto rounded-full flex items-center justify-center mb-4 transition-colors"
>
<Icon :name="action.icon" size="32" />
</div>
<h3 class="font-semibold text-lg text-gray-900 mb-2 group-hover:text-primary transition-colors">
{{ action.title }}
</h3>
<p class="text-gray-600 text-sm">{{ action.description }}</p>
</div>
</div>
</div>
</template>
</rs-card>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Recently Installed -->
<rs-card>
<template #header>
<div class="flex items-center justify-between w-full">
<div class="flex items-center">
<Icon name="mdi:history" class="mr-2" />
Recently Installed
</div>
<NuxtLink
to="/devtool/plugin-manager/installed"
class="text-primary hover:text-primary/80 text-sm font-medium"
>
View All
</NuxtLink>
</div>
</template>
<template #body>
<div class="space-y-4">
<div
v-for="plugin in recentPlugins"
:key="plugin.id"
class="flex items-center p-4 rounded-lg border border-gray-200 hover:shadow-sm transition-shadow"
>
<div
:class="getColorClasses(plugin.color)"
class="w-12 h-12 rounded-lg flex items-center justify-center mr-4"
>
<Icon :name="plugin.icon" size="24" />
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between">
<h4 class="font-medium text-gray-900 truncate">{{ plugin.displayName }}</h4>
<rs-badge :variant="getStatusBadgeVariant(plugin.status)" size="sm">
{{ plugin.status }}
</rs-badge>
</div>
<p class="text-sm text-gray-600 mt-1">v{{ plugin.version }} {{ plugin.author }}</p>
<div class="flex items-center space-x-3 mt-2 text-xs text-gray-500">
<span class="flex items-center">
<Icon name="mdi:menu" size="12" class="mr-1" />
{{ plugin.menuItems }} menus
</span>
<span class="flex items-center">
<Icon name="mdi:api" size="12" class="mr-1" />
{{ plugin.apiEndpoints }} APIs
</span>
<span class="flex items-center">
<Icon name="mdi:database" size="12" class="mr-1" />
{{ plugin.migrations }} migrations
</span>
</div>
<p class="text-xs text-gray-500 mt-1">Installed {{ plugin.installDate }}</p>
</div>
</div>
<div v-if="recentPlugins.length === 0" class="text-center py-8">
<Icon name="mdi:puzzle-outline" size="48" class="mx-auto text-gray-400 mb-2" />
<p class="text-gray-500">No plugins installed yet</p>
</div>
</div>
</template>
</rs-card>
<!-- Available Updates -->
<rs-card>
<template #header>
<div class="flex items-center justify-between w-full">
<div class="flex items-center">
<Icon name="mdi:update" class="mr-2" />
Available Updates
<rs-badge v-if="availableUpdates.length > 0" variant="warning" size="sm" class="ml-2">
{{ availableUpdates.length }}
</rs-badge>
</div>
<rs-button size="sm" variant="outline" v-if="availableUpdates.length > 0">
Update All
</rs-button>
</div>
</template>
<template #body>
<div class="space-y-4">
<div
v-for="update in availableUpdates"
:key="update.id"
class="flex items-center p-4 rounded-lg border border-gray-200 hover:shadow-sm transition-shadow"
>
<div
:class="getColorClasses(update.color)"
class="w-12 h-12 rounded-lg flex items-center justify-center mr-4"
>
<Icon :name="update.icon" size="24" />
</div>
<div class="flex-1 min-w-0">
<h4 class="font-medium text-gray-900">{{ update.displayName }}</h4>
<p class="text-sm text-gray-600 mt-1">
{{ update.currentVersion }} {{ update.newVersion }}
</p>
<p class="text-xs text-gray-500 mt-1">{{ update.updateSize }}</p>
</div>
<rs-button size="sm" variant="primary">
Update
</rs-button>
</div>
<div v-if="availableUpdates.length === 0" class="text-center py-8">
<Icon name="mdi:check-circle" size="48" class="mx-auto text-green-400 mb-2" />
<p class="text-gray-500">All plugins are up to date</p>
</div>
</div>
</template>
</rs-card>
</div>
</div>
</template>

View File

@ -0,0 +1,593 @@
<script setup>
definePageMeta({
title: "My Plugins",
middleware: ["auth"],
requiresAuth: true,
});
const nuxtApp = useNuxtApp();
// Filters and search
const searchQuery = ref('');
const statusFilter = ref('all');
const sortBy = ref('recent');
// Modal states
const showUninstallModal = ref(false);
const showConfigModal = ref(false);
const selectedPlugin = ref(null);
// Installed plugins
const installedPlugins = ref([
{
id: 1,
name: "rbac",
displayName: "RBAC System",
version: "1.0.0",
description: "Role-Based Access Control system integrated with Authentik for user management",
status: "active",
author: "CORRAD Team",
installDate: "2024-01-10",
lastUsed: "2024-01-23",
icon: "mdi:shield-account",
color: "green",
size: "3.2 MB",
menuItems: [
{ title: "User Management", path: "/rbac/users" },
{ title: "Role Management", path: "/rbac/roles" },
{ title: "Permissions", path: "/rbac/permissions" }
],
permissions: ["rbac.manage", "user.assign", "role.create"],
settings: {
authentikEnabled: true,
ssoEnabled: true,
autoSync: true
}
},
{
id: 2,
name: "notification-management",
displayName: "Notification Management System",
version: "1.0.0",
description: "Complete notification system with email, SMS, push notifications and templates",
status: "active",
author: "CORRAD Team",
installDate: "2024-01-15",
lastUsed: "2024-01-23",
icon: "mdi:bell-ring",
color: "orange",
size: "2.8 MB",
menuItems: [
{ title: "Dashboard", path: "/notifications/dashboard" },
{ title: "Templates", path: "/notifications/templates" },
{ title: "Settings", path: "/notifications/settings" },
{ title: "History", path: "/notifications/history" }
],
permissions: ["notification.send", "template.manage", "delivery.track"],
settings: {
emailEnabled: true,
smsEnabled: true,
pushEnabled: true
}
},
{
id: 3,
name: "report-management",
displayName: "Report Management System",
version: "1.0.0",
description: "Advanced reporting system integrated with Metabase for analytics and dashboards",
status: "active",
author: "CORRAD Team",
installDate: "2024-01-12",
lastUsed: "2024-01-22",
icon: "mdi:chart-line",
color: "blue",
size: "3.8 MB",
menuItems: [
{ title: "Dashboard", path: "/reports/dashboard" },
{ title: "Report Builder", path: "/reports/builder" },
{ title: "Analytics", path: "/reports/analytics" },
{ title: "Scheduled Reports", path: "/reports/schedule" }
],
permissions: ["report.create", "report.view", "dashboard.manage"],
settings: {
metabaseEnabled: true,
autoGenerate: false,
exportFormats: ["PDF", "Excel", "CSV"]
}
}
]);
// Computed properties
const filteredPlugins = computed(() => {
let plugins = installedPlugins.value;
// Filter by status
if (statusFilter.value !== 'all') {
plugins = plugins.filter(plugin => plugin.status === statusFilter.value);
}
// Filter by search
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase();
plugins = plugins.filter(plugin =>
plugin.displayName.toLowerCase().includes(query) ||
plugin.description.toLowerCase().includes(query) ||
plugin.author.toLowerCase().includes(query)
);
}
// Sort plugins
switch (sortBy.value) {
case 'recent':
plugins.sort((a, b) => new Date(b.installDate) - new Date(a.installDate));
break;
case 'name':
plugins.sort((a, b) => a.displayName.localeCompare(b.displayName));
break;
case 'status':
plugins.sort((a, b) => b.status.localeCompare(a.status));
break;
case 'lastUsed':
plugins.sort((a, b) => new Date(b.lastUsed) - new Date(a.lastUsed));
break;
}
return plugins;
});
const pluginStats = computed(() => {
const total = installedPlugins.value.length;
const active = installedPlugins.value.filter(p => p.status === 'active').length;
const inactive = total - active;
return { total, active, inactive };
});
const getColorClasses = (color) => {
const colorMap = {
blue: 'bg-blue-100 text-blue-600',
green: 'bg-green-100 text-green-600',
purple: 'bg-purple-100 text-purple-600',
orange: 'bg-orange-100 text-orange-600',
indigo: 'bg-indigo-100 text-indigo-600',
red: 'bg-red-100 text-red-600'
};
return colorMap[color] || 'bg-gray-100 text-gray-600';
};
const getStatusBadgeVariant = (status) => {
return status === 'active' ? 'success' : 'secondary';
};
const togglePluginStatus = async (plugin) => {
try {
plugin.status = plugin.status === 'active' ? 'inactive' : 'active';
nuxtApp.$swal.fire({
title: "Success",
text: `Plugin ${plugin.status === 'active' ? 'activated' : 'deactivated'} successfully`,
icon: "success",
timer: 2000,
showConfirmButton: false,
});
} catch (error) {
nuxtApp.$swal.fire({
title: "Error",
text: "Failed to toggle plugin status",
icon: "error",
});
}
};
const openConfigModal = (plugin) => {
selectedPlugin.value = plugin;
showConfigModal.value = true;
};
const openUninstallModal = (plugin) => {
selectedPlugin.value = plugin;
showUninstallModal.value = true;
};
const confirmUninstall = async () => {
try {
const plugin = selectedPlugin.value;
installedPlugins.value = installedPlugins.value.filter(p => p.id !== plugin.id);
showUninstallModal.value = false;
selectedPlugin.value = null;
nuxtApp.$swal.fire({
title: "Success",
text: "Plugin uninstalled successfully",
icon: "success",
timer: 2000,
showConfirmButton: false,
});
} catch (error) {
nuxtApp.$swal.fire({
title: "Error",
text: "Failed to uninstall plugin",
icon: "error",
});
}
};
const updatePluginSettings = async () => {
try {
showConfigModal.value = false;
nuxtApp.$swal.fire({
title: "Success",
text: "Plugin settings updated successfully",
icon: "success",
timer: 2000,
showConfirmButton: false,
});
} catch (error) {
nuxtApp.$swal.fire({
title: "Error",
text: "Failed to update plugin settings",
icon: "error",
});
}
};
const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString();
};
const viewPluginDetails = (plugin) => {
// Navigate to plugin details or open a detailed modal
navigateTo(`/devtool/plugin-manager/installed/${plugin.name}`);
};
</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">My Plugins</h1>
<p class="text-gray-600">
Manage your installed plugins, configure settings, and control access
</p>
</div>
<div class="hidden md:block">
<Icon name="mdi:puzzle" size="64" class="text-primary/20" />
</div>
</div>
</template>
</rs-card>
<!-- Stats Overview -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<rs-card class="text-center">
<template #body>
<div class="p-4">
<div class="flex items-center justify-center mb-2">
<div class="bg-blue-100 text-blue-600 p-3 rounded-full">
<Icon name="mdi:puzzle" size="24" />
</div>
</div>
<h3 class="text-2xl font-bold text-gray-900">{{ pluginStats.total }}</h3>
<p class="text-sm text-gray-600">Total Installed</p>
</div>
</template>
</rs-card>
<rs-card class="text-center">
<template #body>
<div class="p-4">
<div class="flex items-center justify-center mb-2">
<div class="bg-green-100 text-green-600 p-3 rounded-full">
<Icon name="mdi:check-circle" size="24" />
</div>
</div>
<h3 class="text-2xl font-bold text-gray-900">{{ pluginStats.active }}</h3>
<p class="text-sm text-gray-600">Active</p>
</div>
</template>
</rs-card>
<rs-card class="text-center">
<template #body>
<div class="p-4">
<div class="flex items-center justify-center mb-2">
<div class="bg-gray-100 text-gray-600 p-3 rounded-full">
<Icon name="mdi:pause-circle" size="24" />
</div>
</div>
<h3 class="text-2xl font-bold text-gray-900">{{ pluginStats.inactive }}</h3>
<p class="text-sm text-gray-600">Inactive</p>
</div>
</template>
</rs-card>
</div>
<!-- Filters and Search -->
<rs-card class="mb-6">
<template #body>
<div class="flex flex-col md:flex-row gap-4">
<div class="flex-1">
<FormKit
type="search"
placeholder="Search plugins..."
v-model="searchQuery"
outer-class="mb-0"
>
<template #prefix>
<Icon name="mdi:magnify" class="text-gray-400" />
</template>
</FormKit>
</div>
<div class="w-full md:w-40">
<FormKit
type="select"
v-model="statusFilter"
:options="[
{ label: 'All Status', value: 'all' },
{ label: 'Active', value: 'active' },
{ label: 'Inactive', value: 'inactive' }
]"
outer-class="mb-0"
/>
</div>
<div class="w-full md:w-48">
<FormKit
type="select"
v-model="sortBy"
:options="[
{ label: 'Recently Installed', value: 'recent' },
{ label: 'Name A-Z', value: 'name' },
{ label: 'Status', value: 'status' },
{ label: 'Last Used', value: 'lastUsed' }
]"
outer-class="mb-0"
/>
</div>
</div>
</template>
</rs-card>
<!-- Results Info -->
<div class="flex items-center justify-between mb-6">
<p class="text-gray-600">
{{ filteredPlugins.length }} plugin(s) found
</p>
<NuxtLink
to="/devtool/plugin-manager/store"
class="text-primary hover:text-primary/80 text-sm font-medium"
>
Browse Store
</NuxtLink>
</div>
<!-- Plugin Grid -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<rs-card
v-for="plugin in filteredPlugins"
:key="plugin.id"
class="hover:shadow-lg transition-shadow"
>
<template #body>
<div class="space-y-4">
<!-- Plugin Header -->
<div class="flex items-start justify-between">
<div class="flex items-center space-x-4">
<div
:class="getColorClasses(plugin.color)"
class="w-14 h-14 rounded-xl flex items-center justify-center"
>
<Icon :name="plugin.icon" size="28" />
</div>
<div>
<h3 class="font-semibold text-lg text-gray-900">
{{ plugin.displayName }}
</h3>
<p class="text-sm text-gray-500">v{{ plugin.version }} {{ plugin.author }}</p>
<p class="text-xs text-gray-400 mt-1">{{ plugin.size }}</p>
</div>
</div>
<rs-badge :variant="getStatusBadgeVariant(plugin.status)" size="sm">
{{ plugin.status }}
</rs-badge>
</div>
<!-- Plugin Description -->
<p class="text-gray-600 text-sm">
{{ plugin.description }}
</p>
<!-- Plugin Info -->
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<span class="text-gray-500">Installed:</span>
<div class="font-medium">{{ formatDate(plugin.installDate) }}</div>
</div>
<div>
<span class="text-gray-500">Last Used:</span>
<div class="font-medium">{{ formatDate(plugin.lastUsed) }}</div>
</div>
</div>
<!-- Menu Items -->
<div v-if="plugin.menuItems.length > 0">
<span class="text-sm text-gray-500">Menu Items:</span>
<div class="flex flex-wrap gap-2 mt-1">
<NuxtLink
v-for="item in plugin.menuItems"
:key="item.path"
:to="item.path"
class="px-2 py-1 bg-gray-100 text-gray-700 text-xs rounded-full hover:bg-gray-200 transition-colors"
>
{{ item.title }}
</NuxtLink>
</div>
</div>
<!-- Actions -->
<div class="flex gap-2 pt-2 border-t">
<rs-button
size="sm"
:variant="plugin.status === 'active' ? 'outline' : 'primary'"
@click="togglePluginStatus(plugin)"
>
{{ plugin.status === 'active' ? 'Deactivate' : 'Activate' }}
</rs-button>
<rs-button
size="sm"
variant="outline"
@click="openConfigModal(plugin)"
>
<Icon name="mdi:cog" class="mr-1" size="14" />
Settings
</rs-button>
<rs-button
size="sm"
variant="outline"
@click="viewPluginDetails(plugin)"
>
<Icon name="mdi:information-outline" class="mr-1" size="14" />
Details
</rs-button>
<rs-button
size="sm"
variant="danger"
@click="openUninstallModal(plugin)"
>
<Icon name="mdi:delete-outline" size="14" />
</rs-button>
</div>
</div>
</template>
</rs-card>
</div>
<!-- Empty State -->
<div v-if="filteredPlugins.length === 0" class="text-center py-12">
<rs-card>
<template #body>
<div class="py-8">
<Icon name="mdi:puzzle-outline" size="64" class="mx-auto text-gray-400 mb-4" />
<h3 class="text-lg font-medium text-gray-900 mb-2">No plugins found</h3>
<p class="text-gray-500 mb-4">
Try adjusting your search terms or install plugins from the store
</p>
<rs-button variant="outline" @click="searchQuery = ''; statusFilter = 'all'">
Clear Filters
</rs-button>
</div>
</template>
</rs-card>
</div>
<!-- Uninstall Confirmation Modal -->
<rs-modal
title="Uninstall Plugin"
v-model="showUninstallModal"
ok-title="Uninstall"
ok-variant="danger"
:ok-callback="confirmUninstall"
>
<div v-if="selectedPlugin">
<p class="mb-4">
Are you sure you want to uninstall <strong>{{ selectedPlugin.displayName }}</strong>?
</p>
<div class="bg-red-50 border border-red-200 p-4 rounded-lg">
<p class="text-red-800 text-sm">
<Icon name="mdi:alert" class="mr-2" size="16" />
This action cannot be undone. All plugin data and configurations will be permanently removed.
</p>
</div>
</div>
</rs-modal>
<!-- Plugin Configuration Modal -->
<rs-modal
title="Plugin Settings"
v-model="showConfigModal"
ok-title="Save Settings"
:ok-callback="updatePluginSettings"
size="lg"
>
<div v-if="selectedPlugin">
<div class="space-y-6">
<!-- Plugin Info -->
<div class="flex items-center space-x-4 pb-4 border-b">
<div
:class="getColorClasses(selectedPlugin.color)"
class="w-12 h-12 rounded-lg flex items-center justify-center"
>
<Icon :name="selectedPlugin.icon" size="24" />
</div>
<div>
<h3 class="font-semibold text-lg">{{ selectedPlugin.displayName }}</h3>
<p class="text-sm text-gray-500">v{{ selectedPlugin.version }}</p>
</div>
</div>
<!-- Settings Form -->
<FormKit type="form" :actions="false">
<div v-if="selectedPlugin.name === 'notification-system'">
<FormKit
type="checkbox"
label="Enable Email Notifications"
v-model="selectedPlugin.settings.emailEnabled"
/>
<FormKit
type="checkbox"
label="Enable SMS Notifications"
v-model="selectedPlugin.settings.smsEnabled"
/>
<FormKit
type="checkbox"
label="Enable Push Notifications"
v-model="selectedPlugin.settings.pushEnabled"
/>
</div>
<div v-else-if="selectedPlugin.name === 'backup-system'">
<FormKit
type="checkbox"
label="Enable Automatic Backup"
v-model="selectedPlugin.settings.autoBackup"
/>
<FormKit
type="select"
label="Backup Frequency"
v-model="selectedPlugin.settings.frequency"
:options="[
{ label: 'Daily', value: 'daily' },
{ label: 'Weekly', value: 'weekly' },
{ label: 'Monthly', value: 'monthly' }
]"
/>
<FormKit
type="select"
label="Cloud Storage"
v-model="selectedPlugin.settings.cloudStorage"
:options="[
{ label: 'Enabled', value: 'enabled' },
{ label: 'Disabled', value: 'disabled' }
]"
/>
</div>
<div v-else>
<p class="text-gray-500 text-center py-4">
No configurable settings available for this plugin.
</p>
</div>
</FormKit>
</div>
</div>
</rs-modal>
</div>
</template>

View File

@ -0,0 +1,879 @@
<script setup>
definePageMeta({
title: "Plugin Details",
middleware: ["auth"],
requiresAuth: true,
});
const route = useRoute();
const nuxtApp = useNuxtApp();
const pluginSlug = route.params.slug;
// Plugin installation state
const isInstalling = ref(false);
const installProgress = ref(0);
const installationStep = ref('');
const showInstallModal = ref(false);
const installationLog = ref([]);
// Mock plugin data - in real app, this would be fetched based on slug
const pluginData = ref({
id: 1,
name: "notification-management",
displayName: "Notification Management System",
version: "1.2.0",
description: "Complete notification system with templates, scheduling, and analytics for enterprise-grade communication management.",
author: "CORRAD Team",
authorAvatar: "/img/avatar/1.svg",
category: "communication",
size: "2.8 MB",
rating: 4.8,
downloads: 1890,
price: "Free",
icon: "mdi:bell-ring",
color: "orange",
lastUpdated: "2024-01-18",
license: "MIT",
homepage: "https://corrad.dev/plugins/notification-management",
repository: "https://github.com/corrad/notification-management",
tags: ["Notifications", "Email", "SMS", "Templates", "Scheduling"],
isInstalled: false,
screenshots: [
"/img/template/form1.jpg",
"/img/template/form1.jpg",
"/img/template/form1.jpg"
],
// Technical details
menuStructure: [
{ title: "Dashboard", route: "/notifications/dashboard", icon: "view-dashboard", permission: "notifications:read" },
{ title: "Send Notification", route: "/notifications/send", icon: "send", permission: "notifications:write" },
{ title: "Templates", route: "/notifications/templates", icon: "file-document-multiple", permission: "notifications:write" },
{ title: "History", route: "/notifications/history", icon: "history", permission: "notifications:read" },
{ title: "Settings", route: "/notifications/settings", icon: "cog", permission: "notifications:admin" },
{ title: "Reports", route: "/notifications/reports", icon: "chart-line", permission: "notifications:admin" }
],
apiEndpoints: [
{ path: "/api/notifications", methods: ["GET", "POST", "PUT", "DELETE"] },
{ path: "/api/notifications/templates", methods: ["GET", "POST", "PUT", "DELETE"] },
{ path: "/api/notifications/send", methods: ["POST"] },
{ path: "/api/notifications/schedule", methods: ["GET", "POST", "PUT", "DELETE"] }
],
folderStructure: [
{ type: "folder", name: "pages/notifications/", description: "All notification-related pages" },
{ type: "folder", name: "components/notification/", description: "Notification Vue components" },
{ type: "folder", name: "server/api/notifications/", description: "API endpoints for notifications" },
{ type: "folder", name: "composables/", description: "Notification composables and utilities" },
{ type: "file", name: "migrations/001_create_notifications.sql", description: "Creates notifications table" },
{ type: "file", name: "migrations/002_create_templates.sql", description: "Creates notification templates table" },
{ type: "file", name: "migrations/003_create_schedules.sql", description: "Creates notification schedules table" }
],
authentikIntegration: {
applicationSlug: "notification-management",
requiredScopes: ["notifications:read", "notifications:write", "notifications:admin"],
groups: ["notification-users", "notification-admins"],
permissions: [
{ scope: "notifications:read", description: "View notifications and templates" },
{ scope: "notifications:write", description: "Create and send notifications" },
{ scope: "notifications:admin", description: "Manage settings and view reports" }
]
},
dependencies: {
corrad: "^2.0.0",
authentik: "^2023.10.0",
nodemailer: "^6.9.0",
twilio: "^4.19.0"
},
features: [
"Multi-channel notifications (Email, SMS, Push)",
"Template management with variables",
"Scheduled and recurring notifications",
"Delivery tracking and analytics",
"Authentik integration for permissions",
"REST API with full CRUD operations",
"Real-time dashboard with statistics",
"Export reports in multiple formats"
],
// Mock markdown content
markdownContent: `# Notification Management System
## Overview
The Notification Management System is a comprehensive solution for managing all types of notifications in your CORRAD+ application. It provides a unified interface for sending emails, SMS, and push notifications with advanced templating and scheduling capabilities.
## Key Features
### 🔔 Multi-Channel Support
- **Email Notifications**: Rich HTML templates with attachment support
- **SMS Notifications**: Integrated with Twilio for reliable delivery
- **Push Notifications**: Web and mobile push notification support
### 📝 Template Management
- Visual template editor with drag-and-drop interface
- Variable substitution and dynamic content
- Template versioning and approval workflow
- Multi-language template support
### Advanced Scheduling
- One-time and recurring notifications
- Time-zone aware scheduling
- Bulk notification processing
- Queue management with priority levels
### 📊 Analytics & Reporting
- Real-time delivery tracking
- Open and click-through rates
- Bounce and unsubscribe management
- Comprehensive analytics dashboard
## Technical Specifications
### Database Schema
The plugin creates three main tables:
- \`notifications\`: Stores all notification records
- \`notification_templates\`: Template definitions and content
- \`notification_schedules\`: Scheduling information
### API Endpoints
All endpoints are secured with Authentik permissions:
- \`GET /api/notifications\`: List notifications
- \`POST /api/notifications\`: Send new notification
- \`PUT /api/notifications/{id}\`: Update notification
- \`DELETE /api/notifications/{id}\`: Delete notification
### Menu Integration
The plugin adds a "Notifications" menu with the following items:
- Dashboard (notifications:read)
- Send Notification (notifications:write)
- Templates (notifications:write)
- History (notifications:read)
- Settings (notifications:admin)
- Reports (notifications:admin)
## Installation Requirements
### System Requirements
- CORRAD Framework v2.0.0 or higher
- Authentik v2023.10.0 or higher
- Node.js v18+ for email processing
- Database: PostgreSQL 12+ recommended
### External Services
- **Email**: SMTP server or service (SendGrid, Amazon SES, etc.)
- **SMS**: Twilio account for SMS notifications
- **Push**: Firebase Cloud Messaging for push notifications
## Configuration
After installation, configure the following in your environment:
\`\`\`env
# Email Configuration
SMTP_HOST=your-smtp-host
SMTP_PORT=587
SMTP_USER=your-username
SMTP_PASS=your-password
# SMS Configuration (Twilio)
TWILIO_ACCOUNT_SID=your-account-sid
TWILIO_AUTH_TOKEN=your-auth-token
TWILIO_PHONE_NUMBER=your-twilio-number
# Push Notifications
FCM_SERVER_KEY=your-fcm-server-key
\`\`\`
## Post-Installation Steps
1. **Configure Authentik Groups**: Create notification-users and notification-admins groups
2. **Set Up SMTP**: Configure email settings in the admin panel
3. **SMS Setup**: Add Twilio credentials for SMS functionality
4. **Template Import**: Import default notification templates
5. **User Training**: Provide access to users based on their roles
## Support & Documentation
For detailed documentation and support:
- Plugin Documentation: [Link to docs]
- API Reference: [Link to API docs]
- Community Forum: [Link to forum]
- Bug Reports: [Link to issues]
`
});
// Mock related plugins
const relatedPlugins = ref([
{
id: 2,
name: "report-management",
displayName: "Report Management System",
icon: "mdi:chart-line",
color: "blue",
rating: 4.8,
price: "Free"
},
{
id: 3,
name: "rbac",
displayName: "RBAC System",
icon: "mdi:shield-account",
color: "green",
rating: 4.9,
price: "Free"
}
]);
// Remove activeTab since RsTab handles this internally
// Simple markdown to HTML converter
const markdownToHtml = (markdown) => {
let html = markdown
// Headers
.replace(/^### (.*$)/gim, '<h3 class="text-lg font-semibold mt-3 mb-2">$1</h3>')
.replace(/^## (.*$)/gim, '<h2 class="text-xl font-semibold mt-4 mb-2">$1</h2>')
.replace(/^# (.*$)/gim, '<h1 class="text-2xl font-bold mt-4 mb-3">$1</h1>')
// Bold text
.replace(/\*\*(.*?)\*\*/gim, '<strong>$1</strong>')
// Italic text
.replace(/\*(.*?)\*/gim, '<em>$1</em>')
// Code blocks
.replace(/```([^`]+?)```/gims, '<pre class="bg-gray-100 p-3 rounded-lg mt-2 mb-2 overflow-x-auto"><code>$1</code></pre>')
// Inline code
.replace(/`([^`]+?)`/gim, '<code class="bg-gray-100 px-1.5 py-0.5 rounded text-sm">$1</code>')
// Links
.replace(/\[([^\]]+)\]\(([^\)]+)\)/gim, '<a href="$2" class="text-blue-600 hover:underline">$1</a>')
// Line breaks
.replace(/\n/gim, '<br>');
// Handle bullet points as lists
const lines = html.split('<br>');
let inList = false;
let result = [];
for (let line of lines) {
if (line.match(/^- /)) {
if (!inList) {
result.push('<ul class="list-disc pl-4 space-y-0.5 my-2">');
inList = true;
}
result.push('<li>' + line.replace(/^- /, '') + '</li>');
} else {
if (inList) {
result.push('</ul>');
inList = false;
}
if (line.trim()) {
result.push(line);
}
}
}
if (inList) {
result.push('</ul>');
}
return result.join('<br>').replace(/<br><br>/gim, '<br>');
};
const getColorClasses = (color) => {
const colorMap = {
blue: 'bg-blue-100 text-blue-600',
green: 'bg-green-100 text-green-600',
purple: 'bg-purple-100 text-purple-600',
orange: 'bg-orange-100 text-orange-600',
indigo: 'bg-indigo-100 text-indigo-600',
red: 'bg-red-100 text-red-600'
};
return colorMap[color] || 'bg-gray-100 text-gray-600';
};
const handleInstall = () => {
showInstallModal.value = true;
};
const confirmInstall = async () => {
showInstallModal.value = false;
isInstalling.value = true;
installationLog.value = [];
installProgress.value = 0;
const steps = [
{ progress: 10, step: 'Validating plugin package...', message: 'Checking plugin structure and dependencies' },
{ progress: 25, step: 'Setting up Authentik integration...', message: 'Creating application and configuring scopes' },
{ progress: 40, step: 'Running database migrations...', message: 'Creating notification tables and indexes' },
{ progress: 60, step: 'Installing menu structure...', message: 'Registering navigation menu items' },
{ progress: 75, step: 'Configuring API routes...', message: 'Setting up API endpoints and permissions' },
{ progress: 90, step: 'Running post-install hooks...', message: 'Executing plugin initialization scripts' },
{ progress: 100, step: 'Installation complete!', message: 'Plugin installed successfully and ready to use' }
];
for (const step of steps) {
await new Promise(resolve => setTimeout(resolve, 1500));
installProgress.value = step.progress;
installationStep.value = step.step;
installationLog.value.push({
message: step.message,
timestamp: new Date().toLocaleTimeString(),
type: step.progress === 100 ? 'success' : 'info'
});
}
isInstalling.value = false;
pluginData.value.isInstalled = true;
nuxtApp.$swal.fire({
title: "Success",
text: "Plugin installed successfully! You can now access it from the navigation menu.",
icon: "success",
timer: 3000,
showConfirmButton: false,
});
};
const goBack = () => {
navigateTo('/devtool/plugin-manager/store');
};
// In real app, you would fetch plugin data based on slug
onMounted(async () => {
// Simulate API call
// const plugin = await $fetch(`/api/plugins/${pluginSlug}`);
// pluginData.value = plugin;
});
</script>
<template>
<div>
<LayoutsBreadcrumb />
<!-- Header with plugin info -->
<rs-card class="mb-6">
<template #body>
<div class="flex items-start space-x-6">
<!-- Plugin Icon -->
<div
:class="getColorClasses(pluginData.color)"
class="w-20 h-20 rounded-xl flex items-center justify-center flex-shrink-0"
>
<Icon :name="pluginData.icon" size="40" />
</div>
<!-- Plugin Details -->
<div class="flex-1 min-w-0">
<div class="flex items-start justify-between">
<div>
<h1 class="text-3xl font-bold text-gray-900 mb-2">{{ pluginData.displayName }}</h1>
<p class="text-gray-600 mb-3">{{ pluginData.description }}</p>
<div class="flex items-center space-x-6 text-sm">
<div class="flex items-center space-x-1">
<Icon name="mdi:account" size="16" class="text-gray-400" />
<span>{{ pluginData.author }}</span>
</div>
<div class="flex items-center space-x-1">
<Icon name="mdi:star" size="16" class="text-yellow-500" />
<span>{{ pluginData.rating }}</span>
</div>
<div class="flex items-center space-x-1">
<Icon name="mdi:download" size="16" class="text-gray-400" />
<span>{{ pluginData.downloads.toLocaleString() }} downloads</span>
</div>
<div class="flex items-center space-x-1">
<Icon name="mdi:update" size="16" class="text-gray-400" />
<span>Updated {{ pluginData.lastUpdated }}</span>
</div>
</div>
<!-- Tags -->
<div class="flex flex-wrap gap-2 mt-3">
<rs-badge
v-for="tag in pluginData.tags"
:key="tag"
variant="secondary"
size="sm"
>
{{ tag }}
</rs-badge>
</div>
</div>
<!-- Action buttons -->
<div class="flex flex-col space-y-3">
<rs-button
v-if="!pluginData.isInstalled"
size="lg"
@click="handleInstall"
:disabled="isInstalling"
class="min-w-[120px]"
>
<Icon v-if="!isInstalling" name="mdi:download" class="mr-2" />
<Icon v-else name="mdi:loading" class="mr-2 animate-spin" />
{{ isInstalling ? 'Installing...' : 'Install' }}
</rs-button>
<rs-badge v-else variant="success" size="lg" class="px-4 py-2">
<Icon name="mdi:check" class="mr-2" />
Installed
</rs-badge>
<div class="text-center">
<span class="text-2xl font-bold text-primary">{{ pluginData.price }}</span>
<p class="text-xs text-gray-500">{{ pluginData.size }}</p>
</div>
</div>
</div>
</div>
</div>
</template>
</rs-card>
<!-- Installation Progress -->
<rs-card v-if="isInstalling" class="mb-6">
<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-2">
<span>{{ installationStep }}</span>
<span>{{ installProgress }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-3">
<div
class="bg-primary h-3 rounded-full transition-all duration-500"
:style="{ width: installProgress + '%' }"
></div>
</div>
</div>
<!-- Installation Log -->
<div class="bg-gray-50 rounded-lg p-4 max-h-40 overflow-y-auto">
<div
v-for="(log, index) in installationLog"
:key="index"
class="flex items-start space-x-2 text-sm mb-2 last:mb-0"
>
<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>
<!-- Tabs with Content -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Main Content -->
<div class="lg:col-span-2">
<rs-tab class="mb-6">
<!-- Overview Tab -->
<rs-tab-item title="Overview" :active="true">
<rs-card class="mb-6">
<template #header>
<Icon name="mdi:information-outline" class="mr-2" />
Plugin Overview
</template>
<template #body>
<div class="space-y-4">
<div>
<h3 class="font-semibold text-lg mb-3">What's Included</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="text-center p-4 bg-blue-50 rounded-lg">
<Icon name="mdi:menu" size="32" class="text-blue-600 mx-auto mb-2" />
<p class="font-medium">{{ pluginData.menuStructure.length }} Menu Items</p>
<p class="text-sm text-gray-600">Navigation pages</p>
</div>
<div class="text-center p-4 bg-green-50 rounded-lg">
<Icon name="mdi:api" size="32" class="text-green-600 mx-auto mb-2" />
<p class="font-medium">{{ pluginData.apiEndpoints.length }} API Endpoints</p>
<p class="text-sm text-gray-600">REST API routes</p>
</div>
<div class="text-center p-4 bg-purple-50 rounded-lg">
<Icon name="mdi:database" size="32" class="text-purple-600 mx-auto mb-2" />
<p class="font-medium">3 Database Tables</p>
<p class="text-sm text-gray-600">SQL migrations</p>
</div>
</div>
</div>
<div>
<h3 class="font-semibold text-lg mb-3">Screenshots</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div
v-for="(screenshot, index) in pluginData.screenshots"
:key="index"
class="relative rounded-lg overflow-hidden border"
>
<img
:src="screenshot"
:alt="`Screenshot ${index + 1}`"
class="w-full h-48 object-cover"
/>
</div>
</div>
</div>
</div>
</template>
</rs-card>
</rs-tab-item>
<!-- Features Tab -->
<rs-tab-item title="Features">
<rs-card>
<template #header>
<Icon name="mdi:star" class="mr-2" />
Key Features
</template>
<template #body>
<div class="space-y-4">
<div
v-for="(feature, index) in pluginData.features"
:key="index"
class="flex items-start space-x-3 p-3 bg-gray-50 rounded-lg"
>
<Icon name="mdi:check-circle" class="text-green-500 mt-0.5" size="20" />
<span class="text-gray-900">{{ feature }}</span>
</div>
</div>
</template>
</rs-card>
</rs-tab-item>
<!-- Technical Details Tab -->
<rs-tab-item title="Technical Details">
<div class="space-y-6">
<!-- Menu Structure -->
<rs-card>
<template #header>
<Icon name="mdi:menu" class="mr-2" />
Menu Structure
</template>
<template #body>
<div class="space-y-3">
<div
v-for="(menu, index) in pluginData.menuStructure"
:key="index"
class="flex items-center justify-between p-3 border rounded-lg"
>
<div class="flex items-center space-x-3">
<Icon :name="`mdi:${menu.icon}`" size="20" class="text-gray-600" />
<div>
<p class="font-medium">{{ menu.title }}</p>
<p class="text-sm text-gray-600">{{ menu.route }}</p>
</div>
</div>
<rs-badge variant="secondary" size="sm">{{ menu.permission }}</rs-badge>
</div>
</div>
</template>
</rs-card>
<!-- API Endpoints -->
<rs-card>
<template #header>
<Icon name="mdi:api" class="mr-2" />
API Endpoints
</template>
<template #body>
<div class="space-y-3">
<div
v-for="(endpoint, index) in pluginData.apiEndpoints"
:key="index"
class="p-3 border rounded-lg"
>
<div class="flex items-center justify-between mb-2">
<code class="text-sm bg-gray-100 px-2 py-1 rounded">{{ endpoint.path }}</code>
</div>
<div class="flex flex-wrap gap-2">
<rs-badge
v-for="method in endpoint.methods"
:key="method"
:variant="method === 'GET' ? 'success' : method === 'POST' ? 'primary' : method === 'PUT' ? 'warning' : 'danger'"
size="sm"
>
{{ method }}
</rs-badge>
</div>
</div>
</div>
</template>
</rs-card>
<!-- Folder Structure -->
<rs-card>
<template #header>
<Icon name="mdi:folder" class="mr-2" />
Folder Structure Changes
</template>
<template #body>
<div class="space-y-2">
<div
v-for="(item, index) in pluginData.folderStructure"
:key="index"
class="flex items-start space-x-3 p-2"
>
<Icon
:name="item.type === 'folder' ? 'mdi:folder' : 'mdi:file'"
:class="item.type === 'folder' ? 'text-blue-500' : 'text-gray-500'"
size="16"
class="mt-1"
/>
<div class="flex-1">
<code class="text-sm">{{ item.name }}</code>
<p class="text-xs text-gray-600 mt-1">{{ item.description }}</p>
</div>
</div>
</div>
</template>
</rs-card>
</div>
</rs-tab-item>
<!-- Documentation Tab -->
<rs-tab-item title="Documentation">
<rs-card>
<template #header>
<Icon name="mdi:book-open" class="mr-2" />
Documentation
</template>
<template #body>
<div class="prose max-w-none">
<div class="markdown-content" v-html="markdownToHtml(pluginData.markdownContent)"></div>
</div>
</template>
</rs-card>
</rs-tab-item>
</rs-tab>
</div>
<!-- Sidebar -->
<div class="space-y-6">
<!-- Plugin Info -->
<rs-card>
<template #header>
<Icon name="mdi:information" class="mr-2" />
Plugin Information
</template>
<template #body>
<div class="space-y-3 text-sm">
<div class="flex justify-between">
<span class="text-gray-600">Version:</span>
<span class="font-medium">{{ pluginData.version }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">Size:</span>
<span class="font-medium">{{ pluginData.size }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">License:</span>
<span class="font-medium">{{ pluginData.license }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">Category:</span>
<span class="font-medium capitalize">{{ pluginData.category }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-gray-600">Homepage:</span>
<a :href="pluginData.homepage" target="_blank" class="text-primary hover:text-primary/80 text-xs">
<Icon name="mdi:open-in-new" size="14" />
</a>
</div>
</div>
</template>
</rs-card>
<!-- Authentik Integration -->
<rs-card>
<template #header>
<Icon name="mdi:shield-account" class="mr-2" />
Authentik Integration
</template>
<template #body>
<div class="space-y-3 text-sm">
<div>
<p class="text-gray-600 mb-2">Required Scopes:</p>
<div class="space-y-1">
<rs-badge
v-for="scope in pluginData.authentikIntegration.requiredScopes"
:key="scope"
variant="secondary"
size="sm"
class="block w-fit"
>
{{ scope }}
</rs-badge>
</div>
</div>
<div>
<p class="text-gray-600 mb-2">User Groups:</p>
<div class="space-y-1">
<rs-badge
v-for="group in pluginData.authentikIntegration.groups"
:key="group"
variant="primary"
size="sm"
class="block w-fit"
>
{{ group }}
</rs-badge>
</div>
</div>
</div>
</template>
</rs-card>
<!-- Dependencies -->
<rs-card>
<template #header>
<Icon name="mdi:package" class="mr-2" />
Dependencies
</template>
<template #body>
<div class="space-y-2 text-sm">
<div
v-for="(version, name) in pluginData.dependencies"
:key="name"
class="flex justify-between items-center"
>
<span class="text-gray-900">{{ name }}</span>
<code class="text-xs bg-gray-100 px-2 py-1 rounded">{{ version }}</code>
</div>
</div>
</template>
</rs-card>
<!-- Related Plugins -->
<rs-card>
<template #header>
<Icon name="mdi:puzzle" class="mr-2" />
Related Plugins
</template>
<template #body>
<div class="space-y-3">
<div
v-for="plugin in relatedPlugins"
:key="plugin.id"
class="flex items-center space-x-3 p-2 hover:bg-gray-50 rounded-lg cursor-pointer"
@click="navigateTo(`/devtool/plugin-manager/store/${plugin.name}`)"
>
<div
:class="getColorClasses(plugin.color)"
class="w-10 h-10 rounded-lg flex items-center justify-center"
>
<Icon :name="plugin.icon" size="20" />
</div>
<div class="flex-1 min-w-0">
<p class="font-medium text-sm truncate">{{ plugin.displayName }}</p>
<div class="flex items-center space-x-2">
<div class="flex items-center">
<Icon name="mdi:star" size="12" class="text-yellow-500 mr-1" />
<span class="text-xs">{{ plugin.rating }}</span>
</div>
<span class="text-xs text-gray-600">{{ plugin.price }}</span>
</div>
</div>
</div>
</div>
</template>
</rs-card>
</div>
</div>
<!-- Back Button -->
<div class="mt-6">
<rs-button variant="outline" @click="goBack">
<Icon name="mdi:arrow-left" class="mr-2" />
Back to Store
</rs-button>
</div>
<!-- Install Confirmation Modal -->
<rs-modal v-model="showInstallModal" title="Confirm Installation">
<template #body>
<div class="space-y-4">
<div class="flex items-start space-x-3">
<Icon name="mdi:alert-circle" class="text-orange-500 mt-1" size="20" />
<div>
<h4 class="font-medium text-gray-900 mb-2">Install {{ pluginData.displayName }}?</h4>
<p class="text-gray-600 text-sm mb-3">
This will install the plugin and make the following changes to your system:
</p>
<ul class="text-sm text-gray-600 space-y-1">
<li> Create {{ pluginData.menuStructure.length }} new menu items</li>
<li> Add {{ pluginData.apiEndpoints.length }} API endpoints</li>
<li> Run 3 database migrations</li>
<li> Configure Authentik integration</li>
</ul>
</div>
</div>
</div>
</template>
<template #footer>
<div class="flex justify-end space-x-3">
<rs-button variant="outline" @click="showInstallModal = false">
Cancel
</rs-button>
<rs-button @click="confirmInstall">
Install Plugin
</rs-button>
</div>
</template>
</rs-modal>
</div>
</template>
<style scoped>
.markdown-content {
line-height: 1.6;
}
.markdown-content h1 {
font-size: 1.75rem;
font-weight: bold;
margin: 1.5rem 0 1rem 0;
}
.markdown-content h2 {
font-size: 1.5rem;
font-weight: bold;
margin: 1.25rem 0 0.75rem 0;
}
.markdown-content h3 {
font-size: 1.25rem;
font-weight: bold;
margin: 1rem 0 0.5rem 0;
}
.markdown-content p {
margin: 0.75rem 0;
}
.markdown-content ul {
margin: 0.5rem 0;
padding-left: 1.5rem;
list-style-type: disc;
}
.markdown-content li {
margin: 0.25rem 0;
}
.markdown-content code {
background-color: #f3f4f6;
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
font-family: monospace;
font-size: 0.875rem;
}
</style>

View File

@ -0,0 +1,485 @@
<script setup>
definePageMeta({
title: "Plugin Store",
middleware: ["auth"],
requiresAuth: true,
});
const nuxtApp = useNuxtApp();
// Categories
const categories = ref([
{ id: 'all', name: 'All', icon: 'mdi:apps', count: 7 },
{ id: 'business', name: 'Business', icon: 'mdi:briefcase', count: 2 },
{ id: 'communication', name: 'Communication', icon: 'mdi:message', count: 1 },
{ id: 'productivity', name: 'Productivity', icon: 'mdi:chart-line', count: 2 },
{ id: 'security', name: 'Security', icon: 'mdi:shield', count: 2 }
]);
const selectedCategory = ref('all');
const searchQuery = ref('');
const sortBy = ref('popularity');
// Available plugins in store
const storePlugins = ref([
{
id: 1,
name: "rbac",
displayName: "RBAC System",
version: "1.0.0",
description: "Role-Based Access Control system integrated with Authentik for user management and permissions",
author: "CORRAD Team",
category: "security",
size: "3.2 MB",
rating: 4.9,
downloads: 2450,
price: "Free",
icon: "mdi:shield-account",
color: "green",
images: [],
tags: ["RBAC", "Authentication", "Permissions", "Authentik"],
features: [
"User Management",
"Role Assignment",
"Permission Control",
"Authentik Integration",
"SSO Support"
],
isInstalled: true,
lastUpdated: "2024-01-20"
},
{
id: 2,
name: "notification-management",
displayName: "Notification Management System",
version: "1.0.0",
description: "Complete notification system with email, SMS, push notifications, and template management",
author: "CORRAD Team",
category: "communication",
size: "2.8 MB",
rating: 4.8,
downloads: 1890,
price: "Free",
icon: "mdi:bell-ring",
color: "orange",
images: [],
tags: ["Notifications", "Email", "SMS", "Templates"],
features: [
"Multi-channel Notifications",
"Template Management",
"Delivery Tracking",
"Scheduling System",
"Analytics Dashboard"
],
isInstalled: true,
lastUpdated: "2024-01-18"
},
{
id: 3,
name: "business-process-maker",
displayName: "Business Process Maker",
version: "1.0.0",
description: "Visual business process designer and workflow automation system like ProcessMaker",
author: "CORRAD Team",
category: "productivity",
size: "4.5 MB",
rating: 4.7,
downloads: 1320,
price: "Free",
icon: "mdi:flowchart",
color: "blue",
images: [],
tags: ["BPM", "Workflow", "Automation", "Process"],
features: [
"Visual Process Designer",
"Workflow Automation",
"Task Assignment",
"Progress Tracking",
"Integration APIs"
],
isInstalled: false,
lastUpdated: "2024-01-22"
},
{
id: 4,
name: "queue-management",
displayName: "Queue Management System",
version: "1.0.0",
description: "Complete queue management system for organizing and processing tasks efficiently",
author: "CORRAD Team",
category: "productivity",
size: "2.1 MB",
rating: 4.6,
downloads: 987,
price: "Free",
icon: "mdi:format-list-numbered",
color: "indigo",
images: [],
tags: ["Queue", "Tasks", "Processing", "Management"],
features: [
"Queue Organization",
"Task Processing",
"Priority Management",
"Real-time Monitoring",
"Performance Analytics"
],
isInstalled: false,
lastUpdated: "2024-01-15"
},
{
id: 5,
name: "report-management",
displayName: "Report Management System",
version: "1.0.0",
description: "Advanced reporting system integrated with Metabase for analytics and dashboard creation",
author: "CORRAD Team",
category: "business",
size: "3.8 MB",
rating: 4.8,
downloads: 1567,
price: "Free",
icon: "mdi:chart-line",
color: "blue",
images: [],
tags: ["Reports", "Analytics", "Metabase", "Dashboard"],
features: [
"Custom Reports",
"Metabase Integration",
"Interactive Dashboards",
"Data Visualization",
"Scheduled Reports"
],
isInstalled: true,
lastUpdated: "2024-01-19"
},
{
id: 6,
name: "audit-trail",
displayName: "Audit Trail System",
version: "1.0.0",
description: "Comprehensive audit logging system using Loki and Grafana with integrated Nuxt pages",
author: "CORRAD Team",
category: "security",
size: "3.5 MB",
rating: 4.7,
downloads: 1234,
price: "Free",
icon: "mdi:file-document-alert",
color: "purple",
images: [],
tags: ["Audit", "Logging", "Loki", "Grafana"],
features: [
"Activity Logging",
"Loki Integration",
"Grafana Dashboards",
"Search & Filter",
"Compliance Reports"
],
isInstalled: false,
lastUpdated: "2024-01-16"
},
{
id: 7,
name: "edms",
displayName: "Electronic Document Management System",
version: "1.0.0",
description: "Complete document management system like Paperless-ngx for organizing and managing documents",
author: "CORRAD Team",
category: "business",
size: "4.2 MB",
rating: 4.6,
downloads: 892,
price: "Free",
icon: "mdi:file-document-multiple",
color: "red",
images: [],
tags: ["Documents", "Management", "OCR", "Archive"],
features: [
"Document Storage",
"OCR Processing",
"Search & Index",
"Version Control",
"Access Management"
],
isInstalled: false,
lastUpdated: "2024-01-14"
}
]);
// Computed properties
const filteredPlugins = computed(() => {
let plugins = storePlugins.value;
// Filter by category
if (selectedCategory.value !== 'all') {
plugins = plugins.filter(plugin => plugin.category === selectedCategory.value);
}
// Filter by search
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase();
plugins = plugins.filter(plugin =>
plugin.displayName.toLowerCase().includes(query) ||
plugin.description.toLowerCase().includes(query) ||
plugin.tags.some(tag => tag.toLowerCase().includes(query))
);
}
// Sort plugins
switch (sortBy.value) {
case 'popularity':
plugins.sort((a, b) => b.downloads - a.downloads);
break;
case 'rating':
plugins.sort((a, b) => b.rating - a.rating);
break;
case 'name':
plugins.sort((a, b) => a.displayName.localeCompare(b.displayName));
break;
case 'recent':
plugins.sort((a, b) => new Date(b.lastUpdated) - new Date(a.lastUpdated));
break;
}
return plugins;
});
const getColorClasses = (color) => {
const colorMap = {
blue: 'bg-blue-100 text-blue-600',
green: 'bg-green-100 text-green-600',
purple: 'bg-purple-100 text-purple-600',
orange: 'bg-orange-100 text-orange-600',
indigo: 'bg-indigo-100 text-indigo-600',
red: 'bg-red-100 text-red-600'
};
return colorMap[color] || 'bg-gray-100 text-gray-600';
};
const installPlugin = async (plugin) => {
if (plugin.isInstalled) return;
try {
// Simulate installation
plugin.isInstalled = true;
plugin.downloads += 1;
nuxtApp.$swal.fire({
title: "Success",
text: `${plugin.displayName} installed successfully`,
icon: "success",
timer: 2000,
showConfirmButton: false,
});
} catch (error) {
nuxtApp.$swal.fire({
title: "Error",
text: "Failed to install plugin",
icon: "error",
});
}
};
const viewPluginDetails = (plugin) => {
navigateTo(`/devtool/plugin-manager/store/${plugin.name}`);
};
const formatNumber = (num) => {
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'k';
}
return num.toString();
};
</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">Plugin Store</h1>
<p class="text-gray-600">
Discover and install new plugins to extend your application's functionality
</p>
</div>
<div class="hidden md:block">
<Icon name="mdi:store" size="64" class="text-primary/20" />
</div>
</div>
</template>
</rs-card>
<!-- Search and Filters -->
<rs-card class="mb-6">
<template #body>
<div class="space-y-4">
<!-- Search Bar -->
<div class="flex flex-col md:flex-row gap-4">
<div class="flex-1">
<FormKit
type="search"
placeholder="Search plugins, features, or categories..."
v-model="searchQuery"
outer-class="mb-0"
>
<template #prefix>
<Icon name="mdi:magnify" class="text-gray-400" />
</template>
</FormKit>
</div>
<div class="w-full md:w-48">
<FormKit
type="select"
v-model="sortBy"
:options="[
{ label: 'Most Popular', value: 'popularity' },
{ label: 'Highest Rated', value: 'rating' },
{ label: 'Name A-Z', value: 'name' },
{ label: 'Recently Updated', value: 'recent' }
]"
outer-class="mb-0"
/>
</div>
</div>
<!-- Categories -->
<div class="flex flex-wrap gap-2">
<button
v-for="category in categories"
:key="category.id"
@click="selectedCategory = category.id"
:class="[
'flex items-center px-4 py-2 rounded-full text-sm font-medium transition-colors',
selectedCategory === category.id
? 'bg-primary text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
]"
>
<Icon :name="category.icon" class="mr-2" size="16" />
{{ category.name }}
<span class="ml-2 px-2 py-0.5 bg-white/20 rounded-full text-xs">
{{ category.count }}
</span>
</button>
</div>
</div>
</template>
</rs-card>
<!-- Results Info -->
<div class="flex items-center justify-between mb-6">
<p class="text-gray-600">
{{ filteredPlugins.length }} plugin(s) found
</p>
<div class="flex items-center gap-2 text-sm text-gray-500">
<Icon name="mdi:information-outline" size="16" />
Click on a plugin to view details
</div>
</div>
<!-- Plugin Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-6">
<rs-card
v-for="plugin in filteredPlugins"
:key="plugin.id"
class="group hover:shadow-lg transition-all duration-300 cursor-pointer relative"
@click="viewPluginDetails(plugin)"
>
<template #body>
<div class="space-y-4">
<!-- Plugin Header -->
<div class="flex items-start justify-between">
<div class="flex items-center space-x-3">
<div
:class="getColorClasses(plugin.color)"
class="w-12 h-12 rounded-xl flex items-center justify-center"
>
<Icon :name="plugin.icon" size="24" />
</div>
<div>
<h3 class="font-semibold text-lg text-gray-900 group-hover:text-primary transition-colors">
{{ plugin.displayName }}
</h3>
<p class="text-sm text-gray-500">v{{ plugin.version }}</p>
</div>
</div>
<rs-badge v-if="plugin.isInstalled" variant="success" size="sm">
Installed
</rs-badge>
</div>
<!-- Plugin Description -->
<p class="text-gray-600 text-sm line-clamp-2">
{{ plugin.description }}
</p>
<!-- Plugin Stats -->
<div class="flex items-center justify-between text-sm text-gray-500">
<div class="flex items-center space-x-4">
<div class="flex items-center">
<Icon name="mdi:star" class="text-yellow-500 mr-1" size="16" />
{{ plugin.rating }}
</div>
<div class="flex items-center">
<Icon name="mdi:download" class="mr-1" size="16" />
{{ formatNumber(plugin.downloads) }}
</div>
</div>
<div class="text-right">
<div class="font-medium text-gray-900">{{ plugin.price }}</div>
<div class="text-xs">{{ plugin.size }}</div>
</div>
</div>
<!-- Tags -->
<div class="flex flex-wrap gap-1">
<span
v-for="tag in plugin.tags.slice(0, 3)"
:key="tag"
class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded-full"
>
{{ tag }}
</span>
<span v-if="plugin.tags.length > 3" class="text-xs text-gray-400">
+{{ plugin.tags.length - 3 }}
</span>
</div>
<!-- Action Button -->
<div class="pt-2 border-t">
<rs-button
class="w-full"
@click.stop="viewPluginDetails(plugin)"
>
<Icon name="mdi:eye" class="mr-2" size="16" />
View Details
</rs-button>
</div>
</div>
</template>
</rs-card>
</div>
<!-- Empty State -->
<div v-if="filteredPlugins.length === 0" class="text-center py-12">
<rs-card>
<template #body>
<div class="py-8">
<Icon name="mdi:store-remove" size="64" class="mx-auto text-gray-400 mb-4" />
<h3 class="text-lg font-medium text-gray-900 mb-2">No plugins found</h3>
<p class="text-gray-500 mb-4">
Try adjusting your search terms or browse a different category
</p>
<rs-button variant="outline" @click="searchQuery = ''; selectedCategory = 'all'">
Clear Filters
</rs-button>
</div>
</template>
</rs-card>
</div>
</div>
</template>

View File

@ -0,0 +1,516 @@
<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>

View File

@ -99,14 +99,3 @@ model site_settings {
settingModifiedDate DateTime? @db.DateTime(0) settingModifiedDate DateTime? @db.DateTime(0)
siteLoginLogo String? @db.VarChar(500) siteLoginLogo String? @db.VarChar(500)
} }
model ticket {
ticketId Int @id @default(autoincrement())
title String
description String
dueDate DateTime
priority String @default("medium")
status String @default("pending")
createdAt DateTime @default(now())
updatedAt DateTime
}