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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datajunction-ui",
3
- "version": "0.0.94",
3
+ "version": "0.0.96",
4
4
  "description": "DataJunction UI",
5
5
  "module": "src/index.tsx",
6
6
  "repository": {
@@ -10,6 +10,7 @@ export default function Search() {
10
10
  const [searchResults, setSearchResults] = useState([]);
11
11
  const [isLoading, setIsLoading] = useState(false);
12
12
  const hasLoadedRef = useRef(false);
13
+ const inputRef = useRef(null);
13
14
 
14
15
  const djClient = useContext(DJClientContext).DataJunctionAPI;
15
16
 
@@ -64,39 +65,39 @@ export default function Search() {
64
65
 
65
66
  return (
66
67
  <div>
67
- <form
68
- className="search-box"
69
- onSubmit={e => {
70
- e.preventDefault();
71
- }}
72
- >
68
+ <div className="nav-search-box" onClick={() => inputRef.current?.focus()}>
73
69
  <input
70
+ ref={inputRef}
74
71
  type="text"
75
- placeholder={isLoading ? 'Loading...' : 'Search'}
72
+ placeholder={isLoading ? 'Loading...' : 'Search nodes...'}
76
73
  name="search"
77
74
  value={searchValue}
78
75
  onChange={handleChange}
79
76
  onFocus={loadSearchData}
80
77
  />
81
- </form>
82
- <div className="search-results">
83
- {searchResults.slice(0, 20).map(item => {
84
- const itemUrl =
85
- item.type !== 'tag' ? `/nodes/${item.name}` : `/tags/${item.name}`;
86
- return (
87
- <a key={item.name} href={itemUrl}>
88
- <div className="search-result-item">
89
- <span className={`node_type__${item.type} badge node_type`}>
90
- {item.type}
91
- </span>
92
- {item.display_name} (<b>{item.name}</b>){' '}
93
- {item.description ? '- ' : ' '}
94
- {truncate(item.description || '')}
95
- </div>
96
- </a>
97
- );
98
- })}
99
78
  </div>
79
+ {searchResults.length > 0 && (
80
+ <div className="search-results">
81
+ {searchResults.slice(0, 20).map(item => {
82
+ const itemUrl =
83
+ item.type !== 'tag'
84
+ ? `/nodes/${item.name}`
85
+ : `/tags/${item.name}`;
86
+ return (
87
+ <a key={item.name} href={itemUrl}>
88
+ <div className="search-result-item">
89
+ <span className={`node_type__${item.type} badge node_type`}>
90
+ {item.type}
91
+ </span>
92
+ {item.display_name} (<b>{item.name}</b>){' '}
93
+ {item.description ? '- ' : ' '}
94
+ {truncate(item.description || '')}
95
+ </div>
96
+ </a>
97
+ );
98
+ })}
99
+ </div>
100
+ )}
100
101
  </div>
101
102
  );
102
103
  }
@@ -13,7 +13,12 @@ export default class Tab extends Component {
13
13
  aria-label={this.props.name}
14
14
  aria-hidden="false"
15
15
  >
16
- {this.props.name}
16
+ <span
17
+ style={{ display: 'inline-flex', alignItems: 'center', gap: '4px' }}
18
+ >
19
+ {this.props.icon}
20
+ {this.props.name}
21
+ </span>
17
22
  </button>
18
23
  );
19
24
  }
@@ -56,7 +56,7 @@ describe('<Search />', () => {
56
56
  </DJClientContext.Provider>,
57
57
  );
58
58
 
59
- expect(getByPlaceholderText('Search')).toBeInTheDocument();
59
+ expect(getByPlaceholderText('Search nodes...')).toBeInTheDocument();
60
60
  });
61
61
 
