@@ -6,172 +6,210 @@ import { useNavigate } from "react-router-dom";
66
77const Plugins = ( ) => {
88 const navigate = useNavigate ( "" ) ;
9- const clickAndNavigate = function ( path ) {
10- navigate ( `/${ path } ` ) ;
11- } ;
129
1310 const [ pluginsArr , setPluginsArr ] = useState ( [ ] ) ;
1411 const [ hostGroups , setHostGroups ] = useState ( [ ] ) ;
1512 const [ addGroupOpen , setAddGroupOpen ] = useState ( false ) ;
1613 const [ newGroupName , setNewGroupName ] = useState ( "" ) ;
14+ const [ addPluginGroupId , setAddPluginGroupId ] = useState ( null ) ;
1715
18- const getPlugginsArr = async function ( ) {
16+ const load = async ( ) => {
1917 const data = await getData ( "plugins" ) ;
2018 if ( data ) {
2119 setPluginsArr ( data . plugins ) ;
2220 if ( data . hostGroups ) setHostGroups ( data . hostGroups ) ;
2321 }
2422 } ;
2523
26- useEffect ( ( ) => {
27- getPlugginsArr ( ) ;
28- } , [ ] ) ;
24+ useEffect ( ( ) => { load ( ) ; } , [ ] ) ;
2925
30- const disablePlugin = async function ( e ) {
31- const data = await apiFetch ( { id : e . target . id } , `plugin_disable` ) ;
32- if ( data . status === "success" ) {
33- getPlugginsArr ( ) ;
34- }
26+ const disablePlugin = async ( id ) => {
27+ const data = await apiFetch ( { id } , `plugin_disable` ) ;
28+ if ( data . status === "success" ) load ( ) ;
3529 } ;
3630
3731 const createGroup = async ( ) => {
3832 if ( ! newGroupName . trim ( ) ) return ;
39- await apiFetch ( { name : newGroupName . trim ( ) , slackWebhook : "" } , "host_groups" ) ;
33+ await apiFetch ( { name : newGroupName . trim ( ) } , "host_groups" ) ;
4034 setNewGroupName ( "" ) ;
4135 setAddGroupOpen ( false ) ;
42- getPlugginsArr ( ) ;
36+ load ( ) ;
4337 } ;
4438
4539 const deleteGroup = async ( id ) => {
4640 if ( ! window . confirm ( "Delete this group? Hosts will become ungrouped." ) ) return ;
4741 await apiFetch ( { } , `host_groups/${ id } /delete` ) ;
48- getPlugginsArr ( ) ;
42+ load ( ) ;
43+ } ;
44+
45+ const removePluginFromGroup = async ( groupId , pluginId ) => {
46+ await apiFetch ( { } , `host_groups/${ groupId } /plugin/${ pluginId } /remove` ) ;
47+ load ( ) ;
4948 } ;
5049
51- const slackPlugin = pluginsArr . find ( ( p ) => p . id === "slack-notifications" ) ;
50+ const availablePlugins = pluginsArr ;
5251
5352 return (
5453 < div >
5554 < div className = "container mx-auto flex justify-center px-4" >
5655 < 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" >
5756 < div className = "flex justify-between items-center mb-5" >
58- < div >
59- < a
60- onClick = { ( ) => { clickAndNavigate ( "home" ) ; } }
61- className = "text-white dark:text-gray-800 bg-green-700 hover:bg-green-800 focus:ring-4 focus:outline-none
62- focus:ring-green-300 font-medium rounded-lg text-sm px-5 py-2.5
63- text-center mr-3 md:mr-0 dark:bg-green-400 dark:hover:bg-green-500 dark:focus:ring-green-800 flex items-center"
64- >
65- < svg
66- xmlns = "http://www.w3.org/2000/svg"
67- className = "h-5 w-5 mr-2"
68- viewBox = "0 0 20 20"
69- fill = "currentColor"
70- >
71- < path
72- fillRule = "evenodd"
73- 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"
74- clipRule = "evenodd"
75- />
76- </ svg >
77- Back to hosts list
78- </ a >
79- </ div >
57+ < a
58+ onClick = { ( ) => navigate ( "/home" ) }
59+ className = "text-white dark:text-gray-800 bg-green-700 hover:bg-green-800 focus:ring-4 focus:outline-none
60+ focus:ring-green-300 font-medium rounded-lg text-sm px-5 py-2.5
61+ text-center mr-3 md:mr-0 dark:bg-green-400 dark:hover:bg-green-500 dark:focus:ring-green-800 flex items-center cursor-pointer"
62+ >
63+ < svg xmlns = "http://www.w3.org/2000/svg" className = "h-5 w-5 mr-2" viewBox = "0 0 20 20" fill = "currentColor" >
64+ < 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" />
65+ </ svg >
66+ Back to hosts list
67+ </ a >
8068 </ div >
8169
82- < div className = "flex justify-between items-center mb-4" >
83- < h5 className = "mb-5 text-3xl font-bold leading-none text-gray-900 dark:text-white" >
84- Available plugins
85- </ h5 >
86- </ div >
70+ { /* Plugins section */ }
71+ < h5 className = "mb-5 text-3xl font-bold leading-none text-gray-900 dark:text-white" >
72+ Available plugins
73+ </ h5 >
8774
88- < div className = "max-w-2/3 gap-x-10 gap-y-10 divide-gray-200 dark:divide-gray-700 flex flex-wrap" >
89- { pluginsArr . map ( ( pl ) => {
90- return (
91- < div
92- key = { pl . id }
93- className = "w-60 bg-white rounded-lg shadow-md dark:bg-gray-800 dark:border-gray-700 flex flex-col"
94- >
95- < img
96- className = "w-40 h-40 my-6 mx-auto rounded-t-lg"
97- src = { pl . iconUrlOrBase64 }
98- alt = "product image"
99- />
100- < div className = "px-6 pb-3" > </ div >
101- < div className = "flex justify-between items-center px-6 pb-6 mt-auto" >
102- { pl . pluginEnabled ? (
103- < a
104- onClick = { ( ) => { clickAndNavigate ( `plugin/${ pl . id } ` ) ; } }
105- 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"
106- >
107- Settings
108- </ a >
109- ) : (
110- < a
111- onClick = { ( ) => { clickAndNavigate ( `plugin/${ pl . id } ` ) ; } }
112- 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"
113- >
114- Enable
115- </ a >
116- ) }
117- { pl . pluginEnabled ? (
118- < button
119- onClick = { ( e ) => { disablePlugin ( e ) ; } }
120- id = { pl . id }
121- 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"
122- >
123- Disable
124- </ button >
125- ) : null }
126- </ div >
75+ < div className = "max-w-2/3 gap-x-10 gap-y-10 divide-gray-200 dark:divide-gray-700 flex flex-wrap mb-10" >
76+ { pluginsArr . map ( ( pl ) => (
77+ < div
78+ key = { pl . id }
79+ className = "w-60 bg-white rounded-lg shadow-md dark:bg-gray-800 dark:border-gray-700 flex flex-col"
80+ >
81+ < img className = "w-40 h-40 my-6 mx-auto rounded-t-lg" src = { pl . iconUrlOrBase64 } alt = { pl . id } />
82+ < div className = "flex justify-between items-center px-6 pb-6 mt-auto" >
83+ { pl . pluginEnabled ? (
84+ < a
85+ onClick = { ( ) => navigate ( `/plugin/${ pl . id } ` ) }
86+ 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"
87+ >
88+ Settings
89+ </ a >
90+ ) : (
91+ < a
92+ onClick = { ( ) => navigate ( `/plugin/${ pl . id } ` ) }
93+ 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"
94+ >
95+ Enable
96+ </ a >
97+ ) }
98+ { pl . pluginEnabled && (
99+ < button
100+ onClick = { ( ) => disablePlugin ( pl . id ) }
101+ 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"
102+ >
103+ Disable
104+ </ button >
105+ ) }
127106 </ div >
128- ) ;
129- } ) }
107+ </ div >
108+ ) ) }
109+ </ div >
110+
111+ { /* Groups section */ }
112+ < h5 className = "mb-5 text-3xl font-bold leading-none text-gray-900 dark:text-white" >
113+ Groups
114+ </ h5 >
130115
131- { /* Host group cards */ }
116+ < div className = "max-w-2/3 gap-x-10 gap-y-10 flex flex-wrap" >
132117 { hostGroups . map ( ( g ) => (
133118 < div
134- key = { `group_ ${ g . id } ` }
119+ key = { g . id }
135120 className = "w-60 bg-white rounded-lg shadow-md dark:bg-gray-800 dark:border-gray-700 flex flex-col"
136121 >
137- { slackPlugin && (
138- < img
139- className = "w-40 h-40 my-6 mx-auto rounded-t-lg"
140- src = { slackPlugin . iconUrlOrBase64 }
141- alt = "Slack"
142- />
122+ < div className = "px-6 pt-5 pb-2" >
123+ < p className = "text-base font-semibold text-gray-900 dark:text-white" > { g . name } </ p >
124+ </ div >
125+
126+ { /* Installed plugins */ }
127+ { g . installedPlugins && g . installedPlugins . length > 0 && (
128+ < div className = "px-6 pb-2 flex flex-col gap-2" >
129+ { g . installedPlugins . map ( ( pluginId ) => {
130+ const pl = pluginsArr . find ( ( p ) => p . id === pluginId ) ;
131+ return (
132+ < div key = { pluginId } className = "flex flex-col justify-center gap-2" >
133+ < div className = "flex gap-2" >
134+ { pl && < img className = "w-6 h-6 rounded" src = { pl . iconUrlOrBase64 } alt = { pluginId } /> }
135+ < span className = "text-sm text-gray-700 dark:text-gray-300 flex-1" > { pluginId } </ span >
136+ </ div >
137+ < div className = "flex gap-2" >
138+ < a
139+ onClick = { ( ) => navigate ( `/plugin/${ pluginId } ?groupId=${ g . id } ` ) }
140+ className = "text-xs text-center w-full text-white bg-blue-600 hover:bg-blue-700 rounded px-2 py-1 cursor-pointer"
141+ >
142+ Settings
143+ </ a >
144+ < button
145+ onClick = { ( ) => removePluginFromGroup ( g . id , pluginId ) }
146+ className = "text-xs text-center w-full text-white bg-red-600 hover:bg-red-700 rounded px-2 py-1"
147+ >
148+ Remove
149+ </ button >
150+ </ div >
151+ </ div >
152+ ) ;
153+ } ) }
154+ </ div >
143155 ) }
144- < div className = "px-6 pb-3 text-center" >
145- < p className = "text-sm font-semibold text-gray-900 dark:text-white" > { g . name } </ p >
146- { g . channelName && (
147- < p className = "text-xs text-gray-500 dark:text-gray-400 mt-1" > { g . channelName } </ p >
156+
157+ { /* Add plugin button */ }
158+ < div className = "px-6 pb-4 my-auto pt-3" >
159+ { addPluginGroupId === g . id ? (
160+ < div className = "flex flex-col gap-2" >
161+ < select
162+ 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"
163+ defaultValue = ""
164+ onChange = { ( e ) => {
165+ if ( e . target . value ) {
166+ navigate ( `/plugin/${ e . target . value } ?groupId=${ g . id } ` ) ;
167+ }
168+ } }
169+ >
170+ < option value = "" disabled > Select plugin…</ option >
171+ { availablePlugins
172+ . filter ( ( p ) => ! g . installedPlugins ?. includes ( p . id ) )
173+ . map ( ( p ) => (
174+ < option key = { p . id } value = { p . id } > { p . id } </ option >
175+ ) ) }
176+ </ select >
177+ < button
178+ onClick = { ( ) => setAddPluginGroupId ( null ) }
179+ className = "text-xs text-gray-600 dark:text-gray-300 underline"
180+ >
181+ Cancel
182+ </ button >
183+ </ div >
184+ ) : (
185+ < div className = "flex flex-col gap-2 items-center" >
186+ < button
187+ onClick = { ( ) => setAddPluginGroupId ( g . id ) }
188+ disabled = { availablePlugins . filter ( ( p ) => ! g . installedPlugins ?. includes ( p . id ) ) . length === 0 }
189+ 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"
190+ >
191+ + Add Plugin
192+ </ button >
193+ < button
194+ onClick = { ( ) => deleteGroup ( g . id ) }
195+ 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"
196+ >
197+ Delete Group
198+ </ button >
199+ </ div >
148200 ) }
149201 </ div >
150- < div className = "flex justify-between items-center px-6 pb-6 mt-auto" >
151- < a
152- onClick = { ( ) => { navigate ( `/plugin/slack-notifications?groupId=${ g . id } ` ) ; } }
153- 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"
154- >
155- Settings
156- </ a >
157- < button
158- onClick = { ( ) => deleteGroup ( g . id ) }
159- 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"
160- >
161- Delete
162- </ button >
163- </ div >
164202 </ div >
165203 ) ) }
166204
167205 { /* Add group card */ }
168- < 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 ]" >
206+ < 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 ]" >
169207 { ! addGroupOpen ? (
170208 < button
171209 onClick = { ( ) => setAddGroupOpen ( true ) }
172- 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 "
210+ 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"
173211 >
174- + Add Slack Group
212+ + Add Group
175213 </ button >
176214 ) : (
177215 < div className = "p-4 w-full" >
@@ -182,6 +220,7 @@ const Plugins = () => {
182220 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"
183221 placeholder = "Group name"
184222 onKeyDown = { ( e ) => e . key === "Enter" && createGroup ( ) }
223+ autoFocus
185224 />
186225 < div className = "flex gap-2" >
187226 < button
@@ -203,47 +242,6 @@ const Plugins = () => {
203242 </ div >
204243 </ div >
205244 </ div >
206-
207- < div
208- id = "toast-copied"
209- 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"
210- role = "alert"
211- >
212- < 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" >
213- < svg
214- className = "w-5 h-5"
215- fill = "currentColor"
216- viewBox = "0 0 20 20"
217- xmlns = "http://www.w3.org/2000/svg"
218- >
219- < path
220- fillRule = "evenodd"
221- 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"
222- clipRule = "evenodd"
223- > </ path >
224- </ svg >
225- </ div >
226- < div className = "ml-3 text-sm font-normal" > Copied to clipboard!</ div >
227- < button
228- type = "button"
229- 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"
230- aria-label = "Close"
231- >
232- < span className = "sr-only" > Close</ span >
233- < svg
234- className = "w-5 h-5"
235- fill = "currentColor"
236- viewBox = "0 0 20 20"
237- xmlns = "http://www.w3.org/2000/svg"
238- >
239- < path
240- fillRule = "evenodd"
241- 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"
242- clipRule = "evenodd"
243- > </ path >
244- </ svg >
245- </ button >
246- </ div >
247245 </ div >
248246 ) ;
249247} ;
0 commit comments