datajunction-ui 0.0.43 → 0.0.45

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.
@@ -9,7 +9,6 @@ import NodeGraphTab from './NodeGraphTab';
9
9
  import NodeHistory from './NodeHistory';
10
10
  import NotebookDownload from './NotebookDownload';
11
11
  import DJClientContext from '../../providers/djclient';
12
- import NodeValidateTab from './NodeValidateTab';
13
12
  import NodeMaterializationTab from './NodeMaterializationTab';
14
13
  import NodePreAggregationsTab from './NodePreAggregationsTab';
15
14
  import ClientCodePopover from './ClientCodePopover';
@@ -35,6 +34,11 @@ export function NodePage() {
35
34
  const [node, setNode] = useState(null);
36
35
 
37
36
  const onClickTab = id => () => {
37
+ // Preview tab redirects to Query Planner instead of showing content
38
+ if (id === 'preview') {
39
+ navigate(`/planner?metrics=${encodeURIComponent(name)}`);
40
+ return;
41
+ }
38
42
  navigate(`/nodes/${name}/${id}`);
39
43
  setState({ selectedTab: id });
40
44
  };
@@ -74,7 +78,7 @@ export function NodePage() {
74
78
  {
75
79
  id: 'columns',
76
80
  name: 'Columns',
77
- display: true,
81
+ display: node?.type !== 'metric',
78
82
  },
79
83
  {
80
84
  id: 'graph',
@@ -86,15 +90,10 @@ export function NodePage() {
86
90
  name: 'History',
87
91
  display: true,
88
92
  },
89
- {
90
- id: 'validate',
91
- name: '► Validate',
92
- display: node?.type !== 'source',
93
- },
94
93
  {
95
94
  id: 'materializations',
96
95
  name: 'Materializations',
97
- display: node?.type !== 'source',
96
+ display: node?.type !== 'source' && node?.type !== 'metric',
98
97
  },
99
98
  {
100
99
  id: 'linked',
@@ -111,6 +110,11 @@ export function NodePage() {
111
110
  name: 'Dependencies',
112
111
  display: node?.type !== 'cube',
113
112
  },
113
+ {
114
+ id: 'preview',
115
+ name: 'Preview →',
116
+ display: node?.type === 'metric',
117
+ },
114
118
  ];
115
119
  };
116
120
  let tabToDisplay = null;
@@ -128,9 +132,6 @@ export function NodePage() {
128
132
  case 'history':
129
133
  tabToDisplay = <NodeHistory node={node} djClient={djClient} />;
130
134
  break;
131
- case 'validate':
132
- tabToDisplay = <NodeValidateTab node={node} djClient={djClient} />;
133
- break;
134
135
  case 'materializations':
135
136
  // Cube nodes use cube-specific materialization tab
136
137
  // Other nodes (transform, metric, dimension) use pre-aggregations tab
@@ -292,6 +292,7 @@ export function QueryOverviewPanel({
292
292
  onClearWorkflowUrls,
293
293
  loadedCubeName = null, // Existing cube name if loaded from preset
294
294
  cubeMaterialization = null, // Full cube materialization info {schedule, strategy, lookbackWindow, ...}
295
+ cubeAvailability = null, // Cube availability info for data freshness
295
296
  onUpdateCubeConfig,
296
297
  onRefreshCubeWorkflow,
297
298
  onRunCubeBackfill,
@@ -578,6 +579,9 @@ export function QueryOverviewPanel({
578
579
  const grainGroups = measuresResult.grain_groups || [];
579
580
  const metricFormulas = measuresResult.metric_formulas || [];
580
581
  const sql = metricsResult.sql || '';
582
+ const dialect = metricsResult.dialect || null;
583
+ const cubeName = metricsResult.cube_name || null;
584
+ const isFastQuery = !!cubeName; // Fast if using materialized cube
581
585
 
582
586
  // Determine if materialization is already configured (has active workflows)
583
587
  const isMaterialized =
@@ -609,11 +613,29 @@ export function QueryOverviewPanel({
609
613
  {/* Header */}
610
614
  <div className="details-header">
611
615
  <h2 className="details-title">Query Plan</h2>
612
- <p className="details-full-name">
616
+ <p className="details-info-row">
613
617
  {selectedMetrics.length} metric
614
618
  {selectedMetrics.length !== 1 ? 's' : ''} ×{' '}
615
619
  {selectedDimensions.length} dimension
616
620
  {selectedDimensions.length !== 1 ? 's' : ''}
621
+ {isFastQuery && (
622
+ <>
623
+ {' · '}
624
+ <span className="info-materialized">
625
+ <span style={{ fontFamily: 'sans-serif' }}>⚡</span>{' '}
626
+ Materialized cube available
627
+ </span>
628
+ {cubeAvailability?.validThroughTs && (
629
+ <>
630
+ {' '}
631
+ · Valid thru{' '}
632
+ {new Date(
633
+ cubeAvailability.validThroughTs,
634
+ ).toLocaleDateString()}
635
+ </>
636
+ )}
637
+ </>
638
+ )}
617
639
  </p>
618
640
  </div>
619
641
 
@@ -2198,6 +2220,29 @@ export function QueryOverviewPanel({
2198
2220
  <span className="section-icon">⌘</span>
2199
2221
  Generated SQL
2200
2222
  </h3>
2223
+ <span className="sql-info-inline">
2224
+ {sqlViewMode === 'optimized' && isFastQuery ? (
2225
+ <>
2226
+ <span className="info-materialized">
2227
+ <span style={{ fontFamily: 'sans-serif' }}>⚡</span> Using
2228
+ materialized cube
2229
+ </span>
2230
+ {cubeAvailability?.validThroughTs && (
2231
+ <>
2232
+ {' · Valid thru '}
2233
+ {new Date(
2234
+ cubeAvailability.validThroughTs,
2235
+ ).toLocaleDateString()}
2236
+ </>
2237
+ )}
2238
+ </>
2239
+ ) : sqlViewMode === 'raw' ? (
2240
+ <span className="info-base-tables">
2241
+ <span style={{ fontFamily: 'sans-serif' }}>⚠️</span> Using
2242
+ base tables
2243
+ </span>
2244
+ ) : null}
2245
+ </span>
2201
2246
  <div className="sql-view-toggle">
2202
2247
  <button
2203
2248
  className={`sql-toggle-btn ${
@@ -0,0 +1,281 @@
1
+ import { useState, useCallback, useMemo } from 'react';
2
+ import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
3
+ import { foundation } from 'react-syntax-highlighter/src/styles/hljs';
4
+ import sql from 'react-syntax-highlighter/dist/esm/languages/hljs/sql';
5
+
6
+ SyntaxHighlighter.registerLanguage('sql', sql);
7
+
8
+ /**
9
+ * ResultsView - Displays query results with SQL and data table
10
+ * Layout: SQL in top 1/3, results in bottom 2/3
11
+ */
12
+ export function ResultsView({
13
+ sql: sqlQuery,
14
+ results,
15
+ loading,
16
+ error,
17
+ elapsedTime,
18
+ onBackToPlan,
19
+ selectedMetrics,
20
+ selectedDimensions,
21
+ filters,
22
+ dialect,
23
+ cubeName,
24
+ availability,
25
+ }) {
26
+ const [copied, setCopied] = useState(false);
27
+ const [sortColumn, setSortColumn] = useState(null);
28
+ const [sortDirection, setSortDirection] = useState('asc');
29
+
30
+ const handleCopySql = useCallback(() => {
31
+ if (sqlQuery) {
32
+ navigator.clipboard.writeText(sqlQuery);
33
+ setCopied(true);
34
+ setTimeout(() => setCopied(false), 2000);
35
+ }
36
+ }, [sqlQuery]);
37
+
38
+ // Parse results data - handle new v3 format
39
+ const columns = results?.results?.[0]?.columns || [];
40
+ const rows = results?.results?.[0]?.rows || [];
41
+ const rowCount = rows.length;
42
+
43
+ // Handle column header click for sorting
44
+ const handleSort = useCallback(
45
+ columnIndex => {
46
+ if (sortColumn === columnIndex) {
47
+ setSortDirection(d => (d === 'asc' ? 'desc' : 'asc'));
48
+ } else {
49
+ setSortColumn(columnIndex);
50
+ setSortDirection('asc');
51
+ }
52
+ },
53
+ [sortColumn],
54
+ );
55
+
56
+ // Sort rows based on current sort state
57
+ const sortedRows = useMemo(() => {
58
+ if (sortColumn === null) return rows;
59
+ return [...rows].sort((a, b) => {
60
+ const aVal = a[sortColumn];
61
+ const bVal = b[sortColumn];
62
+ // Handle nulls - nulls go last
63
+ if (aVal === null && bVal === null) return 0;
64
+ if (aVal === null) return 1;
65
+ if (bVal === null) return -1;
66
+ // Compare values
67
+ let cmp;
68
+ if (typeof aVal === 'number' && typeof bVal === 'number') {
69
+ cmp = aVal - bVal;
70
+ } else {
71
+ cmp = String(aVal).localeCompare(String(bVal));
72
+ }
73
+ return sortDirection === 'asc' ? cmp : -cmp;
74
+ });
75
+ }, [rows, sortColumn, sortDirection]);
76
+
77
+ return (
78
+ <div className="results-view">
79
+ {/* Header */}
80
+ <div className="results-header">
81
+ <button className="back-to-plan-btn" onClick={onBackToPlan}>
82
+ <span className="back-arrow">←</span>
83
+ <span>Back to Plan</span>
84
+ </button>
85
+ <div className="results-summary">
86
+ {loading ? (
87
+ <span className="results-loading-text">Running query...</span>
88
+ ) : error ? (
89
+ <span className="results-error-text">Query failed</span>
90
+ ) : (
91
+ <>
92
+ <span className="results-count">
93
+ {rowCount.toLocaleString()} rows
94
+ </span>
95
+ {elapsedTime != null && (
96
+ <span className="results-time">{elapsedTime.toFixed(2)}s</span>
97
+ )}
98
+ </>
99
+ )}
100
+ </div>
101
+ </div>
102
+
103
+ {/* Two-pane layout: SQL (top 1/3) + Results (bottom 2/3) */}
104
+ <div className="results-panes">
105
+ {/* SQL Pane - always visible, top 1/3 */}
106
+ <div className="sql-pane">
107
+ <div className="sql-pane-header">
108
+ <span className="sql-pane-title">SQL Query</span>
109
+ {cubeName && (
110
+ <span
111
+ className="sql-pane-info"
112
+ title={
113
+ availability
114
+ ? `Querying materialized dataset ${[
115
+ availability.catalog,
116
+ availability.schema_,
117
+ availability.table,
118
+ ]
119
+ .filter(Boolean)
120
+ .join('.')}, last refreshed for data through ${new Date(
121
+ availability.validThroughTs,
122
+ ).toLocaleDateString()}`
123
+ : undefined
124
+ }
125
+ >
126
+ <span className="info-materialized">
127
+ <span style={{ fontFamily: 'sans-serif' }}>⚡</span> Using
128
+ materialized cube
129
+ </span>
130
+ {availability?.validThroughTs && (
131
+ <>
132
+ {' · Valid thru '}
133
+ {new Date(availability.validThroughTs).toLocaleDateString()}
134
+ </>
135
+ )}
136
+ </span>
137
+ )}
138
+ <button
139
+ className={`copy-btn ${copied ? 'copied' : ''}`}
140
+ onClick={handleCopySql}
141
+ disabled={!sqlQuery}
142
+ >
143
+ {copied ? '✓ Copied' : 'Copy'}
144
+ </button>
145
+ </div>
146
+ <div className="sql-pane-content">
147
+ {sqlQuery ? (
148
+ <SyntaxHighlighter
149
+ language="sql"
150
+ style={foundation}
151
+ wrapLongLines={true}
152
+ customStyle={{
153
+ margin: 0,
154
+ padding: '12px 16px',
155
+ background: '#f8fafc',
156
+ fontSize: '12px',
157
+ height: '100%',
158
+ overflow: 'auto',
159
+ }}
160
+ >
161
+ {sqlQuery}
162
+ </SyntaxHighlighter>
163
+ ) : (
164
+ <div className="sql-pane-empty">Generating SQL...</div>
165
+ )}
166
+ </div>
167
+ </div>
168
+
169
+ {/* Results Pane - bottom 2/3 */}
170
+ <div className="results-pane">
171
+ {loading ? (
172
+ <div className="results-loading">
173
+ <div className="loading-spinner large" />
174
+ <span>Executing query...</span>
175
+ <span className="loading-hint">
176
+ Querying {selectedMetrics.length} metric(s) with{' '}
177
+ {selectedDimensions.length} dimension(s)
178
+ </span>
179
+ </div>
180
+ ) : error ? (
181
+ <div className="results-error">
182
+ <div className="error-icon">⚠</div>
183
+ <h3>Query Failed</h3>
184
+ <p className="error-message">{error}</p>
185
+ <button
186
+ className="action-btn action-btn-primary"
187
+ onClick={onBackToPlan}
188
+ >
189
+ Back to Plan
190
+ </button>
191
+ </div>
192
+ ) : (
193
+ <div className="results-table-section">
194
+ <div className="table-header">
195
+ <span className="table-title">Results</span>
196
+ <span className="table-count">
197
+ {rowCount.toLocaleString()} rows
198
+ </span>
199
+ {filters && filters.length > 0 && (
200
+ <div className="table-filters">
201
+ {filters.map((filter, idx) => (
202
+ <span key={idx} className="filter-chip small">
203
+ {filter}
204
+ </span>
205
+ ))}
206
+ </div>
207
+ )}
208
+ </div>
209
+ <div className="results-table-wrapper">
210
+ {rowCount === 0 ? (
211
+ <div className="table-empty">
212
+ <p>No results returned</p>
213
+ </div>
214
+ ) : (
215
+ <table className="results-table">
216
+ <thead>
217
+ <tr>
218
+ {columns.map((col, idx) => (
219
+ <th
220
+ key={idx}
221
+ title={col.semantic_name || col.name}
222
+ onClick={() => handleSort(idx)}
223
+ className={sortColumn === idx ? 'sorted' : ''}
224
+ >
225
+ <span className="col-header-content">
226
+ {col.name}
227
+ <span className="sort-arrows">
228
+ <span
229
+ className={`sort-arrow up ${
230
+ sortColumn === idx &&
231
+ sortDirection === 'asc'
232
+ ? 'active'
233
+ : ''
234
+ }`}
235
+ >
236
+
237
+ </span>
238
+ <span
239
+ className={`sort-arrow down ${
240
+ sortColumn === idx &&
241
+ sortDirection === 'desc'
242
+ ? 'active'
243
+ : ''
244
+ }`}
245
+ >
246
+
247
+ </span>
248
+ </span>
249
+ </span>
250
+ <span className="col-type">{col.type}</span>
251
+ </th>
252
+ ))}
253
+ </tr>
254
+ </thead>
255
+ <tbody>
256
+ {sortedRows.map((row, rowIdx) => (
257
+ <tr key={rowIdx}>
258
+ {row.map((cell, cellIdx) => (
259
+ <td key={cellIdx}>
260
+ {cell === null ? (
261
+ <span className="null-value">NULL</span>
262
+ ) : (
263
+ String(cell)
264
+ )}
265
+ </td>
266
+ ))}
267
+ </tr>
268
+ ))}
269
+ </tbody>
270
+ </table>
271
+ )}
272
+ </div>
273
+ </div>
274
+ )}
275
+ </div>
276
+ </div>
277
+ </div>
278
+ );
279
+ }
280
+
281
+ export default ResultsView;