datajunction-ui 0.0.55 → 0.0.57

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 (30) hide show
  1. package/package.json +1 -1
  2. package/src/app/components/NamespaceHeader.jsx +423 -12
  3. package/src/app/components/NodeListActions.jsx +10 -7
  4. package/src/app/components/__tests__/GitModals.test.jsx +1293 -0
  5. package/src/app/components/__tests__/NamespaceHeader.test.jsx +905 -16
  6. package/src/app/components/__tests__/NodeListActions.test.jsx +5 -3
  7. package/src/app/components/__tests__/__snapshots__/NamespaceHeader.test.jsx.snap +41 -0
  8. package/src/app/components/git/CreateBranchModal.jsx +229 -0
  9. package/src/app/components/git/CreatePRModal.jsx +270 -0
  10. package/src/app/components/git/DeleteBranchModal.jsx +173 -0
  11. package/src/app/components/git/GitSettingsModal.jsx +375 -0
  12. package/src/app/components/git/SyncToGitModal.jsx +219 -0
  13. package/src/app/components/git/__tests__/GitSettingsModal.test.jsx +301 -0
  14. package/src/app/components/git/index.js +5 -0
  15. package/src/app/icons/DeleteIcon.jsx +3 -3
  16. package/src/app/icons/EditIcon.jsx +3 -3
  17. package/src/app/icons/EyeIcon.jsx +3 -4
  18. package/src/app/icons/JupyterExportIcon.jsx +3 -7
  19. package/src/app/icons/PythonIcon.jsx +3 -3
  20. package/src/app/pages/AddEditNodePage/index.jsx +8 -5
  21. package/src/app/pages/NamespacePage/index.jsx +34 -21
  22. package/src/app/pages/NodePage/ClientCodePopover.jsx +3 -7
  23. package/src/app/pages/NodePage/NodeInfoTab.jsx +10 -3
  24. package/src/app/pages/NodePage/NotebookDownload.jsx +4 -10
  25. package/src/app/pages/NodePage/WatchNodeButton.jsx +7 -12
  26. package/src/app/pages/NodePage/index.jsx +42 -13
  27. package/src/app/services/DJService.js +218 -1
  28. package/src/styles/index.css +3 -0
  29. package/src/styles/node-creation.scss +22 -0
  30. package/src/styles/settings.css +1 -1
@@ -42,8 +42,8 @@ describe('<NamespaceHeader />', () => {
42
42
  );
43
43
  });
44
44
 
45
- // Should render Git Managed badge for git source
46
- expect(screen.getByText(/Git Managed/)).toBeInTheDocument();
45
+ // Should render Deployed from Git badge for git source
46
+ expect(screen.getByText(/Deployed from Git/)).toBeInTheDocument();
47
47
  });
48
48
 
49
49
  it('should render git source badge when source type is git without branch', async () => {
@@ -73,8 +73,8 @@ describe('<NamespaceHeader />', () => {
73
73
  );
74
74
  });
75
75
 
76
- // Should render Git Managed badge for git source even without branch
77
- expect(screen.getByText(/Git Managed/)).toBeInTheDocument();
76
+ // Should render Deployed from Git badge for git source even without branch
77
+ expect(screen.getByText(/Deployed from Git/)).toBeInTheDocument();
78
78
  });
79
79
 
80
80
  it('should render local source badge when source type is local', async () => {
@@ -131,7 +131,7 @@ describe('<NamespaceHeader />', () => {
131
131
  });
132
132
 
133
133
  // Should not render any source badge
134
- expect(screen.queryByText(/Git Managed/)).not.toBeInTheDocument();
134
+ expect(screen.queryByText(/Deployed from Git/)).not.toBeInTheDocument();
135
135
  expect(screen.queryByText(/Local Deploy/)).not.toBeInTheDocument();
136
136
  });
137
137
 
@@ -158,7 +158,7 @@ describe('<NamespaceHeader />', () => {
158
158
  // Should still render breadcrumb without badge
159
159
  expect(screen.getByText('test')).toBeInTheDocument();
160
160
  expect(screen.getByText('namespace')).toBeInTheDocument();
161
- expect(screen.queryByText(/Git Managed/)).not.toBeInTheDocument();
161
+ expect(screen.queryByText(/Deployed from Git/)).not.toBeInTheDocument();
162
162
  });
163
163
 
