datajunction-ui 0.0.93 → 0.0.95

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 (42) hide show
  1. package/package.json +1 -1
  2. package/src/app/components/NodeComponents.jsx +4 -0
  3. package/src/app/components/Tab.jsx +11 -16
  4. package/src/app/components/__tests__/Tab.test.jsx +4 -2
  5. package/src/app/hooks/useWorkspaceData.js +226 -0
  6. package/src/app/index.tsx +17 -1
  7. package/src/app/pages/MyWorkspacePage/ActiveBranchesSection.jsx +38 -107
  8. package/src/app/pages/MyWorkspacePage/MyNodesSection.jsx +31 -6
  9. package/src/app/pages/MyWorkspacePage/MyWorkspacePage.css +5 -0
  10. package/src/app/pages/MyWorkspacePage/NeedsAttentionSection.jsx +86 -100
  11. package/src/app/pages/MyWorkspacePage/TypeGroupGrid.jsx +7 -11
  12. package/src/app/pages/MyWorkspacePage/__tests__/ActiveBranchesSection.test.jsx +79 -11
  13. package/src/app/pages/MyWorkspacePage/__tests__/CollectionsSection.test.jsx +22 -0
  14. package/src/app/pages/MyWorkspacePage/__tests__/MaterializationsSection.test.jsx +57 -0
  15. package/src/app/pages/MyWorkspacePage/__tests__/MyNodesSection.test.jsx +60 -18
  16. package/src/app/pages/MyWorkspacePage/__tests__/MyWorkspacePage.test.jsx +156 -162
  17. package/src/app/pages/MyWorkspacePage/__tests__/NeedsAttentionSection.test.jsx +17 -18
  18. package/src/app/pages/MyWorkspacePage/__tests__/NotificationsSection.test.jsx +179 -0
  19. package/src/app/pages/MyWorkspacePage/__tests__/TypeGroupGrid.test.jsx +169 -49
  20. package/src/app/pages/MyWorkspacePage/index.jsx +41 -73
  21. package/src/app/pages/NodePage/NodeDataFlowTab.jsx +464 -0
  22. package/src/app/pages/NodePage/NodeDependenciesTab.jsx +1 -1
  23. package/src/app/pages/NodePage/NodeDimensionsTab.jsx +362 -0
  24. package/src/app/pages/NodePage/NodeLineageTab.jsx +1 -0
  25. package/src/app/pages/NodePage/NodesWithDimension.jsx +3 -3
  26. package/src/app/pages/NodePage/__tests__/NodeDataFlowTab.test.jsx +428 -0
  27. package/src/app/pages/NodePage/__tests__/NodeDependenciesTab.test.jsx +18 -1
  28. package/src/app/pages/NodePage/__tests__/NodeDimensionsTab.test.jsx +412 -0
  29. package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +28 -3
  30. package/src/app/pages/NodePage/__tests__/NodeWithDimension.test.jsx +2 -2
  31. package/src/app/pages/NodePage/index.jsx +15 -8
  32. package/src/app/pages/QueryPlannerPage/ResultsView.jsx +420 -86
  33. package/src/app/pages/QueryPlannerPage/SelectionPanel.jsx +32 -1
  34. package/src/app/pages/QueryPlannerPage/__tests__/ResultsView.test.jsx +322 -0
  35. package/src/app/pages/QueryPlannerPage/__tests__/SelectionPanel.test.jsx +431 -2
  36. package/src/app/pages/QueryPlannerPage/index.jsx +31 -5
  37. package/src/app/pages/QueryPlannerPage/styles.css +211 -2
  38. package/src/app/pages/Root/__tests__/index.test.jsx +2 -3
  39. package/src/app/pages/Root/index.tsx +1 -1
  40. package/src/app/services/DJService.js +133 -23
  41. package/src/app/services/__tests__/DJService.test.jsx +600 -11
  42. package/src/styles/index.css +32 -0
@@ -14,6 +14,49 @@ jest.mock('react-syntax-highlighter/src/styles/hljs', () => ({
14
14
  }));
