@rancher/shell 3.0.12-rc.4 → 3.0.12-rc.5

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 (81) hide show
  1. package/assets/styles/global/_button.scss +1 -1
  2. package/assets/translations/en-us.yaml +39 -10
  3. package/components/ActionDropdownShell.vue +5 -3
  4. package/components/ButtonGroup.vue +26 -1
  5. package/components/CruResource.vue +51 -2
  6. package/components/PromptRestore.vue +93 -32
  7. package/components/Questions/index.vue +1 -0
  8. package/components/ResourceTable.vue +1 -0
  9. package/components/SortableTable/index.vue +4 -3
  10. package/components/Wizard.vue +14 -1
  11. package/components/__tests__/ButtonGroup.test.ts +56 -0
  12. package/components/__tests__/PromptRestore.test.ts +169 -19
  13. package/components/fleet/GitRepoAdvancedTab.vue +1 -0
  14. package/components/fleet/GitRepoMetadataTab.vue +5 -0
  15. package/components/fleet/HelmOpAppCoConfigTab.vue +4 -0
  16. package/components/fleet/HelmOpMetadataTab.vue +5 -0
  17. package/components/form/FileSelector.vue +39 -1
  18. package/components/form/PrivateRegistry.constants.ts +7 -0
  19. package/components/form/PrivateRegistry.vue +253 -18
  20. package/components/form/SelectOrCreateAuthSecret.vue +140 -17
  21. package/components/form/__tests__/FileSelector.test.ts +23 -0
  22. package/components/form/__tests__/PrivateRegistry.test.ts +463 -73
  23. package/components/form/__tests__/SelectOrCreateAuthSecret.test.ts +122 -0
  24. package/components/formatter/EtcdSnapshotName.vue +73 -0
  25. package/components/nav/Header.vue +8 -1
  26. package/components/templates/default.vue +7 -0
  27. package/config/features.js +1 -0
  28. package/config/labels-annotations.js +2 -0
  29. package/config/product/manager.js +6 -0
  30. package/config/secret.ts +10 -0
  31. package/config/settings.ts +6 -2
  32. package/config/types.js +7 -0
  33. package/detail/provisioning.cattle.io.cluster.vue +79 -3
  34. package/dialog/RotateEncryptionKeyDialog.vue +33 -9
  35. package/dialog/__tests__/RotateEncryptionKeyDialog.test.ts +78 -0
  36. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +92 -0
  37. package/edit/__tests__/fleet.cattle.io.helmop.test.ts +101 -0
  38. package/edit/__tests__/management.cattle.io.setting.test.ts +2 -1
  39. package/edit/compliance.cattle.io.clusterscanprofile.vue +39 -41
  40. package/edit/fleet.cattle.io.gitrepo.vue +70 -16
  41. package/edit/fleet.cattle.io.helmop.vue +51 -5
  42. package/edit/helm.cattle.io.projecthelmchart.vue +1 -0
  43. package/edit/{management.cattle.io.setting.vue → management.cattle.io.setting/index.vue} +32 -9
  44. package/edit/management.cattle.io.setting/system-default-registry-pull-secrets.vue +81 -0
  45. package/edit/provisioning.cattle.io.cluster/SelectCredential.vue +3 -12
  46. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +18 -0
  47. package/edit/provisioning.cattle.io.cluster/rke2.vue +5 -1
  48. package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +0 -1
  49. package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +14 -55
  50. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +156 -0
  51. package/models/__tests__/secret.test.ts +68 -1
  52. package/models/management.cattle.io.cluster.js +21 -3
  53. package/models/pod.js +13 -2
  54. package/models/provisioning.cattle.io.cluster.js +59 -9
  55. package/models/rke.cattle.io.etcdsnapshot.js +17 -9
  56. package/models/secret.js +19 -0
  57. package/models/workload.js +12 -7
  58. package/package.json +1 -1
  59. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +485 -107
  60. package/pages/c/_cluster/apps/charts/install.vue +114 -28
  61. package/pkg/require-asset.lib.js +25 -0
  62. package/pkg/vue.config.js +7 -0
  63. package/plugins/dashboard-store/__tests__/resource-class.test.ts +84 -0
  64. package/plugins/dashboard-store/getters.js +0 -1
  65. package/plugins/dashboard-store/resource-class.js +52 -12
  66. package/rancher-components/Form/TextArea/TextAreaAutoGrow.vue +30 -0
  67. package/rancher-components/Form/TextArea/__tests__/TextAreaAutoGrow.test.ts +95 -0
  68. package/rancher-components/RcButton/index.ts +1 -1
  69. package/rancher-components/RcDropdown/RcDropdownTrigger.vue +6 -1
  70. package/store/__tests__/features.test.ts +131 -0
  71. package/store/__tests__/growl.test.ts +374 -0
  72. package/store/__tests__/modal.test.ts +131 -0
  73. package/store/__tests__/slideInPanel.test.ts +88 -0
  74. package/store/__tests__/type-map.utils.test.ts +433 -0
  75. package/store/features.js +4 -0
  76. package/types/shell/index.d.ts +62 -0
  77. package/utils/__tests__/operation-cr.test.ts +34 -0
  78. package/utils/operation-cr.js +19 -0
  79. package/utils/require-asset.ts +7 -0
  80. package/utils/validators/__tests__/private-registry.test.ts +27 -15
  81. package/utils/validators/private-registry.ts +15 -4
