@rancher/shell 3.0.10 → 3.0.12-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/assets/styles/base/_mixins.scss +31 -0
  2. package/assets/styles/base/_variables.scss +2 -0
  3. package/assets/styles/themes/_modern.scss +6 -5
  4. package/assets/translations/en-us.yaml +12 -9
  5. package/assets/translations/zh-hans.yaml +0 -3
  6. package/chart/__tests__/rancher-backup-index.test.ts +248 -0
  7. package/chart/rancher-backup/index.vue +41 -2
  8. package/components/BrandImage.vue +6 -5
  9. package/components/ConsumptionGauge.vue +12 -4
  10. package/components/DynamicContent/DynamicContentIcon.vue +3 -2
  11. package/components/EmptyProductPage.vue +76 -0
  12. package/components/ExplorerProjectsNamespaces.vue +1 -4
  13. package/components/LazyImage.vue +2 -1
  14. package/components/Resource/Detail/Card/Scaler.vue +4 -4
  15. package/components/Resource/Detail/CopyToClipboard.vue +1 -2
  16. package/components/Resource/Detail/Metadata/KeyValueRow.vue +9 -3
  17. package/components/Resource/Detail/TitleBar/__tests__/__snapshots__/index.test.ts.snap +31 -0
  18. package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +45 -1
  19. package/components/Resource/Detail/TitleBar/index.vue +1 -1
  20. package/components/Resource/Detail/ViewOptions/__tests__/__snapshots__/index.test.ts.snap +9 -0
  21. package/components/Resource/Detail/ViewOptions/__tests__/index.test.ts +62 -0
  22. package/components/Resource/Detail/ViewOptions/index.vue +2 -1
  23. package/components/ResourceList/Masthead.vue +25 -2
  24. package/components/SideNav.vue +13 -0
  25. package/components/Tabbed/index.vue +6 -0
  26. package/components/__tests__/ConsumptionGauge.test.ts +31 -0
  27. package/components/__tests__/PromptModal.test.ts +2 -0
  28. package/components/fleet/FleetClusters.vue +1 -0
  29. package/components/fleet/__tests__/FleetClusters.test.ts +71 -0
  30. package/components/form/NodeScheduling.vue +17 -3
  31. package/components/form/PrivateRegistry.vue +69 -0
  32. package/components/form/ProjectMemberEditor.vue +0 -10
  33. package/components/form/__tests__/PrivateRegistry.test.ts +133 -0
  34. package/components/formatter/WorkloadHealthScale.vue +3 -1
  35. package/components/nav/Group.vue +26 -3
  36. package/components/nav/Header.vue +32 -7
  37. package/components/nav/TopLevelMenu.helper.ts +7 -79
  38. package/components/nav/TopLevelMenu.vue +15 -1
  39. package/components/nav/__tests__/TopLevelMenu.helper.test.ts +2 -53
  40. package/config/pagination-table-headers.js +8 -1
  41. package/config/private-label.js +2 -1
  42. package/config/product/apps.js +3 -1
  43. package/config/product/auth.js +1 -0
  44. package/config/product/backup.js +1 -0
  45. package/config/product/compliance.js +1 -1
  46. package/config/product/explorer.js +25 -6
  47. package/config/product/fleet.js +1 -0
  48. package/config/product/gatekeeper.js +1 -0
  49. package/config/product/istio.js +1 -0
  50. package/config/product/logging.js +1 -0
  51. package/config/product/longhorn.js +2 -1
  52. package/config/product/manager.js +1 -0
  53. package/config/product/monitoring.js +1 -0
  54. package/config/product/navlinks.js +1 -0
  55. package/config/product/neuvector.js +2 -1
  56. package/config/product/settings.js +1 -0
  57. package/config/product/uiplugins.js +1 -0
  58. package/core/__tests__/extension-manager-impl.test.js +187 -2
  59. package/core/__tests__/plugin-products-helpers.test.ts +454 -0
  60. package/core/__tests__/plugin-products.test.ts +3219 -0
  61. package/core/extension-manager-impl.js +34 -3
  62. package/core/plugin-helpers.ts +31 -0
  63. package/core/plugin-products-base.ts +375 -0
  64. package/core/plugin-products-extending.ts +44 -0
  65. package/core/plugin-products-helpers.ts +262 -0
  66. package/core/plugin-products-top-level.ts +66 -0
  67. package/core/plugin-products-type-guards.ts +33 -0
  68. package/core/plugin-products.ts +50 -0
  69. package/core/plugin-types.ts +222 -0
  70. package/core/plugin.ts +45 -10
  71. package/core/productDebugger.js +48 -0
  72. package/core/types.ts +95 -11
  73. package/detail/__tests__/__snapshots__/fleet.cattle.io.bundle.test.ts.snap +52 -0
  74. package/detail/__tests__/fleet.cattle.io.bundle.test.ts +171 -0
  75. package/detail/__tests__/node.test.ts +83 -0
  76. package/detail/fleet.cattle.io.bundle.vue +21 -34
  77. package/detail/management.cattle.io.oidcclient.vue +2 -1
  78. package/detail/node.vue +1 -0
  79. package/dialog/ExtensionCatalogInstallDialog.vue +1 -1
  80. package/dialog/InstallExtensionDialog.vue +6 -27
  81. package/dialog/UninstallExistingExtensionDialog.vue +141 -0
  82. package/dialog/UninstallExtensionDialog.vue +4 -26
  83. package/dialog/__tests__/UninstallExistingExtensionDialog.test.ts +114 -0
  84. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +1 -0
  85. package/edit/catalog.cattle.io.clusterrepo.vue +17 -3
  86. package/edit/cloudcredential.vue +2 -1
  87. package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +11 -6
  88. package/edit/provisioning.cattle.io.cluster/__tests__/Ingress.test.ts +176 -0
  89. package/edit/provisioning.cattle.io.cluster/index.vue +5 -4
  90. package/edit/provisioning.cattle.io.cluster/rke2.vue +4 -1
  91. package/edit/provisioning.cattle.io.cluster/shared.ts +4 -2
  92. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +6 -0
  93. package/edit/provisioning.cattle.io.cluster/tabs/Ingress.vue +7 -2
  94. package/edit/secret/generic.vue +1 -0
  95. package/edit/secret/index.vue +2 -1
  96. package/edit/service.vue +2 -14
  97. package/list/management.cattle.io.feature.vue +7 -1
  98. package/list/provisioning.cattle.io.cluster.vue +0 -50
  99. package/list/workload.vue +11 -4
  100. package/mixins/brand.js +2 -1
  101. package/mixins/resource-fetch.js +12 -3
  102. package/models/catalog.cattle.io.clusterrepo.js +9 -0
  103. package/models/cluster.x-k8s.io.machinedeployment.js +8 -3
  104. package/models/management.cattle.io.authconfig.js +2 -1
  105. package/models/management.cattle.io.cluster.js +4 -3
  106. package/models/monitoring.coreos.com.receiver.js +11 -6
  107. package/models/pod.js +18 -0
  108. package/models/provisioning.cattle.io.cluster.js +2 -2
  109. package/models/workload.js +20 -2
  110. package/package.json +5 -6
  111. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +0 -1
  112. package/pages/c/_cluster/apps/charts/index.vue +3 -8
  113. package/pages/c/_cluster/apps/charts/install.vue +8 -9
  114. package/pages/c/_cluster/istio/index.vue +4 -2
  115. package/pages/c/_cluster/longhorn/index.vue +2 -1
  116. package/pages/c/_cluster/monitoring/index.vue +2 -2
  117. package/pages/c/_cluster/neuvector/index.vue +2 -1
  118. package/pages/c/_cluster/settings/brand.vue +4 -4
  119. package/pages/c/_cluster/settings/performance.vue +0 -5
  120. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +2 -1
  121. package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +231 -13
  122. package/pages/c/_cluster/uiplugins/index.vue +145 -38
  123. package/plugins/dashboard-store/__tests__/resource-class.test.ts +1 -0
  124. package/plugins/dashboard-store/actions.js +3 -2
  125. package/plugins/dashboard-store/resource-class.js +62 -6
  126. package/plugins/plugin.js +16 -0
  127. package/plugins/steve/steve-pagination-utils.ts +8 -2
  128. package/plugins/steve/subscribe.js +29 -4
  129. package/rancher-components/RcButton/RcButton.vue +3 -3
  130. package/rancher-components/RcButtonSplit/RcButtonSplit.test.ts +253 -0
  131. package/rancher-components/RcButtonSplit/RcButtonSplit.vue +158 -0
  132. package/rancher-components/RcButtonSplit/index.ts +1 -0
  133. package/scripts/test-plugins-build.sh +4 -4
  134. package/scripts/typegen.sh +13 -1
  135. package/store/__tests__/type-map.test.ts +84 -24
  136. package/store/type-map.js +42 -3
  137. package/tsconfig.paths.json +1 -0
  138. package/types/resources/pod.ts +18 -0
  139. package/types/shell/index.d.ts +8506 -2908
  140. package/types/store/dashboard-store.types.ts +5 -0
  141. package/types/store/pagination.types.ts +6 -0
  142. package/utils/__tests__/require-asset.test.ts +98 -0
  143. package/utils/async.ts +1 -5
  144. package/utils/axios.js +1 -4
  145. package/utils/brand.ts +3 -1
  146. package/utils/dynamic-importer.js +3 -2
  147. package/utils/favicon.js +4 -3
  148. package/utils/pagination-utils.ts +1 -1
  149. package/utils/require-asset.ts +95 -0
  150. package/utils/uiplugins.ts +12 -16
  151. package/utils/validators/__tests__/private-registry.test.ts +76 -0
  152. package/utils/validators/private-registry.ts +28 -0
  153. package/vue.config.js +4 -3
  154. package/components/HarvesterServiceAddOnConfig.vue +0 -207
