Skip to content

Commit f9f2a9d

Browse files
committed
Various fixes
1 parent b8b68b2 commit f9f2a9d

16 files changed

Lines changed: 1827 additions & 394 deletions

File tree

exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/DashboardResources.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import io.swagger.v3.oas.annotations.Parameter;
2424
import io.swagger.v3.oas.annotations.tags.Tag;
2525
import org.apache.drill.common.exceptions.DrillRuntimeException;
26+
import org.apache.drill.exec.ExecConstants;
2627
import org.apache.drill.exec.exception.StoreException;
2728
import org.apache.drill.exec.server.rest.auth.DrillUserPrincipal;
2829
import org.apache.drill.exec.store.sys.PersistentStore;
@@ -1099,14 +1100,26 @@ private File getUploadDir() {
10991100
if (cachedUploadDir == null) {
11001101
synchronized (DashboardResources.class) {
11011102
if (cachedUploadDir == null) {
1103+
// 1. Prefer DRILL_LOG_DIR (set by Drill startup scripts)
11021104
String basePath = System.getenv("DRILL_LOG_DIR");
1105+
// 2. Fall back to Drill's persistent store path (same dir as system tables)
11031106
if (basePath == null) {
1104-
basePath = System.getProperty("java.io.tmpdir");
1107+
try {
1108+
basePath = workManager.getContext().getConfig()
1109+
.getString(ExecConstants.SYS_STORE_PROVIDER_LOCAL_PATH);
1110+
} catch (Exception e) {
1111+
logger.debug("Could not read sys.store.provider.local.path", e);
1112+
}
1113+
}
1114+
// 3. Last resort: user home directory
1115+
if (basePath == null) {
1116+
basePath = System.getProperty("user.home");
11051117
}
11061118
File dir = new File(basePath, UPLOAD_DIR_NAME);
11071119
if (!dir.exists() && !dir.mkdirs()) {
11081120
throw new DrillRuntimeException("Failed to create upload directory: " + dir);
11091121
}
1122+
logger.info("Dashboard image upload directory: {}", dir.getAbsolutePath());
11101123
cachedUploadDir = dir;
11111124
}
11121125
}

exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/StatusResources.java

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@
2121
import java.util.ArrayList;
2222
import java.util.Collections;
2323
import java.util.Comparator;
24+
import java.util.LinkedHashMap;
2425
import java.util.LinkedList;
2526
import java.util.List;
27+
import java.util.Map;
2628

2729
import jakarta.annotation.security.PermitAll;
2830
import jakarta.annotation.security.RolesAllowed;
@@ -58,6 +60,13 @@
5860
import org.apache.http.client.methods.HttpGet;
5961
import org.glassfish.jersey.server.mvc.Viewable;
6062

63+
import com.codahale.metrics.Counter;
64+
import com.codahale.metrics.Gauge;
65+
import com.codahale.metrics.Histogram;
66+
import com.codahale.metrics.Meter;
67+
import com.codahale.metrics.MetricRegistry;
68+
import com.codahale.metrics.Snapshot;
69+
import com.codahale.metrics.Timer;
6170
import com.fasterxml.jackson.annotation.JsonCreator;
6271
import com.fasterxml.jackson.annotation.JsonIgnore;
6372

@@ -104,6 +113,102 @@ public Viewable getStatus() {
104113
return ViewableWithPermissions.create(authEnabled.get(), "/rest/status.ftl", sc, getStatusJSON());
105114
}
106115

116+
/**
117+
* Returns the local Drillbit's metrics as JSON.
118+
* Replaces the old Codahale MetricsServlet that was removed during
119+
* the javax.servlet to jakarta.servlet migration.
120+
*/
121+
@GET
122+
@Path(StatusResources.PATH_METRICS)
123+
@Produces(MediaType.APPLICATION_JSON)
124+
@Operation(externalDocs = @ExternalDocumentation(description = "Apache Drill REST API documentation:", url = "https://drill.apache.org/docs/rest-api-introduction/"))
125+
public Map<String, Object> getLocalMetrics() {
126+
MetricRegistry metrics = work.getContext().getMetrics();
127+
Map<String, Object> result = new LinkedHashMap<>();
128+
129+
// Gauges
130+
Map<String, Object> gauges = new LinkedHashMap<>();
131+
for (Map.Entry<String, Gauge> entry : metrics.getGauges().entrySet()) {
132+
Map<String, Object> g = new LinkedHashMap<>();
133+
g.put("value", entry.getValue().getValue());
134+
gauges.put(entry.getKey(), g);
135+
}
136+
result.put("gauges", gauges);
137+
138+
// Counters
139+
Map<String, Object> counters = new LinkedHashMap<>();
140+
for (Map.Entry<String, Counter> entry : metrics.getCounters().entrySet()) {
141+
Map<String, Object> c = new LinkedHashMap<>();
142+
c.put("count", entry.getValue().getCount());
143+
counters.put(entry.getKey(), c);
144+
}
145+
result.put("counters", counters);
146+
147+
// Histograms
148+
Map<String, Object> histograms = new LinkedHashMap<>();
149+
for (Map.Entry<String, Histogram> entry : metrics.getHistograms().entrySet()) {
150+
Snapshot snap = entry.getValue().getSnapshot();
151+
Map<String, Object> h = new LinkedHashMap<>();
152+
h.put("count", entry.getValue().getCount());
153+
h.put("max", snap.getMax());
154+
h.put("mean", snap.getMean());
155+
h.put("min", snap.getMin());
156+
h.put("p50", snap.getMedian());
157+
h.put("p75", snap.get75thPercentile());
158+
h.put("p95", snap.get95thPercentile());
159+
h.put("p98", snap.get98thPercentile());
160+
h.put("p99", snap.get99thPercentile());
161+
h.put("p999", snap.get999thPercentile());
162+
h.put("stddev", snap.getStdDev());
163+
histograms.put(entry.getKey(), h);
164+
}
165+
result.put("histograms", histograms);
166+
167+
// Meters
168+
Map<String, Object> meters = new LinkedHashMap<>();
169+
for (Map.Entry<String, Meter> entry : metrics.getMeters().entrySet()) {
170+
Meter m = entry.getValue();
171+
Map<String, Object> mData = new LinkedHashMap<>();
172+
mData.put("count", m.getCount());
173+
mData.put("m1_rate", m.getOneMinuteRate());
174+
mData.put("m5_rate", m.getFiveMinuteRate());
175+
mData.put("m15_rate", m.getFifteenMinuteRate());
176+
mData.put("mean_rate", m.getMeanRate());
177+
mData.put("units", "events/second");
178+
meters.put(entry.getKey(), mData);
179+
}
180+
result.put("meters", meters);
181+
182+
// Timers
183+
Map<String, Object> timers = new LinkedHashMap<>();
184+
for (Map.Entry<String, Timer> entry : metrics.getTimers().entrySet()) {
185+
Timer t = entry.getValue();
186+
Snapshot snap = t.getSnapshot();
187+
Map<String, Object> tData = new LinkedHashMap<>();
188+
tData.put("count", t.getCount());
189+
tData.put("max", snap.getMax());
190+
tData.put("mean", snap.getMean());
191+
tData.put("min", snap.getMin());
192+
tData.put("p50", snap.getMedian());
193+
tData.put("p75", snap.get75thPercentile());
194+
tData.put("p95", snap.get95thPercentile());
195+
tData.put("p98", snap.get98thPercentile());
196+
tData.put("p99", snap.get99thPercentile());
197+
tData.put("p999", snap.get999thPercentile());
198+
tData.put("stddev", snap.getStdDev());
199+
tData.put("m1_rate", t.getOneMinuteRate());
200+
tData.put("m5_rate", t.getFiveMinuteRate());
201+
tData.put("m15_rate", t.getFifteenMinuteRate());
202+
tData.put("mean_rate", t.getMeanRate());
203+
tData.put("duration_units", "seconds");
204+
tData.put("rate_units", "calls/second");
205+
timers.put(entry.getKey(), tData);
206+
}
207+
result.put("timers", timers);
208+
209+
return result;
210+
}
211+
107212
@GET
108213
@Path(StatusResources.PATH_METRICS + "/{hostname}")
109214
@Produces(MediaType.APPLICATION_JSON)

exec/java-exec/src/main/resources/webapp/package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

exec/java-exec/src/main/resources/webapp/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"react": "^18.2.0",
2929
"react-dom": "^18.2.0",
3030
"react-grid-layout": "^1.4.4",
31+
"react-icons": "^5.5.0",
3132
"react-markdown": "^10.1.0",
3233
"react-redux": "^9.0.4",
3334
"react-router-dom": "^6.21.0",

exec/java-exec/src/main/resources/webapp/src/api/metadata.ts

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
* limitations under the License.
1717
*/
1818
import apiClient from './client';
19-
import type { SchemaInfo, TableInfo, ColumnInfo, PluginInfo } from '../types';
19+
import type { SchemaInfo, TableInfo, ColumnInfo, PluginInfo, NestedFieldInfo } from '../types';
20+
import { executeQuery } from './queries';
2021

2122
const METADATA_BASE = '/api/v1/metadata';
2223

@@ -155,3 +156,68 @@ export async function getFileColumns(schema: string, filePath: string): Promise<
155156
);
156157
return response.data.columns;
157158
}
159+
160+
/**
161+
* Parse a string-serialised map schema like "{field1=BIGINT, field2=VARCHAR}".
162+
*/
163+
function parseMapSchemaString(str: string): NestedFieldInfo[] {
164+
const trimmed = str.replace(/^\{|\}$/g, '').trim();
165+
if (!trimmed) {
166+
return [];
167+
}
168+
return trimmed.split(',').map((pair) => {
169+
const eqIdx = pair.indexOf('=');
170+
if (eqIdx < 0) {
171+
return { name: pair.trim(), type: 'ANY' };
172+
}
173+
return { name: pair.slice(0, eqIdx).trim(), type: pair.slice(eqIdx + 1).trim() || 'ANY' };
174+
}).filter((f) => f.name.length > 0);
175+
}
176+
177+
/**
178+
* Fetch nested sub-fields for a MAP/STRUCT column using Drill's getMapSchema() function.
179+
*
180+
* @param schema the schema name (e.g. "dfs.tmp")
181+
* @param tableOrFile the table or file identifier (e.g. "data.json")
182+
* @param columnPath dot-separated path to the column (e.g. "record" or "record.nested_map")
183+
*/
184+
export async function getNestedColumns(
185+
schema: string,
186+
tableOrFile: string,
187+
columnPath: string,
188+
): Promise<NestedFieldInfo[]> {
189+
// Build the column expression with backtick-quoting on each path segment
190+
const pathParts = columnPath.split('.');
191+
const columnExpr = pathParts.map((p) => `\`${p}\``).join('.');
192+
193+
const query =
194+
`SELECT getMapSchema(${columnExpr}) AS \`schema\`` +
195+
` FROM \`${schema}\`.\`${tableOrFile}\` LIMIT 1`;
196+
197+
const result = await executeQuery({
198+
query,
199+
queryType: 'SQL',
200+
autoLimitRowCount: 1,
201+
});
202+
203+
if (result.rows.length === 0) {
204+
return [];
205+
}
206+
207+
const schemaVal = result.rows[0]['schema'];
208+
209+
// MAP types are serialised as JSON objects by Drill's /query.json endpoint
210+
if (typeof schemaVal === 'object' && schemaVal !== null) {
211+
return Object.entries(schemaVal as Record<string, unknown>).map(([name, type]) => ({
212+
name,
213+
type: String(type),
214+
}));
215+
}
216+
217+
// Fallback: string representation like "{field1=BIGINT, field2=VARCHAR}"
218+
if (typeof schemaVal === 'string') {
219+
return parseMapSchemaString(schemaVal);
220+
}
221+
222+
return [];
223+
}