@@ -1,6 +1,7 @@
1
1
  import { mount } from '@vue/test-utils';
2
2
  import { _CREATE, _EDIT, _VIEW } from '@shell/config/query-params';
3
3
  import { AUTH_TYPE } from '@shell/config/types';
4
+ import { base64Encode } from '@shell/utils/crypto';
4
5
  import GitRepo from '@shell/models/fleet.cattle.io.gitrepo';
5
6
  import GitRepoComponent from '@shell/edit/fleet.cattle.io.gitrepo.vue';
6
7
 
@@ -327,3 +328,94 @@ describe('view: fleet.cattle.io.gitrepo, GitHub password banner - should', () =>
327
328
  expect(githubBanner.exists()).toBe(shouldShowBanner);
328
329
  });
329
330
  });
331
+
332
+ describe('view: fleet.cattle.io.gitrepo, GitHub App auth - should', () => {
333
+ const originalDispatch = mockStore.dispatch;
334
+
335
+ afterEach(() => {
336
+ mockStore.dispatch = originalDispatch;
337
+ });
338
+
339
+ it('create an Opaque secret with the GitHub App data keys on doCreate', async() => {
340
+ const fakeSecret: any = {
341
+ metadata: { name: 'gitrepo-auth-abc' },
342
+ save: jest.fn().mockResolvedValue(undefined),
343
+ };
344
+
345
+ mockStore.dispatch = jest.fn().mockResolvedValue(fakeSecret);
346
+
347
+ const wrapper = mount(GitRepoComponent, initGitRepo({ mode: _CREATE }));
348
+
349
+ await (wrapper.vm as any).doCreate('clientSecretName', {
350
+ selected: AUTH_TYPE._GITHUB_APP,
351
+ githubAppId: 'app-id',
352
+ githubAppInstallationId: 'install-id',
353
+ githubAppPrivateKey: 'private-key',
354
+ });
355
+
356
+ expect(fakeSecret._type).toBe('Opaque');
357
+ expect(fakeSecret.data).toStrictEqual({
358
+ github_app_id: base64Encode('app-id'),
359
+ github_app_installation_id: base64Encode('install-id'),
360
+ github_app_private_key: base64Encode('private-key'),
361
+ });
362
+ expect(fakeSecret.save).toHaveBeenCalledWith();
363
+ });
364
+ });
365
+
366
+ describe('view: fleet.cattle.io.gitrepo, beforeNext dryRun validation', () => {
367
+ it('should call dryRunCreate when leaving stepMetadata in create mode', async() => {
368
+ const wrapper = mount(GitRepoComponent, initGitRepo({ mode: _CREATE }));
369
+ const vm = wrapper.vm as any;
370
+
371
+ vm.value.dryRunCreate = jest.fn().mockResolvedValue({});
372
+
373
+ await vm.beforeNext({ name: 'stepMetadata' });
374
+
375
+ expect(vm.value.dryRunCreate).toHaveBeenCalledWith(expect.objectContaining({
376
+ type: 'fleet.cattle.io.gitrepo',
377
+ metadata: expect.objectContaining({
378
+ name: 'test',
379
+ namespace: 'test',
380
+ }),
381
+ spec: expect.objectContaining({ repo: 'https://example.com/placeholder' }),
382
+ }));
383
+ });
384
+
385
+ it('should not call dryRunCreate for non-metadata steps', async() => {
386
+ const wrapper = mount(GitRepoComponent, initGitRepo({ mode: _CREATE }));
387
+ const vm = wrapper.vm as any;
388
+
389
+ vm.value.dryRunCreate = jest.fn();
390
+
391
+ await vm.beforeNext({ name: 'stepRepo' });
392
+
393
+ expect(vm.value.dryRunCreate).not.toHaveBeenCalled();
394
+ });
395
+
396
+ it('should not call dryRunCreate in edit mode', async() => {
397
+ const wrapper = mount(GitRepoComponent, initGitRepo({ mode: _EDIT }));
398
+ const vm = wrapper.vm as any;
399
+
400
+ vm.value.dryRunCreate = jest.fn();
401
+
402
+ await vm.beforeNext({ name: 'stepMetadata' });
403
+
404
+ expect(vm.value.dryRunCreate).not.toHaveBeenCalled();
405
+ });
406
+
407
+ it('should reject with API errors when dryRunCreate fails', async() => {
408
+ const apiError = {
409
+ _status: 409,
410
+ message: 'gitrepos.fleet.cattle.io "test" already exists',
411
+ statusText: 'Conflict',
412
+ };
413
+
414
+ const wrapper = mount(GitRepoComponent, initGitRepo({ mode: _CREATE }));
415
+ const vm = wrapper.vm as any;
416
+
417
+ vm.value.dryRunCreate = jest.fn().mockRejectedValue(apiError);
418
+
419
+ await expect(vm.beforeNext({ name: 'stepMetadata' })).rejects.toStrictEqual(apiError);
420
+ });
421
+ });
@@ -421,6 +421,12 @@ describe.each([
421
421
  expect(wrapper.vm.value.spec.downstreamResources).toStrictEqual([{ name: 'configMap2', kind: 'ConfigMap' }, { name: 'configMap3', kind: 'ConfigMap' }]);
422
422
  });