@@ -5,12 +5,16 @@ import FleetUtils from '@shell/utils/fleet';
5
5
  import { checkSchemasForFindAllHash } from '@shell/utils/auth';
6
6
  import Loading from '@shell/components/Loading.vue';
7
7
  import { FLEET as FLEET_ANNOTATIONS } from '@shell/config/labels-annotations';
8
+ import ResourceTabs from '@shell/components/form/ResourceTabs';
9
+ import Tab from '@shell/components/Tabbed/Tab';
8
10
 
9
11
  export default {
10
12
  name: 'FleetBundleDetail',
11
13
 
12
- components: { Loading, FleetResources },
13
- props: {
14
+ components: {
15
+ Loading, FleetResources, ResourceTabs, Tab
16
+ },
17
+ props: {
14
18
  value: {
15
19
  type: Object,
16
20
  required: true,
@@ -82,42 +86,25 @@ export default {
82
86
  return res;
83
87
  }, []);
84
88
  },
85
- resourceCount() {
86
- return this.bundleResources.length;
87
- },
88
89
  }
89
90
  };
90
91
 
91
92
  </script>
92
93
 
93
94
  <template>
94
- <div>
95
- <div class="bundle-title mt-20 mb-20">
96
- <h2>{{ t('fleet.bundles.resources') }}</h2>
97
- <span>{{ resourceCount }}</span>
98
- </div>
99
- <Loading v-if="$fetchState.pending" />
100
- <FleetResources
101
- v-else
102
- :rows="bundleResources"
103
- />
104
- </div>
95
+ <Loading v-if="$fetchState.pending" />
96
+ <ResourceTabs
97
+ v-else
98
+ :value="value"
99
+ mode="view"
100
+ :need-related="false"
101
+ >
102
+ <Tab
103
+ label="Resources"
104
+ name="resources"
105
+ :weight="20"
106
+ >
107
+ <FleetResources :rows="bundleResources" />
108
+ </Tab>
109
+ </ResourceTabs>
105
110
  </template>
