datajunction-ui 0.0.153 → 0.0.154
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 +1 -1
- package/src/app/components/NamespaceHeader.jsx +71 -29
- package/src/app/components/__tests__/NamespaceHeader.test.jsx +10 -0
- package/src/app/pages/NodePage/NodeColumnTab.jsx +42 -34
- package/src/app/pages/NodePage/NodeMaterializationTab.jsx +23 -14
- package/src/app/pages/NodePage/index.jsx +48 -2
package/package.json
CHANGED
|
@@ -75,30 +75,48 @@ export default function NamespaceHeader({
|
|
|
75
75
|
onGitConfigLoaded(config);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
// If this is a branch
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
831
|
-
|
|
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
|
-
{
|
|
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={
|
|
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={
|
|
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',
|
|
@@ -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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
283
|
+
)}
|
|
262
284
|
</td>
|
|
263
285
|
) : (
|
|
264
286
|
''
|
|
265
287
|
)}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
<
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
401
|
-
<
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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 =
|
|
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
|
|
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 />
|