423
423
 
424
+ it('should have a beforeNext method', () => {
425
+ const wrapper = mount(HelmOpComponent, initHelmOp({ mode }));
426
+
427
+ expect(typeof wrapper.vm.beforeNext).toBe('function');
428
+ });
429
+
424
430
  if (mode === _CREATE) {
425
431
  it('should set created-by-user-id label when updateBeforeSave is called in CREATE mode', () => {
426
432
  const mockCurrentUser = { id: 'user-123' };
@@ -475,3 +481,98 @@ describe.each([
475
481
  });
476
482
  }
477
483
  });
484
+
485
+ describe('view: fleet.cattle.io.helmop, beforeNext dryRun validation', () => {
486
+ const initHelmOpWithMetadata = (props: any) => {
487
+ const value = new HelmOp({
488
+ ...mockHelmOp,
489
+ metadata: {
490
+ name: 'test-helmop',
491
+ namespace: 'fleet-default',
492
+ },
493
+ }, {
494
+ getters: { schemaFor: () => ({ linkFor: jest.fn() }) },
495
+ dispatch: jest.fn(),
496
+ rootGetters: { 'i18n/t': jest.fn() },
497
+ });
498
+
499
+ value.applyDefaults = () => {};
500
+
501
+ return {
502
+ props: {
503
+ value,
504
+ ...props
505
+ },
506
+ provide: {
507
+ store: createStore({
508
+ getters: {
509
+ currentStore: () => 'current_store',
510
+ 'management/paginationEnabled': () => () => false
511
+ }
512
+ })
513
+ },
514
+ computed: mockComputed,
515
+ global: { mocks },
516
+ };
517
+ };
518
+
519
+ it('should call dryRunCreate when leaving basics step in create mode', async() => {
520
+ const wrapper = mount(HelmOpComponent, initHelmOpWithMetadata({ mode: _CREATE }));
521
+ const vm = wrapper.vm as any;
522
+
523
+ vm.value.dryRunCreate = jest.fn().mockResolvedValue({});
524
+
525
+ await vm.beforeNext({ name: 'basics' });
526
+
527
+ expect(vm.value.dryRunCreate).toHaveBeenCalledWith(expect.objectContaining({
528
+ type: 'fleet.cattle.io.helmop',
529
+ metadata: expect.objectContaining({
530
+ name: 'test-helmop',
531
+ namespace: 'fleet-default',
532
+ }),
533
+ spec: expect.objectContaining({
534
+ helm: expect.objectContaining({
535
+ chart: 'placeholder',
536
+ repo: 'https://example.com',
537
+ }),
538
+ }),
539
+ }));
540
+ });
541
+
542
+ it('should not call dryRunCreate for non-basics steps', async() => {
543
+ const wrapper = mount(HelmOpComponent, initHelmOpWithMetadata({ mode: _CREATE }));
544
+ const vm = wrapper.vm as any;
545
+
546
+ vm.value.dryRunCreate = jest.fn();
547
+
548
+ await vm.beforeNext({ name: 'chart' });
549
+
550
+ expect(vm.value.dryRunCreate).not.toHaveBeenCalled();
551
+ });
552
+
553
+ it('should not call dryRunCreate in edit mode', async() => {
554
+ const wrapper = mount(HelmOpComponent, initHelmOpWithMetadata({ mode: _EDIT }));
555
+ const vm = wrapper.vm as any;
556
+
557
+ vm.value.dryRunCreate = jest.fn();
558
+
559
+ await vm.beforeNext({ name: 'basics' });
560
+
561
+ expect(vm.value.dryRunCreate).not.toHaveBeenCalled();
562
+ });
563
+
564
+ it('should reject with API errors when dryRunCreate fails', async() => {
565
+ const apiError = {
566
+ _status: 409,
567
+ message: 'helmops.fleet.cattle.io "test-helmop" already exists',
568
+ statusText: 'Conflict',
569
+ };
570
+
571
+ const wrapper = mount(HelmOpComponent, initHelmOpWithMetadata({ mode: _CREATE }));
572
+ const vm = wrapper.vm as any;
573
+
574
+ vm.value.dryRunCreate = jest.fn().mockRejectedValue(apiError);
575
+
576
+ await expect(vm.beforeNext({ name: 'basics' })).rejects.toStrictEqual(apiError);
577
+ });
578
+ });
@@ -1,5 +1,5 @@
1
1
  import { mount } from '@vue/test-utils';
