@rancher/shell 3.0.5-rc.8 → 3.0.5-rc.9

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 (171) hide show
  1. package/assets/styles/base/_color.scss +4 -1
  2. package/assets/styles/global/_tooltip.scss +7 -4
  3. package/assets/styles/themes/_dark.scss +11 -0
  4. package/assets/styles/themes/_light.scss +13 -1
  5. package/assets/styles/themes/_modern.scss +22 -0
  6. package/assets/translations/en-us.yaml +136 -14
  7. package/assets/translations/zh-hans.yaml +0 -1
  8. package/chart/monitoring/grafana/index.vue +8 -2
  9. package/components/ActionMenuShell.vue +3 -1
  10. package/components/Cron/CronExpressionEditor.vue +299 -0
  11. package/components/Cron/CronExpressionEditorModal.vue +247 -0
  12. package/components/Cron/CronTooltip.vue +87 -0
  13. package/components/Cron/types.ts +13 -0
  14. package/components/ForceDirectedTreeChart/composable.ts +11 -0
  15. package/components/PromptModal.vue +1 -1
  16. package/components/Resource/Detail/Card/__tests__/StateCard.test.ts +1 -0
  17. package/components/Resource/Detail/CopyToClipboard.vue +78 -0
  18. package/components/Resource/Detail/FetchLoader/__tests__/composables.test.ts +69 -0
  19. package/components/Resource/Detail/FetchLoader/composables.ts +27 -0
  20. package/components/Resource/Detail/Metadata/Annotations/__tests__/index.test.ts +1 -1
  21. package/components/Resource/Detail/Metadata/Annotations/index.vue +1 -1
  22. package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/identifying-fields.test.ts +13 -61
  23. package/components/Resource/Detail/Metadata/IdentifyingInformation/__tests__/index.test.ts +33 -6
  24. package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +24 -38
  25. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +25 -5
  26. package/components/Resource/Detail/Metadata/KeyValue.vue +12 -23
  27. package/components/Resource/Detail/Metadata/KeyValueRow.vue +144 -0
  28. package/components/Resource/Detail/Metadata/Labels/__tests__/index.test.ts +1 -0
  29. package/components/Resource/Detail/Metadata/Labels/index.vue +1 -0
  30. package/components/Resource/Detail/Metadata/__tests__/KeyValue.test.ts +30 -32
  31. package/components/Resource/Detail/Metadata/__tests__/KeyValueRow.test.ts +108 -0
  32. package/components/Resource/Detail/Metadata/__tests__/composables.test.ts +0 -3
  33. package/components/Resource/Detail/Metadata/__tests__/index.test.ts +12 -5
  34. package/components/Resource/Detail/Metadata/composables.ts +1 -4
  35. package/components/Resource/Detail/Metadata/index.vue +1 -0
  36. package/components/Resource/Detail/Preview/Content.vue +63 -0
  37. package/components/Resource/Detail/Preview/Preview.vue +128 -0
  38. package/components/Resource/Detail/Preview/__tests__/Content.spec.ts +71 -0
  39. package/components/Resource/Detail/Preview/__tests__/Preview.spec.ts +121 -0
  40. package/components/Resource/Detail/ResourcePopover/ResourcePopoverCard.vue +141 -0
  41. package/components/Resource/Detail/ResourcePopover/__tests__/ResourcePopoverCard.test.ts +136 -0
  42. package/components/Resource/Detail/ResourcePopover/__tests__/index.test.ts +245 -0
  43. package/components/Resource/Detail/ResourcePopover/index.vue +226 -0
  44. package/components/Resource/Detail/SpacedRow.vue +1 -0
  45. package/components/Resource/Detail/TitleBar/__tests__/composables.test.ts +0 -5
  46. package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +1 -1
  47. package/components/Resource/Detail/TitleBar/composables.ts +1 -3
  48. package/components/Resource/Detail/TitleBar/index.vue +2 -29
  49. package/components/Resource/Detail/ViewOptions/composable.ts +9 -0
  50. package/components/Resource/Detail/ViewOptions/index.vue +41 -0
  51. package/components/Resource/Detail/__tests__/CopyToClipboard.spec.ts +82 -0
  52. package/components/ResourceDetail/Masthead/legacy.vue +0 -19
  53. package/components/ResourceDetail/index.vue +1 -26
  54. package/components/ResourceTable.vue +24 -0
  55. package/components/SortableTable/index.vue +7 -1
  56. package/components/SortableTable/paging.js +3 -0
  57. package/components/Tabbed/Tab.vue +43 -1
  58. package/components/Tabbed/index.vue +3 -1
  59. package/components/__tests__/Cron/CronExpressionEditor.test.ts +151 -0
  60. package/components/__tests__/Cron/CronExpressionEditorModal.test.ts +81 -0
  61. package/components/auth/login/saml.vue +86 -0
  62. package/components/form/LabeledSelect.vue +8 -8
  63. package/components/form/ResourceTabs/composable.ts +54 -0
  64. package/components/form/ResourceTabs/index.vue +10 -7
  65. package/components/form/Select.vue +13 -10
  66. package/components/form/__tests__/LabeledSelect.test.ts +133 -0
  67. package/components/form/__tests__/Select.test.ts +134 -0
  68. package/composables/useExtensionManager.ts +17 -0
  69. package/config/home-links.js +12 -0
  70. package/config/labels-annotations.js +0 -1
  71. package/config/page-actions.js +0 -1
  72. package/config/product/explorer.js +3 -1
  73. package/config/product/fleet.js +2 -7
  74. package/config/product/manager.js +0 -5
  75. package/config/query-params.js +1 -0
  76. package/config/router/navigation-guards/clusters.js +2 -1
  77. package/config/router/navigation-guards/products.js +1 -1
  78. package/core/extension-manager-impl.js +518 -0
  79. package/core/plugins.js +35 -468
  80. package/core/types.ts +8 -2
  81. package/detail/__tests__/autoscaling.horizontalpodautoscaler.test.ts +1 -0
  82. package/detail/catalog.cattle.io.app.vue +7 -4
  83. package/detail/fleet.cattle.io.bundle.vue +1 -5
  84. package/detail/fleet.cattle.io.cluster.vue +3 -2
  85. package/detail/fleet.cattle.io.gitrepo.vue +76 -49
  86. package/detail/fleet.cattle.io.helmop.vue +78 -49
  87. package/dialog/AddonConfigConfirmationDialog.vue +1 -1
  88. package/dialog/GenericPrompt.vue +1 -1
  89. package/dialog/ImportDialog.vue +9 -2
  90. package/dialog/InstallExtensionDialog.vue +18 -10
  91. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +2 -1
  92. package/edit/__tests__/resources.cattle.io.restore.test.ts +106 -0
  93. package/edit/cloudcredential.vue +31 -17
  94. package/edit/constraints.gatekeeper.sh.constraint/index.vue +10 -2
  95. package/edit/fleet.cattle.io.cluster.vue +19 -0
  96. package/edit/fleet.cattle.io.gitrepo.vue +23 -16
  97. package/edit/monitoring.coreos.com.alertmanagerconfig/index.vue +12 -11
  98. package/edit/monitoring.coreos.com.alertmanagerconfig/receiverConfig.vue +11 -1
  99. package/edit/provisioning.cattle.io.cluster/index.vue +14 -19
  100. package/edit/provisioning.cattle.io.cluster/rke2.vue +11 -3
  101. package/edit/resources.cattle.io.restore.vue +5 -8
  102. package/list/__tests__/workload.test.ts +1 -0
  103. package/list/workload.vue +8 -1
  104. package/machine-config/components/GCEImage.vue +6 -5
  105. package/machine-config/google.vue +11 -6
  106. package/mixins/__tests__/chart.test.ts +139 -1
  107. package/mixins/chart.js +58 -18
  108. package/models/__tests__/namespace.test.ts +69 -0
  109. package/models/apps.statefulset.js +8 -10
  110. package/models/chart.js +5 -1
  111. package/models/fleet-application.js +16 -46
  112. package/models/fleet.cattle.io.bundle.js +1 -38
  113. package/models/fleet.cattle.io.gitrepo.js +4 -0
  114. package/models/fleet.cattle.io.helmop.js +4 -0
  115. package/models/management.cattle.io.project.js +12 -0
  116. package/models/namespace.js +30 -0
  117. package/models/workload.js +3 -0
  118. package/package.json +10 -10
  119. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +26 -11
  120. package/pages/c/_cluster/apps/charts/chart.vue +29 -20
  121. package/pages/c/_cluster/apps/charts/index.vue +1 -0
  122. package/pages/c/_cluster/apps/charts/install.vue +6 -5
  123. package/pages/c/_cluster/explorer/tools/__tests__/index.test.ts +102 -12
  124. package/pages/c/_cluster/explorer/tools/index.vue +145 -254
  125. package/pages/c/_cluster/manager/cloudCredential/index.vue +18 -1
  126. package/pages/c/_cluster/manager/drivers/kontainerDriver/index.vue +12 -2
  127. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +1 -1
  128. package/pages/c/_cluster/uiplugins/__tests__/index.spec.ts +318 -0
  129. package/pages/c/_cluster/uiplugins/index.vue +221 -363
  130. package/pages/home.vue +1 -9
  131. package/plugins/dashboard-store/resource-class.js +49 -0
  132. package/public/index.html +2 -1
  133. package/rancher-components/Card/Card.vue +1 -1
  134. package/rancher-components/Form/Checkbox/Checkbox.vue +1 -1
  135. package/rancher-components/Form/Radio/RadioButton.vue +1 -1
  136. package/rancher-components/Form/Radio/RadioGroup.vue +1 -1
  137. package/rancher-components/LabeledTooltip/LabeledTooltip.vue +1 -11
  138. package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.test.ts +53 -0
  139. package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.vue +65 -0
  140. package/rancher-components/Pill/RcCounterBadge/index.ts +1 -0
  141. package/rancher-components/Pill/RcCounterBadge/types.ts +7 -0
  142. package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +1 -1
  143. package/rancher-components/Pill/RcStatusBadge/index.ts +1 -1
  144. package/rancher-components/Pill/RcStatusIndicator/RcStatusIndicator.vue +3 -3
  145. package/rancher-components/Pill/RcStatusIndicator/types.ts +1 -1
  146. package/rancher-components/Pill/RcTag/RcTag.test.ts +64 -0
  147. package/rancher-components/Pill/RcTag/RcTag.vue +94 -0
  148. package/rancher-components/Pill/RcTag/index.ts +1 -0
  149. package/rancher-components/Pill/RcTag/types.ts +9 -0
  150. package/rancher-components/Pill/types.ts +1 -0
  151. package/rancher-components/RcItemCard/RcItemCard.vue +1 -0
  152. package/rancher-components/RcItemCard/RcItemCardAction.vue +12 -0
  153. package/store/__tests__/catalog.test.ts +63 -0
  154. package/store/catalog.js +2 -2
  155. package/store/type-map.js +3 -15
  156. package/types/extension-manager.ts +26 -0
  157. package/types/shell/index.d.ts +121 -16
  158. package/utils/__tests__/product.test.ts +129 -0
  159. package/utils/__tests__/resource.test.ts +87 -0
  160. package/utils/alertmanagerconfig.js +2 -2
  161. package/utils/auth.js +3 -76
  162. package/utils/product.ts +39 -0
  163. package/utils/resource.ts +35 -0
  164. package/utils/select.js +0 -24
  165. package/utils/validators/formRules/__tests__/index.test.ts +3 -0
  166. package/utils/validators/formRules/index.ts +2 -1
  167. package/vue.config.js +1 -1
  168. package/components/Resource/Detail/Metadata/Rectangle.vue +0 -34
  169. package/components/Resource/Detail/Metadata/__tests__/Rectangle.test.ts +0 -24
  170. package/components/ResourceDetail/Masthead/__tests__/legacy.test.ts +0 -65
  171. /package/components/{ForceDirectedTreeChart.vue → ForceDirectedTreeChart/index.vue} +0 -0
