@rancher/shell 0.3.24 → 0.3.25

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 (111) hide show
  1. package/assets/styles/themes/_light.scss +1 -1
  2. package/assets/translations/en-us.yaml +29 -7
  3. package/assets/translations/zh-hans.yaml +1 -1
  4. package/components/ClusterIconMenu.vue +143 -0
  5. package/components/CruResource.vue +7 -1
  6. package/components/ExplorerProjectsNamespaces.vue +11 -1
  7. package/components/FixedBanner.vue +17 -1
  8. package/components/Markdown.vue +1 -1
  9. package/components/Questions/__tests__/Yaml.test.ts +3 -2
  10. package/components/SortableTable/index.vue +3 -2
  11. package/components/auth/RoleDetailEdit.vue +15 -2
  12. package/components/auth/login/saml.vue +12 -1
  13. package/components/form/LabeledSelect.vue +12 -5
  14. package/components/form/Members/ClusterPermissionsEditor.vue +1 -1
  15. package/components/form/Members/MembershipEditor.vue +6 -1
  16. package/components/form/__tests__/KeyValue.test.ts +6 -3
  17. package/components/form/__tests__/LabeledSelect.test.ts +18 -0
  18. package/components/formatter/PodsUsage.vue +11 -36
  19. package/components/formatter/PrincipalGroupBindings.vue +8 -5
  20. package/components/formatter/__tests__/PodsUsage.test.ts +36 -19
  21. package/components/nav/Group.vue +25 -27
  22. package/components/nav/Header.vue +12 -5
  23. package/components/nav/Pinned.vue +47 -0
  24. package/components/nav/TopLevelMenu.vue +233 -60
  25. package/components/nav/Type.vue +57 -3
  26. package/config/home-links.js +1 -1
  27. package/config/product/istio.js +15 -5
  28. package/config/router.js +3 -9
  29. package/config/table-headers.js +5 -6
  30. package/config/uiplugins.js +1 -0
  31. package/core/plugin-helpers.js +3 -0
  32. package/core/types.ts +6 -1
  33. package/creators/app/files/.vscode/settings.json +0 -1
  34. package/detail/__tests__/autoscaling.horizontalpodautoscaler.test.ts +118 -0
  35. package/detail/autoscaling.horizontalpodautoscaler/index.vue +4 -4
  36. package/detail/provisioning.cattle.io.cluster.vue +7 -5
  37. package/edit/__tests__/management.cattle.io.clusterroletemplatebinding.test.ts +58 -0
  38. package/edit/__tests__/namespace.test.ts +5 -3
  39. package/edit/management.cattle.io.clusterroletemplatebinding.vue +3 -11
  40. package/edit/namespace.vue +8 -4
  41. package/edit/provisioning.cattle.io.cluster/Basics.vue +662 -0
  42. package/edit/provisioning.cattle.io.cluster/CustomCommand.vue +6 -0
  43. package/edit/provisioning.cattle.io.cluster/DrainOptions.vue +13 -8
  44. package/edit/provisioning.cattle.io.cluster/MachinePool.vue +11 -2
  45. package/edit/provisioning.cattle.io.cluster/MemberRoles.vue +40 -0
  46. package/edit/provisioning.cattle.io.cluster/__tests__/Basics.tests.ts +237 -0
  47. package/edit/provisioning.cattle.io.cluster/__tests__/CustomCommand.tests.ts +71 -23
  48. package/edit/provisioning.cattle.io.cluster/__tests__/DrainOptions.test.ts +52 -0
  49. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +65 -142
  50. package/edit/provisioning.cattle.io.cluster/rke2.vue +194 -598
  51. package/edit/workload/storage/__tests__/Storage.test.ts +2 -2
  52. package/edit/workload/storage/persistentVolumeClaim/__tests__/persistentvolumeclaim.test.ts +36 -0
  53. package/edit/workload/storage/persistentVolumeClaim/persistentvolumeclaim.vue +15 -7
  54. package/initialize/index.js +5 -5
  55. package/layouts/default.vue +6 -6
  56. package/layouts/home.vue +6 -2
  57. package/layouts/plain.vue +9 -2
  58. package/list/fleet.cattle.io.cluster.vue +2 -2
  59. package/list/management.cattle.io.feature.vue +1 -1
  60. package/machine-config/vmwarevsphere.vue +48 -7
  61. package/mixins/brand.js +0 -8
  62. package/mixins/child-hook.js +2 -2
  63. package/mixins/create-edit-view/impl.js +3 -3
  64. package/models/__tests__/management.cattle.io.node.ts +96 -0
  65. package/models/__tests__/node.ts +74 -0
  66. package/models/cluster/node.js +6 -5
  67. package/models/cluster.x-k8s.io.machinedeployment.js +2 -2
  68. package/models/management.cattle.io.cluster.js +22 -1
  69. package/models/management.cattle.io.clusterroletemplatebinding.js +3 -3
  70. package/models/management.cattle.io.globalrole.js +17 -2
  71. package/models/management.cattle.io.node.js +6 -4
  72. package/models/management.cattle.io.projectroletemplatebinding.js +3 -3
  73. package/models/management.cattle.io.roletemplate.js +17 -2
  74. package/package.json +2 -6
  75. package/pages/about.vue +2 -0
  76. package/pages/auth/setup.vue +5 -4
  77. package/pages/c/_cluster/monitoring/index.vue +8 -3
  78. package/pages/c/_cluster/uiplugins/CatalogList/CatalogLoadDialog.vue +9 -66
  79. package/pages/c/_cluster/uiplugins/CatalogList/CatalogUninstallDialog.vue +182 -0
  80. package/pages/c/_cluster/uiplugins/CatalogList/index.vue +15 -32
  81. package/pages/c/_cluster/uiplugins/UninstallDialog.vue +8 -46
  82. package/pages/c/_cluster/uiplugins/index.vue +64 -64
  83. package/pages/diagnostic.vue +0 -39
  84. package/pages/home.vue +1 -1
  85. package/plugins/dashboard-store/normalize.js +4 -4
  86. package/plugins/int-number.js +5 -2
  87. package/plugins/positive-int-number.js +19 -0
  88. package/plugins/steve/__tests__/getters.spec.ts +15 -0
  89. package/plugins/steve/getters.js +22 -10
  90. package/rancher-components/Form/LabeledInput/LabeledInput.vue +0 -8
  91. package/rancher-components/Form/Radio/RadioButton.test.ts +3 -7
  92. package/store/index.js +4 -0
  93. package/store/prefs.js +1 -0
  94. package/types/shell/index.d.ts +13 -4
  95. package/utils/__tests__/cluster.test.ts +55 -0
  96. package/utils/__tests__/object.test.ts +21 -2
  97. package/utils/cluster.js +47 -1
  98. package/utils/object.js +12 -5
  99. package/utils/validators/formRules/__tests__/index.test.ts +13 -1
  100. package/utils/validators/formRules/index.ts +4 -0
  101. package/utils/validators/role-template.js +9 -1
  102. package/utils/version.js +1 -1
  103. package/yarn-error.log +16 -16
  104. package/components/ClusterProviderIconMenu.vue +0 -161
  105. package/content/docs/en-us/getting-started.md +0 -224
  106. package/content/docs/en-us/whats-new.md +0 -29
  107. package/content/docs/zh-hans/getting-started.md +0 -224
  108. package/content/docs/zh-hans/whats-new.md +0 -28
  109. package/pages/docs/_doc.vue +0 -345
  110. package/pages/docs/toc.js +0 -27
  111. package/plugins/console.js +0 -34