164
164
  it('should open dropdown when clicking the git managed button', async () => {
@@ -195,11 +195,11 @@ describe('<NamespaceHeader />', () => {
195
195
  );
196
196
 
197
197
  await waitFor(() => {
198
- expect(screen.getByText(/Git Managed/)).toBeInTheDocument();
198
+ expect(screen.getByText(/Deployed from Git/)).toBeInTheDocument();
199
199
  });
200
200
 
201
201
  // Click the dropdown button
202
- fireEvent.click(screen.getByText(/Git Managed/));
202
+ fireEvent.click(screen.getByText(/Deployed from Git/));
203
203
 
204
204
  // Should show repository link in dropdown
205
205
  await waitFor(() => {
@@ -297,10 +297,10 @@ describe('<NamespaceHeader />', () => {
297
297
  );
298
298
 
299
299
  await waitFor(() => {
300
- expect(screen.getByText(/Git Managed/)).toBeInTheDocument();
300
+ expect(screen.getByText(/Deployed from Git/)).toBeInTheDocument();
301
301
  });
302
302
 
303
- fireEvent.click(screen.getByText(/Git Managed/));
303
+ fireEvent.click(screen.getByText(/Deployed from Git/));
304
304
 
305
305
  // Should show branch names in deployment list
306
306
  await waitFor(() => {
@@ -375,11 +375,11 @@ describe('<NamespaceHeader />', () => {
375
375
  );
376
376
 
377
377
  await waitFor(() => {
378
- expect(screen.getByText(/Git Managed/)).toBeInTheDocument();
378
+ expect(screen.getByText(/Deployed from Git/)).toBeInTheDocument();
379
379
  });
380
380
 
381
381
  // Open dropdown
382
- fireEvent.click(screen.getByText(/Git Managed/));
382
+ fireEvent.click(screen.getByText(/Deployed from Git/));
383
383
 
384
384
  await waitFor(() => {
385
385
  expect(screen.getByText(/github.com\/test\/repo/)).toBeInTheDocument();
@@ -418,14 +418,14 @@ describe('<NamespaceHeader />', () => {
418
418
  );
419
419
 
420
420
  await waitFor(() => {
421
- expect(screen.getByText(/Git Managed/)).toBeInTheDocument();
421
+ expect(screen.getByText(/Deployed from Git/)).toBeInTheDocument();
422
422
  });
423
423
 
424
424
  // Initially shows down arrow
425
425
  expect(screen.getByText('▼')).toBeInTheDocument();
426
426
 
427
427
  // Click to open
428
- fireEvent.click(screen.getByText(/Git Managed/));
428
+ fireEvent.click(screen.getByText(/Deployed from Git/));
429
429
 
430
430
  // Should show up arrow when open
431
431
  await waitFor(() => {
@@ -455,10 +455,10 @@ describe('<NamespaceHeader />', () => {
455
455
  );
456
456
 
457
457
  await waitFor(() => {
458
- expect(screen.getByText(/Git Managed/)).toBeInTheDocument();
458
+ expect(screen.getByText(/Deployed from Git/)).toBeInTheDocument();
459
459
  });
460
460
 
461
- fireEvent.click(screen.getByText(/Git Managed/));
461
+ fireEvent.click(screen.getByText(/Deployed from Git/));
462
462
 
463
463
  await waitFor(() => {
464
464
  // Find link by its text content (repository URL)
@@ -508,4 +508,893 @@ describe('<NamespaceHeader />', () => {
508
508
  expect(screen.getByText(/Local\/adhoc deployments/)).toBeInTheDocument();
509
509
  });
510
510
  });
511
+
512
+ it('should show Git Settings button and open modal', async () => {
513
+ const mockDjClient = {
514
+ namespaceSources: jest.fn().mockResolvedValue({
515
+ total_deployments: 0,
516
+ primary_source: null,
517
+ }),
518
+ listDeployments: jest.fn().mockResolvedValue([]),
519
+ getNamespaceGitConfig: jest.fn().mockResolvedValue(null),
520
+ };
521
+
522
+ render(
523
+ <MemoryRouter>
524
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
525
+ <NamespaceHeader namespace="test.namespace" />
526
+ </DJClientContext.Provider>
527
+ </MemoryRouter>,
528
+ );
529
+
530
+ await waitFor(() => {
531
+ expect(screen.getByText('Git Settings')).toBeInTheDocument();
532
+ });
533
+
534
+ fireEvent.click(screen.getByText('Git Settings'));
535
+
536
+ await waitFor(() => {
537
+ expect(screen.getByText('Git Configuration')).toBeInTheDocument();
538
+ });
539
+ });
540
+
541
+ it('should show git action buttons when git is configured', async () => {
542
+ const mockDjClient = {
543
+ namespaceSources: jest.fn().mockResolvedValue({
544
+ total_deployments: 1,
545
+ primary_source: {
546
+ type: 'git',
547
+ repository: 'test/repo',
548
+ branch: 'main',
549
+ },
550
+ }),
551
+ listDeployments: jest.fn().mockResolvedValue([]),
552
+ getNamespaceGitConfig: jest.fn().mockResolvedValue({
553
+ github_repo_path: 'test/repo',
554
+ git_branch: 'main',
555
+ git_path: 'nodes/',
556
+ git_only: false,
557
+ }),
558
+ };
559
+
560
+ render(
561
+ <MemoryRouter>
562
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
563
+ <NamespaceHeader namespace="test.namespace" />
564
+ </DJClientContext.Provider>
565
+ </MemoryRouter>,
566
+ );
567
+
568
+ // For non-branch namespaces, button is labeled "New Branch"
569
+ await waitFor(() => {
570
+ expect(screen.getByText('New Branch')).toBeInTheDocument();
571
+ });
572
+ });
573
+
574
+ it('should show Create PR and Delete Branch for branch namespaces', async () => {
575
+ const mockDjClient = {
576
+ namespaceSources: jest.fn().mockResolvedValue({
577
+ total_deployments: 1,
578
+ primary_source: {
579
+ type: 'git',
580
+ repository: 'test/repo',
581
+ branch: 'feature',
582
+ },
583
+ }),
584
+ listDeployments: jest.fn().mockResolvedValue([]),
585
+ getNamespaceGitConfig: jest
586
+ .fn()
587
+ .mockResolvedValueOnce({
588
+ github_repo_path: 'test/repo',
589
+ git_branch: 'feature',
590
+ git_path: 'nodes/',
591
+ git_only: false,
592
+ parent_namespace: 'test.main',
593
+ })
594
+ .mockResolvedValueOnce({
595
+ github_repo_path: 'test/repo',
596
+ git_branch: 'main',
597
+ git_path: 'nodes/',
598
+ }),
599
+ getPullRequest: jest.fn().mockResolvedValue(null),
600
+ };
601
+
602
+ render(
603
+ <MemoryRouter>
604
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
605
+ <NamespaceHeader namespace="test.feature" />
606
+ </DJClientContext.Provider>
607
+ </MemoryRouter>,
608
+ );
609
+
610
+ await waitFor(() => {
611
+ expect(screen.getByText('Create PR')).toBeInTheDocument();
612
+ });
613
+ // Delete Branch button only has an icon with title attribute
614
+ expect(screen.getByTitle('Delete Branch')).toBeInTheDocument();
615
+ });
616
+
617
+ it('should open Create Branch modal when button is clicked', async () => {
618
+ const mockDjClient = {
619
+ namespaceSources: jest.fn().mockResolvedValue({
620
+ total_deployments: 1,
621
+ primary_source: {
622
+ type: 'git',
623
+ repository: 'test/repo',
624
+ branch: 'main',
625
+ },
626
+ }),
627
+ listDeployments: jest.fn().mockResolvedValue([]),
628
+ getNamespaceGitConfig: jest.fn().mockResolvedValue({
629
+ github_repo_path: 'test/repo',
630
+ git_branch: 'main',
631
+ git_path: 'nodes/',
632
+ git_only: false,
633
+ }),
634
+ };
635
+
636
+ render(
637
+ <MemoryRouter>
638
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
639
+ <NamespaceHeader namespace="test.namespace" />
640
+ </DJClientContext.Provider>
641
+ </MemoryRouter>,
642
+ );
643
+
644
+ await waitFor(() => {
645
+ expect(screen.getByText('New Branch')).toBeInTheDocument();
646
+ });
647
+
648
+ fireEvent.click(screen.getByText('New Branch'));
649
+
650
+ await waitFor(() => {
651
+ expect(screen.getByLabelText('Branch Name')).toBeInTheDocument();
652
+ });
653
+ });
654
+
655
+ it('should open Sync to Git modal when button is clicked', async () => {
656
+ // Sync to Git only shows for branch namespaces
657
+ const mockDjClient = {
658
+ namespaceSources: jest.fn().mockResolvedValue({
659
+ total_deployments: 1,
660
+ primary_source: {
661
+ type: 'git',
662
+ repository: 'test/repo',
663
+ branch: 'feature',
664
+ },
665
+ }),
666
+ listDeployments: jest.fn().mockResolvedValue([]),
667
+ getNamespaceGitConfig: jest
668
+ .fn()
669
+ .mockResolvedValueOnce({
670
+ github_repo_path: 'test/repo',
671
+ git_branch: 'feature',
672
+ git_path: 'nodes/',
673
+ git_only: false,
674
+ parent_namespace: 'test.main',
675
+ })
676
+ .mockResolvedValueOnce({
677
+ github_repo_path: 'test/repo',
678
+ git_branch: 'main',
679
+ git_path: 'nodes/',
680
+ }),
681
+ getPullRequest: jest.fn().mockResolvedValue(null),
682
+ };
683
+
684
+ render(
685
+ <MemoryRouter>
686
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
687
+ <NamespaceHeader namespace="test.feature" />
688
+ </DJClientContext.Provider>
689
+ </MemoryRouter>,
690
+ );
691
+
692
+ await waitFor(() => {
693
+ expect(screen.getByText('Sync to Git')).toBeInTheDocument();
694
+ });
695
+
696
+ fireEvent.click(screen.getByText('Sync to Git'));
697
+
698
+ await waitFor(() => {
699
+ expect(screen.getByText(/Sync all nodes in/)).toBeInTheDocument();
700
+ });
701
+ });
702
+
703
+ it('should call updateNamespaceGitConfig when saving git settings', async () => {
704
+ const mockDjClient = {
705
+ namespaceSources: jest.fn().mockResolvedValue({
706
+ total_deployments: 0,
707
+ primary_source: null,
708
+ }),
709
+ listDeployments: jest.fn().mockResolvedValue([]),
710
+ getNamespaceGitConfig: jest.fn().mockResolvedValue(null),
711
+ updateNamespaceGitConfig: jest.fn().mockResolvedValue({
712
+ github_repo_path: 'myorg/repo',
713
+ git_branch: 'main',
714
+ git_path: 'nodes/',
715
+ git_only: true,
716
+ }),
717
+ };
718
+
719
+ render(
720
+ <MemoryRouter>
721
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
722
+ <NamespaceHeader namespace="test.namespace" />
723
+ </DJClientContext.Provider>
724
+ </MemoryRouter>,
725
+ );
726
+
727
+ await waitFor(() => {
728
+ expect(screen.getByText('Git Settings')).toBeInTheDocument();
729
+ });
730
+
731
+ fireEvent.click(screen.getByText('Git Settings'));
732
+
733
+ await waitFor(() => {
734
+ expect(screen.getByLabelText('Repository')).toBeInTheDocument();
735
+ });
736
+
737
+ fireEvent.change(screen.getByLabelText('Repository'), {
738
+ target: { value: 'myorg/repo' },
739
+ });
740
+ fireEvent.change(screen.getByLabelText('Branch'), {
741
+ target: { value: 'main' },
742
+ });
743
+
744
+ fireEvent.click(screen.getByText('Save Settings'));
745
+
746
+ await waitFor(() => {
747
+ expect(mockDjClient.updateNamespaceGitConfig).toHaveBeenCalledWith(
748
+ 'test.namespace',
749
+ expect.objectContaining({
750
+ github_repo_path: 'myorg/repo',
751
+ git_branch: 'main',
752
+ }),
753
+ );
754
+ });
755
+ });
756
+
757
+ it('should call createBranch when creating a branch', async () => {
758
+ const mockDjClient = {
759
+ namespaceSources: jest.fn().mockResolvedValue({
760
+ total_deployments: 1,
761
+ primary_source: {
762
+ type: 'git',
763
+ repository: 'test/repo',
764
+ branch: 'main',
765
+ },
766
+ }),
767
+ listDeployments: jest.fn().mockResolvedValue([]),
768
+ getNamespaceGitConfig: jest.fn().mockResolvedValue({
769
+ github_repo_path: 'test/repo',
770
+ git_branch: 'main',
771
+ git_path: 'nodes/',
772
+ git_only: false,
773
+ }),
774
+ createBranch: jest.fn().mockResolvedValue({
775
+ branch: {
776
+ namespace: 'test.feature_xyz',
777
+ git_branch: 'feature-xyz',
778
+ parent_namespace: 'test.namespace',
779
+ },
780
+ deployment_results: [],
781
+ }),
782
+ };
783
+
784
+ render(
785
+ <MemoryRouter>
786
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
787
+ <NamespaceHeader namespace="test.namespace" />
788
+ </DJClientContext.Provider>
789
+ </MemoryRouter>,
790
+ );
791
+
792
+ await waitFor(() => {
793
+ expect(screen.getByText('New Branch')).toBeInTheDocument();
794
+ });
795
+
796
+ fireEvent.click(screen.getByText('New Branch'));
797
+
798
+ await waitFor(() => {
799
+ expect(screen.getByLabelText('Branch Name')).toBeInTheDocument();
800
+ });
801
+
802
+ fireEvent.change(screen.getByLabelText('Branch Name'), {
803
+ target: { value: 'feature-xyz' },
804
+ });
805
+
806
+ // The button inside the modal is labeled "Create Branch"
807
+ fireEvent.click(screen.getByRole('button', { name: 'Create Branch' }));
808
+
809
+ await waitFor(() => {
810
+ expect(mockDjClient.createBranch).toHaveBeenCalledWith(
811
+ 'test.namespace',
812
+ 'feature-xyz',
813
+ );
814
+ });
815
+ });
816
+
817
+ it('should call syncNamespaceToGit when syncing', async () => {
818
+ // Sync to Git only shows for branch namespaces
819
+ const mockDjClient = {
820
+ namespaceSources: jest.fn().mockResolvedValue({
821
+ total_deployments: 1,
822
+ primary_source: {
823
+ type: 'git',
824
+ repository: 'test/repo',
825
+ branch: 'feature',
826
+ },
827
+ }),
828
+ listDeployments: jest.fn().mockResolvedValue([]),
829
+ getNamespaceGitConfig: jest
830
+ .fn()
831
+ .mockResolvedValueOnce({
832
+ github_repo_path: 'test/repo',
833
+ git_branch: 'feature',
834
+ git_path: 'nodes/',
835
+ git_only: false,
836
+ parent_namespace: 'test.main',
837
+ })
838
+ .mockResolvedValueOnce({
839
+ github_repo_path: 'test/repo',
840
+ git_branch: 'main',
841
+ git_path: 'nodes/',
842
+ }),
843
+ getPullRequest: jest.fn().mockResolvedValue(null),
844
+ syncNamespaceToGit: jest.fn().mockResolvedValue({
845
+ files_synced: 5,
846
+ commit_sha: 'abc123',
847
+ commit_url: 'https://github.com/test/repo/commit/abc123',
848
+ }),
849
+ };
850
+
851
+ render(
852
+ <MemoryRouter>
853
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
854
+ <NamespaceHeader namespace="test.feature" />
855
+ </DJClientContext.Provider>
856
+ </MemoryRouter>,
857
+ );
858
+
859
+ await waitFor(() => {
860
+ expect(screen.getByText('Sync to Git')).toBeInTheDocument();
861
+ });
862
+
863
+ fireEvent.click(screen.getByText('Sync to Git'));
864
+
865
+ await waitFor(() => {
866
+ expect(screen.getByLabelText(/Commit Message/)).toBeInTheDocument();
867
+ });
868
+
869
+ fireEvent.change(screen.getByLabelText(/Commit Message/), {
870
+ target: { value: 'Test commit' },
871
+ });
872
+
873
+ fireEvent.click(screen.getByRole('button', { name: 'Sync Now' }));
874
+
875
+ await waitFor(() => {
876
+ expect(mockDjClient.syncNamespaceToGit).toHaveBeenCalledWith(
877
+ 'test.feature',
878
+ 'Test commit',
879
+ );
880
+ });
881
+ });
882
+
883
+ it('should show View PR button when PR exists', async () => {
884
+ const mockDjClient = {
885
+ namespaceSources: jest.fn().mockResolvedValue({
886
+ total_deployments: 1,
887
+ primary_source: {
888
+ type: 'git',
889
+ repository: 'test/repo',
890
+ branch: 'feature',
891
+ },
892
+ }),
893
+ listDeployments: jest.fn().mockResolvedValue([]),
894
+ getNamespaceGitConfig: jest.fn().mockResolvedValue({
895
+ github_repo_path: 'test/repo',
896
+ git_branch: 'feature',
897
+ git_path: 'nodes/',
898
+ git_only: false,
899
+ parent_namespace: 'test.main',
900
+ }),
901
+ getPullRequest: jest.fn().mockResolvedValue({
902
+ pr_number: 42,
903
+ pr_url: 'https://github.com/test/repo/pull/42',
904
+ }),
905
+ };
906
+
907
+ render(
908
+ <MemoryRouter>
909
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
910
+ <NamespaceHeader namespace="test.feature" />
911
+ </DJClientContext.Provider>
912
+ </MemoryRouter>,
913
+ );
914
+
915
+ await waitFor(() => {
916
+ expect(screen.getByText(/View PR #42/)).toBeInTheDocument();
917
+ });
918
+ });
919
+
920
+ it('should call createPullRequest when creating a PR', async () => {
921
+ const mockDjClient = {
922
+ namespaceSources: jest.fn().mockResolvedValue({
923
+ total_deployments: 1,
924
+ primary_source: {
925
+ type: 'git',
926
+ repository: 'test/repo',
927
+ branch: 'feature',
928
+ },
929
+ }),
930
+ listDeployments: jest.fn().mockResolvedValue([]),
931
+ getNamespaceGitConfig: jest
932
+ .fn()
933
+ .mockResolvedValueOnce({
934
+ github_repo_path: 'test/repo',
935
+ git_branch: 'feature',
936
+ git_path: 'nodes/',
937
+ git_only: false,
938
+ parent_namespace: 'test.main',
939
+ })
940
+ .mockResolvedValueOnce({
941
+ github_repo_path: 'test/repo',
942
+ git_branch: 'main',
943
+ git_path: 'nodes/',
944
+ }),
945
+ getPullRequest: jest.fn().mockResolvedValue(null),
946
+ syncNamespaceToGit: jest.fn().mockResolvedValue({
947
+ files_synced: 3,
948
+ commit_sha: 'abc123',
949
+ commit_url: 'https://github.com/test/repo/commit/abc123',
950
+ }),
951
+ createPullRequest: jest.fn().mockResolvedValue({
952
+ pr_number: 99,
953
+ pr_url: 'https://github.com/test/repo/pull/99',
954
+ head_branch: 'feature',
955
+ base_branch: 'main',
956
+ }),
957
+ };
958
+
959
+ render(
960
+ <MemoryRouter>
961
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
962
+ <NamespaceHeader namespace="test.feature" />
963
+ </DJClientContext.Provider>
964
+ </MemoryRouter>,
965
+ );
966
+
967
+ await waitFor(() => {
968
+ expect(screen.getByText('Create PR')).toBeInTheDocument();
969
+ });
970
+
971
+ fireEvent.click(screen.getByText('Create PR'));
972
+
973
+ await waitFor(() => {
974
+ expect(screen.getByLabelText(/Title/)).toBeInTheDocument();
975
+ });
976
+
977
+ fireEvent.change(screen.getByLabelText(/Title/), {
978
+ target: { value: 'My PR Title' },
979
+ });
980
+ fireEvent.change(screen.getByLabelText(/Description/), {
981
+ target: { value: 'PR description' },
982
+ });
983
+
984
+ // There are two "Create PR" buttons - one in header, one in modal
985
+ // Get all and click the last one (modal's submit button)
986
+ const createPRButtons = screen.getAllByRole('button', {
987
+ name: 'Create PR',
988
+ });
989
+ fireEvent.click(createPRButtons[createPRButtons.length - 1]);
990
+
991
+ await waitFor(() => {
992
+ expect(mockDjClient.syncNamespaceToGit).toHaveBeenCalledWith(
993
+ 'test.feature',
994
+ 'My PR Title',
995
+ );
996
+ });
997
+
998
+ await waitFor(() => {
999
+ expect(mockDjClient.createPullRequest).toHaveBeenCalledWith(
1000
+ 'test.feature',
1001
+ 'My PR Title',
1002
+ 'PR description',
1003
+ );
1004
+ });
1005
+ });
1006
+
1007
+ it('should call deleteBranch when deleting a branch', async () => {
1008
+ const mockDjClient = {
1009
+ namespaceSources: jest.fn().mockResolvedValue({
1010
+ total_deployments: 1,
1011
+ primary_source: {
1012
+ type: 'git',
1013
+ repository: 'test/repo',
1014
+ branch: 'feature',
1015
+ },
1016
+ }),
1017
+ listDeployments: jest.fn().mockResolvedValue([]),
1018
+ getNamespaceGitConfig: jest
1019
+ .fn()
1020
+ .mockResolvedValueOnce({
1021
+ github_repo_path: 'test/repo',
1022
+ git_branch: 'feature',
1023
+ git_path: 'nodes/',
1024
+ git_only: false,
1025
+ parent_namespace: 'test.main',
1026
+ })
1027
+ .mockResolvedValueOnce({
1028
+ github_repo_path: 'test/repo',
1029
+ git_branch: 'main',
1030
+ git_path: 'nodes/',
1031
+ }),
1032
+ getPullRequest: jest.fn().mockResolvedValue(null),
1033
+ deleteBranch: jest.fn().mockResolvedValue({ success: true }),
1034
+ };
1035
+
1036
+ // Mock window.location
1037
+ delete window.location;
1038
+ window.location = { href: '' };
1039
+
1040
+ render(
1041
+ <MemoryRouter>
1042
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
1043
+ <NamespaceHeader namespace="test.feature" />
1044
+ </DJClientContext.Provider>
1045
+ </MemoryRouter>,
1046
+ );
1047
+
1048
+ await waitFor(() => {
1049
+ // Delete Branch button in header only has icon with title attribute
1050
+ expect(screen.getByTitle('Delete Branch')).toBeInTheDocument();
1051
+ });
1052
+
1053
+ fireEvent.click(screen.getByTitle('Delete Branch'));
1054
+
1055
+ await waitFor(() => {
1056
+ expect(screen.getByRole('checkbox')).toBeInTheDocument();
1057
+ });
1058
+
1059
+ // There are two buttons with "Delete Branch" - header icon and modal button
1060
+ // Get all and click the last one (modal's submit button)
1061
+ const deleteBranchButtons = screen.getAllByRole('button', {
1062
+ name: 'Delete Branch',
1063
+ });
1064
+ fireEvent.click(deleteBranchButtons[deleteBranchButtons.length - 1]);
1065
+
1066
+ await waitFor(() => {
1067
+ expect(mockDjClient.deleteBranch).toHaveBeenCalledWith(
1068
+ 'test.main',
1069
+ 'test.feature',
1070
+ true,
1071
+ );
1072
+ });
1073
+ });
1074
+
1075
+ it('should fetch parent git config for branch namespace', async () => {
1076
+ const mockDjClient = {
1077
+ namespaceSources: jest.fn().mockResolvedValue({
1078
+ total_deployments: 1,
1079
+ primary_source: {
1080
+ type: 'git',
1081
+ repository: 'test/repo',
1082
+ branch: 'feature',
1083
+ },
1084
+ }),
1085
+ listDeployments: jest.fn().mockResolvedValue([]),
1086
+ getNamespaceGitConfig: jest
1087
+ .fn()
1088
+ .mockResolvedValueOnce({
1089
+ github_repo_path: 'test/repo',
1090
+ git_branch: 'feature',
1091
+ git_path: 'nodes/',
1092
+ git_only: false,
1093
+ parent_namespace: 'test.main',
1094
+ })
1095
+ .mockResolvedValueOnce({
1096
+ github_repo_path: 'test/repo',
1097
+ git_branch: 'main',
1098
+ git_path: 'nodes/',
1099
+ }),
1100
+ getPullRequest: jest.fn().mockResolvedValue(null),
1101
+ };
1102
+
1103
+ render(
1104
+ <MemoryRouter>
1105
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
1106
+ <NamespaceHeader namespace="test.feature" />
1107
+ </DJClientContext.Provider>
1108
+ </MemoryRouter>,
1109
+ );
1110
+
1111
+ await waitFor(() => {
1112
+ expect(mockDjClient.getNamespaceGitConfig).toHaveBeenCalledWith(
1113
+ 'test.feature',
1114
+ );
1115
+ });
1116
+
1117
+ await waitFor(() => {
1118
+ expect(mockDjClient.getNamespaceGitConfig).toHaveBeenCalledWith(
1119
+ 'test.main',
1120
+ );
1121
+ });
1122
+
1123
+ await waitFor(() => {
1124
+ expect(mockDjClient.getPullRequest).toHaveBeenCalledWith('test.feature');
1125
+ });
1126
+ });
1127
+
1128
+ it('should handle error fetching parent git config gracefully', async () => {
1129
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
1130
+
1131
+ const mockDjClient = {
1132
+ namespaceSources: jest.fn().mockResolvedValue({
1133
+ total_deployments: 1,
1134
+ primary_source: {
1135
+ type: 'git',
1136
+ repository: 'test/repo',
1137
+ branch: 'feature',
1138
+ },
1139
+ }),
1140
+ listDeployments: jest.fn().mockResolvedValue([]),
1141
+ getNamespaceGitConfig: jest
1142
+ .fn()
1143
+ .mockResolvedValueOnce({
1144
+ github_repo_path: 'test/repo',
1145
+ git_branch: 'feature',
1146
+ git_path: 'nodes/',
1147
+ git_only: false,
1148
+ parent_namespace: 'test.main',
1149
+ })
1150
+ .mockRejectedValueOnce(new Error('Parent not found')),
1151
+ getPullRequest: jest.fn().mockResolvedValue(null),
1152
+ };
1153
+
1154
+ render(
1155
+ <MemoryRouter>
1156
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
1157
+ <NamespaceHeader namespace="test.feature" />
1158
+ </DJClientContext.Provider>
1159
+ </MemoryRouter>,
1160
+ );
1161
+
1162
+ await waitFor(() => {
1163
+ expect(consoleSpy).toHaveBeenCalledWith(
1164
+ 'Failed to fetch parent git config:',
1165
+ expect.any(Error),
1166
+ );
1167
+ });
1168
+
1169
+ consoleSpy.mockRestore();
1170
+ });
1171
+
1172
+ it('should handle error fetching PR gracefully', async () => {
1173
+ const mockDjClient = {
1174
+ namespaceSources: jest.fn().mockResolvedValue({
1175
+ total_deployments: 1,
1176
+ primary_source: {
1177
+ type: 'git',
1178
+ repository: 'test/repo',
1179
+ branch: 'feature',
1180
+ },
1181
+ }),
1182
+ listDeployments: jest.fn().mockResolvedValue([]),
1183
+ getNamespaceGitConfig: jest
1184
+ .fn()
1185
+ .mockResolvedValueOnce({
1186
+ github_repo_path: 'test/repo',
1187
+ git_branch: 'feature',
1188
+ git_path: 'nodes/',
1189
+ git_only: false,
1190
+ parent_namespace: 'test.main',
1191
+ })
1192
+ .mockResolvedValueOnce({
1193
+ github_repo_path: 'test/repo',
1194
+ git_branch: 'main',
1195
+ git_path: 'nodes/',
1196
+ }),
1197
+ getPullRequest: jest.fn().mockRejectedValue(new Error('API Error')),
1198
+ };
1199
+
1200
+ render(
1201
+ <MemoryRouter>
1202
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
1203
+ <NamespaceHeader namespace="test.feature" />
1204
+ </DJClientContext.Provider>
1205
+ </MemoryRouter>,
1206
+ );
1207
+
1208
+ // Should render without crashing and show Create PR button
1209
+ await waitFor(() => {
1210
+ expect(screen.getByText('Create PR')).toBeInTheDocument();
1211
+ });
1212
+ });
1213
+
1214
+ it('should call onGitConfigLoaded callback when config is fetched', async () => {
1215
+ const onGitConfigLoaded = jest.fn();
1216
+ const mockDjClient = {
1217
+ namespaceSources: jest.fn().mockResolvedValue({
1218
+ total_deployments: 0,
1219
+ primary_source: null,
1220
+ }),
1221
+ listDeployments: jest.fn().mockResolvedValue([]),
1222
+ getNamespaceGitConfig: jest.fn().mockResolvedValue({
1223
+ github_repo_path: 'test/repo',
1224
+ git_branch: 'main',
1225
+ }),
1226
+ };
1227
+
1228
+ render(
1229
+ <MemoryRouter>
1230
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
1231
+ <NamespaceHeader
1232
+ namespace="test.namespace"
1233
+ onGitConfigLoaded={onGitConfigLoaded}
1234
+ />
1235
+ </DJClientContext.Provider>
1236
+ </MemoryRouter>,
1237
+ );
1238
+
1239
+ await waitFor(() => {
1240
+ expect(onGitConfigLoaded).toHaveBeenCalledWith({
1241
+ github_repo_path: 'test/repo',
1242
+ git_branch: 'main',
1243
+ });
1244
+ });
1245
+ });
1246
+
1247
+ it('should call onGitConfigLoaded with null when git config fetch fails', async () => {
1248
+ const onGitConfigLoaded = jest.fn();
1249
+ const mockDjClient = {
1250
+ namespaceSources: jest.fn().mockResolvedValue({
1251
+ total_deployments: 0,
1252
+ primary_source: null,
1253
+ }),
1254
+ listDeployments: jest.fn().mockResolvedValue([]),
1255
+ getNamespaceGitConfig: jest
1256
+ .fn()
1257
+ .mockRejectedValue(new Error('Config not found')),
1258
+ };
1259
+
1260
+ render(
1261
+ <MemoryRouter>
1262
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
1263
+ <NamespaceHeader
1264
+ namespace="test.namespace"
1265
+ onGitConfigLoaded={onGitConfigLoaded}
1266
+ />
1267
+ </DJClientContext.Provider>
1268
+ </MemoryRouter>,
1269
+ );
1270
+
1271
+ await waitFor(() => {
1272
+ expect(onGitConfigLoaded).toHaveBeenCalledWith(null);
1273
+ });
1274
+ });
1275
+
1276
+ it('should call deleteNamespaceGitConfig when removing git settings', async () => {
1277
+ // Mock window.confirm for this test
1278
+ global.confirm = jest.fn(() => true);
1279
+
1280
+ const mockDjClient = {
1281
+ namespaceSources: jest.fn().mockResolvedValue({
1282
+ total_deployments: 0,
1283
+ primary_source: null,
1284
+ }),
1285
+ listDeployments: jest.fn().mockResolvedValue([]),
1286
+ getNamespaceGitConfig: jest.fn().mockResolvedValue({
1287
+ github_repo_path: 'test/repo',
1288
+ git_branch: 'main',
1289
+ git_path: 'nodes/',
1290
+ git_only: false,
1291
+ }),
1292
+ deleteNamespaceGitConfig: jest.fn().mockResolvedValue({ success: true }),
1293
+ };
1294
+
1295
+ render(
1296
+ <MemoryRouter>
1297
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
1298
+ <NamespaceHeader namespace="test.namespace" />
1299
+ </DJClientContext.Provider>
1300
+ </MemoryRouter>,
1301
+ );
1302
+
1303
+ await waitFor(() => {
1304
+ expect(screen.getByText('Git Settings')).toBeInTheDocument();
1305
+ });
1306
+
1307
+ fireEvent.click(screen.getByText('Git Settings'));
1308
+
1309
+ await waitFor(() => {
1310
+ expect(screen.getByText('Git Configuration')).toBeInTheDocument();
1311
+ });
1312
+
1313
+ // Click reset button in the modal (button text is "Reset")
1314
+ const removeButton = screen.getByText('Reset');
1315
+ fireEvent.click(removeButton);
1316
+
1317
+ await waitFor(() => {
1318
+ expect(mockDjClient.deleteNamespaceGitConfig).toHaveBeenCalledWith(
1319
+ 'test.namespace',
1320
+ );
1321
+ });
1322
+
1323
+ // Clean up mock
1324
+ jest.restoreAllMocks();
1325
+ });
1326
+
1327
+ it('should handle sync error in handleCreatePR', async () => {
1328
+ const mockDjClient = {
1329
+ namespaceSources: jest.fn().mockResolvedValue({
1330
+ total_deployments: 1,
1331
+ primary_source: {
1332
+ type: 'git',
1333
+ repository: 'test/repo',
1334
+ branch: 'feature',
1335
+ },
1336
+ }),
1337
+ listDeployments: jest.fn().mockResolvedValue([]),
1338
+ getNamespaceGitConfig: jest
1339
+ .fn()
1340
+ .mockResolvedValueOnce({
1341
+ github_repo_path: 'test/repo',
1342
+ git_branch: 'feature',
1343
+ git_path: 'nodes/',
1344
+ git_only: false,
1345
+ parent_namespace: 'test.main',
1346
+ })
1347
+ .mockResolvedValueOnce({
1348
+ github_repo_path: 'test/repo',
1349
+ git_branch: 'main',
1350
+ git_path: 'nodes/',
1351
+ }),
1352
+ getPullRequest: jest.fn().mockResolvedValue(null),
1353
+ syncNamespaceToGit: jest.fn().mockResolvedValue({
1354
+ _error: true,
1355
+ message: 'Sync failed: merge conflict',
1356
+ }),
1357
+ };
1358
+
1359
+ render(
1360
+ <MemoryRouter>
1361
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
1362
+ <NamespaceHeader namespace="test.feature" />
1363
+ </DJClientContext.Provider>
1364
+ </MemoryRouter>,
1365
+ );
1366
+
1367
+ await waitFor(() => {
1368
+ expect(screen.getByText('Create PR')).toBeInTheDocument();
1369
+ });
1370
+
1371
+ fireEvent.click(screen.getByText('Create PR'));
1372
+
1373
+ await waitFor(() => {
1374
+ expect(screen.getByLabelText(/Title/)).toBeInTheDocument();
1375
+ });
1376
+
1377
+ fireEvent.change(screen.getByLabelText(/Title/), {
1378
+ target: { value: 'My PR Title' },
1379
+ });
1380
+
1381
+ const createPRButtons = screen.getAllByRole('button', {
1382
+ name: 'Create PR',
1383
+ });
1384
+ fireEvent.click(createPRButtons[createPRButtons.length - 1]);
1385
+
1386
+ await waitFor(() => {
1387
+ expect(mockDjClient.syncNamespaceToGit).toHaveBeenCalledWith(
1388
+ 'test.feature',
1389
+ 'My PR Title',
1390
+ );
1391
+ });
1392
+
1393
+ // Should show error message from sync failure
1394
+ await waitFor(() => {
1395
+ expect(
1396
+ screen.getByText(/Sync failed: merge conflict/),
1397
+ ).toBeInTheDocument();
1398
+ });
1399
+ });
511
1400
  });