datajunction-ui 0.0.94 → 0.0.96

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.
@@ -1,5 +1,11 @@
1
1
  import { useState, useMemo, useEffect, useRef } from 'react';
2
2
 
3
+ const ENGINE_OPTIONS = [
4
+ { value: null, label: 'Auto' },
5
+ { value: 'druid', label: 'Druid' },
6
+ { value: 'trino', label: 'Trino' },
7
+ ];
8
+
3
9
  /**
4
10
  * SelectionPanel - Browse and select metrics and dimensions
5
11
  * Features selected items as chips at the top for visibility
@@ -19,6 +25,8 @@ export function SelectionPanel({
19
25
  onClearSelection,
20
26
  filters = [],
21
27
  onFiltersChange,
28
+ selectedEngine = null,
29
+ onEngineChange,
22
30
  onRunQuery,
23
31
  canRunQuery = false,
24
32
  queryLoading = false,
@@ -604,7 +612,11 @@ export function SelectionPanel({
604
612
 
605
613
  <div className="selection-list dimensions-list">
606
614
  {filteredDimensions.map(dim => (
607
- <label key={dim.name} className="selection-item dimension-item">
615
+ <label
616
+ key={dim.name}
617
+ className="selection-item dimension-item"
618
+ title={dim.name}
619
+ >
608
620
  <input
609
621
  type="checkbox"
610
622
  checked={selectedDimensions.includes(dim.name)}
@@ -614,6 +626,7 @@ export function SelectionPanel({
614
626
  <span className="item-name">
615
627
  {getDimDisplayName(dim.name)}
616
628
  </span>
629
+ <span className="dimension-full-name">{dim.name}</span>
617
630
  {dim.path && dim.path.length > 1 && (
618
631
  <span className="dimension-path">
619
632
  {dim.path.slice(1).join(' ▶ ')}
@@ -683,6 +696,24 @@ export function SelectionPanel({
683
696
  </div>
684
697
  </div>
685
698
 
699
+ {/* Engine Selection */}
700
+ <div className="engine-section">
701
+ <span className="engine-label">Engine</span>
702
+ <div className="engine-pills">
703
+ {ENGINE_OPTIONS.map(({ value, label }) => (
704
+ <button
705
+ key={label}
706
+ className={`engine-pill${
707
+ selectedEngine === value ? ' active' : ''
708
+ }`}
709
+ onClick={() => onEngineChange && onEngineChange(value)}
710
+ >
711
+ {label}
712
+ </button>
713
+ ))}
714
+ </div>
715
+ </div>
716
+
686
717
  {/* Run Query Section */}
687
718
  <div className="run-query-section">
688
719
  <button
