@rancher/shell 3.0.9-rc.6 → 3.0.10

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 (82) hide show
  1. package/assets/styles/base/_color.scss +4 -0
  2. package/assets/styles/themes/_light.scss +6 -6
  3. package/assets/styles/themes/_modern.scss +14 -6
  4. package/assets/translations/en-us.yaml +2 -5
  5. package/components/CopyToClipboard.vue +28 -0
  6. package/components/CopyToClipboardText.vue +4 -0
  7. package/components/CruResource.vue +1 -0
  8. package/components/GlobalRoleBindings.vue +1 -5
  9. package/components/IconOrSvg.vue +61 -42
  10. package/components/ResourceDetail/index.vue +0 -21
  11. package/components/SortableTable/index.vue +2 -2
  12. package/components/__tests__/CruResource.test.ts +35 -1
  13. package/components/form/BannerSettings.vue +2 -2
  14. package/components/form/NotificationSettings.vue +2 -2
  15. package/composables/useIsNewDetailPageEnabled.test.ts +98 -0
  16. package/composables/useIsNewDetailPageEnabled.ts +12 -0
  17. package/config/product/explorer.js +11 -1
  18. package/config/product/manager.js +0 -1
  19. package/config/table-headers.js +0 -9
  20. package/config/types.js +0 -1
  21. package/detail/fleet.cattle.io.cluster.vue +1 -1
  22. package/dialog/FeatureFlagListDialog.vue +1 -1
  23. package/edit/auth/github-app-steps.vue +2 -0
  24. package/edit/auth/github-steps.vue +2 -0
  25. package/edit/catalog.cattle.io.clusterrepo.vue +1 -1
  26. package/edit/management.cattle.io.user.vue +60 -35
  27. package/edit/monitoring.coreos.com.alertmanagerconfig/__tests__/auth.spec.ts +145 -0
  28. package/edit/monitoring.coreos.com.alertmanagerconfig/__tests__/index.test.ts +202 -0
  29. package/edit/monitoring.coreos.com.alertmanagerconfig/__tests__/tls.spec.ts +226 -0
  30. package/edit/monitoring.coreos.com.alertmanagerconfig/auth.vue +24 -21
  31. package/edit/monitoring.coreos.com.alertmanagerconfig/types/__tests__/opsgenie.spec.ts +157 -0
  32. package/edit/monitoring.coreos.com.alertmanagerconfig/types/__tests__/pagerduty.spec.ts +132 -0
  33. package/edit/monitoring.coreos.com.alertmanagerconfig/types/__tests__/slack.spec.ts +108 -0
  34. package/edit/monitoring.coreos.com.alertmanagerconfig/types/pagerduty.vue +2 -1
  35. package/edit/monitoring.coreos.com.receiver/__tests__/auth.spec.ts +165 -0
  36. package/edit/monitoring.coreos.com.receiver/__tests__/index.test.ts +153 -0
  37. package/edit/monitoring.coreos.com.receiver/__tests__/tls.spec.ts +115 -0
  38. package/edit/monitoring.coreos.com.receiver/types/__tests__/email.spec.ts +86 -0
  39. package/edit/monitoring.coreos.com.receiver/types/__tests__/opsgenie.spec.ts +209 -0
  40. package/edit/monitoring.coreos.com.receiver/types/__tests__/pagerduty.spec.ts +105 -0
  41. package/edit/monitoring.coreos.com.receiver/types/__tests__/slack.spec.ts +92 -0
  42. package/edit/monitoring.coreos.com.receiver/types/__tests__/webhook.spec.ts +131 -0
  43. package/edit/provisioning.cattle.io.cluster/ingress/IngressCards.vue +14 -12
  44. package/edit/provisioning.cattle.io.cluster/rke2.vue +4 -5
  45. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +18 -3
  46. package/edit/provisioning.cattle.io.cluster/tabs/Ingress.vue +100 -76
  47. package/edit/token.vue +29 -68
  48. package/list/provisioning.cattle.io.cluster.vue +2 -2
  49. package/models/__tests__/chart.test.ts +2 -2
  50. package/models/chart.js +3 -3
  51. package/models/token.js +0 -4
  52. package/package.json +8 -8
  53. package/pages/account/index.vue +67 -96
  54. package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +108 -24
  55. package/pages/c/_cluster/apps/charts/index.vue +1 -11
  56. package/pages/c/_cluster/explorer/index.vue +2 -19
  57. package/pages/c/_cluster/explorer/tools/index.vue +1 -1
  58. package/pages/c/_cluster/manager/cloudCredential/index.vue +1 -1
  59. package/pages/c/_cluster/uiplugins/index.vue +1 -1
  60. package/pkg/auto-import.js +41 -0
  61. package/plugins/dashboard-store/resource-class.js +2 -2
  62. package/plugins/steve/__tests__/steve-class.test.ts +1 -1
  63. package/plugins/steve/steve-class.js +3 -3
  64. package/plugins/steve/steve-pagination-utils.ts +2 -4
  65. package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.vue +7 -7
  66. package/rancher-components/Pill/RcStatusBadge/RcStatusBadge.vue +5 -2
  67. package/rancher-components/RcIcon/types.ts +2 -2
  68. package/rancher-components/RcItemCard/RcItemCard.vue +8 -1
  69. package/rancher-components/RcSection/RcSection.test.ts +323 -0
  70. package/rancher-components/RcSection/RcSection.vue +252 -0
  71. package/rancher-components/RcSection/RcSectionActions.test.ts +212 -0
  72. package/rancher-components/RcSection/RcSectionActions.vue +85 -0
  73. package/rancher-components/RcSection/RcSectionBadges.test.ts +149 -0
  74. package/rancher-components/RcSection/RcSectionBadges.vue +29 -0
  75. package/rancher-components/RcSection/index.ts +12 -0
  76. package/rancher-components/RcSection/types.ts +86 -0
  77. package/scripts/test-plugins-build.sh +5 -4
  78. package/types/shell/index.d.ts +93 -108
  79. package/utils/style.ts +17 -0
  80. package/utils/svg-filter.js +4 -3
  81. package/utils/units.js +14 -5
  82. package/models/ext.cattle.io.token.js +0 -48