62
62
  it('fetches and initializes search data on focus (lazy loading)', async () => {
@@ -73,7 +73,7 @@ describe('<Search />', () => {
73
73
  expect(mockDjClient.DataJunctionAPI.nodeDetails).not.toHaveBeenCalled();
74
74
 
75
75
  // Focus on search input to trigger lazy loading
76
- const searchInput = getByPlaceholderText('Search');
76
+ const searchInput = getByPlaceholderText('Search nodes...');
77
77
  fireEvent.focus(searchInput);
78
78
 
79
79
  await waitFor(() => {
@@ -92,7 +92,7 @@ describe('<Search />', () => {
92
92
  </DJClientContext.Provider>,
93
93
  );
94
94
 
95
- const searchInput = getByPlaceholderText('Search');
95
+ const searchInput = getByPlaceholderText('Search nodes...');
96
96
  // Focus to trigger lazy loading
97
97
  fireEvent.focus(searchInput);
98
98
 
@@ -117,7 +117,7 @@ describe('<Search />', () => {
117
117
  </DJClientContext.Provider>,
118
118
  );
119
119
 
120
- const searchInput = getByPlaceholderText('Search');
120
+ const searchInput = getByPlaceholderText('Search nodes...');
121
121
  // Focus to trigger lazy loading
122
122
  fireEvent.focus(searchInput);
123
123
 
@@ -143,7 +143,7 @@ describe('<Search />', () => {
143
143
  </DJClientContext.Provider>,
144
144
  );
145
145
 
146
- const searchInput = getByPlaceholderText('Search');
146
+ const searchInput = getByPlaceholderText('Search nodes...');
147
147
  // Focus to trigger lazy loading
148
148
  fireEvent.focus(searchInput);
149
149
 
@@ -169,7 +169,7 @@ describe('<Search />', () => {
169
169
  </DJClientContext.Provider>,
170
170
  );
171
171
 
172
- const searchInput = getByPlaceholderText('Search');
172
+ const searchInput = getByPlaceholderText('Search nodes...');
173
173
  // Focus to trigger lazy loading
174
174
  fireEvent.focus(searchInput);
175
175
 
@@ -194,7 +194,7 @@ describe('<Search />', () => {
194
194
  </DJClientContext.Provider>,
195
195
  );
196
196
 
197
- const searchInput = getByPlaceholderText('Search');
197
+ const searchInput = getByPlaceholderText('Search nodes...');
198
198
  // Focus to trigger lazy loading
199
199
  fireEvent.focus(searchInput);
200
200
 
@@ -226,7 +226,7 @@ describe('<Search />', () => {
226
226
  </DJClientContext.Provider>,
227
227
  );
228
228
 
229
- const searchInput = getByPlaceholderText('Search');
229
+ const searchInput = getByPlaceholderText('Search nodes...');
230
230
  // Focus to trigger lazy loading
231
231
  fireEvent.focus(searchInput);
232
232
 
@@ -256,7 +256,7 @@ describe('<Search />', () => {
256
256
  );
257
257
 
258
258
  // Focus to trigger lazy loading
259
- const searchInput = getByPlaceholderText('Search');
259
+ const searchInput = getByPlaceholderText('Search nodes...');
260
260
  fireEvent.focus(searchInput);
261
261
 
262
262
  await waitFor(() => {
@@ -269,7 +269,7 @@ describe('<Search />', () => {
269
269
  consoleErrorSpy.mockRestore();
270
270
  });
271
271
 
272
- it('prevents form submission', async () => {
272
+ it('renders search input inside nav-search-box container', async () => {
273
273
  mockDjClient.DataJunctionAPI.nodeDetails.mockResolvedValue([]);
274
274
  mockDjClient.DataJunctionAPI.listTags.mockResolvedValue([]);
275
275
 
@@ -279,17 +279,9 @@ describe('<Search />', () => {
279
279
  </DJClientContext.Provider>,
280
280
  );
281
281
 
282
- const form = container.querySelector('form');
283
-
284
- const submitEvent = new Event('submit', {
285
- bubbles: true,
286
- cancelable: true,
287
- });
288
- const preventDefaultSpy = jest.spyOn(submitEvent, 'preventDefault');
289
-
290
- form.dispatchEvent(submitEvent);
291
-
292
- expect(preventDefaultSpy).toHaveBeenCalled();
282
+ const searchBox = container.querySelector('.nav-search-box');
283
+ expect(searchBox).toBeInTheDocument();
284
+ expect(searchBox.querySelector('input')).toBeInTheDocument();
293
285
  });
294
286
 
295
287
  it('handles empty tags array', async () => {
@@ -302,7 +294,7 @@ describe('<Search />', () => {
302
294
  </DJClientContext.Provider>,
303
295
  );
304
296
 
305
- const searchInput = getByPlaceholderText('Search');
297
+ const searchInput = getByPlaceholderText('Search nodes...');
306
298
  // Focus to trigger lazy loading
307
299
  fireEvent.focus(searchInput);
308
300
 
@@ -324,7 +316,7 @@ describe('<Search />', () => {
324
316
  </DJClientContext.Provider>,
325
317
  );
326
318
 
327
- const searchInput = getByPlaceholderText('Search');
319
+ const searchInput = getByPlaceholderText('Search nodes...');
328
320
  // Focus to trigger lazy loading
329
321
  fireEvent.focus(searchInput);
330
322
 
@@ -1,17 +1,55 @@
1
- .search-box {
1
+ .nav-search-box {
2
2
  display: flex;
3
- height: 50%;
3
+ align-items: center;
4
+ gap: 4px;
5
+ padding: 6px 10px;
6
+ background: var(--planner-bg, #f8fafc);
7
+ border: 1px solid var(--planner-border, #e2e8f0);
8
+ border-radius: 6px;
9
+ height: 32px;
10
+ cursor: text;
11
+ transition: border-color 0.15s;
12
+ position: relative;
13
+ }
14
+
15
+ .nav-search-box:focus-within {
16
+ border-color: var(--accent-primary, #3b82f6);
17
+ }
18
+
19
+ .nav-search-box input {
20
+ flex: 1;
21
+ min-width: 160px;
22
+ border: none;
23
+ background: transparent;
24
+ outline: none;
25
+ box-shadow: none;
26
+ -webkit-appearance: none;
27
+ appearance: none;
28
+ font-size: 12px;
29
+ color: var(--planner-text, #1e293b);
30
+ padding: 2px 0;
31
+ }
32
+
33
+ .nav-search-box input::placeholder {
34
+ color: var(--planner-text-dim, #94a3b8);
4
35
  }
5
36
 
6
37
  .search-results {
7
38
  position: absolute;
8
39
  z-index: 1000;
9
40
  width: 75%;
10
- background-color: rgba(244, 244, 244, 0.8);
11
- border-radius: 1rem;
41
+ background-color: var(--planner-surface, #ffffff);
42
+ border: 1px solid var(--planner-border, #e2e8f0);
43
+ border-radius: 6px;
44
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
12
45
  }
13
46
 
14
47
  .search-result-item {
15
- text-decoration: wavy;
16
48
  padding: 0.5rem;
49
+ color: var(--planner-text, #1e293b);
50
+ font-size: 12px;
51
+ }
52
+
53
+ .search-result-item:hover {
54
+ background-color: var(--planner-surface-hover, #f1f5f9);
17
55
  }
@@ -0,0 +1,28 @@
1
+ const ChartIcon = ({ size = 16, ...props }) => (
2
+ <svg
3
+ width={size}
4
+ height={size}
5
+ viewBox="0 0 24 24"
6
+ fill="currentColor"
7
+ stroke="none"
8
+ {...props}
9
+ >
10
+ {/* Bars */}
11
+ <rect x="4" y="11" width="5" height="8" rx="1" />
12
+ <rect x="10" y="5" width="5" height="14" rx="1" />
13
+ <rect x="16" y="8" width="5" height="11" rx="1" />
14
+ {/* X axis */}
15
+ <line
16
+ x1="2"
17
+ y1="21"
18
+ x2="23"
19
+ y2="21"
20
+ stroke="currentColor"
21
+ strokeWidth="1.5"
22
+ strokeLinecap="round"
23
+ opacity="0.35"
24
+ />
25
+ </svg>
26
+ );
27
+
28
+ export default ChartIcon;
@@ -322,6 +322,56 @@ describe('<NodeDimensionsTab />', () => {
322
322
  consoleSpy.mockRestore();
323
323
  });
324
324
 
325
+ it('renders cube dimension graph via dimensionDag (backend handles cube expansion)', async () => {
326
+ const djNode = {
327
+ name: 'default.repairs_cube',
328
+ type: 'cube',
329
+ parents: [],
330
+ dimension_links: [],
331
+ };
332
+
333
+ // The backend now seeds the BFS from the cube's metric upstreams, so the
334
+ // frontend just calls dimensionDag normally and gets back a populated result.
335
+ mockDjClient.dimensionDag.mockResolvedValue({
336
+ inbound: [
337
+ {
338
+ name: 'default.repair_orders',
339
+ type: 'source',
340
+ display_name: 'Repair Orders',
341
+ },
342
+ ],
343
+ inbound_edges: [
344
+ { source: 'default.repair_orders', target: 'default.hard_hat' },
345
+ ],
346
+ outbound: [
347
+ {
348
+ name: 'default.hard_hat',
349
+ type: 'dimension',
350
+ display_name: 'Hard Hat',
351
+ },
352
+ {
353
+ name: 'default.dispatcher',
354
+ type: 'dimension',
355
+ display_name: 'Dispatcher',
356
+ },
357
+ ],
358
+ outbound_edges: [
359
+ { source: 'default.repair_orders', target: 'default.hard_hat' },
360
+ { source: 'default.repair_orders', target: 'default.dispatcher' },
361
+ ],
362
+ });
363
+
364
+ renderWithContext(djNode);
365
+
366
+ await waitFor(() => {
367
+ expect(screen.getByTestId('reactflow')).toBeInTheDocument();
368
+ });
369
+ expect(mockDjClient.dimensionDag).toHaveBeenCalledWith(
370
+ 'default.repairs_cube',
371
+ );
372
+ expect(screen.getAllByText('dimension').length).toBeGreaterThan(0);
373
+ });
374
+
325
375
  it('renders multi-level inbound chain (non-flat graph)', async () => {
326
376
  const djNode = {
327
377
  name: 'default.us_state',
@@ -17,6 +17,7 @@ import WatchButton from './WatchNodeButton';
17
17
  import NodesWithDimension from './NodesWithDimension';
18
18
  import NodeColumnLineage from './NodeLineageTab';
19
19
  import EditIcon from '../../icons/EditIcon';
20
+ import ChartIcon from '../../icons/ChartIcon';
20
21
  import AlertIcon from '../../icons/AlertIcon';
21
22
  import LoadingIcon from '../../icons/LoadingIcon';
22
23
  import NodeDependenciesTab from './NodeDependenciesTab';
@@ -39,7 +40,8 @@ export function NodePage() {
39
40
  const onClickTab = id => () => {
40
41
  // Preview tab redirects to Query Planner instead of showing content
41
42
  if (id === 'preview') {
42
- navigate(`/planner?metrics=${encodeURIComponent(name)}`);
43
+ const param = node?.type === 'cube' ? 'cube' : 'metrics';
44
+ navigate(`/planner?${param}=${encodeURIComponent(name)}`);
43
45
  return;
44
46
  }
45
47
  navigate(`/nodes/${name}/${id}`);
@@ -52,6 +54,7 @@ export function NodePage() {
52
54
  key={tab.id}
53
55
  id={tab.id}
54
56
  name={tab.name}
57
+ icon={tab.icon}
55
58
  onClick={onClickTab(tab.id)}
56
59
  selectedTab={state.selectedTab}
57
60
  />
@@ -121,7 +124,8 @@ export function NodePage() {
121
124
  {
122
125
  id: 'preview',
123
126
  name: 'Preview →',
124
- display: node?.type === 'metric',
127
+ icon: <ChartIcon size={18} />,
128
+ display: node?.type === 'metric' || node?.type === 'cube',
125
129
  },
126
130
  ];
127
131
  };