@rancher/shell 3.0.2-rc.2 → 3.0.2-rc.3

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 (141) hide show
  1. package/assets/styles/base/_basic.scss +5 -7
  2. package/assets/styles/global/_button.scss +10 -0
  3. package/assets/styles/global/_tooltip.scss +2 -2
  4. package/assets/styles/themes/_dark.scss +14 -2
  5. package/assets/styles/themes/_light.scss +7 -2
  6. package/assets/styles/vendor/vue-select.scss +4 -0
  7. package/assets/translations/en-us.yaml +44 -5
  8. package/components/BannerGraphic.vue +0 -42
  9. package/components/ButtonMultiAction.vue +1 -1
  10. package/components/Carousel.vue +36 -29
  11. package/components/CommunityLinks.vue +6 -1
  12. package/components/GrowlManager.vue +9 -2
  13. package/components/LocaleSelector.vue +8 -1
  14. package/components/PaginatedResourceTable.vue +4 -7
  15. package/components/ProgressBarMulti.vue +14 -0
  16. package/components/Questions/Reference.vue +57 -28
  17. package/components/SelectIconGrid.vue +12 -1
  18. package/components/SideNav.vue +12 -38
  19. package/components/SortableTable/index.vue +1 -0
  20. package/components/Tabbed/index.vue +12 -1
  21. package/components/YamlEditor.vue +1 -0
  22. package/components/auth/Principal.vue +5 -3
  23. package/components/fleet/FleetClusters.vue +82 -1
  24. package/components/fleet/FleetRepos.vue +13 -30
  25. package/components/fleet/ForceDirectedTreeChart/index.vue +2 -2
  26. package/components/form/ChangePassword.vue +2 -0
  27. package/components/form/ColorInput.vue +24 -1
  28. package/components/form/FileSelector.vue +2 -0
  29. package/components/form/KeyValue.vue +230 -160
  30. package/components/form/LabeledSelect.vue +1 -1
  31. package/components/form/PlusMinus.vue +14 -2
  32. package/components/form/ResourceLabeledSelect.vue +13 -53
  33. package/components/form/ResourceSelector.vue +1 -0
  34. package/components/form/ResourceTabs/index.vue +79 -36
  35. package/components/form/SecretSelector.vue +2 -2
  36. package/components/form/__tests__/KeyValue.test.ts +1 -1
  37. package/components/formatter/FleetClusterSummaryGraph.vue +2 -2
  38. package/components/formatter/FleetSummaryGraph.vue +6 -7
  39. package/components/formatter/WorkloadHealthScale.vue +7 -0
  40. package/components/nav/Group.vue +30 -4
  41. package/components/nav/Header.vue +82 -114
  42. package/components/nav/HeaderPageActionMenu.vue +27 -131
  43. package/components/nav/NamespaceFilter.vue +1 -1
  44. package/components/nav/Type.vue +15 -0
  45. package/config/home-links.js +21 -13
  46. package/config/labels-annotations.js +2 -0
  47. package/config/page-actions.js +1 -0
  48. package/config/pagination-table-headers.js +15 -1
  49. package/config/product/explorer.js +7 -17
  50. package/config/table-headers.js +6 -0
  51. package/config/version.js +5 -1
  52. package/core/plugin.ts +41 -1
  53. package/core/plugins.js +125 -72
  54. package/core/types-provisioning.ts +91 -2
  55. package/core/types.ts +55 -0
  56. package/detail/__tests__/autoscaling.horizontalpodautoscaler.test.ts +12 -3
  57. package/detail/catalog.cattle.io.app.vue +1 -1
  58. package/detail/fleet.cattle.io.cluster.vue +3 -3
  59. package/detail/namespace.vue +13 -19
  60. package/detail/networking.k8s.io.ingress.vue +13 -53
  61. package/detail/provisioning.cattle.io.cluster.vue +12 -1
  62. package/detail/workload/index.vue +3 -3
  63. package/dialog/AddCustomBadgeDialog.vue +5 -1
  64. package/edit/auth/ldap/__tests__/config.test.ts +18 -0
  65. package/edit/auth/ldap/config.vue +24 -0
  66. package/edit/auth/saml.vue +8 -6
  67. package/edit/fleet.cattle.io.gitrepo.vue +7 -1
  68. package/edit/logging-flow/index.vue +4 -19
  69. package/edit/networking.k8s.io.ingress/index.vue +18 -65
  70. package/edit/networking.k8s.io.networkpolicy/index.vue +4 -5
  71. package/edit/provisioning.cattle.io.cluster/index.vue +13 -1
  72. package/edit/provisioning.cattle.io.cluster/rke2.vue +31 -115
  73. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +2 -2
  74. package/edit/provisioning.cattle.io.cluster/tabs/networking/ACE.vue +14 -28
  75. package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +25 -12
  76. package/edit/service.vue +1 -2
  77. package/list/networking.k8s.io.ingress.vue +1 -1
  78. package/list/node.vue +15 -8
  79. package/list/persistentvolume.vue +12 -4
  80. package/list/service.vue +1 -1
  81. package/list/workload.vue +4 -0
  82. package/mixins/chart.js +4 -1
  83. package/models/catalog.cattle.io.app.js +3 -1
  84. package/models/catalog.cattle.io.clusterrepo.js +56 -7
  85. package/models/fleet.cattle.io.bundle.js +0 -11
  86. package/models/fleet.cattle.io.cluster.js +17 -1
  87. package/models/fleet.cattle.io.gitrepo.js +86 -50
  88. package/models/provisioning.cattle.io.cluster.js +47 -2
  89. package/models/service.js +1 -0
  90. package/models/workload.js +19 -1
  91. package/package.json +5 -4
  92. package/pages/c/_cluster/apps/charts/index.vue +4 -0
  93. package/pages/c/_cluster/explorer/ConfigBadge.vue +8 -7
  94. package/pages/c/_cluster/explorer/index.vue +13 -6
  95. package/pages/c/_cluster/fleet/GitRepoGraphConfig.js +3 -3
  96. package/pages/c/_cluster/fleet/index.vue +75 -89
  97. package/pages/c/_cluster/settings/links.vue +2 -2
  98. package/pages/diagnostic.vue +17 -15
  99. package/pages/home.vue +32 -6
  100. package/plugins/clean-html.js +50 -0
  101. package/plugins/dashboard-store/resource-class.js +4 -0
  102. package/plugins/plugin.js +54 -49
  103. package/plugins/steve/mutations.js +1 -1
  104. package/plugins/steve/steve-class.js +8 -0
  105. package/plugins/steve/steve-pagination-utils.ts +3 -1
  106. package/rancher-components/Accordion/Accordion.vue +4 -4
  107. package/rancher-components/BadgeState/BadgeState.vue +7 -0
  108. package/rancher-components/Card/Card.vue +27 -1
  109. package/rancher-components/Form/Checkbox/Checkbox.vue +9 -2
  110. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +18 -1
  111. package/rancher-components/Form/LabeledInput/LabeledInput.vue +18 -1
  112. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +39 -2
  113. package/rancher-components/RcButton/RcButton.vue +90 -0
  114. package/rancher-components/RcButton/index.ts +2 -0
  115. package/rancher-components/RcButton/types.ts +17 -0
  116. package/rancher-components/RcDropdown/RcDropdown.vue +111 -0
  117. package/rancher-components/RcDropdown/RcDropdownItem.vue +127 -0
  118. package/rancher-components/RcDropdown/RcDropdownSeparator.vue +6 -0
  119. package/rancher-components/RcDropdown/RcDropdownTrigger.vue +43 -0
  120. package/rancher-components/RcDropdown/index.ts +4 -0
  121. package/rancher-components/RcDropdown/types.ts +22 -0
  122. package/rancher-components/RcDropdown/useDropdownCollection.ts +45 -0
  123. package/rancher-components/RcDropdown/useDropdownContext.ts +83 -0
  124. package/scripts/test-plugins-build.sh +2 -0
  125. package/scripts/typegen.sh +2 -0
  126. package/store/catalog.js +1 -1
  127. package/tsconfig.json +2 -1
  128. package/types/components/paginatedResourceTable.ts +25 -0
  129. package/types/components/resourceLabeledSelect.ts +48 -0
  130. package/types/resources/fleet.d.ts +17 -0
  131. package/types/shell/index.d.ts +61 -0
  132. package/utils/auth.js +5 -1
  133. package/utils/cluster.js +106 -0
  134. package/utils/fleet.ts +35 -3
  135. package/utils/ingress.ts +64 -0
  136. package/utils/uiplugins.ts +56 -44
  137. package/utils/validators/cron-schedule.js +7 -2
  138. package/utils/validators/formRules/__tests__/index.test.ts +53 -17
  139. package/utils/validators/formRules/index.ts +20 -5
  140. package/vue.config.js +1 -1
  141. package/components/RelatedWorkloadsTable.vue +0 -50