@@ -22,12 +22,69 @@ export default {
22
22
  window.location.href = idpRedirectUrl;
23
23
  },
24
24
  },
25
+
26
+ computed: {
27
+ // If any of the 3 params is specified, this is a CLI login
28
+ isCLILogin() {
29
+ const {
30
+ cli,
31
+ requestId,
32
+ publicKey,
33
+ responseType
34
+ } = this.$route.query;
35
+
36
+ return cli || publicKey || responseType || requestId;
37
+ },
38
+ // If this is a CLI login, we must have the correct respone type and the other params must not be empty
39
+ invalidCLILogin() {
40
+ const { requestId, publicKey, responseType } = this.$route.query;
41
+
42
+ if (this.isCLILogin) {
43
+ return responseType !== 'kubeconfig' || !requestId || !publicKey;
44
+ }
45
+
46
+ return false;
47
+ },
48
+ cliLoginCode() {
49
+ const { requestId } = this.$route.query;
50
+
51
+ return requestId;
52
+ },
53
+ warningMessageKey() {
54
+ const { cli } = this.$route.query;
55
+
56
+ return cli === 'true' ? 'login.cli.warning' : 'login.cli.warningLegacy';
57
+ }
58
+ }
25
59
  };
26
60
  </script>
27
61
 
28
62
  <template>
