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

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 (172) hide show
  1. package/assets/styles/base/_basic.scss +7 -8
  2. package/assets/styles/global/_button.scss +10 -0
  3. package/assets/styles/global/_form.scss +2 -1
  4. package/assets/styles/global/_tooltip.scss +2 -2
  5. package/assets/styles/themes/_dark.scss +15 -3
  6. package/assets/styles/themes/_light.scss +7 -2
  7. package/assets/styles/vendor/vue-select.scss +4 -0
  8. package/assets/translations/en-us.yaml +66 -9
  9. package/assets/translations/zh-hans.yaml +2 -3
  10. package/components/AppModal.vue +50 -0
  11. package/components/BannerGraphic.vue +0 -42
  12. package/components/ButtonMultiAction.vue +1 -1
  13. package/components/Carousel.vue +88 -74
  14. package/components/CommunityLinks.vue +6 -1
  15. package/components/CopyToClipboardText.vue +3 -0
  16. package/components/Dialog.vue +20 -1
  17. package/components/GrowlManager.vue +9 -2
  18. package/components/LocaleSelector.vue +8 -1
  19. package/components/PaginatedResourceTable.vue +4 -7
  20. package/components/ProgressBarMulti.vue +14 -0
  21. package/components/PromptChangePassword.vue +3 -0
  22. package/components/Questions/Reference.vue +57 -28
  23. package/components/ResourceDetail/Masthead.vue +1 -1
  24. package/components/SelectIconGrid.vue +12 -1
  25. package/components/SideNav.vue +12 -38
  26. package/components/SortableTable/index.vue +1 -0
  27. package/components/Tabbed/index.vue +9 -1
  28. package/components/YamlEditor.vue +1 -0
  29. package/components/__tests__/Carousel.test.ts +56 -27
  30. package/components/auth/Principal.vue +5 -3
  31. package/components/fleet/FleetClusters.vue +82 -1
  32. package/components/fleet/FleetRepos.vue +13 -30
  33. package/components/fleet/ForceDirectedTreeChart/index.vue +2 -2
  34. package/components/form/ChangePassword.vue +2 -0
  35. package/components/form/ColorInput.vue +24 -1
  36. package/components/form/FileSelector.vue +2 -0
  37. package/components/form/KeyValue.vue +230 -160
  38. package/components/form/LabeledSelect.vue +2 -2
  39. package/components/form/PlusMinus.vue +14 -2
  40. package/components/form/ResourceLabeledSelect.vue +13 -53
  41. package/components/form/ResourceSelector.vue +1 -0
  42. package/components/form/ResourceTabs/index.vue +79 -36
  43. package/components/form/SSHKnownHosts/KnownHostsEditDialog.vue +192 -0
  44. package/components/form/SSHKnownHosts/__tests__/KnownHostsEditDialog.test.ts +104 -0
  45. package/components/form/SSHKnownHosts/index.vue +101 -0
  46. package/components/form/SecretSelector.vue +2 -2
  47. package/components/form/Select.vue +1 -1
  48. package/components/form/SelectOrCreateAuthSecret.vue +43 -11
  49. package/components/form/__tests__/KeyValue.test.ts +1 -1
  50. package/components/form/__tests__/SSHKnownHosts.test.ts +59 -0
  51. package/components/formatter/FleetClusterSummaryGraph.vue +2 -2
  52. package/components/formatter/FleetSummaryGraph.vue +6 -7
  53. package/components/formatter/WorkloadHealthScale.vue +7 -0
  54. package/components/nav/Group.vue +30 -4
  55. package/components/nav/Header.vue +82 -114
  56. package/components/nav/HeaderPageActionMenu.vue +27 -131
  57. package/components/nav/NamespaceFilter.vue +1 -1
  58. package/components/nav/Type.vue +15 -0
  59. package/composables/focusTrap.ts +68 -0
  60. package/config/home-links.js +21 -13
  61. package/config/labels-annotations.js +2 -0
  62. package/config/page-actions.js +1 -0
  63. package/config/pagination-table-headers.js +15 -1
  64. package/config/product/explorer.js +7 -17
  65. package/config/table-headers.js +6 -0
  66. package/config/version.js +5 -1
  67. package/core/plugin.ts +41 -1
  68. package/core/plugins.js +125 -72
  69. package/core/types-provisioning.ts +91 -2
  70. package/core/types.ts +55 -0
  71. package/detail/__tests__/autoscaling.horizontalpodautoscaler.test.ts +12 -3
  72. package/detail/catalog.cattle.io.app.vue +1 -1
  73. package/detail/fleet.cattle.io.cluster.vue +3 -3
  74. package/detail/namespace.vue +13 -19
  75. package/detail/networking.k8s.io.ingress.vue +13 -53
  76. package/detail/provisioning.cattle.io.cluster.vue +12 -1
  77. package/detail/secret.vue +25 -0
  78. package/detail/workload/index.vue +3 -3
  79. package/dialog/AddCustomBadgeDialog.vue +5 -1
  80. package/edit/auth/ldap/__tests__/config.test.ts +18 -0
  81. package/edit/auth/ldap/config.vue +24 -0
  82. package/edit/auth/saml.vue +8 -6
  83. package/edit/fleet.cattle.io.gitrepo.vue +34 -23
  84. package/edit/logging-flow/index.vue +4 -19
  85. package/edit/networking.k8s.io.ingress/index.vue +18 -65
  86. package/edit/networking.k8s.io.networkpolicy/index.vue +4 -5
  87. package/edit/provisioning.cattle.io.cluster/index.vue +27 -8
  88. package/edit/provisioning.cattle.io.cluster/rke2.vue +31 -115
  89. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +2 -2
  90. package/edit/provisioning.cattle.io.cluster/tabs/networking/ACE.vue +14 -28
  91. package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +25 -12
  92. package/edit/secret/index.vue +1 -1
  93. package/edit/secret/ssh.vue +21 -3
  94. package/edit/service.vue +1 -2
  95. package/list/networking.k8s.io.ingress.vue +1 -1
  96. package/list/node.vue +15 -8
  97. package/list/persistentvolume.vue +12 -4
  98. package/list/provisioning.cattle.io.cluster.vue +1 -0
  99. package/list/service.vue +1 -1
  100. package/list/workload.vue +4 -0
  101. package/mixins/chart.js +4 -1
  102. package/models/catalog.cattle.io.app.js +3 -1
  103. package/models/catalog.cattle.io.clusterrepo.js +56 -7
  104. package/models/fleet.cattle.io.bundle.js +0 -11
  105. package/models/fleet.cattle.io.cluster.js +17 -1
  106. package/models/fleet.cattle.io.gitrepo.js +88 -52
  107. package/models/provisioning.cattle.io.cluster.js +36 -1
  108. package/models/secret.js +5 -0
  109. package/models/service.js +1 -0
  110. package/models/workload.js +19 -1
  111. package/package.json +5 -4
  112. package/pages/account/index.vue +4 -0
  113. package/pages/c/_cluster/apps/charts/index.vue +4 -0
  114. package/pages/c/_cluster/explorer/ConfigBadge.vue +4 -2
  115. package/pages/c/_cluster/explorer/index.vue +13 -6
  116. package/pages/c/_cluster/fleet/GitRepoGraphConfig.js +3 -3
  117. package/pages/c/_cluster/fleet/index.vue +75 -89
  118. package/pages/c/_cluster/settings/links.vue +2 -2
  119. package/pages/c/_cluster/uiplugins/AddExtensionRepos.vue +3 -1
  120. package/pages/c/_cluster/uiplugins/CatalogList/CatalogLoadDialog.vue +3 -0
  121. package/pages/c/_cluster/uiplugins/CatalogList/CatalogUninstallDialog.vue +7 -1
  122. package/pages/c/_cluster/uiplugins/CatalogList/index.vue +3 -1
  123. package/pages/c/_cluster/uiplugins/DeveloperInstallDialog.vue +10 -7
  124. package/pages/c/_cluster/uiplugins/InstallDialog.vue +7 -0
  125. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +181 -106
  126. package/pages/c/_cluster/uiplugins/SetupUIPlugins.vue +2 -0
  127. package/pages/c/_cluster/uiplugins/UninstallDialog.vue +9 -1
  128. package/pages/c/_cluster/uiplugins/index.vue +50 -12
  129. package/pages/diagnostic.vue +17 -15
  130. package/pages/home.vue +32 -6
  131. package/plugins/clean-html.js +50 -0
  132. package/plugins/dashboard-store/resource-class.js +4 -0
  133. package/plugins/plugin.js +54 -49
  134. package/plugins/steve/mutations.js +1 -1
  135. package/plugins/steve/steve-class.js +8 -0
  136. package/plugins/steve/steve-pagination-utils.ts +3 -1
  137. package/rancher-components/Accordion/Accordion.vue +4 -4
  138. package/rancher-components/BadgeState/BadgeState.vue +7 -0
  139. package/rancher-components/Card/Card.vue +12 -0
  140. package/rancher-components/Form/Checkbox/Checkbox.vue +9 -2
  141. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +18 -1
  142. package/rancher-components/Form/LabeledInput/LabeledInput.vue +19 -1
  143. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +39 -2
  144. package/rancher-components/RcButton/RcButton.vue +90 -0
  145. package/rancher-components/RcButton/index.ts +2 -0
  146. package/rancher-components/RcButton/types.ts +17 -0
  147. package/rancher-components/RcDropdown/RcDropdown.vue +122 -0
  148. package/rancher-components/RcDropdown/RcDropdownItem.vue +127 -0
  149. package/rancher-components/RcDropdown/RcDropdownSeparator.vue +6 -0
  150. package/rancher-components/RcDropdown/RcDropdownTrigger.vue +42 -0
  151. package/rancher-components/RcDropdown/index.ts +4 -0
  152. package/rancher-components/RcDropdown/types.ts +22 -0
  153. package/rancher-components/RcDropdown/useDropdownCollection.ts +46 -0
  154. package/rancher-components/RcDropdown/useDropdownContext.ts +110 -0
  155. package/scripts/test-plugins-build.sh +2 -0
  156. package/scripts/typegen.sh +2 -0
  157. package/store/catalog.js +1 -1
  158. package/tsconfig.json +2 -1
  159. package/types/components/paginatedResourceTable.ts +25 -0
  160. package/types/components/resourceLabeledSelect.ts +48 -0
  161. package/types/resources/fleet.d.ts +17 -0
  162. package/types/shell/index.d.ts +61 -0
  163. package/utils/auth.js +5 -1
  164. package/utils/cluster.js +106 -0
  165. package/utils/fleet.ts +35 -3
  166. package/utils/ingress.ts +64 -0
  167. package/utils/uiplugins.ts +56 -44
  168. package/utils/validators/cron-schedule.js +7 -2
  169. package/utils/validators/formRules/__tests__/index.test.ts +53 -17
  170. package/utils/validators/formRules/index.ts +20 -5
  171. package/vue.config.js +1 -1
  172. package/components/RelatedWorkloadsTable.vue +0 -50
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
 