106
-
107
- <style lang="scss" scoped>
108
- .bundle-title {
109
- display: flex;
110
- align-items: center;
111
-
112
- h2 {
113
- margin: 0 10px 0 0;
114
- }
115
-
116
- span {
117
- background-color: var(--darker);
118
- color: var(--default);
119
- padding: 5px 10px;
120
- border-radius: 15px;
121
- }
122
- }
123
- </style>
@@ -9,6 +9,7 @@ import DateComponent from '@shell/components/formatter/Date.vue';
9
9
  import { RcItemCard } from '@components/RcItemCard';
10
10
  import ActionMenu, { type ActionMenuSelection } from '@shell/components/ActionMenuShell.vue';
11
11
  import { Banner } from '@components/Banner';
12
+ import keySvg from '~shell/assets/images/key.svg';
12
13
 
13
14
  type SecretActionType = 'create-secret' | 'regen-secret' | 'remove-secret'
14
15
  interface ClientSecretData { createdAt: string, lastUsedAt: string, lastFiveCharacters: string }
@@ -228,7 +229,7 @@ export default defineComponent({
228
229
  clientSecrets.push({
229
230
  id: oidcSecretDataKey,
230
231
  header: { title: { text: oidcSecretDataKey } },
231
- image: { src: require('~shell/assets/images/key.svg') },
232
+ image: { src: keySvg },
232
233
  createdAt,
233
234
  lastFiveCharacters: oidcSecretData.lastFiveCharacters,
234
235
  lastUsedAt,
package/detail/node.vue CHANGED
@@ -234,6 +234,7 @@ export default {
234
234
  :resource-name="t('node.detail.glance.consumptionGauge.pods')"
235
235
  :capacity="value.podCapacity"
236
236
  :used="value.podConsumed"
237
+ :used-label="t('node.detail.glance.consumptionGauge.running')"
237
238
  />
238
239
  </div>
239
240
  <div class="spacer" />
@@ -302,7 +302,7 @@ export default {
302
302
  });
303
303
 
304
304
  if (this.extensionSvc) {
305
- this.extensionUrl = `http://${ this.extensionSvc.spec.clusterIP }:${ this.extensionSvc.spec.ports[0].port }`;
305
+ this.extensionUrl = `http://${ this.extensionSvc.metadata.name }.${ this.extensionSvc.metadata.namespace }.svc:${ this.extensionSvc.spec.ports[0].port }`;
306
306
  } else {
307
307
  throw new Error('Error fetching extension service');
308
308
  }
@@ -223,7 +223,7 @@ export default {
223
223
 
224
224
  const plugin = this.plugin;
225
225
 
226
- this.updateStatus(plugin.name, this.action);
226
+ this.updateStatus(plugin.id, this.action);
227
227
 
228
228
  // Find the version that the user wants to install
229
229
  const version = plugin.versions?.find((v) => v.version === this.version);
@@ -370,31 +370,21 @@ export default {
370
370
  </template>
371
371
 
372
372
  <style lang="scss" scoped>
373
- .plugin-install-dialog {
374
- padding: 10px;
373
+ @import '@shell/assets/styles/base/_mixins.scss';
375
374
 
376
- h4 {
377
- font-weight: bold;
378
- }
375
+ .plugin-install-dialog {
376
+ @include extension-dialog;
379
377
 
380
378
  .dialog-panel {
381
- display: flex;
382
- flex-direction: column;
383
- min-height: 100px;
384
-
385
379
  p {
386
- margin-bottom: 5px;
387
- }
388
-
389
- .dialog-info {
390
- flex: 1;
380
+ margin-bottom: 4px;
391
381
  }
392
382
 
393
383
  .toggle-advanced {
394
384
  display: flex;
395
385
  align-items: center;
396
386
  cursor: pointer;
397
- margin: 10px 0;
387
+ margin: 8px 0;
398
388
 
399
389
  &:hover {
400
390
  text-decoration: none;
@@ -403,19 +393,8 @@ export default {
403
393
  }
404
394
 
405
395
  .version-selector {
406
- margin: 0 10px 10px 10px;
407
396
  width: auto;
408
397
  }
409
398
  }
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
- }
420
399
  }
421
400
  </style>
@@ -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>
@@ -65,7 +65,7 @@ export default {
65
65
 
66
66
  const plugin = this.plugin;
67
67
 
68
- this.updateStatus(plugin.name, 'uninstall');
68
+ this.updateStatus(plugin.id, 'uninstall');
69
69
 
70
70
  // Delete the CR if this is a developer plugin (there is no Helm App, so need to remove the CRD ourselves)
71
71
  if (plugin.uiplugin?.isDeveloper) {
@@ -132,31 +132,9 @@ export default {
132
132
  </template>
133
133
 
134
134
  <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';
141
136
 
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;
161
139
  }
162
140
  </style>
@@ -0,0 +1,114 @@
1
+ import { shallowMount, VueWrapper } from '@vue/test-utils';
2
+ import UninstallExistingExtensionDialog from '@shell/dialog/UninstallExistingExtensionDialog.vue';
3
+
4
+ const t = (key: string): string => key;
5
+
6
+ describe('component: UninstallExistingExtensionDialog', () => {
7
+ let wrapper: VueWrapper<any>;
8
+
9
+ const mountComponent = (propsData = {}) => {
10
+ const store = { dispatch: jest.fn().mockResolvedValue([]) };
11
+
12
+ const defaultProps = {
13
+ installedPlugin: {
14
+ id: 'test-plugin', name: 'test-plugin', label: 'Test Plugin'
15
+ },
16
+ updateStatus: jest.fn(),
17
+ closed: jest.fn(),
18
+ };
19
+
20
+ return shallowMount(UninstallExistingExtensionDialog, {
21
+ propsData: {
22
+ ...defaultProps,
23
+ ...propsData,
24
+ },
25
+ global: {
26
+ mocks: {
27
+ $store: store,
28
+ $router: { go: jest.fn() },
29
+ t,
30
+ },
31
+ }
32
+ });
33
+ };
34
+
35
+ describe('rendering', () => {
36
+ it('should render the dialog title', () => {
37
+ wrapper = mountComponent();
38
+
39
+ const title = wrapper.find('h4');
40
+
41
+ expect(title.text()).toBe('plugins.install.alreadyInstalledTitle');
42
+ });
43
+
44
+ it('should render the dialog prompt', () => {
45
+ wrapper = mountComponent();
46
+
47
+ const prompt = wrapper.find('.dialog-info p');
48
+
49
+ expect(prompt.text()).toBe('plugins.install.alreadyInstalledPrompt');
50
+ });
51
+
52
+ it('should render cancel button', () => {
53
+ wrapper = mountComponent();
54
+
55
+ const cancelBtn = wrapper.find('[data-testid="uninstall-existing-ext-modal-cancel-btn"]');
56
+
57
+ expect(cancelBtn.exists()).toBe(true);
58
+ });
59
+
60
+ it('should render uninstall button', () => {
61
+ wrapper = mountComponent();
62
+
63
+ const uninstallBtn = wrapper.find('[data-testid="uninstall-existing-ext-modal-uninstall-btn"]');
64
+
65
+ expect(uninstallBtn.exists()).toBe(true);
66
+ });
67
+ });
68
+
69
+ describe('closeDialog', () => {
70
+ it('should call closed callback and emit close event when cancel is clicked', async() => {
71
+ const closedFn = jest.fn();
72
+
73
+ wrapper = mountComponent({ closed: closedFn });
74
+
75
+ const cancelBtn = wrapper.find('[data-testid="uninstall-existing-ext-modal-cancel-btn"]');
76
+
77
+ await cancelBtn.trigger('click');
78
+
79
+ expect(closedFn).toHaveBeenCalledWith(false);
80
+ expect(wrapper.emitted('close')).toBeTruthy();
81
+ });
82
+ });
83
+
84
+ describe('uninstall', () => {
85
+ it('should call updateStatus with uninstall action', async() => {
86
+ const updateStatusFn = jest.fn();
87
+ const installedPlugin = {
88
+ id: 'test-plugin', name: 'test-plugin', label: 'Test Plugin'
89
+ };
90
+
91
+ wrapper = mountComponent({ installedPlugin, updateStatus: updateStatusFn });
92
+
93
+ await wrapper.vm.uninstall();
94
+
95
+ expect(updateStatusFn).toHaveBeenCalledWith('test-plugin', 'uninstall');
96
+ });
97
+
98
+ it('should remove developer plugin CR if isDeveloper is true', async() => {
99
+ const removeFn = jest.fn().mockResolvedValue(undefined);
100
+ const installedPlugin = {
101
+ id: 'test-plugin',
102
+ name: 'test-plugin',
103
+ label: 'Test Plugin',
104
+ uiplugin: { isDeveloper: true, remove: removeFn }
105
+ };
106
+
107
+ wrapper = mountComponent({ installedPlugin });
108
+
109
+ await wrapper.vm.uninstall();
110
+
111
+ expect(removeFn).toHaveBeenCalledWith();
112
+ });
113
+ });
114
+ });
@@ -70,6 +70,7 @@ const initGitRepo = (props: any, value?: any) => {
70
70
  }, {
71
71
  getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
72
72
  dispatch: jest.fn(),
73
+ rootState: { $extension: { getPlugins: () => ({}) } },
73
74
  rootGetters: { 'i18n/t': jest.fn() },
74
75
  });
75
76
 
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import CreateEditView from '@shell/mixins/create-edit-view';
3
+ import AsyncButton from '@shell/components/AsyncButton.vue';
3
4
  import Footer from '@shell/components/form/Footer';
4
5
  import { LabeledInput } from '@components/Form/LabeledInput';
5
6
  import NameNsDescription from '@shell/components/form/NameNsDescription';
@@ -16,6 +17,7 @@ import { getVersionData } from '@shell/config/version';
16
17
  import { RcItemCard } from '@components/RcItemCard';
17
18
  import { _CREATE, _EDIT, TARGET, _VIEW } from '@shell/config/query-params';
18
19
  import { RcIconType } from '@components/RcIcon/types';
20
+ import { requireAsset } from '@shell/utils/require-asset';
19
21
 
20
22
  export default {
21
23
  name: 'CruCatalogRepo',
@@ -23,6 +25,7 @@ export default {
23
25
  emits: ['input'],
24
26
 
25
27
  components: {
28
+ AsyncButton,
26
29
  Footer,
27
30
  LabeledInput,
28
31
  NameNsDescription,
@@ -64,7 +67,7 @@ export default {
64
67
  {
65
68
  id: CLUSTER_REPO_TYPES.OCI_URL,
66
69
  header: { title: { key: 'catalog.repo.target.oci.title' } },
67
- image: { src: require('@shell/assets/images/providers/oci-open-containers.svg'), alt: { key: 'catalog.repo.target.oci.title' } },
70
+ image: { src: requireAsset('@shell/assets/images/providers/oci-open-containers.svg'), alt: { key: 'catalog.repo.target.oci.title' } },
68
71
  content: { key: 'catalog.repo.target.oci.description' },
69
72
  },
70
73
  ];
@@ -74,7 +77,7 @@ export default {
74
77
  clusterRepoTargets.push({
75
78
  id: CLUSTER_REPO_TYPES.SUSE_APP_COLLECTION,
76
79
  header: { title: { key: 'catalog.repo.target.suseAppCollection.title' } },
77
- image: { src: require('@shell/assets/images/content/suse.svg'), alt: { key: 'catalog.repo.target.suseAppCollection.title' } },
80
+ image: { src: requireAsset('@shell/assets/images/content/suse.svg'), alt: { key: 'catalog.repo.target.suseAppCollection.title' } },
78
81
  content: { key: 'catalog.repo.target.suseAppCollection.description' },
79
82
  });
80
83
  }
@@ -90,6 +93,7 @@ export default {
90
93
  ociMaxRetries: this.value.spec.exponentialBackOffValues?.maxRetries,
91
94
  getVersionData,
92
95
  isView: this.mode === _VIEW,
96
+ isCreate: this.mode === _CREATE,
93
97
  clusterRepoTargets,
94
98
  previousName: '',
95
99
  previousDescription: '',
@@ -450,7 +454,17 @@ export default {
450
454
  :errors="errors"
451
455
  @save="save"
452
456
  @done="done"
453
- />
457
+ >
458
+ <template
459
+ v-if="isCreate"
460
+ #save
461
+ >
462
+ <AsyncButton
463
+ :action-label="t('catalog.repo.add')"
464
+ @click="save"
465
+ />
466
+ </template>
467
+ </Footer>
454
468
  </form>
455
469
  </template>
456
470
 
@@ -3,6 +3,7 @@ import { SECRET_TYPES as TYPES } from '@shell/config/secret';
3
3
  import { MANAGEMENT, NORMAN, SCHEMA, DEFAULT_WORKSPACE } from '@shell/config/types';
4
4
  import CreateEditView from '@shell/mixins/create-edit-view';
5
5
  import NameNsDescription from '@shell/components/form/NameNsDescription';
6
+ import { requireAsset } from '@shell/utils/require-asset';
6
7
  import CruResource from '@shell/components/CruResource';
7
8
  import { _CREATE, _EDIT } from '@shell/config/query-params';
8
9
  import Loading from '@shell/components/Loading';
@@ -177,7 +178,7 @@ export default {
177
178
 
178
179
  if (!bannerImage) {
179
180
  try {
180
- bannerImage = require(`~shell/assets/images/providers/${ id }.svg`);
181
+ bannerImage = requireAsset(`~shell/assets/images/providers/${ id }.svg`);
181
182
  } catch (e) {
182
183
  bannerImage = null;
183
184
  bannerAbbrv = this.initialDisplayFor(id);
@@ -13,6 +13,11 @@ import ButtonDropdown from '@shell/components/ButtonDropdown';
13
13
  import { _CREATE, _VIEW } from '@shell/config/query-params';
14
14
  import FormValidation from '@shell/mixins/form-validation';
15
15
  import { fetchAlertManagerConfigSpecs } from '@shell/utils/alertmanagerconfig';
16
+ import slackLogo from '@shell/assets/images/vendor/slack.svg';
17
+ import emailLogo from '@shell/assets/images/vendor/email.svg';
18
+ import pagerdutyLogo from '@shell/assets/images/vendor/pagerduty.svg';
19
+ import webhookLogo from '@shell/assets/images/vendor/webhook.svg';
20
+ import customLogo from '@shell/assets/images/vendor/custom.svg';
16
21
 
17
22
  // i18n-uses monitoringReceiver.slack.*, monitoringReceiver.email.*, monitoringReceiver.pagerduty.*
18
23
  // i18n-uses monitoringReceiver.opsgenie.*, monitoringReceiver.webhook.*, monitoringReceiver.custom.*
@@ -23,14 +28,14 @@ export const RECEIVERS_TYPES = [
23
28
  title: 'monitoringReceiver.slack.title',
24
29
  info: 'monitoringReceiver.slack.info',
25
30
  key: 'slackConfigs',
26
- logo: require(`@shell/assets/images/vendor/slack.svg`)
31
+ logo: slackLogo
27
32
  },
28
33
  {
29
34
  name: 'email',
30
35
  label: 'monitoringReceiver.email.label',
31
36
  title: 'monitoringReceiver.email.title',
32
37
  key: 'emailConfigs',
33
- logo: require(`@shell/assets/images/vendor/email.svg`)
38
+ logo: emailLogo
34
39
  },
35
40
  {
36
41
  name: 'pagerduty',
@@ -38,21 +43,21 @@ export const RECEIVERS_TYPES = [
38
43
  title: 'monitoringReceiver.pagerduty.title',
39
44
  info: 'monitoringReceiver.pagerduty.info',
40
45
  key: 'pagerdutyConfigs',
41
- logo: require(`@shell/assets/images/vendor/pagerduty.svg`)
46
+ logo: pagerdutyLogo
42
47
  },
43
48
  {
44
49
  name: 'opsgenie',
45
50
  label: 'monitoringReceiver.opsgenie.label',
46
51
  title: 'monitoringReceiver.opsgenie.title',
47
52
  key: 'opsgenieConfigs',
48
- logo: require(`@shell/assets/images/vendor/email.svg`)
53
+ logo: emailLogo
49
54
  },
50
55
  {
51
56
  name: 'webhook',
52
57
  label: 'monitoringReceiver.webhook.label',
53
58
  title: 'monitoringReceiver.webhook.title',
54
59
  key: 'webhookConfigs',
55
- logo: require(`@shell/assets/images/vendor/webhook.svg`),
60
+ logo: webhookLogo,
56
61
  },
57
62
  {
58
63
  name: 'custom',
@@ -60,7 +65,7 @@ export const RECEIVERS_TYPES = [
60
65
  title: 'monitoringReceiver.custom.title',
61
66
  info: 'monitoringReceiver.custom.info',
62
67
  key: 'webhookConfigs',
63
- logo: require(`@shell/assets/images/vendor/custom.svg`)
68
+ logo: customLogo
64
69
  },
65
70
  ];
66
71