29
63
  <div class="text-center">
64
+ <div
65
+ v-if="isCLILogin"
66
+ class="cli-login"
67
+ >
68
+ <div class="cli-message">
69
+ {{ t('login.cli.welcome') }}
70
+ </div>
71
+ <div
72
+ v-if="invalidCLILogin"
73
+ class="cli-message cli-error"
74
+ >
75
+ {{ t('login.cli.invalidParams') }}
76
+ </div>
77
+ <template v-else>
78
+ <div class="cli-message">
79
+ {{ t(warningMessageKey, {}, true) }}
80
+ </div>
81
+ <div class="cli-login-code">
82
+ {{ cliLoginCode }}
83
+ </div>
84
+ </template>
85
+ </div>
30
86
  <button
87
+ v-if="!invalidCLILogin"
31
88
  ref="btn"
32
89
  class="btn bg-primary"
33
90
  style="font-size: 18px;"
@@ -37,3 +94,32 @@ export default {
37
94
  </button>
38
95
  </div>
39
96
  </template>
97
+ <style lang="scss" scoped>
98
+ .cli-login {
99
+ display: flex;
100
+ flex-direction: column;
101
+ align-items: center;
102
+
103
+ > div {
104
+ margin-bottom: 8px
105
+ }
106
+
107
+ .cli-message {
108
+ font-size: 16px;
109
+
110
+ &.cli-error {
111
+ color: var(--error);
112
+ }
113
+ }
114
+
115
+ .cli-login-code {
116
+ font-family: 'Courier New', Courier, monospace;
117
+ border: 1px solid var(--border);
118
+ border-radius: var(--border-radius);
119
+ padding: 4px 8px;
120
+ margin: 8px 0 16px 0;
121
+ letter-spacing: 1px;
122
+ font-size: 16px;
123
+ }
124
+ }
125
+ </style>
@@ -4,7 +4,7 @@ import LabeledFormElement from '@shell/mixins/labeled-form-element';
4
4
  import { get } from '@shell/utils/object';
5
5
  import { LabeledTooltip } from '@components/LabeledTooltip';
6
6
  import VueSelectOverrides from '@shell/mixins/vue-select-overrides';
7
- import { onClickOption, calculatePosition } from '@shell/utils/select';
7
+ import { calculatePosition } from '@shell/utils/select';
8
8
  import { generateRandomAlphaString } from '@shell/utils/string';
9
9
  import LabeledSelectPagination from '@shell/components/form/labeled-select-utils/labeled-select-pagination';
10
10
  import { LABEL_SELECT_NOT_OPTION_KINDS } from '@shell/types/components/labeledSelect';
@@ -169,14 +169,19 @@ export default {
169
169
  },
170
170
 
171
171
  methods: {
172
- // Ensure we only focus on open, otherwise we re-open on close
173
- clickSelect() {
172
+ clickSelect(event) {
174
173
  if (this.mode === _VIEW || this.loading === true || this.disabled === true) {
175
174
  return;
176
175
  }
177
176
 
177
+ // Ensure we don't toggle when clicking the clear button on multi-select
178
+ if (this.$attrs.multiple && event?.target.className === 'vs__deselect') {
179
+ return;
180
+ }
181
+
178
182
  this.isOpen = !this.isOpen;
179
183
 
184
+ // Ensure we only focus on open, otherwise we re-open on close
180
185
  if (this.isOpen) {
181
186
  this.focusSearch();
182
187
  }
@@ -262,10 +267,6 @@ export default {
262
267
 
263
268
  get,
264
269
 
265
- onClickOption(option, event) {
266
- onClickOption.call(this, option, event);
267
- },
268
-
269
270
  dropdownShouldOpen(instance, forceOpen = false) {
270
271
  if (!this.isOpen) {
271
272
  return false;
@@ -428,7 +429,6 @@ export default {
428
429
  v-else
429
430
  class="vs__option-kind"
430
431
  :class="{ 'has-icon' : hasGroupIcon}"
431
- @mousedown="(e) => onClickOption(option, e)"
432
432
  >
433
433
  {{ getOptionLabel(option) }}
434
434
  <i
@@ -0,0 +1,54 @@
1
+ import { randomStr } from '@shell/utils/string';
2
+ import { sum } from 'lodash';
3
+ import { computed, inject, provide, ref } from 'vue';
4
+
5
+ const UPDATE_COUNT_PROVIDER_KEY = 'update-count';
6
+ const USE_COUNTS_KEY = 'is-inside-resource-tabs';
7
+
8
+ type UpdateCountFn = (key: string, count: number | undefined) => void;
9
+
10
+ export const useIndicateUseCounts = () => {
11
+ provide(USE_COUNTS_KEY, true);
12
+ };
13
+
14
+ export const useTabCountWatcher = () => {
15
+ if (!inject<boolean>(USE_COUNTS_KEY, false)) {
16
+ return { isCountVisible: ref<boolean>(false) };
17
+ }
18
+
19
+ const countLedger = ref<{ [key: string]: number | undefined }>({});
20
+
21
+ const isCountVisible = computed(() => {
22
+ // Some tables are destroyed and recreated depending on visibility so we count keys
23
+ // to check if a table has been present in the tab even if the count has been cleared
24
+ return Object.keys(countLedger.value).length > 0;
25
+ });
26
+
27
+ const count = computed(() => {
28
+ return sum(Object.values(countLedger.value).map((count) => count || 0));
29
+ });
30
+
31
+ const updateCount = (key: string, count: number | undefined) => {
32
+ countLedger.value[key] = count;
33
+ };
34
+
35
+ provide(UPDATE_COUNT_PROVIDER_KEY, updateCount);
36
+
37
+ return { isCountVisible, count };
38
+ };
39
+
40
+ export const useTabCountUpdater = () => {
41
+ const tabKey = randomStr();
42
+ const updateCount = inject<UpdateCountFn>(UPDATE_COUNT_PROVIDER_KEY);
43
+
44
+ const updateTabCount = (count: number | undefined) => {
45
+ updateCount?.(tabKey, count);
46
+ };
47
+
48
+ const clearTabCount = () => updateTabCount(undefined);
49
+
50
+ return {
51
+ updateTabCount,
52
+ clearTabCount
53
+ };
54
+ };
@@ -16,6 +16,7 @@ import { PaginationParamFilter } from '@shell/types/store/pagination.types';
16
16
  import { MESSAGE, REASON } from '@shell/config/table-headers';
17
17
  import { STEVE_EVENT_LAST_SEEN, STEVE_EVENT_TYPE, STEVE_NAME_COL } from '@shell/config/pagination-table-headers';
18
18
  import { headerFromSchemaColString } from '@shell/store/type-map.utils';
19
+ import { useIndicateUseCounts } from '@shell/components/form/ResourceTabs/composable';
19
20
 
20
21
  export default {
21
22
 
@@ -75,6 +76,12 @@ export default {
75
76
  }
76
77
  },
77
78
 
79
+ setup(props) {
80
+ if (props.mode === _VIEW) {
81
+ useIndicateUseCounts();
82
+ }
83
+ },
84
+
78
85
  data() {
79
86
  const inStore = this.$store.getters['currentStore'](EVENT);
80
87
  const eventSchema = this.$store.getters[`${ inStore }/schemaFor`](EVENT); // @TODO be smarter about which resources actually ever have events
@@ -155,15 +162,13 @@ export default {
155
162
  }
156
163
 
157
164
  return false;
165
+ },
166
+ children() {
167
+ return this.$slots?.default?.() || [];
158
168
  }
159
169
  },
160
170
 
161
171
  methods: {
162
- // Ensures we only fetch events and show the table when the events tab has been activated
163
- tabChange(neu) {
164
- this.selectedTab = neu?.selectedName;
165
- },
166
-
167
172
  /**
168
173
  * Conditions come from a resource's `status`. They are used by both core resources like workloads as well as those from CRDs
169
174
  * - Workloads
@@ -243,7 +248,6 @@ export default {
243
248
  @changed="tabChange"
244
249
  >
245
250
  <slot />
246
-
247
251
  <Tab
248
252
  v-if="showConditions"
249
253
  label-key="resourceTabs.conditions.tab"
@@ -262,7 +266,6 @@ export default {
262
266
  >
263
267
  <!-- namespaced: false given we don't want the default handling of namespaced resource (apply header filter) -->
264
268
  <PaginatedResourceTable
265
- v-if="selectedTab === 'events'"
266
269
  :schema="eventSchema"
267
270
  :local-filter="filterEventsLocal"
268
271
  :api-filter="filterEventsApi"
@@ -4,7 +4,7 @@ import LabeledFormElement from '@shell/mixins/labeled-form-element';
4
4
  import VueSelectOverrides from '@shell/mixins/vue-select-overrides';
5
5
  import { generateRandomAlphaString } from '@shell/utils/string';
6
6
  import { LabeledTooltip } from '@components/LabeledTooltip';
7
- import { onClickOption, calculatePosition } from '@shell/utils/select';
7
+ import { calculatePosition } from '@shell/utils/select';
8
8
  import { _VIEW } from '@shell/config/query-params';
9
9
  import { useClickOutside } from '@shell/composables/useClickOutside';
10
10
  import { ref } from 'vue';
@@ -94,7 +94,11 @@ export default {
94
94
  isLangSelect: {
95
95
  type: Boolean,
96
96
  default: false
97
- }
97
+ },
98
+ loading: {
99
+ default: false,
100
+ type: Boolean
101
+ },
98
102
  },
99
103
  setup() {
100
104
  const select = ref(null);
@@ -134,14 +138,19 @@ export default {
134
138
  calculatePosition(dropdownList, component, width, this.placement);
135
139
  },
136
140
 
137
- // Ensure we only focus on open, otherwise we re-open on close
138
141
  clickSelect(ev) {
139
142
  if (this.mode === _VIEW || this.loading === true || this.disabled === true) {
140
143
  return;
141
144
  }
142
145
 
146
+ // Ensure we don't toggle when clicking the clear button on multi-select
147
+ if (this.$attrs.multiple && ev?.target.className === 'vs__deselect') {
148
+ return;
149
+ }
150
+
143
151
  this.isOpen = !this.isOpen;
144
152
 
153
+ // Ensure we only focus on open, otherwise we re-open on close
145
154
  if (this.isOpen) {
146
155
  this.focusSearch(ev);
147
156
  }
@@ -163,9 +172,6 @@ export default {
163
172
 
164
173
  get,
165
174
 
166
- onClickOption(option, event) {
167
- onClickOption.call(this, option, event);
168
- },
169
175
  selectable(opt) {
170
176
  // Lets you disable options that are used
171
177
  // for headings on groups of options.
@@ -352,10 +358,7 @@ export default {
352
358
  <template
353
359
  #option="option"
354
360
  >
355
- <div
356
- :lang="isLangSelect ? option.value : undefined"
357
- @mousedown="(e) => onClickOption(option, e)"
358
- >
361
+ <div :lang="isLangSelect ? option.value : undefined">
359
362
  {{ getOptionLabel(option.label) }}
360
363
  </div>
361
364
  </template>
@@ -291,4 +291,137 @@ describe('component: LabeledSelect', () => {
291
291
  expect(spyFocus).toHaveBeenCalled();
292
292
  expect(spyPreventDefault).not.toHaveBeenCalled();
293
293
  });
294
+
295
+ describe('function: clickSelect', () => {
296
+ it('should open dropdown when clickSelect is called and not disabled', async() => {
297
+ const label = 'Foo';
298
+ const value = 'foo';
299
+ const wrapper = mount(LabeledSelect, {
300
+ props: {
301
+ value,
302
+ options: [{ label, value }],
303
+ disabled: false,
304
+ loading: false,
305
+ mode: _EDIT
306
+ }
307
+ });
308
+
309
+ expect(wrapper.vm.isOpen).toBe(false);
310
+
311
+ wrapper.vm.clickSelect();
312
+ await wrapper.vm.$nextTick();
313
+
314
+ expect(wrapper.vm.isOpen).toBe(true);
315
+ });
316
+
317
+ it('should not open dropdown when clickSelect is called and disabled', async() => {
318
+ const label = 'Foo';
319
+ const value = 'foo';
320
+ const wrapper = mount(LabeledSelect, {
321
+ props: {
322
+ value,
323
+ options: [{ label, value }],
324
+ disabled: true,
325
+ loading: false,
326
+ mode: _EDIT
327
+ }
328
+ });
329
+
330
+ expect(wrapper.vm.isOpen).toBe(false);
331
+
332
+ wrapper.vm.clickSelect();
333
+ await wrapper.vm.$nextTick();
334
+
335
+ expect(wrapper.vm.isOpen).toBe(false);
336
+ });
337
+
338
+ it('should not open dropdown when loading is true', async() => {
339
+ const label = 'Foo';
340
+ const value = 'foo';
341
+ const wrapper = mount(LabeledSelect, {
342
+ props: {
343
+ value,
344
+ options: [{ label, value }],
345
+ disabled: false,
346
+ loading: true,
347
+ mode: _EDIT
348
+ }
349
+ });
350
+
351
+ expect(wrapper.vm.isOpen).toBe(false);
352
+
353
+ wrapper.vm.clickSelect();
354
+ await wrapper.vm.$nextTick();
355
+
356
+ expect(wrapper.vm.isOpen).toBe(false);
357
+ });
358
+
359
+ it('should not open dropdown when mode is _VIEW', async() => {
360
+ const label = 'Foo';
361
+ const value = 'foo';
362
+ const wrapper = mount(LabeledSelect, {
363
+ props: {
364
+ value,
365
+ options: [{ label, value }],
366
+ disabled: false,
367
+ loading: false,
368
+ mode: _VIEW
369
+ }
370
+ });
371
+
372
+ expect(wrapper.vm.isOpen).toBe(false);
373
+
374
+ wrapper.vm.clickSelect();
375
+ await wrapper.vm.$nextTick();
376
+
377
+ expect(wrapper.vm.isOpen).toBe(false);
378
+ });
379
+
380
+ it('should not clear value if disabled', async() => {
381
+ const label = 'Foo';
382
+ const value = 'foo';
383
+ const wrapper = mount(LabeledSelect, {
384
+ props: {
385
+ value,
386
+ options: [{ label, value }],
387
+ multiple: true,
388
+ disabled: true,
389
+ mode: _EDIT
390
+ }
391
+ });
392
+
393
+ const clearBtn = wrapper.find('.vs__deselect');
394
+
395
+ expect(clearBtn.exists()).toBe(true);
396
+
397
+ await clearBtn.trigger('mousedown');
398
+ await wrapper.vm.$nextTick();
399
+
400
+ expect(wrapper.emitted('update:value')).toBeUndefined();
401
+ expect(wrapper.vm.isOpen).toBe(false);
402
+ });
403
+
404
+ it('should not open dropdown when remove button is clicked', async() => {
405
+ const label = 'Foo';
406
+ const value = 'foo';
407
+ const wrapper = mount(LabeledSelect, {
408
+ props: {
409
+ value,
410
+ options: [{ label, value }],
411
+ multiple: true,
412
+ mode: _EDIT
413
+ }
414
+ });
415
+
416
+ expect(wrapper.vm.isOpen).toBe(false);
417
+
418
+ const clearBtn = wrapper.find('.vs__deselect');
419
+
420
+ await clearBtn.trigger('mousedown');
421
+ await wrapper.vm.$nextTick();
422
+
423
+ expect(wrapper.emitted('update:value')).toBeUndefined();
424
+ expect(wrapper.vm.isOpen).toBe(false);
425
+ });
426
+ });
294
427
  });
@@ -1,6 +1,7 @@
1
1
  import { shallowMount, mount } from '@vue/test-utils';
2
2
  import { defineComponent } from 'vue';
3
3
  import Select from '@shell/components/form/Select.vue';
4
+ import { _EDIT, _VIEW } from '@shell/config/query-params';
4
5
 
5
6
  const SelectComponent = Select as ReturnType<typeof defineComponent>;
6
7
 
@@ -100,4 +101,137 @@ describe('select.vue', () => {
100
101
  expect(spyFocus).toHaveBeenCalled();
101
102
  expect(spyPreventDefault).not.toHaveBeenCalled();
102
103
  });
104
+
105
+ describe('function: clickSelect', () => {
106
+ it('should open dropdown when clickSelect is called and not disabled', async() => {
107
+ const label = 'Foo';
108
+ const value = 'foo';
109
+ const wrapper = mount(Select, {
110
+ props: {
111
+ value,
112
+ options: [{ label, value }],
113
+ disabled: false,
114
+ loading: false,
115
+ mode: _EDIT
116
+ }
117
+ });
118
+
119
+ expect(wrapper.vm.isOpen).toBe(false);
120
+
121
+ wrapper.vm.clickSelect();
122
+ await wrapper.vm.$nextTick();
123
+
124
+ expect(wrapper.vm.isOpen).toBe(true);
125
+ });
126
+
127
+ it('should not open dropdown when clickSelect is called and disabled', async() => {
128
+ const label = 'Foo';
129
+ const value = 'foo';
130
+ const wrapper = mount(Select, {
131
+ props: {
132
+ value,
133
+ options: [{ label, value }],
134
+ disabled: true,
135
+ loading: false,
136
+ mode: _EDIT
137
+ }
138
+ });
139
+
140
+ expect(wrapper.vm.isOpen).toBe(false);
141
+
142
+ wrapper.vm.clickSelect();
143
+ await wrapper.vm.$nextTick();
144
+
145
+ expect(wrapper.vm.isOpen).toBe(false);
146
+ });
147
+
148
+ it('should not open dropdown when loading is true', async() => {
149
+ const label = 'Foo';
150
+ const value = 'foo';
151
+ const wrapper = mount(Select, {
152
+ props: {
153
+ value,
154
+ options: [{ label, value }],
155
+ disabled: false,
156
+ loading: true,
157
+ mode: _EDIT
158
+ }
159
+ });
160
+
161
+ expect(wrapper.vm.isOpen).toBe(false);
162
+
163
+ wrapper.vm.clickSelect();
164
+ await wrapper.vm.$nextTick();
165
+
166
+ expect(wrapper.vm.isOpen).toBe(false);
167
+ });
168
+
169
+ it('should not open dropdown when mode is _VIEW', async() => {
170
+ const label = 'Foo';
171
+ const value = 'foo';
172
+ const wrapper = mount(Select, {
173
+ props: {
174
+ value,
175
+ options: [{ label, value }],
176
+ disabled: false,
177
+ loading: false,
178
+ mode: _VIEW
179
+ }
180
+ });
181
+
182
+ expect(wrapper.vm.isOpen).toBe(false);
183
+
184
+ wrapper.vm.clickSelect();
185
+ await wrapper.vm.$nextTick();
186
+
187
+ expect(wrapper.vm.isOpen).toBe(false);
188
+ });
189
+
190
+ it('should not clear value if disabled', async() => {
191
+ const label = 'Foo';
192
+ const value = 'foo';
193
+ const wrapper = mount(Select, {
194
+ props: {
195
+ value,
196
+ options: [{ label, value }],
197
+ multiple: true,
198
+ disabled: true,
199
+ mode: _EDIT
200
+ }
201
+ });
202
+
203
+ const clearBtn = wrapper.find('.vs__deselect');
204
+
205
+ expect(clearBtn.exists()).toBe(true);
206
+
207
+ await clearBtn.trigger('mousedown');
208
+ await wrapper.vm.$nextTick();
209
+
210
+ expect(wrapper.emitted('update:value')).toBeUndefined();
211
+ expect(wrapper.vm.isOpen).toBe(false);
212
+ });
213
+
214
+ it('should not open dropdown when remove button is clicked', async() => {
215
+ const label = 'Foo';
216
+ const value = 'foo';
217
+ const wrapper = mount(Select, {
218
+ props: {
219
+ value,
220
+ options: [{ label, value }],
221
+ multiple: true,
222
+ mode: _EDIT
223
+ }
224
+ });
225
+
226
+ expect(wrapper.vm.isOpen).toBe(false);
227
+
228
+ const clearBtn = wrapper.find('.vs__deselect');
229
+
230
+ await clearBtn.trigger('mousedown');
231
+ await wrapper.vm.$nextTick();
232
+
233
+ expect(wrapper.emitted('update:value')).toBeUndefined();
234
+ expect(wrapper.vm.isOpen).toBe(false);
235
+ });
236
+ });
103
237
  });
@@ -0,0 +1,17 @@
1
+ import { ExtensionManager } from '@shell/types/extension-manager';
2
+ import { getExtensionManager } from '@shell/core/extension-manager-impl';
3
+
4
+ /**
5
+ * Provides access to the registered extension manager instance. Used within Vue
6
+ * components or other composables that require extension functionality.
7
+ * @returns The extension manager instance
8
+ */
9
+ export const useExtensionManager = (): ExtensionManager => {
10
+ const extension = getExtensionManager();
11
+
12
+ if (!extension) {
13
+ throw new Error('useExtensionManager must be called after the extensionManager has been initialized');
14
+ }
15
+
16
+ return extension;
17
+ };
@@ -3,6 +3,7 @@ import { MANAGEMENT } from '@shell/config/types';
3
3
  import { SETTING } from '@shell/config/settings';
4
4
  import { allHash } from '@shell/utils/promise';
5
5
  import { isRancherPrime } from '@shell/config/version';
6
+ import DOMPurify from 'dompurify';
6
7
 
7
8
  // i18n-uses customLinks.defaults.*
8
9
  const DEFAULT_LINKS = [
@@ -112,6 +113,17 @@ export async function fetchLinks(store, hasSupport, isSupportPage, t) {
112
113
  uiLinks.defaults = defaults;
113
114
  }
114
115
 
116
+ // Check the link values for each custom link
117
+ uiLinks.custom.forEach((link) => {
118
+ const anchor = `<a href="${ link.value }"></a>`;
119
+ const cleanedLink = DOMPurify.sanitize(anchor);
120
+
121
+ if (cleanedLink !== anchor) {
122
+ console.error(`Custom link value "${ link.value }" is not valid for link "${ link.label }"`); // eslint-disable-line no-console
123
+ link.value = '/#';
124
+ }
125
+ });
126
+
115
127
  return ensureSupportLink(uiLinks, hasSupport, isSupportPage, t, store);
116
128
  }
117
129
 
@@ -129,7 +129,6 @@ export const FLEET = {
129
129
  CLUSTER_NAMESPACE: 'fleet.cattle.io/cluster-namespace',
130
130
  CLUSTER: 'fleet.cattle.io/cluster',
131
131
  CREATED_BY_USER_ID: 'fleet.cattle.io/created-by-user-id',
132
- CREATED_BY_USER_NAME: 'fleet.cattle.io/created-by-display-name',
133
132
  OCI_STORAGE_SECRET_DEFAULT: 'ui-default-oci-registry',
134
133
  OCI_STORAGE_SECRET_GENERATED: 'fleet.cattle.io/bundle-internal-secret',
135
134
  };
@@ -1,4 +1,3 @@
1
- export const RESET_CARDS_ACTION = 'reset-homepage-cards';
2
1
  export const SET_LOGIN_ACTION = 'set-as-login';
3
2
  export const ADD_CUSTOM_NAV_LINK = 'add-custom-nav-link';
4
3
  export const SHOW_HIDE_BANNER_ACTION = 'toggle-homepage-banner';
@@ -176,6 +176,7 @@ export function init(store) {
176
176
  mapGroup(/^(.*\.)?resources\.cattle\.io$/, 'Backup-Restore');
177
177
  mapGroup(/^(.*\.)?cluster\.x-k8s\.io$/, 'clusterProvisioning');
178
178
  mapGroup(/^(aks|eks|gke|rke|rke-machine-config|rke-machine|provisioning)\.cattle\.io$/, 'clusterProvisioning');
179
+ mapGroup(/^(.*\.)?(scc)\.cattle\.io$/, 'SCC');
179
180
 
180
181
  const dePaginateBindings = configureConditionalDepaginate({ maxResourceCount: 5000 });
181
182
  const dePaginateNormanBindings = configureConditionalDepaginate({ maxResourceCount: 5000, isNorman: true }) ;
@@ -494,7 +495,8 @@ export function init(store) {
494
495
 
495
496
  headers(MANAGEMENT.PSA, [STATE, NAME_COL, {
496
497
  ...DESCRIPTION,
497
- width: undefined
498
+ width: undefined,
499
+ formatter: undefined,
498
500
  }, AGE]);
499
501
 
500
502
  headers(STORAGE_CLASS,