@@ -2,7 +2,12 @@
2
2
  import { mapState } from 'vuex';
3
3
  import { FLEET } from '@shell/config/types';
4
4
  import { WORKSPACE } from '@shell/store/prefs';
5
- import { STATES_ENUM, STATES, getStateLabel } from '@shell/plugins/dashboard-store/resource-class';
5
+ import {
6
+ getStateLabel,
7
+ primaryDisplayStatusFromCount,
8
+ STATES,
9
+ STATES_ENUM,
10
+ } from '@shell/plugins/dashboard-store/resource-class';
6
11
  import Loading from '@shell/components/Loading';
7
12
  import CollapsibleCard from '@shell/components/CollapsibleCard.vue';
8
13
  import ResourceTable from '@shell/components/ResourceTable';
@@ -37,10 +42,15 @@ export default {
37
42
  inStoreType: 'management',
38
43
  type: FLEET.CLUSTER_GROUP
39
44
  },
45
+ allBundleDeployments: {
46
+ inStoreType: 'management',
47
+ type: FLEET.BUNDLE_DEPLOYMENT,
48
+ },
40
49
  allBundles: {
41
50
  inStoreType: 'management',
42
51
  type: FLEET.BUNDLE,
43
52
  opt: { excludeFields: ['metadata.managedFields', 'spec.resources'] },
53
+ skipWait: true,
44
54
  },
45
55
  gitRepos: {
46
56
  inStoreType: 'management',
@@ -98,7 +108,6 @@ export default {
98
108
  }
99
109
  ],
