datajunction-ui 0.0.106 → 0.0.108

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.106",
3
+ "version": "0.0.108",
4
4
  "description": "DataJunction UI",
5
5
  "module": "src/index.tsx",
6
6
  "repository": {
@@ -45,6 +45,11 @@ export default function NamespaceHeader({
45
45
  setExistingPR(null);
46
46
 
47
47
  const fetchData = async () => {
48
+ if (!namespace) {
49
+ if (onGitConfigLoaded) onGitConfigLoaded(null);
50
+ setGitConfigLoading(false);
51
+ return;
52
+ }
48
53
  if (namespace) {
49
54
  // Fetch deployment sources
50
55
  try {
@@ -8,7 +8,7 @@ const Explorer = ({
8
8
  item = [],
9
9
  current,
10
10
  isTopLevel = false,
11
- namespaceSources = {},
11
+ gitRoots = new Set(),
12
12
  }) => {
13
13
  const djClient = useContext(DJClientContext).DataJunctionAPI;
14
14
  const [items, setItems] = useState([]);
@@ -139,50 +139,41 @@ const Explorer = ({
139
139
  >
140
140
  {items.namespace}
141
141
  </a>
142
- {/* Deployment source badge */}
143
- {namespaceSources[items.path] &&
144
- namespaceSources[items.path].total_deployments > 0 &&
145
- namespaceSources[items.path].primary_source?.type === 'git' && (
146
- <span
147
- title={`Git: ${
148
- namespaceSources[items.path].primary_source.repository ||
149
- 'unknown'
150
- }${
151
- namespaceSources[items.path].primary_source.branch
152
- ? ` (${namespaceSources[items.path].primary_source.branch})`
153
- : ''
154
- }`}
155
- style={{
156
- marginLeft: '6px',
157
- fontSize: '9px',
158
- padding: '1px 4px',
159
- borderRadius: '3px',
160
- backgroundColor: '#d4edda',
161
- color: '#155724',
162
- display: 'inline-flex',
163
- alignItems: 'center',
164
- gap: '2px',
165
- }}
142
+ {/* Git root badge */}
143
+ {gitRoots.has(items.path) && (
144
+ <span
145
+ title="Git-backed namespace"
146
+ style={{
147
+ marginLeft: '6px',
148
+ fontSize: '9px',
149
+ padding: '1px 4px',
150
+ borderRadius: '3px',
151
+ backgroundColor: '#d4edda',
152
+ color: '#155724',
153
+ display: 'inline-flex',
154
+ alignItems: 'center',
155
+ gap: '2px',
156
+ }}
157
+ >
158
+ <svg
159
+ xmlns="http://www.w3.org/2000/svg"
160
+ width="10"
161
+ height="10"
162
+ viewBox="0 0 24 24"
163
+ fill="none"
164
+ stroke="currentColor"
165
+ strokeWidth="2"
166
+ strokeLinecap="round"
167
+ strokeLinejoin="round"
166
168
  >
167
- <svg
168
- xmlns="http://www.w3.org/2000/svg"
169
- width="10"
170
- height="10"
171
- viewBox="0 0 24 24"
172
- fill="none"
173
- stroke="currentColor"
174
- strokeWidth="2"
175
- strokeLinecap="round"
176
- strokeLinejoin="round"
177
- >
178
- <line x1="6" y1="3" x2="6" y2="15"></line>
179
- <circle cx="18" cy="6" r="3"></circle>
180
- <circle cx="6" cy="18" r="3"></circle>
181
- <path d="M18 9a9 9 0 0 1-9 9"></path>
182
- </svg>
183
- Git
184
- </span>
185
- )}
169
+ <line x1="6" y1="3" x2="6" y2="15"></line>
170
+ <circle cx="18" cy="6" r="3"></circle>
171
+ <circle cx="6" cy="18" r="3"></circle>
172
+ <path d="M18 9a9 9 0 0 1-9 9"></path>
173
+ </svg>
174
+ Git
175
+ </span>
176
+ )}
186
177
  <button
187
178
  className="namespace-add-button"
188
179
  onClick={e => {
@@ -289,7 +280,7 @@ const Explorer = ({
289
280
  item={item}
290
281
  current={highlight}
291
282
  isTopLevel={false}
292
- namespaceSources={namespaceSources}
283
+ gitRoots={gitRoots}
293
284
  />
294
285
  </div>
295
286
  </div>
@@ -8,6 +8,7 @@ import userEvent from '@testing-library/user-event';
8
8
 
9
9
  const mockDjClient = {
10
10
  namespaces: jest.fn(),
11
+ listNamespacesWithGit: jest.fn(),
11
12
  namespace: jest.fn(),
12
13
  listNodesForLanding: jest.fn(),
13
14
  addNamespace: jest.fn(),
@@ -81,40 +82,18 @@ describe('NamespacePage', () => {
81
82
  mockDjClient.getNamespaceBranches.mockResolvedValue([]);
82
83
  mockDjClient.listDeployments.mockResolvedValue([]);
83
84
  mockDjClient.getPullRequest.mockResolvedValue(null);
84
- mockDjClient.namespaces.mockResolvedValue([
85
- {
86
- namespace: 'common.one',
87
- num_nodes: 3,
88
- },
89
- {
90
- namespace: 'common.one.a',
91
- num_nodes: 6,
92
- },
93
- {
94
- namespace: 'common.one.b',
95
- num_nodes: 17,
96
- },
97
- {
98
- namespace: 'common.one.c',
99
- num_nodes: 64,
100
- },
101
- {
102
- namespace: 'default',
103
- num_nodes: 41,
104
- },
105
- {
106
- namespace: 'default.fruits',
107
- num_nodes: 1,
108
- },
109
- {
110
- namespace: 'default.fruits.citrus.lemons',
111
- num_nodes: 1,
112
- },
113
- {
114
- namespace: 'default.vegetables',
115
- num_nodes: 2,
116
- },
117
- ]);
85
+ const mockNamespaces = [
86
+ { namespace: 'common.one', numNodes: 3, git: null },
87
+ { namespace: 'common.one.a', numNodes: 6, git: null },
88
+ { namespace: 'common.one.b', numNodes: 17, git: null },
89
+ { namespace: 'common.one.c', numNodes: 64, git: null },
90
+ { namespace: 'default', numNodes: 41, git: null },
91
+ { namespace: 'default.fruits', numNodes: 1, git: null },
92
+ { namespace: 'default.fruits.citrus.lemons', numNodes: 1, git: null },
93
+ { namespace: 'default.vegetables', numNodes: 2, git: null },
94
+ ];
95
+ mockDjClient.namespaces.mockResolvedValue(mockNamespaces);
96
+ mockDjClient.listNamespacesWithGit.mockResolvedValue(mockNamespaces);
118
97
  mockDjClient.namespace.mockResolvedValue([
119
98
  {
120
99
  name: 'testNode',
@@ -219,7 +219,15 @@ export function NamespacePage() {
219
219
  const ASC = 'ascending';
220
220
  const DESC = 'descending';
221
221
 
222
- const fields = ['name', 'displayName', 'type', 'status', 'mode', 'updatedAt'];
222
+ const fields = [
223
+ 'name',
224
+ 'displayName',
225
+ 'type',
226
+ 'status',
227
+ 'mode',
228
+ 'owners',
229
+ 'updatedAt',
230
+ ];
223
231
 
224
232
  const djClient = useContext(DJClientContext).DataJunctionAPI;
225
233
  const { currentUser } = useCurrentUser();
@@ -368,7 +376,7 @@ export function NamespacePage() {
368
376
  const [retrieved, setRetrieved] = useState(false);
369
377
 
370
378
  const [namespaceHierarchy, setNamespaceHierarchy] = useState([]);
371
- const [namespaceSources, setNamespaceSources] = useState({});
379
+ const [gitRoots, setGitRoots] = useState(new Set());
372
380
  // Use undefined to indicate "not yet loaded", null means "loaded but no config"
373
381
  const [gitConfig, setGitConfig] = useState(undefined);
374
382
 
@@ -486,6 +494,7 @@ export function NamespacePage() {
486
494
  path: path,
487
495
  };
488
496
  currentLevel.push(existingNamespace);
497
+ currentLevel.sort((a, b) => a.namespace.localeCompare(b.namespace));
489
498
  }
490
499
 
491
500
  currentLevel = existingNamespace.children;
@@ -497,23 +506,19 @@ export function NamespacePage() {
497
506
 
498
507
  useEffect(() => {
499
508
  const fetchData = async () => {
500
- const namespaces = await djClient.namespaces();
509
+ const namespaces = await djClient.listNamespacesWithGit();
501
510
  const hierarchy = createNamespaceHierarchy(namespaces);
502
511
  setNamespaceHierarchy(hierarchy);
503
512
 
504
- // Fetch sources for all namespaces in bulk
505
- const allNamespaceNames = namespaces.map(ns => ns.namespace);
506
- if (allNamespaceNames.length > 0) {
507
- const sourcesResponse = await djClient.namespaceSourcesBulk(
508
- allNamespaceNames,
509
- );
510
- if (sourcesResponse && sourcesResponse.sources) {
511
- setNamespaceSources(sourcesResponse.sources);
512
- }
513
- }
513
+ const roots = new Set(
514
+ namespaces
515
+ .filter(ns => ns.git?.__typename === 'GitRootConfig')
516
+ .map(ns => ns.namespace),
517
+ );
518
+ setGitRoots(roots);
514
519
  };
515
520
  fetchData().catch(console.error);
516
- }, [djClient, djClient.namespaces]);
521
+ }, [djClient]);
517
522
 
518
523
  useEffect(() => {
519
524
  const fetchData = async () => {
@@ -620,7 +625,14 @@ export function NamespacePage() {
620
625
  state.nodes.length > 0 ? (
621
626
  state.nodes.map(node => (
622
627
  <tr key={node.name}>
623
- <td>
628
+ <td
629
+ style={{
630
+ maxWidth: '300px',
631
+ overflow: 'hidden',
632
+ whiteSpace: 'nowrap',
633
+ textOverflow: 'ellipsis',
634
+ }}
635
+ >
624
636
  <a href={'/nodes/' + node.name} className="link-table">
625
637
  {isBranchNamespace && node.name.startsWith(namespace + '.')
626
638
  ? node.name.slice(namespace.length + 1)
@@ -633,7 +645,14 @@ export function NamespacePage() {
633
645
  {node.currentVersion}
634
646
  </span>
635
647
  </td>
636
- <td>
648
+ <td
649
+ style={{
650
+ maxWidth: '250px',
651
+ overflow: 'hidden',
652
+ whiteSpace: 'nowrap',
653
+ textOverflow: 'ellipsis',
654
+ }}
655
+ >
637
656
  <a href={'/nodes/' + node.name} className="link-table">
638
657
  {node.type !== 'source' ? node.current.displayName : ''}
639
658
  </a>
@@ -673,9 +692,44 @@ export function NamespacePage() {
673
692
  {node.current.mode === 'PUBLISHED' ? 'P' : 'D'}
674
693
  </span>
675
694
  </td>
695
+ <td>
696
+ {node.owners?.length > 0 && (
697
+ <div style={{ display: 'flex', gap: '2px' }}>
698
+ {node.owners.slice(0, 3).map(owner => {
699
+ const initials = owner.username
700
+ .split('@')[0]
701
+ .slice(0, 2)
702
+ .toUpperCase();
703
+ const [bg, fg] =
704
+ AVATAR_COLORS[avatarColorIndex(owner.username)];
705
+ return (
706
+ <span
707
+ key={owner.username}
708
+ title={owner.username}
709
+ style={{
710
+ display: 'inline-flex',
711
+ alignItems: 'center',
712
+ justifyContent: 'center',
713
+ width: '24px',
714
+ height: '24px',
715
+ borderRadius: '50%',
716
+ backgroundColor: bg,
717
+ color: fg,
718
+ fontSize: '9px',
719
+ fontWeight: '600',
720
+ flexShrink: 0,
721
+ }}
722
+ >
723
+ {initials}
724
+ </span>
725
+ );
726
+ })}
727
+ </div>
728
+ )}
729
+ </td>
676
730
  <td>
677
731
  <span className="status">
678
- {new Date(node.current.updatedAt).toLocaleString('en-us')}
732
+ {new Date(node.current.updatedAt).toLocaleDateString('en-us')}
679
733
  </span>
680
734
  </td>
681
735
  {showEditControls && (
@@ -687,7 +741,7 @@ export function NamespacePage() {
687
741
  ))
688
742
  ) : (
689
743
  <tr>
690
- <td colSpan={7}>
744
+ <td colSpan={8}>
691
745
  <span
692
746
  style={{
693
747
  display: 'block',
@@ -1075,7 +1129,7 @@ export function NamespacePage() {
1075
1129
  defaultExpand={true}
1076
1130
  isTopLevel={true}
1077
1131
  key={child.namespace}
1078
- namespaceSources={namespaceSources}
1132
+ gitRoots={gitRoots}
1079
1133
  />
1080
1134
  ))
1081
1135
  : null}
@@ -1123,6 +1123,44 @@ export const DataJunctionAPI = {
1123
1123
  ).json();
1124
1124
  },
1125
1125
 
1126
+ listNamespacesWithGit: async function () {
1127
+ const query = `
1128
+ query ListNamespaces {
1129
+ listNamespaces {
1130
+ namespace
1131
+ numNodes
1132
+ git {
1133
+ __typename
1134
+ ... on GitRootConfig {
1135
+ repo
1136
+ path
1137
+ defaultBranch
1138
+ }
1139
+ ... on GitBranchConfig {
1140
+ branch
1141
+ gitOnly
1142
+ parentNamespace
1143
+ root {
1144
+ repo
1145
+ path
1146
+ defaultBranch
1147
+ }
1148
+ }
1149
+ }
1150
+ }
1151
+ }
1152
+ `;
1153
+ const result = await (
1154
+ await fetch(DJ_GQL, {
1155
+ method: 'POST',
1156
+ headers: { 'Content-Type': 'application/json' },
1157
+ credentials: 'include',
1158
+ body: JSON.stringify({ query }),
1159
+ })
1160
+ ).json();
1161
+ return result?.data?.listNamespaces || [];
1162
+ },
1163
+
1126
1164
  namespaceSources: async function (namespace) {
1127
1165
  return await (
1128
1166
  await fetch(`${DJ_URL}/namespaces/${namespace}/sources`, {