datajunction-ui 0.0.27 → 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.
@@ -9,6 +9,7 @@ import AddNodeDropdown from '../../components/AddNodeDropdown';
9
9
  import NodeListActions from '../../components/NodeListActions';
10
10
  import LoadingIcon from '../../icons/LoadingIcon';
11
11
  import CompactSelect from './CompactSelect';
12
+ import { getDJUrl } from '../../services/DJService';
12
13
 
13
14
  import 'styles/node-list.css';
14
15
  import 'styles/sorted-table.css';
@@ -64,6 +65,7 @@ export function NamespacePage() {
64
65
 
65
66
  const [filters, setFilters] = useState(getFiltersFromUrl);
66
67
  const [moreFiltersOpen, setMoreFiltersOpen] = useState(false);
68
+ const [deploymentsDropdownOpen, setDeploymentsDropdownOpen] = useState(false);
67
69
 
68
70
  // Sync filters state when URL changes
69
71
  useEffect(() => {
@@ -166,6 +168,9 @@ export function NamespacePage() {
166
168
  const [retrieved, setRetrieved] = useState(false);
167
169
 
168
170
  const [namespaceHierarchy, setNamespaceHierarchy] = useState([]);
171
+ const [namespaceSources, setNamespaceSources] = useState({});
172
+ const [currentNamespaceSources, setCurrentNamespaceSources] = useState(null);
173
+ const [recentDeployments, setRecentDeployments] = useState([]);
169
174
 
170
175
  const [sortConfig, setSortConfig] = useState({
171
176
  key: 'updatedAt',
@@ -230,10 +235,41 @@ export function NamespacePage() {
230
235
  const namespaces = await djClient.namespaces();
231
236
  const hierarchy = createNamespaceHierarchy(namespaces);
232
237
  setNamespaceHierarchy(hierarchy);
238
+
239
+ // Fetch sources for all namespaces in bulk
240
+ const allNamespaceNames = namespaces.map(ns => ns.namespace);
241
+ if (allNamespaceNames.length > 0) {
242
+ const sourcesResponse = await djClient.namespaceSourcesBulk(
243
+ allNamespaceNames,
244
+ );
245
+ if (sourcesResponse && sourcesResponse.sources) {
246
+ setNamespaceSources(sourcesResponse.sources);
247
+ }
248
+ }
233
249
  };
234
250
  fetchData().catch(console.error);
235
251
  }, [djClient, djClient.namespaces]);
236
252
 
253
+ // Fetch sources for the current namespace (for the header badge)
254
+ useEffect(() => {
255
+ const fetchCurrentSources = async () => {
256
+ if (namespace) {
257
+ const sources = await djClient.namespaceSources(namespace);
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
+ }
268
+ }
269
+ };
270
+ fetchCurrentSources().catch(console.error);
271
+ }, [djClient, namespace]);
272
+
237
273
  useEffect(() => {
238
274
  const fetchData = async () => {
239
275
  setRetrieved(false);
@@ -458,7 +494,6 @@ export function NamespacePage() {
458
494
  }}
459
495
  >
460
496
  <h2 style={{ margin: 0 }}>Explore</h2>
461
- <AddNodeDropdown namespace={namespace} />
462
497
  </div>
463
498
 
464
499
  {/* Unified Filter Bar */}
@@ -466,7 +501,7 @@ export function NamespacePage() {
466
501
  style={{
467
502
  marginBottom: '1rem',
468
503
  padding: '1rem',
469
- backgroundColor: '#f8f9fa',
504
+ backgroundColor: '#f8fafc',
470
505
  borderRadius: '8px',
471
506
  }}
472
507
  >
@@ -482,7 +517,17 @@ export function NamespacePage() {
482
517
  <div
483
518
  style={{ display: 'flex', alignItems: 'center', gap: '6px' }}
484
519
  >
485
- <span style={{ fontSize: '12px', color: '#555' }}>Quick:</span>
520
+ <span
521
+ style={{
522
+ fontSize: '11px',
523
+ fontWeight: '600',
524
+ textTransform: 'uppercase',
525
+ letterSpacing: '0.5px',
526
+ color: '#64748b',
527
+ }}
528
+ >
529
+ Quick
530
+ </span>
486
531
  {presets.map(preset => (
487
532
  <button
488
533
  key={preset.id}
@@ -745,18 +790,23 @@ export function NamespacePage() {
745
790
  </div>
746
791
 
747
792
  <div className="table-responsive">
748
- <div className={`sidebar`}>
793
+ <div
794
+ className={`sidebar`}
795
+ style={{ borderRight: '1px solid #e2e8f0', paddingRight: '1rem' }}
796
+ >
749
797
  <div
750
798
  style={{
751
- padding: '1rem 1rem 1rem 0',
799
+ paddingBottom: '12px',
800
+ marginBottom: '8px',
752
801
  }}
753
802
  >
754
803
  <span
755
804
  style={{
756
- textTransform: 'uppercase',
757
- fontSize: '0.8125rem',
805
+ fontSize: '11px',
758
806
  fontWeight: '600',
759
- color: '#95aac9',
807
+ textTransform: 'uppercase',
808
+ letterSpacing: '0.5px',
809
+ color: '#64748b',
760
810
  }}
761
811
  >
762
812
  Namespaces
@@ -770,54 +820,581 @@ export function NamespacePage() {
770
820
  defaultExpand={true}
771
821
  isTopLevel={true}
772
822
  key={child.namespace}
823
+ namespaceSources={namespaceSources}
773
824
  />
774
825
  ))
775
826
  : null}
776
827
  </div>
777
- <table className="card-table table">
778
- <thead>
779
- <tr>
780
- {fields.map(field => {
781
- return (
782
- <th key={field}>
828
+ <div style={{ flex: 1, minWidth: 0, marginLeft: '1.5rem' }}>
829
+ {/* Namespace Header */}
830
+ <div
831
+ style={{
832
+ display: 'flex',
833
+ justifyContent: 'space-between',
834
+ alignItems: 'center',
835
+ paddingBottom: '12px',
836
+ marginBottom: '16px',
837
+ borderBottom: '1px solid #e2e8f0',
838
+ }}
839
+ >
840
+ <div
841
+ style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
842
+ >
843
+ <a href="/" style={{ display: 'flex', alignItems: 'center' }}>
844
+ <svg
845
+ xmlns="http://www.w3.org/2000/svg"
846
+ width="16"
847
+ height="16"
848
+ fill="currentColor"
849
+ viewBox="0 0 16 16"
850
+ >
851
+ <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" />
852
+ </svg>
853
+ </a>
854
+ <span style={{ color: '#6c757d' }}>/</span>
855
+ {namespace ? (
856
+ namespace.split('.').map((part, index, arr) => (
857
+ <span
858
+ key={index}
859
+ style={{
860
+ display: 'flex',
861
+ alignItems: 'center',
862
+ gap: '8px',
863
+ }}
864
+ >
865
+ <a
866
+ href={`/namespaces/${arr
867
+ .slice(0, index + 1)
868
+ .join('.')}`}
869
+ style={{
870
+ fontWeight: '400',
871
+ color: '#1e293b',
872
+ textDecoration: 'none',
873
+ }}
874
+ >
875
+ {part}
876
+ </a>
877
+ {index < arr.length - 1 && (
878
+ <span style={{ color: '#94a3b8', fontWeight: '400' }}>
879
+ /
880
+ </span>
881
+ )}
882
+ </span>
883
+ ))
884
+ ) : (
885
+ <span style={{ fontWeight: '600', color: '#1e293b' }}>
886
+ All Namespaces
887
+ </span>
888
+ )}
889
+ {currentNamespaceSources &&
890
+ currentNamespaceSources.total_deployments > 0 && (
891
+ <div style={{ position: 'relative', marginLeft: '8px' }}>
783
892
  <button
784
- type="button"
785
- onClick={() => requestSort(field)}
786
- className={'sortable ' + getClassNamesFor(field)}
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
+ }}
787
910
  >
788
- {field.replace(/([a-z](?=[A-Z]))/g, '$1 ')}
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>
789
954
  </button>
790
- </th>
791
- );
792
- })}
793
- <th>Actions</th>
794
- </tr>
795
- </thead>
796
- <tbody>{nodesList}</tbody>
797
- <tfoot>
798
- <tr>
799
- <td>
800
- {retrieved && hasPrevPage ? (
801
- <a
802
- onClick={loadPrev}
803
- className="previous round pagination"
804
- >
805
- ← Previous
806
- </a>
807
- ) : (
808
- ''
809
- )}
810
- {retrieved && hasNextPage ? (
811
- <a onClick={loadNext} className="next round pagination">
812
- Next →
813
- </a>
814
- ) : (
815
- ''
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>
816
1269
  )}
817
- </td>
818
- </tr>
819
- </tfoot>
820
- </table>
1270
+ </div>
1271
+ <div
1272
+ style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
1273
+ >
1274
+ <a
1275
+ href={`${getDJUrl()}/namespaces/${namespace}/export/yaml`}
1276
+ download
1277
+ style={{
1278
+ display: 'inline-flex',
1279
+ alignItems: 'center',
1280
+ gap: '4px',
1281
+ // padding: '6px 12px',
1282
+ fontSize: '13px',
1283
+ fontWeight: '500',
1284
+ color: '#475569',
1285
+ // backgroundColor: '#f8fafc',
1286
+ // border: '1px solid #e2e8f0',
1287
+ borderRadius: '6px',
1288
+ textDecoration: 'none',
1289
+ cursor: 'pointer',
1290
+ transition: 'all 0.15s ease',
1291
+ margin: '0.5em 0px 0px 1em',
1292
+ }}
1293
+ onMouseOver={e => {
1294
+ e.currentTarget.style.color = '#333333';
1295
+ }}
1296
+ onMouseOut={e => {
1297
+ e.currentTarget.style.color = '#475569';
1298
+ }}
1299
+ title="Export namespace to YAML"
1300
+ >
1301
+ <svg
1302
+ width="14"
1303
+ height="14"
1304
+ viewBox="0 0 24 24"
1305
+ fill="none"
1306
+ stroke="currentColor"
1307
+ strokeWidth="2"
1308
+ strokeLinecap="round"
1309
+ strokeLinejoin="round"
1310
+ >
1311
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
1312
+ <polyline points="7 10 12 15 17 10"></polyline>
1313
+ <line x1="12" y1="15" x2="12" y2="3"></line>
1314
+ </svg>
1315
+ </a>
1316
+ <AddNodeDropdown namespace={namespace} />
1317
+ </div>
1318
+ </div>
1319
+ <table className="card-table table" style={{ marginBottom: 0 }}>
1320
+ <thead>
1321
+ <tr>
1322
+ {fields.map(field => {
1323
+ const thStyle = {
1324
+ fontFamily:
1325
+ "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
1326
+ fontSize: '11px',
1327
+ fontWeight: '600',
1328
+ textTransform: 'uppercase',
1329
+ letterSpacing: '0.5px',
1330
+ color: '#64748b',
1331
+ padding: '12px 16px',
1332
+ borderBottom: '1px solid #e2e8f0',
1333
+ backgroundColor: 'transparent',
1334
+ };
1335
+ return (
1336
+ <th key={field} style={thStyle}>
1337
+ <button
1338
+ type="button"
1339
+ onClick={() => requestSort(field)}
1340
+ className={'sortable ' + getClassNamesFor(field)}
1341
+ style={{
1342
+ fontSize: 'inherit',
1343
+ fontWeight: 'inherit',
1344
+ letterSpacing: 'inherit',
1345
+ textTransform: 'inherit',
1346
+ fontFamily: 'inherit',
1347
+ }}
1348
+ >
1349
+ {field.replace(/([a-z](?=[A-Z]))/g, '$1 ')}
1350
+ </button>
1351
+ </th>
1352
+ );
1353
+ })}
1354
+ <th
1355
+ style={{
1356
+ fontFamily:
1357
+ "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
1358
+ fontSize: '11px',
1359
+ fontWeight: '600',
1360
+ textTransform: 'uppercase',
1361
+ letterSpacing: '0.5px',
1362
+ color: '#64748b',
1363
+ padding: '12px 16px',
1364
+ borderBottom: '1px solid #e2e8f0',
1365
+ backgroundColor: 'transparent',
1366
+ }}
1367
+ >
1368
+ Actions
1369
+ </th>
1370
+ </tr>
1371
+ </thead>
1372
+ <tbody className="nodes-table-body">{nodesList}</tbody>
1373
+ <tfoot>
1374
+ <tr>
1375
+ <td>
1376
+ {retrieved && hasPrevPage ? (
1377
+ <a
1378
+ onClick={loadPrev}
1379
+ className="previous round pagination"
1380
+ >
1381
+ ← Previous
1382
+ </a>
1383
+ ) : (
1384
+ ''
1385
+ )}
1386
+ {retrieved && hasNextPage ? (
1387
+ <a onClick={loadNext} className="next round pagination">
1388
+ Next →
1389
+ </a>
1390
+ ) : (
1391
+ ''
1392
+ )}
1393
+ </td>
1394
+ </tr>
1395
+ </tfoot>
1396
+ </table>
1397
+ </div>
821
1398
  </div>
822
1399
  </div>
823
1400
  </div>