@@ -157,6 +157,7 @@ class StevePaginationUtils extends NamespaceProjectFilters {
157
157
  { field: '_type' },
158
158
  { field: 'reason' },
159
159
  { field: 'involvedObject.kind' },
160
+ // { field: 'involvedObject.uid' }, // Pending API Support - https://github.com/rancher/rancher/issues/48603
160
161
  { field: 'message' },
161
162
  ],
162
163
  [CATALOG.CLUSTER_REPO]: [
@@ -431,7 +432,8 @@ class StevePaginationUtils extends NamespaceProjectFilters {
431
432
  // Check if the API supports filtering by this field
432
433
  this.validateField(validateFields, schema, field.field);
433
434
 
434
- const exactPartial = field.exact ? `'${ field.value }'` : field.value;
435
+ const value = encodeURIComponent(field.value);
436
+ const exactPartial = field.exact ? `'${ value }'` : value;
435
437
 
436
438
  return `${ this.convertArrayPath(field.field) }${ field.equals ? '=' : '!=' }${ exactPartial }`;
437
439
  }
@@ -47,12 +47,12 @@ export default defineComponent({
47
47
  data-testid="accordion-chevron"
48
48
  />
49
49
  <slot name="header">
50
- <h4
50
+ <h2
51
51
  data-testid="accordion-title-slot-content"
52
52
  class="mb-0"
53
53
  >
54
54
  {{ titleKey ? t(titleKey) : title }}
55
- </h4>
55
+ </h2>
56
56
  </slot>
57
57
  </div>
58
58
  <div
@@ -70,7 +70,7 @@ export default defineComponent({
70
70
  border: 1px solid var(--border)
71
71
  }
72
72
  .accordion-header {
73
- padding: 5px;
73
+ padding: 16px 16px 16px 11px;
74
74
  display: flex;
75
75
  align-items: center;
76
76
  &>*{
@@ -81,6 +81,6 @@ export default defineComponent({
81
81
  }
82
82
  }
83
83
  .accordion-body {
84
- padding: 10px;
84
+ padding: 0px 16px 16px;
85
85
  }
86
86
  </style>
@@ -94,6 +94,13 @@ export default defineComponent({
94
94
  background: transparent;
95
95
  border-color: var(--success);
96
96
  }
97
+
98
+ // Added badge-disabled instead of bg-disabled since bg-disabled is used in other places with !important styling, an investigation is needed to make the naming consistent
99
+ &.badge-disabled {
100
+ color: var(--badge-state-disabled-text);
101
+ background-color: var( --badge-state-disabled-bg);
102
+ border: 1px solid var(--badge-state-disabled-border);
103
+ }
97
104
  }
98
105
  </style>
99
106
  <style lang="scss">
@@ -1,7 +1,9 @@
1
1
  <script lang="ts">
2
2
  import { defineComponent, PropType } from 'vue';
3
+ import { useBasicSetupFocusTrap } from '@shell/composables/focusTrap';
3
4
 
4
5
  export default defineComponent({
6
+
5
7
  name: 'Card',
6
8
  props: {
7
9
  /**
@@ -50,12 +52,22 @@ export default defineComponent({
50
52
  type: Boolean,
51
53
  default: false,
52
54
  },
55
+ triggerFocusTrap: {
56
+ type: Boolean,
57
+ default: false,
58
+ },
59
+ },
60
+ setup(props) {
61
+ if (props.triggerFocusTrap) {
62
+ useBasicSetupFocusTrap('#focus-trap-card-container-element');
63
+ }
53
64
  }
54
65
  });
55
66
  </script>
56
67
 
57
68
  <template>
58
69
  <div
70
+ id="focus-trap-card-container-element"
59
71
  class="card-container"
60
72
  :class="{'highlight-border': showHighlightBorder, 'card-sticky': sticky}"
61
73
  data-testid="card"
@@ -264,13 +264,15 @@ export default defineComponent({
264
264
  <template v-else-if="label">{{ label }}</template>
265
265
  <i
266
266
  v-if="tooltipKey"
267
- v-clean-tooltip="t(tooltipKey)"
267
+ v-clean-tooltip="{content: t(tooltipKey), triggers: ['hover', 'touch', 'focus']}"
268
268
  class="checkbox-info icon icon-info icon-lg"
269
+ :tabindex="isDisabled ? -1 : 0"
269
270
  />
270
271
  <i
271
272
  v-else-if="tooltip"
272
- v-clean-tooltip="tooltip"
273
+ v-clean-tooltip="{content: tooltip, triggers: ['hover', 'touch', 'focus']}"
273
274
  class="checkbox-info icon icon-info icon-lg"
275
+ :tabindex="isDisabled ? -1 : 0"
274
276
  />
275
277
  </slot>
276
278
  </span>
@@ -329,6 +331,11 @@ $fontColor: var(--input-label);
329
331
  .checkbox-info {
330
332
  line-height: normal;
331
333
  margin-left: 2px;
334
+
335
+ &:focus-visible {
336
+ @include focus-outline;
337
+ outline-offset: 2px;
338
+ }
332
339
  }
333
340
 
334
341
  .checkbox-custom {
@@ -20,7 +20,7 @@ describe('component: LabeledInput', () => {
20
20
  expect(wrapper.emitted('update:value')![0][0]).toBe(value);
21
21
  });
22
22
 
23
- it('using mode "multiline" should emit input value correctly', () => {
23
+ it('using type "multiline" should emit input value correctly', () => {
24
24
  const value = 'any-string';
25
25
  const delay = 1;
26
26
  const wrapper = mount(LabeledInput, {
@@ -37,4 +37,21 @@ describe('component: LabeledInput', () => {
37
37
  expect(wrapper.emitted('update:value')).toHaveLength(1);
38
38
  expect(wrapper.emitted('update:value')![0][0]).toBe(value);
39
39
  });
40
+
41
+ describe('using type "chron"', () => {
42
+ it.each([
43
+ ['0 * * * *', 'Every hour, every day'],
44
+ ['@daily', 'At 12:00 AM, every day'],
45
+ ['You must fail! Go!', '%generic.invalidCron%'],
46
+ ])('passing value %p should display hint %p', (value, hint) => {
47
+ const wrapper = mount(LabeledInput, {
48
+ propsData: { value, type: 'cron' },
49
+ mocks: { $store: { getters: { 'i18n/t': jest.fn() } } }
50
+ });
51
+
52
+ const subLabel = wrapper.find('[data-testid="sub-label"]');
53
+
54
+ expect(subLabel.text()).toBe(hint);
55
+ });
56
+ });
40
57
  });
@@ -179,14 +179,28 @@ export default defineComponent({
179
179
  if (this.type !== 'cron' || !this.value) {
180
180
  return;
181
181
  }
182
+
183
+ // TODO - #13202: This is required due use of 2 libraries and 3 different libraries through the code.
184
+ const predefined = [
185
+ '@yearly',
186
+ '@annually',
187
+ '@monthly',
188
+ '@weekly',
189
+ '@daily',
190
+ '@midnight',
191
+ '@hourly'
192
+ ];
193
+ const isPredefined = predefined.includes(this.value as string);
194
+
182
195
  // refer https://github.com/GuillaumeRochat/cron-validator#readme
183
- if (!isValidCron(this.value as string, {
196
+ if (!isPredefined && !isValidCron(this.value as string, {
184
197
  alias: true,
185
198
  allowBlankDay: true,
186
199
  allowSevenAsSunday: true,
187
200
  })) {
188
201
  return this.t('generic.invalidCron');
189
202
  }
203
+
190
204
  try {
191
205
  const hint = cronstrue.toString(this.value as string || '', { verbose: true });
192
206
 
@@ -350,6 +364,7 @@ export default defineComponent({
350
364
  <input
351
365
  v-else
352
366
  ref="value"
367
+ role="textbox"
353
368
  :class="{ 'no-label': !hasLabel }"
354
369
  v-bind="$attrs"
355
370
  :maxlength="_maxlength"
@@ -382,9 +397,12 @@ export default defineComponent({
382
397
  <div
383
398
  v-if="cronHint || subLabel"
384
399
  class="sub-label"
400
+ data-testid="sub-label"
385
401
  >
386
402
  <div
387
403
  v-if="cronHint"
404
+ role="alert"
405
+ :aria-label="cronHint"
388
406
  >
389
407
  {{ cronHint }}
390
408
  </div>
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { defineComponent } from 'vue';
2
+ import { defineComponent, onMounted, onBeforeUnmount, useTemplateRef } from 'vue';
3
3
 
4
4
  type StateType = boolean | 'true' | 'false' | undefined;
5
5
 
@@ -33,6 +33,29 @@ export default defineComponent({
33
33
 
34
34
  emits: ['update:value'],
35
35
 
36
+ setup() {
37
+ const switchChrome = useTemplateRef<HTMLElement>('switchChrome');
38
+ const focus = () => {
39
+ switchChrome.value?.classList.add('focus');
40
+ };
41
+
42
+ const blur = () => {
43
+ switchChrome.value?.classList.remove('focus');
44
+ };
45
+
46
+ const switchInput = useTemplateRef<HTMLInputElement>('switchInput');
47
+
48
+ onMounted(() => {
49
+ switchInput.value?.addEventListener('focus', focus);
50
+ switchInput.value?.addEventListener('blur', blur);
51
+ });
52
+
53
+ onBeforeUnmount(() => {
54
+ switchInput.value?.removeEventListener('focus', focus);
55
+ switchInput.value?.removeEventListener('blur', blur);
56
+ });
57
+ },
58
+
36
59
  data() {
37
60
  return { state: false as StateType };
38
61
  },
@@ -64,11 +87,18 @@ export default defineComponent({
64
87
  >{{ offLabel }}</span>
65
88
  <label class="switch hand">
66
89
  <input
90
+ ref="switchInput"
67
91
  type="checkbox"
92
+ role="switch"
68
93
  :checked="state"
94
+ :aria-label="onLabel"
69
95
  @input="toggle(null)"
96
+ @keydown.enter="toggle(null)"
70
97
  >
71
- <span class="slider round" />
98
+ <span
99
+ ref="switchChrome"
100
+ class="slider round"
101
+ />
72
102
  </label>
73
103
  <span
74
104
  class="label no-select hand"
@@ -118,6 +148,13 @@ $toggle-height: 16px;
118
148
  background-color: var(--checkbox-disabled-bg);
119
149
  -webkit-transition: .4s;
120
150
  transition: .4s;
151
+
152
+ &.focus {
153
+ @include focus-outline;
154
+ outline-offset: 2px;
155
+ -webkit-transition: 0s;
156
+ transition: 0s;
157
+ }
121
158
  }
122
159
 
123
160
  .slider:before {