@@ -9,107 +9,94 @@ import { mapGetters } from 'vuex';
9
9
 
10
10
  import { Banner } from '@components/Banner';
11
11
  import ResourceTable from '@shell/components/ResourceTable';
12
+ import CopyToClipboardText from '@shell/components/CopyToClipboardText';
12
13
  import TabTitle from '@shell/components/TabTitle';
13
14
 
14
- import { allHash } from '@shell/utils/promise';
15
-
16
- import {
17
- ACCESS_KEY, DESCRIPTION, EXPIRES, EXPIRY_STATE,
18
- LAST_USED, AGE_NORMAN, SCOPE_NORMAN, NORMAN_KEY_DEPRECATION
19
- } from '@shell/config/table-headers';
20
- import { FilterArgs, PaginationParamFilter } from '@shell/types/store/pagination.types';
15
+ const API_ENDPOINT = '/v3';
21
16
 
22
17
  export default {
23
18
  components: {
24
- BackLink, Banner, Loading, ResourceTable, Principal, TabTitle
19
+ CopyToClipboardText, BackLink, Banner, Loading, ResourceTable, Principal, TabTitle
25
20
  },
26
21
  mixins: [BackRoute],
27
22
  async fetch() {
28
- const hashedRequests = {};
29
-
30
23
  this.canChangePassword = await this.calcCanChangePassword();
31
24
 
32
- this.normanTokenSchema = this.$store.getters[`rancher/schemaFor`](NORMAN.TOKEN);
33
- this.steveTokenSchema = this.$store.getters[`management/schemaFor`](EXT.TOKEN);
25
+ if (this.apiKeySchema) {
26
+ this.rows = await this.$store.dispatch('rancher/findAll', { type: NORMAN.TOKEN });
27
+ }
34
28
 
35
- const selfUser = await this.$store.dispatch('auth/getSelfUser');
29
+ // Get all settings - the API host setting may not be set, so this avoids a 404 request if we look for the specific setting
30
+ const allSettings = await this.$store.dispatch('management/findAll', { type: MANAGEMENT.SETTING });
31
+ const apiHostSetting = allSettings.find((i) => i.id === SETTING.API_HOST);
32
+ const serverUrlSetting = allSettings.find((i) => i.id === SETTING.SERVER_URL);
36
33
 
37
- if (this.normanTokenSchema) {
38
- hashedRequests.normanTokens = this.$store.dispatch('rancher/findAll', { type: NORMAN.TOKEN });
39
- }
34
+ this.apiHostSetting = apiHostSetting?.value;
35
+ this.serverUrlSetting = serverUrlSetting?.value;
40
36
 
41
- if (this.steveTokenSchema) {
42
- this.filterByUserTokens = this.$store.getters[`management/paginationEnabled`](EXT.TOKEN);
43
-
44
- if (this.filterByUserTokens && selfUser?.status?.userID) {
45
- // Only get associated with the current user
46
- const opt = { // Of type ActionFindPageArgs
47
- pagination: new FilterArgs({
48
- filters: PaginationParamFilter.createSingleField({
49
- field: 'metadata.fields.1',
50
- value: selfUser.status?.userID,
51
- })
52
- })
53
- };
54
-
55
- hashedRequests.steveTokens = this.$store.dispatch(`management/findPage`, { type: EXT.TOKEN, opt });
56
- } else {
57
- hashedRequests.steveTokens = this.$store.dispatch('management/findAll', { type: EXT.TOKEN });
58
- }
59
- }
37
+ const selfUser = await this.$store.dispatch('auth/getSelfUser');
60
38
 
61
39
  if (selfUser?.canGetUser && selfUser.status?.userID) {
62
40
  // Fetch the user info for ChangePassword (ChangePasswordDialog needs the user info for the user whose password is being changed)
63
- hashedRequests.user = this.$store.dispatch('management/find', {
41
+ this.user = await this.$store.dispatch('management/find', {
64
42
  type: MANAGEMENT.USER,
65
43
  id: selfUser.status?.userID
66
44
  });
45
+ } else {
46
+ throw new Error(this.t('changePassword.errors.cannotFetchSelf'));
67
47
  }
68
-
69
- // Get all settings - the API host setting may not be set, so this avoids a 404 request if we look for the specific setting
70
- hashedRequests.allSettings = this.$store.dispatch('management/findAll', { type: MANAGEMENT.SETTING });
71
-
72
- const {
73
- normanTokens, steveTokens, allSettings, user
74
- } = await allHash(hashedRequests);
75
-
76
- this.normanTokens = normanTokens;
77
- this.steveTokens = steveTokens;
78
- this.user = user;
79
-
80
- const apiHostSetting = allSettings.find((i) => i.id === SETTING.API_HOST);
81
- const serverUrlSetting = allSettings.find((i) => i.id === SETTING.SERVER_URL);
82
-
83
- this.apiHostSetting = apiHostSetting?.value;
84
- this.serverUrlSetting = serverUrlSetting?.value;
85
48
  },
86
49
  data() {
87
50
  return {
88
- normanTokenSchema: undefined,
89
- steveTokenSchema: undefined,
90
51
  apiHostSetting: null,
91
52
  serverUrlSetting: null,
92
53
  rows: null,
93
54
  canChangePassword: false,
94
- user: null,
95
- normanTokens: null,
96
- steveTokens: null,
55
+ user: null
97
56
  };
98
57
  },
99
58
  computed: {
100
59
  ...mapGetters({ t: 'i18n/t' }),
101
60
 
102
61
  apiKeyheaders() {
103
- return [
104
- EXPIRY_STATE,
105
- ACCESS_KEY,
106
- DESCRIPTION,
107
- SCOPE_NORMAN,
108
- NORMAN_KEY_DEPRECATION,
109
- LAST_USED,
110
- EXPIRES,
111
- AGE_NORMAN
112
- ];
62
+ return this.apiKeySchema ? this.$store.getters['type-map/headersFor'](this.apiKeySchema) : [];
63
+ },
64
+
65
+ // Port of Ember code for API Url - see: https://github.com/rancher/ui/blob/8e07c492673171731f3b26af14c978bc103d1828/lib/shared/addon/endpoint/service.js#L58
66
+ apiUrlBase() {
67
+ let setting = this.apiHostSetting;
68
+
69
+ if (setting && setting.indexOf('http') !== 0) {
70
+ setting = `http://${ setting }`;
71
+ }
72
+
73
+ // Use Server Setting URL if the api host setting is not set
74
+ let url = setting || this.serverUrlSetting;
75
+
76
+ // If the URL is relative, add on the current base URL from the browser
77
+ if ( url.indexOf('http') !== 0 ) {
78
+ url = `${ window.location.origin }/${ url.replace(/^\/+/, '') }`;
79
+ }
80
+
81
+ // URL must end in a single slash
82
+ url = `${ url.replace(/\/+$/, '') }/`;
83
+
84
+ return url;
85
+ },
86
+
87
+ apiUrl() {
88
+ const base = this.apiUrlBase;
89
+ const path = API_ENDPOINT.replace(/^\/+/, '');
90
+
91
+ return `${ base }${ path }`;
92
+ },
93
+
94
+ apiKeySchema() {
95
+ try {
96
+ return this.$store.getters[`rancher/schemaFor`](NORMAN.TOKEN);
97
+ } catch (e) {}
98
+
99
+ return null;
113
100
  },
114
101
 
115
102
  principal() {
@@ -123,7 +110,7 @@ export default {
123
110
  return principal || {};
124
111
  },
125
112
 
126
- filteredNormanTokens() {
113
+ apiKeys() {
127
114
  // Filter out tokens that are not API Keys and are not expired UI Sessions
128
115
  const isApiKey = (key) => {
129
116
  const labels = key.labels;
@@ -133,24 +120,7 @@ export default {
133
120
  return ( !expired || !labels || !labels['ui-session'] ) && !current;
134
121
  };
135
122
 
136
- return !this.normanTokens ? [] : this.normanTokens.filter(isApiKey);
137
- },
138
-
139
- filteredNewTokens() {
140
- // Filter out tokens that are not API Keys and are not expired UI Sessions
141
- const isApiKey = (key) => {
142
- const labels = key.metadata?.labels;
143
- const expired = key.status?.expired;
144
- const current = key.status?.current;
145
-
146
- return ( !expired || !labels || !labels['ui-session'] ) && !current;
147
- };
148
-
149
- return !this.steveTokens ? [] : this.steveTokens.filter(isApiKey);
150
- },
151
-
152
- apiKeys() {
153
- return (this.filteredNormanTokens || []).concat(this.filteredNewTokens || []);
123
+ return !this.rows ? [] : this.rows.filter(isApiKey);
154
124
  }
155
125
  },
156
126
 
@@ -221,9 +191,16 @@ export default {
221
191
  <div class="keys-header">
222
192
  <div>
223
193
  <h2 v-t="'accountAndKeys.apiKeys.title'" />
194
+ <div class="api-url">
195
+ <span>{{ t("accountAndKeys.apiKeys.apiEndpoint") }}</span>
196
+ <CopyToClipboardText
197
+ :aria-label="t('accountAndKeys.apiKeys.copyApiEnpoint')"
198
+ :text="apiUrl"
199
+ />
200
+ </div>
224
201
  </div>
225
202
  <button
226
- v-if="steveTokenSchema"
203
+ v-if="apiKeySchema"
227
204
  role="button"
228
205
  :aria-label="t('accountAndKeys.apiKeys.add.label')"
229
206
  class="btn role-primary add mb-20"
@@ -234,17 +211,11 @@ export default {
234
211
  </button>
235
212
  </div>
236
213
  <div
237
- v-if="steveTokenSchema"
214
+ v-if="apiKeySchema"
238
215
  class="keys"
239
216
  >
240
- <Banner
241
- v-if="filteredNormanTokens.length"
242
- color="warning"
243
- class="mb-20"
244
- :label="t('accountAndKeys.apiKeys.normanTokenDeprecation')"
245
- />
246
217
  <ResourceTable
247
- :schema="steveTokenSchema"
218
+ :schema="apiKeySchema"
248
219
  :rows="apiKeys"
249
220
  :headers="apiKeyheaders"
250
221
  key-field="id"
@@ -1,26 +1,78 @@
1
1
  <script setup lang="ts">
2
+ import { reactive } from 'vue';
2
3
  import { RcItemCardAction } from '@components/RcItemCard';
3
4
  import { RcButton } from '@components/RcButton';
5
+ import { isTruncated } from '@shell/utils/style';
6
+ import RcIcon from '@components/RcIcon/RcIcon.vue';
7
+ import type { RcIconType } from '@components/RcIcon/types';
4
8
 
5
9
  interface FooterItem {
6
- icon?: string;
7
- iconTooltip?: Record<{key?: string, text?: string}>;
10
+ icon?: RcIconType;
11
+ iconTooltip?: { key?: string; text?: string };
8
12
  labels: string[];
9
13
  labelTooltip?: string;
10
14
  type?: string;
11
15
  }
12
16
 
13
- const emit = defineEmits<{(e: 'click:item', type: string, label: string): void; }>();
17
+ const emit = defineEmits<{(e: 'click:item', type: string, label: string): void }>();
14
18
 
15
19
  defineProps<{
16
20
  items: FooterItem[];
17
21
  clickable?: boolean;
18
22
  }>();
19
23
 
20
- function onClickItem(type: string, label: string) {
24
+ const tooltipOverrides = reactive<Record<string, string | undefined>>({});
25
+ const labelElements: Record<string, HTMLElement | null> = {};
26
+
27
+ function onClickItem(type: string, label: string): void {
21
28
  emit('click:item', type, label);
22
29
  }
23
30
 
31
+ /**
32
+ * Creates a unique key for identifying a label element.
33
+ * @param itemIndex - Index of the footer item
34
+ * @param labelIndex - Index of the label within the footer item
35
+ */
36
+ function createLabelKey(itemIndex: number, labelIndex: number): string {
37
+ return `${ itemIndex }-${ labelIndex }`;
38
+ }
39
+
40
+ /**
41
+ * Registers a label element reference for truncation detection.
42
+ * @param key - Unique identifier for the label
43
+ * @param el - The HTML element or null
44
+ */
45
+ function registerLabelRef(key: string, el: HTMLElement | null): void {
46
+ labelElements[key] = el;
47
+ }
48
+
49
+ /**
50
+ * Updates the tooltip content based on whether the label text is truncated.
51
+ * If truncated, prepends the full label text to the base tooltip.
52
+ * @param key - Unique identifier for the label
53
+ * @param label - The label text content
54
+ * @param baseTooltip - The original tooltip content
55
+ */
56
+ function updateTooltipOnHover(key: string, label: string, baseTooltip?: string): void {
57
+ if (!baseTooltip) {
58
+ tooltipOverrides[key] = undefined;
59
+
60
+ return;
61
+ }
62
+
63
+ const element = labelElements[key];
64
+
65
+ tooltipOverrides[key] = isTruncated(element) ? `${ label }. ${ baseTooltip }` : baseTooltip;
66
+ }
67
+
68
+ /**
69
+ * Returns the tooltip to display for a given label.
70
+ * @param key - Unique identifier for the label
71
+ * @param fallback - Fallback tooltip if no override exists
72
+ */
73
+ function getTooltip(key: string, fallback?: string): string | undefined {
74
+ return tooltipOverrides[key] ?? fallback;
75
+ }
24
76
  </script>
25
77
 
26
78
  <template>
@@ -31,43 +83,52 @@ function onClickItem(type: string, label: string) {
31
83
  class="app-chart-card-footer-item"
32
84
  data-testid="app-chart-card-footer-item"
33
85
  >
86
+ <RcIcon
87
+ v-if="footerItem.icon"
88
+ v-clean-tooltip="footerItem.iconTooltip?.key ? t(footerItem.iconTooltip.key) : undefined"
89
+ class="app-chart-card-footer-item-icon"
90
+ :type="footerItem.icon"
91
+ />
34
92
  <template
35
93
  v-for="(label, j) in footerItem.labels"
36
94
  :key="j"
37
95
  >
38
96
  <rc-item-card-action
39
97
  v-if="clickable && footerItem.type"
40
- class="app-chart-card-footer-item-text"
98
+ class="app-chart-card-footer-item-action"
41
99
  >
42
100
  <rc-button
43
- v-clean-tooltip="footerItem.labelTooltip"
101
+ v-clean-tooltip="getTooltip(createLabelKey(i, j), footerItem.labelTooltip)"
44
102
  variant="ghost"
45
103
  class="app-chart-card-footer-button secondary-text-link"
46
104
  data-testid="app-chart-card-footer-item-text"
47
105
  :aria-label="t('catalog.charts.appChartCard.footerItem.ariaLabel', { filter: label })"
48
106
  @click="onClickItem(footerItem.type, label)"
107
+ @mouseenter="updateTooltipOnHover(createLabelKey(i, j), label, footerItem.labelTooltip)"
49
108
  >
50
- <template
51
- v-if="footerItem.icon"
52
- #before
53
- >
54
- <i
55
- v-clean-tooltip="t(footerItem.iconTooltip?.key)"
56
- :class="['icon', 'app-chart-card-footer-item-icon', footerItem.icon]"
57
- />
58
- </template>
59
- {{ label }}
60
- <span v-if="footerItem.labels.length > 1 && j !== footerItem.labels.length - 1">, </span>
109
+ <span
110
+ :ref="(el) => registerLabelRef(createLabelKey(i, j), el as HTMLElement)"
111
+ class="app-chart-card-footer-button-label"
112
+ >{{ label }}</span>
61
113
  </rc-button>
114
+ <span
115
+ v-if="footerItem.labels.length > 1 && j !== footerItem.labels.length - 1"
116
+ class="app-chart-card-footer-item-separator"
117
+ >,</span>
62
118
  </rc-item-card-action>
63
119
  <span
64
120
  v-else
65
- v-clean-tooltip="footerItem.labelTooltip"
121
+ :ref="(el) => registerLabelRef(createLabelKey(i, j), el as HTMLElement)"
122
+ v-clean-tooltip="getTooltip(createLabelKey(i, j), footerItem.labelTooltip)"
66
123
  class="app-chart-card-footer-item-text"
67
124
  data-testid="app-chart-card-footer-item-text"
125
+ @mouseenter="updateTooltipOnHover(createLabelKey(i, j), label, footerItem.labelTooltip)"
68
126
  >
69
127
  {{ label }}
70
- <span v-if="footerItem.labels.length > 1 && j !== footerItem.labels.length - 1">, </span>
128
+ <span
129
+ v-if="footerItem.labels.length > 1 && j !== footerItem.labels.length - 1"
130
+ class="app-chart-card-footer-item-separator"
131
+ >,</span>
71
132
  </span>
72
133
  </template>
73
134
  </div>
@@ -78,6 +139,7 @@ function onClickItem(type: string, label: string) {
78
139
  .app-chart-card-footer {
79
140
  display: flex;
80
141
  flex-wrap: wrap;
142
+ max-width: 100%;
81
143
 
82
144
  &-item {
83
145
  display: flex;
@@ -85,18 +147,31 @@ function onClickItem(type: string, label: string) {
85
147
  color: var(--link-text-secondary);
86
148
  margin-top: 8px;
87
149
  margin-right: 8px;
150
+ min-width: 0;
151
+ max-width: 100%;
152
+
153
+ &-action {
154
+ display: flex;
155
+ align-items: center;
156
+ min-width: 0; // Critical for truncation in flex containers
157
+ max-width: 100%;
158
+ }
88
159
 
89
160
  &-text {
90
- margin-right: 8px;
91
- display: -webkit-box;
92
- -webkit-line-clamp: 1;
93
- -webkit-box-orient: vertical;
161
+ display: block;
94
162
  overflow: hidden;
95
163
  text-overflow: ellipsis;
96
- word-break: break-all;
164
+ white-space: nowrap;
165
+ min-width: 0;
166
+ }
167
+
168
+ &-separator {
169
+ flex-shrink: 0;
170
+ margin-right: 4px;
97
171
  }
98
172
 
99
173
  &-icon {
174
+ flex-shrink: 0;
100
175
  width: 20px;
101
176
  height: 20px;
102
177
  display: flex;
@@ -109,6 +184,15 @@ function onClickItem(type: string, label: string) {
109
184
 
110
185
  &-button {
111
186
  text-transform: capitalize;
187
+ min-width: 0;
188
+ max-width: 100%;
189
+
190
+ &-label {
191
+ display: block;
192
+ overflow: hidden;
193
+ text-overflow: ellipsis;
194
+ white-space: nowrap;
195
+ }
112
196
  }
113
197
  }
114
198
 
@@ -321,7 +321,7 @@ export default {
321
321
  pill: chart.featured ? { label: { key: 'generic.shortFeatured' }, tooltip: { key: 'generic.featured' } } : undefined,
322
322
  header: {
323
323
  title: { text: chart.chartNameDisplay },
324
- statuses: this.addStatusesToCharts(chart),
324
+ statuses: chart.cardContent.statuses
325
325
  },
326
326
  subHeaderItems: chart.cardContent.subHeaderItems,
327
327
  image: { src: chart.latestCompatibleVersion.icon, alt: { text: this.t('catalog.charts.iconAlt', { app: get(chart, 'chartNameDisplay') }) } },
@@ -553,16 +553,6 @@ export default {
553
553
  }
554
554
  },
555
555
 
556
- addStatusesToCharts(chart) {
557
- if (this.suseAppCollectionRepo.includes(chart.repoName)) {
558
- return [{
559
- icon: 'icon-notify-info', color: 'info', tooltip: { key: 'catalog.charts.isFromSuseAppCoRepository' }
560
- }, ...chart.cardContent.statuses];
561
- }
562
-
563
- return chart.cardContent.statuses;
564
- },
565
-
566
556
  async closeSuseAppCollectionBanner() {
567
557
  this.showAppCollectionBanner = false;
568
558
  await this.$store.dispatch('prefs/set', { key: HIDE_SUSE_APP_COLLECTION_REPO_BANNER, value: true });
@@ -394,29 +394,12 @@ export default {
394
394
  },
395
395
 
396
396
  metricAggregations() {
397
- let checkNodes = this.nodes;
398
-
399
- // Special case local cluster
400
- if (this.currentCluster.isLocal) {
401
- const nodeNames = this.nodes.reduce((acc, n) => {
402
- acc[n.id] = n;
403
-
404
- return acc;
405
- }, {});
406
-
407
- checkNodes = this.mgmtNodes.filter((n) => {
408
- const nodeName = n.metadata?.labels?.['management.cattle.io/nodename'] || n.id;
409
-
410
- return !!nodeNames[nodeName];
411
- });
412
- }
413
-
414
- const someNonWorkerRoles = checkNodes.some((node) => node.hasARole && !node.isWorker);
415
397
  const metrics = this.nodeMetrics.filter((nodeMetrics) => {
416
398
  const node = this.nodes.find((nd) => nd.id === nodeMetrics.id);
417
399
 
418
- return node && (!someNonWorkerRoles || node.isWorker);
400
+ return node;
419
401
  });
402
+
420
403
  const initialAggregation = {
421
404
  cpu: 0,
422
405
  memory: 0
@@ -98,7 +98,7 @@ export default {
98
98
  },
99
99
  subHeaderItems: chart.cardContent.subHeaderItems,
100
100
  footerItems: chart.deploysOnWindows ? [{
101
- icon: 'icon-tag-alt',
101
+ icon: 'tag-alt',
102
102
  iconTooltip: { key: 'generic.tags' },
103
103
  labels: [this.t('catalog.charts.deploysOnWindows')],
104
104
  }] : [],
@@ -9,7 +9,7 @@ import {
9
9
  ID_UNLINKED,
10
10
  NAME_UNLINKED,
11
11
  } from '@shell/config/table-headers';
12
- import { allHash } from 'utils/promise';
12
+ import { allHash } from '@shell/utils/promise';
13
13
 
14
14
  export default {
15
15
  components: {
@@ -873,7 +873,7 @@ export default {
873
873
  }
874
874
 
875
875
  return labels.length ? [{
876
- icon: 'icon-tag-alt',
876
+ icon: 'tag-alt',
877
877
  iconTooltip: { key: 'generic.tags' },
878
878
  labels,
879
879
  }] : [];
@@ -11,6 +11,43 @@ function replaceAll(str, find, replace) {
11
11
  return str.split(find).join(replace);
12
12
  }
13
13
 
14
+ // Injected at the top of every generated importTypes() function.
15
+ // Ensures both $extension (newer Rancher) and $plugin (older Rancher) are available on
16
+ // Vue globalProperties, regardless of which one the host injected. This makes all
17
+ // extensions compatible across Rancher versions without any per-extension code changes.
18
+ const COMPAT_SHIM = ` if (typeof document !== 'undefined') {
19
+ var patchGlobalProps = function() {
20
+ var __vueApp = document.getElementById('app').__vue_app__;
21
+
22
+ if (!__vueApp) {
23
+ // no __vue_app__, vueApp.mount('#app') has not been called yet
24
+ return false;
25
+ }
26
+
27
+ if (__vueApp.config && __vueApp.config.globalProperties) {
28
+ var __gp = __vueApp.config.globalProperties;
29
+ if (!__gp.$extension && __gp.$plugin) { __gp.$extension = __gp.$plugin; }
30
+ else if (!__gp.$plugin && __gp.$extension) { __gp.$plugin = __gp.$extension; }
31
+ return true;
32
+ }
33
+
34
+ // Fallback to failure case
35
+ return false;
36
+ };
37
+
38
+ if (!patchGlobalProps()) {
39
+ // Could not patch, keep retrying until it works
40
+ var __retry = setInterval(function() {
41
+ if (patchGlobalProps()) {
42
+ clearInterval(__retry);
43
+ }
44
+ }, 100);
45
+
46
+ // Fallback: clear interval after 10 seconds just in case
47
+ setTimeout(function() { clearInterval(__retry); }, 10000);
48
+ }
49
+ }\n`;
50
+
14
51
  function registerFile(file, type, pkg, f) {
15
52
  const importType = (f === 'models') ? 'require' : 'import';
16
53
  const chunkName = (f === 'l10n') ? '' : `/* webpackChunkName: "${ f }" */`;
@@ -31,6 +68,8 @@ function register(file, pkg, f) {
31
68
  function generateTypeImport(pkg, dir) {
32
69
  let content = 'export function importTypes($extension) { \n';
33
70
 
71
+ content += COMPAT_SHIM;
72
+
34
73
  // Auto-import if the folder exists
35
74
  contextFolders.forEach((f) => {
36
75
  const filePath = path.join(dir, f);
@@ -79,6 +118,8 @@ function generateDynamicTypeImport(pkg, dir) {
79
118
  const template = fs.readFileSync(path.join(__dirname, 'import.js'), { encoding: 'utf8' });
80
119
  let content = 'export function importTypes($extension) { \n';
81
120
 
121
+ content += COMPAT_SHIM;
122
+
82
123
  // Auto-import if the folder exists
83
124
  contextFolders.forEach((f) => {
84
125
  if (fs.existsSync(path.join(dir, f))) {
@@ -1193,7 +1193,7 @@ export default class Resource {
1193
1193
  * Allow to handle the response of the save request
1194
1194
  * @param {*} res Full request response
1195
1195
  */
1196
- processSaveResponse(res) { }
1196
+ processSaveResponse(res, opt = {}) { }
1197
1197
 
1198
1198
  async _save(opt = { }) {
1199
1199
  const forNew = !this.id;
@@ -1280,7 +1280,7 @@ export default class Resource {
1280
1280
  const res = await this.$dispatch('request', { opt, type: this.type } );
1281
1281
 
1282
1282
  // Allow to process response independently from the related models
1283
- this.processSaveResponse(res);
1283
+ this.processSaveResponse(res, opt);
1284
1284
 
1285
1285
  // Steve sometimes returns Table responses instead of the resource you just saved.. ignore
1286
1286
  if ( res && res.kind !== 'Table') {
@@ -74,7 +74,7 @@ describe('class: Steve', () => {
74
74
 
75
75
  steve.processSaveResponse(response);
76
76
 
77
- expect(parentProcessSaveResponse).toHaveBeenCalledWith(response);
77
+ expect(parentProcessSaveResponse).toHaveBeenCalledWith(response, {});
78
78
  });
79
79
 
80
80
  describe('growl notifications', () => {
@@ -70,11 +70,11 @@ export default class SteveModel extends HybridModel {
70
70
  *
71
71
  * @param {*} res
72
72
  */
73
- processSaveResponse(res) {
74
- super.processSaveResponse(res);
73
+ processSaveResponse(res, opt = {}) {
74
+ super.processSaveResponse(res, opt);
75
75
 
76
76
  // Conditionally show the growl for autogenerated names
77
- if (res && res._status === 201 && res.metadata?.generateName && res.id) {
77
+ if (res && res._status === 201 && res.metadata?.generateName && res.id && !opt.suppressSuccessToast) {
78
78
  // Split to remove the namespace if present (default/generated-xxx)
79
79
  const nameOnly = res.id.split('/').pop();
80
80
 
@@ -15,8 +15,7 @@ import {
15
15
  INGRESS,
16
16
  WORKLOAD_TYPES,
17
17
  HPA,
18
- SECRET,
19
- EXT
18
+ SECRET
20
19
  } from '@shell/config/types';
21
20
  import { CAPI as CAPI_LAB_AND_ANO, CATTLE_PUBLIC_ENDPOINTS, STORAGE, UI_PROJECT_SECRET_COPY } from '@shell/config/labels-annotations';
22
21
  import { Schema } from '@shell/plugins/steve/schema';
@@ -768,8 +767,7 @@ export const PAGINATION_SETTINGS_STORE_DEFAULTS: PaginationSettingsStores = {
768
767
  { resource: MANAGEMENT.CLUSTER, context: ['side-bar'] },
769
768
  { resource: CATALOG.APP, context: ['branding'] },
770
769
  SECRET,
771
- CAPI.MACHINE_SET,
772
- EXT.TOKEN
770
+ CAPI.MACHINE_SET
773
771
  ],
774
772
  generic: false,
775
773
  }