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

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 (89) hide show
  1. package/apis/intf/resources-api/resource-base.ts +42 -2
  2. package/apis/intf/resources-api/resource-instance.ts +101 -0
  3. package/apis/intf/resources-api/resources-api.ts +177 -23
  4. package/apis/intf/resources.ts +8 -6
  5. package/apis/resources/__tests__/resources-api-class.test.ts +309 -28
  6. package/apis/resources/resources-api-class.ts +232 -43
  7. package/assets/images/providers/traefik.png +0 -0
  8. package/assets/translations/en-us.yaml +10 -4
  9. package/chart/__tests__/rancher-monitoring-dashboards-index.test.ts +269 -0
  10. package/chart/rancher-monitoring-dashboards/index.vue +155 -0
  11. package/cloud-credential/__tests__/generic.test.ts +132 -0
  12. package/cloud-credential/generic.vue +33 -3
  13. package/components/EtcdInfoBanner.vue +2 -12
  14. package/components/GrafanaDashboard.vue +2 -8
  15. package/components/Resource/Detail/Masthead/__tests__/index.test.ts +68 -14
  16. package/components/Resource/Detail/Masthead/index.vue +8 -3
  17. package/components/form/Security.vue +1 -1
  18. package/components/formatter/Translate.vue +22 -1
  19. package/components/nav/Group.vue +5 -10
  20. package/components/nav/TopLevelMenu.vue +1 -30
  21. package/components/nav/__tests__/Group.test.ts +61 -0
  22. package/config/labels-annotations.js +1 -0
  23. package/config/table-headers.js +5 -2
  24. package/core/__tests__/plugin-products-apply.test.ts +787 -0
  25. package/core/__tests__/plugin-products-extend.test.ts +189 -0
  26. package/core/__tests__/plugin-products-helpers.test.ts +81 -78
  27. package/core/__tests__/plugin-products-new.test.ts +389 -0
  28. package/core/__tests__/plugin-products-scenarios.test.ts +980 -0
  29. package/core/__tests__/plugin-products-side-menu.test.ts +1935 -0
  30. package/core/plugin-products-base.ts +201 -93
  31. package/core/plugin-products-extending.ts +1 -1
  32. package/core/plugin-products-external.ts +380 -0
  33. package/core/plugin-products-helpers.ts +28 -24
  34. package/core/plugin-products-internal.ts +207 -0
  35. package/core/plugin-products-top-level.ts +10 -5
  36. package/core/plugin-products-type-guards.ts +17 -6
  37. package/core/plugin-products.ts +5 -7
  38. package/core/plugin-types.ts +1 -389
  39. package/core/plugin.ts +10 -11
  40. package/core/types.ts +6 -202
  41. package/detail/__tests__/provisioning.cattle.io.cluster.test.ts +52 -0
  42. package/detail/pod.vue +1 -1
  43. package/detail/provisioning.cattle.io.cluster.vue +0 -2
  44. package/edit/auth/AuthProviderWarningBanners.vue +12 -0
  45. package/edit/auth/__tests__/AuthProviderWarningBanners.test.ts +10 -1
  46. package/edit/auth/__tests__/azuread.test.ts +1 -0
  47. package/edit/auth/__tests__/github.test.ts +1 -0
  48. package/edit/auth/__tests__/oidc.test.ts +1 -0
  49. package/edit/auth/__tests__/saml.test.ts +1 -0
  50. package/edit/auth/ldap/__tests__/index.test.ts +1 -0
  51. package/edit/provisioning.cattle.io.cluster/index.vue +1 -1
  52. package/edit/provisioning.cattle.io.cluster/rke2.vue +1 -1
  53. package/edit/provisioning.cattle.io.cluster/tabs/networking/index.vue +2 -2
  54. package/list/management.cattle.io.user.vue +24 -5
  55. package/list/utils/management.cattle.io.cluster.utils.ts +1 -1
  56. package/machine-config/__tests__/generic.test.ts +163 -0
  57. package/machine-config/components/EC2Networking.vue +31 -7
  58. package/machine-config/components/__tests__/EC2Networking.test.ts +38 -15
  59. package/machine-config/generic.vue +32 -5
  60. package/mixins/resource-fetch.js +1 -1
  61. package/models/__tests__/rke-machine.cattle.io.vmwarevspheremachinetemplate.test.ts +22 -0
  62. package/models/pod.js +12 -10
  63. package/models/rke-machine.cattle.io.vmwarevspheremachinetemplate.js +9 -0
  64. package/package.json +1 -1
  65. package/pages/auth/login.vue +1 -1
  66. package/pages/c/_cluster/apps/charts/__tests__/install.test.ts +110 -0
  67. package/pages/c/_cluster/apps/charts/install.vue +29 -0
  68. package/pages/c/_cluster/auth/config/index.vue +11 -4
  69. package/pages/c/_cluster/monitoring/index.vue +6 -1
  70. package/plugins/dashboard-store/resource-class.js +10 -3
  71. package/plugins/steve/__tests__/steve-class-resource-api.test.ts +147 -0
  72. package/plugins/steve/steve-class.js +43 -0
  73. package/plugins/steve/subscribe.js +11 -0
  74. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +115 -0
  75. package/rancher-components/Form/LabeledInput/LabeledInput.vue +83 -3
  76. package/store/__tests__/auth.test.ts +18 -0
  77. package/store/__tests__/notifications.test.ts +466 -1
  78. package/store/__tests__/ui-context.test.ts +255 -0
  79. package/store/auth.js +11 -1
  80. package/store/features.js +1 -0
  81. package/store/plugins.js +6 -0
  82. package/types/rancher/steve.api.ts +2 -2
  83. package/types/shell/index.d.ts +58 -0
  84. package/types/store/dashboard-store.types.ts +2 -2
  85. package/types/store/type-map.ts +262 -1
  86. package/utils/async.ts +2 -0
  87. package/utils/grafana.js +2 -17
  88. package/utils/monitoring.js +38 -1
  89. package/core/__tests__/plugin-products.test.ts +0 -4694
