@rancher/shell 3.0.11 → 3.0.12-rc.2

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 (219) hide show
  1. package/assets/images/providers/entraid-black.svg +4 -0
  2. package/assets/images/providers/entraid.svg +9 -0
  3. package/assets/images/vendor/entraid.svg +9 -0
  4. package/assets/styles/app.scss +0 -1
  5. package/assets/styles/base/_mixins.scss +31 -0
  6. package/assets/styles/base/_variables.scss +2 -0
  7. package/assets/styles/themes/_modern.scss +6 -5
  8. package/assets/translations/en-us.yaml +24 -21
  9. package/assets/translations/zh-hans.yaml +4 -11
  10. package/chart/__tests__/S3.test.ts +10 -3
  11. package/components/CountBox.vue +20 -0
  12. package/components/CreateDriver.vue +0 -12
  13. package/components/DetailText.vue +12 -3
  14. package/components/EmptyProductPage.vue +76 -0
  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/SelectIconGrid.vue +5 -0
  25. package/components/SideNav.vue +13 -0
  26. package/components/__tests__/CountBox.test.ts +72 -0
  27. package/components/__tests__/DetailText.test.ts +113 -0
  28. package/components/__tests__/PromptModal.test.ts +2 -0
  29. package/components/fleet/FleetClusterTargets/index.vue +18 -1
  30. package/components/fleet/FleetClusters.vue +1 -0
  31. package/components/fleet/__tests__/FleetClusters.test.ts +71 -0
  32. package/components/form/InputWithSelect.vue +18 -10
  33. package/components/form/KeyValue.vue +17 -1
  34. package/components/form/LabeledSelect.vue +82 -24
  35. package/components/form/NodeScheduling.vue +17 -3
  36. package/components/form/PrivateRegistry.vue +69 -0
  37. package/components/form/Select.vue +73 -56
  38. package/components/form/ServiceNameSelect.vue +13 -11
  39. package/components/form/__tests__/KeyValue.test.ts +66 -0
  40. package/components/form/__tests__/NodeScheduling.test.ts +9 -0
  41. package/components/form/__tests__/PrivateRegistry.test.ts +133 -0
  42. package/components/form/labeled-select-utils/useLabeledSelectPagination.ts +138 -0
  43. package/components/formatter/WorkloadHealthScale.vue +3 -1
  44. package/components/nav/Group.vue +33 -9
  45. package/components/nav/Header.vue +56 -10
  46. package/components/nav/NotificationCenter/Notification.vue +4 -1
  47. package/components/nav/NotificationCenter/NotificationHeader.vue +20 -8
  48. package/components/nav/NotificationCenter/__tests__/NotificationHeader.test.ts +80 -0
  49. package/components/nav/TopLevelMenu.vue +15 -1
  50. package/components/nav/Type.vue +8 -7
  51. package/components/nav/WindowManager/index.vue +2 -1
  52. package/components/nav/WorkspaceSwitcher.vue +13 -0
  53. package/components/nav/__tests__/Group.test.ts +67 -0
  54. package/components/nav/__tests__/Header.test.ts +235 -0
  55. package/components/nav/__tests__/Type.test.ts +20 -3
  56. package/components/templates/default.vue +34 -4
  57. package/components/templates/home.vue +12 -25
  58. package/components/templates/plain.vue +13 -26
  59. package/composables/useLabeledFormElement.ts +10 -2
  60. package/composables/useLabeledSelect.ts +60 -0
  61. package/composables/useUserRetentionValidation.ts +1 -49
  62. package/config/cookies.js +0 -1
  63. package/config/labels-annotations.js +1 -0
  64. package/config/pagination-table-headers.js +8 -1
  65. package/config/product/apps.js +2 -1
  66. package/config/product/auth.js +1 -0
  67. package/config/product/backup.js +1 -0
  68. package/config/product/compliance.js +1 -1
  69. package/config/product/explorer.js +25 -6
  70. package/config/product/fleet.js +1 -0
  71. package/config/product/gatekeeper.js +1 -0
  72. package/config/product/istio.js +1 -0
  73. package/config/product/logging.js +1 -0
  74. package/config/product/longhorn.js +2 -1
  75. package/config/product/manager.js +1 -0
  76. package/config/product/monitoring.js +1 -0
  77. package/config/product/navlinks.js +1 -0
  78. package/config/product/neuvector.js +2 -1
  79. package/config/product/settings.js +1 -0
  80. package/config/product/uiplugins.js +1 -0
  81. package/config/query-params.js +1 -0
  82. package/config/router/routes.js +0 -8
  83. package/core/__tests__/plugin-products-helpers.test.ts +454 -0
  84. package/core/__tests__/plugin-products.test.ts +3810 -0
  85. package/core/extension-manager-impl.js +30 -1
  86. package/core/plugin-products-base.ts +392 -0
  87. package/core/plugin-products-extending.ts +44 -0
  88. package/core/plugin-products-helpers.ts +263 -0
  89. package/core/plugin-products-top-level.ts +66 -0
  90. package/core/plugin-products-type-guards.ts +33 -0
  91. package/core/plugin-products.ts +50 -0
  92. package/core/plugin-types.ts +237 -0
  93. package/core/plugin.ts +45 -10
  94. package/core/productDebugger.js +48 -0
  95. package/core/types.ts +97 -11
  96. package/detail/__tests__/__snapshots__/fleet.cattle.io.bundle.test.ts.snap +52 -0
  97. package/detail/__tests__/fleet.cattle.io.bundle.test.ts +171 -0
  98. package/detail/__tests__/management.cattle.io.fleetworkspace.test.ts +128 -0
  99. package/detail/fleet.cattle.io.bundle.vue +21 -34
  100. package/detail/management.cattle.io.fleetworkspace.vue +49 -0
  101. package/dialog/ExtensionCatalogInstallDialog.vue +1 -1
  102. package/dialog/InstallExtensionDialog.vue +6 -27
  103. package/dialog/UninstallExistingExtensionDialog.vue +141 -0
  104. package/dialog/UninstallExtensionDialog.vue +4 -26
  105. package/dialog/__tests__/UninstallExistingExtensionDialog.test.ts +114 -0
  106. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +1 -0
  107. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +9 -0
  108. package/edit/__tests__/kontainerDriver.test.ts +0 -13
  109. package/edit/__tests__/nodeDriver.test.ts +5 -11
  110. package/edit/__tests__/resources.cattle.io.restore.test.ts +9 -0
  111. package/edit/auditlog.cattle.io.auditpolicy/__tests__/__snapshots__/General.test.ts.snap +6 -0
  112. package/edit/auth/__tests__/oidc.test.ts +54 -0
  113. package/edit/auth/azuread.vue +1 -1
  114. package/edit/auth/oidc.vue +8 -0
  115. package/edit/kontainerDriver.vue +1 -2
  116. package/edit/nodeDriver.vue +0 -2
  117. package/edit/provisioning.cattle.io.cluster/AgentEnv.vue +1 -0
  118. package/edit/provisioning.cattle.io.cluster/__tests__/AgentEnv.test.ts +25 -0
  119. package/edit/provisioning.cattle.io.cluster/__tests__/Ingress.test.ts +176 -0
  120. package/edit/provisioning.cattle.io.cluster/index.vue +70 -99
  121. package/edit/provisioning.cattle.io.cluster/rke2.vue +4 -1
  122. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +6 -0
  123. package/edit/provisioning.cattle.io.cluster/tabs/Ingress.vue +7 -2
  124. package/initialize/App.vue +29 -2
  125. package/initialize/install-plugins.js +0 -2
  126. package/list/__tests__/management.cattle.io.feature.test.ts +105 -0
  127. package/list/catalog.cattle.io.app.vue +25 -5
  128. package/list/management.cattle.io.feature.vue +1 -1
  129. package/list/management.cattle.io.fleetworkspace.vue +8 -0
  130. package/list/provisioning.cattle.io.cluster.vue +0 -1
  131. package/list/workload.vue +11 -4
  132. package/machine-config/amazonec2.vue +1 -0
  133. package/mixins/chart.js +40 -9
  134. package/mixins/resource-fetch.js +12 -3
  135. package/models/__tests__/catalog.cattle.io.app.test.ts +15 -1
  136. package/models/__tests__/catalog.cattle.io.clusterrepo.test.ts +84 -0
  137. package/models/__tests__/chart.test.ts +99 -6
  138. package/models/__tests__/management.cattle.io.feature.test.ts +131 -0
  139. package/models/__tests__/monitoring.coreos.com.alertmanagerconfig.test.ts +98 -0
  140. package/models/catalog.cattle.io.app.js +21 -17
  141. package/models/catalog.cattle.io.clusterrepo.js +39 -11
  142. package/models/chart.js +33 -19
  143. package/models/fleet-application.js +1 -1
  144. package/models/fleet.cattle.io.bundle.js +1 -1
  145. package/models/kontainerdriver.js +11 -0
  146. package/models/management.cattle.io.authconfig.js +5 -1
  147. package/models/management.cattle.io.cluster.js +0 -53
  148. package/models/management.cattle.io.feature.js +3 -3
  149. package/models/management.cattle.io.kontainerdriver.js +1 -26
  150. package/models/monitoring.coreos.com.alertmanagerconfig.js +31 -17
  151. package/models/nodedriver.js +7 -0
  152. package/models/pod.js +18 -0
  153. package/models/workload.js +20 -2
  154. package/package.json +13 -13
  155. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +0 -1
  156. package/pages/c/_cluster/apps/charts/__tests__/chart.test.ts +189 -0
  157. package/pages/c/_cluster/apps/charts/__tests__/index.test.ts +55 -0
  158. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +53 -0
  159. package/pages/c/_cluster/apps/charts/chart.vue +217 -33
  160. package/pages/c/_cluster/apps/charts/index.vue +2 -2
  161. package/pages/c/_cluster/apps/charts/install.vue +8 -3
  162. package/pages/c/_cluster/auth/user.retention/index.vue +55 -22
  163. package/pages/c/_cluster/manager/drivers/kontainerDriver/index.vue +5 -7
  164. package/pages/c/_cluster/settings/brand.vue +4 -4
  165. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +39 -2
  166. package/pages/c/_cluster/uiplugins/__tests__/PluginInfoPanel.test.ts +61 -0
  167. package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +246 -23
  168. package/pages/c/_cluster/uiplugins/index.vue +166 -62
  169. package/plugins/dashboard-store/__tests__/resource-class.test.ts +1 -0
  170. package/plugins/dashboard-store/actions.js +3 -2
  171. package/plugins/dashboard-store/resource-class.js +62 -6
  172. package/plugins/plugin.js +16 -0
  173. package/plugins/steve/steve-pagination-utils.ts +7 -0
  174. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +205 -1
  175. package/rancher-components/Form/LabeledInput/LabeledInput.vue +82 -4
  176. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +1 -1
  177. package/scripts/test-plugins-build.sh +5 -2
  178. package/scripts/typegen.sh +13 -1
  179. package/server/server-middleware.js +2 -2
  180. package/static/humans.txt +1 -0
  181. package/static/robots.txt +34 -0
  182. package/static/welcome-cow.svg +18 -0
  183. package/store/__tests__/catalog.test.ts +161 -11
  184. package/store/__tests__/type-map.test.ts +84 -24
  185. package/store/auth.js +0 -3
  186. package/store/catalog.js +60 -8
  187. package/store/type-map.js +42 -3
  188. package/tsconfig.paths.json +1 -0
  189. package/types/resources/pod.ts +18 -0
  190. package/types/shell/index.d.ts +8539 -2938
  191. package/types/store/dashboard-store.types.ts +5 -0
  192. package/types/store/pagination.types.ts +6 -0
  193. package/utils/__tests__/git.test.ts +270 -0
  194. package/utils/__tests__/inactivity.test.ts +316 -0
  195. package/utils/__tests__/object.test.ts +77 -0
  196. package/utils/__tests__/time.test.ts +14 -1
  197. package/utils/__tests__/url.test.ts +246 -0
  198. package/utils/axios.js +1 -4
  199. package/utils/dynamic-importer.js +3 -2
  200. package/utils/object.js +33 -2
  201. package/utils/pagination-utils.ts +1 -1
  202. package/utils/time.ts +5 -0
  203. package/utils/uiplugins.ts +12 -16
  204. package/utils/validators/__tests__/private-registry.test.ts +76 -0
  205. package/utils/validators/private-registry.ts +28 -0
  206. package/vue.config.js +0 -9
  207. package/assets/images/providers/azuread-black.svg +0 -22
  208. package/assets/images/providers/azuread.svg +0 -25
  209. package/assets/images/vendor/azuread.svg +0 -18
  210. package/assets/styles/fonts/_dots.scss +0 -18
  211. package/components/EmberPage.vue +0 -622
  212. package/components/EmberPageView.vue +0 -39
  213. package/components/form/labeled-select-utils/labeled-select-pagination.ts +0 -116
  214. package/mixins/labeled-form-element.ts +0 -225
  215. package/pages/c/_cluster/explorer/tools/pages/_page.vue +0 -28
  216. package/pages/c/_cluster/manager/pages/_page.vue +0 -22
  217. package/pages/c/_cluster/mcapps/pages/_page.vue +0 -22
  218. package/plugins/ember-cookie.js +0 -17
  219. package/utils/ember-page.js +0 -30
