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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datajunction-ui",
3
- "version": "0.0.29",
3
+ "version": "0.0.30",
4
4
  "description": "DataJunction Metrics Platform UI",
5
5
  "module": "src/index.tsx",
6
6
  "repository": {
@@ -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
- hasMultiple
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: hasMultiple
71
- ? '#fff3cd'
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
- {hasMultiple ? '⚠️' : isGit ? '🔗' : '📁'}
80
- {hasMultiple
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 local source badge when source type is local', async () => {
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: 2,
60
- has_multiple_sources: false,
51
+ total_deployments: 3,
61
52
  primary_source: {
62
- type: 'local',
63
- hostname: 'localhost',
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 Local badge for local source
89
- expect(screen.getByText(/Local/)).toBeInTheDocument();
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 warning badge when multiple sources exist', async () => {
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: 10,
96
- has_multiple_sources: true,
81
+ total_deployments: 2,
97
82
  primary_source: {
98
- type: 'git',
99
- repository: 'github.com/test/repo',
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 warning badge for multiple sources
123
- expect(screen.getByText(/2 sources/)).toBeInTheDocument();
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
- (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' }}
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
- <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
- ))}
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
- const context = useContext(UserContext);
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)) {