summaryrefslogtreecommitdiffstats
path: root/client/js/app/src
diff options
context:
space:
mode:
Diffstat (limited to 'client/js/app/src')
-rw-r--r--client/js/app/src/app/main.jsx5
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Buttons/CopyResponseButton.jsx40
-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/Buttons/OverlayImageButton.jsx2
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Buttons/PasteJSONButton.jsx48
-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/QueryDropDownForm.jsx12
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Text/QueryInput.jsx11
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Text/QueryInputChild.jsx7
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Text/ResponseBox.jsx2
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Text/SendQuery.jsx7
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Text/SimpleDropDownForm.jsx10
-rw-r--r--client/js/app/src/app/pages/querybuilder/TransformVespaTrace.jsx345
-rw-r--r--client/js/app/src/app/pages/querybuilder/query-builder.jsx34
-rw-r--r--client/js/app/src/app/pages/querytracer/query-tracer.jsx24
15 files changed, 533 insertions, 59 deletions
diff --git a/client/js/app/src/app/main.jsx b/client/js/app/src/app/main.jsx
index 96514d419e1..08f86115d40 100644
--- a/client/js/app/src/app/main.jsx
+++ b/client/js/app/src/app/main.jsx
@@ -1,9 +1,12 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { App } from 'app/app';
+import { ResponseProvider } from './pages/querybuilder/Components/Contexts/ResponseContext';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
- <App />
+ <ResponseProvider>
+ <App />
+ </ResponseProvider>
</React.StrictMode>
);
diff --git a/client/js/app/src/app/pages/querybuilder/Components/Buttons/CopyResponseButton.jsx b/client/js/app/src/app/pages/querybuilder/Components/Buttons/CopyResponseButton.jsx
new file mode 100644
index 00000000000..d01daa7b0d6
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/Components/Buttons/CopyResponseButton.jsx
@@ -0,0 +1,40 @@
+import React, { useContext, useState } from 'react';
+import OverlayImageButton from './OverlayImageButton';
+
+import copyImage from '../../assets/img/copy.svg';
+import { ResponseContext } from '../Contexts/ResponseContext';
+import { OverlayTrigger, Tooltip } from 'react-bootstrap';
+
+export default function CopyResponseButton() {
+ const { response } = useContext(ResponseContext);
+ const [show, setShow] = useState(false);
+
+ const handleCopy = () => {
+ setShow(true);
+ navigator.clipboard.writeText(response);
+ setTimeout(() => {
+ setShow(false);
+ }, 2000);
+ };
+
+ return (
+ <OverlayTrigger
+ placement="left-end"
+ show={show}
+ overlay={
+ <Tooltip id="copy-tooltip">Response copied to clipboard</Tooltip>
+ }
+ >
+ <span>
+ <OverlayImageButton
+ className="intro-copy"
+ image={copyImage}
+ height="30"
+ width="30"
+ tooltip="Copy"
+ onClick={handleCopy}
+ />
+ </span>
+ </OverlayTrigger>
+ );
+}
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..7ec6683afa3
--- /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({ children }) {
+ const { response } = useContext(ResponseContext);
+
+ const transformResponse = (response) => {
+ return transform(response);
+ };
+
+ const handleClick = () => {
+ if (response != '') {
+ let transformedResponse = JSON.stringify(
+ transformResponse(JSON.parse(response), undefined, '\t')
+ );
+ // copied 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);
+
+ // open Jaeger in a new tab
+ window.open('http://localhost:16686/search', '__blank');
+ } else {
+ alert('Response was empty');
+ }
+ };
+
+ return <SimpleButton onClick={handleClick}>{children}</SimpleButton>;
+}
diff --git a/client/js/app/src/app/pages/querybuilder/Components/Buttons/OverlayImageButton.jsx b/client/js/app/src/app/pages/querybuilder/Components/Buttons/OverlayImageButton.jsx
index 2cb9c7d6e9a..788d88fd0e6 100644
--- a/client/js/app/src/app/pages/querybuilder/Components/Buttons/OverlayImageButton.jsx
+++ b/client/js/app/src/app/pages/querybuilder/Components/Buttons/OverlayImageButton.jsx
@@ -36,5 +36,3 @@ export default function OverlayImageButton({
</OverlayTrigger>
);
}
-
-//
diff --git a/client/js/app/src/app/pages/querybuilder/Components/Buttons/PasteJSONButton.jsx b/client/js/app/src/app/pages/querybuilder/Components/Buttons/PasteJSONButton.jsx
index 73dce637500..df380c62fa1 100644
--- a/client/js/app/src/app/pages/querybuilder/Components/Buttons/PasteJSONButton.jsx
+++ b/client/js/app/src/app/pages/querybuilder/Components/Buttons/PasteJSONButton.jsx
@@ -4,18 +4,22 @@ import pasteImage from '../../assets/img/paste.svg';
import { QueryInputContext } from '../Contexts/QueryInputContext';
export default function PasteJSONButton() {
- const { inputs, setInputs, id, setId, levelZeroParameters, childMap } =
+ const { setInputs, setId, levelZeroParameters, childMap } =
useContext(QueryInputContext);
const [paste, setPaste] = useState(false);
- const handleClick = (e) => {
- alert('Button is non-functional');
- // setPaste(true);
- // window.addEventListener('paste', handlePaste)
+ //TODO: fix that the second-level dropdowns do not get set properly when pasting a JSON query
+
+ const handleClick = () => {
+ setPaste(true);
+ window.addEventListener('paste', handlePaste);
};
const handlePaste = (e) => {
setPaste(false);
+ // Stop data actually being pasted into div
+ e.stopPropagation();
+ e.preventDefault();
const pastedData = e.clipboardData.getData('text');
alert('Converting JSON: \n\n ' + pastedData);
window.removeEventListener('paste', handlePaste);
@@ -25,37 +29,51 @@ export default function PasteJSONButton() {
const convertPastedJSON = (pastedData) => {
try {
var json = JSON.parse(pastedData);
- setId(1);
- const newInputs = buildFromJSON(json, id);
+ const newInputs = buildFromJSON(json, 2);
setInputs(newInputs);
} catch (error) {
+ console.log(error);
alert('Could not parse JSON, with error-message: \n\n' + error.message);
}
};
- const buildFromJSON = (json, id) => {
+ const buildFromJSON = (json, id, parentTypeof) => {
let newInputs = [];
let keys = Object.keys(json);
for (let i = 0; i < keys.length; i++) {
let childId = 1;
- let newInput = { id: id, type: keys[i] };
- if (json[keys[i]] === Object) {
+ let newInput = { id: `${id}`, type: keys[i] };
+ //If the value for the key is a child object
+ if (typeof json[keys[i]] === 'object') {
newInput['typeof'] = 'Parent';
newInput['input'] = '';
newInput['hasChildren'] = true;
+ // Construct the id of the correct pattern
let tempId = id + '.' + childId;
childId += 1;
- newInput['children'] = buildFromJSON(json[keys[i]], tempId);
+ let type;
+ if (id.length > 1) {
+ //Used to get the correct value from childMap
+ type = parentTypeof + '_' + keys[i];
+ } else {
+ type = keys[i];
+ }
+ newInput['children'] = buildFromJSON(json[keys[i]], tempId, type);
} else {
- newInput['typeof'] = levelZeroParameters[keys[i]].type;
+ if (id.length > 1) {
+ const choices = childMap[parentTypeof];
+ newInput['typeof'] = choices[keys[i]].type;
+ } else {
+ newInput['typeof'] = levelZeroParameters[keys[i]].type;
+ }
newInput['input'] = json[keys[i]];
newInput['hasChildren'] = false;
newInput['children'] = [];
}
- setId(id + 1);
- console.log(newInput);
+ id += 1;
newInputs.push(newInput);
}
+ setId(id);
return newInputs;
};
@@ -65,7 +83,7 @@ export default function PasteJSONButton() {
id="pasteJSON"
className="pasteJSON"
image={pasteImage}
- style={{ marginTop: '-2px', marginRight: '3px' }}
+ //style={{ marginTop: '-2px', marginRight: '3px' }}
onClick={handleClick}
>
{paste ? 'Press CMD + V' : 'Paste JSON'}
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..bc21ea81d9a 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
@@ -20,6 +20,7 @@ export const QueryInputProvider = (prop) => {
},
searchChain: { name: 'searchChain', type: 'String', hasChildren: false },
timeout: { name: 'timeout', type: 'Float', hasChildren: false },
+ trace: { name: 'trace', type: 'Parent', hasChildren: true },
tracelevel: { name: 'tracelevel', type: 'Parent', hasChildren: true },
traceLevel: { name: 'traceLevel', type: 'Integer', hasChildren: false },
explainLevel: { name: 'explainLevel', type: 'Integer', hasChildren: false },
diff --git a/client/js/app/src/app/pages/querybuilder/Components/Text/QueryDropDownForm.jsx b/client/js/app/src/app/pages/querybuilder/Components/Text/QueryDropDownForm.jsx
index d9a3417752c..88bd545282a 100644
--- a/client/js/app/src/app/pages/querybuilder/Components/Text/QueryDropDownForm.jsx
+++ b/client/js/app/src/app/pages/querybuilder/Components/Text/QueryDropDownForm.jsx
@@ -1,8 +1,13 @@
+import React, { useContext, useState } from 'react';
import { QueryInputContext } from '../Contexts/QueryInputContext';
-import React, { useCallback, useContext, useState } from 'react';
import SimpleDropDownForm from './SimpleDropDownForm';
-export default function QueryDropdownForm({ choices, id, child = false }) {
+export default function QueryDropdownForm({
+ choices,
+ id,
+ child = false,
+ initial,
+}) {
const {
inputs,
setInputs,
@@ -28,6 +33,8 @@ export default function QueryDropdownForm({ choices, id, child = false }) {
let parentTypes = newInputs[index].type;
let children = newInputs[index].children;
let childChoices = childMap[parentTypes];
+ //TODO: try to rafactor this loop into a separate function that can be
+ //used everywhere in the code similar loops are used
for (let i = 3; i < id.length; i += 2) {
currentId = id.substring(0, i);
index = children.findIndex((element) => element.id === currentId);
@@ -62,6 +69,7 @@ export default function QueryDropdownForm({ choices, id, child = false }) {
onChange={updateType}
choices={choices}
value={choice}
+ initial={initial}
></SimpleDropDownForm>
);
}
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 c5410ae7c4a..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) {
@@ -36,8 +36,12 @@ export default function QueryInput() {
const inputList = inputs.map((value) => {
return (
- <div key={value.id} id={value.id} className="queryinput">
- <QueryDropdownForm choices={levelZeroParameters} id={value.id} />
+ <div key={value.id + value.typeof} id={value.id} className="queryinput">
+ <QueryDropdownForm
+ choices={levelZeroParameters}
+ id={value.id}
+ initial={value.type}
+ />
{value.hasChildren ? (
<>
<AddPropertyButton id={value.id} />
@@ -49,6 +53,7 @@ export default function QueryInput() {
size="30"
onChange={updateInput}
placeholder={setPlaceholder(value.id)}
+ initial={value.input}
/>
)}
<OverlayTrigger
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 0440d6ef1ba..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,10 +94,14 @@ 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)
+ }
<QueryDropdownForm
choices={childMap[currentTypes]}
id={child.id}
child={true}
+ inital={child.type}
/>
{child.hasChildren ? (
<>
@@ -109,6 +113,7 @@ export default function QueryInputChild({ id }) {
size="30"
onChange={updateInput}
placeholder={setPlaceHolder(child.id)}
+ inital={child.input}
/>
)}
<OverlayTrigger
@@ -150,6 +155,7 @@ function Child({ child, type, onChange, placeholder, removeRow }) {
choices={childMap[type]}
id={child.id}
child={true}
+ initial={child.type}
/>
{child.hasChildren ? (
<>
@@ -161,6 +167,7 @@ function Child({ child, type, onChange, placeholder, removeRow }) {
size="30"
onChange={onChange}
placeholder={placeholder(child.id)}
+ initial={child.input}
/>
)}
<OverlayTrigger
diff --git a/client/js/app/src/app/pages/querybuilder/Components/Text/ResponseBox.jsx b/client/js/app/src/app/pages/querybuilder/Components/Text/ResponseBox.jsx
index 08c65238434..dac98271965 100644
--- a/client/js/app/src/app/pages/querybuilder/Components/Text/ResponseBox.jsx
+++ b/client/js/app/src/app/pages/querybuilder/Components/Text/ResponseBox.jsx
@@ -1,4 +1,4 @@
-import React, { useContext, useEffect, useState } from 'react';
+import React, { useContext } from 'react';
import { ResponseContext } from '../Contexts/ResponseContext';
export default function ResponseBox() {
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/Components/Text/SimpleDropDownForm.jsx b/client/js/app/src/app/pages/querybuilder/Components/Text/SimpleDropDownForm.jsx
index 01288cea44f..94c6c01b619 100644
--- a/client/js/app/src/app/pages/querybuilder/Components/Text/SimpleDropDownForm.jsx
+++ b/client/js/app/src/app/pages/querybuilder/Components/Text/SimpleDropDownForm.jsx
@@ -7,10 +7,11 @@ export default function SimpleDropDownForm({
className = 'input',
onChange,
value,
+ initial,
}) {
const { selectedItems } = useContext(QueryInputContext);
- //FIXME: using the filtered list to render options results in dropdown not changing the displayed selection to what was actually selected.
+ //TODO: using the filtered list to render options results in dropdown not changing the displayed selection to what was actually selected.
let filtered = Object.keys(choices).filter(
(choice) => !selectedItems.includes(choice)
);
@@ -30,7 +31,12 @@ export default function SimpleDropDownForm({
return (
<form id={id}>
- <select className={className} id={id} value={value} onChange={onChange}>
+ <select
+ className={className}
+ id={id}
+ defaultValue={initial}
+ onChange={onChange}
+ >
{options}
</select>
</form>
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..a6981b8a521
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/TransformVespaTrace.jsx
@@ -0,0 +1,345 @@
+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);
+ traceStartTimestamp = findTraceStartTime(spans);
+ topSpanId = genRanHex(16);
+ let topSpanFirstHalf = createNewSpan(traceStartTimestamp)[0];
+ data.push(topSpanFirstHalf);
+
+ const retrieved = findLogsAndChildren(spans, topSpanFirstHalf);
+ const logs = retrieved['logs'];
+ const children = retrieved['children'];
+ traverseLogs(logs);
+ createChildren(children);
+
+ return output;
+}
+
+function findLogsAndChildren(spans, topSpanFirstHalf) {
+ let logs = [];
+ let children = [];
+ let data = output['data'][0]['spans'];
+ //let hitQuery = false;
+ //let topSpanSecondHalf = createNewSpan();
+ //let secondHalfDuration = 0;
+ //output['data'][0]['spans'].push(topSpanSecondHalf);
+ //let firstHitSecondHalf = true;
+ for (let i = 0; i < spans.length - 1; i++) {
+ if (spans[i].hasOwnProperty('children')) {
+ let a = spans[i]['timestamp'];
+ topSpanFirstHalf = createNewSpan(traceStartTimestamp + a);
+ //firstHitSecondHalf = true;
+ //topSpanSecondHalf = createNewSpan();
+ //output['data'][0]['spans'].push(topSpanSecondHalf);
+ let log = [];
+ for (let x of spans[i]['children']) {
+ if (Array.isArray(x['message'])) {
+ if (log.length > 0) {
+ // finished moving down the search chain
+ // create a new array for holding the logs that represent moving up the search chain
+ logs.push(log);
+ log = [];
+ }
+ //hitQuery = true;
+ children.push(x['message']);
+ } else {
+ // only add logs with a timestamp
+ if (x.hasOwnProperty('timestamp')) {
+ log.push(x);
+ }
+ }
+ }
+ logs.push(log);
+ } else if (
+ spans[i].hasOwnProperty('message') &&
+ spans[i].hasOwnProperty('timestamp')
+ ) {
+ let span = createNewSpan(0, 0, 'p0', spans[i]['message'], [
+ {
+ refType: 'CHILD_OF',
+ traceID: traceID,
+ spanID: topSpanFirstHalf['spanID'],
+ },
+ ])[0];
+ data.push(span);
+ span['startTime'] = traceStartTimestamp + spans[i]['timestamp'] * 1000;
+ let duration;
+ if (i === spans.length - 1) {
+ duration = 1;
+ } else {
+ duration = (spans[i + 1]['timestamp'] - spans[i]['timestamp']) * 1000;
+ duration = duration === 0 ? 1 : duration;
+ }
+ topSpanFirstHalf['duration'] = topSpanFirstHalf['duration'] + duration;
+ span['duration'] = duration;
+ // if (hitQuery) {
+ // if (firstHitSecondHalf) {
+ // secondHalfDuration = span['timestamp'] * 1000;
+ // topSpanSecondHalf['startTime'] =
+ // traceStartTimestamp + secondHalfDuration;
+ // firstHitSecondHalf = false;
+ // }
+ // topSpanSecondHalf['duration'] =
+ // span['timestamp'] * 1000 - secondHalfDuration;
+ // topSpanSecondHalf['logs'].push({
+ // timestamp: traceStartTimestamp + span['timestamp'] * 1000,
+ // fields: [{ key: 'message', type: 'string', value: span['message'] }],
+ // });
+ // } else {
+ // topSpanFirstHalf['duration'] = span['timestamp'] * 1000;
+ // topSpanFirstHalf['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;
+ if (logDuration === 0) {
+ logDuration = 1;
+ }
+ let spanID = genRanHex(16);
+ if (first) {
+ parentID = spanID;
+ first = false;
+ }
+ let temp = createNewSpan(
+ logStartTimestamp,
+ logDuration,
+ 'p0',
+ 'test'
+ //[{ refType: 'CHILD_OF', traceID: traceID, spanID: topSpanId }]
+ );
+ let childSpan = temp[0];
+ let childSpanID = temp[1];
+ data.push(childSpan);
+ for (let i = 0; i < log.length - 1; i++) {
+ if (log[i].hasOwnProperty('message')) {
+ let logPointStart = traceStartTimestamp + log[i]['timestamp'] * 1000;
+ let logPointDuration;
+ if (i > log.length - 1) {
+ logPointDuration = 1;
+ } else {
+ logPointDuration =
+ (log[i + 1]['timestamp'] - log[i]['timestamp']) * 1000;
+ logPointDuration = logPointDuration === 0 ? 1 : logPointDuration;
+ }
+ let logSpan = createNewSpan(
+ logPointStart,
+ logPointDuration,
+ 'p0',
+ log[i]['message'],
+ [{ refType: 'CHILD_OF', traceID: traceID, spanID: childSpanID }]
+ )[0];
+ data.push(logSpan);
+ // childSpan['logs'].push({
+ // timestamp: traceStartTimestamp + log[i]['timestamp'] * 1000,
+ // fields: [
+ // { key: 'message', type: 'string', value: log[i]['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) {
+ if (Array.isArray(trace['message'])) {
+ let timestamp = Date.parse(trace['message'][0]['start_time']) * 1000;
+ let currentTimestamp = trace['timestamp'] * 1000;
+ return timestamp - currentTimestamp;
+ }
+}
+
+function findTraceStartTime(spans) {
+ let startTime = 0;
+ for (let span of spans) {
+ if (span.hasOwnProperty('children')) {
+ startTime = findTraceStartTime(span['children']);
+ } else if (span.hasOwnProperty('message')) {
+ if (Array.isArray(span['message'])) {
+ return getTraceStartTime(span);
+ }
+ }
+ if (startTime !== 0) {
+ return startTime;
+ }
+ }
+ return startTime;
+}
+
+//TODO: remove if not needed later
+function findDuration(spans) {
+ let notFound = true;
+ let duration = 0;
+ let i = spans.length - 1;
+ while (notFound && i >= 0) {
+ if (spans[i].hasOwnProperty('timestamp')) {
+ duration = spans[i]['timestamp'];
+ notFound = false;
+ } else {
+ i--;
+ }
+ }
+ return duration;
+}
+
+function createNewSpan(
+ startTime = 0,
+ duration = 0,
+ processID = 'p0',
+ operationName = 'Complete',
+ references = []
+) {
+ let spanID = genRanHex(16);
+ let newSpan = {
+ traceID: traceID,
+ spanID: spanID,
+ operationName: operationName,
+ startTime: startTime,
+ duration: duration,
+ references: references,
+ tags: [],
+ logs: [],
+ processID: processID,
+ };
+ return [newSpan, spanID];
+}
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 472ec1efe49..42a4ed829c3 100644
--- a/client/js/app/src/app/pages/querybuilder/query-builder.jsx
+++ b/client/js/app/src/app/pages/querybuilder/query-builder.jsx
@@ -1,27 +1,19 @@
-import React, { useContext } from 'react';
-import SimpleButton from './Components/Buttons/SimpleButton';
+import React from 'react';
import QueryInput from './Components/Text/QueryInput';
import TextBox from './Components/Text/TextBox';
-import ImageButton from './Components/Buttons/ImageButton';
-import OverlayImageButton from './Components/Buttons/OverlayImageButton';
import AddQueryInput from './Components/Buttons/AddQueryInputButton';
import { QueryInputProvider } from './Components/Contexts/QueryInputContext';
import SendQuery from './Components/Text/SendQuery';
-import {
- ResponseContext,
- ResponseProvider,
-} from './Components/Contexts/ResponseContext';
+import { ResponseProvider } from './Components/Contexts/ResponseContext';
import ResponseBox from './Components/Text/ResponseBox';
-
-import copyImage from './assets/img/copy.svg';
-
-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 (
@@ -49,19 +41,9 @@ export function QueryBuilder() {
</QueryProvider>
<TextBox className="response">Response</TextBox>
<ResponseBox />
+ <CopyResponseButton />
+ <DownloadJSONButton>Download response as JSON</DownloadJSONButton>
</ResponseProvider>
- <OverlayImageButton
- className="intro-copy"
- image={copyImage}
- height="30"
- width="30"
- tooltip="Copy"
- onClick={() => {
- alert('Button is non-functional');
- }}
- >
- Copy
- </OverlayImageButton>
<br />
<br />
</div>
diff --git a/client/js/app/src/app/pages/querytracer/query-tracer.jsx b/client/js/app/src/app/pages/querytracer/query-tracer.jsx
index 94c293d37ed..c700b73ebba 100644
--- a/client/js/app/src/app/pages/querytracer/query-tracer.jsx
+++ b/client/js/app/src/app/pages/querytracer/query-tracer.jsx
@@ -1,6 +1,26 @@
-import React from 'react';
+import React, { useContext } from 'react';
+import DownloadJSONButton from '../querybuilder/Components/Buttons/DownloadJSONButton';
+import { ResponseContext } from '../querybuilder/Components/Contexts/ResponseContext';
import { Container } from 'app/components';
export function QueryTracer() {
- return <Container>query tracer</Container>;
+ const { response, setResponse } = useContext(ResponseContext);
+
+ const updateResponse = (e) => {
+ setResponse(e.target.value);
+ };
+
+ return (
+ <Container>
+ <textarea
+ cols="70"
+ rows="25"
+ value={response}
+ onChange={updateResponse}
+ ></textarea>
+ <DownloadJSONButton>
+ Convert to Jeager format and download trace
+ </DownloadJSONButton>
+ </Container>
+ );
}