Skip to content

Commit 006d1f8

Browse files
momesginMo Mesgin
andauthored
Handle multiple sources for one extension (#17031)
* display repo on extension cards * handle install/uninstall process for multiple extenstions with the same name * minor style + remove log * display repo name on installed card even after repo is removed * implement the final ux/ui * address feedback * remove unused code * update comments * add UninstallExistingExtensionDialog to PromptModal.test.ts * remove capitalize styling for the cards footer --------- Co-authored-by: Mo Mesgin <mmesgin@Mos-M2-MacBook-Pro.local>
1 parent 0d4e7d8 commit 006d1f8

10 files changed

Lines changed: 675 additions & 104 deletions

File tree

shell/assets/styles/base/_mixins.scss

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,34 @@
161161
// we need to use !important because it needs to superseed other classes that might impact outlines
162162
outline: 2px solid var(--primary-keyboard-focus);
163163
}
164+
165+
// -------------------------------------------------------------------------------------------------
166+
// Extension dialog styles
167+
168+
@mixin extension-dialog {
169+
padding: 8px 16px 16px 16px;
170+
171+
h4 {
172+
font-weight: bold;
173+
}
174+
175+
.dialog-panel {
176+
display: flex;
177+
flex-direction: column;
178+
min-height: 96px;
179+
180+
.dialog-info {
181+
flex: 1;
182+
}
183+
}
184+
185+
.dialog-buttons {
186+
display: flex;
187+
justify-content: flex-end;
188+
margin-top: 24px;
189+
190+
> *:not(:last-child) {
191+
margin-right: 8px;
192+
}
193+
}
194+
}

shell/assets/translations/en-us.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5578,6 +5578,9 @@ plugins:
55785578
prompt: "Are you sure that you want to install this extension?"
55795579
version: Version
55805580
warnNotCertified: Please ensure that you are aware of the risks of installing Extensions from untrusted authors
5581+
alreadyInstalledTitle: This extension is already installed from another source
5582+
alreadyInstalledPrompt: To install it from this source, you need to uninstall the existing version first and reload the page (required). Would you like to continue?
5583+
uninstallExisting: Uninstall existing version
55815584
upgrade:
55825585
label: Upgrade
55835586
title: Upgrade extension {name}