@@ -0,0 +1,128 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import DetailWorkspace from '@shell/detail/management.cattle.io.fleetworkspace.vue';
3
+ import { FLEET } from '@shell/config/types';
4
+ import { NAME as FLEET_NAME } from '@shell/config/product/fleet';
5
+ import { BLANK_CLUSTER } from '@shell/store/store-types.js';
6
+
7
+ describe('component: DetailWorkspace', () => {
8
+ const mockValue = {
9
+ id: 'fleet-default',
10
+ counts: {
11
+ gitRepos: 3,
12
+ helmOps: 2,
13
+ clusters: 5,
14
+ cluster: 5,
15
+ clusterGroup: 1,
16
+ clusterGroups: 1,
17
+ },
18
+ };
19
+
20
+ const mockRouter = { push: jest.fn() };
21
+
22
+ const defaultStore = {
23
+ commit: jest.fn(),
24
+ dispatch: jest.fn(),
25
+ getters: {
26
+ 'i18n/t': (key: string) => key,
27
+ 'i18n/exists': () => true,
28
+ currentProduct: { name: FLEET_NAME },
29
+ },
30
+ };
31
+
32
+ const createWrapper = (props = {}) => {
33
+ return shallowMount(DetailWorkspace, {
34
+ props: { value: mockValue, ...props },
35
+ global: {
36
+ mocks: {
37
+ $store: defaultStore,
38
+ $route: { params: {} },
39
+ $router: mockRouter,
40
+ },
41
+ stubs: {
42
+ CountBox: { template: '<div />', props: ['clickable', 'count', 'name', 'primaryColorVar'] },
43
+ ResourceTabs: { template: '<div />' },
44
+ },
45
+ },
46
+ });
47
+ };
48
+
49
+ beforeEach(() => {
50
+ jest.clearAllMocks();
51
+ });
52
+
53
+ describe('applicationRoute', () => {
54
+ it('should return the fleet application route', () => {
55
+ const wrapper = createWrapper();
56
+
57
+ expect(wrapper.vm.applicationRoute).toStrictEqual({
58
+ name: 'c-cluster-fleet-application',
59
+ params: { cluster: BLANK_CLUSTER },
60
+ });
61
+ });
62
+ });
63
+
64
+ describe('clustersRoute', () => {
65
+ it('should return the fleet clusters list route', () => {
66
+ const wrapper = createWrapper();
67
+
68
+ expect(wrapper.vm.clustersRoute).toStrictEqual({
69
+ name: 'c-cluster-product-resource',
70
+ params: {
71
+ cluster: BLANK_CLUSTER,
72
+ product: FLEET_NAME,
73
+ resource: FLEET.CLUSTER,
74
+ },
75
+ });
76
+ });
77
+ });
78
+
79
+ describe('clusterGroupsRoute', () => {
80
+ it('should return the fleet cluster groups list route', () => {
81
+ const wrapper = createWrapper();
82
+
83
+ expect(wrapper.vm.clusterGroupsRoute).toStrictEqual({
84
+ name: 'c-cluster-product-resource',
85
+ params: {
86
+ cluster: BLANK_CLUSTER,
87
+ product: FLEET_NAME,
88
+ resource: FLEET.CLUSTER_GROUP,
89
+ },
90
+ });
91
+ });
92
+ });
93
+
94
+ describe('setWorkspaceAndNavigate', () => {
95
+ it('should commit updateWorkspace with the workspace id', () => {
96
+ const wrapper = createWrapper();
97
+ const route = wrapper.vm.applicationRoute;
98
+
99
+ wrapper.vm.setWorkspaceAndNavigate(route);
100
+
101
+ expect(defaultStore.commit).toHaveBeenCalledWith('updateWorkspace', {
102
+ value: 'fleet-default',
103
+ getters: defaultStore.getters,
104
+ });
105
+ });
106
+
107
+ it('should dispatch prefs/set with the workspace id', () => {
108
+ const wrapper = createWrapper();
109
+ const route = wrapper.vm.applicationRoute;
110
+
111
+ wrapper.vm.setWorkspaceAndNavigate(route);
112
+
113
+ expect(defaultStore.dispatch).toHaveBeenCalledWith('prefs/set', {
114
+ key: expect.any(String),
115
+ value: 'fleet-default',
116
+ });
117
+ });
118
+
119
+ it('should navigate to the given route', () => {
120
+ const wrapper = createWrapper();
121
+ const route = wrapper.vm.clustersRoute;
122
+
123
+ wrapper.vm.setWorkspaceAndNavigate(route);
124
+
125
+ expect(mockRouter.push).toHaveBeenCalledWith(route);
126
+ });
127
+ });
128
+ });
@@ -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>
@@ -4,6 +4,8 @@ import ResourceTabs from '@shell/components/form/ResourceTabs';
4
4
  import { SCOPE_NAMESPACE, SCOPE_CLUSTER } from '@shell/components/RoleBindings.vue';
