@rancher/shell 0.3.24 → 0.3.25

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 (111) hide show
  1. package/assets/styles/themes/_light.scss +1 -1
  2. package/assets/translations/en-us.yaml +29 -7
  3. package/assets/translations/zh-hans.yaml +1 -1
  4. package/components/ClusterIconMenu.vue +143 -0
  5. package/components/CruResource.vue +7 -1
  6. package/components/ExplorerProjectsNamespaces.vue +11 -1
  7. package/components/FixedBanner.vue +17 -1
  8. package/components/Markdown.vue +1 -1
  9. package/components/Questions/__tests__/Yaml.test.ts +3 -2
  10. package/components/SortableTable/index.vue +3 -2
  11. package/components/auth/RoleDetailEdit.vue +15 -2
  12. package/components/auth/login/saml.vue +12 -1
  13. package/components/form/LabeledSelect.vue +12 -5
  14. package/components/form/Members/ClusterPermissionsEditor.vue +1 -1
  15. package/components/form/Members/MembershipEditor.vue +6 -1
  16. package/components/form/__tests__/KeyValue.test.ts +6 -3
  17. package/components/form/__tests__/LabeledSelect.test.ts +18 -0
  18. package/components/formatter/PodsUsage.vue +11 -36
  19. package/components/formatter/PrincipalGroupBindings.vue +8 -5
  20. package/components/formatter/__tests__/PodsUsage.test.ts +36 -19
  21. package/components/nav/Group.vue +25 -27
  22. package/components/nav/Header.vue +12 -5
  23. package/components/nav/Pinned.vue +47 -0
  24. package/components/nav/TopLevelMenu.vue +233 -60
  25. package/components/nav/Type.vue +57 -3
  26. package/config/home-links.js +1 -1
  27. package/config/product/istio.js +15 -5
  28. package/config/router.js +3 -9
  29. package/config/table-headers.js +5 -6
  30. package/config/uiplugins.js +1 -0
  31. package/core/plugin-helpers.js +3 -0
  32. package/core/types.ts +6 -1
  33. package/creators/app/files/.vscode/settings.json +0 -1
  34. package/detail/__tests__/autoscaling.horizontalpodautoscaler.test.ts +118 -0
  35. package/detail/autoscaling.horizontalpodautoscaler/index.vue +4 -4
  36. package/detail/provisioning.cattle.io.cluster.vue +7 -5
  37. package/edit/__tests__/management.cattle.io.clusterroletemplatebinding.test.ts +58 -0
  38. package/edit/__tests__/namespace.test.ts +5 -3
  39. package/edit/management.cattle.io.clusterroletemplatebinding.vue +3 -11
  40. package/edit/namespace.vue +8 -4
  41. package/edit/provisioning.cattle.io.cluster/Basics.vue +662 -0
  42. package/edit/provisioning.cattle.io.cluster/CustomCommand.vue +6 -0
  43. package/edit/provisioning.cattle.io.cluster/DrainOptions.vue +13 -8
  44. package/edit/provisioning.cattle.io.cluster/MachinePool.vue +11 -2
  45. package/edit/provisioning.cattle.io.cluster/MemberRoles.vue +40 -0
  46. package/edit/provisioning.cattle.io.cluster/__tests__/Basics.tests.ts +237 -0
  47. package/edit/provisioning.cattle.io.cluster/__tests__/CustomCommand.tests.ts +71 -23
  48. package/edit/provisioning.cattle.io.cluster/__tests__/DrainOptions.test.ts +52 -0
  49. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +65 -142
  50. package/edit/provisioning.cattle.io.cluster/rke2.vue +194 -598
  51. package/edit/workload/storage/__tests__/Storage.test.ts +2 -2
  52. package/edit/workload/storage/persistentVolumeClaim/__tests__/persistentvolumeclaim.test.ts +36 -0
  53. package/edit/workload/storage/persistentVolumeClaim/persistentvolumeclaim.vue +15 -7
  54. package/initialize/index.js +5 -5
  55. package/layouts/default.vue +6 -6
  56. package/layouts/home.vue +6 -2
  57. package/layouts/plain.vue +9 -2
  58. package/list/fleet.cattle.io.cluster.vue +2 -2
  59. package/list/management.cattle.io.feature.vue +1 -1
  60. package/machine-config/vmwarevsphere.vue +48 -7
  61. package/mixins/brand.js +0 -8
  62. package/mixins/child-hook.js +2 -2
  63. package/mixins/create-edit-view/impl.js +3 -3
  64. package/models/__tests__/management.cattle.io.node.ts +96 -0
  65. package/models/__tests__/node.ts +74 -0
  66. package/models/cluster/node.js +6 -5
  67. package/models/cluster.x-k8s.io.machinedeployment.js +2 -2
  68. package/models/management.cattle.io.cluster.js +22 -1
  69. package/models/management.cattle.io.clusterroletemplatebinding.js +3 -3
  70. package/models/management.cattle.io.globalrole.js +17 -2
  71. package/models/management.cattle.io.node.js +6 -4
  72. package/models/management.cattle.io.projectroletemplatebinding.js +3 -3
  73. package/models/management.cattle.io.roletemplate.js +17 -2
  74. package/package.json +2 -6
  75. package/pages/about.vue +2 -0
  76. package/pages/auth/setup.vue +5 -4
  77. package/pages/c/_cluster/monitoring/index.vue +8 -3
  78. package/pages/c/_cluster/uiplugins/CatalogList/CatalogLoadDialog.vue +9 -66
  79. package/pages/c/_cluster/uiplugins/CatalogList/CatalogUninstallDialog.vue +182 -0
  80. package/pages/c/_cluster/uiplugins/CatalogList/index.vue +15 -32
  81. package/pages/c/_cluster/uiplugins/UninstallDialog.vue +8 -46
  82. package/pages/c/_cluster/uiplugins/index.vue +64 -64
  83. package/pages/diagnostic.vue +0 -39
  84. package/pages/home.vue +1 -1
  85. package/plugins/dashboard-store/normalize.js +4 -4
  86. package/plugins/int-number.js +5 -2
  87. package/plugins/positive-int-number.js +19 -0
  88. package/plugins/steve/__tests__/getters.spec.ts +15 -0
  89. package/plugins/steve/getters.js +22 -10
  90. package/rancher-components/Form/LabeledInput/LabeledInput.vue +0 -8
  91. package/rancher-components/Form/Radio/RadioButton.test.ts +3 -7
  92. package/store/index.js +4 -0
  93. package/store/prefs.js +1 -0
  94. package/types/shell/index.d.ts +13 -4
  95. package/utils/__tests__/cluster.test.ts +55 -0
  96. package/utils/__tests__/object.test.ts +21 -2
  97. package/utils/cluster.js +47 -1
  98. package/utils/object.js +12 -5
  99. package/utils/validators/formRules/__tests__/index.test.ts +13 -1
  100. package/utils/validators/formRules/index.ts +4 -0
  101. package/utils/validators/role-template.js +9 -1
  102. package/utils/version.js +1 -1
  103. package/yarn-error.log +16 -16
  104. package/components/ClusterProviderIconMenu.vue +0 -161
  105. package/content/docs/en-us/getting-started.md +0 -224
  106. package/content/docs/en-us/whats-new.md +0 -29
  107. package/content/docs/zh-hans/getting-started.md +0 -224
  108. package/content/docs/zh-hans/whats-new.md +0 -28
  109. package/pages/docs/_doc.vue +0 -345
  110. package/pages/docs/toc.js +0 -27
  111. package/plugins/console.js +0 -34