@@ -2,9 +2,8 @@
2
2
  import { mapGetters } from 'vuex';
3
3
 
4
4
  import AsyncButton from '@shell/components/AsyncButton';
5
- import { CATALOG, SERVICE, WORKLOAD_TYPES } from '@shell/config/types';
6
- import { UI_PLUGIN_LABELS, UI_PLUGIN_NAMESPACE } from '@shell/config/uiplugins';
7
- import { allHash } from '@shell/utils/promise';
5
+ import { CATALOG } from '@shell/config/types';
6
+ import { UI_PLUGIN_NAMESPACE } from '@shell/config/uiplugins';
8
7
 
9
8
  export default {
10
9
  components: { AsyncButton },
@@ -33,7 +32,7 @@ export default {
33
32
  this.$emit('update', plugin.name, 'uninstall');
34
33
 
35
34
  // Delete the CR if this is a developer plugin (there is no Helm App, so need to remove the CRD ourselves)
36
- if (plugin.uiplugin?.isDeveloper || (plugin.catalog && plugin.uiplugin)) {
35
+ if (plugin.uiplugin?.isDeveloper) {
37
36
  // Delete the custom resource
38
37
  await plugin.uiplugin.remove();
39
38
  }
@@ -41,28 +40,13 @@ export default {
41
40
  // Find the app for this plugin
42
41
  const apps = await this.$store.dispatch('management/findAll', { type: CATALOG.APP });
43
42
 
44
- const pluginApps = apps.filter((app) => {
45
- if (plugin.catalog && app.namespace === UI_PLUGIN_NAMESPACE) {
46
- // Find the related apps from the deployed helm repository
47
- const charts = this.allCharts.filter((chart) => chart.repoName === plugin.repo?.metadata?.name);
48
-
49
- return charts.some((chart) => chart.chartName === app.metadata.name);
50
- }
51
-
52
- if (app.namespace === UI_PLUGIN_NAMESPACE && app.name === plugin.name) {
53
- return app;
54
- }
55
-
56
- return false;
43
+ const pluginApp = apps.find((app) => {
44
+ return app.namespace === UI_PLUGIN_NAMESPACE && app.name === plugin.name;
57
45
  });
58
46
 
59
- if (plugin.catalog) {
60
- await this.removePluginImageResources(plugin.uiplugin);
61
- }
62
-
63
- if (pluginApps.length) {
47
+ if (pluginApp) {
64
48
  try {
65
- pluginApps.forEach((app) => app.remove());
49
+ await pluginApp.remove();
66
50
  } catch (e) {
67
51
  this.$store.dispatch('growl/error', {
68
52
  title: this.t('plugins.error.generic'),
@@ -75,28 +59,6 @@ export default {
75
59
  }
76
60
 
77
61
  this.closeDialog(plugin);
78
- },
79
- async removePluginImageResources(plugin) {
80
- const selector = `${ UI_PLUGIN_LABELS.CATALOG_IMAGE }=${ plugin.metadata?.labels?.[UI_PLUGIN_LABELS.CATALOG_IMAGE] }`;
81
- const namespace = UI_PLUGIN_NAMESPACE;
82
-
83
- if (selector) {
84
- const hash = await allHash({
85
- deployment: this.$store.dispatch('management/findMatching', {
86
- type: WORKLOAD_TYPES.DEPLOYMENT, selector, namespace
87
- }),
88
- service: this.$store.dispatch('management/findMatching', {
89
- type: SERVICE, selector, namespace
90
- }),
91
- repo: this.$store.dispatch('management/findMatching', { type: CATALOG.CLUSTER_REPO, selector })
92
- });
93
-
94
- for (const resource of Object.keys(hash)) {
95
- if (hash[resource]) {
96
- hash[resource].forEach((r) => r.remove());
97
- }
98
- }
99
- }
100
62
  }
101
63
  }
102
64
  };
@@ -118,7 +80,7 @@ export default {
118
80
  <div class="mt-10 dialog-panel">
119
81
  <div class="dialog-info">
120
82
  <p>
121
- {{ plugin.catalog ? t('plugins.uninstall.custom') : t('plugins.uninstall.prompt') }}
83
+ {{ t('plugins.uninstall.prompt') }}
122
84
  </p>
123
85
  </div>
124
86
  <div class="dialog-buttons">
@@ -19,6 +19,7 @@ import { BadgeState } from '@components/BadgeState';
19
19
  import UninstallDialog from './UninstallDialog.vue';
20
20
  import InstallDialog from './InstallDialog.vue';
21
21
  import CatalogLoadDialog from './CatalogList/CatalogLoadDialog.vue';
22
+ import CatalogUninstallDialog from './CatalogList/CatalogUninstallDialog.vue';
22
23
  import DeveloperInstallDialog from './DeveloperInstallDialog.vue';
23
24
  import PluginInfoPanel from './PluginInfoPanel.vue';
24
25
  import SetupUIPlugins from './SetupUIPlugins';
@@ -35,7 +36,6 @@ import {
35
36
  isChartVersionHigher,
36
37
  UI_PLUGIN_NAMESPACE,
37
38
  UI_PLUGIN_CHART_ANNOTATIONS,
38
- UI_PLUGIN_LABELS,
39
39
  UI_PLUGINS_REPO_URL,
40
40
  UI_PLUGINS_PARTNERS_REPO_URL
41
41
  } from '@shell/config/uiplugins';
@@ -58,6 +58,7 @@ export default {
58
58
  CatalogList,
59
59
  Banner,
60
60
  CatalogLoadDialog,
61
+ CatalogUninstallDialog,
61
62
  InstallDialog,
62
63
  LazyImage,
63
64
  PluginInfoPanel,
@@ -318,7 +319,11 @@ export default {
318
319
  builtin: !!p.builtin,
319
320
  };
320
321
 
321
- all.push(item);
322
+ // Built-in plugins can chose to be hidden - used where we implement as extensions
323
+ // but don't want to shows them individually on the extensions page
324
+ if (!(item.builtin && rancher[UI_PLUGIN_CHART_ANNOTATIONS.HIDDEN_BUILTIN])) {
325
+ all.push(item);
326
+ }
322
327
  }
323
328
  });
324
329
 
@@ -326,11 +331,6 @@ export default {
326
331
  this.plugins.forEach((p) => {
327
332
  const chart = all.find((c) => c.name === p.name);
328
333
 
329
- // Plugin is a container image, do not add to charts
330
- if (p.metadata?.labels?.[UI_PLUGIN_LABELS.CATALOG_IMAGE]) {
331
- return;
332
- }
333
-
334
334
  if (chart) {
335
335
  chart.installed = true;
336
336
  chart.uiplugin = p;
@@ -409,10 +409,6 @@ export default {
409
409
 
410
410
  // Sort by name
411
411
  return sortBy(all, 'name', false);
412
- },
413
-
414
- pluginsFromCatalogImage() {
415
- return this.plugins.filter((p) => p.metadata?.labels?.[UI_PLUGIN_LABELS.CATALOG_IMAGE]);
416
412
  }
417
413
  },
418
414
 
@@ -464,15 +460,11 @@ export default {
464
460
 
465
461
  neu.forEach((plugin) => {
466
462
  const existing = installed.find((p) => !p.removed && p.name === plugin.name && p.version === plugin.version);
467
- const isCustomImage = plugin.metadata?.labels?.[UI_PLUGIN_LABELS.CATALOG_IMAGE];
468
463
 
469
464
  if (!existing && plugin.isCached) {
470
- if (!this.uiErrors[plugin.name] && !isCustomImage) {
465
+ if (!this.uiErrors[plugin.name]) {
471
466
  changes++;
472
467
  }
473
- if (isCustomImage) {
474
- this.refreshCharts(true);
475
- }
476
468
 
477
469
  this.updatePluginInstallStatus(plugin.name, false);
478
470
  }
@@ -481,7 +473,7 @@ export default {
481
473
  if (changes > 0) {
482
474
  Vue.set(this, 'reloadRequired', true);
483
475
  }
484
- },
476
+ }
485
477
  },
486
478
 
487
479
  // Forget the types when we leave the page
@@ -550,6 +542,10 @@ export default {
550
542
  this.$refs.catalogLoadDialog.showDialog();
551
543
  },
552
544
 
545
+ showCatalogUninstallDialog(ev) {
546
+ this.$refs.catalogUninstallDialog.showDialog(ev);
547
+ },
548
+
553
549
  showInstallDialog(plugin, mode, ev) {
554
550
  ev.target?.blur();
555
551
  ev.preventDefault();
@@ -667,50 +663,49 @@ export default {
667
663
  {{ t('plugins.title') }}
668
664
  </h2>
669
665
  </template>
670
- <div
671
- v-if="reloadRequired"
672
- class="plugin-reload-banner mr-20"
673
- data-testid="extension-reload-banner"
674
- >
675
- <i class="icon icon-checkmark mr-10" />
676
- <span>
677
- {{ t('plugins.reload') }}
678
- </span>
679
- <button
680
- class="ml-10 btn btn-sm role-primary"
681
- data-testid="extension-reload-banner-reload-btn"
682
- @click="reload()"
683
- >
684
- {{ t('generic.reload') }}
685
- </button>
686
- </div>
687
- <div
688
- v-if="hasService && hasMenuActions"
689
- class="actions-container"
690
- >
691
- <button
692
- ref="actions"
693
- aria-haspopup="true"
694
- type="button"
695
- class="btn role-multi-action actions"
696
- data-testid="extensions-page-menu"
697
- @click="setMenu"
666
+ <div class="actions-container">
667
+ <div
668
+ v-if="reloadRequired"
669
+ class="plugin-reload-banner mr-20"
670
+ data-testid="extension-reload-banner"
698
671
  >
699
- <i class="icon icon-actions" />
700
- </button>
701
- <ActionMenu
702
- :custom-actions="menuActions"
703
- :open="menuOpen"
704
- :use-custom-target-element="true"
705
- :custom-target-element="menuTargetElement"
706
- :custom-target-event="menuTargetEvent"
707
- @close="setMenu(false)"
708
- @devLoad="showDeveloperLoadDialog"
709
- @removePluginSupport="removePluginSupport"
710
- @manageRepos="manageRepos"
711
- @addRancherRepos="showAddExtensionReposDialog"
712
- @manageExtensionView="manageExtensionView"
713
- />
672
+ <i class="icon icon-checkmark mr-10" />
673
+ <span>
674
+ {{ t('plugins.reload') }}
675
+ </span>
676
+ <button
677
+ class="ml-10 btn btn-sm role-primary"
678
+ data-testid="extension-reload-banner-reload-btn"
679
+ @click="reload()"
680
+ >
681
+ {{ t('generic.reload') }}
682
+ </button>
683
+ </div>
684
+ <div v-if="hasService && hasMenuActions">
685
+ <button
686
+ ref="actions"
687
+ aria-haspopup="true"
688
+ type="button"
689
+ class="btn role-multi-action actions"
690
+ data-testid="extensions-page-menu"
691
+ @click="setMenu"
692
+ >
693
+ <i class="icon icon-actions" />
694
+ </button>
695
+ <ActionMenu
696
+ :custom-actions="menuActions"
697
+ :open="menuOpen"
698
+ :use-custom-target-element="true"
699
+ :custom-target-element="menuTargetElement"
700
+ :custom-target-event="menuTargetEvent"
701
+ @close="setMenu(false)"
702
+ @devLoad="showDeveloperLoadDialog"
703
+ @removePluginSupport="removePluginSupport"
704
+ @manageRepos="manageRepos"
705
+ @addRancherRepos="showAddExtensionReposDialog"
706
+ @manageExtensionView="manageExtensionView"
707
+ />
708
+ </div>
714
709
  </div>
715
710
  </div>
716
711
 
@@ -737,9 +732,8 @@ export default {
737
732
  <div v-else>
738
733
  <template v-if="showCatalogList">
739
734
  <CatalogList
740
- :plugins="pluginsFromCatalogImage"
741
735
  @showCatalogLoadDialog="showCatalogLoadDialog"
742
- @showUninstallDialog="showUninstallDialog"
736
+ @showCatalogUninstallDialog="showCatalogUninstallDialog($event)"
743
737
  />
744
738
  </template>
745
739
  <template v-else>
@@ -814,8 +808,8 @@ export default {
814
808
  />
815
809
  <template v-else>
816
810
  <div
817
- v-for="plugin in list"
818
- :key="plugin.name"
811
+ v-for="(plugin, i) in list"
812
+ :key="plugin.name + i"
819
813
  class="plugin"
820
814
  :data-testid="`extension-card-${plugin.name}`"
821
815
  @click="showPluginDetail(plugin)"
@@ -999,6 +993,12 @@ export default {
999
993
  <CatalogLoadDialog
1000
994
  ref="catalogLoadDialog"
1001
995
  @closed="didInstall"
996
+ @refresh="() => reloadRequired = true"
997
+ />
998
+ <CatalogUninstallDialog
999
+ ref="catalogUninstallDialog"
1000
+ @closed="didUninstall"
1001
+ @refresh="() => reloadRequired = true"
1002
1002
  />
1003
1003
  <DeveloperInstallDialog
1004
1004
  ref="developerInstallDialog"
@@ -120,9 +120,6 @@ export default {
120
120
  systemInformation.jsMemory.value += `, ${ this.t('about.diagnostic.systemInformation.memUsedJsHeapSize', { usedJSHeapSize: window?.performance?.memory?.usedJSHeapSize }) }`;
121
121
  }
122
122
 
123
- // scroll logs container to the bottom
124
- this.scrollLogsToBottom();
125
-
126
123
  return {
127
124
  systemInformation,
128
125
  topFifteenForResponseTime: null,
@@ -130,16 +127,9 @@ export default {
130
127
  finalCounts: null,
131
128
  includeResponseTimes: true,
132
129
  storeMapping: this.$store?._modules?.root?.state,
133
- latestLogs: console.logs // eslint-disable-line no-console
134
130
  };
135
131
  },
136
132
 
137
- watch: {
138
- latestLogs() {
139
- this.scrollLogsToBottom();
140
- }
141
- },
142
-
143
133
  computed: {
144
134
  clusterCount() {
145
135
  return this.finalCounts?.length;
@@ -147,14 +137,6 @@ export default {
147
137
  },
148
138
 
149
139
  methods: {
150
- scrollLogsToBottom() {
151
- this.$nextTick(() => {
152
- const logsContainer = document.querySelector('.logs-container');
153
-
154
- logsContainer.scrollTop = logsContainer.scrollHeight;
155
- });
156
- },
157
-
158
140
  generateKey(data) {
159
141
  const randomize = Math.random() * 10000;
160
142
 
@@ -166,7 +148,6 @@ export default {
166
148
  const fileName = 'rancher-diagnostic-data.json';
167
149
  const data = {
168
150
  systemInformation: this.systemInformation,
169
- logs: this.latestLogs,
170
151
  storeMapping: this.parseStoreData(this.storeMapping),
171
152
  resourceCounts: this.finalCounts,
172
153
  responseTimes: this.responseTimes
@@ -411,26 +392,6 @@ export default {
411
392
  </div>
412
393
  </div>
413
394
 
414
- <!-- Logs -->
415
- <div class="mb-40">
416
- <h2 class="mb-20">
417
- {{ t('about.diagnostic.logs.subtitle') }}
418
- </h2>
419
- <ul class="logs-container">
420
- <li
421
- v-for="logEntry in latestLogs"
422
- :key="generateKey(logEntry.timestamp)"
423
- :class="logEntry.type"
424
- >
425
- <span class="log-entry-type">{{ logEntry.type }} :: </span>
426
- <span
427
- v-for="(arg, i) in logEntry.data"
428
- :key="i"
429
- >{{ arg }}</span>
430
- </li>
431
- </ul>
432
- </div>
433
-
434
395
  <PromptModal />
435
396
  </div>
436
397
  </template>
package/pages/home.vue CHANGED
@@ -192,7 +192,7 @@ export default {
192
192
  label: this.t('tableHeaders.pods'),
193
193
  name: 'pods',
194
194
  value: '',
195
- sort: ['status.allocatable.pods', 'status.available.pods'],
195
+ sort: ['status.allocatable.pods', 'status.requested.pods'],
196
196
  formatter: 'PodsUsage',
197
197
  delayLoading: true
198
198
  },
@@ -20,10 +20,10 @@ export function normalizeType(type) {
20
20
  // Detect and resolve conflicts from a 409 response.
21
21
  // If they are resolved, return a false-y value
22
22
  // Else they can't be resolved, return an array of errors to show to the user.
23
- export function handleConflict(initialValueJSON, value, liveValue, rootGetters, store) {
24
- const orig = store.dispatch(`cleanForDiff`, initialValueJSON);
25
- const user = store.dispatch(`cleanForDiff`, value.toJSON());
26
- const cur = store.dispatch(`cleanForDiff`, liveValue.toJSON());
23
+ export async function handleConflict(initialValueJSON, value, liveValue, rootGetters, store, storeNamespace) {
24
+ const orig = await store.dispatch(`${ storeNamespace }/cleanForDiff`, initialValueJSON, { root: true });
25
+ const user = await store.dispatch(`${ storeNamespace }/cleanForDiff`, value.toJSON(), { root: true });
26
+ const cur = await store.dispatch(`${ storeNamespace }/cleanForDiff`, liveValue.toJSON(), { root: true });
27
27
 
28
28
  const bgChange = changeset(orig, cur);
29
29
  const userChange = changeset(orig, user);
@@ -5,9 +5,12 @@ export default Vue.directive('intNumber', {
5
5
  el.addEventListener('keypress', (e) => {
6
6
  e = e || window.event;
7
7
  const charcode = typeof e.charCode === 'number' ? e.charCode : e.keyCode;
8
- const re = /\d/;
8
+ const inputChar = String.fromCharCode(charcode);
9
9
 
10
- if (!re.test(String.fromCharCode(charcode)) && charcode > 9 && !e.ctrlKey) {
10
+ // Allow digits, minus sign at the beginning, and Ctrl key combinations
11
+ const re = /^-?\d*$/;
12
+
13
+ if (!re.test(inputChar) && charcode > 9 && !e.ctrlKey) {
11
14
  if (e.preventDefault) {
12
15
  e.preventDefault();
13
16
  } else {
@@ -0,0 +1,19 @@
1
+ import Vue from 'vue';
2
+
3
+ export default Vue.directive('positiveIntNumber', {
4
+ inserted(el) {
5
+ el.addEventListener('keypress', (e) => {
6
+ e = e || window.event;
7
+ const charcode = typeof e.charCode === 'number' ? e.charCode : e.keyCode;
8
+ const re = /^\d+$/; // Use regex to match positive numbers
9
+
10
+ if (!re.test(String.fromCharCode(charcode)) && charcode > 9 && !e.ctrlKey) {
11
+ if (e.preventDefault) {
12
+ e.preventDefault();
13
+ } else {
14
+ e.returnValue = false;
15
+ }
16
+ }
17
+ });
18
+ }
19
+ });
@@ -68,9 +68,15 @@ describe('steve: getters', () => {
68
68
  it('returns a string with a single filter statement applied if a single filter statement is applied', () => {
69
69
  expect(urlOptionsGetter('foo', { filter: { bar: 'baz' } })).toBe('foo?bar=baz');
70
70
  });
71
+ it('returns a string with a single filter statement applied and formatted for steve if a single filter statement is applied and the url starts with "/v1"', () => {
72
+ expect(urlOptionsGetter('/v1/foo', { filter: { bar: 'baz' } })).toBe('/v1/foo?filter=bar=baz&exclude=metadata.managedFields');
73
+ });
71
74
  it('returns a string with a multiple filter statements applied if a single filter statement is applied', () => {
72
75
  expect(urlOptionsGetter('foo', { filter: { bar: 'baz', far: 'faz' } })).toBe('foo?bar=baz&far=faz');
73
76
  });
77
+ it('returns a string with a multiple filter statements applied and formatted for steve if a single filter statement is applied and the url starts with "/v1"', () => {
78
+ expect(urlOptionsGetter('/v1/foo', { filter: { bar: 'baz', far: 'faz' } })).toBe('/v1/foo?filter=bar=baz&far=faz&exclude=metadata.managedFields');
79
+ });
74
80
  it('returns a string with an exclude statement for "bar" and "metadata.managedFields" if excludeFields is a single element array with the string "bar" and the url starts with "/v1/"', () => {
75
81
  expect(urlOptionsGetter('/v1/foo', { excludeFields: ['bar'] })).toBe('/v1/foo?exclude=bar&exclude=metadata.managedFields');
76
82
  });
@@ -86,8 +92,17 @@ describe('steve: getters', () => {
86
92
  it('returns a string with a sorting criteria if the sort option is provided', () => {
87
93
  expect(urlOptionsGetter('foo', { sortBy: 'bar' })).toBe('foo?sort=bar');
88
94
  });
95
+ it('returns a string with a sorting criteria formatted for steve if the sort option is provided and the url starts with "/v1"', () => {
96
+ expect(urlOptionsGetter('/v1/foo', { sortBy: 'bar' })).toBe('/v1/foo?exclude=metadata.managedFields&sort=bar');
97
+ });
89
98
  it('returns a string with a sorting criteria if the sort option is provided and an order if sortOrder is provided', () => {
90
99
  expect(urlOptionsGetter('foo', { sortBy: 'bar', sortOrder: 'baz' })).toBe('foo?sort=bar&order=baz');
91
100
  });
101
+ it('returns a string with a sorting criteria formatted for steve if the sort option is provided and an order if sortOrder is provided and the url starts with "/v1"', () => {
102
+ expect(urlOptionsGetter('/v1/foo', { sortBy: 'bar', sortOrder: 'baz' })).toBe('/v1/foo?exclude=metadata.managedFields&sort=bar');
103
+ });
104
+ it('returns a string with a sorting criteria formatted for steve if the sort option is provided and an order if sortOrder is "desc" and the url starts with "/v1"', () => {
105
+ expect(urlOptionsGetter('/v1/foo', { sortBy: 'bar', sortOrder: 'desc' })).toBe('/v1/foo?exclude=metadata.managedFields&sort=-bar');
106
+ });
92
107
  });
93
108
  });
@@ -31,8 +31,8 @@ export default {
31
31
  const isSteve = parsedUrl.path.startsWith('/v1');
32
32
 
33
33
  // Filter
34
- // Steve's filter options work differently nowadays (https://github.com/rancher/steve#filter) #9341
35
34
  if ( opt.filter ) {
35
+ url += `${ (url.includes('?') ? '&' : '?') }`;
36
36
  const keys = Object.keys(opt.filter);
37
37
 
38
38
  keys.forEach((key) => {
@@ -42,9 +42,18 @@ export default {
42
42
  vals = [vals];
43
43
  }
44
44
 
45
- vals.forEach((val) => {
46
- url += `${ (url.includes('?') ? '&' : '?') + encodeURIComponent(key) }=${ encodeURIComponent(val) }`;
45
+ // Steve's filter options now support more complex filtering not yet implemented here #9341
46
+ if (isSteve) {
47
+ url += `${ (url.includes('filter=') ? '&' : 'filter=') }`;
48
+ }
49
+
50
+ const filterStrings = vals.map((val) => {
51
+ return `${ encodeURI(key) }=${ encodeURI(val) }`;
47
52
  });
53
+ const urlEnding = url.charAt(url.length - 1);
54
+ const nextStringConnector = ['&', '?', '='].includes(urlEnding) ? '' : '&';
55
+
56
+ url += `${ nextStringConnector }${ filterStrings.join('&') }`;
48
57
  });
49
58
  }
50
59
 
@@ -82,18 +91,21 @@ export default {
82
91
  // End: Limit
83
92
 
84
93
  // Sort
85
- // Steve's sort options work differently nowadays (https://github.com/rancher/steve#sort) #9341
94
+ // Steve's sort options supports multi-column sorting and column specific sort orders, not implemented yet #9341
86
95
  const sortBy = opt.sortBy;
96
+ const orderBy = opt.sortOrder;
87
97
 
88
98
  if ( sortBy ) {
89
- url += `${ url.includes('?') ? '&' : '?' }sort=${ encodeURIComponent(sortBy) }`;
99
+ if (isSteve) {
100
+ url += `${ url.includes('?') ? '&' : '?' }sort=${ (orderBy === 'desc' ? '-' : '') + encodeURI(sortBy) }`;
101
+ } else {
102
+ url += `${ url.includes('?') ? '&' : '?' }sort=${ encodeURI(sortBy) }`;
103
+ if ( orderBy ) {
104
+ url += `${ url.includes('?') ? '&' : '?' }order=${ encodeURI(orderBy) }`;
105
+ }
106
+ }
90
107
  }
91
108
 
92
- const orderBy = opt.sortOrder;
93
-
94
- if ( orderBy ) {
95
- url += `${ url.includes('?') ? '&' : '?' }order=${ encodeURIComponent(orderBy) }`;
96
- }
97
109
  // End: Sort
98
110
 
99
111
  return url;
@@ -206,13 +206,6 @@ export default (
206
206
  }
207
207
  },
208
208
 
209
- /**
210
- * Emit on input change
211
- */
212
- onChange(event: Event): void {
213
- this.$emit('change', event);
214
- },
215
-
216
209
  /**
217
210
  * Emit on input with delay. Note: Arrow function is avoided due context
218
211
  * binding.
@@ -306,7 +299,6 @@ export default (
306
299
  @input="onInput($event.target.value)"
307
300
  @focus="onFocus"
308
301
  @blur="onBlur"
309
- @change="onChange"
310
302
  >
311
303
  </slot>
312
304
 
@@ -4,7 +4,7 @@ import { cleanHtmlDirective } from '@shell/plugins/clean-html-directive';
4
4
 
5
5
  describe('radioButton.vue', () => {
6
6
  it('renders label slot contents', () => {
7
- const wrapper = shallowMount(RadioButton, { slots: { label: 'Test Label' }, propsData: { val: {}, value: {} } });
7
+ const wrapper = shallowMount(RadioButton, { slots: { label: 'Test Label' } });
8
8
 
9
9
  expect(wrapper.find('.radio-label').text()).toBe('Test Label');
10
10
  });
@@ -14,9 +14,7 @@ describe('radioButton.vue', () => {
14
14
  RadioButton,
15
15
  {
16
16
  directives: { cleanHtmlDirective },
17
- propsData: {
18
- label: 'Test Label', val: {}, value: {}
19
- }
17
+ propsData: { label: 'Test Label' }
20
18
  });
21
19
 
22
20
  expect(wrapper.find('.radio-label').text()).toBe('Test Label');
@@ -25,9 +23,7 @@ describe('radioButton.vue', () => {
25
23
  it('renders slot contents when both slot and label prop are provided', () => {
26
24
  const wrapper = shallowMount(RadioButton, {
27
25
  slots: { label: 'Test Label - Slot' },
28
- propsData: {
29
- label: 'Test Label - Props', val: {}, value: {}
30
- },
26
+ propsData: { label: 'Test Label - Props' },
31
27
  });
32
28
 
33
29
  expect(wrapper.find('.radio-label').text()).toBe('Test Label - Slot');
package/store/index.js CHANGED
@@ -568,6 +568,10 @@ export const getters = {
568
568
  return getters['isSingleProduct'] && cluster.isHarvester && !getters['isRancherInHarvester'];
569
569
  },
570
570
 
571
+ showTopLevelMenu(getters) {
572
+ return getters['isRancherInHarvester'] || getters['isMultiCluster'] || !getters['isSingleProduct'];
573
+ },
574
+
571
575
  targetRoute(state) {
572
576
  return state.targetRoute;
573
577
  },
package/store/prefs.js CHANGED
@@ -54,6 +54,7 @@ export const NAMESPACE_FILTERS = create('ns-by-cluster', {}, { parseJSON });
54
54
  export const WORKSPACE = create('workspace', '');
55
55
  export const EXPANDED_GROUPS = create('open-groups', ['cluster', 'policy', 'rbac', 'serviceDiscovery', 'storage', 'workload'], { parseJSON });
56
56
  export const FAVORITE_TYPES = create('fav-type', [], { parseJSON });
57
+ export const PINNED_CLUSTERS = create('pinned-clusters', [], { parseJSON });
57
58
  export const GROUP_RESOURCES = create('group-by', 'namespace');
58
59
  export const DIFF = create('diff', 'unified', { options: ['unified', 'split'] });
59
60
  export const THEME = create('theme', 'auto', {