5
5
  import { NAME as FLEET_NAME } from '@shell/config/product/fleet';
6
6
  import { FLEET } from '@shell/config/types';
7
+ import { BLANK_CLUSTER } from '@shell/store/store-types.js';
8
+ import { WORKSPACE } from '@shell/store/prefs';
7
9
 
8
10
  export default {
9
11
  name: 'DetailWorkspace',
@@ -34,6 +36,35 @@ export default {
34
36
  return this.t(`typeLabel."${ FLEET.HELM_OP }"`, { count: this.value.counts.helmOps });
35
37
  },
36
38
 
39
+ applicationRoute() {
40
+ return {
41
+ name: 'c-cluster-fleet-application',
42
+ params: { cluster: BLANK_CLUSTER }
43
+ };
44
+ },
45
+
46
+ clustersRoute() {
47
+ return {
48
+ name: 'c-cluster-product-resource',
49
+ params: {
50
+ cluster: BLANK_CLUSTER,
51
+ product: FLEET_NAME,
52
+ resource: FLEET.CLUSTER,
53
+ }
54
+ };
55
+ },
56
+
57
+ clusterGroupsRoute() {
58
+ return {
59
+ name: 'c-cluster-product-resource',
60
+ params: {
61
+ cluster: BLANK_CLUSTER,
62
+ product: FLEET_NAME,
63
+ resource: FLEET.CLUSTER_GROUP,
64
+ }
65
+ };
66
+ },
67
+
37
68
  SCOPE_NAMESPACE() {
38
69
  return SCOPE_NAMESPACE;
39
70
  },
@@ -46,6 +77,16 @@ export default {
46
77
  return FLEET_NAME;
47
78
  }
48
79
  },