15
15
  jest.mock('react-syntax-highlighter/dist/esm/languages/hljs/sql', () => ({}));
16
16
 
17
+ // Mock recharts to avoid canvas rendering issues in jsdom.
18
+ // YAxis calls tickFormatter with sample values (covering the formatYAxis helper).
19
+ jest.mock('recharts', () => {
20
+ const React = require('react');
21
+ const mockComponent =
22
+ name =>
23
+ ({ children, ...props }) =>
24
+ React.createElement('div', { 'data-testid': name, ...props }, children);
25
+
26
+ // YAxis: call tickFormatter with a spread of magnitudes so formatYAxis branches are hit
27
+ const MockYAxis = ({ children, tickFormatter, ...props }) => {
28
+ if (tickFormatter) {
29
+ // Exercise all four branches of formatYAxis
30
+ tickFormatter(1_500_000_000); // >=1B
31
+ tickFormatter(2_500_000); // >=1M
32
+ tickFormatter(5_000); // >=1K
33
+ tickFormatter(42); // plain
34
+ }
35
+ return React.createElement(
36
+ 'div',
37
+ { 'data-testid': 'YAxis', ...props },
38
+ children,
39
+ );
40
+ };
41
+
42
+ return {
43
+ LineChart: mockComponent('LineChart'),
44
+ BarChart: mockComponent('BarChart'),
45
+ Line: mockComponent('Line'),
46
+ Bar: mockComponent('Bar'),
47
+ XAxis: mockComponent('XAxis'),
48
+ YAxis: MockYAxis,
49
+ CartesianGrid: mockComponent('CartesianGrid'),
50
+ Tooltip: mockComponent('Tooltip'),
51
+ ResponsiveContainer: ({ children }) =>
52
+ React.createElement(
53
+ 'div',
54
+ { 'data-testid': 'ResponsiveContainer' },
55
+ children,
56
+ ),
57
+ };
58
+ });
59
+
17
60
  // Mock clipboard API
18
61
  const mockWriteText = jest.fn();
19
62
  Object.assign(navigator, {
@@ -385,4 +428,283 @@ describe('ResultsView', () => {
385
428
  );
386
429
  });
387
430
  });
