aboutsummaryrefslogtreecommitdiffstats
path: root/client/js
diff options
context:
space:
mode:
authorErlend <erlendniko@hotmail.com>2022-07-28 11:07:56 +0200
committerErlend <erlendniko@hotmail.com>2022-07-28 11:07:56 +0200
commit076635da2ea27d938180076476d43c33ea86ff51 (patch)
tree5cdf49379ece31f91958954b5f053f9f3ff1383b /client/js
parent4c235d3b8db1b046c20a1d8f75f8fb9e6e28b4fe (diff)
Added ability to download trace from response for use in visualization
Diffstat (limited to 'client/js')
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Buttons/DownloadJSONButton.jsx44
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Contexts/QueryInputContext.jsx1
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Text/QueryInput.jsx2
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Text/QueryInputChild.jsx4
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Text/SendQuery.jsx7
-rw-r--r--client/js/app/src/app/pages/querybuilder/TransformVespaTrace.jsx230
-rw-r--r--client/js/app/src/app/pages/querybuilder/query-builder.jsx8
7 files changed, 285 insertions, 11 deletions
diff --git a/client/js/app/src/app/pages/querybuilder/Components/Buttons/DownloadJSONButton.jsx b/client/js/app/src/app/pages/querybuilder/Components/Buttons/DownloadJSONButton.jsx
new file mode 100644
index 00000000000..3997c9c4bcc
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/Components/Buttons/DownloadJSONButton.jsx
@@ -0,0 +1,44 @@
+import React, { useContext } from 'react';
+import { ResponseContext } from '../Contexts/ResponseContext';
+import transform from '../../TransformVespaTrace';
+import SimpleButton from './SimpleButton';
+
+export default function DownloadJSONButton() {
+ const { response } = useContext(ResponseContext);
+
+ const transformResponse = (response) => {
+ return transform(response);
+ };
+
+ const handleClick = () => {
+ if (response != '') {
+ let transformedResponse = JSON.stringify(
+ transformResponse(JSON.parse(response), undefined, '\t')
+ );
+ // taken from safakeskinĀ“s answer on SO, link: https://stackoverflow.com/questions/55613438/reactwrite-to-json-file-or-export-download-no-server
+ const fileName = 'vespa-response';
+ const blob = new Blob([transformedResponse], {
+ type: 'application/json',
+ });
+ const href = URL.createObjectURL(blob);
+
+ // create "a" HTLM element with href to file
+ const link = document.createElement('a');
+ link.href = href;
+ link.download = fileName + '.json';
+ document.body.appendChild(link);
+ link.click();
+
+ // clean up "a" element & remove ObjectURL
+ document.body.removeChild(link);
+ URL.revokeObjectURL(href);
+ window.open('http://localhost:16686/search', '__blank');
+ } else {
+ alert('Response was empty');
+ }
+ };
+
+ return (
+ <SimpleButton onClick={handleClick}>Download response as JSON</SimpleButton>
+ );
+}
diff --git a/client/js/app/src/app/pages/querybuilder/Components/Contexts/QueryInputContext.jsx b/client/js/app/src/app/pages/querybuilder/Components/Contexts/QueryInputContext.jsx
index 064bf51795b..7126a472d55 100644
--- a/client/js/app/src/app/pages/querybuilder/Components/Contexts/QueryInputContext.jsx
+++ b/client/js/app/src/app/pages/querybuilder/Components/Contexts/QueryInputContext.jsx
@@ -40,6 +40,7 @@ export const QueryInputProvider = (prop) => {
recall: { name: 'recall', type: 'List', hasChildren: false },
user: { name: 'user', type: 'String', hasChildren: false },
metrics: { name: 'metrics', type: 'Parent', hasChildren: true },
+ trace: { name: 'trace', type: 'Parent', hasChildren: true },
};
// Children of the levelZeroParameters that have child attributes
diff --git a/client/js/app/src/app/pages/querybuilder/Components/Text/QueryInput.jsx b/client/js/app/src/app/pages/querybuilder/Components/Text/QueryInput.jsx
index 65e32945c55..bd41edd6008 100644
--- a/client/js/app/src/app/pages/querybuilder/Components/Text/QueryInput.jsx
+++ b/client/js/app/src/app/pages/querybuilder/Components/Text/QueryInput.jsx
@@ -8,7 +8,7 @@ import AddPropertyButton from '../Buttons/AddPropertyButton';
import QueryInputChild from './QueryInputChild';
export default function QueryInput() {
- const { inputs, setInputs, levelZeroParameters, childMap } =
+ const { inputs, setInputs, levelZeroParameters } =
useContext(QueryInputContext);
function removeRow(id) {
diff --git a/client/js/app/src/app/pages/querybuilder/Components/Text/QueryInputChild.jsx b/client/js/app/src/app/pages/querybuilder/Components/Text/QueryInputChild.jsx
index d10935bf4c9..4ab2a074214 100644
--- a/client/js/app/src/app/pages/querybuilder/Components/Text/QueryInputChild.jsx
+++ b/client/js/app/src/app/pages/querybuilder/Components/Text/QueryInputChild.jsx
@@ -94,7 +94,9 @@ export default function QueryInputChild({ id }) {
const inputList = childArray.map((child) => {
return (
<div key={child.id} id={child.id}>
- {child.id == '4.1' && console.log(child.type)}
+ {
+ //child.id == '4.1' && console.log(child.type)
+ }
<QueryDropdownForm
choices={childMap[currentTypes]}
id={child.id}
diff --git a/client/js/app/src/app/pages/querybuilder/Components/Text/SendQuery.jsx b/client/js/app/src/app/pages/querybuilder/Components/Text/SendQuery.jsx
index 9ee370de7c9..a3714f27fb5 100644
--- a/client/js/app/src/app/pages/querybuilder/Components/Text/SendQuery.jsx
+++ b/client/js/app/src/app/pages/querybuilder/Components/Text/SendQuery.jsx
@@ -1,10 +1,10 @@
import React, { useContext, useEffect, useState } from 'react';
-import SimpleDropDownForm from './SimpleDropDownForm';
import SimpleButton from '../Buttons/SimpleButton';
-import SimpleForm from './SimpleForm';
import { QueryInputContext } from '../Contexts/QueryInputContext';
import { ResponseContext } from '../Contexts/ResponseContext';
import { QueryContext } from '../Contexts/QueryContext';
+import SimpleForm from './SimpleForm';
+import SimpleDropDownForm from './SimpleDropDownForm';
export default function SendQuery() {
const { inputs } = useContext(QueryInputContext);
@@ -65,15 +65,12 @@ export default function SendQuery() {
case 'Integer':
case 'Long':
return parseInt(input);
- break;
case 'Float':
return parseFloat(input);
- break;
case 'Boolean':
return input.toLowerCase() === 'true' ? true : false;
- break;
default:
return input;
diff --git a/client/js/app/src/app/pages/querybuilder/TransformVespaTrace.jsx b/client/js/app/src/app/pages/querybuilder/TransformVespaTrace.jsx
new file mode 100644
index 00000000000..e6f89a39be4
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/TransformVespaTrace.jsx
@@ -0,0 +1,230 @@
+let traceID = '';
+let processes = {};
+let output = {};
+let traceStartTimestamp = 0;
+let topSpanId = '';
+let parentID = '';
+
+// Generates a random hex string of size "size"
+const genRanHex = (size) =>
+ [...Array(size)]
+ .map(() => Math.floor(Math.random() * 16).toString(16))
+ .join('');
+
+export default function transform(trace) {
+ traceID = genRanHex(32);
+ output = { data: [{ traceID: traceID, spans: [], processes: {} }] };
+ let data = output['data'][0]['spans'];
+ processes = output['data'][0]['processes'];
+ processes.p0 = { serviceName: 'Query', tags: [] };
+ let temp = trace['trace']['children'];
+ let spans = findChildren(temp);
+ let query = findChildren(spans); // only used for getting the start time
+ traceStartTimestamp = getTraceStartTime(query);
+ let totalTraceDuration = spans[spans.length - 1]['timestamp'] * 1000;
+ topSpanId = genRanHex(16);
+ let topSpan = {
+ traceID: traceID,
+ spanID: topSpanId,
+ operationName: 'Complete',
+ startTime: traceStartTimestamp,
+ duration: totalTraceDuration,
+ references: [],
+ tags: [],
+ logs: [],
+ processID: 'p0',
+ };
+ data.push(topSpan);
+
+ const retrieved = findLogsAndChildren(spans, topSpan);
+ const logs = retrieved['logs'];
+ const children = retrieved['children'];
+ traverseLogs(logs);
+ createChildren(children);
+
+ return output;
+}
+
+function findLogsAndChildren(spans, topSpan) {
+ let logs = [];
+ let children = [];
+ for (let span of spans) {
+ if (span.hasOwnProperty('children')) {
+ let log = [];
+ for (let x of span['children']) {
+ if (Array.isArray(x['message'])) {
+ children.push(x['message']);
+ } else {
+ log.push(x);
+ }
+ }
+ logs.push(log);
+ } else if (span.hasOwnProperty('message')) {
+ topSpan['logs'].push({
+ timestamp: traceStartTimestamp + span['timestamp'] * 1000,
+ fields: [{ key: 'message', type: 'string', value: span['message'] }],
+ });
+ }
+ }
+ return { logs: logs, children: children };
+}
+
+function traverseLogs(logs) {
+ let first = true;
+ let data = output['data'][0]['spans'];
+ for (let log of logs) {
+ let logStartTimestamp = traceStartTimestamp + log[0]['timestamp'] * 1000;
+ let logDuration =
+ (log[log.length - 1]['timestamp'] - log[0]['timestamp']) * 1000;
+ let spanID = genRanHex(16);
+ if (first) {
+ parentID = spanID;
+ first = false;
+ }
+ let childSpan = {
+ traceID: traceID,
+ spanID: spanID,
+ operationName: 'test',
+ startTime: logStartTimestamp,
+ duration: logDuration,
+ references: [
+ { refType: 'CHILD_OF', traceID: traceID, spanID: topSpanId },
+ ],
+ tags: [],
+ logs: [],
+ processID: 'p0',
+ };
+ data.push(childSpan);
+ for (let logPoint of log) {
+ if (logPoint.hasOwnProperty('message')) {
+ childSpan['logs'].push({
+ timestamp: traceStartTimestamp + logPoint['timestamp'] * 1000,
+ fields: [
+ { key: 'message', type: 'string', value: logPoint['message'] },
+ ],
+ });
+ }
+ }
+ }
+}
+
+function createChildren(children) {
+ for (let i = 0; i < children.length; i++) {
+ let child = children[i][0];
+ let processKey = `p${i + 1}`;
+ processes[processKey] = { serviceName: `Span${i}`, tags: [] };
+ let spanID = genRanHex(16);
+ let data = output['data'][0]['spans'];
+ let startTimestamp = Date.parse(child['start_time']) * 1000;
+ let newSpan = {
+ traceID: traceID,
+ spanID: spanID,
+ operationName: `query${i}`,
+ startTime: startTimestamp,
+ duration: child['duration_ms'] * 1000,
+ references: [{ refType: 'CHILD_OF', traceID: traceID, spanID: parentID }],
+ tags: [],
+ logs: [],
+ processID: processKey,
+ };
+ data.push(newSpan);
+ let traces = child['traces'];
+ for (let k = 0; k < traces.length; k++) {
+ let trace = traces[k];
+ let traceTimestamp = trace['timestamp_ms'];
+ let events;
+ let firstEvent;
+ let duration;
+ if (trace['tag'] === 'query_execution') {
+ events = trace['threads'][0]['traces'];
+ firstEvent = events[0];
+ duration = (traceTimestamp - firstEvent['timestamp_ms']) * 1000;
+ } else if (trace['tag'] === 'query_execution_plan') {
+ events = [];
+ let nextTrace = traces[k + 1];
+ firstEvent = trace;
+ // query execution plan has no events, duration must therefore be found using the next trace
+ if (nextTrace['tag'] === 'query_execution') {
+ duration =
+ (nextTrace['threads'][0]['traces'][0]['timestamp_ms'] -
+ traceTimestamp) *
+ 1000;
+ } else {
+ duration = (nextTrace['timestamp_ms'] - traceTimestamp) * 1000;
+ }
+ } else {
+ events = trace['traces'];
+ firstEvent = events[0];
+ duration = (traceTimestamp - firstEvent['timestamp_ms']) * 1000;
+ }
+ let childSpanID = genRanHex(16);
+ let childSpan = {
+ traceID: traceID,
+ spanID: childSpanID,
+ operationName: trace['tag'],
+ startTime: startTimestamp + firstEvent['timestamp_ms'] * 1000,
+ duration: duration,
+ references: [{ refType: 'CHILD_OF', traceID: traceID, spanID: spanID }],
+ tags: [],
+ logs: [],
+ processID: processKey,
+ };
+ data.push(childSpan);
+ if (events.length > 0) {
+ for (let j = 0; j < events.length; j++) {
+ let event = events[j];
+ let eventID = genRanHex(16);
+ let eventStart = event['timestamp_ms'];
+ let operationName;
+ if (event.hasOwnProperty('event')) {
+ operationName = event['event'];
+ if (
+ operationName === 'Complete query setup' ||
+ operationName === 'MatchThread::run Done'
+ ) {
+ duration = (traceTimestamp - eventStart) * 1000;
+ } else {
+ duration = (events[j + 1]['timestamp_ms'] - eventStart) * 1000;
+ }
+ } else {
+ operationName = event['tag'];
+ duration = (events[j + 1]['timestamp_ms'] - eventStart) * 1000;
+ }
+ let eventSpan = {
+ traceID: traceID,
+ spanID: eventID,
+ operationName: operationName,
+ startTime: startTimestamp + eventStart * 1000,
+ duration: duration,
+ references: [
+ { refType: 'CHILD_OF', traceID: traceID, spanID: childSpanID },
+ ],
+ tags: [],
+ logs: [],
+ processID: processKey,
+ };
+ data.push(eventSpan);
+ }
+ }
+ }
+ }
+}
+
+function findChildren(traces) {
+ for (let trace of traces) {
+ if (trace.hasOwnProperty('children')) {
+ return trace['children'];
+ }
+ }
+}
+
+// Get an estimated start time by using the start time of the query and subtracting the current run time
+function getTraceStartTime(trace) {
+ for (let x of trace) {
+ if (Array.isArray(x['message'])) {
+ let timestamp = Date.parse(x['message'][0]['start_time']) * 1000;
+ let currentTimestamp = x['timestamp'] * 1000;
+ return timestamp - currentTimestamp;
+ }
+ }
+}
diff --git a/client/js/app/src/app/pages/querybuilder/query-builder.jsx b/client/js/app/src/app/pages/querybuilder/query-builder.jsx
index ebe8b759fa4..465720595f7 100644
--- a/client/js/app/src/app/pages/querybuilder/query-builder.jsx
+++ b/client/js/app/src/app/pages/querybuilder/query-builder.jsx
@@ -6,15 +6,14 @@ import { QueryInputProvider } from './Components/Contexts/QueryInputContext';
import SendQuery from './Components/Text/SendQuery';
import { ResponseProvider } from './Components/Contexts/ResponseContext';
import ResponseBox from './Components/Text/ResponseBox';
-
-import '../../styles/agency.css';
-import '../../styles/vespa.css';
import ShowQueryButton from './Components/Buttons/ShowQueryButton';
import { QueryProvider } from './Components/Contexts/QueryContext';
import PasteJSONButton from './Components/Buttons/PasteJSONButton';
import CopyResponseButton from './Components/Buttons/CopyResponseButton';
+import DownloadJSONButton from './Components/Buttons/DownloadJSONButton';
-//import 'bootstrap/dist/css/bootstrap.min.css'; //TODO: Find out how to get this css
+import '../../styles/agency.css';
+import '../../styles/vespa.css';
export function QueryBuilder() {
return (
@@ -43,6 +42,7 @@ export function QueryBuilder() {
<TextBox className="response">Response</TextBox>
<ResponseBox />
<CopyResponseButton />
+ <DownloadJSONButton />
</ResponseProvider>
<br />
<br />