593 lines
18 KiB
Vue
593 lines
18 KiB
Vue
<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> |