datajunction-ui 0.0.1-a90 → 0.0.1-a91.dev1
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/icons/WrenchIcon.jsx +36 -0
- package/src/app/pages/AddEditNodePage/ColumnMetadata.jsx +61 -0
- package/src/app/pages/AddEditNodePage/ColumnsMetadataInput.jsx +72 -0
- package/src/app/pages/AddEditNodePage/ExperimentationExtension.jsx +338 -0
- package/src/app/pages/NodePage/EditColumnDescriptionPopover.jsx +116 -0
- package/src/app/pages/NodePage/LinkComplexDimensionPopover.jsx +141 -0
- package/src/app/pages/NodePage/NodeColumnTab.jsx +20 -0
- package/src/app/pages/NodePage/__tests__/EditColumnDescriptionPopover.test.jsx +149 -0
- package/src/app/services/DJService.js +13 -0
package/package.json
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const WrenchIcon = props => (
|
|
2
|
+
<svg
|
|
3
|
+
width="16"
|
|
4
|
+
height="17"
|
|
5
|
+
viewBox="0 0 16 17"
|
|
6
|
+
fill="none"
|
|
7
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
8
|
+
>
|
|
9
|
+
<g clip-path="url(#clip0_24_77)">
|
|
10
|
+
<rect
|
|
11
|
+
width="16"
|
|
12
|
+
height="16"
|
|
13
|
+
transform="translate(0 0.649803)"
|
|
14
|
+
fill="white"
|
|
15
|
+
/>
|
|
16
|
+
<path
|
|
17
|
+
fill-rule="evenodd"
|
|
18
|
+
clip-rule="evenodd"
|
|
19
|
+
d="M6.41665 5.44985C6.41665 2.8265 8.5433 0.699852 11.1667 0.699852C11.4613 0.699852 11.7504 0.726773 12.0313 0.778484C12.5762 0.878798 12.9364 1.28101 13.0406 1.74377C13.1406 2.18774 13.0066 2.67055 12.6619 3.01531L11.2273 4.44985L12.1667 5.38919L13.6012 3.95465C13.946 3.60989 14.4288 3.47589 14.8728 3.57591C15.3355 3.68017 15.7377 4.04033 15.838 4.58525C15.8898 4.86615 15.9167 5.15519 15.9167 5.44985C15.9167 8.0732 13.79 10.1999 11.1667 10.1999C10.658 10.1999 10.167 10.1196 9.70625 9.97091L3.50172 16.1754C2.94847 16.7287 2.05149 16.7287 1.49825 16.1754L0.441055 15.1183C-0.112189 14.565 -0.112186 13.668 0.441056 13.1148L6.64559 6.91025C6.49685 6.44951 6.41665 5.9585 6.41665 5.44985ZM11.1667 2.19985C9.37172 2.19985 7.91665 3.65492 7.91665 5.44985C7.91665 5.92812 8.01948 6.38034 8.20352 6.7873L8.41725 7.25991L8.05048 7.62667L1.56064 14.1165L2.49998 15.0559L8.98982 8.56601L9.35659 8.19925L9.8292 8.41298C10.2362 8.59702 10.6884 8.69985 11.1667 8.69985C12.9616 8.69985 14.4167 7.24477 14.4167 5.44985C14.4167 5.38796 14.415 5.32654 14.4116 5.26562L12.697 6.98018L12.1667 7.51051L11.6363 6.98018L9.63632 4.98018L9.10599 4.44985L9.63632 3.91952L11.3509 2.20496C11.29 2.20157 11.2286 2.19985 11.1667 2.19985Z"
|
|
20
|
+
fill="black"
|
|
21
|
+
fill-opacity="0.9"
|
|
22
|
+
/>
|
|
23
|
+
</g>
|
|
24
|
+
<defs>
|
|
25
|
+
<clipPath id="clip0_24_77">
|
|
26
|
+
<rect
|
|
27
|
+
width="16"
|
|
28
|
+
height="16"
|
|
29
|
+
fill="white"
|
|
30
|
+
transform="translate(0 0.649803)"
|
|
31
|
+
/>
|
|
32
|
+
</clipPath>
|
|
33
|
+
</defs>
|
|
34
|
+
</svg>
|
|
35
|
+
);
|
|
36
|
+
export default WrenchIcon;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component for arbitrary column metadata
|
|
3
|
+
*/
|
|
4
|
+
import { ErrorMessage, useFormikContext } from 'formik';
|
|
5
|
+
import { useContext, useMemo, useState } from 'react';
|
|
6
|
+
import DJClientContext from '../../providers/djclient';
|
|
7
|
+
import { FormikSelect } from './FormikSelect';
|
|
8
|
+
|
|
9
|
+
export const ColumnMetadata = ({ name, label, defaultValue }) => {
|
|
10
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
11
|
+
|
|
12
|
+
// Used to pull out current form values for node validation
|
|
13
|
+
const { values } = useFormikContext();
|
|
14
|
+
|
|
15
|
+
// The available columns, determined from validating the node query
|
|
16
|
+
const [availableColumns, setAvailableColumns] = useState([]);
|
|
17
|
+
const selectableOptions = useMemo(() => {
|
|
18
|
+
if (availableColumns && availableColumns.length > 0) {
|
|
19
|
+
return availableColumns;
|
|
20
|
+
}
|
|
21
|
+
}, [availableColumns]);
|
|
22
|
+
|
|
23
|
+
// When focus is on the input field, refresh the list of available columns for selection
|
|
24
|
+
const refreshColumns = event => {
|
|
25
|
+
async function fetchData() {
|
|
26
|
+
// eslint-disable-next-line no-unused-vars
|
|
27
|
+
const { status, json } = await djClient.validateNode(
|
|
28
|
+
values.type,
|
|
29
|
+
values.name,
|
|
30
|
+
values.display_name,
|
|
31
|
+
values.description,
|
|
32
|
+
values.query,
|
|
33
|
+
);
|
|
34
|
+
setAvailableColumns(
|
|
35
|
+
json.columns.map(col => {
|
|
36
|
+
return { value: col.name, label: col.name };
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
fetchData();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="NodeCreationInput" style={{ width: '25%' }}>
|
|
45
|
+
<ErrorMessage name={name} component="span" />
|
|
46
|
+
<label htmlFor="react-select-3-input">{label}</label>
|
|
47
|
+
<span data-testid={`select-${name}`}>
|
|
48
|
+
<FormikSelect
|
|
49
|
+
className="SelectInput"
|
|
50
|
+
style={{ width: '100px' }}
|
|
51
|
+
defaultValue={defaultValue}
|
|
52
|
+
selectOptions={selectableOptions}
|
|
53
|
+
formikFieldName={name}
|
|
54
|
+
// placeholder={`Choose ${label}`}
|
|
55
|
+
onFocus={event => refreshColumns(event)}
|
|
56
|
+
isMulti={false}
|
|
57
|
+
/>
|
|
58
|
+
</span>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Column metadata configuration component
|
|
3
|
+
*/
|
|
4
|
+
import { ErrorMessage, useFormikContext } from 'formik';
|
|
5
|
+
import { useContext, useMemo, useState, useEffect } from 'react';
|
|
6
|
+
import DJClientContext from '../../providers/djclient';
|
|
7
|
+
import { FormikSelect } from './FormikSelect';
|
|
8
|
+
import WrenchIcon from 'app/icons/WrenchIcon';
|
|
9
|
+
|
|
10
|
+
export const ColumnsMetadataInput = ({ columns }) => {
|
|
11
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
12
|
+
|
|
13
|
+
// Used to pull out current form values for node validation
|
|
14
|
+
const { values } = useFormikContext();
|
|
15
|
+
|
|
16
|
+
// The available columns, determined from validating the node query
|
|
17
|
+
const [availableColumns, setAvailableColumns] = useState([]);
|
|
18
|
+
const selectableOptions = useMemo(() => {
|
|
19
|
+
if (availableColumns && availableColumns.length > 0) {
|
|
20
|
+
return availableColumns;
|
|
21
|
+
}
|
|
22
|
+
}, [availableColumns]);
|
|
23
|
+
|
|
24
|
+
// When focus is on the primary key field, refresh the list of available
|
|
25
|
+
// primary key columns for selection
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const fetchData = async () => {
|
|
28
|
+
// eslint-disable-next-line no-unused-vars
|
|
29
|
+
const { status, json } = await djClient.validateNode(
|
|
30
|
+
values.type,
|
|
31
|
+
values.name,
|
|
32
|
+
values.display_name,
|
|
33
|
+
values.description,
|
|
34
|
+
values.query,
|
|
35
|
+
);
|
|
36
|
+
setAvailableColumns(
|
|
37
|
+
json.columns.map(col => {
|
|
38
|
+
return { value: col.name, label: col.name };
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
fetchData().catch(console.error);
|
|
43
|
+
}, [djClient, name]);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div
|
|
47
|
+
className="ColumnsMetadataInput NodeCreationInput"
|
|
48
|
+
style={{ border: 'dashed 1px #ccc', padding: '20px' }}
|
|
49
|
+
>
|
|
50
|
+
<ErrorMessage name="mode" component="span" />
|
|
51
|
+
<label htmlFor="Mode">Columns Metadata</label>
|
|
52
|
+
<table>
|
|
53
|
+
<thead>
|
|
54
|
+
<tr>
|
|
55
|
+
<th>Column</th>
|
|
56
|
+
<th>Metadata</th>
|
|
57
|
+
</tr>
|
|
58
|
+
</thead>
|
|
59
|
+
<tbody>
|
|
60
|
+
{availableColumns.map(col => (
|
|
61
|
+
<tr key={col.value}>
|
|
62
|
+
<td>{col.value}</td>
|
|
63
|
+
<td>
|
|
64
|
+
<WrenchIcon />
|
|
65
|
+
</td>
|
|
66
|
+
</tr>
|
|
67
|
+
))}
|
|
68
|
+
</tbody>
|
|
69
|
+
</table>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { useFormikContext } from 'formik';
|
|
3
|
+
import { ColumnsSelect } from 'datajunction-ui/src/app/pages/AddEditNodePage/ColumnsSelect';
|
|
4
|
+
|
|
5
|
+
const AllocationJoinHeader = () => {
|
|
6
|
+
return (
|
|
7
|
+
<div
|
|
8
|
+
style={{
|
|
9
|
+
display: 'flex',
|
|
10
|
+
alignItems: 'center',
|
|
11
|
+
justifyContent: 'space-between',
|
|
12
|
+
cursor: 'pointer',
|
|
13
|
+
fontSize: '16px',
|
|
14
|
+
fontWeight: 'bold',
|
|
15
|
+
padding: '10px',
|
|
16
|
+
borderRadius: '6px',
|
|
17
|
+
transition: 'background 0.2s',
|
|
18
|
+
}}
|
|
19
|
+
>
|
|
20
|
+
🔥 Fields to enable automatic allocation joins for use in ABlaze
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const AllocationUnitTypeHeader = ({ unit }) => {
|
|
26
|
+
return (
|
|
27
|
+
<span
|
|
28
|
+
style={{
|
|
29
|
+
backgroundColor: '#fff7ea',
|
|
30
|
+
color: '#333',
|
|
31
|
+
fontWeight: '600',
|
|
32
|
+
width: '100%',
|
|
33
|
+
borderRadius: '8px 8px 0 0',
|
|
34
|
+
padding: '15px',
|
|
35
|
+
display: 'inline-block',
|
|
36
|
+
marginBottom: '10px',
|
|
37
|
+
borderBottom: '1px solid #ffe2b2',
|
|
38
|
+
textTransform: 'capitalize',
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
Allocation Unit Type: {unit.replace('_', ' ')}
|
|
42
|
+
</span>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const AddAllocationJoin = ({ onClick }) => {
|
|
47
|
+
return (
|
|
48
|
+
<button
|
|
49
|
+
onClick={onClick}
|
|
50
|
+
style={{
|
|
51
|
+
marginTop: '10px',
|
|
52
|
+
cursor: 'pointer',
|
|
53
|
+
textTransform: 'none',
|
|
54
|
+
background: '#ffe4b2',
|
|
55
|
+
padding: '15px',
|
|
56
|
+
fontWeight: '400',
|
|
57
|
+
fontSize: '16px',
|
|
58
|
+
display: 'none',
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
➕ Add another allocation join type
|
|
62
|
+
</button>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const ExperimentationExtension = ({
|
|
67
|
+
node,
|
|
68
|
+
action,
|
|
69
|
+
registerSubmitHandler,
|
|
70
|
+
}) => {
|
|
71
|
+
const AllocationUnitType = Object.freeze({
|
|
72
|
+
ACCOUNT_ID: {
|
|
73
|
+
name: 'account_id',
|
|
74
|
+
node: 'common.dimensions.xp.allocation_day',
|
|
75
|
+
},
|
|
76
|
+
VISITOR_DEVICE_ID: { name: 'visitor_device_id', node: null },
|
|
77
|
+
PARTNER_VISITOR_DEVICE_ID: {
|
|
78
|
+
name: 'partner_visitor_device_id',
|
|
79
|
+
node: null,
|
|
80
|
+
},
|
|
81
|
+
PARTNER_ACCOUNT_ID: { name: 'partner_account_id', node: null },
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const ALLOCATION_LINK_FIELD = 'allocation_links';
|
|
85
|
+
|
|
86
|
+
const { values, setFieldValue } = useFormikContext();
|
|
87
|
+
|
|
88
|
+
const [allocationConfigFields, setAllocationConfigFields] = useState({});
|
|
89
|
+
|
|
90
|
+
// Extracts any Allocation dimension node links
|
|
91
|
+
const getAllocationLinks = node => {
|
|
92
|
+
return Object.values(AllocationUnitType)
|
|
93
|
+
.map(alloc => ({
|
|
94
|
+
...alloc,
|
|
95
|
+
link:
|
|
96
|
+
node?.dimension_links?.find(
|
|
97
|
+
link => link.dimension.name === alloc.node,
|
|
98
|
+
) || null,
|
|
99
|
+
}))
|
|
100
|
+
.filter(alloc => alloc.link !== null);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const linkAllocations = async (
|
|
104
|
+
nodeName,
|
|
105
|
+
allocationNode,
|
|
106
|
+
allocationUnit,
|
|
107
|
+
eventTimestamp,
|
|
108
|
+
) => {
|
|
109
|
+
const response = await fetch(
|
|
110
|
+
`${process.env.REACT_APP_DJ_URL}/nodes/${nodeName}/link/${allocationNode}`,
|
|
111
|
+
{
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: {
|
|
114
|
+
'Content-Type': 'application/json',
|
|
115
|
+
},
|
|
116
|
+
body: JSON.stringify({
|
|
117
|
+
event_timestamp_column: eventTimestamp,
|
|
118
|
+
account_id_column: allocationUnit,
|
|
119
|
+
}),
|
|
120
|
+
credentials: 'include',
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
return { status: response.status, json: await response.json() };
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const unlinkAllocations = async (nodeName, allocationNode) => {
|
|
127
|
+
const response = await fetch(
|
|
128
|
+
`${process.env.REACT_APP_DJ_URL}/nodes/${nodeName}/link`,
|
|
129
|
+
{
|
|
130
|
+
method: 'DELETE',
|
|
131
|
+
headers: {
|
|
132
|
+
'Content-Type': 'application/json',
|
|
133
|
+
},
|
|
134
|
+
body: JSON.stringify({ dimension_node: allocationNode }),
|
|
135
|
+
credentials: 'include',
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
return { status: response.status, json: await response.json() };
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const handleSubmit = async (values, { setSubmitting, setStatus }) => {
|
|
142
|
+
for (const [unit, fields] of Object.entries(
|
|
143
|
+
values[ALLOCATION_LINK_FIELD],
|
|
144
|
+
)) {
|
|
145
|
+
const shouldDelete = Object.values(fields).every(
|
|
146
|
+
value => value === null || value === '',
|
|
147
|
+
);
|
|
148
|
+
const linkResponse = shouldDelete
|
|
149
|
+
? await unlinkAllocations(
|
|
150
|
+
values.name,
|
|
151
|
+
AllocationUnitType[unit.toUpperCase()].node,
|
|
152
|
+
)
|
|
153
|
+
: await linkAllocations(
|
|
154
|
+
values.name,
|
|
155
|
+
AllocationUnitType[unit.toUpperCase()].node,
|
|
156
|
+
fields.allocation_unit,
|
|
157
|
+
fields.event_timestamp,
|
|
158
|
+
);
|
|
159
|
+
if (
|
|
160
|
+
linkResponse.status === 200 ||
|
|
161
|
+
linkResponse.status === 201 ||
|
|
162
|
+
linkResponse.status === 204
|
|
163
|
+
) {
|
|
164
|
+
const verbiage = shouldDelete ? 'removed the link' : 'linked it';
|
|
165
|
+
setStatus({
|
|
166
|
+
success: (
|
|
167
|
+
<>
|
|
168
|
+
Successfully {action.name}ed {values.type} node{' '}
|
|
169
|
+
<a href={`/nodes/${values.name}`}>{values.name}</a> and {verbiage}{' '}
|
|
170
|
+
with{' '}
|
|
171
|
+
<a href={`/nodes/${AllocationUnitType[unit.toUpperCase()].node}`}>
|
|
172
|
+
Allocations
|
|
173
|
+
</a>
|
|
174
|
+
!
|
|
175
|
+
</>
|
|
176
|
+
),
|
|
177
|
+
});
|
|
178
|
+
} else {
|
|
179
|
+
if (linkResponse.status === 404) {
|
|
180
|
+
setStatus({
|
|
181
|
+
failure: `The DJ API is not set up to support linking to ${
|
|
182
|
+
AllocationUnitType[unit.toUpperCase()].node
|
|
183
|
+
}.`,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Register this function so the main form can call it on submit
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
if (registerSubmitHandler) {
|
|
193
|
+
if (action.name === 'edit') {
|
|
194
|
+
registerSubmitHandler(handleSubmit, { prepend: false });
|
|
195
|
+
} else {
|
|
196
|
+
registerSubmitHandler(handleSubmit, { prepend: false });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}, []);
|
|
200
|
+
|
|
201
|
+
const getAllocationUnitColumn = link => {
|
|
202
|
+
return Object.entries(link.link.foreign_keys)
|
|
203
|
+
.find(([_, value]) => value === `${link.node}.${link.name}`)?.[0]
|
|
204
|
+
?.replace(node.name + '.', '');
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const getEventTimestampColumn = link => {
|
|
208
|
+
return Object.entries(link.link.foreign_keys)
|
|
209
|
+
.find(([_, value]) => value === null)?.[0]
|
|
210
|
+
?.replace(node.name + '.', '');
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const createColumnsSelect = (
|
|
214
|
+
fieldName,
|
|
215
|
+
label,
|
|
216
|
+
placeholder,
|
|
217
|
+
defaultValue = null,
|
|
218
|
+
) => (
|
|
219
|
+
<ColumnsSelect
|
|
220
|
+
defaultValue={defaultValue}
|
|
221
|
+
fieldName={fieldName}
|
|
222
|
+
label={label}
|
|
223
|
+
labelStyle={{
|
|
224
|
+
textTransform: 'revert',
|
|
225
|
+
fontFamily: 'inherit',
|
|
226
|
+
fontWeight: 'inherit',
|
|
227
|
+
}}
|
|
228
|
+
placeholder={placeholder}
|
|
229
|
+
isClearable={true}
|
|
230
|
+
/>
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
const allocationLinks = getAllocationLinks(node);
|
|
235
|
+
if (
|
|
236
|
+
action.name === 'add' ||
|
|
237
|
+
(node &&
|
|
238
|
+
!Array.isArray(node) &&
|
|
239
|
+
node.length !== 0 &&
|
|
240
|
+
allocationLinks.length === 0)
|
|
241
|
+
) {
|
|
242
|
+
setAllocationConfigFields(prevFields => ({
|
|
243
|
+
...prevFields,
|
|
244
|
+
[AllocationUnitType.ACCOUNT_ID.name]: {
|
|
245
|
+
allocationUnit: createColumnsSelect(
|
|
246
|
+
`${ALLOCATION_LINK_FIELD}.${AllocationUnitType.ACCOUNT_ID.name}.allocation_unit`,
|
|
247
|
+
`Column that maps to ${AllocationUnitType.ACCOUNT_ID.name.replace(
|
|
248
|
+
'_',
|
|
249
|
+
' ',
|
|
250
|
+
)}`,
|
|
251
|
+
'Choose Account ID Column',
|
|
252
|
+
),
|
|
253
|
+
eventTimestamp: createColumnsSelect(
|
|
254
|
+
`${ALLOCATION_LINK_FIELD}.${AllocationUnitType.ACCOUNT_ID.name}.event_timestamp`,
|
|
255
|
+
'Event Timestamp Column',
|
|
256
|
+
'Choose Event Timestamp Column',
|
|
257
|
+
),
|
|
258
|
+
},
|
|
259
|
+
}));
|
|
260
|
+
} else {
|
|
261
|
+
for (const link of allocationLinks) {
|
|
262
|
+
const allocationUnit = getAllocationUnitColumn(link);
|
|
263
|
+
const eventTimestamp = getEventTimestampColumn(link);
|
|
264
|
+
|
|
265
|
+
setFieldValue(
|
|
266
|
+
`${ALLOCATION_LINK_FIELD}.${link.name}.allocation_unit`,
|
|
267
|
+
allocationUnit || '',
|
|
268
|
+
false,
|
|
269
|
+
);
|
|
270
|
+
setFieldValue(
|
|
271
|
+
`${ALLOCATION_LINK_FIELD}.${link.name}.event_timestamp`,
|
|
272
|
+
eventTimestamp || '',
|
|
273
|
+
false,
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
setAllocationConfigFields(prevFields => ({
|
|
277
|
+
...prevFields,
|
|
278
|
+
[link.name]: {
|
|
279
|
+
allocationUnit: createColumnsSelect(
|
|
280
|
+
`${ALLOCATION_LINK_FIELD}.${link.name}.allocation_unit`,
|
|
281
|
+
`Column that maps to ${link.name.replace('_', ' ')}`,
|
|
282
|
+
'Choose Account ID Column',
|
|
283
|
+
allocationUnit,
|
|
284
|
+
),
|
|
285
|
+
eventTimestamp: createColumnsSelect(
|
|
286
|
+
`${ALLOCATION_LINK_FIELD}.${link.name}.event_timestamp`,
|
|
287
|
+
'Event Timestamp Column',
|
|
288
|
+
'Choose Event Timestamp Column',
|
|
289
|
+
eventTimestamp,
|
|
290
|
+
),
|
|
291
|
+
},
|
|
292
|
+
}));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}, [node]);
|
|
296
|
+
|
|
297
|
+
if (
|
|
298
|
+
node?.type !== 'transform' &&
|
|
299
|
+
window.location.pathname.split('/')[2] !== 'transform'
|
|
300
|
+
) {
|
|
301
|
+
return <></>;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return (
|
|
305
|
+
<>
|
|
306
|
+
{
|
|
307
|
+
<div
|
|
308
|
+
style={{
|
|
309
|
+
borderRadius: '8px',
|
|
310
|
+
padding: '10px 10px 20px 10px',
|
|
311
|
+
margin: '32px 0',
|
|
312
|
+
background: '#f9f9f9',
|
|
313
|
+
width: 'max-content',
|
|
314
|
+
}}
|
|
315
|
+
>
|
|
316
|
+
<AllocationJoinHeader />
|
|
317
|
+
{Object.keys(allocationConfigFields).map(unit => (
|
|
318
|
+
<>
|
|
319
|
+
<div
|
|
320
|
+
style={{
|
|
321
|
+
backgroundColor: '#ffffff',
|
|
322
|
+
borderRadius: '8px',
|
|
323
|
+
margin: '15px 25px',
|
|
324
|
+
paddingBottom: '10px',
|
|
325
|
+
border: '1px solid #ffe2b2',
|
|
326
|
+
}}
|
|
327
|
+
>
|
|
328
|
+
<AllocationUnitTypeHeader unit={unit} />
|
|
329
|
+
{Object.values(allocationConfigFields[unit])}
|
|
330
|
+
</div>
|
|
331
|
+
<AddAllocationJoin />
|
|
332
|
+
</>
|
|
333
|
+
))}
|
|
334
|
+
</div>
|
|
335
|
+
}
|
|
336
|
+
</>
|
|
337
|
+
);
|
|
338
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { useContext, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import DJClientContext from '../../providers/djclient';
|
|
4
|
+
import { Form, Formik } from 'formik';
|
|
5
|
+
import EditIcon from '../../icons/EditIcon';
|
|
6
|
+
import { displayMessageAfterSubmit } from '../../../utils/form';
|
|
7
|
+
|
|
8
|
+
export default function EditColumnDescriptionPopover({
|
|
9
|
+
column,
|
|
10
|
+
node,
|
|
11
|
+
onSubmit,
|
|
12
|
+
}) {
|
|
13
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
14
|
+
const [popoverAnchor, setPopoverAnchor] = useState(false);
|
|
15
|
+
const ref = useRef(null);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const handleClickOutside = event => {
|
|
19
|
+
if (ref.current && !ref.current.contains(event.target)) {
|
|
20
|
+
setPopoverAnchor(false);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
document.addEventListener('click', handleClickOutside, true);
|
|
24
|
+
return () => {
|
|
25
|
+
document.removeEventListener('click', handleClickOutside, true);
|
|
26
|
+
};
|
|
27
|
+
}, [setPopoverAnchor]);
|
|
28
|
+
|
|
29
|
+
const saveDescription = async (
|
|
30
|
+
{ node, column, description },
|
|
31
|
+
{ setSubmitting, setStatus },
|
|
32
|
+
) => {
|
|
33
|
+
setSubmitting(false);
|
|
34
|
+
const response = await djClient.setColumnDescription(
|
|
35
|
+
node,
|
|
36
|
+
column,
|
|
37
|
+
description,
|
|
38
|
+
);
|
|
39
|
+
if (response.status === 200 || response.status === 201) {
|
|
40
|
+
setStatus({ success: 'Saved!' });
|
|
41
|
+
} else {
|
|
42
|
+
setStatus({
|
|
43
|
+
failure: `${response.json.message}`,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
onSubmit();
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<>
|
|
51
|
+
<button
|
|
52
|
+
className="edit_button"
|
|
53
|
+
aria-label="EditColumnDescription"
|
|
54
|
+
tabIndex="0"
|
|
55
|
+
onClick={() => {
|
|
56
|
+
setPopoverAnchor(!popoverAnchor);
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
<EditIcon />
|
|
60
|
+
</button>
|
|
61
|
+
<div
|
|
62
|
+
className="popover"
|
|
63
|
+
role="dialog"
|
|
64
|
+
aria-label="edit-description"
|
|
65
|
+
style={{ display: popoverAnchor === false ? 'none' : 'block' }}
|
|
66
|
+
ref={ref}
|
|
67
|
+
>
|
|
68
|
+
<Formik
|
|
69
|
+
initialValues={{
|
|
70
|
+
column: column.name,
|
|
71
|
+
node: node.name,
|
|
72
|
+
description: column.description || '',
|
|
73
|
+
}}
|
|
74
|
+
onSubmit={saveDescription}
|
|
75
|
+
>
|
|
76
|
+
{function Render({ isSubmitting, status, setFieldValue }) {
|
|
77
|
+
return (
|
|
78
|
+
<Form>
|
|
79
|
+
{displayMessageAfterSubmit(status)}
|
|
80
|
+
<div className="form-group mt-3">
|
|
81
|
+
<label htmlFor="description">Description</label>
|
|
82
|
+
<textarea
|
|
83
|
+
name="description"
|
|
84
|
+
className="form-control"
|
|
85
|
+
rows="3"
|
|
86
|
+
onChange={e => setFieldValue('description', e.target.value)}
|
|
87
|
+
defaultValue={column.description || ''}
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
<input
|
|
91
|
+
hidden={true}
|
|
92
|
+
name="column"
|
|
93
|
+
value={column.name}
|
|
94
|
+
readOnly={true}
|
|
95
|
+
/>
|
|
96
|
+
<input
|
|
97
|
+
hidden={true}
|
|
98
|
+
name="node"
|
|
99
|
+
value={node.name}
|
|
100
|
+
readOnly={true}
|
|
101
|
+
/>
|
|
102
|
+
<button
|
|
103
|
+
className="btn btn-primary mt-3"
|
|
104
|
+
type="submit"
|
|
105
|
+
disabled={isSubmitting}
|
|
106
|
+
>
|
|
107
|
+
Save
|
|
108
|
+
</button>
|
|
109
|
+
</Form>
|
|
110
|
+
);
|
|
111
|
+
}}
|
|
112
|
+
</Formik>
|
|
113
|
+
</div>
|
|
114
|
+
</>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { useContext, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import DJClientContext from '../../providers/djclient';
|
|
4
|
+
import { Field, Form, Formik } from 'formik';
|
|
5
|
+
import { FormikSelect } from '../AddEditNodePage/FormikSelect';
|
|
6
|
+
import EditIcon from '../../icons/EditIcon';
|
|
7
|
+
import { displayMessageAfterSubmit } from '../../../utils/form';
|
|
8
|
+
import LoadingIcon from '../../icons/LoadingIcon';
|
|
9
|
+
import { MetricQueryField } from '../AddEditNodePage/MetricQueryField';
|
|
10
|
+
|
|
11
|
+
export default function LinkComplexDimensionPopover({ link, onSubmit }) {
|
|
12
|
+
const djClient = useContext(DJClientContext).DataJunctionAPI;
|
|
13
|
+
const [popoverAnchor, setPopoverAnchor] = useState(false);
|
|
14
|
+
const ref = useRef(null);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const handleClickOutside = event => {
|
|
18
|
+
if (ref.current && !ref.current.contains(event.target)) {
|
|
19
|
+
setPopoverAnchor(false);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
document.addEventListener('click', handleClickOutside, true);
|
|
23
|
+
return () => {
|
|
24
|
+
document.removeEventListener('click', handleClickOutside, true);
|
|
25
|
+
};
|
|
26
|
+
}, [setPopoverAnchor]);
|
|
27
|
+
|
|
28
|
+
const handleSubmit = async (
|
|
29
|
+
{ node, column, dimension },
|
|
30
|
+
{ setSubmitting, setStatus },
|
|
31
|
+
) => {
|
|
32
|
+
if (referencedDimensionNode && dimension === 'Remove') {
|
|
33
|
+
await unlinkDimension(
|
|
34
|
+
node,
|
|
35
|
+
column,
|
|
36
|
+
referencedDimensionNode,
|
|
37
|
+
setStatus,
|
|
38
|
+
).then(_ => setSubmitting(false));
|
|
39
|
+
} else {
|
|
40
|
+
await linkDimension(node, column, dimension, setStatus).then(_ =>
|
|
41
|
+
setSubmitting(false),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
onSubmit();
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const linkDimension = async (node, column, dimension, setStatus) => {
|
|
48
|
+
const response = await djClient.linkDimension(node, column, dimension);
|
|
49
|
+
if (response.status === 200 || response.status === 201) {
|
|
50
|
+
setStatus({ success: 'Saved!' });
|
|
51
|
+
} else {
|
|
52
|
+
setStatus({
|
|
53
|
+
failure: `${response.json.message}`,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const unlinkDimension = async (node, column, currentDimension, setStatus) => {
|
|
59
|
+
const response = await djClient.unlinkDimension(
|
|
60
|
+
node,
|
|
61
|
+
column,
|
|
62
|
+
currentDimension,
|
|
63
|
+
);
|
|
64
|
+
if (response.status === 200 || response.status === 201) {
|
|
65
|
+
setStatus({ success: 'Removed dimension link!' });
|
|
66
|
+
} else {
|
|
67
|
+
setStatus({
|
|
68
|
+
failure: `${response.json.message}`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<>
|
|
75
|
+
<button
|
|
76
|
+
className="edit_button"
|
|
77
|
+
aria-label="LinkDimension"
|
|
78
|
+
tabIndex="0"
|
|
79
|
+
onClick={() => {
|
|
80
|
+
setPopoverAnchor(!popoverAnchor);
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
<EditIcon />
|
|
84
|
+
</button>
|
|
85
|
+
<div
|
|
86
|
+
className="popover"
|
|
87
|
+
role="dialog"
|
|
88
|
+
aria-label="client-code"
|
|
89
|
+
style={{ display: popoverAnchor === false ? 'none' : 'block' }}
|
|
90
|
+
ref={ref}
|
|
91
|
+
>
|
|
92
|
+
<Formik
|
|
93
|
+
initialValues={
|
|
94
|
+
{
|
|
95
|
+
// column: column.name,
|
|
96
|
+
// node: node.name,
|
|
97
|
+
// dimension: '',
|
|
98
|
+
// currentDimension: referencedDimensionNode,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
onSubmit={handleSubmit}
|
|
102
|
+
>
|
|
103
|
+
{function Render({ isSubmitting, status, setFieldValue }) {
|
|
104
|
+
return (
|
|
105
|
+
<Form>
|
|
106
|
+
{displayMessageAfterSubmit(status)}
|
|
107
|
+
<span data-testid="link-dimension">
|
|
108
|
+
<label>Join Type</label>
|
|
109
|
+
<FormikSelect
|
|
110
|
+
selectOptions={[{ value: 'left', label: 'LEFT' }]}
|
|
111
|
+
formikFieldName="join_type"
|
|
112
|
+
placeholder="Select join type"
|
|
113
|
+
className="join_type"
|
|
114
|
+
defaultValue={link.join_type || ''}
|
|
115
|
+
isMulti={false}
|
|
116
|
+
/>
|
|
117
|
+
{console.log('link, ', link)}
|
|
118
|
+
|
|
119
|
+
<label>Join On</label>
|
|
120
|
+
<MetricQueryField
|
|
121
|
+
djClient={djClient}
|
|
122
|
+
value={link.join_sql ? link.join_sql : ''}
|
|
123
|
+
/>
|
|
124
|
+
</span>
|
|
125
|
+
<button
|
|
126
|
+
className="add_node"
|
|
127
|
+
type="submit"
|
|
128
|
+
aria-label="SaveComplexDimension"
|
|
129
|
+
aria-hidden="false"
|
|
130
|
+
disabled={isSubmitting}
|
|
131
|
+
>
|
|
132
|
+
{isSubmitting ? <LoadingIcon /> : 'Save'}
|
|
133
|
+
</button>
|
|
134
|
+
</Form>
|
|
135
|
+
);
|
|
136
|
+
}}
|
|
137
|
+
</Formik>
|
|
138
|
+
</div>
|
|
139
|
+
</>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
import EditColumnPopover from './EditColumnPopover';
|
|
4
|
+
import EditColumnDescriptionPopover from './EditColumnDescriptionPopover';
|
|
4
5
|
import LinkDimensionPopover from './LinkDimensionPopover';
|
|
5
6
|
import { labelize } from '../../../utils/form';
|
|
6
7
|
import PartitionColumnPopover from './PartitionColumnPopover';
|
|
@@ -113,6 +114,24 @@ export default function NodeColumnTab({ node, djClient }) {
|
|
|
113
114
|
{col.display_name}
|
|
114
115
|
</span>
|
|
115
116
|
</td>
|
|
117
|
+
<td>
|
|
118
|
+
<span
|
|
119
|
+
className=""
|
|
120
|
+
role="columnheader"
|
|
121
|
+
aria-label="ColumnDescription"
|
|
122
|
+
aria-hidden="false"
|
|
123
|
+
>
|
|
124
|
+
{col.description || ''}
|
|
125
|
+
<EditColumnDescriptionPopover
|
|
126
|
+
column={col}
|
|
127
|
+
node={node}
|
|
128
|
+
onSubmit={async () => {
|
|
129
|
+
const res = await djClient.node(node.name);
|
|
130
|
+
setColumns(res.columns);
|
|
131
|
+
}}
|
|
132
|
+
/>
|
|
133
|
+
</span>
|
|
134
|
+
</td>
|
|
116
135
|
<td>
|
|
117
136
|
<span
|
|
118
137
|
className={`node_type__${
|
|
@@ -188,6 +207,7 @@ export default function NodeColumnTab({ node, djClient }) {
|
|
|
188
207
|
<tr>
|
|
189
208
|
<th className="text-start">Column</th>
|
|
190
209
|
<th>Display Name</th>
|
|
210
|
+
<th>Description</th>
|
|
191
211
|
<th>Type</th>
|
|
192
212
|
{node?.type !== 'cube' ? (
|
|
193
213
|
<>
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
render,
|
|
4
|
+
fireEvent,
|
|
5
|
+
waitFor,
|
|
6
|
+
screen,
|
|
7
|
+
within,
|
|
8
|
+
} from '@testing-library/react';
|
|
9
|
+
import EditColumnDescriptionPopover from '../EditColumnDescriptionPopover';
|
|
10
|
+
import DJClientContext from '../../../providers/djclient';
|
|
11
|
+
|
|
12
|
+
const mockDjClient = {
|
|
13
|
+
DataJunctionAPI: {
|
|
14
|
+
setColumnDescription: jest.fn(),
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
describe('<EditColumnDescriptionPopover />', () => {
|
|
19
|
+
it('renders correctly and handles successful form submission', async () => {
|
|
20
|
+
// Mock necessary data
|
|
21
|
+
const column = {
|
|
22
|
+
name: 'column1',
|
|
23
|
+
description: 'Initial description',
|
|
24
|
+
};
|
|
25
|
+
const node = { name: 'default.node1' };
|
|
26
|
+
|
|
27
|
+
// Mock onSubmit function
|
|
28
|
+
const onSubmitMock = jest.fn();
|
|
29
|
+
|
|
30
|
+
mockDjClient.DataJunctionAPI.setColumnDescription.mockReturnValue({
|
|
31
|
+
status: 200,
|
|
32
|
+
json: { message: 'Description updated successfully' },
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Render the component
|
|
36
|
+
render(
|
|
37
|
+
<DJClientContext.Provider value={mockDjClient}>
|
|
38
|
+
<EditColumnDescriptionPopover
|
|
39
|
+
column={column}
|
|
40
|
+
node={node}
|
|
41
|
+
onSubmit={onSubmitMock}
|
|
42
|
+
/>
|
|
43
|
+
</DJClientContext.Provider>,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Open the popover
|
|
47
|
+
fireEvent.click(screen.getByLabelText('EditColumnDescription'));
|
|
48
|
+
|
|
49
|
+
// Update the description
|
|
50
|
+
const dialog = screen.getByRole('dialog', { name: /edit-description/i });
|
|
51
|
+
const descriptionTextarea = within(dialog).getByRole('textbox');
|
|
52
|
+
fireEvent.change(descriptionTextarea, {
|
|
53
|
+
target: { value: 'Updated description' },
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Submit the form
|
|
57
|
+
fireEvent.click(screen.getByText('Save'));
|
|
58
|
+
|
|
59
|
+
// Expect setColumnDescription to be called
|
|
60
|
+
await waitFor(() => {
|
|
61
|
+
expect(
|
|
62
|
+
mockDjClient.DataJunctionAPI.setColumnDescription,
|
|
63
|
+
).toHaveBeenCalledWith('default.node1', 'column1', 'Updated description');
|
|
64
|
+
expect(screen.getByText('Saved!')).toBeInTheDocument();
|
|
65
|
+
expect(onSubmitMock).toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('handles failed form submission', async () => {
|
|
70
|
+
// Mock necessary data
|
|
71
|
+
const column = {
|
|
72
|
+
name: 'column1',
|
|
73
|
+
description: 'Initial description',
|
|
74
|
+
};
|
|
75
|
+
const node = { name: 'default.node1' };
|
|
76
|
+
|
|
77
|
+
// Mock onSubmit function
|
|
78
|
+
const onSubmitMock = jest.fn();
|
|
79
|
+
|
|
80
|
+
mockDjClient.DataJunctionAPI.setColumnDescription.mockReturnValue({
|
|
81
|
+
status: 500,
|
|
82
|
+
json: { message: 'Server error' },
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Render the component
|
|
86
|
+
render(
|
|
87
|
+
<DJClientContext.Provider value={mockDjClient}>
|
|
88
|
+
<EditColumnDescriptionPopover
|
|
89
|
+
column={column}
|
|
90
|
+
node={node}
|
|
91
|
+
onSubmit={onSubmitMock}
|
|
92
|
+
/>
|
|
93
|
+
</DJClientContext.Provider>,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// Open the popover
|
|
97
|
+
fireEvent.click(screen.getByLabelText('EditColumnDescription'));
|
|
98
|
+
|
|
99
|
+
// Update the description
|
|
100
|
+
const dialog = screen.getByRole('dialog', { name: /edit-description/i });
|
|
101
|
+
const descriptionTextarea = within(dialog).getByRole('textbox');
|
|
102
|
+
fireEvent.change(descriptionTextarea, {
|
|
103
|
+
target: { value: 'Updated description' },
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Submit the form
|
|
107
|
+
fireEvent.click(screen.getByText('Save'));
|
|
108
|
+
|
|
109
|
+
// Expect setColumnDescription to be called and the failure message to show up
|
|
110
|
+
await waitFor(() => {
|
|
111
|
+
expect(
|
|
112
|
+
mockDjClient.DataJunctionAPI.setColumnDescription,
|
|
113
|
+
).toHaveBeenCalledWith('default.node1', 'column1', 'Updated description');
|
|
114
|
+
expect(screen.getByText('Server error')).toBeInTheDocument();
|
|
115
|
+
expect(onSubmitMock).toHaveBeenCalled();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('renders with empty initial description', async () => {
|
|
120
|
+
// Mock necessary data with no description
|
|
121
|
+
const column = {
|
|
122
|
+
name: 'column1',
|
|
123
|
+
description: null,
|
|
124
|
+
};
|
|
125
|
+
const node = { name: 'default.node1' };
|
|
126
|
+
|
|
127
|
+
// Mock onSubmit function
|
|
128
|
+
const onSubmitMock = jest.fn();
|
|
129
|
+
|
|
130
|
+
// Render the component
|
|
131
|
+
render(
|
|
132
|
+
<DJClientContext.Provider value={mockDjClient}>
|
|
133
|
+
<EditColumnDescriptionPopover
|
|
134
|
+
column={column}
|
|
135
|
+
node={node}
|
|
136
|
+
onSubmit={onSubmitMock}
|
|
137
|
+
/>
|
|
138
|
+
</DJClientContext.Provider>,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// Open the popover
|
|
142
|
+
fireEvent.click(screen.getByLabelText('EditColumnDescription'));
|
|
143
|
+
|
|
144
|
+
// Check that the textarea is empty
|
|
145
|
+
const dialog = screen.getByRole('dialog', { name: /edit-description/i });
|
|
146
|
+
const descriptionTextarea = within(dialog).getByRole('textbox');
|
|
147
|
+
expect(descriptionTextarea.value).toBe('');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -742,6 +742,19 @@ export const DataJunctionAPI = {
|
|
|
742
742
|
);
|
|
743
743
|
return { status: response.status, json: await response.json() };
|
|
744
744
|
},
|
|
745
|
+
|
|
746
|
+
setColumnDescription: async function (nodeName, columnName, description) {
|
|
747
|
+
const response = await fetch(
|
|
748
|
+
`${DJ_URL}/nodes/${nodeName}/columns/${columnName}/description?description=${encodeURIComponent(
|
|
749
|
+
description,
|
|
750
|
+
)}`,
|
|
751
|
+
{
|
|
752
|
+
method: 'PATCH',
|
|
753
|
+
credentials: 'include',
|
|
754
|
+
},
|
|
755
|
+
);
|
|
756
|
+
return { status: response.status, json: await response.json() };
|
|
757
|
+
},
|
|
745
758
|
dimensions: async function () {
|
|
746
759
|
return await (
|
|
747
760
|
await fetch(`${DJ_URL}/dimensions`, {
|