datajunction-ui 0.0.30 → 0.0.31

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.30",
3
+ "version": "0.0.31",
4
4
  "description": "DataJunction Metrics Platform UI",
5
5
  "module": "src/index.tsx",
6
6
  "repository": {
@@ -1,10 +1,12 @@
1
- import { useContext, useEffect, useState } from 'react';
2
- import HorizontalHierarchyIcon from '../icons/HorizontalHierarchyIcon';
1
+ import { useContext, useEffect, useState, useRef } from 'react';
3
2
  import DJClientContext from '../providers/djclient';
4
3
 
5
- export default function NamespaceHeader({ namespace }) {
4
+ export default function NamespaceHeader({ namespace, children }) {
6
5
  const djClient = useContext(DJClientContext).DataJunctionAPI;
7
6
  const [sources, setSources] = useState(null);
7
+ const [recentDeployments, setRecentDeployments] = useState([]);
8
+ const [deploymentsDropdownOpen, setDeploymentsDropdownOpen] = useState(false);
9
+ const dropdownRef = useRef(null);
8
10
 
9
11
  useEffect(() => {
10
12
  const fetchSources = async () => {
@@ -12,6 +14,15 @@ export default function NamespaceHeader({ namespace }) {
12
14
  try {
13
15
  const data = await djClient.namespaceSources(namespace);
14
16
  setSources(data);
17
+
18
+ // Fetch recent deployments for this namespace
19
+ try {
20
+ const deployments = await djClient.listDeployments(namespace, 5);
21
+ setRecentDeployments(deployments || []);
22
+ } catch (err) {
23
+ console.error('Failed to fetch deployments:', err);
24
+ setRecentDeployments([]);
25
+ }
15
26
  } catch (e) {
16
27
  // Silently fail - badge just won't show
17
28
  }
@@ -20,71 +31,418 @@ export default function NamespaceHeader({ namespace }) {
20
31
  fetchSources();
21
32
  }, [djClient, namespace]);
22
33
 
34
+ // Close dropdown when clicking outside
35
+ useEffect(() => {
36
+ const handleClickOutside = event => {
37
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
38
+ setDeploymentsDropdownOpen(false);
39
+ }
40
+ };
41
+ document.addEventListener('mousedown', handleClickOutside);
42
+ return () => document.removeEventListener('mousedown', handleClickOutside);
43
+ }, []);
44
+
23
45
  const namespaceParts = namespace ? namespace.split('.') : [];
24
- const namespaceList = namespaceParts.map((piece, index) => {
25
- return (
26
- <li className="breadcrumb-item" key={index}>
27
- <a
28
- className="link-body-emphasis"
29
- href={'/namespaces/' + namespaceParts.slice(0, index + 1).join('.')}
30
- >
31
- {piece}
32
- </a>
33
- </li>
34
- );
35
- });
36
-
37
- // Render source badge
38
- const renderSourceBadge = () => {
39
- if (!sources || sources.total_deployments === 0) {
40
- return null;
41
- }
42
-
43
- const isGit = sources.primary_source?.type === 'git';
44
-
45
- return (
46
- <li
47
- className="breadcrumb-item"
48
- style={{ display: 'flex', alignItems: 'center' }}
49
- >
50
- <span
51
- title={
52
- isGit
53
- ? `CI-managed: ${sources.primary_source.repository}${
54
- sources.primary_source.branch
55
- ? ` (${sources.primary_source.branch})`
56
- : ''
57
- }`
58
- : 'Local/adhoc deployment'
59
- }
60
- style={{
61
- display: 'inline-flex',
62
- alignItems: 'center',
63
- gap: '4px',
64
- padding: '2px 8px',
65
- fontSize: '11px',
66
- borderRadius: '12px',
67
- backgroundColor: isGit ? '#d4edda' : '#e2e3e5',
68
- color: isGit ? '#155724' : '#383d41',
69
- cursor: 'help',
70
- }}
71
- >
72
- {isGit ? '🔗' : '📁'}
73
- {isGit ? 'CI' : 'Local'}
74
- </span>
75
- </li>
76
- );
77
- };
78
46
 
79
47
  return (
80
- <ol className="breadcrumb breadcrumb-chevron p-3 bg-body-tertiary rounded-3">
81
- <li className="breadcrumb-item">
82
- <a href="/">
83
- <HorizontalHierarchyIcon />
48
+ <div
49
+ style={{
50
+ display: 'flex',
51
+ justifyContent: 'space-between',
52
+ alignItems: 'center',
53
+ padding: '12px 12px 12px 20px',
54
+ marginBottom: '16px',
55
+ borderTop: '1px solid #e2e8f0',
56
+ borderBottom: '1px solid #e2e8f0',
57
+ background: '#ffffff',
58
+ }}
59
+ >
60
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
61
+ <a href="/" style={{ display: 'flex', alignItems: 'center' }}>
62
+ <svg
63
+ xmlns="http://www.w3.org/2000/svg"
64
+ width="16"
65
+ height="16"
66
+ fill="currentColor"
67
+ viewBox="0 0 16 16"
68
+ >
69
+ <path d="M8.186 1.113a.5.5 0 0 0-.372 0L1.846 3.5 8 5.961 14.154 3.5 8.186 1.113zM15 4.239l-6.5 2.6v7.922l6.5-2.6V4.24zM7.5 14.762V6.838L1 4.239v7.923l6.5 2.6zM7.443.184a1.5 1.5 0 0 1 1.114 0l7.129 2.852A.5.5 0 0 1 16 3.5v8.662a1 1 0 0 1-.629.928l-7.185 2.874a.5.5 0 0 1-.372 0L.63 13.09a1 1 0 0 1-.63-.928V3.5a.5.5 0 0 1 .314-.464L7.443.184z" />
70
+ </svg>
84
71
  </a>
85
- </li>
86
- {namespaceList}
87
- {renderSourceBadge()}
88
- </ol>
72
+ <svg
73
+ xmlns="http://www.w3.org/2000/svg"
74
+ width="12"
75
+ height="12"
76
+ fill="#6c757d"
77
+ viewBox="0 0 16 16"
78
+ >
79
+ <path
80
+ fillRule="evenodd"
81
+ d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"
82
+ />
83
+ </svg>
84
+ {namespace ? (
85
+ namespaceParts.map((part, index, arr) => (
86
+ <span
87
+ key={index}
88
+ style={{
89
+ display: 'flex',
90
+ alignItems: 'center',
91
+ gap: '8px',
92
+ }}
93
+ >
94
+ <a
95
+ href={`/namespaces/${arr.slice(0, index + 1).join('.')}`}
96
+ style={{
97
+ fontWeight: '400',
98
+ color: '#1e293b',
99
+ textDecoration: 'none',
100
+ }}
101
+ >
102
+ {part}
103
+ </a>
104
+ {index < arr.length - 1 && (
105
+ <svg
106
+ xmlns="http://www.w3.org/2000/svg"
107
+ width="12"
108
+ height="12"
109
+ fill="#94a3b8"
110
+ viewBox="0 0 16 16"
111
+ >
112
+ <path
113
+ fillRule="evenodd"
114
+ d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"
115
+ />
116
+ </svg>
117
+ )}
118
+ </span>
119
+ ))
120
+ ) : (
121
+ <span style={{ fontWeight: '600', color: '#1e293b' }}>
122
+ All Namespaces
123
+ </span>
124
+ )}
125
+
126
+ {/* Deployment badge + dropdown */}
127
+ {sources && sources.total_deployments > 0 && (
128
+ <div
129
+ style={{ position: 'relative', marginLeft: '8px' }}
130
+ ref={dropdownRef}
131
+ >
132
+ <button
133
+ onClick={() =>
134
+ setDeploymentsDropdownOpen(!deploymentsDropdownOpen)
135
+ }
136
+ style={{
137
+ height: '32px',
138
+ padding: '0 12px',
139
+ fontSize: '12px',
140
+ border: 'none',
141
+ borderRadius: '4px',
142
+ backgroundColor: '#ffffff',
143
+ color: '#0b3d91',
144
+ cursor: 'pointer',
145
+ display: 'flex',
146
+ alignItems: 'center',
147
+ gap: '4px',
148
+ whiteSpace: 'nowrap',
149
+ }}
150
+ >
151
+ {sources.primary_source?.type === 'git' ? (
152
+ <>
153
+ <svg
154
+ xmlns="http://www.w3.org/2000/svg"
155
+ width="12"
156
+ height="12"
157
+ viewBox="0 0 24 24"
158
+ fill="none"
159
+ stroke="currentColor"
160
+ strokeWidth="2"
161
+ strokeLinecap="round"
162
+ strokeLinejoin="round"
163
+ >
164
+ <line x1="6" y1="3" x2="6" y2="15"></line>
165
+ <circle cx="18" cy="6" r="3"></circle>
166
+ <circle cx="6" cy="18" r="3"></circle>
167
+ <path d="M18 9a9 9 0 0 1-9 9"></path>
168
+ </svg>
169
+ Git Managed
170
+ </>
171
+ ) : (
172
+ <>
173
+ <svg
174
+ xmlns="http://www.w3.org/2000/svg"
175
+ width="12"
176
+ height="12"
177
+ viewBox="0 0 24 24"
178
+ fill="none"
179
+ stroke="currentColor"
180
+ strokeWidth="2"
181
+ strokeLinecap="round"
182
+ strokeLinejoin="round"
183
+ >
184
+ <circle cx="12" cy="7" r="4" />
185
+ <path d="M5.5 21a6.5 6.5 0 0 1 13 0Z" />
186
+ </svg>
187
+ Local Deploy
188
+ </>
189
+ )}
190
+ <span style={{ fontSize: '8px' }}>
191
+ {deploymentsDropdownOpen ? '▲' : '▼'}
192
+ </span>
193
+ </button>
194
+
195
+ {deploymentsDropdownOpen && (
196
+ <div
197
+ style={{
198
+ position: 'absolute',
199
+ top: '100%',
200
+ left: 0,
201
+ marginTop: '4px',
202
+ padding: '12px',
203
+ backgroundColor: 'white',
204
+ border: '1px solid #ddd',
205
+ borderRadius: '8px',
206
+ boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
207
+ zIndex: 1000,
208
+ minWidth: 'max-content',
209
+ }}
210
+ >
211
+ {sources.primary_source?.type === 'git' ? (
212
+ <a
213
+ href={
214
+ sources.primary_source.repository?.startsWith('http')
215
+ ? sources.primary_source.repository
216
+ : `https://${sources.primary_source.repository}`
217
+ }
218
+ target="_blank"
219
+ rel="noopener noreferrer"
220
+ style={{
221
+ display: 'flex',
222
+ alignItems: 'center',
223
+ gap: '8px',
224
+ fontSize: '13px',
225
+ fontWeight: 400,
226
+ textDecoration: 'none',
227
+ marginBottom: '12px',
228
+ }}
229
+ >
230
+ <svg
231
+ width="16"
232
+ height="16"
233
+ viewBox="0 0 24 24"
234
+ fill="none"
235
+ stroke="currentColor"
236
+ strokeWidth="2"
237
+ strokeLinecap="round"
238
+ strokeLinejoin="round"
239
+ >
240
+ <line x1="6" y1="3" x2="6" y2="15" />
241
+ <circle cx="18" cy="6" r="3" />
242
+ <circle cx="6" cy="18" r="3" />
243
+ <path d="M18 9a9 9 0 0 1-9 9" />
244
+ </svg>
245
+ {sources.primary_source.repository}
246
+ {sources.primary_source.branch &&
247
+ ` (${sources.primary_source.branch})`}
248
+ </a>
249
+ ) : (
250
+ <div
251
+ style={{
252
+ display: 'flex',
253
+ alignItems: 'center',
254
+ gap: '8px',
255
+ fontSize: '13px',
256
+ fontWeight: 600,
257
+ color: '#0b3d91',
258
+ marginBottom: '12px',
259
+ }}
260
+ >
261
+ <svg
262
+ width="16"
263
+ height="16"
264
+ viewBox="0 0 24 24"
265
+ fill="none"
266
+ stroke="currentColor"
267
+ strokeWidth="2"
268
+ strokeLinecap="round"
269
+ strokeLinejoin="round"
270
+ >
271
+ <circle cx="12" cy="7" r="4" />
272
+ <path d="M5.5 21a6.5 6.5 0 0 1 13 0Z" />
273
+ </svg>
274
+ {recentDeployments?.[0]?.created_by
275
+ ? `Local deploys by ${recentDeployments[0].created_by}`
276
+ : 'Local/adhoc deployments'}
277
+ </div>
278
+ )}
279
+
280
+ {/* Separator */}
281
+ <div
282
+ style={{
283
+ height: '1px',
284
+ backgroundColor: '#e2e8f0',
285
+ marginBottom: '8px',
286
+ }}
287
+ />
288
+
289
+ {/* Recent deployments list */}
290
+ {recentDeployments?.length > 0 ? (
291
+ recentDeployments.map((d, idx) => {
292
+ const isGit = d.source?.type === 'git';
293
+ const statusColor =
294
+ d.status === 'success'
295
+ ? '#22c55e'
296
+ : d.status === 'failed'
297
+ ? '#ef4444'
298
+ : '#94a3b8';
299
+
300
+ const commitUrl =
301
+ isGit && d.source?.repository && d.source?.commit_sha
302
+ ? `${
303
+ d.source.repository.startsWith('http')
304
+ ? d.source.repository
305
+ : `https://${d.source.repository}`
306
+ }/commit/${d.source.commit_sha}`
307
+ : null;
308
+
309
+ const detail = isGit
310
+ ? d.source?.branch || 'main'
311
+ : d.source?.reason || d.source?.hostname || 'adhoc';
312
+
313
+ const shortSha = d.source?.commit_sha?.slice(0, 7);
314
+
315
+ return (
316
+ <div
317
+ key={`${d.uuid}-${idx}`}
318
+ style={{
319
+ display: 'grid',
320
+ gridTemplateColumns: '18px 1fr auto',
321
+ alignItems: 'center',
322
+ gap: '8px',
323
+ padding: '6px 0',
324
+ borderBottom:
325
+ idx === recentDeployments.length - 1
326
+ ? 'none'
327
+ : '1px solid #f1f5f9',
328
+ fontSize: '12px',
329
+ }}
330
+ >
331
+ {/* Status dot */}
332
+ <div
333
+ style={{
334
+ width: '8px',
335
+ height: '8px',
336
+ borderRadius: '50%',
337
+ backgroundColor: statusColor,
338
+ }}
339
+ title={d.status}
340
+ />
341
+
342
+ {/* User + detail */}
343
+ <div
344
+ style={{
345
+ display: 'flex',
346
+ alignItems: 'center',
347
+ gap: '6px',
348
+ minWidth: 0,
349
+ }}
350
+ >
351
+ <span
352
+ style={{
353
+ fontWeight: 500,
354
+ color: '#0f172a',
355
+ whiteSpace: 'nowrap',
356
+ }}
357
+ >
358
+ {d.created_by || 'unknown'}
359
+ </span>
360
+ <span style={{ color: '#cbd5e1' }}>—</span>
361
+ {isGit ? (
362
+ <>
363
+ <span
364
+ style={{
365
+ color: '#64748b',
366
+ whiteSpace: 'nowrap',
367
+ }}
368
+ >
369
+ {detail}
370
+ </span>
371
+ {shortSha && (
372
+ <>
373
+ <span style={{ color: '#cbd5e1' }}>@</span>
374
+ {commitUrl ? (
375
+ <a
376
+ href={commitUrl}
377
+ target="_blank"
378
+ rel="noopener noreferrer"
379
+ style={{
380
+ fontFamily: 'monospace',
381
+ fontSize: '11px',
382
+ color: '#3b82f6',
383
+ textDecoration: 'none',
384
+ }}
385
+ >
386
+ {shortSha}
387
+ </a>
388
+ ) : (
389
+ <span
390
+ style={{
391
+ fontFamily: 'monospace',
392
+ fontSize: '11px',
393
+ color: '#64748b',
394
+ }}
395
+ >
396
+ {shortSha}
397
+ </span>
398
+ )}
399
+ </>
400
+ )}
401
+ </>
402
+ ) : (
403
+ <span
404
+ style={{
405
+ color: '#64748b',
406
+ overflow: 'hidden',
407
+ textOverflow: 'ellipsis',
408
+ whiteSpace: 'nowrap',
409
+ }}
410
+ >
411
+ {detail}
412
+ </span>
413
+ )}
414
+ </div>
415
+
416
+ {/* Timestamp */}
417
+ <span
418
+ style={{
419
+ color: '#94a3b8',
420
+ fontSize: '11px',
421
+ whiteSpace: 'nowrap',
422
+ }}
423
+ >
424
+ {new Date(d.created_at).toLocaleDateString()}
425
+ </span>
426
+ </div>
427
+ );
428
+ })
429
+ ) : (
430
+ <div style={{ color: '#94a3b8', fontSize: '12px' }}>
431
+ No recent deployments
432
+ </div>
433
+ )}
434
+ </div>
435
+ )}
436
+ </div>
437
+ )}
438
+ </div>
439
+
440
+ {/* Right side actions passed as children */}
441
+ {children && (
442
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
443
+ {children}
444
+ </div>
445
+ )}
446
+ </div>
89
447
  );
90
448
  }
@@ -25,6 +25,7 @@ describe('<NamespaceHeader />', () => {
25
25
  branch: 'main',
26
26
  },
27
27
  }),
28
+ listDeployments: jest.fn().mockResolvedValue([]),
28
29
  };
29
30
 
30
31
  render(
@@ -41,8 +42,8 @@ describe('<NamespaceHeader />', () => {
41
42
  );
42
43
  });
43
44
 
44
- // Should render CI badge for git source
45
- expect(screen.getByText(/CI/)).toBeInTheDocument();
45
+ // Should render Git Managed badge for git source
46
+ expect(screen.getByText(/Git Managed/)).toBeInTheDocument();
46
47
  });
47
48
 
48
49
  it('should render git source badge when source type is git without branch', async () => {
@@ -55,6 +56,7 @@ describe('<NamespaceHeader />', () => {
55
56
  branch: null,
56
57
  },
57
58
  }),
59
+ listDeployments: jest.fn().mockResolvedValue([]),
58
60
  };
59
61
 
60
62
  render(
@@ -71,8 +73,8 @@ describe('<NamespaceHeader />', () => {
71
73
  );
72
74
  });
73
75
 
74
- // Should render CI badge for git source even without branch
75
- expect(screen.getByText(/CI/)).toBeInTheDocument();
76
+ // Should render Git Managed badge for git source even without branch
77
+ expect(screen.getByText(/Git Managed/)).toBeInTheDocument();
76
78
  });
77
79
 
78
80
  it('should render local source badge when source type is local', async () => {
@@ -84,6 +86,7 @@ describe('<NamespaceHeader />', () => {
84
86
  hostname: 'localhost',
85
87
  },
86
88
  }),
89
+ listDeployments: jest.fn().mockResolvedValue([]),
87
90
  };
88
91
 
89
92
  render(
@@ -100,8 +103,8 @@ describe('<NamespaceHeader />', () => {
100
103
  );
101
104
  });
102
105
 
103
- // Should render Local badge for local source
104
- expect(screen.getByText(/Local/)).toBeInTheDocument();
106
+ // Should render Local Deploy badge for local source
107
+ expect(screen.getByText(/Local Deploy/)).toBeInTheDocument();
105
108
  });
106
109
 
107
110
  it('should not render badge when no deployments', async () => {
@@ -110,6 +113,7 @@ describe('<NamespaceHeader />', () => {
110
113
  total_deployments: 0,
111
114
  primary_source: null,
112
115
  }),
116
+ listDeployments: jest.fn().mockResolvedValue([]),
113
117
  };
114
118
 
115
119
  render(
@@ -127,13 +131,14 @@ describe('<NamespaceHeader />', () => {
127
131
  });
128
132
 
129
133
  // Should not render any source badge
130
- expect(screen.queryByText(/CI/)).not.toBeInTheDocument();
131
- expect(screen.queryByText(/Local/)).not.toBeInTheDocument();
134
+ expect(screen.queryByText(/Git Managed/)).not.toBeInTheDocument();
135
+ expect(screen.queryByText(/Local Deploy/)).not.toBeInTheDocument();
132
136
  });
133
137
 
134
138
  it('should handle API error gracefully', async () => {
135
139
  const mockDjClient = {
136
140
  namespaceSources: jest.fn().mockRejectedValue(new Error('API Error')),
141
+ listDeployments: jest.fn().mockResolvedValue([]),
137
142
  };
138
143
 
139
144
  render(
@@ -153,6 +158,6 @@ describe('<NamespaceHeader />', () => {
153
158
  // Should still render breadcrumb without badge
154
159
  expect(screen.getByText('test')).toBeInTheDocument();
155
160
  expect(screen.getByText('namespace')).toBeInTheDocument();
156
- expect(screen.queryByText(/CI/)).not.toBeInTheDocument();
161
+ expect(screen.queryByText(/Git Managed/)).not.toBeInTheDocument();
157
162
  });
158
163
  });