shell/components/__tests__/PromptModal.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import DeveloperLoadExtensionDialog from '@shell/dialog/DeveloperLoadExtensionDi
2424
import AddExtensionReposDialog from '@shell/dialog/AddExtensionReposDialog.vue';
2525
import InstallExtensionDialog from '@shell/dialog/InstallExtensionDialog.vue';
2626
import UninstallExtensionDialog from '@shell/dialog/UninstallExtensionDialog.vue';
27+
import UninstallExistingExtensionDialog from '@shell/dialog/UninstallExistingExtensionDialog.vue';
2728
import KnownHostsEditDialog from '@shell/dialog/KnownHostsEditDialog.vue';
2829
import ImportDialog from '@shell/dialog/ImportDialog.vue';
2930
import SearchDialog from '@shell/dialog/SearchDialog.vue';
@@ -110,6 +111,7 @@ describe('component: PromptModal', () => {
110111
['AddExtensionReposDialog', AddExtensionReposDialog],
111112
['InstallExtensionDialog', InstallExtensionDialog],
112113
['UninstallExtensionDialog', UninstallExtensionDialog],
114+
['UninstallExistingExtensionDialog', UninstallExistingExtensionDialog],
113115
['KnownHostsEditDialog', KnownHostsEditDialog],
114116
['ImportDialog', ImportDialog],
115117
['SearchDialog', SearchDialog],

shell/dialog/InstallExtensionDialog.vue

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ export default {
223223
224224
const plugin = this.plugin;
225225
226-
this.updateStatus(plugin.name, this.action);
226+
this.updateStatus(plugin.id, this.action);
227227
228228
// Find the version that the user wants to install
229229
const version = plugin.versions?.find((v) => v.version === this.version);
@@ -370,31 +370,21 @@ export default {
370370
</template>
371371
372372
<style lang="scss" scoped>
373-
.plugin-install-dialog {
374-
padding: 10px;
373+
@import '@shell/assets/styles/base/_mixins.scss';
375374
376-
h4 {
377-
font-weight: bold;
378-
}
375+
.plugin-install-dialog {
376+
@include extension-dialog;
379377
380378
.dialog-panel {
381-
display: flex;
382-
flex-direction: column;
383-
min-height: 100px;
384-
385379
p {
386-
margin-bottom: 5px;
387-
}
388-
389-
.dialog-info {
390-
flex: 1;
380+
margin-bottom: 4px;
391381
}
392382
393383
.toggle-advanced {
394384
display: flex;
395385
align-items: center;
396386
cursor: pointer;
397-
margin: 10px 0;
387+
margin: 8px 0;
398388
399389
&:hover {
400390
text-decoration: none;
@@ -403,19 +393,8 @@ export default {
403393
}
404394
405395
.version-selector {
406-
margin: 0 10px 10px 10px;
407396
width: auto;
408397
}
409398
}
410-
411-
.dialog-buttons {
412-
display: flex;
413-
justify-content: flex-end;
414-
margin-top: 10px;
415-
416-
> *:not(:last-child) {
417-
margin-right: 10px;
418-
}
419-
}
420399
}
421400
</style>
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
<script>
2+
import AsyncButton from '@shell/components/AsyncButton';
3+
import { CATALOG } from '@shell/config/types';
4+
import { UI_PLUGIN_NAMESPACE } from '@shell/config/uiplugins';
5+
6+
/**
7+
* Dialog shown when user tries to install an extension that is already installed from a different source.
8+
* Prompts the user to uninstall the existing version first before installing from the new source.
9+
*/
10+
export default {
11+
emits: ['close'],
12+
13+
components: { AsyncButton },
14+
15+
props: {
16+
/**
17+
* The installed plugin that needs to be uninstalled
18+
*/
19+
installedPlugin: {
20+
type: Object,
21+
default: () => {},
22+
required: true
23+
},
24+
/**
25+
* Callback to update install status on extensions main screen
26+
*/
27+
updateStatus: {
28+
type: Function,
29+
default: () => {},
30+
required: true
31+
},
32+
/**
33+
* Callback when modal is closed
34+
*/
35+
closed: {
36+
type: Function,
37+
default: () => {},
38+
required: true
39+
}
40+
},
41+
42+
data() {
43+
return { busy: false };
44+
},
45+
46+
methods: {
47+
closeDialog(result) {
48+
this.closed(result);
49+
this.$emit('close');
50+
},
51+
async uninstall() {
52+
this.busy = true;
53+
54+
const plugin = this.installedPlugin;
55+
56+
this.updateStatus(plugin.id, 'uninstall');
57+
58+
// Delete the CR if this is a developer plugin (there is no Helm App, so need to remove the CRD ourselves)
59+
if (plugin.uiplugin?.isDeveloper) {
60+
// Delete the custom resource
61+
await plugin.uiplugin.remove();
62+
}
63+
64+
// Find the app for this plugin using direct lookup (more efficient than findAll)
65+
let pluginApp = null;
66+
67+
try {
68+
const appId = `${ UI_PLUGIN_NAMESPACE }/${ plugin.name }`;
69+
70+
pluginApp = await this.$store.dispatch('management/find', {
71+
type: CATALOG.APP,
72+
id: appId
73+
});
74+
} catch (e) {
75+
// If the app cannot be found (e.g. already removed), proceed without error
76+
pluginApp = null;
77+
}
78+
79+
if (pluginApp) {
80+
try {
81+
await pluginApp.remove();
82+
} catch (e) {
83+
this.$store.dispatch('growl/error', {
84+
title: this.t('plugins.error.generic'),
85+
message: e.message ? e.message : e,
86+
timeout: 10000
87+
}, { root: true });
88+
89+
this.busy = false;
90+
91+
return;
92+
}
93+
94+
await this.$store.dispatch('management/findAll', { type: CATALOG.OPERATION });
95+
}
96+
97+
// Close the dialog
98+
this.closeDialog({ uninstalled: true, plugin });
99+
}
100+
}
101+
};
102+
</script>
103+
104+
<template>
105+
<div class="plugin-install-dialog">
106+
<h4 class="mt-10">
107+
{{ t('plugins.install.alreadyInstalledTitle') }}
108+
</h4>
109+
<div class="mt-10 dialog-panel">
110+
<div class="dialog-info">
111+
<p>
112+
{{ t('plugins.install.alreadyInstalledPrompt') }}
113+
</p>
114+
</div>
115+
<div class="dialog-buttons">
116+
<button
117+
:disabled="busy"
118+
class="btn role-secondary"
119+
data-testid="uninstall-existing-ext-modal-cancel-btn"
120+
@click="closeDialog(false)"
121+
>
122+
{{ t('generic.cancel') }}
123+
</button>
124+
<AsyncButton
125+
mode="uninstall"
126+
:action-label="t('plugins.install.uninstallExisting')"
127+
data-testid="uninstall-existing-ext-modal-uninstall-btn"
128+
@click="uninstall()"
129+
/>
130+
</div>
131+
</div>
132+
</div>
133+
</template>
134+
135+
<style lang="scss" scoped>
136+
@import '@shell/assets/styles/base/_mixins.scss';
137+
138+
.plugin-install-dialog {
139+
@include extension-dialog;
140+
}
141+
</style>

shell/dialog/UninstallExtensionDialog.vue

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export default {
6565
6666
const plugin = this.plugin;
6767
68-
this.updateStatus(plugin.name, 'uninstall');
68+
this.updateStatus(plugin.id, 'uninstall');
6969
7070
// Delete the CR if this is a developer plugin (there is no Helm App, so need to remove the CRD ourselves)
7171
if (plugin.uiplugin?.isDeveloper) {
@@ -132,31 +132,9 @@ export default {
132132
</template>
133133
134134
<style lang="scss" scoped>
135-
.plugin-install-dialog {
136-
padding: 10px;
137-
138-
h4 {
139-
font-weight: bold;
140-
}
135+
@import '@shell/assets/styles/base/_mixins.scss';
141136
142-
.dialog-panel {
143-
display: flex;
144-
flex-direction: column;
145-
min-height: 100px;
146-
147-
.dialog-info {
148-
flex: 1;
149-
}
150-
}
151-
152-
.dialog-buttons {
153-
display: flex;
154-
justify-content: flex-end;
155-
margin-top: 10px;
156-
157-
> *:not(:last-child) {
158-
margin-right: 10px;
159-
}
160-
}
137+
.plugin-install-dialog {
138+
@include extension-dialog;
161139
}
162140
</style>

0 commit comments

Comments
 (0)