diff options
author | Erlend <erlendniko@hotmail.com> | 2022-07-28 14:02:34 +0200 |
---|---|---|
committer | Erlend <erlendniko@hotmail.com> | 2022-07-28 14:02:34 +0200 |
commit | 145e696b6dbb6a4b32a9b97daa1c9cccebc89be3 (patch) | |
tree | 6c9c5a0217b9b8a72203c489aead106fee5a1fd1 | |
parent | 07528297082f940c1b8bb3fa8054b3027d1dc75c (diff) | |
parent | 8a6137df661f44636120a0dbfc6cc5da4419438b (diff) |
Merge branch 'enikolaisen/querybuilder-js'
15 files changed, 418 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..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 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> + ); } |