@@ -1,5 +1,6 @@
1
- import { getters, mutations, state } from '@shell/store/notifications';
1
+ import { actions, getters, mutations, state } from '@shell/store/notifications';
2
2
  import { NotificationLevel, Notification, StoredNotification } from '@shell/types/notifications';
3
+ import { encrypt, decrypt, deriveKey } from '@shell/utils/crypto/encryption';
3
4
 
4
5
  jest.mock('@shell/utils/string', () => ({ randomStr: jest.fn(() => 'mock-random-id') }));
5
6
  jest.mock('@shell/utils/crypto', () => ({ md5: jest.fn((v: string) => `hash-${ v }`) }));
@@ -431,4 +432,468 @@ describe('notifications store', () => {
431
432
  });
432
433
  });
433
434
  });
435
+
436
+ describe('actions', () => {
437
+ const mockEncrypt = encrypt as jest.Mock;
438
+ const mockDecrypt = decrypt as jest.Mock;
439
+ const mockDeriveKey = deriveKey as jest.Mock;
440
+ const mockKey = {} as CryptoKey;
441
+ let mockCommit: jest.Mock;
442
+ let mockDispatch: jest.Mock;
443
+ let mockGetters: any;
444
+
445
+ beforeEach(() => {
446
+ mockCommit = jest.fn();
447
+ mockDispatch = jest.fn();
448
+ mockGetters = {
449
+ localStorageKey: 'rancher-notifications-test',
450
+ encryptionKey: mockKey,
451
+ userId: 'user-1',
452
+ item: jest.fn(() => undefined),
453
+ all: [],
454
+ };
455
+ mockEncrypt.mockResolvedValue({ iv: 'test-iv', data: 'test-data' });
456
+ mockDecrypt.mockResolvedValue('{}');
457
+ mockDeriveKey.mockResolvedValue(mockKey);
458
+ localStorage.clear();
459
+ });
460
+
461
+ describe('add', () => {
462
+ it('generates an id via randomStr when none is provided', async() => {
463
+ const notification = makeNotification({ id: '' });
464
+
465
+ await actions.add(
466
+ {
467
+ commit: mockCommit, dispatch: mockDispatch, getters: mockGetters
468
+ },
469
+ notification
470
+ );
471
+
472
+ expect(notification.id).toStrictEqual('mock-random-id');
473
+ });
474
+
475
+ it('commits add with the notification', async() => {
476
+ const notification = makeNotification({ id: 'n1' });
477
+
478
+ await actions.add(
479
+ {
480
+ commit: mockCommit, dispatch: mockDispatch, getters: mockGetters
481
+ },
482
+ notification
483
+ );
484
+
485
+ expect(mockCommit).toHaveBeenCalledWith('add', notification);
486
+ });
487
+
488
+ it('dispatches growl/notification with root:true', async() => {
489
+ const notification = makeNotification({ id: 'n1' });
490
+
491
+ await actions.add(
492
+ {
493
+ commit: mockCommit, dispatch: mockDispatch, getters: mockGetters
494
+ },
495
+ notification
496
+ );
497
+
498
+ expect(mockDispatch).toHaveBeenCalledWith('growl/notification', notification, { root: true });
499
+ });
500
+
501
+ it('returns the notification id', async() => {
502
+ const result = await actions.add(
503
+ {
504
+ commit: mockCommit, dispatch: mockDispatch, getters: mockGetters
505
+ },
506
+ makeNotification({ id: 'n1' })
507
+ );
508
+
509
+ expect(result).toStrictEqual('n1');
510
+ });
511
+
512
+ it('saves the encrypted notification to localStorage', async() => {
513
+ const setItemSpy = jest.spyOn(Storage.prototype, 'setItem');
514
+
515
+ await actions.add(
516
+ {
517
+ commit: mockCommit, dispatch: mockDispatch, getters: mockGetters
518
+ },
519
+ makeNotification({ id: 'n1' })
520
+ );
521
+
522
+ expect(setItemSpy).toHaveBeenCalledWith(
523
+ 'rancher-notifications-test-n1',
524
+ JSON.stringify({ iv: 'test-iv', data: 'test-data' })
525
+ );
526
+ setItemSpy.mockRestore();
527
+ });
528
+ });
529
+
530
+ describe('fromGrowl', () => {
531
+ it('always assigns a new id via randomStr', async() => {
532
+ const notification = makeNotification({ id: 'original-id' });
533
+
534
+ await actions.fromGrowl({ commit: mockCommit, getters: mockGetters }, notification);
535
+
536
+ expect(notification.id).toStrictEqual('mock-random-id');
537
+ });
538
+
539
+ it('commits add with the notification', async() => {
540
+ const notification = makeNotification({ id: 'n1' });
541
+
542
+ await actions.fromGrowl({ commit: mockCommit, getters: mockGetters }, notification);
543
+
544
+ expect(mockCommit).toHaveBeenCalledWith('add', notification);
545
+ });
546
+
547
+ it('returns the new notification id', async() => {
548
+ const result = await actions.fromGrowl(
549
+ { commit: mockCommit, getters: mockGetters },
550
+ makeNotification({ id: 'original' })
551
+ );
552
+
553
+ expect(result).toStrictEqual('mock-random-id');
554
+ });
555
+ });
556
+
557
+ describe('update', () => {
558
+ it('commits update with the notification', () => {
559
+ const notification = makeNotification({ id: 'n1', title: 'updated' });
560
+
561
+ actions.update({ commit: mockCommit, getters: mockGetters }, notification);
562
+
563
+ expect(mockCommit).toHaveBeenCalledWith('update', notification);
564
+ });
565
+ });
566
+
567
+ describe('markRead', () => {
568
+ it('commits markRead with the notification id', async() => {
569
+ await actions.markRead(
570
+ {
571
+ commit: mockCommit, dispatch: mockDispatch, getters: mockGetters
572
+ },
573
+ 'n1'
574
+ );
575
+
576
+ expect(mockCommit).toHaveBeenCalledWith('markRead', 'n1');
577
+ });
578
+
579
+ it('dispatches prefs/set when the notification has a preference', async() => {
580
+ const pref = { key: 'some-pref', value: 'on' };
581
+
582
+ mockGetters.item = jest.fn(() => makeStoredNotification({ id: 'n1', preference: pref }));
583
+
584
+ await actions.markRead(
585
+ {
586
+ commit: mockCommit, dispatch: mockDispatch, getters: mockGetters
587
+ },
588
+ 'n1'
589
+ );
590
+
591
+ expect(mockDispatch).toHaveBeenCalledWith('prefs/set', pref, { root: true });
592
+ });
593
+
594
+ it('does not dispatch when the notification has no preference', async() => {
595
+ mockGetters.item = jest.fn(() => makeStoredNotification({ id: 'n1' }));
596
+
597
+ await actions.markRead(
598
+ {
599
+ commit: mockCommit, dispatch: mockDispatch, getters: mockGetters
600
+ },
601
+ 'n1'
602
+ );
603
+
604
+ expect(mockDispatch).not.toHaveBeenCalled();
605
+ });
606
+
607
+ it('calls the extension handler when the notification has a handlerName', async() => {
608
+ const mockOnReadUpdated = jest.fn().mockResolvedValue(undefined);
609
+ const mockExtension = { getDynamic: jest.fn(() => ({ onReadUpdated: mockOnReadUpdated })) };
610
+
611
+ mockGetters.item = jest.fn(() => makeStoredNotification({ id: 'n1', handlerName: 'my-handler' }));
612
+
613
+ await actions.markRead.call(
614
+ { $extension: mockExtension },
615
+ {
616
+ commit: mockCommit, dispatch: mockDispatch, getters: mockGetters
617
+ },
618
+ 'n1'
619
+ );
620
+
621
+ expect(mockOnReadUpdated).toHaveBeenCalledWith(
622
+ expect.objectContaining({ id: 'n1' }),
623
+ true
624
+ );
625
+ });
626
+ });
627
+
628
+ describe('markUnread', () => {
629
+ it('commits markUnread with the notification id', async() => {
630
+ await actions.markUnread(
631
+ {
632
+ commit: mockCommit, dispatch: mockDispatch, getters: mockGetters
633
+ },
634
+ 'n1'
635
+ );
636
+
637
+ expect(mockCommit).toHaveBeenCalledWith('markUnread', 'n1');
638
+ });
639
+
640
+ it.each([
641
+ {
642
+ desc: 'uses unsetValue when the preference has one',
643
+ pref: {
644
+ key: 'k', value: 'on', unsetValue: 'off'
645
+ },
646
+ expected: { key: 'k', value: 'off' },
647
+ },
648
+ {
649
+ desc: 'falls back to empty string when no unsetValue is set',
650
+ pref: { key: 'k', value: 'on' },
651
+ expected: { key: 'k', value: '' },
652
+ },
653
+ ])('dispatches prefs/set: $desc', async({ pref, expected }) => {
654
+ mockGetters.item = jest.fn(() => makeStoredNotification({ id: 'n1', preference: pref }));
655
+
656
+ await actions.markUnread(
657
+ {
658
+ commit: mockCommit, dispatch: mockDispatch, getters: mockGetters
659
+ },
660
+ 'n1'
661
+ );
662
+
663
+ expect(mockDispatch).toHaveBeenCalledWith('prefs/set', expected, { root: true });
664
+ });
665
+ });
666
+
667
+ describe('markAllRead', () => {
668
+ it('commits markAllRead', async() => {
669
+ await actions.markAllRead({
670
+ commit: mockCommit, dispatch: mockDispatch, getters: mockGetters
671
+ });
672
+
673
+ expect(mockCommit).toHaveBeenCalledWith('markAllRead');
674
+ });
675
+
676
+ it('dispatches prefs/set for every notification with a preference', async() => {
677
+ const pref1 = { key: 'pref-1', value: 'on' };
678
+ const pref2 = { key: 'pref-2', value: 'off' };
679
+
680
+ mockGetters.all = [
681
+ makeStoredNotification({ id: 'n1', preference: pref1 }),
682
+ makeStoredNotification({ id: 'n2' }),
683
+ makeStoredNotification({ id: 'n3', preference: pref2 }),
684
+ ];
685
+
686
+ await actions.markAllRead({
687
+ commit: mockCommit, dispatch: mockDispatch, getters: mockGetters
688
+ });
689
+
690
+ expect(mockDispatch).toHaveBeenCalledWith('prefs/set', pref1, { root: true });
691
+ expect(mockDispatch).toHaveBeenCalledWith('prefs/set', pref2, { root: true });
692
+ expect(mockDispatch).toHaveBeenCalledTimes(2);
693
+ });
694
+
695
+ it('calls handlers for notifications with a handlerName', async() => {
696
+ const mockOnReadUpdated = jest.fn().mockResolvedValue(undefined);
697
+ const mockExtension = { getDynamic: jest.fn(() => ({ onReadUpdated: mockOnReadUpdated })) };
698
+
699
+ mockGetters.all = [
700
+ makeStoredNotification({ id: 'n1', handlerName: 'handler-a' }),
701
+ makeStoredNotification({ id: 'n2' }),
702
+ ];
703
+
704
+ await actions.markAllRead.call(
705
+ { $extension: mockExtension },
706
+ {
707
+ commit: mockCommit, dispatch: mockDispatch, getters: mockGetters
708
+ }
709
+ );
710
+
711
+ expect(mockOnReadUpdated).toHaveBeenCalledTimes(1);
712
+ expect(mockOnReadUpdated).toHaveBeenCalledWith(
713
+ expect.objectContaining({ id: 'n1' }),
714
+ true
715
+ );
716
+ });
717
+ });
718
+
719
+ describe('remove', () => {
720
+ it('commits remove with the notification id', () => {
721
+ actions.remove({ commit: mockCommit, getters: mockGetters }, 'n1');
722
+
723
+ expect(mockCommit).toHaveBeenCalledWith('remove', 'n1');
724
+ });
725
+ });
726
+
727
+ describe('clearAll', () => {
728
+ it('commits clearAll', () => {
729
+ actions.clearAll({ commit: mockCommit, getters: mockGetters });
730
+
731
+ expect(mockCommit).toHaveBeenCalledWith('clearAll');
732
+ });
733
+ });
734
+
735
+ describe('init', () => {
736
+ let MockBroadcastChannel: jest.Mock;
737
+ let mockBcInstance: any;
738
+
739
+ beforeEach(() => {
740
+ mockBcInstance = {
741
+ onmessage: null as any,
742
+ postMessage: jest.fn(),
743
+ };
744
+ MockBroadcastChannel = jest.fn(() => mockBcInstance);
745
+ (global as any).BroadcastChannel = MockBroadcastChannel;
746
+ });
747
+
748
+ it.each([
749
+ {
750
+ desc: 'userKey is missing',
751
+ userData: { id: '', user: { metadata: { uid: 'u1' } } },
752
+ },
753
+ {
754
+ desc: 'userId is missing',
755
+ userData: { id: 'key1', user: { metadata: {} } },
756
+ },
757
+ ])('returns without committing when $desc', async({ userData }) => {
758
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
759
+
760
+ await actions.init({ commit: mockCommit, getters: mockGetters }, userData);
761
+
762
+ expect(mockCommit).not.toHaveBeenCalled();
763
+ consoleErrorSpy.mockRestore();
764
+ });
765
+
766
+ it('commits localStorageKey and userId for valid user data', async() => {
767
+ await actions.init({ commit: mockCommit, getters: mockGetters }, {
768
+ id: 'user-key',
769
+ user: { metadata: { uid: 'user-uid' } },
770
+ });
771
+
772
+ expect(mockCommit).toHaveBeenCalledWith('localStorageKey', 'hash-user-key');
773
+ expect(mockCommit).toHaveBeenCalledWith('userId', 'user-uid');
774
+ });
775
+
776
+ it('commits encryptionKey from the deriveKey result', async() => {
777
+ await actions.init({ commit: mockCommit, getters: mockGetters }, {
778
+ id: 'user-key',
779
+ user: { metadata: { uid: 'user-uid' } },
780
+ });
781
+
782
+ expect(mockCommit).toHaveBeenCalledWith('encryptionKey', mockKey);
783
+ });
784
+
785
+ it('returns early without loading notifications when deriveKey fails', async() => {
786
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
787
+
788
+ mockDeriveKey.mockRejectedValue(new Error('derive failed'));
789
+
790
+ await actions.init({ commit: mockCommit, getters: mockGetters }, {
791
+ id: 'user-key',
792
+ user: { metadata: { uid: 'user-uid' } },
793
+ });
794
+
795
+ expect(mockCommit).not.toHaveBeenCalledWith('load', expect.anything());
796
+ consoleErrorSpy.mockRestore();
797
+ });
798
+
799
+ it('loads and decrypts notifications from localStorage', async() => {
800
+ const createdDate = new Date().toISOString();
801
+ const indexEntry = {
802
+ id: 'n1',
803
+ created: createdDate,
804
+ read: false,
805
+ };
806
+
807
+ mockGetters.localStorageKey = 'rancher-notifications-test';
808
+ localStorage.setItem('rancher-notifications-test', JSON.stringify([indexEntry]));
809
+ localStorage.setItem('rancher-notifications-test-n1', JSON.stringify({ iv: 'iv', data: 'enc' }));
810
+ mockDecrypt.mockResolvedValue(JSON.stringify({ title: 'decrypted', level: NotificationLevel.Info }));
811
+
812
+ await actions.init({ commit: mockCommit, getters: mockGetters }, {
813
+ id: 'user-key',
814
+ user: { metadata: { uid: 'user-uid' } },
815
+ });
816
+
817
+ const loadCall = mockCommit.mock.calls.find((c) => c[0] === 'load');
818
+
819
+ expect(loadCall).toBeDefined();
820
+ expect(loadCall![1]).toStrictEqual([{
821
+ id: 'n1',
822
+ created: createdDate,
823
+ read: false,
824
+ title: 'decrypted',
825
+ level: NotificationLevel.Info,
826
+ }]);
827
+ });
828
+
829
+ it('filters out notifications older than 14 days', async() => {
830
+ const expiredDate = new Date(Date.now() - 15 * 24 * 60 * 60 * 1000).toISOString();
831
+ const indexEntry = {
832
+ id: 'old',
833
+ created: expiredDate,
834
+ read: false,
835
+ };
836
+
837
+ mockGetters.localStorageKey = 'rancher-notifications-test';
838
+ localStorage.setItem('rancher-notifications-test', JSON.stringify([indexEntry]));
839
+ localStorage.setItem('rancher-notifications-test-old', JSON.stringify({ iv: 'iv', data: 'enc' }));
840
+
841
+ await actions.init({ commit: mockCommit, getters: mockGetters }, {
842
+ id: 'user-key',
843
+ user: { metadata: { uid: 'user-uid' } },
844
+ });
845
+
846
+ const loadCall = mockCommit.mock.calls.find((c) => c[0] === 'load');
847
+
848
+ expect(loadCall).toBeDefined();
849
+ expect(loadCall![1]).toStrictEqual([]);
850
+ });
851
+
852
+ it('sets up a BroadcastChannel listener', async() => {
853
+ await actions.init({ commit: mockCommit, getters: mockGetters }, {
854
+ id: 'user-key',
855
+ user: { metadata: { uid: 'user-uid' } },
856
+ });
857
+
858
+ expect(MockBroadcastChannel).toHaveBeenCalledWith('rancher-notification-sync');
859
+ expect(typeof mockBcInstance.onmessage).toStrictEqual('function');
860
+ });
861
+
862
+ it('commits the operation from a matching broadcast message', async() => {
863
+ await actions.init({ commit: mockCommit, getters: mockGetters }, {
864
+ id: 'user-key',
865
+ user: { metadata: { uid: 'user-uid' } },
866
+ });
867
+
868
+ mockCommit.mockClear();
869
+ mockBcInstance.onmessage({
870
+ data: {
871
+ userId: 'user-uid',
872
+ operation: 'add',
873
+ param: { id: 'n99' },
874
+ },
875
+ });
876
+
877
+ expect(mockCommit).toHaveBeenCalledWith('add', { id: 'n99' });
878
+ });
879
+
880
+ it('ignores broadcast messages when the userId does not match', async() => {
881
+ await actions.init({ commit: mockCommit, getters: mockGetters }, {
882
+ id: 'user-key',
883
+ user: { metadata: { uid: 'user-uid' } },
884
+ });
885
+
886
+ mockCommit.mockClear();
887
+ mockBcInstance.onmessage({
888
+ data: {
889
+ userId: 'other-user',
890
+ operation: 'add',
891
+ param: { id: 'n99' },
892
+ },
893
+ });
894
+
895
+ expect(mockCommit).not.toHaveBeenCalled();
896
+ });
897
+ });
898
+ });
434
899
  });