431
+
432
+ describe('Chart Tab', () => {
433
+ // Results with a string dimension + numeric metric → bar chart config
434
+ const barChartResults = {
435
+ results: [
436
+ {
437
+ columns: [
438
+ { name: 'country', type: 'STRING' },
439
+ { name: 'revenue', type: 'FLOAT' },
440
+ ],
441
+ rows: [
442
+ ['US', 1000],
443
+ ['UK', 500],
444
+ ],
445
+ },
446
+ ],
447
+ };
448
+
449
+ // Results with a time dimension + numeric metric → line chart config
450
+ const lineChartResults = {
451
+ results: [
452
+ {
453
+ columns: [
454
+ { name: 'date', type: 'DATE' },
455
+ { name: 'revenue', type: 'FLOAT' },
456
+ ],
457
+ rows: [
458
+ ['2024-01-01', 1000],
459
+ ['2024-01-02', 1500],
460
+ ],
461
+ },
462
+ ],
463
+ };
464
+
465
+ // Results with two numeric columns (no string/time dim) → line with first as x
466
+ const allNumericResults = {
467
+ results: [
468
+ {
469
+ columns: [
470
+ { name: 'x_val', type: 'FLOAT' },
471
+ { name: 'y_val', type: 'FLOAT' },
472
+ ],
473
+ rows: [
474
+ [1.0, 10.5],
475
+ [2.0, 20.5],
476
+ ],
477
+ },
478
+ ],
479
+ };
480
+
481
+ // Scalar result: single numeric column, one row → KPI cards
482
+ const scalarResults = {
483
+ results: [
484
+ {
485
+ columns: [{ name: 'total_revenue', type: 'FLOAT' }],
486
+ rows: [[1234567.89]],
487
+ },
488
+ ],
489
+ };
490
+
491
+ it('renders Chart tab button', () => {
492
+ render(<ResultsView {...defaultProps} results={barChartResults} />);
493
+ expect(screen.getByText('Chart')).toBeInTheDocument();
494
+ });
495
+
496
+ it('Chart tab is enabled when data is chartable (bar chart data)', () => {
497
+ render(<ResultsView {...defaultProps} results={barChartResults} />);
498
+ const chartTab = screen.getByText('Chart').closest('button');
499
+ expect(chartTab).not.toHaveClass('disabled');
500
+ });
501
+
502
+ it('Chart tab is disabled when data is not chartable (string-only columns)', () => {
503
+ render(
504
+ <ResultsView
505
+ {...defaultProps}
506
+ results={{
507
+ results: [
508
+ {
509
+ columns: [{ name: 'name', type: 'STRING' }],
510
+ rows: [['Alice']],
511
+ },
512
+ ],
513
+ }}
514
+ />,
515
+ );
516
+ const chartTab = screen.getByText('Chart').closest('button');
517
+ expect(chartTab).toHaveClass('disabled');
518
+ });
519
+
520
+ it('switches to chart view when Chart tab is clicked (bar chart)', () => {
521
+ render(<ResultsView {...defaultProps} results={barChartResults} />);
522
+
523
+ fireEvent.click(screen.getByText('Chart'));
524
+
525
+ // The chart wrapper should appear
526
+ expect(
527
+ document.querySelector('.results-chart-wrapper'),
528
+ ).toBeInTheDocument();
529
+ expect(screen.getByTestId('BarChart')).toBeInTheDocument();
530
+ });
531
+
532
+ it('switches to chart view when Chart tab is clicked (line chart)', () => {
533
+ render(<ResultsView {...defaultProps} results={lineChartResults} />);
534
+
535
+ fireEvent.click(screen.getByText('Chart'));
536
+
537
+ expect(
538
+ document.querySelector('.results-chart-wrapper'),
539
+ ).toBeInTheDocument();
540
+ expect(screen.getByTestId('LineChart')).toBeInTheDocument();
541
+ });
542
+
543
+ it('switches back to table view when Table tab is clicked', () => {
544
+ render(<ResultsView {...defaultProps} results={barChartResults} />);
545
+
546
+ // Switch to chart
547
+ fireEvent.click(screen.getByText('Chart'));
548
+ expect(
549
+ document.querySelector('.results-chart-wrapper'),
550
+ ).toBeInTheDocument();
551
+
552
+ // Switch back to table
553
+ fireEvent.click(screen.getByText('Table'));
554
+ expect(
555
+ document.querySelector('.results-table-wrapper'),
556
+ ).toBeInTheDocument();
557
+ });
558
+
559
+ it('renders line chart for all-numeric columns (lines 93-95: no time/string dim)', () => {
560
+ render(<ResultsView {...defaultProps} results={allNumericResults} />);
561
+
562
+ fireEvent.click(screen.getByText('Chart'));
563
+
564
+ expect(screen.getByTestId('LineChart')).toBeInTheDocument();
565
+ });
566
+
567
+ it('renders KPI cards for scalar numeric result', () => {
568
+ render(<ResultsView {...defaultProps} results={scalarResults} />);
569
+
570
+ fireEvent.click(screen.getByText('Chart'));
571
+
572
+ expect(document.querySelector('.kpi-cards')).toBeInTheDocument();
573
+ expect(document.querySelector('.kpi-label')).toHaveTextContent(
574
+ 'total_revenue',
575
+ );
576
+ });
577
+
578
+ it('KPI card formats null value as em-dash', () => {
579
+ render(
580
+ <ResultsView
581
+ {...defaultProps}
582
+ results={{
583
+ results: [
584
+ {
585
+ columns: [{ name: 'revenue', type: 'FLOAT' }],
586
+ rows: [[null]],
587
+ },
588
+ ],
589
+ }}
590
+ />,
591
+ );
592
+
593
+ fireEvent.click(screen.getByText('Chart'));
594
+
595
+ expect(document.querySelector('.kpi-value')).toHaveTextContent('—');
596
+ });
597
+
598
+ it('KPI card formats numeric value with toLocaleString', () => {
599
+ render(<ResultsView {...defaultProps} results={scalarResults} />);
600
+
601
+ fireEvent.click(screen.getByText('Chart'));
602
+
603
+ // 1234567.89 formatted
604
+ const kpiValue = document.querySelector('.kpi-value');
605
+ expect(kpiValue).toBeInTheDocument();
606
+ // Value should be a localized number string (not null)
607
+ expect(kpiValue.textContent).not.toBe('—');
608
+ });
609
+
610
+ it('KPI card shows column type when present', () => {
611
+ render(<ResultsView {...defaultProps} results={scalarResults} />);
612
+
613
+ fireEvent.click(screen.getByText('Chart'));
614
+
615
+ expect(document.querySelector('.kpi-type')).toHaveTextContent('FLOAT');
616
+ });
617
+
618
+ it('renders small multiples when 3+ metric columns present', () => {
619
+ const threeMetricResults = {
620
+ results: [
621
+ {
622
+ columns: [
623
+ { name: 'date', type: 'DATE' },
624
+ { name: 'metric_a', type: 'FLOAT' },
625
+ { name: 'metric_b', type: 'FLOAT' },
626
+ { name: 'metric_c', type: 'FLOAT' },
627
+ ],
628
+ rows: [
629
+ ['2024-01-01', 10, 20, 30],
630
+ ['2024-01-02', 15, 25, 35],
631
+ ],
632
+ },
633
+ ],
634
+ };
635
+
636
+ render(<ResultsView {...defaultProps} results={threeMetricResults} />);
637
+
638
+ fireEvent.click(screen.getByText('Chart'));
639
+
640
+ // SMALL_MULTIPLES_THRESHOLD is 2, so 3 metric cols triggers small multiples
641
+ expect(document.querySelector('.small-multiples')).toBeInTheDocument();
642
+ const labels = document.querySelectorAll('.small-multiple-label');
643
+ expect(labels.length).toBe(3);
644
+ });
645
+
646
+ it('does not click disabled Chart tab (canChart false)', () => {
647
+ // Single string column → not chartable
648
+ render(
649
+ <ResultsView
650
+ {...defaultProps}
651
+ results={{
652
+ results: [
653
+ {
654
+ columns: [{ name: 'label', type: 'STRING' }],
655
+ rows: [['x']],
656
+ },
657
+ ],
658
+ }}
659
+ />,
660
+ );
661
+
662
+ const chartTab = screen.getByText('Chart').closest('button');
663
+ fireEvent.click(chartTab);
664
+
665
+ // Should still be on table view
666
+ expect(
667
+ document.querySelector('.results-table-wrapper'),
668
+ ).toBeInTheDocument();
669
+ });
670
+
671
+ it('resets to table view if new results are not chartable while on chart tab', () => {
672
+ const { rerender } = render(
673
+ <ResultsView {...defaultProps} results={barChartResults} />,
674
+ );
675
+
676
+ // Switch to chart view
677
+ fireEvent.click(screen.getByText('Chart'));
678
+ expect(
679
+ document.querySelector('.results-chart-wrapper'),
680
+ ).toBeInTheDocument();
681
+
682
+ // Re-render with non-chartable results (empty rows)
683
+ rerender(
684
+ <ResultsView
685
+ {...defaultProps}
686
+ results={{ results: [{ columns: [], rows: [] }] }}
687
+ />,
688
+ );
689
+
690
+ // Should auto-reset to table view
691
+ expect(
692
+ document.querySelector('.results-table-wrapper'),
693
+ ).toBeInTheDocument();
694
+ });
695
+
696
+ it('shows links during loading when links prop is provided', () => {
697
+ render(
698
+ <ResultsView
699
+ {...defaultProps}
700
+ loading={true}
701
+ links={['https://example.com/query/123']}
702
+ />,
703
+ );
704
+
705
+ const link = screen.getByText('View query ↗');
706
+ expect(link).toBeInTheDocument();
707
+ expect(link).toHaveAttribute('href', 'https://example.com/query/123');
708
+ });
709
+ });
388
710
  });