@@ -1,7 +1,7 @@
1
1
  import Vue from 'vue';
2
2
  import { CATALOG, CLUSTER_BADGE } from '@shell/config/labels-annotations';
3
3
  import { NODE, FLEET, MANAGEMENT, CAPI } from '@shell/config/types';
4
- import { insertAt } from '@shell/utils/array';
4
+ import { insertAt, addObject, removeObject } from '@shell/utils/array';
5
5
  import { downloadFile } from '@shell/utils/download';
6
6
  import { parseSi } from '@shell/utils/units';
7
7
  import { parseColor, textColor } from '@shell/utils/color';
@@ -14,6 +14,7 @@ import { isHarvesterCluster } from '@shell/utils/cluster';
14
14
  import HybridModel from '@shell/plugins/steve/hybrid-class';
15
15
  import { LINUX, WINDOWS } from '@shell/store/catalog';
16
16
  import { KONTAINER_TO_DRIVER } from './management.cattle.io.kontainerdriver';
17
+ import { PINNED_CLUSTERS } from '@shell/store/prefs';
17
18
 
18
19
  // See translation file cluster.providers for list of providers
19
20
  // If the logo is not named with the provider name, add an override here
@@ -455,4 +456,24 @@ export default class MgmtCluster extends HybridModel {
455
456
 
456
457
  return findRelationship(verb === 'to' ? 'from' : 'to', CAPI.RANCHER_CLUSTER, this.metadata?.relationships);
457
458
  }
