Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
308 changes: 153 additions & 155 deletions server/frontend/src/Components/Plugins/Plugins.jsx
Original file line number Diff line number Diff line change
@@ -1,177 +1,215 @@
import React from "react";
import { useState, useEffect } from "react";
import { apiFetch, getData } from "../../../FetchApi";
import { useNavigate } from "react-router-dom";
import { useNavigate, Link } from "react-router-dom";


const Plugins = () => {
const navigate = useNavigate("");
const clickAndNavigate = function (path) {
navigate(`/${path}`);
};

const [pluginsArr, setPluginsArr] = useState([]);
const [hostGroups, setHostGroups] = useState([]);
const [addGroupOpen, setAddGroupOpen] = useState(false);
const [newGroupName, setNewGroupName] = useState("");
const [addPluginGroupId, setAddPluginGroupId] = useState(null);

const getPlugginsArr = async function () {
const load = async () => {
const data = await getData("plugins");
if (data) {
setPluginsArr(data.plugins);
if (data.hostGroups) setHostGroups(data.hostGroups);
}
};

useEffect(() => {
getPlugginsArr();
}, []);
useEffect(() => { load(); }, []);

const disablePlugin = async function (e) {
const data = await apiFetch({ id: e.target.id }, `plugin_disable`);
if (data.status === "success") {
getPlugginsArr();
}
const disablePlugin = async (id) => {
const data = await apiFetch({ id }, `plugin_disable`);
if (data.status === "success") load();
};

const createGroup = async () => {
if (!newGroupName.trim()) return;
await apiFetch({ name: newGroupName.trim(), slackWebhook: "" }, "host_groups");
await apiFetch({ name: newGroupName.trim() }, "host_groups");
setNewGroupName("");
setAddGroupOpen(false);
getPlugginsArr();
load();
};

const deleteGroup = async (id) => {
if (!window.confirm("Delete this group? Hosts will become ungrouped.")) return;
await apiFetch({}, `host_groups/${id}/delete`);
getPlugginsArr();
load();
};

const removePluginFromGroup = async (groupId, pluginId) => {
await apiFetch({}, `host_groups/${groupId}/plugin/${pluginId}/remove`);
load();
};

const slackPlugin = pluginsArr.find((p) => p.id === "slack-notifications");
const availablePlugins = pluginsArr;

return (
<div>
<div className="container mx-auto flex justify-center px-4">
<div className="min-w-2/3 p-4 my-5 bg-gray-100 rounded-lg shadow-md sm:p-8 dark:bg-gray-600 dark:border-gray-700">
<div className="flex justify-between items-center mb-5">
<div>
<a
onClick={() => { clickAndNavigate("home"); }}
className="text-white dark:text-gray-800 bg-green-700 hover:bg-green-800 focus:ring-4 focus:outline-none
focus:ring-green-300 font-medium rounded-lg text-sm px-5 py-2.5
text-center mr-3 md:mr-0 dark:bg-green-400 dark:hover:bg-green-500 dark:focus:ring-green-800 flex items-center"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5 mr-2"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z"
clipRule="evenodd"
/>
</svg>
Back to hosts list
</a>
</div>
<Link
to="/home"
className="text-white dark:text-gray-800 bg-green-700 hover:bg-green-800 focus:ring-4 focus:outline-none
focus:ring-green-300 font-medium rounded-lg text-sm px-5 py-2.5
text-center mr-3 md:mr-0 dark:bg-green-400 dark:hover:bg-green-500 dark:focus:ring-green-800 flex items-center"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clipRule="evenodd" />
</svg>
Back to hosts list
</Link>
</div>

<div className="flex justify-between items-center mb-4">
<h5 className="mb-5 text-3xl font-bold leading-none text-gray-900 dark:text-white">
Available plugins
</h5>
</div>
{/* Plugins section */}
<h5 className="mb-5 text-3xl font-bold leading-none text-gray-900 dark:text-white">
Available plugins
</h5>

<div className="max-w-2/3 gap-x-10 gap-y-10 divide-gray-200 dark:divide-gray-700 flex flex-wrap">
{pluginsArr.map((pl) => {
return (
<div
key={pl.id}
className="w-60 bg-white rounded-lg shadow-md dark:bg-gray-800 dark:border-gray-700 flex flex-col"
>
<img
className="w-40 h-40 my-6 mx-auto rounded-t-lg"
src={pl.iconUrlOrBase64}
alt="product image"
/>
<div className="px-6 pb-3"></div>
<div className="flex justify-between items-center px-6 pb-6 mt-auto">
{pl.pluginEnabled ? (
<a
onClick={() => { clickAndNavigate(`plugin/${pl.id}`); }}
className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 cursor-pointer"
>
Settings
</a>
) : (
<a
onClick={() => { clickAndNavigate(`plugin/${pl.id}`); }}
className="text-white w-full bg-green-700 hover:bg-green-800 focus:ring-4 focus:outline-none focus:ring-green-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-green-600 dark:hover:bg-green-700 dark:focus:ring-green-800 cursor-pointer"
>
Enable
</a>
)}
{pl.pluginEnabled ? (
<button
onClick={(e) => { disablePlugin(e); }}
id={pl.id}
className="ml-5 text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800 cursor-pointer"
>
Disable
</button>
) : null}
</div>
<div className="max-w-2/3 gap-x-10 gap-y-10 divide-gray-200 dark:divide-gray-700 flex flex-wrap mb-10">
{pluginsArr.map((pl) => (
<div
key={pl.id}
className="w-60 bg-white rounded-lg shadow-md dark:bg-gray-800 dark:border-gray-700 flex flex-col"
>
<img className="w-40 h-40 my-6 mx-auto rounded-t-lg" src={pl.iconUrlOrBase64} alt={pl.id} />
<div className="flex justify-between items-center px-6 pb-6 mt-auto">
{pl.pluginEnabled ? (
<Link
to={`/plugin/${pl.id}`}
className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
>
Settings
</Link>
) : (
<Link
to={`/plugin/${pl.id}`}
className="text-white w-full bg-green-700 hover:bg-green-800 focus:ring-4 focus:outline-none focus:ring-green-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-green-600 dark:hover:bg-green-700 dark:focus:ring-green-800"
>
Enable
</Link>
)}
{pl.pluginEnabled && (
<button
onClick={() => disablePlugin(pl.id)}
className="ml-5 text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800 cursor-pointer"
>
Disable
</button>
)}
</div>
);
})}
</div>
))}
</div>

{/* Groups section */}
<h5 className="mb-5 text-3xl font-bold leading-none text-gray-900 dark:text-white">
Groups
</h5>

{/* Host group cards */}
<div className="max-w-2/3 gap-x-10 gap-y-10 flex flex-wrap">
{hostGroups.map((g) => (
<div
key={`group_${g.id}`}
key={g.id}
className="w-60 bg-white rounded-lg shadow-md dark:bg-gray-800 dark:border-gray-700 flex flex-col"
>
{slackPlugin && (
<img
className="w-40 h-40 my-6 mx-auto rounded-t-lg"
src={slackPlugin.iconUrlOrBase64}
alt="Slack"
/>
<div className="px-6 pt-5 pb-2">
<p className="text-base font-semibold text-gray-900 dark:text-white">{g.name}</p>
</div>

{/* Installed plugins */}
{g.installedPlugins && g.installedPlugins.length > 0 && (
<div className="px-6 pb-2 flex flex-col gap-2">
{g.installedPlugins.map((pluginId) => {
const pl = pluginsArr.find((p) => p.id === pluginId);
return (
<div key={pluginId} className="flex flex-col justify-center gap-2">
<div className="flex gap-2">
{pl && <img className="w-6 h-6 rounded" src={pl.iconUrlOrBase64} alt={pluginId} />}
<span className="text-sm text-gray-700 dark:text-gray-300 flex-1">{pluginId}</span>
</div>
<div className="flex gap-2">
<Link
to={`/plugin/${pluginId}?groupId=${g.id}`}
className="text-xs text-center w-full text-white bg-blue-600 hover:bg-blue-700 rounded px-2 py-1"
>
Settings
</Link>
<button
onClick={() => removePluginFromGroup(g.id, pluginId)}
className="text-xs text-center w-full text-white bg-red-600 hover:bg-red-700 rounded px-2 py-1"
>
Remove
</button>
</div>
</div>
);
})}
</div>
)}
<div className="px-6 pb-3 text-center">
<p className="text-sm font-semibold text-gray-900 dark:text-white">{g.name}</p>
{g.channelName && (
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">{g.channelName}</p>

{/* Add plugin button */}
<div className="px-6 pb-4 my-auto pt-3">
{addPluginGroupId === g.id ? (
<div className="flex flex-col gap-2">
<select
className="w-full bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg p-2 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
defaultValue=""
onChange={(e) => {
if (e.target.value) {
navigate(`/plugin/${e.target.value}?groupId=${g.id}`);
}
}}
>
<option value="" disabled>Select plugin…</option>
{availablePlugins
.filter((p) => !g.installedPlugins?.includes(p.id))
.map((p) => (
<option key={p.id} value={p.id}>{p.id}</option>
))}
</select>
<button
onClick={() => setAddPluginGroupId(null)}
className="text-xs text-gray-600 dark:text-gray-300 underline"
>
Cancel
</button>
</div>
) : (
<div className="flex flex-col gap-2 items-center">
<button
onClick={() => setAddPluginGroupId(g.id)}
disabled={availablePlugins.filter((p) => !g.installedPlugins?.includes(p.id)).length === 0}
className="text-white w-full bg-green-700 hover:bg-green-800 disabled:opacity-40 font-medium rounded-lg text-sm px-4 py-2 dark:bg-green-600 dark:hover:bg-green-700"
>
+ Add Plugin
</button>
<button
onClick={() => deleteGroup(g.id)}
className="text-white w-full bg-red-700 hover:bg-red-800 font-medium rounded-lg text-sm px-4 py-2 dark:bg-red-600 dark:hover:bg-red-700"
>
Delete Group
</button>
</div>
)}
</div>
<div className="flex justify-between items-center px-6 pb-6 mt-auto">
<a
onClick={() => { navigate(`/plugin/slack-notifications?groupId=${g.id}`); }}
className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 cursor-pointer"
>
Settings
</a>
<button
onClick={() => deleteGroup(g.id)}
className="ml-5 text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800 cursor-pointer"
>
Delete
</button>
</div>
</div>
))}

{/* Add group card */}
<div className="w-60 bg-white rounded-lg shadow-md dark:bg-gray-800 dark:border-gray-700 flex flex-col items-center justify-center min-h-[280px]">
<div className="w-60 bg-white rounded-lg shadow-md dark:bg-gray-800 dark:border-gray-700 flex flex-col items-center justify-center min-h-[160px]">
{!addGroupOpen ? (
<button
onClick={() => setAddGroupOpen(true)}
className="text-white bg-green-700 hover:bg-green-800 focus:ring-4 focus:outline-none focus:ring-green-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-green-600 dark:hover:bg-green-700 dark:focus:ring-green-800 cursor-pointer"
className="text-white bg-green-700 hover:bg-green-800 focus:ring-4 focus:outline-none focus:ring-green-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-green-600 dark:hover:bg-green-700"
>
+ Add Slack Group
+ Add Group
</button>
) : (
<div className="p-4 w-full">
Expand All @@ -182,6 +220,7 @@ const Plugins = () => {
className="w-full mb-3 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:text-white"
placeholder="Group name"
onKeyDown={(e) => e.key === "Enter" && createGroup()}
autoFocus
/>
<div className="flex gap-2">
<button
Expand All @@ -203,47 +242,6 @@ const Plugins = () => {
</div>
</div>
</div>

<div
id="toast-copied"
className="fixed right-2 bottom-2 hidden flex items-center w-full max-w-xs p-4 mb-4 text-gray-500 bg-white rounded-lg shadow dark:text-green-900 dark:bg-green-100"
role="alert"
>
<div className="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-green-500 bg-green-100 rounded-lg dark:bg-green-100 dark:text-green-900">
<svg
className="w-5 h-5"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
></path>
</svg>
</div>
<div className="ml-3 text-sm font-normal">Copied to clipboard!</div>
<button
type="button"
className="bg-transparent ml-auto -mx-1.5 -my-1.5 text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 inline-flex h-8 w-8 dark:text-green-900 dark:hover:text-black"
aria-label="Close"
>
<span className="sr-only">Close</span>
<svg
className="w-5 h-5"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
></path>
</svg>
</button>
</div>
</div>
);
};
Expand Down
Loading