2
- import Settings from '@shell/edit/management.cattle.io.setting.vue';
2
+ import Settings from '@shell/edit/management.cattle.io.setting/index.vue';
3
3
  import { SETTING } from '@shell/config/settings';
4
4
 
5
5
  const requiredSetup = () => ({
@@ -18,6 +18,7 @@ const requiredSetup = () => ({
18
18
  },
19
19
  $route: { query: { AS: '' } },
20
20
  $router: { applyQuery: jest.fn() },
21
+ t: (key: string) => key,
21
22
  }
22
23
  }
23
24
  });
@@ -103,48 +103,46 @@ export default {
103
103
  </script>
104
104
 
105
105
  <template>
106
- <div>
107
- <CruResource
108
- :validation-passed="!!name && !!value.spec.benchmarkVersion"
109
- :done-route="doneRoute"
110
- :resource="value"
111
- :mode="mode"
112
- :errors="errors"
113
- @finish="save"
114
- @error="e=>errors = e"
115
- >
116
- <div class="spacer" />
117
- <div class="row">
118
- <div class="col span-12">
119
- <NameNsDescription
120
- :mode="mode"
121
- :value="value"
122
- :namespaced="false"
123
- @change="name=value.metadata.name"
124
- />
125
- </div>
106
+ <CruResource
107
+ :validation-passed="!!name && !!value.spec.benchmarkVersion"
108
+ :done-route="doneRoute"
109
+ :resource="value"
110
+ :mode="mode"
111
+ :errors="errors"
112
+ @finish="save"
113
+ @error="e=>errors = e"
114
+ >
115
+ <div class="spacer" />
116
+ <div class="row">
117
+ <div class="col span-12">
118
+ <NameNsDescription
119
+ :mode="mode"
120
+ :value="value"
121
+ :namespaced="false"
122
+ @change="name=value.metadata.name"
123
+ />
126
124
  </div>
127
- <div class="row">
128
- <div class="col span-6">
129
- <LabeledSelect
130
- v-model:value="value.spec.benchmarkVersion"
131
- :mode="mode"
132
- :label="t('compliance.benchmarkVersion')"
133
- :options="compatibleBenchmarkNames"
134
- />
135
- </div>
125
+ </div>
126
+ <div class="row">
127
+ <div class="col span-6">
128
+ <LabeledSelect
129
+ v-model:value="value.spec.benchmarkVersion"
130
+ :mode="mode"
131
+ :label="t('compliance.benchmarkVersion')"
132
+ :options="compatibleBenchmarkNames"
133
+ />
136
134
  </div>
137
- <div class="spacer" />
138
- <div class="row">
139
- <div class="col span-6">
140
- <ArrayList
141
- v-model:value="value.spec.skipTests"
142
- :title="t('compliance.testsToSkip')"
143
- :value-placeholder="t('compliance.testID')"
144
- :mode="mode"
145
- />
146
- </div>
135
+ </div>
136
+ <div class="spacer" />
137
+ <div class="row">
138
+ <div class="col span-6">
139
+ <ArrayList
140
+ v-model:value="value.spec.skipTests"
141
+ :title="t('compliance.testsToSkip')"
142
+ :value-placeholder="t('compliance.testID')"
143
+ :mode="mode"
144
+ />
147
145
  </div>
148
- </CruResource>
149
- </div>
146
+ </div>
147
+ </CruResource>
150
148
  </template>
@@ -8,7 +8,7 @@ import Loading from '@shell/components/Loading';
8
8
  import { base64Decode, base64Encode } from '@shell/utils/crypto';
9
9
  import { _CREATE, _EDIT, _VIEW } from '@shell/config/query-params';
10
10
  import { CATALOG, FLEET as FLEET_LABELS } from '@shell/config/labels-annotations';
11
- import { SECRET_TYPES } from '@shell/config/secret';
11
+ import { SECRET_TYPES, GITHUB_APP_SECRET_KEYS } from '@shell/config/secret';
12
12
  import FormValidation from '@shell/mixins/form-validation';
13
13
  import { toSeconds } from '@shell/utils/duration';
14
14
  import Tab from '@shell/components/Tabbed/Tab.vue';
@@ -97,10 +97,19 @@ export default {
97
97
  refValue,
98
98
  displayHelmRepoUrlRegex: false,
99
99
  targetsCreated: '',
100
- fvFormRuleSets: [{
101
- path: 'spec.repo',
102
- rules: ['urlRepository'],
103
- }],
100
+ fvFormRuleSets: [
101
+ {
102
+ step: 'stepMetadata',
103
+ path: 'metadata.name',
104
+ rules: ['subDomain'],
105
+ translationKey: 'nameNsDescription.name.label'
106
+ },
107
+ {
108
+ step: 'stepRepo',
109
+ path: 'spec.repo',
110
+ rules: ['urlRepository'],
111
+ },
112
+ ],
104
113
  touched: null,
105
114
  };
106
115
  },