100
110
  schema: {},
101
- allBundles: [],
102
111
  gitRepos: [],
103
112
  fleetWorkspacesData: [],
104
113
  isCollapsed: {},
@@ -138,14 +147,24 @@ export default {
138
147
  });
139
148
  },
140
149
  workspacesData() {
141
- return this.fleetWorkspaces.filter((ws) => ws.repos && ws.repos.length);
150
+ return this.fleetWorkspaces.filter((ws) => ws.counts.gitRepos > 0);
142
151
  },
143
152
  emptyWorkspaces() {
144
- return this.fleetWorkspaces.filter((ws) => !ws.repos || !ws.repos.length);
153
+ return this.fleetWorkspaces.filter((ws) => ws.counts.gitRepos === 0);
145
154
  },
146
155
  areAllCardsExpanded() {
147
156
  return Object.keys(this.isCollapsed).every((key) => !this.isCollapsed[key]);
148
- }
157
+ },
158
+ gitReposCounts() {
159
+ return this.gitRepos.reduce((prev, gitRepo) => {
160
+ prev[gitRepo.id] = {
161
+ bundles: gitRepo.allBundlesStatuses,
162
+ resources: gitRepo.allResourceStatuses,
163
+ };
164
+
165
+ return prev;
166
+ }, {});
167
+ },
149
168
  },
150
169
  methods: {
151
170
  setWorkspaceFilterAndLinkToGitRepo(value) {
@@ -160,108 +179,71 @@ export default {
160
179
  },
161
180
  });
162
181
  },
