datajunction-ui 0.0.29 → 0.0.30
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 +5 -16
- package/src/app/components/__tests__/NamespaceHeader.test.jsx +14 -35
- package/src/app/components/__tests__/NotificationBell.test.tsx +23 -0
- 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 +390 -82
- 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
|
+
});
|
|
@@ -41,7 +41,6 @@ export default function NamespaceHeader({ namespace }) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
const isGit = sources.primary_source?.type === 'git';
|
|
44
|
-
const hasMultiple = sources.has_multiple_sources;
|
|
45
44
|
|
|
46
45
|
return (
|
|
47
46
|
<li
|
|
@@ -50,9 +49,7 @@ export default function NamespaceHeader({ namespace }) {
|
|
|
50
49
|
>
|
|
51
50
|
<span
|
|
52
51
|
title={
|
|
53
|
-
|
|
54
|
-
? `Warning: ${sources.sources.length} deployment sources`
|
|
55
|
-
: isGit
|
|
52
|
+
isGit
|
|
56
53
|
? `CI-managed: ${sources.primary_source.repository}${
|
|
57
54
|
sources.primary_source.branch
|
|
58
55
|
? ` (${sources.primary_source.branch})`
|
|
@@ -67,21 +64,13 @@ export default function NamespaceHeader({ namespace }) {
|
|
|
67
64
|
padding: '2px 8px',
|
|
68
65
|
fontSize: '11px',
|
|
69
66
|
borderRadius: '12px',
|
|
70
|
-
backgroundColor:
|
|
71
|
-
|
|
72
|
-
: isGit
|
|
73
|
-
? '#d4edda'
|
|
74
|
-
: '#e2e3e5',
|
|
75
|
-
color: hasMultiple ? '#856404' : isGit ? '#155724' : '#383d41',
|
|
67
|
+
backgroundColor: isGit ? '#d4edda' : '#e2e3e5',
|
|
68
|
+
color: isGit ? '#155724' : '#383d41',
|
|
76
69
|
cursor: 'help',
|
|
77
70
|
}}
|
|
78
71
|
>
|
|
79
|
-
{
|
|
80
|
-
{
|
|
81
|
-
? `${sources.sources.length} sources`
|
|
82
|
-
: isGit
|
|
83
|
-
? 'CI'
|
|
84
|
-
: 'Local'}
|
|
72
|
+
{isGit ? '🔗' : '📁'}
|
|
73
|
+
{isGit ? 'CI' : 'Local'}
|
|
85
74
|
</span>
|
|
86
75
|
</li>
|
|
87
76
|
);
|
|
@@ -15,23 +15,15 @@ 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
|
}),
|
|
36
28
|
};
|
|
37
29
|
|
|
@@ -53,21 +45,15 @@ describe('<NamespaceHeader />', () => {
|
|
|
53
45
|
expect(screen.getByText(/CI/)).toBeInTheDocument();
|
|
54
46
|
});
|
|
55
47
|
|
|
56
|
-
it('should render
|
|
48
|
+
it('should render git source badge when source type is git without branch', async () => {
|
|
57
49
|
const mockDjClient = {
|
|
58
50
|
namespaceSources: jest.fn().mockResolvedValue({
|
|
59
|
-
total_deployments:
|
|
60
|
-
has_multiple_sources: false,
|
|
51
|
+
total_deployments: 3,
|
|
61
52
|
primary_source: {
|
|
62
|
-
type: '
|
|
63
|
-
|
|
53
|
+
type: 'git',
|
|
54
|
+
repository: 'github.com/test/repo',
|
|
55
|
+
branch: null,
|
|
64
56
|
},
|
|
65
|
-
sources: [
|
|
66
|
-
{
|
|
67
|
-
type: 'local',
|
|
68
|
-
hostname: 'localhost',
|
|
69
|
-
},
|
|
70
|
-
],
|
|
71
57
|
}),
|
|
72
58
|
};
|
|
73
59
|
|
|
@@ -85,23 +71,18 @@ describe('<NamespaceHeader />', () => {
|
|
|
85
71
|
);
|
|
86
72
|
});
|
|
87
73
|
|
|
88
|
-
// Should render
|
|
89
|
-
expect(screen.getByText(/
|
|
74
|
+
// Should render CI badge for git source even without branch
|
|
75
|
+
expect(screen.getByText(/CI/)).toBeInTheDocument();
|
|
90
76
|
});
|
|
91
77
|
|
|
92
|
-
it('should render
|
|
78
|
+
it('should render local source badge when source type is local', async () => {
|
|
93
79
|
const mockDjClient = {
|
|
94
80
|
namespaceSources: jest.fn().mockResolvedValue({
|
|
95
|
-
total_deployments:
|
|
96
|
-
has_multiple_sources: true,
|
|
81
|
+
total_deployments: 2,
|
|
97
82
|
primary_source: {
|
|
98
|
-
type: '
|
|
99
|
-
|
|
83
|
+
type: 'local',
|
|
84
|
+
hostname: 'localhost',
|
|
100
85
|
},
|
|
101
|
-
sources: [
|
|
102
|
-
{ type: 'git', repository: 'github.com/test/repo' },
|
|
103
|
-
{ type: 'local', hostname: 'localhost' },
|
|
104
|
-
],
|
|
105
86
|
}),
|
|
106
87
|
};
|
|
107
88
|
|
|
@@ -119,17 +100,15 @@ describe('<NamespaceHeader />', () => {
|
|
|
119
100
|
);
|
|
120
101
|
});
|
|
121
102
|
|
|
122
|
-
// Should render
|
|
123
|
-
expect(screen.getByText(/
|
|
103
|
+
// Should render Local badge for local source
|
|
104
|
+
expect(screen.getByText(/Local/)).toBeInTheDocument();
|
|
124
105
|
});
|
|
125
106
|
|
|
126
107
|
it('should not render badge when no deployments', async () => {
|
|
127
108
|
const mockDjClient = {
|
|
128
109
|
namespaceSources: jest.fn().mockResolvedValue({
|
|
129
110
|
total_deployments: 0,
|
|
130
|
-
has_multiple_sources: false,
|
|
131
111
|
primary_source: null,
|
|
132
|
-
sources: [],
|
|
133
112
|
}),
|
|
134
113
|
};
|
|
135
114
|
|
|
@@ -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
|
});
|
|
@@ -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
|
+
});
|
|
@@ -65,6 +65,7 @@ export function NamespacePage() {
|
|
|
65
65
|
|
|
66
66
|
const [filters, setFilters] = useState(getFiltersFromUrl);
|
|
67
67
|
const [moreFiltersOpen, setMoreFiltersOpen] = useState(false);
|
|
68
|
+
const [deploymentsDropdownOpen, setDeploymentsDropdownOpen] = useState(false);
|
|
68
69
|
|
|
69
70
|
// Sync filters state when URL changes
|
|
70
71
|
useEffect(() => {
|
|
@@ -169,6 +170,7 @@ export function NamespacePage() {
|
|
|
169
170
|
const [namespaceHierarchy, setNamespaceHierarchy] = useState([]);
|
|
170
171
|
const [namespaceSources, setNamespaceSources] = useState({});
|
|
171
172
|
const [currentNamespaceSources, setCurrentNamespaceSources] = useState(null);
|
|
173
|
+
const [recentDeployments, setRecentDeployments] = useState([]);
|
|
172
174
|
|
|
173
175
|
const [sortConfig, setSortConfig] = useState({
|
|
174
176
|
key: 'updatedAt',
|
|
@@ -254,6 +256,15 @@ export function NamespacePage() {
|
|
|
254
256
|
if (namespace) {
|
|
255
257
|
const sources = await djClient.namespaceSources(namespace);
|
|
256
258
|
setCurrentNamespaceSources(sources);
|
|
259
|
+
|
|
260
|
+
// Fetch recent deployments for this namespace
|
|
261
|
+
try {
|
|
262
|
+
const deployments = await djClient.listDeployments(namespace, 5);
|
|
263
|
+
setRecentDeployments(deployments || []);
|
|
264
|
+
} catch (err) {
|
|
265
|
+
console.error('Failed to fetch deployments:', err);
|
|
266
|
+
setRecentDeployments([]);
|
|
267
|
+
}
|
|
257
268
|
}
|
|
258
269
|
};
|
|
259
270
|
fetchCurrentSources().catch(console.error);
|
|
@@ -876,89 +887,386 @@ export function NamespacePage() {
|
|
|
876
887
|
</span>
|
|
877
888
|
)}
|
|
878
889
|
{currentNamespaceSources &&
|
|
879
|
-
currentNamespaceSources.total_deployments > 0 &&
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
: ''
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
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' }}
|
|
890
|
+
currentNamespaceSources.total_deployments > 0 && (
|
|
891
|
+
<div style={{ position: 'relative', marginLeft: '8px' }}>
|
|
892
|
+
<button
|
|
893
|
+
onClick={() =>
|
|
894
|
+
setDeploymentsDropdownOpen(!deploymentsDropdownOpen)
|
|
895
|
+
}
|
|
896
|
+
style={{
|
|
897
|
+
height: '32px',
|
|
898
|
+
padding: '0 12px',
|
|
899
|
+
fontSize: '12px',
|
|
900
|
+
border: 'none',
|
|
901
|
+
borderRadius: '4px',
|
|
902
|
+
backgroundColor: '#ffffff',
|
|
903
|
+
color: '#0b3d91',
|
|
904
|
+
cursor: 'pointer',
|
|
905
|
+
display: 'flex',
|
|
906
|
+
alignItems: 'center',
|
|
907
|
+
gap: '4px',
|
|
908
|
+
whiteSpace: 'nowrap',
|
|
909
|
+
}}
|
|
918
910
|
>
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
: '
|
|
960
|
-
|
|
961
|
-
|
|
911
|
+
{currentNamespaceSources.primary_source?.type ===
|
|
912
|
+
'git' ? (
|
|
913
|
+
<>
|
|
914
|
+
<svg
|
|
915
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
916
|
+
width="12"
|
|
917
|
+
height="12"
|
|
918
|
+
viewBox="0 0 24 24"
|
|
919
|
+
fill="none"
|
|
920
|
+
stroke="currentColor"
|
|
921
|
+
strokeWidth="2"
|
|
922
|
+
strokeLinecap="round"
|
|
923
|
+
strokeLinejoin="round"
|
|
924
|
+
>
|
|
925
|
+
<line x1="6" y1="3" x2="6" y2="15"></line>
|
|
926
|
+
<circle cx="18" cy="6" r="3"></circle>
|
|
927
|
+
<circle cx="6" cy="18" r="3"></circle>
|
|
928
|
+
<path d="M18 9a9 9 0 0 1-9 9"></path>
|
|
929
|
+
</svg>
|
|
930
|
+
Git Managed
|
|
931
|
+
</>
|
|
932
|
+
) : (
|
|
933
|
+
<>
|
|
934
|
+
<svg
|
|
935
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
936
|
+
width="12"
|
|
937
|
+
height="12"
|
|
938
|
+
viewBox="0 0 24 24"
|
|
939
|
+
fill="none"
|
|
940
|
+
stroke="currentColor"
|
|
941
|
+
strokeWidth="2"
|
|
942
|
+
strokeLinecap="round"
|
|
943
|
+
strokeLinejoin="round"
|
|
944
|
+
>
|
|
945
|
+
<circle cx="12" cy="7" r="4" />
|
|
946
|
+
<path d="M5.5 21a6.5 6.5 0 0 1 13 0Z" />
|
|
947
|
+
</svg>
|
|
948
|
+
Local Deploy
|
|
949
|
+
</>
|
|
950
|
+
)}
|
|
951
|
+
<span style={{ fontSize: '8px' }}>
|
|
952
|
+
{deploymentsDropdownOpen ? '▲' : '▼'}
|
|
953
|
+
</span>
|
|
954
|
+
</button>
|
|
955
|
+
|
|
956
|
+
{deploymentsDropdownOpen && (
|
|
957
|
+
<div
|
|
958
|
+
style={{
|
|
959
|
+
position: 'absolute',
|
|
960
|
+
top: '100%',
|
|
961
|
+
left: 0,
|
|
962
|
+
marginTop: '4px',
|
|
963
|
+
padding: '12px',
|
|
964
|
+
backgroundColor: 'white',
|
|
965
|
+
border: '1px solid #ddd',
|
|
966
|
+
borderRadius: '8px',
|
|
967
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
|
968
|
+
zIndex: 1000,
|
|
969
|
+
minWidth: '340px',
|
|
970
|
+
}}
|
|
971
|
+
>
|
|
972
|
+
{currentNamespaceSources.primary_source?.type ===
|
|
973
|
+
'git' ? (
|
|
974
|
+
<a
|
|
975
|
+
href={
|
|
976
|
+
currentNamespaceSources.primary_source.repository?.startsWith(
|
|
977
|
+
'http',
|
|
978
|
+
)
|
|
979
|
+
? currentNamespaceSources.primary_source
|
|
980
|
+
.repository
|
|
981
|
+
: `https://${currentNamespaceSources.primary_source.repository}`
|
|
982
|
+
}
|
|
983
|
+
target="_blank"
|
|
984
|
+
rel="noopener noreferrer"
|
|
985
|
+
style={{
|
|
986
|
+
display: 'flex',
|
|
987
|
+
alignItems: 'center',
|
|
988
|
+
gap: '8px',
|
|
989
|
+
fontSize: '13px',
|
|
990
|
+
fontWeight: 400,
|
|
991
|
+
textDecoration: 'none',
|
|
992
|
+
marginBottom: '12px',
|
|
993
|
+
}}
|
|
994
|
+
>
|
|
995
|
+
<svg
|
|
996
|
+
width="16"
|
|
997
|
+
height="16"
|
|
998
|
+
viewBox="0 0 24 24"
|
|
999
|
+
fill="none"
|
|
1000
|
+
stroke="currentColor"
|
|
1001
|
+
strokeWidth="2"
|
|
1002
|
+
strokeLinecap="round"
|
|
1003
|
+
strokeLinejoin="round"
|
|
1004
|
+
>
|
|
1005
|
+
<line x1="6" y1="3" x2="6" y2="15" />
|
|
1006
|
+
<circle cx="18" cy="6" r="3" />
|
|
1007
|
+
<circle cx="6" cy="18" r="3" />
|
|
1008
|
+
<path d="M18 9a9 9 0 0 1-9 9" />
|
|
1009
|
+
</svg>
|
|
1010
|
+
{
|
|
1011
|
+
currentNamespaceSources.primary_source
|
|
1012
|
+
.repository
|
|
1013
|
+
}
|
|
1014
|
+
{currentNamespaceSources.primary_source
|
|
1015
|
+
.branch &&
|
|
1016
|
+
` (${currentNamespaceSources.primary_source.branch})`}
|
|
1017
|
+
</a>
|
|
1018
|
+
) : (
|
|
1019
|
+
<div
|
|
1020
|
+
style={{
|
|
1021
|
+
display: 'flex',
|
|
1022
|
+
alignItems: 'center',
|
|
1023
|
+
gap: '8px',
|
|
1024
|
+
fontSize: '13px',
|
|
1025
|
+
fontWeight: 600,
|
|
1026
|
+
color: '#0b3d91',
|
|
1027
|
+
marginBottom: '12px',
|
|
1028
|
+
}}
|
|
1029
|
+
>
|
|
1030
|
+
<svg
|
|
1031
|
+
width="16"
|
|
1032
|
+
height="16"
|
|
1033
|
+
viewBox="0 0 24 24"
|
|
1034
|
+
fill="none"
|
|
1035
|
+
stroke="currentColor"
|
|
1036
|
+
strokeWidth="2"
|
|
1037
|
+
strokeLinecap="round"
|
|
1038
|
+
strokeLinejoin="round"
|
|
1039
|
+
>
|
|
1040
|
+
<circle cx="12" cy="7" r="4" />
|
|
1041
|
+
<path d="M5.5 21a6.5 6.5 0 0 1 13 0Z" />
|
|
1042
|
+
</svg>
|
|
1043
|
+
{recentDeployments?.[0]?.created_by
|
|
1044
|
+
? `Local deploys by ${recentDeployments[0].created_by}`
|
|
1045
|
+
: 'Local/adhoc deployments'}
|
|
1046
|
+
</div>
|
|
1047
|
+
)}
|
|
1048
|
+
|
|
1049
|
+
{/* Separator */}
|
|
1050
|
+
<div
|
|
1051
|
+
style={{
|
|
1052
|
+
height: '1px',
|
|
1053
|
+
backgroundColor: '#e2e8f0',
|
|
1054
|
+
marginBottom: '8px',
|
|
1055
|
+
}}
|
|
1056
|
+
/>
|
|
1057
|
+
|
|
1058
|
+
{/* Recent deployments list (no header) */}
|
|
1059
|
+
{recentDeployments?.length > 0 ? (
|
|
1060
|
+
recentDeployments.map((d, idx) => {
|
|
1061
|
+
const isGit = d.source?.type === 'git';
|
|
1062
|
+
const statusColor =
|
|
1063
|
+
d.status === 'success'
|
|
1064
|
+
? '#22c55e'
|
|
1065
|
+
: d.status === 'failed'
|
|
1066
|
+
? '#ef4444'
|
|
1067
|
+
: '#94a3b8';
|
|
1068
|
+
|
|
1069
|
+
// Build commit URL if available
|
|
1070
|
+
const commitUrl =
|
|
1071
|
+
isGit &&
|
|
1072
|
+
d.source?.repository &&
|
|
1073
|
+
d.source?.commit_sha
|
|
1074
|
+
? `${
|
|
1075
|
+
d.source.repository.startsWith('http')
|
|
1076
|
+
? d.source.repository
|
|
1077
|
+
: `https://${d.source.repository}`
|
|
1078
|
+
}/commit/${d.source.commit_sha}`
|
|
1079
|
+
: null;
|
|
1080
|
+
|
|
1081
|
+
// For git: show branch + short SHA; for local: reason or hostname
|
|
1082
|
+
const detail = isGit
|
|
1083
|
+
? d.source?.branch || 'main'
|
|
1084
|
+
: d.source?.reason ||
|
|
1085
|
+
d.source?.hostname ||
|
|
1086
|
+
'adhoc';
|
|
1087
|
+
|
|
1088
|
+
const shortSha = d.source?.commit_sha?.slice(
|
|
1089
|
+
0,
|
|
1090
|
+
7,
|
|
1091
|
+
);
|
|
1092
|
+
|
|
1093
|
+
return (
|
|
1094
|
+
<div
|
|
1095
|
+
key={`${d.uuid}-${idx}`}
|
|
1096
|
+
style={{
|
|
1097
|
+
display: 'grid',
|
|
1098
|
+
gridTemplateColumns: '18px 1fr auto auto',
|
|
1099
|
+
alignItems: 'center',
|
|
1100
|
+
gap: '8px',
|
|
1101
|
+
padding: '6px 0',
|
|
1102
|
+
borderBottom:
|
|
1103
|
+
idx === recentDeployments.length - 1
|
|
1104
|
+
? 'none'
|
|
1105
|
+
: '1px solid #f1f5f9',
|
|
1106
|
+
fontSize: '12px',
|
|
1107
|
+
}}
|
|
1108
|
+
>
|
|
1109
|
+
{/* Status dot */}
|
|
1110
|
+
<div
|
|
1111
|
+
style={{
|
|
1112
|
+
width: '8px',
|
|
1113
|
+
height: '8px',
|
|
1114
|
+
borderRadius: '50%',
|
|
1115
|
+
backgroundColor: statusColor,
|
|
1116
|
+
}}
|
|
1117
|
+
title={d.status}
|
|
1118
|
+
/>
|
|
1119
|
+
|
|
1120
|
+
{/* User + detail */}
|
|
1121
|
+
<div
|
|
1122
|
+
style={{
|
|
1123
|
+
display: 'flex',
|
|
1124
|
+
alignItems: 'center',
|
|
1125
|
+
gap: '6px',
|
|
1126
|
+
minWidth: 0,
|
|
1127
|
+
}}
|
|
1128
|
+
>
|
|
1129
|
+
<span
|
|
1130
|
+
style={{
|
|
1131
|
+
fontWeight: 500,
|
|
1132
|
+
color: '#0f172a',
|
|
1133
|
+
whiteSpace: 'nowrap',
|
|
1134
|
+
}}
|
|
1135
|
+
>
|
|
1136
|
+
{d.created_by || 'unknown'}
|
|
1137
|
+
</span>
|
|
1138
|
+
<span style={{ color: '#cbd5e1' }}>
|
|
1139
|
+
—
|
|
1140
|
+
</span>
|
|
1141
|
+
{isGit ? (
|
|
1142
|
+
<>
|
|
1143
|
+
<span
|
|
1144
|
+
style={{
|
|
1145
|
+
color: '#64748b',
|
|
1146
|
+
whiteSpace: 'nowrap',
|
|
1147
|
+
}}
|
|
1148
|
+
>
|
|
1149
|
+
{detail}
|
|
1150
|
+
</span>
|
|
1151
|
+
{shortSha && (
|
|
1152
|
+
<>
|
|
1153
|
+
<span
|
|
1154
|
+
style={{ color: '#cbd5e1' }}
|
|
1155
|
+
>
|
|
1156
|
+
@
|
|
1157
|
+
</span>
|
|
1158
|
+
{commitUrl ? (
|
|
1159
|
+
<a
|
|
1160
|
+
href={commitUrl}
|
|
1161
|
+
target="_blank"
|
|
1162
|
+
rel="noopener noreferrer"
|
|
1163
|
+
style={{
|
|
1164
|
+
fontFamily: 'monospace',
|
|
1165
|
+
fontSize: '11px',
|
|
1166
|
+
color: '#3b82f6',
|
|
1167
|
+
textDecoration: 'none',
|
|
1168
|
+
}}
|
|
1169
|
+
>
|
|
1170
|
+
{shortSha}
|
|
1171
|
+
</a>
|
|
1172
|
+
) : (
|
|
1173
|
+
<span
|
|
1174
|
+
style={{
|
|
1175
|
+
fontFamily: 'monospace',
|
|
1176
|
+
fontSize: '11px',
|
|
1177
|
+
color: '#64748b',
|
|
1178
|
+
}}
|
|
1179
|
+
>
|
|
1180
|
+
{shortSha}
|
|
1181
|
+
</span>
|
|
1182
|
+
)}
|
|
1183
|
+
</>
|
|
1184
|
+
)}
|
|
1185
|
+
</>
|
|
1186
|
+
) : (
|
|
1187
|
+
<span
|
|
1188
|
+
style={{
|
|
1189
|
+
color: '#64748b',
|
|
1190
|
+
overflow: 'hidden',
|
|
1191
|
+
textOverflow: 'ellipsis',
|
|
1192
|
+
whiteSpace: 'nowrap',
|
|
1193
|
+
}}
|
|
1194
|
+
>
|
|
1195
|
+
{detail}
|
|
1196
|
+
</span>
|
|
1197
|
+
)}
|
|
1198
|
+
</div>
|
|
1199
|
+
|
|
1200
|
+
{/* Timestamp */}
|
|
1201
|
+
<span
|
|
1202
|
+
style={{
|
|
1203
|
+
color: '#94a3b8',
|
|
1204
|
+
fontSize: '11px',
|
|
1205
|
+
whiteSpace: 'nowrap',
|
|
1206
|
+
}}
|
|
1207
|
+
>
|
|
1208
|
+
{new Date(
|
|
1209
|
+
d.created_at,
|
|
1210
|
+
).toLocaleDateString()}
|
|
1211
|
+
</span>
|
|
1212
|
+
|
|
1213
|
+
{/* Icon */}
|
|
1214
|
+
<div
|
|
1215
|
+
style={{
|
|
1216
|
+
color: isGit ? '#155724' : '#0b3d91',
|
|
1217
|
+
}}
|
|
1218
|
+
>
|
|
1219
|
+
{isGit ? (
|
|
1220
|
+
<svg
|
|
1221
|
+
width="12"
|
|
1222
|
+
height="12"
|
|
1223
|
+
viewBox="0 0 24 24"
|
|
1224
|
+
fill="none"
|
|
1225
|
+
stroke="currentColor"
|
|
1226
|
+
strokeWidth="2"
|
|
1227
|
+
strokeLinecap="round"
|
|
1228
|
+
strokeLinejoin="round"
|
|
1229
|
+
>
|
|
1230
|
+
<line x1="6" y1="3" x2="6" y2="15" />
|
|
1231
|
+
<circle cx="18" cy="6" r="3" />
|
|
1232
|
+
<circle cx="6" cy="18" r="3" />
|
|
1233
|
+
<path d="M18 9a9 9 0 0 1-9 9" />
|
|
1234
|
+
</svg>
|
|
1235
|
+
) : (
|
|
1236
|
+
<svg
|
|
1237
|
+
width="12"
|
|
1238
|
+
height="12"
|
|
1239
|
+
viewBox="0 0 24 24"
|
|
1240
|
+
fill="none"
|
|
1241
|
+
stroke="currentColor"
|
|
1242
|
+
strokeWidth="2"
|
|
1243
|
+
strokeLinecap="round"
|
|
1244
|
+
strokeLinejoin="round"
|
|
1245
|
+
>
|
|
1246
|
+
<circle cx="12" cy="7" r="4" />
|
|
1247
|
+
<path d="M5.5 21a6.5 6.5 0 0 1 13 0Z" />
|
|
1248
|
+
</svg>
|
|
1249
|
+
)}
|
|
1250
|
+
</div>
|
|
1251
|
+
</div>
|
|
1252
|
+
);
|
|
1253
|
+
})
|
|
1254
|
+
) : (
|
|
1255
|
+
<div
|
|
1256
|
+
style={{
|
|
1257
|
+
color: '#94a3b8',
|
|
1258
|
+
fontSize: '12px',
|
|
1259
|
+
textAlign: 'center',
|
|
1260
|
+
padding: '8px 0',
|
|
1261
|
+
}}
|
|
1262
|
+
>
|
|
1263
|
+
No deployments yet
|
|
1264
|
+
</div>
|
|
1265
|
+
)}
|
|
1266
|
+
</div>
|
|
1267
|
+
)}
|
|
1268
|
+
</div>
|
|
1269
|
+
)}
|
|
962
1270
|
</div>
|
|
963
1271
|
<div
|
|
964
1272
|
style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
|
|
@@ -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)) {
|