459
+
460
+ get pinned() {
461
+ return this.$rootGetters['prefs/get'](PINNED_CLUSTERS).includes(this.id);
462
+ }
463
+
464
+ pin() {
465
+ const types = this.$rootGetters['prefs/get'](PINNED_CLUSTERS) || [];
466
+
467
+ addObject(types, this.id);
468
+
469
+ this.$dispatch('prefs/set', { key: PINNED_CLUSTERS, value: types }, { root: true });
470
+ }
471
+
472
+ unpin() {
473
+ const types = this.$rootGetters['prefs/get'](PINNED_CLUSTERS) || [];
474
+
475
+ removeObject(types, this.id);
476
+
477
+ this.$dispatch('prefs/set', { key: PINNED_CLUSTERS, value: types }, { root: true });
478
+ }
458
479
  }
@@ -43,7 +43,7 @@ export default class CRTB extends HybridModel {
43
43
 
44
44
  get principalId() {
45
45
  // We've either set it ourselves or it's comes from native properties
46
- return this.principalName || this.userPrincipalName || this.groupPrincipalName;
46
+ return this.principalName || this.userPrincipalName || this.groupPrincipalName || '';
47
47
  }
48
48
 
49
49
  get nameDisplay() {
@@ -117,12 +117,12 @@ export default class CRTB extends HybridModel {
117
117
  get norman() {
118
118
  return (async() => {
119
119
  const principal = await this.principal;
120
- const principalProperty = principal.principalType === 'group' ? 'groupPrincipalId' : 'userPrincipalId';
120
+ const principalProperty = principal?.principalType === 'group' ? 'groupPrincipalId' : 'userPrincipalId';
121
121
 
122
122
  return this.$dispatch(`rancher/create`, {
123
123
  type: NORMAN.CLUSTER_ROLE_TEMPLATE_BINDING,
124
124
  roleTemplateId: this.roleTemplateName,
125
- [principalProperty]: principal.id,
125
+ [principalProperty]: principal?.id,
126
126
  clusterId: this.clusterName,
127
127
  id: this.id?.replace('/', ':')
128
128
  }, { root: true });
@@ -4,7 +4,6 @@ import { CATTLE_API_GROUP, SUBTYPE_MAPPING, CREATE_VERBS } from '@shell/models/m
4
4
  import { uniq } from '@shell/utils/array';
5
5
  import { get } from '@shell/utils/object';
6
6
  import SteveDescriptionModel from '@shell/plugins/steve/steve-description-class';
7
- import Role from './rbac.authorization.k8s.io.role';
8
7
  import { AS, MODE, _CLONE, _UNFLAG } from '@shell/config/query-params';
9
8
 
10
9
  const BASE = 'user-base';
@@ -16,7 +15,14 @@ const GLOBAL = SUBTYPE_MAPPING.GLOBAL.key;
16
15
 
17
16
  export default class GlobalRole extends SteveDescriptionModel {
18
17
  get customValidationRules() {
19
- return Role.customValidationRules();
18
+ return [
19
+ {
20
+ path: 'rules',
21
+ validators: [`roleTemplateRules:${ this.type }`],
22
+ nullable: false,
23
+ type: 'array',
24
+ },
25
+ ];
20
26
  }
21
27
 
22
28
  get details() {
@@ -137,6 +143,15 @@ export default class GlobalRole extends SteveDescriptionModel {
137
143
  async save() {
138
144
  const norman = await this.norman;
139
145
 
146
+ for (const rule of norman.rules) {
147
+ if (rule.nonResourceURLs.length) {
148
+ delete rule.resources;
149
+ delete rule.apiGroups;
150
+ } else {
151
+ delete rule.nonResourceURLs;
152
+ }
153
+ }
154
+
140
155
  return norman.save();
141
156
  }
142
157
 
@@ -125,11 +125,14 @@ export default class MgmtNode extends HybridModel {
125
125
  return false;
126
126
  }
127
127
 
128
+ get addresses() {
129
+ return this.status?.addresses || this.status?.internalNodeStatus?.addresses || [];
130
+ }
131
+
128
132
  get internalIp() {
129
133
  // This shows in the IP address column for RKE1 nodes in the
130
134
  // list of nodes in the cluster detail page of Cluster Management.
131
-
132
- const internal = this.status?.addresses?.find(({ type }) => {
135
+ const internal = this.addresses.find(({ type }) => {
133
136
  return type === ADDRESSES.INTERNAL_IP;
134
137
  });
135
138
 
@@ -147,8 +150,7 @@ export default class MgmtNode extends HybridModel {
147
150
  }
148
151
 
149
152
  get externalIp() {
150
- const addresses = this.status?.addresses || [];
151
- const statusAddress = findLast(addresses, (address) => address.type === 'ExternalIP')?.address;
153
+ const statusAddress = findLast(this.addresses, (address) => address.type === 'ExternalIP')?.address;
152
154
 
153
155
  if (statusAddress) {
154
156
  return statusAddress;
@@ -31,7 +31,7 @@ export default class PRTB extends HybridModel {
31
31
 
32
32
  get principalId() {
33
33
  // We've either set it ourselves or it's comes from native properties
34
- return this.principalName || this.userPrincipalName || this.groupPrincipalName;
34
+ return this.principalName || this.userPrincipalName || this.groupPrincipalName || '';
35
35
  }
36
36
 
37
37
  get nameDisplay() {
@@ -123,12 +123,12 @@ export default class PRTB extends HybridModel {
123
123
  get norman() {
124
124
  return (async() => {
125
125
  const principal = await this.principal;
126
- const principalProperty = principal.principalType === 'group' ? 'groupPrincipalId' : 'userPrincipalId';
126
+ const principalProperty = principal?.principalType === 'group' ? 'groupPrincipalId' : 'userPrincipalId';
127
127
 
128
128
  return this.$dispatch(`rancher/create`, {
129
129
  type: NORMAN.PROJECT_ROLE_TEMPLATE_BINDING,
130
130
  roleTemplateId: this.roleTemplateName,
131
- [principalProperty]: principal.id,
131
+ [principalProperty]: principal?.id,
132
132
  projectId: this.projectName,
133
133
  projectRoleTemplateId: '',
134
134
  id: this.id?.replace('/', ':')
@@ -3,7 +3,6 @@ import { get } from '@shell/utils/object';
3
3
  import { DESCRIPTION } from '@shell/config/labels-annotations';
4
4
  import { NORMAN } from '@shell/config/types';
5
5
  import SteveDescriptionModel from '@shell/plugins/steve/steve-description-class';
6
- import Role from './rbac.authorization.k8s.io.role';
7
6
  import { AS, MODE, _CLONE, _UNFLAG } from '@shell/config/query-params';
8
7
 
9
8
  export const CATTLE_API_GROUP = '.cattle.io';
@@ -60,7 +59,14 @@ export const CREATE_VERBS = new Set(['PUT', 'blocked-PUT']);
60
59
 
61
60
  export default class RoleTemplate extends SteveDescriptionModel {
62
61
  get customValidationRules() {
63
- return Role.customValidationRules();
62
+ return [
63
+ {
64
+ path: 'rules',
65
+ validators: [`roleTemplateRules:${ this.type }`],
66
+ nullable: false,
67
+ type: 'array',
68
+ },
69
+ ];
64
70
  }
65
71
 
66
72
  get details() {
@@ -185,6 +191,15 @@ export default class RoleTemplate extends SteveDescriptionModel {
185
191
  async save() {
186
192
  const norman = await this.norman;
187
193
 
194
+ for (const rule of norman.rules) {
195
+ if (rule.nonResourceURLs.length) {
196
+ delete rule.resources;
197
+ delete rule.apiGroups;
198
+ } else {
199
+ delete rule.nonResourceURLs;
200
+ }
201
+ }
202
+
188
203
  return norman.save();
189
204
  }
190
205
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rancher/shell",
3
- "version": "0.3.24",
3
+ "version": "0.3.25",
4
4
  "description": "Rancher Dashboard Shell",
5
5
  "repository": "https://github.com/rancherlabs/dashboard",
6
6
  "license": "Apache-2.0",
@@ -17,7 +17,6 @@
17
17
  "lint": "./node_modules/.bin/eslint --max-warnings 0 --ext .ts,.js,.vue .",
18
18
  "test": "./node_modules/.bin/nyc ava --serial --verbose",
19
19
  "dev": "./node_modules/.bin/vue-cli-service dev",
20
- "docker-dev": "docker run --rm --name dashboard-dev -p 8005:8005 -e API=$API -v $(pwd):/src -v dashboard_node:/src/node_modules rancher/dashboard:dev",
21
20
  "build": "./node_modules/.bin/vue-cli-service build",
22
21
  "analyze": "./node_modules/.bin/vue-cli-service build --report",
23
22
  "start": "./node_modules/.bin/vue-cli-service start",
@@ -38,7 +37,7 @@
38
37
  "@novnc/novnc": "1.2.0",
39
38
  "@nuxt/types": "2.14.6",
40
39
  "@nuxt/typescript-build": "2.1.0",
41
- "@nuxtjs/axios": "5.12.0",
40
+ "@nuxtjs/axios": "5.13.6",
42
41
  "@nuxtjs/eslint-config-typescript": "6.0.1",
43
42
  "@nuxtjs/webpack-profile": "0.1.0",
44
43
  "@popperjs/core": "2.4.4",
@@ -107,9 +106,6 @@
107
106
  "papaparse": "5.3.0",
108
107
  "portal-vue": "2.1.7",
109
108
  "rancher-icons": "rancher/icons#v2.0.16",
110
- "require-extension-hooks": "0.3.3",
111
- "require-extension-hooks-babel": "1.0.0",
112
- "require-extension-hooks-vue": "3.0.0",
113
109
  "sass": "1.51.0",
114
110
  "sass-loader": "10.2.1",
115
111
  "serve-static": "1.14.1",
package/pages/about.vue CHANGED
@@ -101,6 +101,7 @@ export default {
101
101
  <n-link
102
102
  :to="{ name: 'diagnostic' }"
103
103
  class="btn role-primary"
104
+ data-testid="about__diagnostics_button"
104
105
  >
105
106
  {{ t('about.diagnostic.title') }}
106
107
  </n-link>
@@ -195,6 +196,7 @@ export default {
195
196
  <td>
196
197
  <a
197
198
  v-if="d.imageList"
199
+ :data-testid="`image_list_download_link__${d.label}`"
198
200
  @click="d.imageList"
199
201
  >
200
202
  {{ t('asyncButton.download.action') }}
@@ -17,6 +17,7 @@ import { exceptionToErrorsArray } from '@shell/utils/error';
17
17
  import Password from '@shell/components/form/Password';
18
18
  import { applyProducts } from '@shell/store/type-map';
19
19
  import BrandImage from '@shell/components/BrandImage';
20
+ import { waitFor } from '@shell/utils/async';
20
21
 
21
22
  const calcIsFirstLogin = (store) => {
22
23
  const firstLoginSetting = store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.FIRST_LOGIN);
@@ -250,10 +251,10 @@ export default {
250
251
 
251
252
  await Promise.all(promises);
252
253
 
253
- setTimeout(() => {
254
- buttonCb(true);
255
- this.done();
256
- }, 2000);
254
+ await waitFor(() => !calcIsFirstLogin(this.$store), 'first login to be completed', 10000, 1000, true);
255
+
256
+ buttonCb(true);
257
+ this.done();
257
258
  } catch (err) {
258
259
  console.error(err) ; // eslint-disable-line no-console
259
260
  buttonCb(false);
@@ -94,12 +94,11 @@ export default {
94
94
  methods: {
95
95
  async fetchDeps() {
96
96
  const { $store, externalLinks } = this;
97
- const currentCluster = this.$store.getters['currentCluster'];
98
97
 
99
98
  this.v1Installed = await haveV1MonitoringWorkloads($store);
100
99
  const hash = await allHash({
100
+ apps: $store.dispatch('cluster/findAll', { type: CATALOG.APP }),
101
101
  endpoints: $store.dispatch('cluster/findAll', { type: ENDPOINTS }),
102
- app: $store.dispatch(`cluster/find`, { type: CATALOG.APP, id: 'cattle-monitoring-system/rancher-monitoring' })
103
102
  });
104
103
 
105
104
  if (!isEmpty(hash.endpoints)) {
@@ -109,7 +108,13 @@ export default {
109
108
  (el) => el.group === 'prometheus'
110
109
  );
111
110
 
112
- grafanaMatch.link = `${ getClusterPrefix(hash.app?.currentVersion || '', currentCluster.id) }/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/`;
111
+ // Generate Grafana link
112
+ const currentCluster = this.$store.getters['currentCluster'];
113
+ const rancherMonitoring = !isEmpty(hash.apps) ? findBy(hash.apps, 'id', 'cattle-monitoring-system/rancher-monitoring') : '';
114
+ const clusterPrefix = getClusterPrefix(rancherMonitoring?.currentVersion || '', currentCluster.id);
115
+
116
+ grafanaMatch.link = `${ clusterPrefix }/api/v1/namespaces/cattle-monitoring-system/services/http:rancher-monitoring-grafana:80/proxy/`;
117
+
113
118
  const alertmanager = findBy(
114
119
  hash.endpoints,
115
120
  'id',
@@ -2,9 +2,7 @@
2
2
  import { mapGetters } from 'vuex';
3
3
  import isEmpty from 'lodash/isEmpty';
4
4
 
5
- import {
6
- CATALOG, SECRET, SERVICE, UI_PLUGIN, WORKLOAD_TYPES
7
- } from '@shell/config/types';
5
+ import { CATALOG, SECRET, SERVICE, WORKLOAD_TYPES } from '@shell/config/types';
8
6
  import { UI_PLUGIN_LABELS, UI_PLUGIN_NAMESPACE } from '@shell/config/uiplugins';
9
7
  import { TYPES as SECRET_TYPES } from '@shell/models/secret';
10
8
  import { allHash } from '@shell/utils/promise';
@@ -195,11 +193,15 @@ export default {
195
193
  }
196
194
 
197
195
  if (this.extensionRepo) {
198
- // Create uiplugin crd
199
- await this.loadPlugin(name, this.extensionUrl, image);
196
+ btnCb(true);
197
+ this.closeDialog();
198
+ this.$store.dispatch('growl/success', {
199
+ title: this.t('plugins.manageCatalog.imageLoad.success.title', { name }),
200
+ message: this.t('plugins.manageCatalog.imageLoad.success.message'),
201
+ timeout: 4000,
202
+ }, { root: true });
203
+ this.$emit('refresh');
200
204
  }
201
-
202
- btnCb(true);
203
205
  } else {
204
206
  throw new Error('Unable to determine image name');
205
207
  }
@@ -313,62 +315,6 @@ export default {
313
315
  }
314
316
  },
315
317
 
316
- async loadPlugin(name, url, image, btnCb) {
317
- // Try and parse version number from the image
318
- const version = this.extractImageVersion(image) || 'latest';
319
-
320
- if (!this.extractImageVersion(image)) {
321
- this.$store.dispatch('growl/warning', {
322
- title: this.t('plugins.manageCatalog.imageLoad.imageVersion.title'),
323
- message: this.t('plugins.manageCatalog.imageLoad.imageVersion.message', { image }),
324
- timeout: 4000,
325
- }, { root: true });
326
- }
327
-
328
- let crdName = name;
329
-
330
- const parts = name.split('-');
331
-
332
- if (parts.length >= 2) {
333
- crdName = parts.join('-');
334
- }
335
-
336
- this.extensionCrd = await this.$store.dispatch('management/create', {
337
- type: UI_PLUGIN,
338
- metadata: {
339
- name,
340
- namespace: UI_PLUGIN_NAMESPACE,
341
- labels: {
342
- [UI_PLUGIN_LABELS.CATALOG_IMAGE]: name,
343
- [UI_PLUGIN_LABELS.REPOSITORY]: this.extensionRepo.metadata.name
344
- }
345
- },
346
- spec: {
347
- plugin: {
348
- name: crdName,
349
- version,
350
- endpoint: url,
351
- noCache: false,
352
- metadata: { [UI_PLUGIN_LABELS.CATALOG]: 'true' }
353
- }
354
- }
355
- });
356
-
357
- try {
358
- await this.extensionCrd.save({ url: `/v1/${ UI_PLUGIN }`, method: 'POST' });
359
-
360
- this.closeDialog();
361
- this.$store.dispatch('growl/success', {
362
- title: this.t('plugins.manageCatalog.imageLoad.success.title', { name }),
363
- message: this.t('plugins.manageCatalog.imageLoad.success.message'),
364
- timeout: 4000,
365
- }, { root: true });
366
- } catch (e) {
367
- this.handleGrowlError(e, true);
368
- btnCb(false);
369
- }
370
- },
371
-
372
318
  parseDeploymentValues(name) {
373
319
  let out = {};
374
320
 
@@ -459,9 +405,6 @@ export default {
459
405
  if (this.extensionRepo) {
460
406
  this.extensionRepo.remove();
461
407
  }
462
- if (this.extensionCrd) {
463
- this.extensionCrd.remove();
464
- }
465
408
  },
466
409
 
467
410
  handleGrowlError(e, clean = false) {
@@ -0,0 +1,182 @@
1
+ <script>
2
+ import { mapGetters } from 'vuex';
3
+
4
+ import { CATALOG, UI_PLUGIN, SERVICE, WORKLOAD_TYPES } from '@shell/config/types';
5
+ import { UI_PLUGIN_LABELS, UI_PLUGIN_NAMESPACE } from '@shell/config/uiplugins';
6
+ import { allHash } from '@shell/utils/promise';
7
+
8
+ import AsyncButton from '@shell/components/AsyncButton';
9
+
10
+ export default {
11
+ components: { AsyncButton },
12
+
13
+ async fetch() {
14
+ if ( this.$store.getters['management/schemaFor'](UI_PLUGIN) ) {
15
+ const plugins = this.$store.dispatch('management/findAll', { type: UI_PLUGIN });
16
+
17
+ this.plugins = plugins || [];
18
+ }
19
+ },
20
+
21
+ data() {
22
+ return {
23
+ catalog: undefined, busy: false, plugins: null
24
+ };
25
+ },
26
+
27
+ computed: {
28
+ ...mapGetters({ allCharts: 'catalog/charts' }),
29
+
30
+ pluginsFromCatalogImage() {
31
+ return this.plugins.filter((p) => p.metadata?.labels?.[UI_PLUGIN_LABELS.CATALOG_IMAGE]);
32
+ }
33
+ },
34
+
35
+ methods: {
36
+ showDialog(catalog) {
37
+ this.catalog = catalog;
38
+ this.busy = false;
39
+ this.$modal.show('uninstallCatalogDialog');
40
+ },
41
+
42
+ closeDialog(result) {
43
+ this.$modal.hide('uninstallCatalogDialog');
44
+ this.$emit('closed', result);
45
+
46
+ if ( result ) {
47
+ this.$emit('refresh');
48
+ }
49
+ },
50
+
51
+ async uninstall() {
52
+ this.busy = true;
53
+
54
+ const catalog = this.catalog;
55
+ const apps = await this.$store.dispatch('management/findAll', { type: CATALOG.APP });
56
+ const pluginApps = apps.filter((app) => {
57
+ if ( app.namespace === UI_PLUGIN_NAMESPACE ) {
58
+ // Find the related apps from the deployed helm repository
59
+ const charts = this.allCharts.filter((chart) => chart.repoName === catalog.repo?.metadata?.name);
60
+
61
+ return charts.some((chart) => chart.chartName === app.metadata.name);
62
+ }
63
+
64
+ return false;
65
+ });
66
+
67
+ await this.removeCatalogResources(catalog);
68
+
69
+ if ( pluginApps.length ) {
70
+ try {
71
+ pluginApps.forEach((app) => {
72
+ this.$emit('update', app.name, 'uninstall');
73
+ app.remove();
74
+ });
75
+ } catch (e) {
76
+ this.$store.dispatch('growl/error', {
77
+ title: this.t('plugins.error.generic'),
78
+ message: e.message ? e.message : e,
79
+ timeout: 10000
80
+ }, { root: true });
81
+ }
82
+
83
+ await this.$store.dispatch('management/findAll', { type: CATALOG.OPERATION });
84
+ }
85
+
86
+ this.closeDialog(catalog);
87
+ },
88
+
89
+ async removeCatalogResources(catalog) {
90
+ const selector = `${ UI_PLUGIN_LABELS.CATALOG_IMAGE }=${ catalog.name }`;
91
+ const namespace = UI_PLUGIN_NAMESPACE;
92
+
93
+ if ( selector ) {
94
+ const hash = await allHash({
95
+ deployment: this.$store.dispatch('management/findMatching', {
96
+ type: WORKLOAD_TYPES.DEPLOYMENT, selector, namespace
97
+ }),
98
+ service: this.$store.dispatch('management/findMatching', {
99
+ type: SERVICE, selector, namespace
100
+ }),
101
+ repo: this.$store.dispatch('management/findMatching', { type: CATALOG.CLUSTER_REPO, selector })
102
+ });
103
+
104
+ for ( const resource of Object.keys(hash) ) {
105
+ if ( hash[resource] ) {
106
+ hash[resource].forEach((r) => r.remove());
107
+ }
108
+ }
109
+ }
110
+ }
111
+ }
112
+ };
113
+ </script>
114
+
115
+ <template>
116
+ <modal
117
+ name="uninstallCatalogDialog"
118
+ height="auto"
119
+ :scrollable="true"
120
+ >
121
+ <div
122
+ v-if="catalog"
123
+ class="plugin-install-dialog"
124
+ >
125
+ <h4 class="mt-10">
126
+ {{ t('plugins.uninstall.title', { name: catalog.name }) }}
127
+ </h4>
128
+ <div class="mt-10 dialog-panel">
129
+ <div class="dialog-info">
130
+ <p>
131
+ {{ t('plugins.uninstall.catalog') }}
132
+ </p>
133
+ </div>
134
+ <div class="dialog-buttons">
135
+ <button
136
+ :disabled="busy"
137
+ class="btn role-secondary"
138
+ data-testid="uninstall-ext-modal-cancel-btn"
139
+ @click="closeDialog(false)"
140
+ >
141
+ {{ t('generic.cancel') }}
142
+ </button>
143
+ <AsyncButton
144
+ mode="uninstall"
145
+ data-testid="uninstall-ext-modal-uninstall-btn"
146
+ @click="uninstall()"
147
+ />
148
+ </div>
149
+ </div>
150
+ </div>
151
+ </modal>
152
+ </template>
153
+
154
+ <style lang="scss" scoped>
155
+ .plugin-install-dialog {
156
+ padding: 10px;
157
+
158
+ h4 {
159
+ font-weight: bold;
160
+ }
161
+
162
+ .dialog-panel {
163
+ display: flex;
164
+ flex-direction: column;
165
+ min-height: 100px;
166
+
167
+ .dialog-info {
168
+ flex: 1;
169
+ }
170
+ }
171
+
172
+ .dialog-buttons {
173
+ display: flex;
174
+ justify-content: flex-end;
175
+ margin-top: 10px;
176
+
177
+ > *:not(:last-child) {
178
+ margin-right: 10px;
179
+ }
180
+ }
181
+ }
182
+ </style>
@@ -13,13 +13,6 @@ import ResourceTable from '@shell/components/ResourceTable';
13
13
  export default {
14
14
  name: 'CatalogList',
15
15
 
16
- props: {
17
- plugins: {
18
- type: Array,
19
- required: true
20
- }
21
- },
22
-
23
16
  components: { ActionMenu, ResourceTable },
24
17
 
25
18
  mixins: [ResourceManager],
@@ -27,7 +20,7 @@ export default {
27
20
  data() {
28
21
  const actions = [
29
22
  {
30
- action: 'uninstall',
23
+ action: 'showCatalogUninstallDialog',
31
24
  label: this.t('plugins.uninstall.label'),
32
25
  icon: 'icon icon-trash',
33
26
  enabled: true,
@@ -57,36 +50,26 @@ export default {
57
50
  catalogRows() {
58
51
  const rows = [];
59
52
 
60
- if (this.plugins.length) {
61
- // Find the resources associated with the image by the CATALOG_IMAGE label
62
- this.plugins.forEach((plugin) => {
63
- const resources = [this.namespacedDeployments, this.namespacedServices, this.allRepos];
64
- const pluginName = plugin.metadata?.labels?.[UI_PLUGIN_LABELS.CATALOG_IMAGE];
53
+ if ( !isEmpty(this.namespacedDeployments) ) {
54
+ this.namespacedDeployments.forEach((deploy) => {
55
+ const resources = [this.namespacedServices, this.allRepos];
56
+ const deployName = deploy.metadata?.labels?.[UI_PLUGIN_LABELS.CATALOG_IMAGE];
65
57
 
66
- if (pluginName) {
58
+ if ( deployName ) {
67
59
  const out = {
68
- uiplugin: plugin,
69
- catalog: true,
70
- name: pluginName,
71
- state: plugin.metadata?.state?.name,
72
- cacheState: plugin.status?.cacheState,
73
- version: plugin.spec?.plugin?.version,
74
- deployment: null,
75
- deploymentImage: null,
76
- service: null,
77
- repo: null
60
+ name: deployName,
61
+ state: deploy.metadata?.state?.name,
62
+ image: deploy.spec?.template?.spec?.containers[0]?.image,
63
+ service: null,
64
+ repo: null
78
65
  };
79
- const keys = ['deployment', 'service', 'repo'];
66
+ const keys = ['service', 'repo'];
80
67
 
81
68
  resources.forEach((resource, i) => {
82
- out[keys[i]] = resource?.filter((item) => item.metadata?.labels?.[UI_PLUGIN_LABELS.CATALOG_IMAGE] === pluginName)[0];
69
+ out[keys[i]] = resource?.filter((item) => item.metadata?.labels?.[UI_PLUGIN_LABELS.CATALOG_IMAGE] === deployName)[0];
83
70
  });
84
71
 
85
- if (!isEmpty(out?.deployment)) {
86
- out.deploymentImage = out.deployment.spec?.template?.spec?.containers[0]?.image;
87
-
88
- rows.push(out);
89
- }
72
+ rows.push(out);
90
73
  }
91
74
  });
92
75
  }
@@ -153,7 +136,7 @@ export default {
153
136
  :custom-target-element="menuTargetElement"
154
137
  :custom-target-event="menuTargetEvent"
155
138
  @close="setMenu(false)"
156
- @uninstall="e => $emit('showUninstallDialog', row, e.event)"
139
+ @showCatalogUninstallDialog="e => $emit('showCatalogUninstallDialog', row, e.event)"
157
140
  />
158
141
  </template>
159
142
  </ResourceTable>