@rancher/shell 0.3.25 → 0.3.27

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 (51) hide show
  1. package/.DS_Store +0 -0
  2. package/assets/translations/en-us.yaml +11 -3
  3. package/assets/translations/zh-hans.yaml +2 -3
  4. package/components/AlertTable.vue +8 -6
  5. package/components/CruResource.vue +7 -4
  6. package/components/EmberPage.vue +2 -2
  7. package/components/EtcdInfoBanner.vue +12 -2
  8. package/components/GlobalRoleBindings.vue +10 -0
  9. package/components/GrafanaDashboard.vue +8 -3
  10. package/components/Wizard.vue +17 -1
  11. package/components/__tests__/ProjectRow.test.ts +63 -0
  12. package/components/auth/RoleDetailEdit.vue +21 -1
  13. package/components/auth/__tests__/RoleDetailEdit.test.ts +41 -0
  14. package/components/form/ArrayList.vue +20 -11
  15. package/components/form/ResourceQuota/ProjectRow.vue +6 -2
  16. package/components/form/__tests__/ArrayList.test.ts +44 -0
  17. package/components/nav/Header.vue +5 -4
  18. package/components/nav/TopLevelMenu.vue +38 -15
  19. package/components/nav/__tests__/TopLevelMenu.test.ts +120 -0
  20. package/components/nav/__tests__/Type.test.ts +139 -0
  21. package/config/private-label.js +1 -1
  22. package/config/settings.ts +0 -2
  23. package/core/types.ts +11 -4
  24. package/edit/provisioning.cattle.io.cluster/Basics.vue +13 -0
  25. package/edit/provisioning.cattle.io.cluster/CustomCommand.vue +1 -1
  26. package/edit/provisioning.cattle.io.cluster/rke2.vue +18 -2
  27. package/edit/workload/mixins/workload.js +14 -4
  28. package/models/fleet.cattle.io.cluster.js +11 -1
  29. package/models/management.cattle.io.globalrole.js +1 -1
  30. package/models/management.cattle.io.roletemplate.js +1 -1
  31. package/package.json +1 -1
  32. package/pages/c/_cluster/auth/roles/index.vue +11 -1
  33. package/pages/c/_cluster/explorer/index.vue +7 -2
  34. package/pages/c/_cluster/monitoring/index.vue +26 -39
  35. package/pages/support/index.vue +1 -8
  36. package/promptRemove/management.cattle.io.project.vue +6 -9
  37. package/rancher-components/components/Form/Radio/RadioGroup.test.ts +30 -0
  38. package/rancher-components/components/Form/Radio/RadioGroup.vue +4 -0
  39. package/scripts/extension/helm/charts/ui-plugin-server/Chart.yaml +2 -2
  40. package/store/features.js +1 -0
  41. package/types/shell/index.d.ts +4 -1
  42. package/utils/__tests__/object.test.ts +67 -1
  43. package/utils/__tests__/version.test.ts +13 -23
  44. package/utils/cluster.js +1 -1
  45. package/utils/grafana.js +1 -2
  46. package/utils/monitoring.js +25 -1
  47. package/utils/object.js +4 -3
  48. package/utils/sort.js +1 -1
  49. package/utils/validators/formRules/index.ts +1 -1
  50. package/utils/validators/role-template.js +1 -1
  51. package/utils/version.js +0 -13
package/.DS_Store ADDED
Binary file
@@ -653,6 +653,10 @@ asyncButton:
653
653
  action: Save
654
654
  success: Saved
655
655
  waiting: Saving…
656
+ editAndContinue:
657
+ action: Save and Continue
658
+ success: Saved
659
+ waiting: Saving…
656
660
  enable:
657
661
  action: Enable
658
662
  success: Enabled
@@ -1673,6 +1677,7 @@ cluster:
1673
1677
  haveArgInfo: Configuration information is not available for the selected Kubernetes version. The options available on this screen will be limited; you may want to use the YAML editor.
1674
1678
  deprecatedPsp: Pod Security Policies are deprecated as of Kubernetes v1.21, and have been removed in Kubernetes v1.25.
1675
1679
  removedPsp: Pod Security Policies have been removed in Kubernetes v1.25. Use Pod Security Admission instead.
