datajunction-ui 0.0.26 → 0.0.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 (28) hide show
  1. package/package.json +2 -2
  2. package/src/app/components/Search.jsx +41 -33
  3. package/src/app/components/__tests__/Search.test.jsx +46 -11
  4. package/src/app/index.tsx +3 -3
  5. package/src/app/pages/AddEditNodePage/MetricQueryField.jsx +57 -8
  6. package/src/app/pages/AddEditNodePage/UpstreamNodeField.jsx +17 -5
  7. package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +97 -1
  8. package/src/app/pages/AddEditNodePage/index.jsx +61 -17
  9. package/src/app/pages/NodePage/WatchNodeButton.jsx +12 -5
  10. package/src/app/pages/QueryPlannerPage/MetricFlowGraph.jsx +93 -15
  11. package/src/app/pages/QueryPlannerPage/PreAggDetailsPanel.jsx +2320 -65
  12. package/src/app/pages/QueryPlannerPage/SelectionPanel.jsx +234 -25
  13. package/src/app/pages/QueryPlannerPage/__tests__/MetricFlowGraph.test.jsx +315 -122
  14. package/src/app/pages/QueryPlannerPage/__tests__/PreAggDetailsPanel.test.jsx +2672 -314
  15. package/src/app/pages/QueryPlannerPage/__tests__/SelectionPanel.test.jsx +567 -0
  16. package/src/app/pages/QueryPlannerPage/__tests__/index.test.jsx +480 -55
  17. package/src/app/pages/QueryPlannerPage/index.jsx +1021 -14
  18. package/src/app/pages/QueryPlannerPage/styles.css +1990 -62
  19. package/src/app/pages/Root/__tests__/index.test.jsx +79 -8
  20. package/src/app/pages/Root/index.tsx +1 -6
  21. package/src/app/pages/SQLBuilderPage/__tests__/index.test.jsx +82 -0
  22. package/src/app/pages/SettingsPage/__tests__/CreateServiceAccountModal.test.jsx +37 -0
  23. package/src/app/pages/SettingsPage/__tests__/ServiceAccountsSection.test.jsx +48 -0
  24. package/src/app/pages/SettingsPage/__tests__/index.test.jsx +169 -1
  25. package/src/app/services/DJService.js +492 -3
  26. package/src/app/services/__tests__/DJService.test.jsx +582 -0
  27. package/src/mocks/mockNodes.jsx +36 -0
  28. package/webpack.config.js +27 -0
@@ -426,4 +426,571 @@ describe('SelectionPanel', () => {
426
426
  expect(checkbox).toBeChecked();
427
427
  });
428
428
  });
