datajunction-ui 0.0.153 → 0.0.155

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.153",
3
+ "version": "0.0.155",
4
4
  "description": "DataJunction UI",
5
5
  "module": "src/index.tsx",
6
6
  "repository": {
@@ -75,30 +75,48 @@ export default function NamespaceHeader({
75
75
  onGitConfigLoaded(config);
76
76
  }
77
77
 
78
- // If this is a branch namespace, fetch parent's git config, branches, and check for existing PR
79
- if (config?.parent_namespace) {
80
- try {
81
- const parentConfig = await djClient.getNamespaceGitConfig(
82
- config.parent_namespace,
83
- );
84
- setParentGitConfig(parentConfig);
85
- } catch (e) {
86
- console.error('Failed to fetch parent git config:', e);
87
- }
78
+ // If this namespace is inside a branch (or IS the branch), fetch
79
+ // parent/branches/PR.
80
+ const branchNs = config?.branch_namespace;
81
+ if (branchNs) {
82
+ // The branch namespace itself carries the parent_namespace FK to
83
+ // the git root. If this is a subnamespace, fetch the branch's
84
+ // config to get that FK; otherwise the current config already
85
+ // has it.
86
+ const branchConfig =
87
+ branchNs === namespace
88
+ ? config
89
+ : await djClient
90
+ .getNamespaceGitConfig(branchNs)
91
+ .catch(() => null);
92
+ const gitRootNs = branchConfig?.parent_namespace;
88
93
 
89
- try {
90
- const branchList = await djClient.getNamespaceBranches(
91
- config.parent_namespace,
92
- );
93
- setBranches(branchList || []);
94
- } catch (e) {
95
- console.error('Failed to fetch branches:', e);
94
+ if (gitRootNs) {
95
+ try {
96
+ const parentConfig = await djClient.getNamespaceGitConfig(
97
+ gitRootNs,
98
+ );
99
+ setParentGitConfig(parentConfig);
100
+ } catch (e) {
101
+ console.error('Failed to fetch parent git config:', e);
102
+ }
103
+
104
+ try {
105
+ const branchList = await djClient.getNamespaceBranches(
106
+ gitRootNs,
107
+ );
108
+ setBranches(branchList || []);
109
+ } catch (e) {
110
+ console.error('Failed to fetch branches:', e);
111
+ }
96
112
  }
97
113
 
98
- // Check for existing PR
114
+ // Check for existing PR scoped to the branch, not the current
115
+ // (sub)namespace — a branch has at most one PR regardless of
116
+ // which subnamespace page the user is on.
99
117
  setPrLoading(true);
100
118
  try {
101
- const pr = await djClient.getPullRequest(namespace);
119
+ const pr = await djClient.getPullRequest(branchNs);
102
120
  setExistingPR(pr);
103
121
  } catch (e) {
104
122
  // No PR or error - that's fine
@@ -146,7 +164,16 @@ export default function NamespaceHeader({
146
164
  const hasGitConfig =
147
165
  gitConfig?.github_repo_path ||
148
166
  (gitConfig?.parent_namespace && gitConfig?.git_branch);
149
- const isBranchNamespace = !!gitConfig?.parent_namespace;
167
+ // ``branch_namespace`` is the branch this namespace belongs to (equals the
168
+ // namespace when called against the branch itself, the ancestor branch when
169
+ // called against a descendant).
170
+ // isBranchNamespace = "this IS the branch" (used for the breadcrumb branch
171
+ // switcher); isInBranch = "this lives under a branch" (used to show
172
+ // branch-scoped git controls on subnamespaces too).
173
+ const branchNamespace = gitConfig?.branch_namespace || null;
174
+ const isBranchNamespace = branchNamespace === namespace;
175
+ const isInBranch = !!branchNamespace;
176
+ const branchScopeNamespace = branchNamespace || namespace;
150
177
  const isGitRoot = gitConfig?.github_repo_path && !gitConfig?.parent_namespace;
151
178
  const canCreateBranches = isGitRoot && gitConfig?.default_branch;
152
179
 
@@ -169,21 +196,33 @@ export default function NamespaceHeader({
169
196
  return await djClient.createBranch(namespace, branchName);
170
197
  };
171
198
 
199
+ // Sync/PR operations always target the whole branch namespace, even
200
+ // when invoked from a subnamespace page.
172
201
  const handleSyncToGit = async commitMessage => {
173
- return await djClient.syncNamespaceToGit(namespace, commitMessage);
202
+ return await djClient.syncNamespaceToGit(
203
+ branchScopeNamespace,
204
+ commitMessage,
205
+ );
174
206
  };
175
207
 
176
208
  const handleCreatePR = async (title, body, onProgress) => {
177
209
  // First sync changes to git using PR title as commit message
178
210
  if (onProgress) onProgress('syncing');
179
- const syncResult = await djClient.syncNamespaceToGit(namespace, title);
211
+ const syncResult = await djClient.syncNamespaceToGit(
212
+ branchScopeNamespace,
213
+ title,
214
+ );
180
215
  if (syncResult?._error) {
181
216
  return syncResult;
182
217
  }
183
218
 
184
219
  // Then create the PR
185
220
  if (onProgress) onProgress('creating');
186
- const result = await djClient.createPullRequest(namespace, title, body);
221
+ const result = await djClient.createPullRequest(
222
+ branchScopeNamespace,
223
+ title,
224
+ body,
225
+ );
187
226
  if (result && !result._error) {
188
227
  setExistingPR(result);
189
228
  }
@@ -827,8 +866,11 @@ export default function NamespaceHeader({
827
866
 
828
867
  {/* Right side: git actions + children */}
829
868
  <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
830
- {/* Git controls for non-branch namespaces */}
831
- {namespace && !isBranchNamespace && !gitConfigLoading && (
869
+ {/* Git controls for namespaces not inside a branch (git roots,
870
+ orphan namespaces). Subnamespaces under a branch fall through
871
+ to the branch-scoped block below so we don't render two sets
872
+ of buttons. */}
873
+ {namespace && !isInBranch && !gitConfigLoading && (
832
874
  <>
833
875
  <button
834
876
  style={buttonStyle}
@@ -878,8 +920,8 @@ export default function NamespaceHeader({
878
920
  </>
879
921
  )}
880
922
 
881
- {/* Git controls for branch namespaces */}
882
- {isBranchNamespace && hasGitConfig && (
923
+ {/* Git controls for branch namespaces (and subnamespaces under them) */}
924
+ {isInBranch && hasGitConfig && (
883
925
  <>
884
926
  <button
885
927
  style={buttonStyle}
@@ -1063,7 +1105,7 @@ export default function NamespaceHeader({
1063
1105
  isOpen={showSyncToGit}
1064
1106
  onClose={() => setShowSyncToGit(false)}
1065
1107
  onSync={handleSyncToGit}
1066
- namespace={namespace}
1108
+ namespace={branchScopeNamespace}
1067
1109
  gitBranch={gitConfig?.git_branch}
1068
1110
  repoPath={gitConfig?.github_repo_path}
1069
1111
  />
@@ -1072,7 +1114,7 @@ export default function NamespaceHeader({
1072
1114
  isOpen={showCreatePR}
1073
1115
  onClose={() => setShowCreatePR(false)}
1074
1116
  onCreate={handleCreatePR}
1075
- namespace={namespace}
1117
+ namespace={branchScopeNamespace}
1076
1118
  gitBranch={gitConfig?.git_branch}
1077
1119
  parentBranch={
1078
1120
  parentGitConfig?.git_branch || parentGitConfig?.default_branch
@@ -590,6 +590,7 @@ describe('<NamespaceHeader />', () => {
590
590
  git_path: 'nodes/',
591
591
  git_only: false,
592
592
  parent_namespace: 'test.main',
593
+ branch_namespace: 'test.feature',
593
594
  })
594
595
  .mockResolvedValueOnce({
595
596
  github_repo_path: 'test/repo',
@@ -672,6 +673,7 @@ describe('<NamespaceHeader />', () => {
672
673
  git_path: 'nodes/',
673
674
  git_only: false,
674
675
  parent_namespace: 'test.main',
676
+ branch_namespace: 'test.feature',
675
677
  })
676
678
  .mockResolvedValueOnce({
677
679
  github_repo_path: 'test/repo',
@@ -830,6 +832,7 @@ describe('<NamespaceHeader />', () => {
830
832
  git_path: 'nodes/',
831
833
  git_only: false,
832
834
  parent_namespace: 'test.main',
835
+ branch_namespace: 'test.feature',
833
836
  })
834
837
  .mockResolvedValueOnce({
835
838
  github_repo_path: 'test/repo',
@@ -893,6 +896,7 @@ describe('<NamespaceHeader />', () => {
893
896
  git_path: 'nodes/',
894
897
  git_only: false,
895
898
  parent_namespace: 'test.main',
899
+ branch_namespace: 'test.feature',
896
900
  }),
897
901
  getPullRequest: jest.fn().mockResolvedValue({
898
902
  pr_number: 42,
@@ -932,6 +936,7 @@ describe('<NamespaceHeader />', () => {
932
936
  git_path: 'nodes/',
933
937
  git_only: false,
934
938
  parent_namespace: 'test.main',
939
+ branch_namespace: 'test.feature',
935
940
  })
936
941
  .mockResolvedValueOnce({
937
942
  github_repo_path: 'test/repo',
@@ -1019,6 +1024,7 @@ describe('<NamespaceHeader />', () => {
1019
1024
  git_path: 'nodes/',
1020
1025
  git_only: false,
1021
1026
  parent_namespace: 'test.main',
1027
+ branch_namespace: 'test.feature',
1022
1028
  })
1023
1029
  .mockResolvedValueOnce({
1024
1030
  github_repo_path: 'test/repo',
@@ -1087,6 +1093,7 @@ describe('<NamespaceHeader />', () => {
1087
1093
  git_path: 'nodes/',
1088
1094
  git_only: false,
1089
1095
  parent_namespace: 'test.main',
1096
+ branch_namespace: 'test.feature',
1090
1097
  })
1091
1098
  .mockResolvedValueOnce({
1092
1099
  github_repo_path: 'test/repo',
@@ -1142,6 +1149,7 @@ describe('<NamespaceHeader />', () => {
1142
1149
  git_path: 'nodes/',
1143
1150
  git_only: false,
1144
1151
  parent_namespace: 'test.main',
1152
+ branch_namespace: 'test.feature',
1145
1153
  })
1146
1154
  .mockRejectedValueOnce(new Error('Parent not found')),
1147
1155
  getPullRequest: jest.fn().mockResolvedValue(null),
@@ -1184,6 +1192,7 @@ describe('<NamespaceHeader />', () => {
1184
1192
  git_path: 'nodes/',
1185
1193
  git_only: false,
1186
1194
  parent_namespace: 'test.main',
1195
+ branch_namespace: 'test.feature',
1187
1196
  })
1188
1197
  .mockResolvedValueOnce({
1189
1198
  github_repo_path: 'test/repo',
@@ -1339,6 +1348,7 @@ describe('<NamespaceHeader />', () => {
1339
1348
  git_path: 'nodes/',
1340
1349
  git_only: false,
1341
1350
  parent_namespace: 'test.main',
1351
+ branch_namespace: 'test.feature',
1342
1352
  })
1343
1353
  .mockResolvedValueOnce({
1344
1354
  github_repo_path: 'test/repo',
package/src/app/index.tsx CHANGED
@@ -10,6 +10,7 @@ import { BrowserRouter, Routes, Route } from 'react-router-dom';
10
10
  import { NamespacePage } from './pages/NamespacePage/Loadable';
11
11
  import { MyWorkspacePage } from './pages/MyWorkspacePage/Loadable';
12
12
  import { OverviewPage } from './pages/OverviewPage/Loadable';
13
+ import { SystemMetricsExplorerPage } from './pages/SystemMetricsExplorerPage/Loadable';
13
14
  import { SettingsPage } from './pages/SettingsPage/Loadable';
14
15
  import { NotificationsPage } from './pages/NotificationsPage/Loadable';
15
16
  import { NodePage } from './pages/NodePage/Loadable';
@@ -153,6 +154,11 @@ export function App() {
153
154
  key="overview"
154
155
  element={<OverviewPage />}
155
156
  />
157
+ <Route
158
+ path="overview/explore"
159
+ key="overview-explore"
160
+ element={<SystemMetricsExplorerPage />}
161
+ />
156
162
  <Route
157
163
  path="workspace"
158
164
  key="workspace"
@@ -9,7 +9,7 @@ import PartitionColumnPopover from './PartitionColumnPopover';
9
9
  import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
10
10
  import { foundation } from 'react-syntax-highlighter/src/styles/hljs';
11
11
 
12
- export default function NodeColumnTab({ node, djClient }) {
12
+ export default function NodeColumnTab({ node, djClient, readOnly = false }) {
13
13
  const [attributes, setAttributes] = useState([]);
14
14
  const [dimensions, setDimensions] = useState([]);
15
15
  const [columns, setColumns] = useState([]);
@@ -182,14 +182,16 @@ export default function NodeColumnTab({ node, djClient }) {
182
182
  aria-hidden="false"
183
183
  >
184
184
  {col.description || ''}
185
- <EditColumnDescriptionPopover
186
- column={col}
187
- node={node}
188
- onSubmit={async () => {
189
- const res = await djClient.node(node.name);
190
- setColumns(res.columns);
191
- }}
192
- />
185
+ {!readOnly && (
186
+ <EditColumnDescriptionPopover
187
+ column={col}
188
+ node={node}
189
+ onSubmit={async () => {
190
+ const res = await djClient.node(node.name);
191
+ setColumns(res.columns);
192
+ }}
193
+ />
194
+ )}
193
195
  </span>
194
196
  </td>
195
197
  <td>
@@ -246,49 +248,55 @@ export default function NodeColumnTab({ node, djClient }) {
246
248
  </a>
247
249
  </span>
248
250
  )}
249
- <ManageDimensionLinksDialog
251
+ {!readOnly && (
252
+ <ManageDimensionLinksDialog
253
+ column={col}
254
+ node={node}
255
+ dimensions={dimensions}
256
+ fkLinks={fkLinksForColumn}
257
+ referenceLink={referenceLink}
258
+ onSubmit={async () => {
259
+ const res = await djClient.node(node.name);
260
+ setLinks(res.dimension_links);
261
+ setColumns(res.columns);
262
+ }}
263
+ />
264
+ )}
265
+ </div>
266
+ </td>
267
+ ) : (
268
+ ''
269
+ )}
270
+ {node.type !== 'cube' ? (
271
+ <td>
272
+ {showColumnAttributes(col)}
273
+ {!readOnly && (
274
+ <EditColumnPopover
250
275
  column={col}
251
276
  node={node}
252
- dimensions={dimensions}
253
- fkLinks={fkLinksForColumn}
254
- referenceLink={referenceLink}
277
+ options={attributes}
255
278
  onSubmit={async () => {
256
279
  const res = await djClient.node(node.name);
257
- setLinks(res.dimension_links);
258
280
  setColumns(res.columns);
259
281
  }}
260
282
  />
261
- </div>
283
+ )}
262
284
  </td>
263
285
  ) : (
264
286
  ''
265
287
  )}
266
- {node.type !== 'cube' ? (
267
- <td>
268
- {showColumnAttributes(col)}
269
- <EditColumnPopover
288
+ <td>
289
+ {showColumnPartition(col)}
290
+ {!readOnly && (
291
+ <PartitionColumnPopover
270
292
  column={col}
271
293
  node={node}
272
- options={attributes}
273
294
  onSubmit={async () => {
274
295
  const res = await djClient.node(node.name);
275
296
  setColumns(res.columns);
276
297
  }}
277
298
  />
278
- </td>
279
- ) : (
280
- ''
281
- )}
282
- <td>
283
- {showColumnPartition(col)}
284
- <PartitionColumnPopover
285
- column={col}
286
- node={node}
287
- onSubmit={async () => {
288
- const res = await djClient.node(node.name);
289
- setColumns(res.columns);
290
- }}
291
- />
299
+ )}
292
300
  </td>
293
301
  </tr>
294
302
  );
@@ -16,7 +16,11 @@ const cronstrue = require('cronstrue');
16
16
  * For non-cube nodes, the parent component (index.jsx) renders
17
17
  * NodePreAggregationsTab instead.
18
18
  */
19
- export default function NodeMaterializationTab({ node, djClient }) {
19
+ export default function NodeMaterializationTab({
20
+ node,
21
+ djClient,
22
+ readOnly = false,
23
+ }) {
20
24
  const [rawMaterializations, setRawMaterializations] = useState([]);
21
25
  const [selectedRevisionTab, setSelectedRevisionTab] = useState(null);
22
26
  const [showInactive, setShowInactive] = useState(false);
@@ -179,7 +183,7 @@ export default function NodeMaterializationTab({ node, djClient }) {
179
183
  Show Inactive
180
184
  </label>
181
185
  )}
182
- {node && <AddMaterializationPopover node={node} />}
186
+ {node && !readOnly && <AddMaterializationPopover node={node} />}
183
187
  </div>
184
188
  </div>
185
189
  );
@@ -282,6 +286,7 @@ export default function NodeMaterializationTab({ node, djClient }) {
282
286
  Show Inactive
283
287
  </label>
284
288
  {node &&
289
+ !readOnly &&
285
290
  (hasLatestVersionMaterialization ? (
286
291
  <button
287
292
  className="edit_button"
@@ -322,11 +327,13 @@ export default function NodeMaterializationTab({ node, djClient }) {
322
327
  .join(' ')}
323
328
  </div>
324
329
  <div className="td">
325
- <NodeMaterializationDelete
326
- nodeName={node.name}
327
- materializationName={materialization.name}
328
- nodeVersion={selectedRevisionTab}
329
- />
330
+ {!readOnly && (
331
+ <NodeMaterializationDelete
332
+ nodeName={node.name}
333
+ materializationName={materialization.name}
334
+ nodeVersion={selectedRevisionTab}
335
+ />
336
+ )}
330
337
  </div>
331
338
  <div className="td">
332
339
  <span className={`badge cron`}>{materialization.schedule}</span>
@@ -397,13 +404,15 @@ export default function NodeMaterializationTab({ node, djClient }) {
397
404
  </summary>
398
405
  {materialization.strategy === 'incremental_time' ? (
399
406
  <ul>
400
- <li>
401
- <AddBackfillPopover
402
- node={node}
403
- materialization={materialization}
404
- onSubmit={fetchData}
405
- />
406
- </li>
407
+ {!readOnly && (
408
+ <li>
409
+ <AddBackfillPopover
410
+ node={node}
411
+ materialization={materialization}
412
+ onSubmit={fetchData}
413
+ />
414
+ </li>
415
+ )}
407
416
  {materialization.backfills.map(backfill => (
408
417
  <li className="backfill">
409
418
  <div className="partitionLink">
@@ -136,7 +136,13 @@ export function NodePage() {
136
136
  tabToDisplay = node ? <NodeInfoTab node={node} /> : '';
137
137
  break;
138
138
  case 'columns':
139
- tabToDisplay = <NodeColumnTab node={node} djClient={djClient} />;
139
+ tabToDisplay = (
140
+ <NodeColumnTab
141
+ node={node}
142
+ djClient={djClient}
143
+ readOnly={!!gitConfig?.git_only}
144
+ />
145
+ );
140
146
  break;
141
147
  case 'data-flow':
142
148
  tabToDisplay = <NodeDataFlowTab djNode={node} />;
@@ -152,7 +158,11 @@ export function NodePage() {
152
158
  // Other nodes (transform, metric, dimension) use pre-aggregations tab
153
159
  tabToDisplay =
154
160
  node?.type === 'cube' ? (
155
- <NodeMaterializationTab node={node} djClient={djClient} />
161
+ <NodeMaterializationTab
162
+ node={node}
163
+ djClient={djClient}
164
+ readOnly={!!gitConfig?.git_only}
165
+ />
156
166
  ) : (
157
167
  <NodePreAggregationsTab node={node} />
158
168
  );
@@ -253,6 +263,42 @@ export function NodePage() {
253
263
  >
254
264
  {node?.type}
255
265
  </span>
266
+ {gitConfigLoaded && isGitOnly && (
267
+ <span
268
+ style={{
269
+ display: 'inline-flex',
270
+ alignItems: 'center',
271
+ marginLeft: '4px',
272
+ color: '#92400e',
273
+ verticalAlign: 'middle',
274
+ cursor: 'help',
275
+ }}
276
+ aria-label="ReadOnly"
277
+ title="Read-only — edits must go through git."
278
+ >
279
+ <svg
280
+ xmlns="http://www.w3.org/2000/svg"
281
+ width="14"
282
+ height="14"
283
+ viewBox="0 0 24 24"
284
+ fill="none"
285
+ stroke="currentColor"
286
+ strokeWidth="2"
287
+ strokeLinecap="round"
288
+ strokeLinejoin="round"
289
+ >
290
+ <rect
291
+ x="3"
292
+ y="11"
293
+ width="18"
294
+ height="11"
295
+ rx="2"
296
+ ry="2"
297
+ />
298
+ <path d="M7 11V7a5 5 0 0 1 10 0v4" />
299
+ </svg>
300
+ </span>
301
+ )}
256
302
  </span>
257
303
  </h3>
258
304
  <NodeButtons />
@@ -7,6 +7,17 @@ import { DimensionNodeUsagePanel } from './DimensionNodeUsagePanel';
7
7
  export function OverviewPage() {
8
8
  return (
9
9
  <div className="mid">
10
+ <div
11
+ style={{
12
+ display: 'flex',
13
+ justifyContent: 'flex-end',
14
+ padding: '0.5rem 1rem',
15
+ }}
16
+ >
17
+ <a href="/overview/explore" className="button-3 neutral-button">
18
+ Explore system metrics →
19
+ </a>
20
+ </div>
10
21
  <div className="chart-container">
11
22
  <OverviewPanel />
12
23
  <NodesByTypePanel />
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Asynchronously loads the component for the System Metrics Explorer page
3
+ */
4
+
5
+ import * as React from 'react';
6
+ import { lazyLoad } from '../../../utils/loadable';
7
+
8
+ export const SystemMetricsExplorerPage = props => {
9
+ return lazyLoad(
10
+ () => import('./index'),
11
+ module => module.SystemMetricsExplorerPage,
12
+ {
13
+ fallback: <div></div>,
14
+ },
15
+ )(props);
16
+ };