datajunction-ui 0.0.30 → 0.0.34

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.
Files changed (25) hide show
  1. package/TODO.md +265 -0
  2. package/package.json +1 -1
  3. package/src/app/components/ListGroupItem.jsx +2 -2
  4. package/src/app/components/NamespaceHeader.jsx +423 -65
  5. package/src/app/components/QueryInfo.jsx +2 -1
  6. package/src/app/components/__tests__/NamespaceHeader.test.jsx +14 -9
  7. package/src/app/components/__tests__/__snapshots__/ListGroupItem.test.tsx.snap +2 -2
  8. package/src/app/components/__tests__/__snapshots__/NamespaceHeader.test.jsx.snap +139 -34
  9. package/src/app/components/djgraph/__tests__/Collapse.test.jsx +6 -3
  10. package/src/app/pages/AddEditNodePage/index.jsx +1 -1
  11. package/src/app/pages/AddEditTagPage/index.jsx +1 -1
  12. package/src/app/pages/CubeBuilderPage/__tests__/index.test.jsx +55 -21
  13. package/src/app/pages/NamespacePage/index.jsx +44 -511
  14. package/src/app/pages/NodePage/NodeInfoTab.jsx +17 -6
  15. package/src/app/pages/NodePage/NodeMaterializationTab.jsx +5 -0
  16. package/src/app/pages/NodePage/NodePreAggregationsTab.jsx +656 -0
  17. package/src/app/pages/NodePage/NodeValidateTab.jsx +4 -2
  18. package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +58 -45
  19. package/src/app/pages/NodePage/__tests__/NodePreAggregationsTab.test.jsx +654 -0
  20. package/src/app/pages/NodePage/index.jsx +9 -1
  21. package/src/app/pages/NotificationsPage/__tests__/index.test.jsx +19 -4
  22. package/src/app/pages/SQLBuilderPage/__tests__/index.test.jsx +47 -9
  23. package/src/app/pages/SQLBuilderPage/index.jsx +2 -2
  24. package/src/app/services/DJService.js +26 -0
  25. package/src/styles/preaggregations.css +547 -0
@@ -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';
@@ -65,7 +66,6 @@ export function NamespacePage() {
65
66
 
66
67
  const [filters, setFilters] = useState(getFiltersFromUrl);
67
68
  const [moreFiltersOpen, setMoreFiltersOpen] = useState(false);
68
- const [deploymentsDropdownOpen, setDeploymentsDropdownOpen] = useState(false);
69
69
 
70
70
  // Sync filters state when URL changes
71
71
  useEffect(() => {
@@ -169,8 +169,6 @@ export function NamespacePage() {
169
169
 
170
170
  const [namespaceHierarchy, setNamespaceHierarchy] = useState([]);
171
171
  const [namespaceSources, setNamespaceSources] = useState({});
172
- const [currentNamespaceSources, setCurrentNamespaceSources] = useState(null);
173
- const [recentDeployments, setRecentDeployments] = useState([]);
174
172
 
175
173
  const [sortConfig, setSortConfig] = useState({
176
174
  key: 'updatedAt',
@@ -250,26 +248,6 @@ export function NamespacePage() {
250
248
  fetchData().catch(console.error);
251
249
  }, [djClient, djClient.namespaces]);
252
250
 
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
-
273
251
  useEffect(() => {
274
252
  const fetchData = async () => {
275
253
  setRetrieved(false);
@@ -826,496 +804,51 @@ export function NamespacePage() {
826
804
  : null}
827
805
  </div>
828
806
  <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' }}>
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
- }}
910
- >
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
- )}
1270
- </div>
1271
- <div
1272
- 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"
1273
834
  >
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"
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"
1300
844
  >
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>
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>
1319
852
  <table className="card-table table" style={{ marginBottom: 0 }}>
1320
853
  <thead>
1321
854
  <tr>
@@ -37,9 +37,13 @@ export default function NodeInfoTab({ node }) {
37
37
  const [metricInfo, setMetricInfo] = useState(null);
38
38
 
39
39
  const nodeTags = node?.tags.map(tag => (
40
- <div className={'badge tag_value'}>
40
+ <span
41
+ key={tag.name}
42
+ className={'badge tag_value'}
43
+ style={{ marginRight: '4px' }}
44
+ >
41
45
  <a href={`/tags/${tag.name}`}>{tag.display_name}</a>
42
- </div>
46
+ </span>
43
47
  ));
44
48
  const djClient = useContext(DJClientContext).DataJunctionAPI;
45
49
 
@@ -314,13 +318,19 @@ export default function NodeInfoTab({ node }) {
314
318
  }
315
319
  >
316
320
  {node?.type !== 'metric'
317
- ? node?.primary_key?.map(dim => (
318
- <span className="rounded-pill badge bg-secondary-soft PrimaryKey">
321
+ ? node?.primary_key?.map((dim, idx) => (
322
+ <span
323
+ key={`pk-${idx}`}
324
+ className="rounded-pill badge bg-secondary-soft PrimaryKey"
325
+ >
319
326
  <a href={`/nodes/${node?.name}`}>{dim}</a>
320
327
  </span>
321
328
  ))
322
- : node?.required_dimensions?.map(dim => (
323
- <span className="rounded-pill badge bg-secondary-soft PrimaryKey">
329
+ : node?.required_dimensions?.map((dim, idx) => (
330
+ <span
331
+ key={`rd-${idx}`}
332
+ className="rounded-pill badge bg-secondary-soft PrimaryKey"
333
+ >
324
334
  <a href={`/nodes/${node?.upstream_node}`}>{dim.name}</a>
325
335
  </span>
326
336
  ))}
@@ -341,6 +351,7 @@ export default function NodeInfoTab({ node }) {
341
351
  <p className="mb-0 opacity-75">
342
352
  {node?.owners.map(owner => (
343
353
  <span
354
+ key={owner.username}
344
355
  className="badge node_type__transform"
345
356
  style={{ margin: '2px', fontSize: '16px', cursor: 'pointer' }}
346
357
  >
@@ -11,6 +11,11 @@ import AvailabilityStateBlock from './AvailabilityStateBlock';
11
11
 
12
12
  const cronstrue = require('cronstrue');
13
13
 
14
+ /**
15
+ * Cube materialization tab - shows cube-specific materializations.
16
+ * For non-cube nodes, the parent component (index.jsx) renders
17
+ * NodePreAggregationsTab instead.
18
+ */
14
19
  export default function NodeMaterializationTab({ node, djClient }) {
15
20
  const [rawMaterializations, setRawMaterializations] = useState([]);
16
21
  const [selectedRevisionTab, setSelectedRevisionTab] = useState(null);