exec/java-exec/src/main/resources/webapp/src/components/query-editor/SqlEditor.tsx

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* See the License for the specific language governing permissions and
1616
* limitations under the License.
1717
*/
18-
import { useRef, useCallback, useEffect } from 'react';
18+
import { useState, useRef, useCallback, useEffect } from 'react';
1919
import Editor, { OnMount, OnChange, Monaco } from '@monaco-editor/react';
2020

2121
// Use Monaco's editor type from the package
@@ -164,8 +164,65 @@ export default function SqlEditor({
164164
};
165165
}, [insertText]);
166166

167+
// ---- Drag-and-drop support (Phase 3) ----
168+
const [dragOver, setDragOver] = useState(false);
169+
170+
const handleDragOver = useCallback((e: React.DragEvent) => {
171+
e.preventDefault();
172+
e.dataTransfer.dropEffect = 'copy';
173+
setDragOver(true);
174+
}, []);
175+
176+
const handleDragLeave = useCallback(() => {
177+
setDragOver(false);
178+
}, []);
179+
180+
const handleDrop = useCallback((e: React.DragEvent) => {
181+
e.preventDefault();
182+
setDragOver(false);
183+
const text = e.dataTransfer.getData('text/plain');
184+
if (!text) {
185+
return;
186+
}
187+
const editor = editorRef.current;
188+
if (editor) {
189+
// Try to place the text at the mouse drop position
190+
const target = editor.getTargetAtClientPoint(e.clientX, e.clientY);
191+
if (target?.position) {
192+
const pos = target.position;
193+
editor.executeEdits('drop', [
194+
{
195+
range: {
196+
startLineNumber: pos.lineNumber,
197+
startColumn: pos.column,
198+
endLineNumber: pos.lineNumber,
199+
endColumn: pos.column,
200+
},
201+
text,
202+
forceMoveMarkers: true,
203+
},
204+
]);
205+
} else {
206+
// Fallback: insert at current cursor position
207+
insertText(text);
208+
}
209+
editor.focus();
210+
}
211+
}, [insertText]);
212+
167213
return (
168-
<div className="monaco-container" style={{ height }}>
214+
<div
215+
className="monaco-container"
216+
style={{
217+
height,
218+
border: dragOver ? '2px solid #1890ff' : '2px solid transparent',
219+
borderRadius: 4,
220+
transition: 'border-color 0.2s',
221+
}}
222+
onDragOver={handleDragOver}
223+
onDragLeave={handleDragLeave}
224+
onDrop={handleDrop}
225+
>
169226
<Editor
170227
height="100%"
171228
defaultLanguage="sql"

0 commit comments

Comments
 (0)