429
+
430
+ describe('Cube Preset Loading', () => {
431
+ const cubeProps = {
432
+ ...defaultProps,
433
+ cubes: [
434
+ { name: 'default.test_cube', display_name: 'Test Cube' },
435
+ { name: 'sales.revenue_cube', display_name: 'Revenue Cube' },
436
+ ],
437
+ onLoadCubePreset: jest.fn(),
438
+ };
439
+
440
+ it('shows Load from Cube button when cubes are available', () => {
441
+ render(<SelectionPanel {...cubeProps} />);
442
+ expect(screen.getByText('Load from Cube')).toBeInTheDocument();
443
+ });
444
+
445
+ it('opens dropdown when Load from Cube button is clicked', () => {
446
+ render(<SelectionPanel {...cubeProps} />);
447
+
448
+ fireEvent.click(screen.getByText('Load from Cube'));
449
+
450
+ expect(
451
+ screen.getByPlaceholderText('Search cubes...'),
452
+ ).toBeInTheDocument();
453
+ });
454
+
455
+ it('displays cube options in dropdown', () => {
456
+ render(<SelectionPanel {...cubeProps} />);
457
+
458
+ fireEvent.click(screen.getByText('Load from Cube'));
459
+
460
+ expect(screen.getByText('Test Cube')).toBeInTheDocument();
461
+ expect(screen.getByText('Revenue Cube')).toBeInTheDocument();
462
+ });
463
+
464
+ it('filters cubes by search term', () => {
465
+ render(<SelectionPanel {...cubeProps} />);
466
+
467
+ fireEvent.click(screen.getByText('Load from Cube'));
468
+
469
+ const searchInput = screen.getByPlaceholderText('Search cubes...');
470
+ fireEvent.change(searchInput, { target: { value: 'Revenue' } });
471
+
472
+ expect(screen.getByText('Revenue Cube')).toBeInTheDocument();
473
+ expect(screen.queryByText('Test Cube')).not.toBeInTheDocument();
474
+ });
475
+
476
+ it('calls onLoadCubePreset when a cube is selected', () => {
477
+ const onLoadCubePreset = jest.fn();
478
+ render(
479
+ <SelectionPanel {...cubeProps} onLoadCubePreset={onLoadCubePreset} />,
480
+ );
481
+
482
+ fireEvent.click(screen.getByText('Load from Cube'));
483
+ fireEvent.click(screen.getByText('Test Cube'));
484
+
485
+ expect(onLoadCubePreset).toHaveBeenCalledWith('default.test_cube');
486
+ });
487
+
488
+ it('shows loaded cube name in button when cube is loaded', () => {
489
+ render(
490
+ <SelectionPanel {...cubeProps} loadedCubeName="default.test_cube" />,
491
+ );
492
+
493
+ // Should show the cube display name or short name
494
+ expect(screen.getByText('Test Cube')).toBeInTheDocument();
495
+ });
496
+
497
+ it('shows "No cubes match your search" when search has no results', () => {
498
+ render(<SelectionPanel {...cubeProps} />);
499
+
500
+ fireEvent.click(screen.getByText('Load from Cube'));
501
+
502
+ const searchInput = screen.getByPlaceholderText('Search cubes...');
503
+ fireEvent.change(searchInput, { target: { value: 'nonexistent' } });
504
+
505
+ expect(
506
+ screen.getByText('No cubes match your search'),
507
+ ).toBeInTheDocument();
508
+ });
509
+
510
+ it('closes dropdown when clicking outside', () => {
511
+ render(<SelectionPanel {...cubeProps} />);
512
+
513
+ fireEvent.click(screen.getByText('Load from Cube'));
514
+ expect(
515
+ screen.getByPlaceholderText('Search cubes...'),
516
+ ).toBeInTheDocument();
517
+
518
+ // Simulate clicking outside
519
+ fireEvent.mouseDown(document.body);
520
+
521
+ expect(
522
+ screen.queryByPlaceholderText('Search cubes...'),
523
+ ).not.toBeInTheDocument();
524
+ });
525
+ });
526
+
527
+ describe('Selected Metrics Chips', () => {
528
+ it('displays selected metrics as chips', () => {
529
+ render(
530
+ <SelectionPanel
531
+ {...defaultProps}
532
+ selectedMetrics={['default.num_repair_orders']}
533
+ />,
534
+ );
535
+
536
+ expect(screen.getByText('num_repair_orders')).toBeInTheDocument();
537
+ });
538
+
539
+ it('removes metric when chip remove button is clicked', () => {
540
+ const onMetricsChange = jest.fn();
541
+ render(
542
+ <SelectionPanel
543
+ {...defaultProps}
544
+ selectedMetrics={[
545
+ 'default.num_repair_orders',
546
+ 'default.avg_repair_price',
547
+ ]}
548
+ onMetricsChange={onMetricsChange}
549
+ />,
550
+ );
551
+
552
+ // Find the remove button for num_repair_orders chip
553
+ const removeBtn = screen.getByTitle('Remove num_repair_orders');
554
+ fireEvent.click(removeBtn);
555
+
556
+ expect(onMetricsChange).toHaveBeenCalledWith([
557
+ 'default.avg_repair_price',
558
+ ]);
559
+ });
560
+
561
+ it('shows "Show all" button when many metrics are selected', () => {
562
+ const manyMetrics = Array.from(
563
+ { length: 12 },
564
+ (_, i) => `default.metric_${i}`,
565
+ );
566
+ render(
567
+ <SelectionPanel
568
+ {...defaultProps}
569
+ metrics={manyMetrics}
570
+ selectedMetrics={manyMetrics}
571
+ />,
572
+ );
573
+
574
+ expect(screen.getByText(/Show all 12/)).toBeInTheDocument();
575
+ });
576
+
577
+ it('toggles chips expansion when Show all/Show less is clicked', () => {
578
+ const manyMetrics = Array.from(
579
+ { length: 12 },
580
+ (_, i) => `default.metric_${i}`,
581
+ );
582
+ render(
583
+ <SelectionPanel
584
+ {...defaultProps}
585
+ metrics={manyMetrics}
586
+ selectedMetrics={manyMetrics}
587
+ />,
588
+ );
589
+
590
+ // Click to expand
591
+ const expandBtn = screen.getByText(/Show all 12/);
592
+ fireEvent.click(expandBtn);
593
+
594
+ // Should now show "Show less"
595
+ expect(screen.getByText('Show less')).toBeInTheDocument();
596
+
597
+ // Click to collapse
598
+ fireEvent.click(screen.getByText('Show less'));
599
+
600
+ // Should show "Show all" again
601
+ expect(screen.getByText(/Show all 12/)).toBeInTheDocument();
602
+ });
603
+ });
604
+
605
+ describe('Selected Dimensions Chips', () => {
606
+ it('displays selected dimensions as chips', () => {
607
+ render(
608
+ <SelectionPanel
609
+ {...defaultProps}
610
+ selectedMetrics={['default.test']}
611
+ selectedDimensions={['default.date_dim.dateint']}
612
+ />,
613
+ );
614
+
615
+ // Check for chip by looking for the chip container with the dimension display name
616
+ const chipElements = screen.getAllByText('date_dim.dateint');
617
+ // Should have at least one chip (and possibly one in the list)
618
+ expect(chipElements.length).toBeGreaterThanOrEqual(1);
619
+ // The chip should have the chip-label class
620
+ expect(
621
+ document.querySelector('.dimension-chip .chip-label'),
622
+ ).toBeInTheDocument();
623
+ });
624
+
625
+ it('removes dimension when chip remove button is clicked', () => {
626
+ const onDimensionsChange = jest.fn();
627
+ render(
628
+ <SelectionPanel
629
+ {...defaultProps}
630
+ selectedMetrics={['default.test']}
631
+ selectedDimensions={[
632
+ 'default.date_dim.dateint',
633
+ 'default.date_dim.month',
634
+ ]}
635
+ onDimensionsChange={onDimensionsChange}
636
+ />,
637
+ );
638
+
639
+ // Find the remove button for dateint chip
640
+ const removeBtn = screen.getByTitle('Remove date_dim.dateint');
641
+ fireEvent.click(removeBtn);
642
+
643
+ expect(onDimensionsChange).toHaveBeenCalledWith([
644
+ 'default.date_dim.month',
645
+ ]);
646
+ });
647
+ });
648
+
649
+ describe('Clear All Button', () => {
650
+ it('shows Clear all button when items are selected', () => {
651
+ render(
652
+ <SelectionPanel
653
+ {...defaultProps}
654
+ selectedMetrics={['default.num_repair_orders']}
655
+ cubes={[{ name: 'default.cube', display_name: 'Cube' }]}
656
+ />,
657
+ );
658
+
659
+ expect(screen.getByText('Clear all')).toBeInTheDocument();
660
+ });
661
+
662
+ it('calls onClearSelection when Clear all is clicked', () => {
663
+ const onClearSelection = jest.fn();
664
+ render(
665
+ <SelectionPanel
666
+ {...defaultProps}
667
+ selectedMetrics={['default.num_repair_orders']}
668
+ cubes={[{ name: 'default.cube', display_name: 'Cube' }]}
669
+ onClearSelection={onClearSelection}
670
+ />,
671
+ );
672
+
673
+ fireEvent.click(screen.getByText('Clear all'));
674
+
675
+ expect(onClearSelection).toHaveBeenCalled();
676
+ });
677
+
678
+ it('clears metrics and dimensions if no onClearSelection provided', () => {
679
+ const onMetricsChange = jest.fn();
680
+ const onDimensionsChange = jest.fn();
681
+ render(
682
+ <SelectionPanel
683
+ {...defaultProps}
684
+ selectedMetrics={['default.num_repair_orders']}
685
+ selectedDimensions={['default.date_dim.dateint']}
686
+ onMetricsChange={onMetricsChange}
687
+ onDimensionsChange={onDimensionsChange}
688
+ cubes={[{ name: 'default.cube', display_name: 'Cube' }]}
689
+ />,
690
+ );
691
+
692
+ fireEvent.click(screen.getByText('Clear all'));
693
+
694
+ expect(onMetricsChange).toHaveBeenCalledWith([]);
695
+ expect(onDimensionsChange).toHaveBeenCalledWith([]);
696
+ });
697
+ });
698
+
699
+ describe('Dimension Path Display', () => {
700
+ it('shows dimension path when path has multiple segments', () => {
701
+ const dimensionsWithPath = [
702
+ {
703
+ name: 'default.date_dim.dateint',
704
+ type: 'timestamp',
705
+ path: ['default.orders', 'default.date_dim.dateint'],
706
+ },
707
+ ];
708
+
709
+ render(
710
+ <SelectionPanel
711
+ {...defaultProps}
712
+ dimensions={dimensionsWithPath}
713
+ selectedMetrics={['default.test']}
714
+ />,
715
+ );
716
+
717
+ // Should show the path
718
+ expect(screen.getByText('default.date_dim.dateint')).toBeInTheDocument();
719
+ });
720
+ });
721
+
722
+ describe('Namespace Sorting Logic', () => {
723
+ it('prioritizes namespaces that start with search term', () => {
724
+ const metricsWithNamespaces = [
725
+ 'zebra.metric1',
726
+ 'alpha.metric2',
727
+ 'alpha_test.metric3',
728
+ 'beta.metric4',
729
+ ];
730
+
731
+ render(
732
+ <SelectionPanel {...defaultProps} metrics={metricsWithNamespaces} />,
733
+ );
734
+
735
+ const searchInput = screen.getByPlaceholderText('Search metrics...');
736
+ fireEvent.change(searchInput, { target: { value: 'alpha' } });
737
+
738
+ // Alpha namespace should be expanded first since it starts with 'alpha'
739
+ const namespaces = document.querySelectorAll('.namespace-header');
740
+ expect(namespaces.length).toBeGreaterThan(0);
741
+ });
742
+
743
+ it('sorts namespaces with more matching items higher', () => {
744
+ const metricsWithNamespaces = [
745
+ 'default.test_metric1',
746
+ 'default.test_metric2',
747
+ 'default.test_metric3',
748
+ 'other.test_metric4',
749
+ ];
750
+
751
+ render(
752
+ <SelectionPanel {...defaultProps} metrics={metricsWithNamespaces} />,
753
+ );
754
+
755
+ const searchInput = screen.getByPlaceholderText('Search metrics...');
756
+ fireEvent.change(searchInput, { target: { value: 'test' } });
757
+
758
+ // Should show namespaces - default has more matching items
759
+ expect(screen.getByText('default')).toBeInTheDocument();
760
+ expect(screen.getByText('other')).toBeInTheDocument();
761
+ });
762
+
763
+ it('sorts namespaces alphabetically when other criteria are equal', () => {
764
+ const metricsWithNamespaces = [
765
+ 'zebra.metric1',
766
+ 'alpha.metric2',
767
+ 'beta.metric3',
768
+ ];
769
+
770
+ render(
771
+ <SelectionPanel {...defaultProps} metrics={metricsWithNamespaces} />,
772
+ );
773
+
774
+ // Namespaces should be available
775
+ expect(screen.getByText('alpha')).toBeInTheDocument();
776
+ expect(screen.getByText('beta')).toBeInTheDocument();
777
+ expect(screen.getByText('zebra')).toBeInTheDocument();
778
+ });
779
+ });
780
+
781
+ describe('Dimension Sorting Logic', () => {
782
+ it('prioritizes dimensions that start with search term', () => {
783
+ const sortableDimensions = [
784
+ { name: 'default.zebra.column', path: [] },
785
+ { name: 'default.alpha.column', path: [] },
786
+ { name: 'default.date_dim.alpha_col', path: [] },
787
+ ];
788
+
789
+ render(
790
+ <SelectionPanel
791
+ {...defaultProps}
792
+ dimensions={sortableDimensions}
793
+ selectedMetrics={['default.test']}
794
+ />,
795
+ );
796
+
797
+ const searchInput = screen.getByPlaceholderText('Search dimensions...');
798
+ fireEvent.change(searchInput, { target: { value: 'alpha' } });
799
+
800
+ // Should show matching dimensions
801
+ const checkboxes = screen.getAllByRole('checkbox');
802
+ expect(checkboxes.length).toBeGreaterThan(0);
803
+ });
804
+
805
+ it('sorts dimensions alphabetically by short name', () => {
806
+ const sortableDimensions = [
807
+ { name: 'default.zebra.col', path: [] },
808
+ { name: 'default.alpha.col', path: [] },
809
+ { name: 'default.beta.col', path: [] },
810
+ ];
811
+
812
+ render(
813
+ <SelectionPanel
814
+ {...defaultProps}
815
+ dimensions={sortableDimensions}
816
+ selectedMetrics={['default.test']}
817
+ />,
818
+ );
819
+
820
+ const searchInput = screen.getByPlaceholderText('Search dimensions...');
821
+ fireEvent.change(searchInput, { target: { value: 'col' } });
822
+
823
+ // All three should be visible
824
+ expect(screen.getByText('alpha.col')).toBeInTheDocument();
825
+ expect(screen.getByText('beta.col')).toBeInTheDocument();
826
+ expect(screen.getByText('zebra.col')).toBeInTheDocument();
827
+ });
828
+
829
+ it('handles dimensions with prefix matches before contains matches', () => {
830
+ const sortableDimensions = [
831
+ { name: 'default.country_code', path: [] },
832
+ { name: 'default.customer.country', path: [] },
833
+ ];
834
+
835
+ render(
836
+ <SelectionPanel
837
+ {...defaultProps}
838
+ dimensions={sortableDimensions}
839
+ selectedMetrics={['default.test']}
840
+ />,
841
+ );
842
+
843
+ const searchInput = screen.getByPlaceholderText('Search dimensions...');
844
+ fireEvent.change(searchInput, { target: { value: 'country' } });
845
+
846
+ // Both should be visible
847
+ const checkboxes = screen.getAllByRole('checkbox');
848
+ expect(checkboxes.length).toBe(2);
849
+ });
850
+ });
851
+
852
+ describe('Dimensions Chips Toggle', () => {
853
+ it('shows "Show all" button when many dimensions are selected', () => {
854
+ const manyDimensions = Array.from({ length: 15 }, (_, i) => ({
855
+ name: `default.dim_${i}`,
856
+ path: [],
857
+ }));
858
+
859
+ render(
860
+ <SelectionPanel
861
+ {...defaultProps}
862
+ dimensions={manyDimensions}
863
+ selectedMetrics={['default.test']}
864
+ selectedDimensions={manyDimensions.map(d => d.name)}
865
+ />,
866
+ );
867
+
868
+ expect(screen.getByText(/Show all 15/)).toBeInTheDocument();
869
+ });
870
+
871
+ it('toggles dimension chips expansion', () => {
872
+ const manyDimensions = Array.from({ length: 15 }, (_, i) => ({
873
+ name: `default.dim_${i}`,
874
+ path: [],
875
+ }));
876
+
877
+ render(
878
+ <SelectionPanel
879
+ {...defaultProps}
880
+ dimensions={manyDimensions}
881
+ selectedMetrics={['default.test']}
882
+ selectedDimensions={manyDimensions.map(d => d.name)}
883
+ />,
884
+ );
885
+
886
+ // Click to expand
887
+ const expandBtn = screen.getByText(/Show all 15/);
888
+ fireEvent.click(expandBtn);
889
+
890
+ // Should show "Show less"
891
+ expect(screen.getByText('Show less')).toBeInTheDocument();
892
+
893
+ // Click to collapse
894
+ fireEvent.click(screen.getByText('Show less'));
895
+
896
+ // Should show "Show all" again
897
+ expect(screen.getByText(/Show all 15/)).toBeInTheDocument();
898
+ });
899
+ });
900
+
901
+ describe('Toggle Namespace', () => {
902
+ it('toggles namespace expansion state', () => {
903
+ render(<SelectionPanel {...defaultProps} />);
904
+
905
+ // Click to expand 'default'
906
+ fireEvent.click(screen.getByText('default'));
907
+ expect(screen.getByText('num_repair_orders')).toBeInTheDocument();
908
+
909
+ // Click again to collapse
910
+ fireEvent.click(screen.getByText('default'));
911
+ expect(screen.queryByText('num_repair_orders')).not.toBeInTheDocument();
912
+
913
+ // Click again to expand
914
+ fireEvent.click(screen.getByText('default'));
915
+ expect(screen.getByText('num_repair_orders')).toBeInTheDocument();
916
+ });
917
+
918
+ it('allows multiple namespaces to be expanded', () => {
919
+ render(<SelectionPanel {...defaultProps} />);
920
+
921
+ // Expand both default and sales
922
+ fireEvent.click(screen.getByText('default'));
923
+ fireEvent.click(screen.getByText('sales'));
924
+
925
+ // Both should show their metrics
926
+ expect(screen.getByText('num_repair_orders')).toBeInTheDocument();
927
+ expect(screen.getByText('revenue')).toBeInTheDocument();
928
+ });
929
+ });
930
+
931
+ describe('Clear Dimension Search', () => {
932
+ it('clears dimension search when clear button is clicked', () => {
933
+ render(
934
+ <SelectionPanel {...defaultProps} selectedMetrics={['default.test']} />,
935
+ );
936
+
937
+ const searchInput = screen.getByPlaceholderText('Search dimensions...');
938
+ fireEvent.change(searchInput, { target: { value: 'test' } });
939
+
940
+ expect(searchInput.value).toBe('test');
941
+
942
+ // Find the clear button (there are two × buttons, one for each search)
943
+ const clearButtons = screen.getAllByText('×');
944
+ // The second one is for dimension search
945
+ fireEvent.click(clearButtons[clearButtons.length - 1]);
946
+
947
+ expect(searchInput.value).toBe('');
948
+ });
949
+ });
950
+
951
+ describe('Remove Dimension from Selected', () => {
952
+ it('removes dimension when clicking X on dimension chip', () => {
953
+ const onDimensionsChange = jest.fn();
954
+ render(
955
+ <SelectionPanel
956
+ {...defaultProps}
957
+ selectedMetrics={['default.test']}
958
+ selectedDimensions={[
959
+ 'default.date_dim.dateint',
960
+ 'default.date_dim.month',
961
+ 'default.date_dim.year',
962
+ ]}
963
+ onDimensionsChange={onDimensionsChange}
964
+ />,
965
+ );
966
+
967
+ // Find and click remove button for dateint
968
+ const removeBtn = screen.getByTitle('Remove date_dim.dateint');
969
+ fireEvent.click(removeBtn);
970
+
971
+ expect(onDimensionsChange).toHaveBeenCalledWith([
972
+ 'default.date_dim.month',
973
+ 'default.date_dim.year',
974
+ ]);
975
+ });
976
+ });
977
+
978
+ describe('Toggle Dimension Selection', () => {
979
+ it('removes dimension when unchecking already selected dimension', () => {
980
+ const onDimensionsChange = jest.fn();
981
+ render(
982
+ <SelectionPanel
983
+ {...defaultProps}
984
+ selectedMetrics={['default.test']}
985
+ selectedDimensions={['default.date_dim.dateint']}
986
+ onDimensionsChange={onDimensionsChange}
987
+ />,
988
+ );
989
+
990
+ const checkbox = screen.getByRole('checkbox', { name: /dateint/i });
991
+ fireEvent.click(checkbox);
992
+
993
+ expect(onDimensionsChange).toHaveBeenCalledWith([]);
994
+ });
995
+ });
429
996
  });