11<script lang="ts" setup>
2- import { computed , ref } from ' vue' ;
2+ import { computed , reactive , ref } from ' vue' ;
33import { useI18n } from ' vue-i18n' ;
44
55import { BrandButton } from ' @unraid/ui' ;
@@ -46,19 +46,44 @@ const availablePlugins: Plugin[] = [
4646 },
4747];
4848
49+ type PluginStatus = ' pending' | ' installing' | ' success' | ' error' ;
50+
51+ type PluginState = {
52+ status: PluginStatus ;
53+ logs: string [];
54+ };
55+
56+ const pluginStates = reactive <Record <string , PluginState >>(
57+ Object .fromEntries (
58+ availablePlugins .map ((plugin ) => [
59+ plugin .id ,
60+ {
61+ status: ' pending' ,
62+ logs: [],
63+ },
64+ ])
65+ )
66+ );
67+
4968const selectedPlugins = ref <Set <string >>(new Set ());
5069const isInstalling = ref (false );
5170const error = ref <string | null >(null );
52- const installationLogs = ref <string []>([]);
5371const installationFinished = ref (false );
5472
5573const { installPlugin } = usePluginInstaller ();
5674
57- const appendLogs = (lines : string [] | string ) => {
75+ const combinedLogs = computed (() => {
76+ return availablePlugins .flatMap ((plugin ) =>
77+ pluginStates [plugin .id ].logs .map ((line ) => ` [${plugin .name }] ${line } ` )
78+ );
79+ });
80+
81+ const appendPluginLogs = (pluginId : string , lines : string [] | string ) => {
82+ const state = pluginStates [pluginId ];
5883 if (Array .isArray (lines )) {
59- lines . forEach (( line ) => installationLogs . value . push (line ) );
84+ state . logs . push (... lines );
6085 } else {
61- installationLogs . value .push (lines );
86+ state . logs .push (lines );
6287 }
6388};
6489
@@ -70,8 +95,14 @@ const togglePlugin = (pluginId: string) => {
7095 const next = new Set (selectedPlugins .value );
7196 if (next .has (pluginId )) {
7297 next .delete (pluginId );
98+ pluginStates [pluginId ].status = ' pending' ;
99+ pluginStates [pluginId ].logs = [];
73100 } else {
74101 next .add (pluginId );
102+ if (pluginStates [pluginId ].status !== ' pending' ) {
103+ pluginStates [pluginId ].status = ' pending' ;
104+ pluginStates [pluginId ].logs = [];
105+ }
75106 }
76107 selectedPlugins .value = next ;
77108 resetCompletionState ();
@@ -80,40 +111,62 @@ const togglePlugin = (pluginId: string) => {
80111const handleInstall = async () => {
81112 if (selectedPlugins .value .size === 0 ) {
82113 installationFinished .value = true ;
83- props .onComplete ();
84114 return ;
85115 }
86116
87117 isInstalling .value = true ;
88118 error .value = null ;
89- installationLogs .value = [];
90119 installationFinished .value = false ;
91120
92121 try {
93122 const pluginsToInstall = availablePlugins .filter ((p ) => selectedPlugins .value .has (p .id ));
94123
95124 for (const plugin of pluginsToInstall ) {
96- appendLogs (t (' activation.pluginsStep.installingPluginMessage' , { name: plugin .name }));
97-
98- const result = await installPlugin ({
99- url: plugin .url ,
100- name: plugin .name ,
101- forced: true ,
102- onEvent : (event ) => {
103- if (event .output ?.length ) {
104- appendLogs (event .output .map ((line ) => ` [${plugin .name }] ${line } ` ));
105- }
106- },
107- });
125+ const state = pluginStates [plugin .id ];
126+ state .status = ' installing' ;
127+ state .logs = [];
128+ appendPluginLogs (
129+ plugin .id ,
130+ t (' activation.pluginsStep.installingPluginMessage' , { name: plugin .name })
131+ );
132+
133+ let result;
134+ try {
135+ result = await installPlugin ({
136+ url: plugin .url ,
137+ name: plugin .name ,
138+ forced: true ,
139+ onEvent : (event ) => {
140+ if (event .output ?.length ) {
141+ appendPluginLogs (plugin .id , event .output );
142+ }
143+ },
144+ });
145+ } catch (installError ) {
146+ state .status = ' error' ;
147+ appendPluginLogs (plugin .id , t (' activation.pluginsStep.installFailed' ));
148+ throw installError ;
149+ }
108150
109151 if (result .status !== PluginInstallStatus .SUCCEEDED ) {
152+ state .status = ' error' ;
153+ appendPluginLogs (plugin .id , t (' activation.pluginsStep.installFailed' ));
110154 throw new Error (` Plugin installation failed for ${plugin .name } ` );
111155 }
112156
113- appendLogs (t (' activation.pluginsStep.pluginInstalledMessage' , { name: plugin .name }));
157+ if (result .output ?.length ) {
158+ appendPluginLogs (plugin .id , result .output );
159+ }
160+ appendPluginLogs (
161+ plugin .id ,
162+ t (' activation.pluginsStep.pluginInstalledMessage' , { name: plugin .name })
163+ );
164+ state .status = ' success' ;
114165 }
115166
116- installationFinished .value = true ;
167+ installationFinished .value = pluginsToInstall .every (
168+ (plugin ) => pluginStates [plugin .id ].status === ' success'
169+ );
117170 } catch (err ) {
118171 error .value = t (' activation.pluginsStep.installFailed' );
119172 console .error (' Failed to install plugins:' , err );
@@ -181,26 +234,53 @@ const isPrimaryActionDisabled = computed(() => {
181234 :for =" plugin.id"
182235 class =" border-border bg-card hover:bg-accent/50 flex cursor-pointer items-start gap-3 rounded-lg border p-4 transition-colors"
183236 >
184- <input
185- :id =" plugin.id"
186- type =" checkbox"
187- :checked =" selectedPlugins.has(plugin.id)"
188- :disabled =" isInstalling"
189- @change =" () => togglePlugin(plugin.id)"
190- class =" text-primary focus:ring-primary mt-1 h-5 w-5 cursor-pointer rounded border-gray-300 focus:ring-2"
191- />
237+ <div class =" mt-1 h-5 w-5" >
238+ <div
239+ v-if =" pluginStates[plugin.id].status === 'installing'"
240+ class =" border-primary h-5 w-5 animate-spin rounded-full border-2 border-t-transparent"
241+ />
242+ <input
243+ v-else
244+ :id =" plugin.id"
245+ type =" checkbox"
246+ :checked =" selectedPlugins.has(plugin.id)"
247+ :disabled =" isInstalling"
248+ @change =" () => togglePlugin(plugin.id)"
249+ class =" text-primary focus:ring-primary h-5 w-5 cursor-pointer rounded border-gray-300 focus:ring-2"
250+ />
251+ </div >
192252 <div class =" flex-1" >
193- <div class =" font-semibold" >{{ plugin.name }}</div >
253+ <div class =" flex items-center gap-2" >
254+ <div class =" font-semibold" >{{ plugin.name }}</div >
255+ <span
256+ v-if =" pluginStates[plugin.id].status === 'installing'"
257+ class =" text-primary flex items-center gap-1 text-xs"
258+ >
259+ <span
260+ class =" h-3 w-3 animate-spin rounded-full border border-current border-t-transparent"
261+ />
262+ {{ t('activation.pluginsStep.status.installing') }}
263+ </span >
264+ <span
265+ v-else-if =" pluginStates[plugin.id].status === 'success'"
266+ class =" text-xs text-green-600"
267+ >
268+ {{ t('activation.pluginsStep.status.success') }}
269+ </span >
270+ <span v-else-if =" pluginStates[plugin.id].status === 'error'" class =" text-xs text-red-500" >
271+ {{ t('activation.pluginsStep.status.error') }}
272+ </span >
273+ </div >
194274 <div class =" text-sm opacity-75" >{{ plugin.description }}</div >
195275 </div >
196276 </label >
197277 </div >
198278
199279 <div
200- v-if =" installationLogs .length > 0"
280+ v-if =" combinedLogs .length > 0"
201281 class =" border-border bg-muted/40 mb-4 max-h-48 w-full overflow-y-auto rounded border p-3 text-left font-mono text-xs"
202282 >
203- <div v-for =" (line, index) in installationLogs " :key =" `${index}-${line}`" >
283+ <div v-for =" (line, index) in combinedLogs " :key =" `${index}-${line}`" >
204284 {{ line }}
205285 </div >
206286 </div >
0 commit comments