163
- getStatusInfo(area, row) {
182
+ getStatusInfo(area, row, rowCounts) {
164
183
  const defaultStatusInfo = {
165
184
  badgeClass: `${ STATES[STATES_ENUM.NOT_READY].color } badge-class-default`,
166
185
  icon: STATES[STATES_ENUM.NOT_READY].compoundIcon
167
186
  };
168
187
 
169
188
  // classes are defined in the themes SASS files...
170
- return this.getBadgeClassAndIcon(area, row) || defaultStatusInfo;
189
+ return this.getBadgeClassAndIcon(area, row, rowCounts) || defaultStatusInfo;
171
190
  },
172
- getBadgeClassAndIcon(area, row) {
173
- let group;
174
-
191
+ getBadgeClassAndIcon(area, row, rowCounts) {
175
192
  if (!this.admissableAreas.includes(area)) {
176
193
  return false;
177
194
  }
178
195
 
196
+ let group;
197
+
179
198
  if (area === 'clusters') {
180
- if (row.clusterInfo?.ready === row.clusterInfo?.total && row.clusterInfo?.ready) {
181
- return {
182
- badgeClass: STATES[STATES_ENUM.ACTIVE].color,
183
- icon: STATES[STATES_ENUM.ACTIVE].compoundIcon
184
- };
185
- }
186
- } else if (area === 'bundles') {
187
- group = row.bundles;
188
- } else if (area === 'resources') {
189
- group = row.status?.resources;
190
- }
199
+ const clusterInfo = row.clusterInfo;
200
+ const state = clusterInfo.ready === clusterInfo.total ? STATES_ENUM.ACTIVE : STATES_ENUM.NOT_READY;
191
201
 
192
- if (group?.length && group?.every((item) => item.state?.toLowerCase() === STATES_ENUM.ACTIVE)) {
193
- return {
194
- badgeClass: STATES[STATES_ENUM.ACTIVE].color ? STATES[STATES_ENUM.ACTIVE].color : `${ STATES[STATES_ENUM.UNKNOWN].color } bg-unmapped-state`,
195
- icon: STATES[STATES_ENUM.ACTIVE].compoundIcon ? STATES[STATES_ENUM.ACTIVE].compoundIcon : `${ STATES[STATES_ENUM.UNKNOWN].compoundIcon } unmapped-icon`
196
- };
197
- }
198
- if (group?.length && group?.some((item) => item.state?.toLowerCase() === STATES_ENUM.ERR_APPLIED)) {
199
202
  return {
200
- badgeClass: STATES[STATES_ENUM.ERR_APPLIED].color ? STATES[STATES_ENUM.ERR_APPLIED].color : `${ STATES[STATES_ENUM.UNKNOWN].color } bg-unmapped-state`,
201
- icon: STATES[STATES_ENUM.ERR_APPLIED].compoundIcon ? STATES[STATES_ENUM.ERR_APPLIED].compoundIcon : `${ STATES[STATES_ENUM.UNKNOWN].compoundIcon } unmapped-icon`
203
+ badgeClass: `${ STATES[state].color } badge-class-area-${ area }`,
204
+ icon: STATES[state].compoundIcon
202
205
  };
206
+ } else if (area === 'bundles') {
207
+ group = rowCounts[row.id].bundles;
208
+ } else if (area === 'resources') {
209
+ group = rowCounts[row.id].resources;
210
+ } else {
211
+ // unreachable
212
+ return false;
203
213
  }
204
- if (group?.length && group?.some((item) => item.state?.toLowerCase() === STATES_ENUM.NOT_READY)) {
214
+
215
+ if (group.total === group.states.ready) {
205
216
  return {
206
- badgeClass: STATES[STATES_ENUM.NOT_READY].color ? STATES[STATES_ENUM.NOT_READY].color : `${ STATES[STATES_ENUM.UNKNOWN].color } bg-unmapped-state`,
207
- icon: STATES[STATES_ENUM.NOT_READY].compoundIcon ? STATES[STATES_ENUM.NOT_READY].compoundIcon : `${ STATES[STATES_ENUM.UNKNOWN].compoundIcon } unmapped-icon`
217
+ badgeClass: STATES[STATES_ENUM.ACTIVE].color,
218
+ icon: STATES[STATES_ENUM.ACTIVE].compoundIcon,
208
219
  };
209
220
  }
210
-
211
- if (area === 'resources') {
212
- if (row.status?.resourceCounts?.desiredReady === row.status?.resourceCounts?.ready && row.status?.resourceCounts?.desiredReady) {
213
- return {
214
- badgeClass: STATES[STATES_ENUM.ACTIVE].color,
215
- icon: STATES[STATES_ENUM.ACTIVE].compoundIcon
216
- };
217
- }
218
- }
221
+ const state = primaryDisplayStatusFromCount(group.states);
219
222
 
220
223
  return {
221
- badgeClass: `${ STATES[STATES_ENUM.NOT_READY].color } badge-class-area-${ area }`,
222
- icon: STATES[STATES_ENUM.NOT_READY].compoundIcon
224
+ badgeClass: STATES[state].color ? STATES[state].color : `${ STATES[STATES_ENUM.UNKNOWN].color } bg-unmapped-state`,
225
+ icon: STATES[state].compoundIcon ? STATES[state].compoundIcon : `${ STATES[STATES_ENUM.UNKNOWN].compoundIcon } unmapped-icon`
223
226
  };
224
227
  },
225
- getTooltipInfo(area, row) {
226
- let group;
227
-
228
+ getTooltipInfo(area, row, rowCounts) {
228
229
  if (!this.admissableAreas.includes(area)) {
229
230
  return {};
230
231
  }
231
232
 
232
- if (area === 'clusters') {
233
- group = '';
234
- } else if (area === 'bundles') {
235
- group = row.bundles;
233
+ if (area === 'bundles') {
234
+ return this.generateTooltipData(rowCounts[row.id].bundles.states);
236
235
  } else if (area === 'resources') {
237
- group = row.status?.resources;
238
- }
239
-
240
- if (group?.length) {
241
- return this.generateTooltipData(group);
236
+ return this.generateTooltipData(rowCounts[row.id].resources.states);
242
237
  }
243
238
 
244
239
  return '';
245
240
  },
246
- generateTooltipData(data) {
247
- const infoObj = {};
248
- let tooltipData = '';
249
-
250
- data.forEach((item) => {
251
- if (!infoObj[item.state]) {
252
- infoObj[item.state] = 0;
253
- }
254
-
255
- infoObj[item.state]++;
256
- });
257
-
258
- Object.keys(infoObj).forEach((key) => {
259
- tooltipData += `${ getStateLabel(key) }: ${ infoObj[key] }<br>`;
260
- });
261
-
262
- return tooltipData;
241
+ generateTooltipData(infoObj) {
242
+ return Object.keys(infoObj)
243
+ .filter((key) => infoObj[key] > 0) // filter zero values
244
+ .map((key) => `${ getStateLabel(key) }: ${ infoObj[key] }<br>`).join('');
263
245
  },
264
- getBadgeValue(area, row) {
246
+ getBadgeValue(area, row, rowCounts) {
265
247
  let value;
266
248
 
267
249
  if (!this.admissableAreas.includes(area)) {
@@ -269,11 +251,15 @@ export default {
269
251
  }
270
252
 
271
253
  if (area === 'clusters') {
272
- return `${ row.clusterInfo.ready }/${ row.clusterInfo.total }`;
254
+ value = `${ row.clusterInfo.ready }/${ row.clusterInfo.total }`;
273
255
  } else if (area === 'bundles') {
274
- value = xOfy(row.bundlesReady?.length, row.bundles?.length);
256
+ const bundles = rowCounts[row.id].bundles;
257
+
258
+ value = xOfy(bundles.states.ready || 0, bundles.total);
275
259
  } else if (area === 'resources') {
276
- value = xOfy(row.status?.resourceCounts?.ready, row.status?.resourceCounts?.desiredReady);
260
+ const resources = rowCounts[row.id].resources;
261
+
262
+ value = xOfy(resources.states.ready || 0, resources.total);
277
263
  }
278
264
 
279
265
  return value;
@@ -413,10 +399,10 @@ export default {
413
399
  <CompoundStatusBadge
414
400
  v-else
415
401
  data-testid="clusters-ready"
416
- :tooltip-text="getTooltipInfo('clusters', row)"
417
- :badge-class="getStatusInfo('clusters', row).badgeClass"
418
- :icon="getStatusInfo('clusters', row).icon"
419
- :value="getBadgeValue('clusters', row)"
402
+ :tooltip-text="getTooltipInfo('clusters', row, gitReposCounts)"
403
+ :badge-class="getStatusInfo('clusters', row, gitReposCounts).badgeClass"
404
+ :icon="getStatusInfo('clusters', row, gitReposCounts).icon"
405
+ :value="getBadgeValue('clusters', row, gitReposCounts)"
420
406
  />
421
407
  </template>
422
408
  <template #cell:bundlesReady="{row}">
@@ -424,19 +410,19 @@ export default {
424
410
  <CompoundStatusBadge
425
411
  v-else
426
412
  data-testid="bundles-ready"
427
- :tooltip-text="getTooltipInfo('bundles', row)"
428
- :badge-class="getStatusInfo('bundles', row).badgeClass"
429
- :icon="getStatusInfo('bundles', row).icon"
430
- :value="getBadgeValue('bundles', row)"
413
+ :tooltip-text="getTooltipInfo('bundles', row, gitReposCounts)"
414
+ :badge-class="getStatusInfo('bundles', row, gitReposCounts).badgeClass"
415
+ :icon="getStatusInfo('bundles', row, gitReposCounts).icon"
416
+ :value="getBadgeValue('bundles', row, gitReposCounts)"
431
417
  />
432
418
  </template>
433
419
  <template #cell:resourcesReady="{row}">
434
420
  <CompoundStatusBadge
435
421
  data-testid="resources-ready"
436
- :tooltip-text="getTooltipInfo('resources', row)"
437
- :badge-class="getStatusInfo('resources', row).badgeClass"
438
- :icon="getStatusInfo('resources', row).icon"
439
- :value="getBadgeValue('resources', row)"
422
+ :tooltip-text="getTooltipInfo('resources', row, gitReposCounts)"
423
+ :badge-class="getStatusInfo('resources', row, gitReposCounts).badgeClass"
424
+ :icon="getStatusInfo('resources', row, gitReposCounts).icon"
425
+ :value="getBadgeValue('resources', row, gitReposCounts)"
440
426
  />
441
427
  </template>
442
428
 
@@ -10,7 +10,7 @@ import KeyValue from '@shell/components/form/KeyValue';
10
10
  import { mapGetters } from 'vuex';
11
11
  import { isRancherPrime } from '@shell/config/version';
12
12
  import DefaultLinksEditor from './DefaultLinksEditor';
13
- import { CUSTOM_LINKS_COLLECTIVE_VERSION, fetchLinks } from '@shell/config/home-links';
13
+ import { CUSTOM_LINKS_APP_CO_VERSION, fetchLinks } from '@shell/config/home-links';
14
14
  import TabTitle from '@shell/components/TabTitle';
15
15
 
16
16
  export default {
@@ -47,7 +47,7 @@ export default {
47
47
 
48
48
  allValues() {
49
49
  return {
50
- version: CUSTOM_LINKS_COLLECTIVE_VERSION,
50
+ version: CUSTOM_LINKS_APP_CO_VERSION,
51
51
  defaults: this.value.defaults.filter((obj) => obj.enabled).map((obj) => obj.key),
52
52
  custom: this.value.custom
53
53
  };
@@ -346,21 +346,23 @@ export default {
346
346
  class="full-width"
347
347
  >
348
348
  <thead @click="toggleTable(cluster.id)">
349
- <th colspan="4">
350
- <div class="cluster-row">
351
- <span>Cluster: <b>{{ cluster.name }}</b></span>
352
- <span>Namespace: <b>{{ cluster.namespace }}</b></span>
353
- <span>Total Resources: <b>{{ sumResourceCount(cluster.counts) }}</b></span>
354
- <span>Nodes: <b>{{ nodeCount(cluster.counts) }}</b></span>
355
- <i
356
- class="icon"
357
- :class="{
358
- 'icon-chevron-down': !cluster.isTableVisible,
359
- 'icon-chevron-up': cluster.isTableVisible
360
- }"
361
- />
362
- </div>
363
- </th>
349
+ <tr>
350
+ <th colspan="4">
351
+ <div class="cluster-row">
352
+ <span>Cluster: <b>{{ cluster.name }}</b></span>
353
+ <span>Namespace: <b>{{ cluster.namespace }}</b></span>
354
+ <span>Total Resources: <b>{{ sumResourceCount(cluster.counts) }}</b></span>
355
+ <span>Nodes: <b>{{ nodeCount(cluster.counts) }}</b></span>
356
+ <i
357
+ class="icon"
358
+ :class="{
359
+ 'icon-chevron-down': !cluster.isTableVisible,
360
+ 'icon-chevron-up': cluster.isTableVisible
361
+ }"
362
+ />
363
+ </div>
364
+ </th>
365
+ </tr>
364
366
  </thead>
365
367
  <tbody v-show="cluster.isTableVisible">
366
368
  <tr>
package/pages/home.vue CHANGED
@@ -4,7 +4,7 @@ import { mapPref, AFTER_LOGIN_ROUTE, READ_WHATS_NEW, HIDE_HOME_PAGE_CARDS } from
4
4
  import { Banner } from '@components/Banner';
5
5
  import BannerGraphic from '@shell/components/BannerGraphic.vue';
6
6
  import IndentedPanel from '@shell/components/IndentedPanel.vue';
7
- import PaginatedResourceTable, { FetchPageSecondaryResourcesOpts, FetchSecondaryResourcesOpts } from '@shell/components/PaginatedResourceTable.vue';
7
+ import PaginatedResourceTable from '@shell/components/PaginatedResourceTable.vue';
8
8
  import { BadgeState } from '@components/BadgeState';
9
9
  import CommunityLinks from '@shell/components/CommunityLinks.vue';
10
10
  import SingleClusterInfo from '@shell/components/SingleClusterInfo.vue';
@@ -23,11 +23,12 @@ import { filterHiddenLocalCluster, filterOnlyKubernetesClusters, paginationFilte
23
23
  import TabTitle from '@shell/components/TabTitle.vue';
24
24
  import { ActionFindPageArgs } from '@shell/types/store/dashboard-store.types';
25
25
 
26
- import { RESET_CARDS_ACTION, SET_LOGIN_ACTION } from '@shell/config/page-actions';
26
+ import { RESET_CARDS_ACTION, SET_LOGIN_ACTION, SHOW_HIDE_BANNER_ACTION } from '@shell/config/page-actions';
27
27
  import { STEVE_NAME_COL, STEVE_STATE_COL } from '@shell/config/pagination-table-headers';
28
28
  import { PaginationParamFilter, FilterArgs, PaginationFilterField, PaginationArgs } from '@shell/types/store/pagination.types';
29
29
  import ProvCluster from '@shell/models/provisioning.cattle.io.cluster';
30
30
  import { sameContents } from '@shell/utils/array';
31
+ import { PagTableFetchPageSecondaryResourcesOpts, PagTableFetchSecondaryResourcesOpts, PagTableFetchSecondaryResourcesReturns } from '@shell/types/components/paginatedResourceTable';
31
32
 
32
33
  export default defineComponent({
33
34
  name: 'Home',
@@ -56,6 +57,10 @@ export default defineComponent({
56
57
  action: SET_LOGIN_ACTION
57
58
  },
58
59
  { separator: true },
60
+ {
61
+ labelKey: 'nav.header.showHideBanner',
62
+ action: SHOW_HIDE_BANNER_ACTION
63
+ },
59
64
  {
60
65
  labelKey: 'nav.header.restoreCards',
61
66
  action: RESET_CARDS_ACTION
@@ -238,9 +243,9 @@ export default defineComponent({
238
243
 
239
244
  methods: {
240
245
  /**
241
- * Of type FetchSecondaryResources
246
+ * Of type PagTableFetchSecondaryResources
242
247
  */
243
- fetchSecondaryResources(opts: FetchSecondaryResourcesOpts): Promise<any> {
248
+ fetchSecondaryResources(opts: PagTableFetchSecondaryResourcesOpts): PagTableFetchSecondaryResourcesReturns {
244
249
  if (opts.canPaginate) {
245
250
  return Promise.resolve({});
246
251
  }
@@ -271,7 +276,7 @@ export default defineComponent({
271
276
 
272
277
  async fetchPageSecondaryResources({
273
278
  canPaginate, force, page, pagResult
274
- }: FetchPageSecondaryResourcesOpts) {
279
+ }: PagTableFetchPageSecondaryResourcesOpts) {
275
280
  if (!canPaginate || !page?.length) {
276
281
  this.clusterCount = 0;
277
282
 
@@ -367,6 +372,10 @@ export default defineComponent({
367
372
  this.resetCards();
368
373
  break;
369
374
 
375
+ case SHOW_HIDE_BANNER_ACTION:
376
+ this.toggleBanner();
377
+ break;
378
+
370
379
  case SET_LOGIN_ACTION:
371
380
  this.afterLoginRoute = 'home';
372
381
  break;
@@ -406,10 +415,27 @@ export default defineComponent({
406
415
  },
407
416
 
408
417
  async resetCards() {
409
- await this.$store.dispatch('prefs/set', { key: HIDE_HOME_PAGE_CARDS, value: {} });
418
+ const value = this.$store.getters['prefs/get'](HIDE_HOME_PAGE_CARDS) || {};
419
+
420
+ delete value.setLoginPage;
421
+
422
+ await this.$store.dispatch('prefs/set', { key: HIDE_HOME_PAGE_CARDS, value });
423
+
410
424
  await this.$store.dispatch('prefs/set', { key: READ_WHATS_NEW, value: '' });
411
425
  },
412
426
 
427
+ async toggleBanner() {
428
+ const value = this.$store.getters['prefs/get'](HIDE_HOME_PAGE_CARDS) || {};
429
+
430
+ if (value.welcomeBanner) {
431
+ delete value.welcomeBanner;
432
+ } else {
433
+ value.welcomeBanner = true;
434
+ }
435
+
436
+ await this.$store.dispatch('prefs/set', { key: HIDE_HOME_PAGE_CARDS, value });
437
+ },
438
+
413
439
  async closeSetLoginBanner(retry = 0) {
414
440
  let value = this.$store.getters['prefs/get'](HIDE_HOME_PAGE_CARDS);
415
441
 
@@ -30,6 +30,8 @@ const ALLOWED_TAGS = [
30
30
  'blockquote'
31
31
  ];
32
32
 
33
+ let linkInterceptors = [];
34
+
33
35
  // Allow 'A' tags to keep the target=_blank attribute if they have it
34
36
  DOMPurify.addHook('uponSanitizeAttribute', (node, data) => {
35
37
  if (node.tagName === 'A' && data.attrName === 'target' && data.attrValue === '_blank') {
@@ -46,8 +48,56 @@ DOMPurify.addHook('afterSanitizeAttributes', (node) => {
46
48
 
47
49
  node.setAttribute('rel', combined.join(' '));
48
50
  }
51
+
52
+ if (node.tagName === 'A' && linkInterceptors.length) {
53
+ let link = node.href;
54
+
55
+ // Allow each interceptor to modify the link href
56
+ link = processLink(link);
57
+
58
+ // If the link is different from the original update the href
59
+ if (link !== node.href) {
60
+ node.href = link;
61
+ }
62
+ }
49
63
  });
50
64
 
51
65
  export const purifyHTML = (value, options = { ALLOWED_TAGS }) => {
52
66
  return DOMPurify.sanitize(value, options);
53
67
  };
68
+
69
+ // Link Interceptors are typically used to allow different doc links to be used
70
+
71
+ export function addLinkInterceptor(fn, name) {
72
+ // Check the arg is not undefined and is a function
73
+ if (fn && typeof fn === 'function') {
74
+ linkInterceptors.push(fn);
75
+ } else {
76
+ if (name) {
77
+ console.error(`Invalid link interceptor function for ${ name }`); // eslint-disable-line no-console
78
+ } else {
79
+ console.error('Invalid link interceptor function'); // eslint-disable-line no-console
80
+ }
81
+ }
82
+ }
83
+
84
+ export function removeLinkInterceptor(fn) {
85
+ linkInterceptors = linkInterceptors.filter((item) => item !== fn);
86
+ }
87
+
88
+ /**
89
+ * Process a link through all of the link interceptors
90
+ */
91
+ export function processLink(link) {
92
+ // Allow each interceptor to modify the link href
93
+ for (let i = 0; i < linkInterceptors.length; i++) {
94
+ const updated = linkInterceptors[i](link);
95
+
96
+ // If a value if returned, use that in place of the original value
97
+ if (updated) {
98
+ link = updated;
99
+ }
100
+ }
101
+
102
+ return link;
103
+ }
@@ -604,6 +604,10 @@ export default class Resource {
604
604
  return this.$ctx.rootState;
605
605
  }
606
606
 
607
+ get '$plugin'() {
608
+ return this.$ctx.rootState?.$plugin;
609
+ }
610
+
607
611
  get customValidationRules() {
608
612
  return [
609
613
  /**
package/plugins/plugin.js CHANGED
@@ -1,4 +1,4 @@
1
- // This plugin loads any UI Plugins at app load time
1
+ // This plugin loads any UI Extensions at app load time
2
2
  import { allHashSettled } from '@shell/utils/promise';
3
3
  import { shouldNotLoadPlugin, UI_PLUGIN_BASE_URL } from '@shell/config/uiplugins';
4
4
  import { getKubeVersionData, getVersionData } from '@shell/config/version';
@@ -11,14 +11,14 @@ export default async function(context) {
11
11
 
12
12
  const hash = {};
13
13
 
14
- // Provide a mechanism to load the UI without the plugins loaded - in case there is a problem
14
+ // Provide a mechanism to load the UI without the extensions loaded - in case there is a problem
15
15
  let loadPlugins = true;
16
16
 
17
17
  const queryKeys = Object.keys(context.route?.query || {}).map((q) => q.toLowerCase());
18
18
 
19
19
  if (queryKeys.includes('safemode')) {
20
20
  loadPlugins = false;
21
- console.warn('Safe Mode - plugins will not be loaded'); // eslint-disable-line no-console
21
+ console.warn('Safe Mode - extensions will not be loaded'); // eslint-disable-line no-console
22
22
  setTimeout(() => {
23
23
  context.store.dispatch('growl/success', {
24
24
  title: context.store.getters['i18n/t']('plugins.safeMode.title'),
@@ -27,62 +27,67 @@ export default async function(context) {
27
27
  }, 1000);
28
28
  }
29
29
 
30
+ const fetches = { versions: versions.fetch(context) };
31
+
32
+ // If we are loading extensions then add the API fetch for the list of extensions to the fetches we will make
30
33
  if (loadPlugins) {
31
- // Fetch list of installed plugins from endpoint
32
- try {
33
- const res = await allHashSettled({
34
- versions: versions.fetch(context),
35
- plugins: context.store.dispatch('management/request', {
36
- url: `${ UI_PLUGIN_BASE_URL }`,
37
- method: 'GET',
38
- headers: { accept: 'application/json' },
39
- redirectUnauthorized: false,
40
- })
41
- });
42
-
43
- if (res.plugins.status === 'rejected') {
44
- throw new Error(res.reason);
45
- }
34
+ fetches.plugins = context.store.dispatch('management/request', {
35
+ url: `${ UI_PLUGIN_BASE_URL }`,
36
+ method: 'GET',
37
+ headers: { accept: 'application/json' },
38
+ redirectUnauthorized: false,
39
+ });
40
+ }
46
41
 
47
- const kubeVersion = getKubeVersionData()?.gitVersion;
48
- const rancherVersion = getVersionData().Version;
49
-
50
- const plugins = res.plugins.value;
51
- const entries = plugins.entries || plugins.Entries || {};
52
-
53
- Object.values(entries).forEach((plugin) => {
54
- const shouldNotLoad = shouldNotLoadPlugin(plugin, { rancherVersion, kubeVersion }, context.store.getters['uiplugins/plugins'] || []); // Error key string or boolean
55
-
56
- if (!shouldNotLoad) {
57
- hash[plugin.name] = context.$plugin.loadPluginAsync(plugin);
58
- } else {
59
- context.store.dispatch('uiplugins/setError', { name: plugin.name, error: shouldNotLoad });
60
- }
61
- });
62
- } catch (e) {
63
- if (e?.code === 404) {
64
- // Not found, so extensions operator probably not installed
65
- console.log('Could not load UI Extensions list (Extensions Operator may not be installed)'); // eslint-disable-line no-console
66
- } else {
67
- console.error('Could not load UI Extensions list', e); // eslint-disable-line no-console
68
- }
42
+ // Fetch list of installed extensions from the extensions endpoint if needed and the version information
43
+ try {
44
+ const res = await allHashSettled(fetches);
45
+
46
+ // Initialize the built-in extensions now - this is now done here so that built-in extensions get the same, correct environment data (version etc)
47
+ context.$plugin.loadBuiltinExtensions();
48
+
49
+ if (res.plugins?.status === 'rejected') {
50
+ throw new Error(res.reason);
69
51
  }
70
52
 
71
- // Load all of the plugins
72
- const pluginLoads = await allHashSettled(hash);
53
+ const kubeVersion = getKubeVersionData()?.gitVersion;
54
+ const rancherVersion = getVersionData().Version;
73
55
 
74
- // Some pluigns may have failed to load - store this
75
- Object.keys(pluginLoads).forEach((name) => {
76
- const res = pluginLoads[name];
56
+ const plugins = res.plugins?.value || {};
57
+ const entries = plugins.entries || plugins.Entries || {};
77
58
 
78
- if (res?.status === 'rejected') {
79
- console.error(`Failed to load plugin: ${ name }. `, res.reason || 'Unknown reason'); // eslint-disable-line no-console
59
+ Object.values(entries).forEach((plugin) => {
60
+ const shouldNotLoad = shouldNotLoadPlugin(plugin, { rancherVersion, kubeVersion }, context.store.getters['uiplugins/plugins'] || []); // Error key string or boolean
80
61
 
81
- // Record error in the uiplugins store, so that we can show this to the user
82
- context.store.dispatch('uiplugins/setError', { name, error: 'plugins.error.load' }); // i18n-uses plugins.error.load
62
+ if (!shouldNotLoad) {
63
+ hash[plugin.name] = context.$plugin.loadPluginAsync(plugin);
64
+ } else {
65
+ context.store.dispatch('uiplugins/setError', { name: plugin.name, error: shouldNotLoad });
83
66
  }
84
67
  });
68
+ } catch (e) {
69
+ if (e?.code === 404) {
70
+ // Not found, so extensions operator probably not installed
71
+ console.log('Could not load UI Extensions list (Extensions Operator may not be installed)'); // eslint-disable-line no-console
72
+ } else {
73
+ console.error('Could not load UI Extensions list', e); // eslint-disable-line no-console
74
+ }
85
75
  }
86
76
 
77
+ // Load all of the extensions
78
+ const pluginLoads = await allHashSettled(hash);
79
+
80
+ // Some extensions may have failed to load - store this
81
+ Object.keys(pluginLoads).forEach((name) => {
82
+ const res = pluginLoads[name];
83
+
84
+ if (res?.status === 'rejected') {
85
+ console.error(`Failed to load extension: ${ name }. `, res.reason || 'Unknown reason'); // eslint-disable-line no-console
86
+
87
+ // Record error in the uiplugins store, so that we can show this to the user
88
+ context.store.dispatch('uiplugins/setError', { name, error: 'plugins.error.load' }); // i18n-uses plugins.error.load
89
+ }
90
+ });
91
+
87
92
  return true;
88
93
  }
@@ -31,7 +31,7 @@ function registerNamespace(state, namespace) {
31
31
  }
32
32
 
33
33
  /**
34
- * update the podsByNamespace cache with new or changed pods
34
+ * update the podsByNamespace cache with new or changed pods.
35
35
  */
36
36
  function updatePodsByNamespaceCache(state, ctx, pods, loadAll) {
37
37
  if (loadAll) {
@@ -2,6 +2,7 @@ import { DESCRIPTION } from '@shell/config/labels-annotations';
2
2
  import HybridModel from './hybrid-class';
3
3
  import { NEVER_ADD } from '@shell/utils/create-yaml';
4
4
  import { deleteProperty } from '@shell/utils/object';
5
+ import { EXT_IDS } from '@shell/core/plugin';
5
6
 
6
7
  // Some fields that are removed for YAML (NEVER_ADD) are required via API
7
8
  const STEVE_ADD = [
@@ -41,6 +42,13 @@ export default class SteveModel extends HybridModel {
41
42
  this._description = value;
42
43
  }
43
44
 
45
+ /**
46
+ * Get all model extensions for this model
47
+ */
48
+ get modelExtensions() {
49
+ return this.$plugin.getDynamic(EXT_IDS.MODEL_EXTENSION, this.type) || [];
50
+ }
51
+
44
52
  cleanForSave(data, forNew) {
45
53
  const val = super.cleanForSave(data);
46
54