@@ -129,7 +138,7 @@ export default {
129
138
  label: this.t('fleet.gitRepo.add.steps.metadata.label'),
130
139
  subtext: this.t('fleet.gitRepo.add.steps.metadata.subtext'),
131
140
  descriptionKey: 'fleet.gitRepo.add.steps.metadata.description',
132
- ready: this.isView || !!this.value.metadata.name,
141
+ ready: this.isView || (!!this.value.metadata.name && this.stepPathErrors('stepMetadata').length === 0),
133
142
  weight: 1
134
143
  },
135
144
  {
@@ -206,6 +215,15 @@ export default {
206
215
  },
207
216
 
208
217
  methods: {
218
+ stepPathErrors(stepName) {
219
+ // Helper is used to check which validations is for each step
220
+ const paths = this.fvFormRuleSets
221
+ .filter((rule) => rule.step === stepName)
222
+ .map((rule) => rule.path);
223
+
224
+ return this.fvGetPathErrors(paths);
225
+ },
226
+
209
227
  updatePaths(value) {
210
228
  const { paths, bundles } = value;
211
229
 
@@ -283,10 +301,13 @@ export default {
283
301
  selected,
284
302
  publicKey,
285
303
  privateKey,
286
- sshKnownHosts
304
+ sshKnownHosts,
305
+ githubAppId,
306
+ githubAppInstallationId,
307
+ githubAppPrivateKey,
287
308
  } = credentials;
288
309
 
289
- if ( ![AUTH_TYPE._SSH, AUTH_TYPE._BASIC, AUTH_TYPE._S3].includes(selected) ) {
310
+ if ( ![AUTH_TYPE._SSH, AUTH_TYPE._BASIC, AUTH_TYPE._S3, AUTH_TYPE._GITHUB_APP].includes(selected) ) {
290
311
  return;
291
312
  }
292
313
 
@@ -323,19 +344,31 @@ export default {
323
344
  publicField = 'username';
324
345
  privateField = 'password';
325
346
  break;
347
+ case AUTH_TYPE._GITHUB_APP:
348
+ type = SECRET_TYPES.OPAQUE;
349
+ break;
326
350
  default:
327
351
  throw new Error('Unknown type');
328
352
  }
329
353
 
330
354
  secret._type = type;
331
- secret.data = {
332
- [publicField]: base64Encode(publicKey),
333
- [privateField]: base64Encode(privateKey),
334
- };
335
-
336
- // Add ssh known hosts
337
- if (selected === AUTH_TYPE._SSH && sshKnownHosts) {
338
- secret.data.known_hosts = base64Encode(sshKnownHosts);
355
+
356
+ if (selected === AUTH_TYPE._GITHUB_APP) {
357
+ secret.data = {
358
+ [GITHUB_APP_SECRET_KEYS.APP_ID]: base64Encode(githubAppId),
359
+ [GITHUB_APP_SECRET_KEYS.INSTALLATION_ID]: base64Encode(githubAppInstallationId),
360
+ [GITHUB_APP_SECRET_KEYS.PRIVATE_KEY]: base64Encode(githubAppPrivateKey),
361
+ };
362
+ } else {
363
+ secret.data = {
364
+ [publicField]: base64Encode(publicKey),
365
+ [privateField]: base64Encode(privateKey),
366
+ };
367
+
368
+ // Add ssh known hosts
369
+ if (selected === AUTH_TYPE._SSH && sshKnownHosts) {
370
+ secret.data.known_hosts = base64Encode(sshKnownHosts);
371
+ }
339
372
  }
340
373
  }
341
374
 
@@ -413,6 +446,25 @@ export default {
413
446
  }
414
447
  },
415
448
 
449
+ async beforeNext(activeStep) {
450
+ if (activeStep.name !== 'stepMetadata' || !this.isCreate) {
451
+ return;
452
+ }
453
+
454
+ await this.value.dryRunCreate({
455
+ type: this.value.type,
456
+ metadata: {
457
+ name: this.value.metadata.name,
458
+ namespace: this.value.metadata.namespace,
459
+ },
460
+ spec: {
461
+ repo: 'https://example.com/placeholder',
462
+ branch: 'master',
463
+ paths: [],
464
+ }
465
+ });
466
+ },
467
+
416
468
  durationSeconds(value) {
417
469
  return `${ value }s`;
418
470
  }
@@ -432,6 +484,7 @@ export default {
432
484
  :errors="errors"
433
485
  :steps="!isView ? steps : undefined"
434
486
  :finish-mode="'finish'"
487
+ :before-next="beforeNext"
435
488
  class="wizard"
436
489
  @cancel="done"
437
490
  @error="e=>errors = e"
@@ -442,6 +495,7 @@ export default {
442
495
  :value="value"
443
496
  :mode="mode"
444
497
  :is-view="isView"
498
+ :name-rules="fvGetAndReportPathRules('metadata.name')"
445
499
  @input="$emit('input', $event)"
446
500
  />
447
501
  </template>
@@ -196,7 +196,7 @@ export default {
196
196
  label: this.t('fleet.helmOp.add.steps.metadata.label'),
197
197
  subtext: this.t('fleet.helmOp.add.steps.metadata.subtext'),
198
198
  descriptionKey: 'fleet.helmOp.add.steps.metadata.description',
199
- ready: this.isView || !!this.value.metadata.name,
199
+ ready: this.isView || (!!this.value.metadata.name && this.stepPathErrors('basics').length === 0),
200
200
  weight: 1
201
201
  },
202
202
  {
@@ -443,6 +443,15 @@ export default {
443
443
  this.done();
444
444
  },
445
445
 
446
+ stepPathErrors(stepName) {
447
+ // Helper is used to check which validations is for each step
448
+ const paths = this.fvFormRuleSets
449
+ .filter((rule) => rule.step === stepName)
450
+ .map((rule) => rule.path);
451
+
452
+ return this.fvGetPathErrors(paths);
453
+ },
454
+
446
455
  onSourceTypeSelect(type) {
447
456
  if (this.isSuseAppCollection) {
448
457
  return;
@@ -659,35 +668,49 @@ export default {
659
668
  },
660
669
 
661
670
  updateValidationRules(sourceType) {
671
+ const nameRule = {
672
+ step: 'basics',
673
+ path: 'metadata.name',
674
+ rules: ['subDomain'],
675
+ translationKey: 'nameNsDescription.name.label'
676
+ };
677
+
662
678
  switch (sourceType) {
663
679
  case SOURCE_TYPE.REPO:
664
- this.fvFormRuleSets = [{
680
+ this.fvFormRuleSets = [nameRule, {
681
+ step: 'chart',
665
682
  path: 'spec.helm.repo',
666
683
  rules: ['urlRepository'],
667
684
  }, {
685
+ step: 'chart',
668
686
  path: 'spec.helm.chart',
669
687
  rules: ['required'],
670
688
  }, {
689
+ step: 'chart',
671
690
  path: 'spec.helm.version',
672
691
  rules: ['semanticVersion'],
673
692
  }];
674
693
  break;
675
694
  case SOURCE_TYPE.OCI:
676
- this.fvFormRuleSets = [{
695
+ this.fvFormRuleSets = [nameRule, {
696
+ step: 'chart',
677
697
  path: 'spec.helm.repo',
678
698
  rules: ['ociRegistry'],
679
699
  },
680
700
  ...(this.isSuseAppCollection ? [{
701
+ step: 'chart',
681
702
  path: 'spec.helm.chart',
682
703
  rules: ['required'],
683
704
  }] : []),
684
705
  {
706
+ step: 'chart',
685
707
  path: 'spec.helm.version',
686
708
  rules: this.isSuseAppCollection ? ['required', 'semanticVersion'] : ['semanticVersion'],
687
709
  }];
688
710
  break;
689
711
  case SOURCE_TYPE.TARBALL:
690
- this.fvFormRuleSets = [{
712
+ this.fvFormRuleSets = [nameRule, {
713
+ step: 'chart',
691
714
  path: 'spec.helm.chart',
692
715
  rules: ['urlRepository'],
693
716
  }];
@@ -739,6 +762,26 @@ export default {
739
762
  break;
740
763
  }
741
764
  },
765
+
766
+ async beforeNext(activeStep) {
767
+ if (activeStep.name !== 'basics' || !this.isCreate) {
768
+ return;
769
+ }
770
+
771
+ await this.value.dryRunCreate({
772
+ type: this.value.type,
773
+ metadata: {
774
+ name: this.value.metadata.name,
775
+ namespace: this.value.metadata.namespace,
776
+ },
777
+ spec: {
778
+ helm: {
779
+ chart: 'placeholder',
780
+ repo: 'https://example.com',
781
+ },
782
+ }
783
+ });
784
+ },
742
785
  },
743
786
  };
744
787
  </script>
@@ -753,11 +796,12 @@ export default {
753
796
  :mode="mode"
754
797
  :resource="value"
755
798
  :subtypes="[]"
756
- :validation-passed="true"
799
+ :validation-passed="fvFormIsValid"
757
800
  :errors="errors"
758
801
  :steps="!isView ? steps : undefined"
759
802
  :finish-mode="'finish'"
760
803
  :cancel-event="true"
804
+ :before-next="beforeNext"
761
805
  class="wizard"
762
806
  data-testid="helmop-cru-resource"
763
807
  @cancel="onCancel"
@@ -773,6 +817,7 @@ export default {
773
817
  :mode="mode"
774
818
  :is-view="isView"
775
819
  data-testid="helmop-metadata-tab"
820
+ :name-rules="fvGetAndReportPathRules('metadata.name')"
776
821
  @update:value="$emit('input', $event)"
777
822
  />
778
823
  </template>
@@ -1049,6 +1094,7 @@ export default {
1049
1094
  >
1050
1095
  <HelmOpAppCoConfigTab
1051
1096
  v-bind="appCoConfigProps"
1097
+ :name-rules="fvGetAndReportPathRules('metadata.name')"
1052
1098
  data-testid="helmop-appco-edit-config-tab"
1053
1099
  v-on="appCoConfigListeners"
1054
1100
  />
@@ -150,6 +150,7 @@ export default {
150
150
  tabbed="multiple"
151
151
  :target-namespace="value.metadata.namespace"
152
152
  :source="selectedNamespaceQuestions"
153
+ :mode="mode"
153
154
  />
154
155
  </Tabbed>
155
156
  </div>
@@ -32,13 +32,14 @@ export default {
32
32
  const t = this.$store.getters['i18n/t'];
33
33
 
34
34
  return {
35
- setting: ALLOWED_SETTINGS[this.value.id],
36
- description: t(`advancedSettings.descriptions.${ this.value.id }`),
37
- editHelp: t(`advancedSettings.editHelp.${ this.value.id }`),
38
- enumOptions: [],
39
- canReset: false,
40
- errors: [],
41
- fvFormRuleSets: [],
35
+ setting: ALLOWED_SETTINGS[this.value.id],
36
+ description: t(`advancedSettings.descriptions.${ this.value.id }`),
37
+ editHelp: t(`advancedSettings.editHelp.${ this.value.id }`),
38
+ enumOptions: [],
39
+ canReset: false,
40
+ errors: [],
41
+ fvFormRuleSets: [],
42
+ customEditComponent: null
42
43
  };
43
44
  },
44
45
 
@@ -60,6 +61,14 @@ export default {
60
61
  if (isServerUrl(this.value.id) && !this.value.default) {
61
62
  this.canReset = false;
62
63
  }
64
+
65
+ let component;
66
+
67
+ try {
68
+ component = require(`@shell/edit/management.cattle.io.setting/${ this.value.id }.vue`).default;
69
+ } catch {}
70
+
71
+ this.customEditComponent = component;
63
72
  },
64
73
 
65
74
  computed: {
@@ -195,8 +204,22 @@ export default {
195
204
  :label="err"
196
205
  data-testid="setting-error-banner"
197
206
  />
198
-
199
- <div class="mt-20">
207
+ <div
208
+ v-if="customEditComponent"
209
+ class="mt-20"
210
+ >
211
+ <component
212
+ :is="customEditComponent"
213
+ :setting-value="value.value"
214
+ :default-value="value.default"
215
+ :rules="fvGetAndReportPathRules('value')"
216
+ @update:setting-value="value.value=$event"
217
+ />
218
+ </div>
219
+ <div
220
+ v-else
221
+ class="mt-20"
222
+ >
200
223
  <div v-if="setting.kind === 'enum'">
201
224
  <LabeledSelect
202
225
  v-model:value="value.value"