1680
+ cloudProviderAddConfig: 'On Kubernetes 1.27 or greater, the Amazon Cloud Provider requires additional configuration. See <a href="https://ranchermanager.docs.rancher.com/how-to-guides/new-user-guides/kubernetes-clusters-in-rancher-setup/set-up-cloud-providers/amazon" target="_blank" rel="noopener noreferrer nofollow">the documentation</a> for more information.'
1676
1681
  machinePoolError: |-
1677
1682
  {count, plural,
1678
1683
  =1 { {pool_name}: The provided value for {fields} was not found in the list of expected values. This can happen with clusters provisioned outside of Rancher or when options for the provider have changed. }
@@ -1841,6 +1846,9 @@ cluster:
1841
1846
  pspChange:
1842
1847
  title: Pod Security Policy deprecation
1843
1848
  body: <p>Kubernetes has removed support for Pod Security Policies (PSPs) starting with version 1.25. If your cluster has PodSecurityPolicy admission controller enabled via "kube-apiserver-arg.enable-admission-plugins" in Cluster YAML, it has to be <i>manually</i> removed before proceeding with the upgrade. Additionally, any PSPs that may be present in the cluster will no longer be available/enforced. Do you want to proceed?</p>
1849
+ editYamlMachinePool:
1850
+ title: Save Machine Configurations
1851
+ body: Machine Configurations define how machines in Pools are deployed.<br><br> They will be saved upfront to ensure valid Cluster YAML can be saved.
1844
1852
  snapshots:
1845
1853
  suffix: Snapshots per node
1846
1854
  systemService:
@@ -2756,7 +2764,7 @@ landing:
2756
2764
  cpuUsed: CPU Used
2757
2765
  memoryUsed: Memory Used
2758
2766
  seeWhatsNew: Learn more about the improvements and new capabilities in this version.
2759
- whatsNewLink: "What's new in 2.7"
2767
+ whatsNewLink: "What's new in 2.8"
2760
2768
  learnMore: Learn More
2761
2769
  support: Support
2762
2770
  psps: PSPs
@@ -2826,7 +2834,7 @@ logging:
2826
2834
  dockerRootDirectory: Docker Root Directory
2827
2835
  systemdLogPath: systemd Log Path
2828
2836
  tooltip: 'Some kubernetes distributions log to <code>journald</code>. In order to collect these logs the <code>systemdLogPath</code> needs to be defined. While the <code>/run/log/journal</code> directory is used by default, some Linux distributions do not default to this path.'
2829
- url: '<a href="https://rancher.com/docs/rancher/v2.6/en/logging/helm-chart-options/" target="_blank" rel="noopener nofollow noreferrer">Learn more</a>'
2837
+ url: '<a href="https://ranchermanager.docs.rancher.com/v2.8/integrations-in-rancher/logging/logging-helm-chart-options" target="_blank" rel="noopener nofollow noreferrer">Learn more</a>'
2830
2838
  default: /run/log/journal
2831
2839
  elasticsearch:
2832
2840
  host: Host
@@ -4606,6 +4614,7 @@ rbac:
4606
4614
  restricted-admin:
4607
4615
  label: Restricted Administrator
4608
4616
  description: Restricted Admins have full control over all resources in all downstream clusters but no access to the local cluster.
4617
+ deprecation: 'Warning: The Restricted Administrator role has been deprecated as of Rancher 2.8.0 and will be removed in a future release - Check out the <a href="{releaseNotesUrl}" target="_blank" rel="noopener noreferrer nofollow">Release Notes</a>'
4609
4618
  user:
4610
4619
  label: Standard User
4611
4620
  description: Standard Users can create new clusters and manage clusters and projects they have been granted access to.
@@ -7072,7 +7081,6 @@ advancedSettings:
7072
7081
  'auth-user-session-ttl-minutes': 'Custom TTL (in minutes) on a user auth session.'
7073
7082
  'auth-token-max-ttl-minutes': 'Max TTL (in minutes) for all authentication tokens. When set to 0, the token never expires.'
7074
7083
  'kubeconfig-generate-token': 'Automatically generate tokens for users when a kubeconfig is requested.'
7075
- 'kubeconfig-token-ttl-minutes': 'TTL used for tokens generated via the CLI. Deprecated: This setting will be removed, and kubeconfig-default-token-ttl-minutes will be used for all kubeconfig tokens.'
7076
7084
  'kubeconfig-default-token-ttl-minutes': 'TTL (in minutes) applied on all kubeconfig tokens. When set to 0, the token never expires.'
7077
7085
  'rke-metadata-config': 'Configure RKE metadata refresh parameters.'
7078
7086
  'ui-banners': 'Classification banner is used to display a custom fixed banner in the header, footer, or both.'
@@ -2735,7 +2735,7 @@ landing:
2735
2735
  cpuUsed: 已用 CPU
2736
2736
  memoryUsed: 已用内存
2737
2737
  seeWhatsNew: 点击右侧链接,了解此版本的新功能和优化。
2738
- whatsNewLink: "2.7 的新功能"
2738
+ whatsNewLink: "2.8 的新功能"
2739
2739
  learnMore: 了解更多
2740
2740
  support: 支持
2741
2741
  psps: PSP
@@ -2805,7 +2805,7 @@ logging:
2805
2805
  dockerRootDirectory: Docker 根目录
2806
2806
  systemdLogPath: systemd 日志路径
2807
2807
  tooltip: '某些 Kubernetes 发行版在 <code>journald</code>中记录日志。你需要定义<code>systemdLogPath</code> 以收集日志。默认路径是<code>/run/log/journal</code>,但某些 Linux 发行版不默认使用该路径。'
2808
- url: '<a href="https://rancher.com/docs/rancher/v2.6/en/logging/helm-chart-options/" target="_blank" rel="noopener nofollow noreferrer">了解更多</a>'
2808
+ url: '<a href="https://ranchermanager.docs.rancher.com/v2.8/integrations-in-rancher/logging/logging-helm-chart-options" target="_blank" rel="noopener nofollow noreferrer">了解更多</a>'
2809
2809
  default: /run/log/journal
2810
2810
  elasticsearch:
2811
2811
  host: 主机
@@ -7051,7 +7051,6 @@ advancedSettings:
7051
7051
  'auth-user-session-ttl-minutes': '用户认证会话的自定义 TTL(单位:分钟)。'
7052
7052
  'auth-token-max-ttl-minutes': '所有身份认证 Token 的最大 TTL(单位:分钟)。如果设置为 0,则 Token 永不过期。'
7053
7053
  'kubeconfig-generate-token': '请求 kubeconfig 时自动为用户生成 Token。'
7054
- 'kubeconfig-token-ttl-minutes': '在 CLI 中生成的 Token TTL。已弃用:此设置将被删除,kubeconfig-default-token-ttl-minutes 将用于所有 kubeconfig Token。'
7055
7054
  'kubeconfig-default-token-ttl-minutes': '应用于所有 kubeconfig Token 的 TTL(单位:分钟)。如果设置为 0,则 Token 永不过期。'
7056
7055
  'rke-metadata-config': '配置 RKE 元数据刷新参数。'
7057
7056
  'ui-banners': '分类横幅用于在页眉、页脚或两者中显示自定义的固定横幅。'
@@ -82,14 +82,16 @@ export default {
82
82
  },
83
83
 
84
84
  async fetchDeps() {
85
- try {
86
- const am = await this.$store.dispatch('cluster/find', { type: ENDPOINTS, id: `${ this.monitoringNamespace }/${ this.alertServiceEndpoint }` });
85
+ if (this.$store.getters['cluster/canList'](ENDPOINTS)) {
86
+ try {
87
+ const am = await this.$store.dispatch('cluster/find', { type: ENDPOINTS, id: `${ this.monitoringNamespace }/${ this.alertServiceEndpoint }` });
87
88
 
88
- if (!isEmpty(am) && !isEmpty(am.subsets)) {
89
- this.alertManagerPoller.start();
90
- }
91
- } catch {
89
+ if (!isEmpty(am) && !isEmpty(am.subsets)) {
90
+ this.alertManagerPoller.start();
91
+ }
92
+ } catch {
92
93
 
94
+ }
93
95
  }
94
96
  },
95
97
  }
@@ -327,10 +327,13 @@ export default {
327
327
 
328
328
  async showPreviewYaml() {
329
329
  if ( this.applyHooks ) {
330
- await this.applyHooks(
331
- BEFORE_SAVE_HOOKS,
332
- CONTEXT_HOOK_EDIT_YAML,
333
- );
330
+ try {
331
+ await this.applyHooks(BEFORE_SAVE_HOOKS, CONTEXT_HOOK_EDIT_YAML);
332
+ } catch (e) {
333
+ console.warn('Unable to show yaml: ', e); // eslint-disable-line no-console
334
+
335
+ return;
336
+ }
334
337
  }
335
338
 
336
339
  const resourceYaml = this.createResourceYaml(this.yamlModifiers);
@@ -597,11 +597,11 @@ export default {
597
597
 
598
598
  .ember-iframe {
599
599
  border: 0;
600
- left: var(--nav-width);
600
+ left: calc(var(--nav-width) + $app-bar-collapsed-width);
601
601
  height: calc(100vh - var(--header-height));
602
602
  position: absolute;
603
603
  top: var(--header-height);
604
- width: calc(100vw - var(--nav-width));
604
+ width: calc(100vw - var(--nav-width) - $app-bar-collapsed-width);
605
605
  visibility: show;
606
606
  }
607
607
 
@@ -9,8 +9,18 @@ export default {
9
9
  components: { Banner, Loading },
10
10
  async fetch() {
11
11
  const inStore = this.$store.getters['currentProduct'].inStore;
12
- const res = await this.$store.dispatch(`${ inStore }/find`, { type: CATALOG.APP, id: 'cattle-monitoring-system/rancher-monitoring' });
13
- const monitoringVersion = res?.currentVersion;
12
+ let monitoringVersion = '';
13
+
14
+ if (this.$store.getters[`${ inStore }/canList}`](CATALOG.APP)) {
15
+ try {
16
+ const res = await this.$store.dispatch(`${ inStore }/find`, { type: CATALOG.APP, id: 'cattle-monitoring-system/rancher-monitoring' });
17
+
18
+ monitoringVersion = res?.currentVersion;
19
+ } catch (err) {
20
+
21
+ }
22
+ }
23
+
14
24
  const leader = await hasLeader(monitoringVersion, this.$store.dispatch, this.currentCluster.id);
15
25
 
16
26
  this.hasLeader = leader ? this.t('generic.yes') : this.t('generic.no');
@@ -94,6 +94,7 @@ export default {
94
94
  };
95
95
  },
96
96
  computed: {
97
+ ...mapGetters(['releaseNotesUrl']),
97
98
  ...mapGetters({ t: 'i18n/t' }),
98
99
 
99
100
  isCreate() {
@@ -347,6 +348,11 @@ export default {
347
348
  </div>
348
349
  </template>
349
350
  </Checkbox>
351
+ <p
352
+ v-if="role.id === 'restricted-admin'"
353
+ v-clean-html="t('rbac.globalRoles.role.restricted-admin.deprecation', { releaseNotesUrl }, true)"
354
+ class="deprecation-notice"
355
+ />
350
356
  </div>
351
357
  </div>
352
358
  </template>
@@ -364,6 +370,10 @@ export default {
364
370
  </style>
365
371
  <style lang='scss' scoped>
366
372
  $detailSize: 11px;
373
+
374
+ .deprecation-notice {
375
+ margin: 8px 0 8px 20px;
376
+ }
367
377
  .role-group {
368
378
  .type-title {
369
379
  display: flex;
@@ -40,13 +40,18 @@ export default {
40
40
  },
41
41
  async fetch() {
42
42
  const inStore = this.$store.getters['currentProduct'].inStore;
43
- const res = await this.$store.dispatch(`${ inStore }/find`, { type: CATALOG.APP, id: 'cattle-monitoring-system/rancher-monitoring' });
44
43
 
45
- this.monitoringVersion = res?.currentVersion;
44
+ if (this.$store.getters[`${ inStore }/canList`](CATALOG.APP)) {
45
+ try {
46
+ const res = await this.$store.dispatch(`${ inStore }/find`, { type: CATALOG.APP, id: 'cattle-monitoring-system/rancher-monitoring' });
47
+
48
+ this.monitoringVersion = res?.currentVersion;
49
+ } catch (err) {}
50
+ }
46
51
  },
47
52
  data() {
48
53
  return {
49
- loading: false, error: false, interval: null, errorTimer: null, monitoringVersion: null
54
+ loading: false, error: false, interval: null, errorTimer: null, monitoringVersion: ''
50
55
  };
51
56
  },
52
57
  computed: {
@@ -182,6 +182,12 @@ export default {
182
182
  this.activeStep = this.visibleSteps[this.initStepIndex];
183
183
  this.goToStep(this.activeStepIndex + 1);
184
184
  }
185
+ },
186
+ errors() {
187
+ // Ensurce we scroll the errors into view
188
+ this.$nextTick(() => {
189
+ this.$refs.wizard.scrollTop = this.$refs.wizard.scrollHeight;
190
+ });
185
191
  }
186
192
  },
187
193
 
@@ -253,7 +259,10 @@ export default {
253
259
  </script>
254
260
 
255
261
  <template>
256
- <div class="outer-container">
262
+ <div
263
+ ref="wizard"
264
+ class="outer-container"
265
+ >
257
266
  <Loading
258
267
  v-if="!stepsLoaded"
259
268
  mode="relative"
@@ -397,6 +406,7 @@ export default {
397
406
  color="error"
398
407
  :label="err"
399
408
  :closable="true"
409
+ class="footer-error"
400
410
  @close="errors.splice(idx, 1)"
401
411
  />
402
412
  </div>
@@ -647,6 +657,12 @@ $spacer: 10px;
647
657
  }
648
658
  }
649
659
 
660
+ // We have to account for the absolute position of the .controls-row
661
+ .footer-error {
662
+ margin-top: -40px;
663
+ margin-bottom: 70px;
664
+ }
665
+
650
666
  .controls-row {
651
667
 
652
668
  // Overrides outlet padding
@@ -0,0 +1,63 @@
1
+ import ProjectRow from '@shell/components/form/ResourceQuota/ProjectRow.vue';
2
+ import { RANCHER_TYPES } from '@shell/components/form/ResourceQuota/shared';
3
+ import { shallowMount } from '@vue/test-utils';
4
+ import Vue from 'vue';
5
+
6
+ const CONFIGMAP_STRING = RANCHER_TYPES[0].value;
7
+
8
+ describe('component: ProjectRow.vue', () => {
9
+ const wrapper = shallowMount(ProjectRow,
10
+ {
11
+ propsData: {
12
+ mode: 'edit',
13
+ types: RANCHER_TYPES,
14
+ type: CONFIGMAP_STRING,
15
+ value: {
16
+ spec: {
17
+ namespaceDefaultResourceQuota: { limit: {} },
18
+ resourceQuota: { limit: {} }
19
+ }
20
+ }
21
+ }
22
+ });
23
+
24
+ it('should render the correct input fields and set the correct computed values, based on the provided data', () => {
25
+ const typeInput = wrapper.find(`[data-testid="projectrow-type-input"]`);
26
+ const projectQuotaInput = wrapper.find(`[data-testid="projectrow-project-quota-input"]`);
27
+ const namespaceQuotaInput = wrapper.find(`[data-testid="projectrow-namespace-quota-input"]`);
28
+
29
+ expect(typeInput.exists()).toBe(true);
30
+ expect(projectQuotaInput.exists()).toBe(true);
31
+ expect(namespaceQuotaInput.exists()).toBe(true);
32
+ expect(wrapper.vm.resourceQuotaLimit).toStrictEqual({});
33
+ expect(wrapper.vm.namespaceDefaultResourceQuotaLimit).toStrictEqual({});
34
+ });
35
+
36
+ it('triggering "updateQuotaLimit" should trigger Vue.set with the correct data', () => {
37
+ const vueSet = jest.spyOn(Vue, 'set');
38
+
39
+ wrapper.vm.updateQuotaLimit('resourceQuota', CONFIGMAP_STRING, 10);
40
+
41
+ expect(vueSet).toHaveBeenCalledTimes(1);
42
+ expect(wrapper.vm.value).toStrictEqual({
43
+ spec: {
44
+ namespaceDefaultResourceQuota: { limit: {} },
45
+ resourceQuota: { limit: { [`${ CONFIGMAP_STRING }`]: 10 } }
46
+ }
47
+ });
48
+ });
49
+
50
+ it('triggering "updateType" with the same type that existed should clear limits and trigger emit', () => {
51
+ wrapper.vm.updateType(CONFIGMAP_STRING);
52
+
53
+ expect(wrapper.vm.value).toStrictEqual({
54
+ spec: {
55
+ namespaceDefaultResourceQuota: { limit: {} },
56
+ resourceQuota: { limit: {} }
57
+ }
58
+ });
59
+
60
+ expect(wrapper.emitted('type-change')).toBeTruthy();
61
+ expect(wrapper.emitted('type-change')[0]).toStrictEqual([CONFIGMAP_STRING]);
62
+ });
63
+ });
@@ -1,4 +1,5 @@
1
1
  <script>
2
+ import { mapGetters } from 'vuex';
2
3
  import { MANAGEMENT, RBAC } from '@shell/config/types';
3
4
  import CruResource from '@shell/components/CruResource';
4
5
  import CreateEditView from '@shell/mixins/create-edit-view';
@@ -14,6 +15,7 @@ import { ucFirst } from '@shell/utils/string';
14
15
  import SortableTable from '@shell/components/SortableTable';
15
16
  import { _CLONE, _DETAIL } from '@shell/config/query-params';
16
17
  import { SCOPED_RESOURCES } from '@shell/config/roles';
18
+ import { Banner } from '@components/Banner';
17
19
 
18
20
  import { SUBTYPE_MAPPING, VERBS } from '@shell/models/management.cattle.io.roletemplate';
19
21
  import Loading from '@shell/components/Loading';
@@ -60,7 +62,8 @@ export default {
60
62
  Tabbed,
61
63
  SortableTable,
62
64
  Loading,
63
- Error
65
+ Error,
66
+ Banner
64
67
  },
65
68
 
66
69
  mixins: [CreateEditView, FormValidation],
@@ -152,12 +155,22 @@ export default {
152
155
  });
153
156
  }
154
157
 
158
+ if (this.value?.metadata?.name && !this.value.displayName) {
159
+ this.$set(this.value, 'displayName', this.value.metadata.name);
160
+ }
161
+
155
162
  this.$nextTick(() => {
156
163
  this.$emit('set-subtype', this.label);
157
164
  });
158
165
  },
159
166
 
160
167
  computed: {
168
+ ...mapGetters(['releaseNotesUrl']),
169
+
170
+ showRestrictedAdminDeprecationBanner() {
171
+ return this.value.subtype === GLOBAL && this.value.id === 'restricted-admin';
172
+ },
173
+
161
174
  label() {
162
175
  return this.t(`rbac.roletemplate.subtypes.${ this.value.subtype }.label`);
163
176
  },
@@ -537,6 +550,13 @@ export default {
537
550
  @finish="save"
538
551
  @cancel="cancel"
539
552
  >
553
+ <Banner
554
+ v-if="showRestrictedAdminDeprecationBanner"
555
+ color="warning"
556
+ class="mb-20"
557
+ >
558
+ <span v-clean-html="t('rbac.globalRoles.role.restricted-admin.deprecation', { releaseNotesUrl }, true)" />
559
+ </Banner>
540
560
  <template v-if="isDetail">
541
561
  <SortableTable
542
562
  key-field="index"
@@ -0,0 +1,41 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import RoleDetailEdit from '@shell/components/auth/RoleDetailEdit.vue';
3
+ import { SUBTYPE_MAPPING } from '@shell/models/management.cattle.io.roletemplate';
4
+
5
+ const role = {
6
+ apiVersion: 'management.cattle.io/v3',
7
+ kind: 'GlobalRole',
8
+ metadata: { name: 'global-role-with-inherited' },
9
+ inheritedClusterRoles: ['cluster-admin'],
10
+ rules:
11
+ [{
12
+ verbs: ['get', 'list'],
13
+ resources: ['pods'],
14
+ apiGroups: ['']
15
+ }],
16
+ subtype: SUBTYPE_MAPPING.GLOBAL.id
17
+ };
18
+
19
+ describe('component: RoleDetailEdit', () => {
20
+ it('does not have validation errors when the role has no displayName', () => {
21
+ const wrapper = mount(RoleDetailEdit, {
22
+ propsData: { value: role },
23
+ mocks: {
24
+ $fetchState: { pending: false },
25
+ $route: { name: 'anything' },
26
+ $store: {
27
+ getters: {
28
+ currentStore: () => 'store', 'i18n/t': jest.fn(), 'store/schemaFor': jest.fn()
29
+ }
30
+ }
31
+ },
32
+ stubs: {
33
+ CruResource: { template: '<div><slot></slot></div>' },
34
+ // NameNsDescription: true,
35
+ Tab: { template: '<div><slot></slot></div>' },
36
+ }
37
+ });
38
+
39
+ expect((wrapper.vm as any).fvFormIsValid).toBe(true);
40
+ });
41
+ });
@@ -164,6 +164,10 @@ export default {
164
164
  removeAt(this.rows, index);
165
165
  this.queueUpdate();
166
166
  },
167
+
168
+ /**
169
+ * Cleanup rows and emit input
170
+ */
167
171
  update() {
168
172
  if ( this.isView ) {
169
173
  return;
@@ -180,22 +184,24 @@ export default {
180
184
  }
181
185
  this.$emit('input', out);
182
186
  },
187
+
188
+ /**
189
+ * Handle paste event, e.g. split multiple lines in rows
190
+ */
183
191
  onPaste(index, event) {
184
- if (this.valueMultiline) {
185
- return;
186
- }
187
192
  event.preventDefault();
188
193
  const text = event.clipboardData.getData('text/plain');
189
- const split = text.split('\n').map((value) => ({ value }));
190
-
191
- if (split.length === 1) {
192
- // It's not multi-line, so don't treat it as such
193
- return;
194
- }
195
194
 
196
- event.preventDefault();
195
+ if (this.valueMultiline) {
196
+ // Allow to paste multiple lines
197
+ this.rows[index].value = text;
198
+ } else {
199
+ // Prevent to paste the value and emit text in multiple rows
200
+ const split = text.split('\n').map((value) => ({ value }));
197
201
 
198
- this.rows.splice(index, 1, ...split);
202
+ event.preventDefault();
203
+ this.rows.splice(index, 1, ...split);
204
+ }
199
205
 
200
206
  this.update();
201
207
  }
@@ -256,6 +262,7 @@ export default {
256
262
  v-if="valueMultiline"
257
263
  ref="value"
258
264
  v-model="row.value"
265
+ :data-testid="`textarea-${idx}`"
259
266
  :placeholder="valuePlaceholder"
260
267
  :mode="mode"
261
268
  :disabled="disabled"
@@ -266,6 +273,7 @@ export default {
266
273
  v-else-if="rules.length > 0"
267
274
  ref="value"
268
275
  v-model="row.value"
276
+ :data-testid="`labeled-input-${idx}`"
269
277
  :placeholder="valuePlaceholder"
270
278
  :disabled="isView || disabled"
271
279
  :rules="rules"
@@ -277,6 +285,7 @@ export default {
277
285
  v-else
278
286
  ref="value"
279
287
  v-model="row.value"
288
+ :data-testid="`input-${idx}`"
280
289
  :placeholder="valuePlaceholder"
281
290
  :disabled="isView || disabled"
282
291
  @paste="onPaste(idx, $event)"
@@ -2,6 +2,7 @@
2
2
  import Select from '@shell/components/form/Select';
3
3
  import UnitInput from '@shell/components/form/UnitInput';
4
4
  import { ROW_COMPUTED } from './shared';
5
+ import Vue from 'vue';
5
6
 
6
7
  export default {
7
8
  components: { Select, UnitInput },
@@ -57,10 +58,10 @@ export default {
57
58
 
58
59
  updateQuotaLimit(prop, type, val) {
59
60
  if (!this.value.spec[prop]) {
60
- this.value.spec[prop] = { limit: { } };
61
+ Vue.set(this.value.spec, prop, { limit: { } });
61
62
  }
62
63
 
63
- this.value.spec[prop].limit[type] = val;
64
+ Vue.set(this.value.spec[prop].limit, type, val);
64
65
  }
65
66
  },
66
67
  };
@@ -75,6 +76,7 @@ export default {
75
76
  :mode="mode"
76
77
  :value="type"
77
78
  :options="types"
79
+ data-testid="projectrow-type-input"
78
80
  @input="updateType($event)"
79
81
  />
80
82
  <UnitInput
@@ -86,6 +88,7 @@ export default {
86
88
  :input-exponent="typeOption.inputExponent"
87
89
  :base-unit="typeOption.baseUnit"
88
90
  :output-modifier="true"
91
+ data-testid="projectrow-project-quota-input"
89
92
  @input="updateQuotaLimit('resourceQuota', type, $event)"
90
93
  />
91
94
  <UnitInput
@@ -96,6 +99,7 @@ export default {
96
99
  :input-exponent="typeOption.inputExponent"
97
100
  :base-unit="typeOption.baseUnit"
98
101
  :output-modifier="true"
102
+ data-testid="projectrow-namespace-quota-input"
99
103
  @input="updateQuotaLimit('namespaceDefaultResourceQuota', type, $event)"
100
104
  />
101
105
  </div>
@@ -1,6 +1,8 @@
1
1
  import { mount } from '@vue/test-utils';
2
2
  import ArrayList from '@shell/components/form/ArrayList.vue';
3
3
  import { _EDIT, _VIEW } from '@shell/config/query-params';
4
+ import { ExtendedVue, Vue } from 'vue/types/vue';
5
+ import { DefaultProps } from 'vue/types/options';
4
6
 
5
7
  describe('the ArrayList', () => {
6
8
  it('is empty', () => {
@@ -71,4 +73,46 @@ describe('the ArrayList', () => {
71
73
 
72
74
  expect(arrayListButtons).toHaveLength(0);
73
75
  });
76
+
77
+ describe('onPaste', () => {
78
+ it('should emit value with updated row text', () => {
79
+ const text = 'test';
80
+ const expectation = [text];
81
+ const wrapper = mount(ArrayList as unknown as ExtendedVue<Vue, {}, {}, {}, DefaultProps>, { propsData: { value: [''] } });
82
+ const event = { preventDefault: jest.fn(), clipboardData: { getData: jest.fn().mockReturnValue(text) } } as any;
83
+
84
+ wrapper.vm.onPaste(0, event);
85
+
86
+ expect(wrapper.emitted().input?.[0][0]).toStrictEqual(expectation);
87
+ });
88
+
89
+ it('should emit value with multiple rows', () => {
90
+ const wrapper = mount(ArrayList as unknown as ExtendedVue<Vue, {}, {}, {}, DefaultProps>, { propsData: { value: [''] } });
91
+ const text = `multiline
92
+ rows`;
93
+ const expectation = ['multiline', 'rows'];
94
+ const event = { preventDefault: jest.fn(), clipboardData: { getData: jest.fn().mockReturnValue(text) } } as any;
95
+
96
+ wrapper.vm.onPaste(0, event);
97
+
98
+ expect(wrapper.emitted().input?.[0][0]).toStrictEqual(expectation);
99
+ });
100
+
101
+ it('should allow emit multiline pasted values if enabled', () => {
102
+ const wrapper = mount(ArrayList as unknown as ExtendedVue<Vue, {}, {}, {}, DefaultProps>, {
103
+ propsData: {
104
+ value: [''],
105
+ valueMultiline: true,
106
+ }
107
+ });
108
+ const text = `multiline
109
+ text`;
110
+ const expectation = [text];
111
+ const event = { preventDefault: jest.fn(), clipboardData: { getData: jest.fn().mockReturnValue(text) } } as any;
112
+
113
+ wrapper.vm.onPaste(0, event);
114
+
115
+ expect(wrapper.emitted().input?.[0][0]).toStrictEqual(expectation);
116
+ });
117
+ });
74
118
  });
@@ -706,7 +706,6 @@ export default {
706
706
  </template>
707
707
 
708
708
  <style lang="scss" scoped>
709
- $side-menu-logo-margin-left: 5px;
710
709
  // It would be nice to grab this from `Group.vue`, but there's margin, padding and border, which is overkill to var
711
710
  $side-menu-group-padding-left: 16px;
712
711
 
@@ -724,9 +723,11 @@ export default {
724
723
  &.isSingleProduct {
725
724
  display: flex;
726
725
  justify-content: center;
726
+
727
727
  // Align the icon with the side nav menu items ($side-menu-group-padding-left)
728
- // There's margin already in the icon component, so take that in to account ($side-menu-logo-margin-left)
729
- margin-left: $side-menu-group-padding-left - $side-menu-logo-margin-left;
728
+ .side-menu-logo {
729
+ margin-left: $side-menu-group-padding-left;
730
+ }
730
731
  }
731
732
  }
732
733
 
@@ -815,7 +816,7 @@ export default {
815
816
  display: flex;
816
817
  margin-right: 8px;
817
818
  height: 55px;
818
- margin-left: $side-menu-logo-margin-left;
819
+ margin-left: 5px;
819
820
  max-width: 200px;
820
821
  padding: 12px 0;
821
822
  }