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>