datajunction-ui 0.0.26-alpha.0 → 0.0.27-alpha.0
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 +2 -2
- package/src/app/components/Search.jsx +41 -33
- package/src/app/components/__tests__/Search.test.jsx +46 -11
- package/src/app/index.tsx +1 -1
- package/src/app/pages/AddEditNodePage/MetricQueryField.jsx +57 -8
- package/src/app/pages/AddEditNodePage/UpstreamNodeField.jsx +17 -5
- package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +97 -1
- package/src/app/pages/AddEditNodePage/index.jsx +61 -17
- package/src/app/pages/NodePage/WatchNodeButton.jsx +12 -5
- package/src/app/pages/QueryPlannerPage/MetricFlowGraph.jsx +93 -15
- package/src/app/pages/QueryPlannerPage/PreAggDetailsPanel.jsx +2320 -65
- package/src/app/pages/QueryPlannerPage/SelectionPanel.jsx +234 -25
- package/src/app/pages/QueryPlannerPage/__tests__/MetricFlowGraph.test.jsx +315 -122
- package/src/app/pages/QueryPlannerPage/__tests__/PreAggDetailsPanel.test.jsx +2672 -314
- package/src/app/pages/QueryPlannerPage/__tests__/SelectionPanel.test.jsx +567 -0
- package/src/app/pages/QueryPlannerPage/__tests__/index.test.jsx +480 -55
- package/src/app/pages/QueryPlannerPage/index.jsx +1021 -14
- package/src/app/pages/QueryPlannerPage/styles.css +1990 -62
- package/src/app/pages/Root/__tests__/index.test.jsx +79 -8
- package/src/app/pages/Root/index.tsx +1 -1
- package/src/app/pages/SQLBuilderPage/__tests__/index.test.jsx +82 -0
- package/src/app/pages/SettingsPage/__tests__/CreateServiceAccountModal.test.jsx +37 -0
- package/src/app/pages/SettingsPage/__tests__/ServiceAccountsSection.test.jsx +48 -0
- package/src/app/pages/SettingsPage/__tests__/index.test.jsx +169 -1
- package/src/app/services/DJService.js +492 -3
- package/src/app/services/__tests__/DJService.test.jsx +582 -0
- package/src/mocks/mockNodes.jsx +36 -0
- package/webpack.config.js +27 -0
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
// Note: MarkerType.Arrow is just the string "arrow" - we use the literal
|
|
2
|
+
// to avoid importing reactflow in this service (which would bloat the main bundle)
|
|
3
|
+
const MARKER_TYPE_ARROW = 'arrow';
|
|
2
4
|
|
|
3
5
|
const DJ_URL = process.env.REACT_APP_DJ_URL
|
|
4
6
|
? process.env.REACT_APP_DJ_URL
|
|
@@ -115,6 +117,121 @@ export const DataJunctionAPI = {
|
|
|
115
117
|
).json();
|
|
116
118
|
},
|
|
117
119
|
|
|
120
|
+
// Lightweight GraphQL query for listing cubes with display names (for preset dropdown)
|
|
121
|
+
listCubesForPreset: async function () {
|
|
122
|
+
const query = `
|
|
123
|
+
query ListCubes {
|
|
124
|
+
findNodes(nodeTypes: [CUBE]) {
|
|
125
|
+
name
|
|
126
|
+
current {
|
|
127
|
+
displayName
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
`;
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const result = await (
|
|
135
|
+
await fetch(DJ_GQL, {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: {
|
|
138
|
+
'Content-Type': 'application/json',
|
|
139
|
+
},
|
|
140
|
+
credentials: 'include',
|
|
141
|
+
body: JSON.stringify({ query }),
|
|
142
|
+
})
|
|
143
|
+
).json();
|
|
144
|
+
|
|
145
|
+
// Transform to simple array: [{name, display_name}]
|
|
146
|
+
const nodes = result?.data?.findNodes || [];
|
|
147
|
+
return nodes.map(node => ({
|
|
148
|
+
name: node.name,
|
|
149
|
+
display_name: node.current?.displayName || null,
|
|
150
|
+
}));
|
|
151
|
+
} catch (err) {
|
|
152
|
+
console.error('Failed to fetch cubes via GraphQL:', err);
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
// Lightweight GraphQL query for planner page - only fetches fields needed
|
|
158
|
+
// Much faster than REST /cubes/{name}/ which loads all columns, elements, etc.
|
|
159
|
+
cubeForPlanner: async function (name) {
|
|
160
|
+
const query = `
|
|
161
|
+
query GetCubeForPlanner($name: String!) {
|
|
162
|
+
findNodes(names: [$name]) {
|
|
163
|
+
name
|
|
164
|
+
current {
|
|
165
|
+
displayName
|
|
166
|
+
cubeMetrics {
|
|
167
|
+
name
|
|
168
|
+
}
|
|
169
|
+
cubeDimensions {
|
|
170
|
+
name
|
|
171
|
+
}
|
|
172
|
+
materializations {
|
|
173
|
+
name
|
|
174
|
+
config
|
|
175
|
+
schedule
|
|
176
|
+
strategy
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
`;
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const result = await (
|
|
185
|
+
await fetch(DJ_GQL, {
|
|
186
|
+
method: 'POST',
|
|
187
|
+
headers: {
|
|
188
|
+
'Content-Type': 'application/json',
|
|
189
|
+
},
|
|
190
|
+
credentials: 'include',
|
|
191
|
+
body: JSON.stringify({ query, variables: { name } }),
|
|
192
|
+
})
|
|
193
|
+
).json();
|
|
194
|
+
|
|
195
|
+
const node = result?.data?.findNodes?.[0];
|
|
196
|
+
if (!node) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Transform to match the shape expected by QueryPlannerPage
|
|
201
|
+
const current = node.current || {};
|
|
202
|
+
const cubeMetrics = (current.cubeMetrics || []).map(m => m.name);
|
|
203
|
+
const cubeDimensions = (current.cubeDimensions || []).map(d => d.name);
|
|
204
|
+
|
|
205
|
+
// Extract druid_cube materialization if present (v3 or legacy)
|
|
206
|
+
const druidMat = (current.materializations || []).find(
|
|
207
|
+
m => m.name === 'druid_cube' || m.name === 'druid_cube_v3',
|
|
208
|
+
);
|
|
209
|
+
const cubeMaterialization = druidMat
|
|
210
|
+
? {
|
|
211
|
+
strategy: druidMat.strategy,
|
|
212
|
+
schedule: druidMat.schedule,
|
|
213
|
+
lookbackWindow: druidMat.config?.lookback_window,
|
|
214
|
+
druidDatasource: druidMat.config?.druid_datasource,
|
|
215
|
+
preaggTables: druidMat.config?.preagg_tables || [],
|
|
216
|
+
workflowUrls: druidMat.config?.workflow_urls || [],
|
|
217
|
+
timestampColumn: druidMat.config?.timestamp_column,
|
|
218
|
+
timestampFormat: druidMat.config?.timestamp_format,
|
|
219
|
+
}
|
|
220
|
+
: null;
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
name: node.name,
|
|
224
|
+
display_name: current.displayName,
|
|
225
|
+
cube_node_metrics: cubeMetrics,
|
|
226
|
+
cube_node_dimensions: cubeDimensions,
|
|
227
|
+
cubeMaterialization, // Included so we don't need a second fetch
|
|
228
|
+
};
|
|
229
|
+
} catch (err) {
|
|
230
|
+
console.error('Failed to fetch cube via GraphQL:', err);
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
|
|
118
235
|
whoami: async function () {
|
|
119
236
|
return await (
|
|
120
237
|
await fetch(`${DJ_URL}/whoami/`, { credentials: 'include' })
|
|
@@ -294,7 +411,7 @@ export const DataJunctionAPI = {
|
|
|
294
411
|
description
|
|
295
412
|
primaryKey
|
|
296
413
|
query
|
|
297
|
-
parents { name }
|
|
414
|
+
parents { name type }
|
|
298
415
|
metricMetadata {
|
|
299
416
|
direction
|
|
300
417
|
unit { name }
|
|
@@ -758,6 +875,56 @@ export const DataJunctionAPI = {
|
|
|
758
875
|
).json();
|
|
759
876
|
},
|
|
760
877
|
|
|
878
|
+
// Fetch node columns with partition info via GraphQL
|
|
879
|
+
// Used to check if a node has temporal partitions defined
|
|
880
|
+
getNodeColumnsWithPartitions: async function (nodeName) {
|
|
881
|
+
const query = `
|
|
882
|
+
query GetNodeColumnsWithPartitions($name: String!) {
|
|
883
|
+
findNodes(names: [$name]) {
|
|
884
|
+
name
|
|
885
|
+
current {
|
|
886
|
+
columns {
|
|
887
|
+
name
|
|
888
|
+
type
|
|
889
|
+
partition {
|
|
890
|
+
type_
|
|
891
|
+
format
|
|
892
|
+
granularity
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
`;
|
|
899
|
+
|
|
900
|
+
try {
|
|
901
|
+
const results = await (
|
|
902
|
+
await fetch(DJ_GQL, {
|
|
903
|
+
method: 'POST',
|
|
904
|
+
headers: { 'Content-Type': 'application/json' },
|
|
905
|
+
credentials: 'include',
|
|
906
|
+
body: JSON.stringify({ query, variables: { name: nodeName } }),
|
|
907
|
+
})
|
|
908
|
+
).json();
|
|
909
|
+
|
|
910
|
+
const node = results.data?.findNodes?.[0];
|
|
911
|
+
if (!node) return { columns: [], temporalPartitions: [] };
|
|
912
|
+
|
|
913
|
+
const columns = node.current?.columns || [];
|
|
914
|
+
const temporalPartitions = columns.filter(
|
|
915
|
+
col => col.partition?.type_ === 'TEMPORAL',
|
|
916
|
+
);
|
|
917
|
+
|
|
918
|
+
return {
|
|
919
|
+
columns,
|
|
920
|
+
temporalPartitions,
|
|
921
|
+
};
|
|
922
|
+
} catch (err) {
|
|
923
|
+
console.error('Failed to fetch node columns with partitions:', err);
|
|
924
|
+
return { columns: [], temporalPartitions: [] };
|
|
925
|
+
}
|
|
926
|
+
},
|
|
927
|
+
|
|
761
928
|
node_lineage: async function (name) {
|
|
762
929
|
return await (
|
|
763
930
|
await fetch(`${DJ_URL}/nodes/${name}/lineage/`, {
|
|
@@ -1074,7 +1241,7 @@ export const DataJunctionAPI = {
|
|
|
1074
1241
|
source: parent.name,
|
|
1075
1242
|
animated: true,
|
|
1076
1243
|
markerEnd: {
|
|
1077
|
-
type:
|
|
1244
|
+
type: MARKER_TYPE_ARROW,
|
|
1078
1245
|
},
|
|
1079
1246
|
});
|
|
1080
1247
|
}
|
|
@@ -1479,6 +1646,29 @@ export const DataJunctionAPI = {
|
|
|
1479
1646
|
);
|
|
1480
1647
|
return { status: response.status, json: await response.json() };
|
|
1481
1648
|
},
|
|
1649
|
+
// New V2 cube materialization endpoint for Druid cubes
|
|
1650
|
+
materializeCubeV2: async function (
|
|
1651
|
+
cubeName,
|
|
1652
|
+
schedule,
|
|
1653
|
+
strategy = 'incremental_time',
|
|
1654
|
+
lookbackWindow = '1 DAY',
|
|
1655
|
+
runBackfill = true,
|
|
1656
|
+
) {
|
|
1657
|
+
const response = await fetch(`${DJ_URL}/cubes/${cubeName}/materialize`, {
|
|
1658
|
+
method: 'POST',
|
|
1659
|
+
headers: {
|
|
1660
|
+
'Content-Type': 'application/json',
|
|
1661
|
+
},
|
|
1662
|
+
body: JSON.stringify({
|
|
1663
|
+
schedule: schedule,
|
|
1664
|
+
strategy: strategy,
|
|
1665
|
+
lookback_window: lookbackWindow,
|
|
1666
|
+
run_backfill: runBackfill,
|
|
1667
|
+
}),
|
|
1668
|
+
credentials: 'include',
|
|
1669
|
+
});
|
|
1670
|
+
return { status: response.status, json: await response.json() };
|
|
1671
|
+
},
|
|
1482
1672
|
runBackfill: async function (nodeName, materializationName, partitionValues) {
|
|
1483
1673
|
const response = await fetch(
|
|
1484
1674
|
`${DJ_URL}/nodes/${nodeName}/materializations/${materializationName}/backfill`,
|
|
@@ -1646,4 +1836,303 @@ export const DataJunctionAPI = {
|
|
|
1646
1836
|
});
|
|
1647
1837
|
return await response.json();
|
|
1648
1838
|
},
|
|
1839
|
+
|
|
1840
|
+
// =================================
|
|
1841
|
+
// Pre-aggregation API
|
|
1842
|
+
// =================================
|
|
1843
|
+
|
|
1844
|
+
// List pre-aggregations with optional filters
|
|
1845
|
+
listPreaggs: async function (filters = {}) {
|
|
1846
|
+
const params = new URLSearchParams();
|
|
1847
|
+
if (filters.node_name) params.append('node_name', filters.node_name);
|
|
1848
|
+
if (filters.grain) params.append('grain', filters.grain);
|
|
1849
|
+
if (filters.grain_mode) params.append('grain_mode', filters.grain_mode);
|
|
1850
|
+
if (filters.measures) params.append('measures', filters.measures);
|
|
1851
|
+
if (filters.status) params.append('status', filters.status);
|
|
1852
|
+
|
|
1853
|
+
return await (
|
|
1854
|
+
await fetch(`${DJ_URL}/preaggs/?${params}`, {
|
|
1855
|
+
credentials: 'include',
|
|
1856
|
+
})
|
|
1857
|
+
).json();
|
|
1858
|
+
},
|
|
1859
|
+
|
|
1860
|
+
// Plan pre-aggregations for given metrics and dimensions
|
|
1861
|
+
planPreaggs: async function (
|
|
1862
|
+
metrics,
|
|
1863
|
+
dimensions,
|
|
1864
|
+
strategy = null,
|
|
1865
|
+
schedule = null,
|
|
1866
|
+
lookbackWindow = null,
|
|
1867
|
+
) {
|
|
1868
|
+
const body = {
|
|
1869
|
+
metrics,
|
|
1870
|
+
dimensions,
|
|
1871
|
+
};
|
|
1872
|
+
if (strategy) body.strategy = strategy;
|
|
1873
|
+
if (schedule) body.schedule = schedule;
|
|
1874
|
+
if (lookbackWindow) body.lookback_window = lookbackWindow;
|
|
1875
|
+
|
|
1876
|
+
const response = await fetch(`${DJ_URL}/preaggs/plan`, {
|
|
1877
|
+
method: 'POST',
|
|
1878
|
+
credentials: 'include',
|
|
1879
|
+
headers: {
|
|
1880
|
+
'Content-Type': 'application/json',
|
|
1881
|
+
},
|
|
1882
|
+
body: JSON.stringify(body),
|
|
1883
|
+
});
|
|
1884
|
+
const result = await response.json();
|
|
1885
|
+
// If there's an error, include the status for the caller to check
|
|
1886
|
+
if (!response.ok) {
|
|
1887
|
+
return {
|
|
1888
|
+
...result,
|
|
1889
|
+
_error: true,
|
|
1890
|
+
_status: response.status,
|
|
1891
|
+
message:
|
|
1892
|
+
result.message || result.detail || 'Failed to plan pre-aggregations',
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
return result;
|
|
1896
|
+
},
|
|
1897
|
+
|
|
1898
|
+
// Get a specific pre-aggregation by ID
|
|
1899
|
+
getPreagg: async function (preaggId) {
|
|
1900
|
+
return await (
|
|
1901
|
+
await fetch(`${DJ_URL}/preaggs/${preaggId}`, {
|
|
1902
|
+
credentials: 'include',
|
|
1903
|
+
})
|
|
1904
|
+
).json();
|
|
1905
|
+
},
|
|
1906
|
+
|
|
1907
|
+
// Trigger materialization for a pre-aggregation
|
|
1908
|
+
materializePreagg: async function (preaggId) {
|
|
1909
|
+
const response = await fetch(`${DJ_URL}/preaggs/${preaggId}/materialize`, {
|
|
1910
|
+
method: 'POST',
|
|
1911
|
+
credentials: 'include',
|
|
1912
|
+
headers: {
|
|
1913
|
+
'Content-Type': 'application/json',
|
|
1914
|
+
},
|
|
1915
|
+
});
|
|
1916
|
+
const result = await response.json();
|
|
1917
|
+
// If there's an error, include the status for the caller to check
|
|
1918
|
+
if (!response.ok) {
|
|
1919
|
+
return {
|
|
1920
|
+
...result,
|
|
1921
|
+
_error: true,
|
|
1922
|
+
_status: response.status,
|
|
1923
|
+
message: result.message || result.detail || 'Materialization failed',
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
return result;
|
|
1927
|
+
},
|
|
1928
|
+
|
|
1929
|
+
// Update a single pre-aggregation's config
|
|
1930
|
+
updatePreaggConfig: async function (
|
|
1931
|
+
preaggId,
|
|
1932
|
+
strategy = null,
|
|
1933
|
+
schedule = null,
|
|
1934
|
+
lookbackWindow = null,
|
|
1935
|
+
) {
|
|
1936
|
+
const body = {};
|
|
1937
|
+
if (strategy) body.strategy = strategy;
|
|
1938
|
+
if (schedule) body.schedule = schedule;
|
|
1939
|
+
if (lookbackWindow) body.lookback_window = lookbackWindow;
|
|
1940
|
+
|
|
1941
|
+
const response = await fetch(`${DJ_URL}/preaggs/${preaggId}/config`, {
|
|
1942
|
+
method: 'PATCH',
|
|
1943
|
+
credentials: 'include',
|
|
1944
|
+
headers: {
|
|
1945
|
+
'Content-Type': 'application/json',
|
|
1946
|
+
},
|
|
1947
|
+
body: JSON.stringify(body),
|
|
1948
|
+
});
|
|
1949
|
+
const result = await response.json();
|
|
1950
|
+
if (!response.ok) {
|
|
1951
|
+
return {
|
|
1952
|
+
...result,
|
|
1953
|
+
_error: true,
|
|
1954
|
+
_status: response.status,
|
|
1955
|
+
message: result.message || result.detail || 'Failed to update config',
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
return result;
|
|
1959
|
+
},
|
|
1960
|
+
|
|
1961
|
+
// Deactivate (pause) a pre-aggregation's workflow
|
|
1962
|
+
deactivatePreaggWorkflow: async function (preaggId) {
|
|
1963
|
+
const response = await fetch(`${DJ_URL}/preaggs/${preaggId}/workflow`, {
|
|
1964
|
+
method: 'DELETE',
|
|
1965
|
+
credentials: 'include',
|
|
1966
|
+
});
|
|
1967
|
+
const result = await response.json();
|
|
1968
|
+
if (!response.ok) {
|
|
1969
|
+
return {
|
|
1970
|
+
...result,
|
|
1971
|
+
_error: true,
|
|
1972
|
+
_status: response.status,
|
|
1973
|
+
message:
|
|
1974
|
+
result.message || result.detail || 'Failed to deactivate workflow',
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
return result;
|
|
1978
|
+
},
|
|
1979
|
+
|
|
1980
|
+
// Run a backfill for a pre-aggregation
|
|
1981
|
+
runPreaggBackfill: async function (preaggId, startDate, endDate = null) {
|
|
1982
|
+
const body = {
|
|
1983
|
+
start_date: startDate,
|
|
1984
|
+
};
|
|
1985
|
+
if (endDate) body.end_date = endDate;
|
|
1986
|
+
|
|
1987
|
+
const response = await fetch(`${DJ_URL}/preaggs/${preaggId}/backfill`, {
|
|
1988
|
+
method: 'POST',
|
|
1989
|
+
credentials: 'include',
|
|
1990
|
+
headers: {
|
|
1991
|
+
'Content-Type': 'application/json',
|
|
1992
|
+
},
|
|
1993
|
+
body: JSON.stringify(body),
|
|
1994
|
+
});
|
|
1995
|
+
const result = await response.json();
|
|
1996
|
+
if (!response.ok) {
|
|
1997
|
+
return {
|
|
1998
|
+
...result,
|
|
1999
|
+
_error: true,
|
|
2000
|
+
_status: response.status,
|
|
2001
|
+
message: result.message || result.detail || 'Failed to run backfill',
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
return result;
|
|
2005
|
+
},
|
|
2006
|
+
|
|
2007
|
+
// Get cube details including materializations
|
|
2008
|
+
getCubeDetails: async function (cubeName) {
|
|
2009
|
+
const response = await fetch(`${DJ_URL}/cubes/${cubeName}`, {
|
|
2010
|
+
credentials: 'include',
|
|
2011
|
+
});
|
|
2012
|
+
if (!response.ok) {
|
|
2013
|
+
return { status: response.status, json: null };
|
|
2014
|
+
}
|
|
2015
|
+
return { status: response.status, json: await response.json() };
|
|
2016
|
+
},
|
|
2017
|
+
|
|
2018
|
+
// Extract Druid cube workflow URLs from cube materializations
|
|
2019
|
+
getCubeWorkflowUrls: async function (cubeName) {
|
|
2020
|
+
console.log('getCubeWorkflowUrls: Fetching for cube', cubeName);
|
|
2021
|
+
const result = await this.getCubeDetails(cubeName);
|
|
2022
|
+
console.log('getCubeWorkflowUrls: Cube details result', result);
|
|
2023
|
+
if (!result.json || !result.json.materializations) {
|
|
2024
|
+
console.log('getCubeWorkflowUrls: No materializations found');
|
|
2025
|
+
return [];
|
|
2026
|
+
}
|
|
2027
|
+
console.log(
|
|
2028
|
+
'getCubeWorkflowUrls: Materializations',
|
|
2029
|
+
result.json.materializations,
|
|
2030
|
+
);
|
|
2031
|
+
const druidMat = result.json.materializations.find(
|
|
2032
|
+
m => m.name === 'druid_cube',
|
|
2033
|
+
);
|
|
2034
|
+
console.log('getCubeWorkflowUrls: druid_cube materialization', druidMat);
|
|
2035
|
+
if (druidMat && druidMat.config && druidMat.config.workflow_urls) {
|
|
2036
|
+
console.log(
|
|
2037
|
+
'getCubeWorkflowUrls: Found URLs',
|
|
2038
|
+
druidMat.config.workflow_urls,
|
|
2039
|
+
);
|
|
2040
|
+
return druidMat.config.workflow_urls;
|
|
2041
|
+
}
|
|
2042
|
+
console.log('getCubeWorkflowUrls: No workflow_urls in config');
|
|
2043
|
+
return [];
|
|
2044
|
+
},
|
|
2045
|
+
|
|
2046
|
+
// Get full cube materialization info (for edit/refresh/backfill)
|
|
2047
|
+
getCubeMaterialization: async function (cubeName) {
|
|
2048
|
+
const result = await this.getCubeDetails(cubeName);
|
|
2049
|
+
if (!result.json || !result.json.materializations) {
|
|
2050
|
+
return null;
|
|
2051
|
+
}
|
|
2052
|
+
const druidMat = result.json.materializations.find(
|
|
2053
|
+
m => m.name === 'druid_cube',
|
|
2054
|
+
);
|
|
2055
|
+
if (!druidMat) {
|
|
2056
|
+
return null;
|
|
2057
|
+
}
|
|
2058
|
+
// Return combined info from materialization record and config
|
|
2059
|
+
return {
|
|
2060
|
+
id: druidMat.id,
|
|
2061
|
+
strategy: druidMat.strategy,
|
|
2062
|
+
schedule: druidMat.schedule,
|
|
2063
|
+
lookbackWindow: druidMat.lookback_window,
|
|
2064
|
+
druidDatasource: druidMat.config?.druid_datasource,
|
|
2065
|
+
preaggTables: druidMat.config?.preagg_tables || [],
|
|
2066
|
+
workflowUrls: druidMat.config?.workflow_urls || [],
|
|
2067
|
+
timestampColumn: druidMat.config?.timestamp_column,
|
|
2068
|
+
timestampFormat: druidMat.config?.timestamp_format,
|
|
2069
|
+
};
|
|
2070
|
+
},
|
|
2071
|
+
|
|
2072
|
+
// Refresh cube workflow (re-push to scheduler without backfill)
|
|
2073
|
+
refreshCubeWorkflow: async function (
|
|
2074
|
+
cubeName,
|
|
2075
|
+
schedule,
|
|
2076
|
+
strategy = 'incremental_time',
|
|
2077
|
+
lookbackWindow = '1 DAY',
|
|
2078
|
+
) {
|
|
2079
|
+
// Re-call materialize with run_backfill=false
|
|
2080
|
+
return this.materializeCubeV2(
|
|
2081
|
+
cubeName,
|
|
2082
|
+
schedule,
|
|
2083
|
+
strategy,
|
|
2084
|
+
lookbackWindow,
|
|
2085
|
+
false, // run_backfill = false for refresh
|
|
2086
|
+
);
|
|
2087
|
+
},
|
|
2088
|
+
|
|
2089
|
+
// Deactivate (pause) a cube's workflow
|
|
2090
|
+
deactivateCubeWorkflow: async function (cubeName) {
|
|
2091
|
+
const response = await fetch(`${DJ_URL}/cubes/${cubeName}/materialize`, {
|
|
2092
|
+
method: 'DELETE',
|
|
2093
|
+
credentials: 'include',
|
|
2094
|
+
});
|
|
2095
|
+
if (!response.ok) {
|
|
2096
|
+
const result = await response.json().catch(() => ({}));
|
|
2097
|
+
return {
|
|
2098
|
+
status: response.status,
|
|
2099
|
+
json: {
|
|
2100
|
+
...result,
|
|
2101
|
+
message:
|
|
2102
|
+
result.message ||
|
|
2103
|
+
result.detail ||
|
|
2104
|
+
'Failed to deactivate cube workflow',
|
|
2105
|
+
},
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
return { status: response.status, json: await response.json() };
|
|
2109
|
+
},
|
|
2110
|
+
|
|
2111
|
+
// Run cube backfill (trigger ad-hoc backfill with date range)
|
|
2112
|
+
runCubeBackfill: async function (cubeName, startDate, endDate = null) {
|
|
2113
|
+
const body = {
|
|
2114
|
+
start_date: startDate,
|
|
2115
|
+
};
|
|
2116
|
+
if (endDate) body.end_date = endDate;
|
|
2117
|
+
|
|
2118
|
+
const response = await fetch(`${DJ_URL}/cubes/${cubeName}/backfill`, {
|
|
2119
|
+
method: 'POST',
|
|
2120
|
+
credentials: 'include',
|
|
2121
|
+
headers: {
|
|
2122
|
+
'Content-Type': 'application/json',
|
|
2123
|
+
},
|
|
2124
|
+
body: JSON.stringify(body),
|
|
2125
|
+
});
|
|
2126
|
+
const result = await response.json();
|
|
2127
|
+
if (!response.ok) {
|
|
2128
|
+
return {
|
|
2129
|
+
...result,
|
|
2130
|
+
_error: true,
|
|
2131
|
+
_status: response.status,
|
|
2132
|
+
message:
|
|
2133
|
+
result.message || result.detail || 'Failed to run cube backfill',
|
|
2134
|
+
};
|
|
2135
|
+
}
|
|
2136
|
+
return result;
|
|
2137
|
+
},
|
|
1649
2138
|
};
|