datajunction-ui 0.0.29 → 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 +1 -1
- package/src/__tests__/reportWebVitals.test.ts +27 -0
- package/src/app/components/NamespaceHeader.jsx +423 -76
- package/src/app/components/__tests__/NamespaceHeader.test.jsx +24 -40
- package/src/app/components/__tests__/NotificationBell.test.tsx +23 -0
- package/src/app/components/__tests__/__snapshots__/NamespaceHeader.test.jsx.snap +139 -34
- package/src/app/components/djgraph/__tests__/DJNode.test.tsx +36 -0
- package/src/app/icons/__tests__/Icons.test.jsx +24 -0
- package/src/app/pages/NamespacePage/index.jsx +44 -203
- package/src/app/pages/OverviewPage/OverviewPanel.jsx +1 -3
- package/src/app/providers/UserProvider.tsx +1 -5
- package/src/app/services/DJService.js +13 -0
package/package.json
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import reportWebVitals from '../reportWebVitals';
|
|
2
|
+
|
|
3
|
+
describe('reportWebVitals', () => {
|
|
4
|
+
it('calls web vitals functions when handler is provided', async () => {
|
|
5
|
+
const mockHandler = jest.fn();
|
|
6
|
+
|
|
7
|
+
// Call reportWebVitals with a handler
|
|
8
|
+
reportWebVitals(mockHandler);
|
|
9
|
+
|
|
10
|
+
// Wait for dynamic import to resolve
|
|
11
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
12
|
+
|
|
13
|
+
// The handler should have been called by web vitals
|
|
14
|
+
// (we just verify it doesn't throw)
|
|
15
|
+
expect(mockHandler).toBeDefined();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('does nothing when no handler is provided', () => {
|
|
19
|
+
// Should not throw
|
|
20
|
+
expect(() => reportWebVitals()).not.toThrow();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('does nothing when handler is not a function', () => {
|
|
24
|
+
// Should not throw
|
|
25
|
+
expect(() => reportWebVitals(undefined)).not.toThrow();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -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,82 +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
|
-
const hasMultiple = sources.has_multiple_sources;
|
|
45
|
-
|
|
46
|
-
return (
|
|
47
|
-
<li
|
|
48
|
-
className="breadcrumb-item"
|
|
49
|
-
style={{ display: 'flex', alignItems: 'center' }}
|
|
50
|
-
>
|
|
51
|
-
<span
|
|
52
|
-
title={
|
|
53
|
-
hasMultiple
|
|
54
|
-
? `Warning: ${sources.sources.length} deployment sources`
|
|
55
|
-
: isGit
|
|
56
|
-
? `CI-managed: ${sources.primary_source.repository}${
|
|
57
|
-
sources.primary_source.branch
|
|
58
|
-
? ` (${sources.primary_source.branch})`
|
|
59
|
-
: ''
|
|
60
|
-
}`
|
|
61
|
-
: 'Local/adhoc deployment'
|
|
62
|
-
}
|
|
63
|
-
style={{
|
|
64
|
-
display: 'inline-flex',
|
|
65
|
-
alignItems: 'center',
|
|
66
|
-
gap: '4px',
|
|
67
|
-
padding: '2px 8px',
|
|
68
|
-
fontSize: '11px',
|
|
69
|
-
borderRadius: '12px',
|
|
70
|
-
backgroundColor: hasMultiple
|
|
71
|
-
? '#fff3cd'
|
|
72
|
-
: isGit
|
|
73
|
-
? '#d4edda'
|
|
74
|
-
: '#e2e3e5',
|
|
75
|
-
color: hasMultiple ? '#856404' : isGit ? '#155724' : '#383d41',
|
|
76
|
-
cursor: 'help',
|
|
77
|
-
}}
|
|
78
|
-
>
|
|
79
|
-
{hasMultiple ? '⚠️' : isGit ? '🔗' : '📁'}
|
|
80
|
-
{hasMultiple
|
|
81
|
-
? `${sources.sources.length} sources`
|
|
82
|
-
: isGit
|
|
83
|
-
? 'CI'
|
|
84
|
-
: 'Local'}
|
|
85
|
-
</span>
|
|
86
|
-
</li>
|
|
87
|
-
);
|
|
88
|
-
};
|
|
89
46
|
|
|
90
47
|
return (
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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>
|
|
95
71
|
</a>
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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>
|
|
100
447
|
);
|
|
101
448
|
}
|
|
@@ -15,24 +15,17 @@ describe('<NamespaceHeader />', () => {
|
|
|
15
15
|
expect(renderedOutput).toMatchSnapshot();
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
it('should render git source badge when source type is git', async () => {
|
|
18
|
+
it('should render git source badge when source type is git with branch', async () => {
|
|
19
19
|
const mockDjClient = {
|
|
20
20
|
namespaceSources: jest.fn().mockResolvedValue({
|
|
21
21
|
total_deployments: 5,
|
|
22
|
-
has_multiple_sources: false,
|
|
23
22
|
primary_source: {
|
|
24
23
|
type: 'git',
|
|
25
24
|
repository: 'github.com/test/repo',
|
|
26
25
|
branch: 'main',
|
|
27
26
|
},
|
|
28
|
-
sources: [
|
|
29
|
-
{
|
|
30
|
-
type: 'git',
|
|
31
|
-
repository: 'github.com/test/repo',
|
|
32
|
-
branch: 'main',
|
|
33
|
-
},
|
|
34
|
-
],
|
|
35
27
|
}),
|
|
28
|
+
listDeployments: jest.fn().mockResolvedValue([]),
|
|
36
29
|
};
|
|
37
30
|
|
|
38
31
|
render(
|
|
@@ -49,26 +42,21 @@ describe('<NamespaceHeader />', () => {
|
|
|
49
42
|
);
|
|
50
43
|
});
|
|
51
44
|
|
|
52
|
-
// Should render
|
|
53
|
-
expect(screen.getByText(/
|
|
45
|
+
// Should render Git Managed badge for git source
|
|
46
|
+
expect(screen.getByText(/Git Managed/)).toBeInTheDocument();
|
|
54
47
|
});
|
|
55
48
|
|
|
56
|
-
it('should render
|
|
49
|
+
it('should render git source badge when source type is git without branch', async () => {
|
|
57
50
|
const mockDjClient = {
|
|
58
51
|
namespaceSources: jest.fn().mockResolvedValue({
|
|
59
|
-
total_deployments:
|
|
60
|
-
has_multiple_sources: false,
|
|
52
|
+
total_deployments: 3,
|
|
61
53
|
primary_source: {
|
|
62
|
-
type: '
|
|
63
|
-
|
|
54
|
+
type: 'git',
|
|
55
|
+
repository: 'github.com/test/repo',
|
|
56
|
+
branch: null,
|
|
64
57
|
},
|
|
65
|
-
sources: [
|
|
66
|
-
{
|
|
67
|
-
type: 'local',
|
|
68
|
-
hostname: 'localhost',
|
|
69
|
-
},
|
|
70
|
-
],
|
|
71
58
|
}),
|
|
59
|
+
listDeployments: jest.fn().mockResolvedValue([]),
|
|
72
60
|
};
|
|
73
61
|
|
|
74
62
|
render(
|
|
@@ -85,24 +73,20 @@ describe('<NamespaceHeader />', () => {
|
|
|
85
73
|
);
|
|
86
74
|
});
|
|
87
75
|
|
|
88
|
-
// Should render
|
|
89
|
-
expect(screen.getByText(/
|
|
76
|
+
// Should render Git Managed badge for git source even without branch
|
|
77
|
+
expect(screen.getByText(/Git Managed/)).toBeInTheDocument();
|
|
90
78
|
});
|
|
91
79
|
|
|
92
|
-
it('should render
|
|
80
|
+
it('should render local source badge when source type is local', async () => {
|
|
93
81
|
const mockDjClient = {
|
|
94
82
|
namespaceSources: jest.fn().mockResolvedValue({
|
|
95
|
-
total_deployments:
|
|
96
|
-
has_multiple_sources: true,
|
|
83
|
+
total_deployments: 2,
|
|
97
84
|
primary_source: {
|
|
98
|
-
type: '
|
|
99
|
-
|
|
85
|
+
type: 'local',
|
|
86
|
+
hostname: 'localhost',
|
|
100
87
|
},
|
|
101
|
-
sources: [
|
|
102
|
-
{ type: 'git', repository: 'github.com/test/repo' },
|
|
103
|
-
{ type: 'local', hostname: 'localhost' },
|
|
104
|
-
],
|
|
105
88
|
}),
|
|
89
|
+
listDeployments: jest.fn().mockResolvedValue([]),
|
|
106
90
|
};
|
|
107
91
|
|
|
108
92
|
render(
|
|
@@ -119,18 +103,17 @@ describe('<NamespaceHeader />', () => {
|
|
|
119
103
|
);
|
|
120
104
|
});
|
|
121
105
|
|
|
122
|
-
// Should render
|
|
123
|
-
expect(screen.getByText(/
|
|
106
|
+
// Should render Local Deploy badge for local source
|
|
107
|
+
expect(screen.getByText(/Local Deploy/)).toBeInTheDocument();
|
|
124
108
|
});
|
|
125
109
|
|
|
126
110
|
it('should not render badge when no deployments', async () => {
|
|
127
111
|
const mockDjClient = {
|
|
128
112
|
namespaceSources: jest.fn().mockResolvedValue({
|
|
129
113
|
total_deployments: 0,
|
|
130
|
-
has_multiple_sources: false,
|
|
131
114
|
primary_source: null,
|
|
132
|
-
sources: [],
|
|
133
115
|
}),
|
|
116
|
+
listDeployments: jest.fn().mockResolvedValue([]),
|
|
134
117
|
};
|
|
135
118
|
|
|
136
119
|
render(
|
|
@@ -148,13 +131,14 @@ describe('<NamespaceHeader />', () => {
|
|
|
148
131
|
});
|
|
149
132
|
|
|
150
133
|
// Should not render any source badge
|
|
151
|
-
expect(screen.queryByText(/
|
|
152
|
-
expect(screen.queryByText(/Local/)).not.toBeInTheDocument();
|
|
134
|
+
expect(screen.queryByText(/Git Managed/)).not.toBeInTheDocument();
|
|
135
|
+
expect(screen.queryByText(/Local Deploy/)).not.toBeInTheDocument();
|
|
153
136
|
});
|
|
154
137
|
|
|
155
138
|
it('should handle API error gracefully', async () => {
|
|
156
139
|
const mockDjClient = {
|
|
157
140
|
namespaceSources: jest.fn().mockRejectedValue(new Error('API Error')),
|
|
141
|
+
listDeployments: jest.fn().mockResolvedValue([]),
|
|
158
142
|
};
|
|
159
143
|
|
|
160
144
|
render(
|
|
@@ -174,6 +158,6 @@ describe('<NamespaceHeader />', () => {
|
|
|
174
158
|
// Should still render breadcrumb without badge
|
|
175
159
|
expect(screen.getByText('test')).toBeInTheDocument();
|
|
176
160
|
expect(screen.getByText('namespace')).toBeInTheDocument();
|
|
177
|
-
expect(screen.queryByText(/
|
|
161
|
+
expect(screen.queryByText(/Git Managed/)).not.toBeInTheDocument();
|
|
178
162
|
});
|
|
179
163
|
});
|
|
@@ -310,4 +310,27 @@ describe('<NotificationBell />', () => {
|
|
|
310
310
|
// onDropdownToggle should have been called with false
|
|
311
311
|
expect(onDropdownToggle).toHaveBeenCalledWith(false);
|
|
312
312
|
});
|
|
313
|
+
|
|
314
|
+
it('handles error when fetching notifications fails', async () => {
|
|
315
|
+
const consoleErrorSpy = jest
|
|
316
|
+
.spyOn(console, 'error')
|
|
317
|
+
.mockImplementation(() => {});
|
|
318
|
+
|
|
319
|
+
const mockDjClient = createMockDjClient({
|
|
320
|
+
getSubscribedHistory: jest
|
|
321
|
+
.fn()
|
|
322
|
+
.mockRejectedValue(new Error('Network error')),
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
renderWithContext(mockDjClient);
|
|
326
|
+
|
|
327
|
+
await waitFor(() => {
|
|
328
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
329
|
+
'Error fetching notifications:',
|
|
330
|
+
expect.any(Error),
|
|
331
|
+
);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
consoleErrorSpy.mockRestore();
|
|
335
|
+
});
|
|
313
336
|
});
|
|
@@ -1,47 +1,152 @@
|
|
|
1
1
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
2
|
|
|
3
3
|
exports[`<NamespaceHeader /> should render and match the snapshot 1`] = `
|
|
4
|
-
<
|
|
5
|
-
|
|
4
|
+
<div
|
|
5
|
+
style={
|
|
6
|
+
Object {
|
|
7
|
+
"alignItems": "center",
|
|
8
|
+
"background": "#ffffff",
|
|
9
|
+
"borderBottom": "1px solid #e2e8f0",
|
|
10
|
+
"borderTop": "1px solid #e2e8f0",
|
|
11
|
+
"display": "flex",
|
|
12
|
+
"justifyContent": "space-between",
|
|
13
|
+
"marginBottom": "16px",
|
|
14
|
+
"padding": "12px 12px 12px 20px",
|
|
15
|
+
}
|
|
16
|
+
}
|
|
6
17
|
>
|
|
7
|
-
<
|
|
8
|
-
|
|
18
|
+
<div
|
|
19
|
+
style={
|
|
20
|
+
Object {
|
|
21
|
+
"alignItems": "center",
|
|
22
|
+
"display": "flex",
|
|
23
|
+
"gap": "8px",
|
|
24
|
+
}
|
|
25
|
+
}
|
|
9
26
|
>
|
|
10
27
|
<a
|
|
11
28
|
href="/"
|
|
29
|
+
style={
|
|
30
|
+
Object {
|
|
31
|
+
"alignItems": "center",
|
|
32
|
+
"display": "flex",
|
|
33
|
+
}
|
|
34
|
+
}
|
|
12
35
|
>
|
|
13
|
-
<
|
|
36
|
+
<svg
|
|
37
|
+
fill="currentColor"
|
|
38
|
+
height="16"
|
|
39
|
+
viewBox="0 0 16 16"
|
|
40
|
+
width="16"
|
|
41
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
42
|
+
>
|
|
43
|
+
<path
|
|
44
|
+
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"
|
|
45
|
+
/>
|
|
46
|
+
</svg>
|
|
14
47
|
</a>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
href="/namespaces/shared"
|
|
48
|
+
<svg
|
|
49
|
+
fill="#6c757d"
|
|
50
|
+
height="12"
|
|
51
|
+
viewBox="0 0 16 16"
|
|
52
|
+
width="12"
|
|
53
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
22
54
|
>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
55
|
+
<path
|
|
56
|
+
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"
|
|
57
|
+
fillRule="evenodd"
|
|
58
|
+
/>
|
|
59
|
+
</svg>
|
|
60
|
+
<span
|
|
61
|
+
style={
|
|
62
|
+
Object {
|
|
63
|
+
"alignItems": "center",
|
|
64
|
+
"display": "flex",
|
|
65
|
+
"gap": "8px",
|
|
66
|
+
}
|
|
67
|
+
}
|
|
32
68
|
>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
69
|
+
<a
|
|
70
|
+
href="/namespaces/shared"
|
|
71
|
+
style={
|
|
72
|
+
Object {
|
|
73
|
+
"color": "#1e293b",
|
|
74
|
+
"fontWeight": "400",
|
|
75
|
+
"textDecoration": "none",
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
>
|
|
79
|
+
shared
|
|
80
|
+
</a>
|
|
81
|
+
<svg
|
|
82
|
+
fill="#94a3b8"
|
|
83
|
+
height="12"
|
|
84
|
+
viewBox="0 0 16 16"
|
|
85
|
+
width="12"
|
|
86
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
87
|
+
>
|
|
88
|
+
<path
|
|
89
|
+
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"
|
|
90
|
+
fillRule="evenodd"
|
|
91
|
+
/>
|
|
92
|
+
</svg>
|
|
93
|
+
</span>
|
|
94
|
+
<span
|
|
95
|
+
style={
|
|
96
|
+
Object {
|
|
97
|
+
"alignItems": "center",
|
|
98
|
+
"display": "flex",
|
|
99
|
+
"gap": "8px",
|
|
100
|
+
}
|
|
101
|
+
}
|
|
42
102
|
>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
103
|
+
<a
|
|
104
|
+
href="/namespaces/shared.dimensions"
|
|
105
|
+
style={
|
|
106
|
+
Object {
|
|
107
|
+
"color": "#1e293b",
|
|
108
|
+
"fontWeight": "400",
|
|
109
|
+
"textDecoration": "none",
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
>
|
|
113
|
+
dimensions
|
|
114
|
+
</a>
|
|
115
|
+
<svg
|
|
116
|
+
fill="#94a3b8"
|
|
117
|
+
height="12"
|
|
118
|
+
viewBox="0 0 16 16"
|
|
119
|
+
width="12"
|
|
120
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
121
|
+
>
|
|
122
|
+
<path
|
|
123
|
+
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"
|
|
124
|
+
fillRule="evenodd"
|
|
125
|
+
/>
|
|
126
|
+
</svg>
|
|
127
|
+
</span>
|
|
128
|
+
<span
|
|
129
|
+
style={
|
|
130
|
+
Object {
|
|
131
|
+
"alignItems": "center",
|
|
132
|
+
"display": "flex",
|
|
133
|
+
"gap": "8px",
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
>
|
|
137
|
+
<a
|
|
138
|
+
href="/namespaces/shared.dimensions.accounts"
|
|
139
|
+
style={
|
|
140
|
+
Object {
|
|
141
|
+
"color": "#1e293b",
|
|
142
|
+
"fontWeight": "400",
|
|
143
|
+
"textDecoration": "none",
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
>
|
|
147
|
+
accounts
|
|
148
|
+
</a>
|
|
149
|
+
</span>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
47
152
|
`;
|
|
@@ -21,4 +21,40 @@ describe('<DJNode />', () => {
|
|
|
21
21
|
const renderedOutput = renderer.getRenderOutput();
|
|
22
22
|
expect(renderedOutput).toMatchSnapshot();
|
|
23
23
|
});
|
|
24
|
+
|
|
25
|
+
it('should render with is_current true and non-metric type (collapsed=false)', () => {
|
|
26
|
+
renderer.render(
|
|
27
|
+
<DJNode
|
|
28
|
+
id="2"
|
|
29
|
+
data={{
|
|
30
|
+
name: 'shared.dimensions.accounts',
|
|
31
|
+
column_names: ['a', 'b'],
|
|
32
|
+
type: 'dimension',
|
|
33
|
+
primary_key: ['id'],
|
|
34
|
+
is_current: true,
|
|
35
|
+
display_name: 'Accounts',
|
|
36
|
+
}}
|
|
37
|
+
/>,
|
|
38
|
+
);
|
|
39
|
+
const renderedOutput = renderer.getRenderOutput();
|
|
40
|
+
expect(renderedOutput).toBeTruthy();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should render with metric type (collapsed=true)', () => {
|
|
44
|
+
renderer.render(
|
|
45
|
+
<DJNode
|
|
46
|
+
id="3"
|
|
47
|
+
data={{
|
|
48
|
+
name: 'default.revenue',
|
|
49
|
+
column_names: [],
|
|
50
|
+
type: 'metric',
|
|
51
|
+
primary_key: [],
|
|
52
|
+
is_current: true,
|
|
53
|
+
display_name: 'Revenue',
|
|
54
|
+
}}
|
|
55
|
+
/>,
|
|
56
|
+
);
|
|
57
|
+
const renderedOutput = renderer.getRenderOutput();
|
|
58
|
+
expect(renderedOutput).toBeTruthy();
|
|
59
|
+
});
|
|
24
60
|
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
|
|
4
|
+
import CommitIcon from '../CommitIcon';
|
|
5
|
+
import InvalidIcon from '../InvalidIcon';
|
|
6
|
+
|
|
7
|
+
describe('Icon components', () => {
|
|
8
|
+
it('should render CommitIcon with default props', () => {
|
|
9
|
+
render(<CommitIcon />);
|
|
10
|
+
expect(document.querySelector('svg')).toBeInTheDocument();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should render InvalidIcon with default props', () => {
|
|
14
|
+
render(<InvalidIcon />);
|
|
15
|
+
expect(screen.getByTestId('invalid-icon')).toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should render InvalidIcon with custom props', () => {
|
|
19
|
+
render(<InvalidIcon width="50px" height="50px" style={{ color: 'red' }} />);
|
|
20
|
+
const icon = screen.getByTestId('invalid-icon');
|
|
21
|
+
expect(icon).toHaveAttribute('width', '50px');
|
|
22
|
+
expect(icon).toHaveAttribute('height', '50px');
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -7,6 +7,7 @@ import { useCurrentUser } from '../../providers/UserProvider';
|
|
|
7
7
|
import Explorer from '../NamespacePage/Explorer';
|
|
8
8
|
import AddNodeDropdown from '../../components/AddNodeDropdown';
|
|
9
9
|
import NodeListActions from '../../components/NodeListActions';
|
|
10
|
+
import NamespaceHeader from '../../components/NamespaceHeader';
|
|
10
11
|
import LoadingIcon from '../../icons/LoadingIcon';
|
|
11
12
|
import CompactSelect from './CompactSelect';
|
|
12
13
|
import { getDJUrl } from '../../services/DJService';
|
|
@@ -168,7 +169,6 @@ export function NamespacePage() {
|
|
|
168
169
|
|
|
169
170
|
const [namespaceHierarchy, setNamespaceHierarchy] = useState([]);
|
|
170
171
|
const [namespaceSources, setNamespaceSources] = useState({});
|
|
171
|
-
const [currentNamespaceSources, setCurrentNamespaceSources] = useState(null);
|
|
172
172
|
|
|
173
173
|
const [sortConfig, setSortConfig] = useState({
|
|
174
174
|
key: 'updatedAt',
|
|
@@ -248,17 +248,6 @@ export function NamespacePage() {
|
|
|
248
248
|
fetchData().catch(console.error);
|
|
249
249
|
}, [djClient, djClient.namespaces]);
|
|
250
250
|
|
|
251
|
-
// Fetch sources for the current namespace (for the header badge)
|
|
252
|
-
useEffect(() => {
|
|
253
|
-
const fetchCurrentSources = async () => {
|
|
254
|
-
if (namespace) {
|
|
255
|
-
const sources = await djClient.namespaceSources(namespace);
|
|
256
|
-
setCurrentNamespaceSources(sources);
|
|
257
|
-
}
|
|
258
|
-
};
|
|
259
|
-
fetchCurrentSources().catch(console.error);
|
|
260
|
-
}, [djClient, namespace]);
|
|
261
|
-
|
|
262
251
|
useEffect(() => {
|
|
263
252
|
const fetchData = async () => {
|
|
264
253
|
setRetrieved(false);
|
|
@@ -815,199 +804,51 @@ export function NamespacePage() {
|
|
|
815
804
|
: null}
|
|
816
805
|
</div>
|
|
817
806
|
<div style={{ flex: 1, minWidth: 0, marginLeft: '1.5rem' }}>
|
|
818
|
-
{
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
namespace.split('.').map((part, index, arr) => (
|
|
846
|
-
<span
|
|
847
|
-
key={index}
|
|
848
|
-
style={{
|
|
849
|
-
display: 'flex',
|
|
850
|
-
alignItems: 'center',
|
|
851
|
-
gap: '8px',
|
|
852
|
-
}}
|
|
853
|
-
>
|
|
854
|
-
<a
|
|
855
|
-
href={`/namespaces/${arr
|
|
856
|
-
.slice(0, index + 1)
|
|
857
|
-
.join('.')}`}
|
|
858
|
-
style={{
|
|
859
|
-
fontWeight: '400',
|
|
860
|
-
color: '#1e293b',
|
|
861
|
-
textDecoration: 'none',
|
|
862
|
-
}}
|
|
863
|
-
>
|
|
864
|
-
{part}
|
|
865
|
-
</a>
|
|
866
|
-
{index < arr.length - 1 && (
|
|
867
|
-
<span style={{ color: '#94a3b8', fontWeight: '400' }}>
|
|
868
|
-
/
|
|
869
|
-
</span>
|
|
870
|
-
)}
|
|
871
|
-
</span>
|
|
872
|
-
))
|
|
873
|
-
) : (
|
|
874
|
-
<span style={{ fontWeight: '600', color: '#1e293b' }}>
|
|
875
|
-
All Namespaces
|
|
876
|
-
</span>
|
|
877
|
-
)}
|
|
878
|
-
{currentNamespaceSources &&
|
|
879
|
-
currentNamespaceSources.total_deployments > 0 &&
|
|
880
|
-
(currentNamespaceSources.primary_source?.type === 'git' ? (
|
|
881
|
-
<a
|
|
882
|
-
href={`https://${currentNamespaceSources.primary_source.repository}`}
|
|
883
|
-
target="_blank"
|
|
884
|
-
rel="noopener noreferrer"
|
|
885
|
-
title={`${
|
|
886
|
-
currentNamespaceSources.primary_source.repository
|
|
887
|
-
}${
|
|
888
|
-
currentNamespaceSources.primary_source.branch
|
|
889
|
-
? ` (${currentNamespaceSources.primary_source.branch})`
|
|
890
|
-
: ''
|
|
891
|
-
}`}
|
|
892
|
-
style={{
|
|
893
|
-
display: 'inline-flex',
|
|
894
|
-
alignItems: 'center',
|
|
895
|
-
gap: '4px',
|
|
896
|
-
padding: '4px 8px',
|
|
897
|
-
marginLeft: '8px',
|
|
898
|
-
fontSize: '13px',
|
|
899
|
-
borderRadius: '12px',
|
|
900
|
-
backgroundColor: '#fff4de',
|
|
901
|
-
border: '1px solid #d4edda',
|
|
902
|
-
color: '#155724',
|
|
903
|
-
textDecoration: 'none',
|
|
904
|
-
cursor: 'pointer',
|
|
905
|
-
}}
|
|
906
|
-
>
|
|
907
|
-
<svg
|
|
908
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
909
|
-
width="12"
|
|
910
|
-
height="12"
|
|
911
|
-
viewBox="0 0 24 24"
|
|
912
|
-
fill="none"
|
|
913
|
-
stroke="currentColor"
|
|
914
|
-
strokeWidth="2"
|
|
915
|
-
strokeLinecap="round"
|
|
916
|
-
strokeLinejoin="round"
|
|
917
|
-
style={{ marginRight: '2px' }}
|
|
918
|
-
>
|
|
919
|
-
<line x1="6" y1="3" x2="6" y2="15"></line>
|
|
920
|
-
<circle cx="18" cy="6" r="3"></circle>
|
|
921
|
-
<circle cx="6" cy="18" r="3"></circle>
|
|
922
|
-
<path d="M18 9a9 9 0 0 1-9 9"></path>
|
|
923
|
-
</svg>
|
|
924
|
-
Git Managed
|
|
925
|
-
</a>
|
|
926
|
-
) : (
|
|
927
|
-
<span
|
|
928
|
-
title={
|
|
929
|
-
currentNamespaceSources.has_multiple_sources
|
|
930
|
-
? `Warning: ${currentNamespaceSources.sources.length} deployment sources`
|
|
931
|
-
: currentNamespaceSources.sources?.[0]
|
|
932
|
-
?.last_deployed_by
|
|
933
|
-
? `Last deployed by ${currentNamespaceSources.sources[0].last_deployed_by}`
|
|
934
|
-
: 'Local/adhoc deployment'
|
|
935
|
-
}
|
|
936
|
-
style={{
|
|
937
|
-
display: 'inline-flex',
|
|
938
|
-
alignItems: 'center',
|
|
939
|
-
gap: '4px',
|
|
940
|
-
padding: '2px 8px',
|
|
941
|
-
marginLeft: '8px',
|
|
942
|
-
fontSize: '11px',
|
|
943
|
-
borderRadius: '12px',
|
|
944
|
-
backgroundColor:
|
|
945
|
-
currentNamespaceSources.has_multiple_sources
|
|
946
|
-
? '#fff3cd'
|
|
947
|
-
: '#e2e3e5',
|
|
948
|
-
color: currentNamespaceSources.has_multiple_sources
|
|
949
|
-
? '#856404'
|
|
950
|
-
: '#383d41',
|
|
951
|
-
cursor: 'help',
|
|
952
|
-
}}
|
|
953
|
-
>
|
|
954
|
-
{currentNamespaceSources.has_multiple_sources
|
|
955
|
-
? `⚠️ ${currentNamespaceSources.sources.length} sources`
|
|
956
|
-
: currentNamespaceSources.sources?.[0]
|
|
957
|
-
?.last_deployed_by
|
|
958
|
-
? `Local deploy by ${currentNamespaceSources.sources[0].last_deployed_by}`
|
|
959
|
-
: 'Local'}
|
|
960
|
-
</span>
|
|
961
|
-
))}
|
|
962
|
-
</div>
|
|
963
|
-
<div
|
|
964
|
-
style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
|
|
807
|
+
<NamespaceHeader namespace={namespace}>
|
|
808
|
+
<a
|
|
809
|
+
href={`${getDJUrl()}/namespaces/${namespace}/export/yaml`}
|
|
810
|
+
download
|
|
811
|
+
style={{
|
|
812
|
+
display: 'inline-flex',
|
|
813
|
+
alignItems: 'center',
|
|
814
|
+
gap: '4px',
|
|
815
|
+
// padding: '6px 12px',
|
|
816
|
+
fontSize: '13px',
|
|
817
|
+
fontWeight: '500',
|
|
818
|
+
color: '#475569',
|
|
819
|
+
// backgroundColor: '#f8fafc',
|
|
820
|
+
// border: '1px solid #e2e8f0',
|
|
821
|
+
borderRadius: '6px',
|
|
822
|
+
textDecoration: 'none',
|
|
823
|
+
cursor: 'pointer',
|
|
824
|
+
transition: 'all 0.15s ease',
|
|
825
|
+
margin: '0.5em 0px 0px 1em',
|
|
826
|
+
}}
|
|
827
|
+
onMouseOver={e => {
|
|
828
|
+
e.currentTarget.style.color = '#333333';
|
|
829
|
+
}}
|
|
830
|
+
onMouseOut={e => {
|
|
831
|
+
e.currentTarget.style.color = '#475569';
|
|
832
|
+
}}
|
|
833
|
+
title="Export namespace to YAML"
|
|
965
834
|
>
|
|
966
|
-
<
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
fontWeight: '500',
|
|
976
|
-
color: '#475569',
|
|
977
|
-
// backgroundColor: '#f8fafc',
|
|
978
|
-
// border: '1px solid #e2e8f0',
|
|
979
|
-
borderRadius: '6px',
|
|
980
|
-
textDecoration: 'none',
|
|
981
|
-
cursor: 'pointer',
|
|
982
|
-
transition: 'all 0.15s ease',
|
|
983
|
-
margin: '0.5em 0px 0px 1em',
|
|
984
|
-
}}
|
|
985
|
-
onMouseOver={e => {
|
|
986
|
-
e.currentTarget.style.color = '#333333';
|
|
987
|
-
}}
|
|
988
|
-
onMouseOut={e => {
|
|
989
|
-
e.currentTarget.style.color = '#475569';
|
|
990
|
-
}}
|
|
991
|
-
title="Export namespace to YAML"
|
|
835
|
+
<svg
|
|
836
|
+
width="14"
|
|
837
|
+
height="14"
|
|
838
|
+
viewBox="0 0 24 24"
|
|
839
|
+
fill="none"
|
|
840
|
+
stroke="currentColor"
|
|
841
|
+
strokeWidth="2"
|
|
842
|
+
strokeLinecap="round"
|
|
843
|
+
strokeLinejoin="round"
|
|
992
844
|
>
|
|
993
|
-
<
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
strokeLinecap="round"
|
|
1001
|
-
strokeLinejoin="round"
|
|
1002
|
-
>
|
|
1003
|
-
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
|
1004
|
-
<polyline points="7 10 12 15 17 10"></polyline>
|
|
1005
|
-
<line x1="12" y1="15" x2="12" y2="3"></line>
|
|
1006
|
-
</svg>
|
|
1007
|
-
</a>
|
|
1008
|
-
<AddNodeDropdown namespace={namespace} />
|
|
1009
|
-
</div>
|
|
1010
|
-
</div>
|
|
845
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
|
846
|
+
<polyline points="7 10 12 15 17 10"></polyline>
|
|
847
|
+
<line x1="12" y1="15" x2="12" y2="3"></line>
|
|
848
|
+
</svg>
|
|
849
|
+
</a>
|
|
850
|
+
<AddNodeDropdown namespace={namespace} />
|
|
851
|
+
</NamespaceHeader>
|
|
1011
852
|
<table className="card-table table" style={{ marginBottom: 0 }}>
|
|
1012
853
|
<thead>
|
|
1013
854
|
<tr>
|
|
@@ -37,9 +37,7 @@ export const OverviewPanel = () => {
|
|
|
37
37
|
<NodeIcon color="#FFBB28" style={{ marginTop: '0.75em' }} />
|
|
38
38
|
<div style={{ display: 'inline-grid', alignItems: 'center' }}>
|
|
39
39
|
<strong className="horiz-box-value">{entry.value}</strong>
|
|
40
|
-
<span className={'horiz-box-label'}>
|
|
41
|
-
{entry.name === 'true' ? 'Active Nodes' : 'Deactivated'}
|
|
42
|
-
</span>
|
|
40
|
+
<span className={'horiz-box-label'}>Active Nodes</span>
|
|
43
41
|
</div>
|
|
44
42
|
</div>
|
|
45
43
|
))}
|
|
@@ -68,11 +68,7 @@ export function UserProvider({ children }: { children: React.ReactNode }) {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
export function useCurrentUser() {
|
|
71
|
-
|
|
72
|
-
if (context === undefined) {
|
|
73
|
-
throw new Error('useCurrentUser must be used within a UserProvider');
|
|
74
|
-
}
|
|
75
|
-
return context;
|
|
71
|
+
return useContext(UserContext);
|
|
76
72
|
}
|
|
77
73
|
|
|
78
74
|
export default UserContext;
|
|
@@ -1038,6 +1038,19 @@ export const DataJunctionAPI = {
|
|
|
1038
1038
|
).json();
|
|
1039
1039
|
},
|
|
1040
1040
|
|
|
1041
|
+
listDeployments: async function (namespace, limit = 5) {
|
|
1042
|
+
const params = new URLSearchParams();
|
|
1043
|
+
if (namespace) {
|
|
1044
|
+
params.append('namespace', namespace);
|
|
1045
|
+
}
|
|
1046
|
+
params.append('limit', limit);
|
|
1047
|
+
return await (
|
|
1048
|
+
await fetch(`${DJ_URL}/deployments?${params.toString()}`, {
|
|
1049
|
+
credentials: 'include',
|
|
1050
|
+
})
|
|
1051
|
+
).json();
|
|
1052
|
+
},
|
|
1053
|
+
|
|
1041
1054
|
sql: async function (metric_name, selection) {
|
|
1042
1055
|
const params = new URLSearchParams(selection);
|
|
1043
1056
|
for (const [key, value] of Object.entries(selection)) {
|