80
+
81
+ methods: {
82
+ setWorkspaceAndNavigate(route) {
83
+ const workspaceId = this.value.id;
84
+
85
+ this.$store.commit('updateWorkspace', { value: workspaceId, getters: this.$store.getters });
86
+ this.$store.dispatch('prefs/set', { key: WORKSPACE, value: workspaceId });
87
+ this.$router.push(route);
88
+ }
89
+ }
49
90
  };
50
91
  </script>
51
92
 
@@ -58,6 +99,8 @@ export default {
58
99
  :count="value.counts.gitRepos"
59
100
  :name="gitRepoLabel"
60
101
  :primary-color-var="'--sizzle-3'"
102
+ :clickable="true"
103
+ @click="setWorkspaceAndNavigate(applicationRoute)"
61
104
  />
62
105
  </div>
63
106
  <div class="col span-3">
@@ -65,6 +108,8 @@ export default {
65
108
  :count="value.counts.helmOps"
66
109
  :name="helmOpsLabel"
67
110
  :primary-color-var="'--sizzle-3'"
111
+ :clickable="true"
112
+ @click="setWorkspaceAndNavigate(applicationRoute)"
68
113
  />
69
114
  </div>
70
115
  <div class="col span-3">
@@ -72,6 +117,8 @@ export default {
72
117
  :count="value.counts.clusters"
73
118
  :name="clustersLabel"
74
119
  :primary-color-var="'--sizzle-1'"
120
+ :clickable="true"
121
+ @click="setWorkspaceAndNavigate(clustersRoute)"
75
122
  />
76
123
  </div>
77
124
  <div class="col span-3">
@@ -79,6 +126,8 @@ export default {
79
126
  :count="value.counts.clusterGroups"
80
127
  :name="clusterGroupsLabel"
81
128
  :primary-color-var="'--sizzle-2'"
129
+ :clickable="true"
130
+ @click="setWorkspaceAndNavigate(clusterGroupsRoute)"
82
131
  />
83
132
  </div>
84
133
  </div>
@@ -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