@@ -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,287 @@ 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 enabled for any data with rows, showing no-data message when unchartable', () => {
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).not.toHaveClass('disabled');
518
+
519
+ fireEvent.click(chartTab);
520
+ expect(
521
+ screen.getByText('No chartable data detected'),
522
+ ).toBeInTheDocument();
523
+ });
524
+
525
+ it('switches to chart view when Chart tab is clicked (bar chart)', () => {
526
+ render(<ResultsView {...defaultProps} results={barChartResults} />);
527
+
528
+ fireEvent.click(screen.getByText('Chart'));
529
+
530
+ // The chart wrapper should appear
531
+ expect(
532
+ document.querySelector('.results-chart-wrapper'),
533
+ ).toBeInTheDocument();
534
+ expect(screen.getByTestId('BarChart')).toBeInTheDocument();
535
+ });
536
+
537
+ it('switches to chart view when Chart tab is clicked (line chart)', () => {
538
+ render(<ResultsView {...defaultProps} results={lineChartResults} />);
539
+
540
+ fireEvent.click(screen.getByText('Chart'));
541
+
542
+ expect(
543
+ document.querySelector('.results-chart-wrapper'),
544
+ ).toBeInTheDocument();
545
+ expect(screen.getByTestId('LineChart')).toBeInTheDocument();
546
+ });
547
+
548
+ it('switches back to table view when Table tab is clicked', () => {
549
+ render(<ResultsView {...defaultProps} results={barChartResults} />);
550
+
551
+ // Switch to chart
552
+ fireEvent.click(screen.getByText('Chart'));
553
+ expect(
554
+ document.querySelector('.results-chart-wrapper'),
555
+ ).toBeInTheDocument();
556
+
557
+ // Switch back to table
558
+ fireEvent.click(screen.getByText('Table'));
559
+ expect(
560
+ document.querySelector('.results-table-wrapper'),
561
+ ).toBeInTheDocument();
562
+ });
563
+
564
+ it('renders line chart for all-numeric columns (lines 93-95: no time/string dim)', () => {
565
+ render(<ResultsView {...defaultProps} results={allNumericResults} />);
566
+
567
+ fireEvent.click(screen.getByText('Chart'));
568
+
569
+ expect(screen.getByTestId('LineChart')).toBeInTheDocument();
570
+ });
571
+
572
+ it('renders KPI cards for scalar numeric result', () => {
573
+ render(<ResultsView {...defaultProps} results={scalarResults} />);
574
+
575
+ fireEvent.click(screen.getByText('Chart'));
576
+
577
+ expect(document.querySelector('.kpi-cards')).toBeInTheDocument();
578
+ expect(document.querySelector('.kpi-label')).toHaveTextContent(
579
+ 'total_revenue',
580
+ );
581
+ });
582
+
583
+ it('KPI card formats null value as em-dash', () => {
584
+ render(
585
+ <ResultsView
586
+ {...defaultProps}
587
+ results={{
588
+ results: [
589
+ {
590
+ columns: [{ name: 'revenue', type: 'FLOAT' }],
591
+ rows: [[null]],
592
+ },
593
+ ],
594
+ }}
595
+ />,
596
+ );
597
+
598
+ fireEvent.click(screen.getByText('Chart'));
599
+
600
+ expect(document.querySelector('.kpi-value')).toHaveTextContent('—');
601
+ });
602
+
603
+ it('KPI card formats numeric value with toLocaleString', () => {
604
+ render(<ResultsView {...defaultProps} results={scalarResults} />);
605
+
606
+ fireEvent.click(screen.getByText('Chart'));
607
+
608
+ // 1234567.89 formatted
609
+ const kpiValue = document.querySelector('.kpi-value');
610
+ expect(kpiValue).toBeInTheDocument();
611
+ // Value should be a localized number string (not null)
612
+ expect(kpiValue.textContent).not.toBe('—');
613
+ });
614
+
615
+ it('KPI card shows column type when present', () => {
616
+ render(<ResultsView {...defaultProps} results={scalarResults} />);
617
+
618
+ fireEvent.click(screen.getByText('Chart'));
619
+
620
+ expect(document.querySelector('.kpi-type')).toHaveTextContent('FLOAT');
621
+ });
622
+
623
+ it('renders small multiples when 3+ metric columns present', () => {
624
+ const threeMetricResults = {
625
+ results: [
626
+ {
627
+ columns: [
628
+ { name: 'date', type: 'DATE' },
629
+ { name: 'metric_a', type: 'FLOAT' },
630
+ { name: 'metric_b', type: 'FLOAT' },
631
+ { name: 'metric_c', type: 'FLOAT' },
632
+ ],
633
+ rows: [
634
+ ['2024-01-01', 10, 20, 30],
635
+ ['2024-01-02', 15, 25, 35],
636
+ ],
637
+ },
638
+ ],
639
+ };
640
+
641
+ render(<ResultsView {...defaultProps} results={threeMetricResults} />);
642
+
643
+ fireEvent.click(screen.getByText('Chart'));
644
+
645
+ // SMALL_MULTIPLES_THRESHOLD is 2, so 3 metric cols triggers small multiples
646
+ expect(document.querySelector('.small-multiples')).toBeInTheDocument();
647
+ const labels = document.querySelectorAll('.small-multiple-label');
648
+ expect(labels.length).toBe(3);
649
+ });
650
+
651
+ it('shows no-data message when Chart tab clicked with unchartable data', () => {
652
+ // Single string column → not chartable, but tab is still clickable
653
+ render(
654
+ <ResultsView
655
+ {...defaultProps}
656
+ results={{
657
+ results: [
658
+ {
659
+ columns: [{ name: 'label', type: 'STRING' }],
660
+ rows: [['x']],
661
+ },
662
+ ],
663
+ }}
664
+ />,
665
+ );
666
+
667
+ const chartTab = screen.getByText('Chart').closest('button');
668
+ fireEvent.click(chartTab);
669
+
670
+ expect(
671
+ screen.getByText('No chartable data detected'),
672
+ ).toBeInTheDocument();
673
+ });
674
+
675
+ it('resets to table view if new results are not chartable while on chart tab', () => {
676
+ const { rerender } = render(
677
+ <ResultsView {...defaultProps} results={barChartResults} />,
678
+ );
679
+
680
+ // Switch to chart view
681
+ fireEvent.click(screen.getByText('Chart'));
682
+ expect(
683
+ document.querySelector('.results-chart-wrapper'),
684
+ ).toBeInTheDocument();
685
+
686
+ // Re-render with non-chartable results (empty rows)
687
+ rerender(
688
+ <ResultsView
689
+ {...defaultProps}
690
+ results={{ results: [{ columns: [], rows: [] }] }}
691
+ />,
692
+ );
693
+
694
+ // Should auto-reset to table view
695
+ expect(
696
+ document.querySelector('.results-table-wrapper'),
697
+ ).toBeInTheDocument();
698
+ });
699
+
700
+ it('shows links during loading when links prop is provided', () => {
701
+ render(
702
+ <ResultsView
703
+ {...defaultProps}
704
+ loading={true}
705
+ links={['https://example.com/query/123']}
706
+ />,
707
+ );
708
+
709
+ const link = screen.getByText('View query ↗');
710
+ expect(link).toBeInTheDocument();
711
+ expect(link).toHaveAttribute('href', 'https://example.com/query/123');
712
+ });
713
+ });
388
714
  });