diff options
author | Erlend <erlendniko@hotmail.com> | 2022-08-10 14:17:34 +0200 |
---|---|---|
committer | Erlend <erlendniko@hotmail.com> | 2022-08-10 14:17:34 +0200 |
commit | b2b99295d1ee48ba29f902c28cef468eea57b144 (patch) | |
tree | c5bdb05fad9713274e8f72c119338e666f317ab0 /client | |
parent | 0d0ad9b655496480bcbb4f7566cc0b1b0a66f85d (diff) | |
parent | b0fdd9f2e8885ed21cbf584d6e3b1218ba6ae4d2 (diff) |
Merge remote-tracking branch 'upstream/master'
Diffstat (limited to 'client')
30 files changed, 506 insertions, 1191 deletions
diff --git a/client/js/app/package.json b/client/js/app/package.json index 030fe818ba6..f88c4234ad5 100644 --- a/client/js/app/package.json +++ b/client/js/app/package.json @@ -32,9 +32,11 @@ "eslint-plugin-react-perf": "^3", "eslint-plugin-unused-imports": "^2", "husky": "^7", + "lodash": "^4", "prettier": "2", "pretty-quick": "^3", - "react-router-dom": "^6.3.0", + "react-router-dom": "^6", + "use-context-selector": "^1", "vite": "^2" } } diff --git a/client/js/app/src/app/main.jsx b/client/js/app/src/app/main.jsx index 08f86115d40..96514d419e1 100644 --- a/client/js/app/src/app/main.jsx +++ b/client/js/app/src/app/main.jsx @@ -1,12 +1,9 @@ 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> - <ResponseProvider> - <App /> - </ResponseProvider> + <App /> </React.StrictMode> ); diff --git a/client/js/app/src/app/pages/querybuilder/Components/Buttons/AddPropertyButton.jsx b/client/js/app/src/app/pages/querybuilder/Components/Buttons/AddPropertyButton.jsx deleted file mode 100644 index 47b5a67875b..00000000000 --- a/client/js/app/src/app/pages/querybuilder/Components/Buttons/AddPropertyButton.jsx +++ /dev/null @@ -1,46 +0,0 @@ -import React, { useContext, useState } from 'react'; -import { QueryInputContext } from '../Contexts/QueryInputContext'; -import SimpleButton from './SimpleButton'; - -export default function AddPropertyButton({ id }) { - const { inputs, setInputs, childMap } = useContext(QueryInputContext); - const [childId, setChildId] = useState(1); - - /** - * Add a child to the input that has the provided id - */ - const addChildProperty = () => { - const newInputs = inputs.slice(); - let currentId = id.substring(0, 1); - let index = newInputs.findIndex((element) => element.id === currentId); //get the index of the root parent - let children = newInputs[index].children; - let parentType = newInputs[index].type; - for (let i = 3; i < id.length + 1; i += 2) { - currentId = id.substring(0, i); - index = children.findIndex((element) => element.id === currentId); - parentType = parentType + '_' + children[index].type; - children = children[index].children; - } - let type = childMap[parentType]; - children.push({ - id: id + '.' + childId, - type: type[Object.keys(type)[0]].name, - typeof: type[Object.keys(type)[0]].type, - input: '', - hasChildren: false, - children: [], - }); - setInputs(newInputs); - setChildId((childId) => childId + 1); - }; - - return ( - <SimpleButton - id={`propb${id}`} - className={'addpropsbutton'} - onClick={addChildProperty} - > - + Add property - </SimpleButton> - ); -} diff --git a/client/js/app/src/app/pages/querybuilder/Components/Buttons/AddQueryInputButton.jsx b/client/js/app/src/app/pages/querybuilder/Components/Buttons/AddQueryInputButton.jsx deleted file mode 100644 index 8aeaff971bf..00000000000 --- a/client/js/app/src/app/pages/querybuilder/Components/Buttons/AddQueryInputButton.jsx +++ /dev/null @@ -1,48 +0,0 @@ -import React, { useContext } from 'react'; -import { QueryInputContext } from '../Contexts/QueryInputContext'; -import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; -import Tooltip from 'react-bootstrap/Tooltip'; - -export default function AddQueryInput() { - const { inputs, setInputs, id, setId } = useContext(QueryInputContext); - - /** - * Adds a new element to inputs. - * @param {Event} e Event that happened. - */ - const updateInputs = (e) => { - e.preventDefault(); - setId((id) => id + 1); - setInputs((prevInputs) => [ - ...prevInputs, - { - id: `${id + 1}`, - type: 'yql', - typeof: 'String', - input: '', - hasChildren: false, - children: [], - }, - ]); - }; - - return ( - <OverlayTrigger - placement="right" - delay={{ show: 250, hide: 400 }} - overlay={<Tooltip id="button-tooltip">Add row</Tooltip>} - > - <span> - <button - id="addRow" - className="addRow" - height="0" - width="0" - onClick={updateInputs} - > - + - </button> - </span> - </OverlayTrigger> - ); -} 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 index d01daa7b0d6..2c53c4dd1e6 100644 --- a/client/js/app/src/app/pages/querybuilder/Components/Buttons/CopyResponseButton.jsx +++ b/client/js/app/src/app/pages/querybuilder/Components/Buttons/CopyResponseButton.jsx @@ -1,12 +1,11 @@ -import React, { useContext, useState } from 'react'; -import OverlayImageButton from './OverlayImageButton'; - -import copyImage from '../../assets/img/copy.svg'; -import { ResponseContext } from '../Contexts/ResponseContext'; +import React, { useState } from 'react'; import { OverlayTrigger, Tooltip } from 'react-bootstrap'; +import ImageButton from './ImageButton'; +import { useQueryBuilderContext } from 'app/pages/querybuilder/Components/Contexts/QueryBuilderProvider'; +import copyImage from 'app/pages/querybuilder/assets/img/copy.svg'; export default function CopyResponseButton() { - const { response } = useContext(ResponseContext); + const response = useQueryBuilderContext((ctx) => ctx.http.response); const [show, setShow] = useState(false); const handleCopy = () => { @@ -20,18 +19,16 @@ export default function CopyResponseButton() { return ( <OverlayTrigger placement="left-end" - show={show} overlay={ - <Tooltip id="copy-tooltip">Response copied to clipboard</Tooltip> + <Tooltip>{show ? 'Response copied to clipboard' : 'Copy'}</Tooltip> } > <span> - <OverlayImageButton + <ImageButton className="intro-copy" image={copyImage} - height="30" - width="30" - tooltip="Copy" + height={30} + width={30} onClick={handleCopy} /> </span> 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 index 7ec6683afa3..b89e3f14b3b 100644 --- a/client/js/app/src/app/pages/querybuilder/Components/Buttons/DownloadJSONButton.jsx +++ b/client/js/app/src/app/pages/querybuilder/Components/Buttons/DownloadJSONButton.jsx @@ -1,44 +1,31 @@ -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); - }; +import React from 'react'; +import transform from 'app/pages/querybuilder/TransformVespaTrace'; +export default function DownloadJSONButton({ children, 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); + let content; + try { + content = JSON.stringify(transform(JSON.parse(response), null, 4)); + } catch (error) { + alert(`Failed to transform response to Jaeger format: ${error}`); // TODO: Change to toast + return; + } - // 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(); + // copied from safakeskinĀ“s answer on SO, link: https://stackoverflow.com/questions/55613438/reactwrite-to-json-file-or-export-download-no-server + const blob = new Blob([content], { type: 'application/json' }); + const href = URL.createObjectURL(blob); - // clean up "a" element & remove ObjectURL - document.body.removeChild(link); - URL.revokeObjectURL(href); + // create "a" HTML element with href to file + const link = document.createElement('a'); + link.href = href; + link.download = 'vespa-response.json'; + document.body.appendChild(link); + link.click(); - // open Jaeger in a new tab - window.open('http://localhost:16686/search', '__blank'); - } else { - alert('Response was empty'); - } + // clean up "a" element & remove ObjectURL + document.body.removeChild(link); + URL.revokeObjectURL(href); }; - return <SimpleButton onClick={handleClick}>{children}</SimpleButton>; + return <button onClick={handleClick}>{children}</button>; } diff --git a/client/js/app/src/app/pages/querybuilder/Components/Buttons/ImageButton.jsx b/client/js/app/src/app/pages/querybuilder/Components/Buttons/ImageButton.jsx index f620146bea5..711229d82cd 100644 --- a/client/js/app/src/app/pages/querybuilder/Components/Buttons/ImageButton.jsx +++ b/client/js/app/src/app/pages/querybuilder/Components/Buttons/ImageButton.jsx @@ -4,14 +4,13 @@ export default function ImageButton({ onClick, children, className, - id, image, - height = '15', - width = '15', + height = 15, + width = 15, style, }) { return ( - <button id={id} className={className} onClick={onClick}> + <button className={className} onClick={onClick}> <img src={image} height={height} 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 deleted file mode 100644 index 788d88fd0e6..00000000000 --- a/client/js/app/src/app/pages/querybuilder/Components/Buttons/OverlayImageButton.jsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; -import Tooltip from 'react-bootstrap/Tooltip'; -import ImageButton from './ImageButton'; - -export default function OverlayImageButton({ - onClick, - children, - className, - id, - image, - height = '15', - width = '15', - style, - tooltip, -}) { - return ( - <OverlayTrigger - placement="right" - delay={{ show: 250, hide: 400 }} - overlay={<Tooltip id="button-tooltip">{tooltip}</Tooltip>} - > - <span> - <ImageButton - id={id} - className={className} - image={image} - height={height} - width={width} - style={style} - onClick={onClick} - > - {children} - </ImageButton> - </span> - </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 df380c62fa1..f01f5b177a5 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 @@ -1,15 +1,14 @@ -import React, { useContext, useState } from 'react'; -import ImageButton from './ImageButton'; +import React, { useState } from 'react'; import pasteImage from '../../assets/img/paste.svg'; -import { QueryInputContext } from '../Contexts/QueryInputContext'; +import ImageButton from './ImageButton'; +import { + ACTION, + dispatch, +} from 'app/pages/querybuilder/Components/Contexts/QueryBuilderProvider'; export default function PasteJSONButton() { - const { setInputs, setId, levelZeroParameters, childMap } = - useContext(QueryInputContext); const [paste, setPaste] = useState(false); - //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); @@ -21,69 +20,15 @@ export default function PasteJSONButton() { e.stopPropagation(); e.preventDefault(); const pastedData = e.clipboardData.getData('text'); - alert('Converting JSON: \n\n ' + pastedData); window.removeEventListener('paste', handlePaste); - convertPastedJSON(pastedData); - }; - - const convertPastedJSON = (pastedData) => { - try { - var json = JSON.parse(pastedData); - 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, 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 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; - 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 { - 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'] = []; - } - id += 1; - newInputs.push(newInput); - } - setId(id); - return newInputs; + dispatch(ACTION.SET_QUERY, pastedData); }; return ( <> <ImageButton - id="pasteJSON" className="pasteJSON" image={pasteImage} - //style={{ marginTop: '-2px', marginRight: '3px' }} onClick={handleClick} > {paste ? 'Press CMD + V' : 'Paste JSON'} diff --git a/client/js/app/src/app/pages/querybuilder/Components/Buttons/ShowQueryButton.jsx b/client/js/app/src/app/pages/querybuilder/Components/Buttons/ShowQueryButton.jsx deleted file mode 100644 index 789fc387b38..00000000000 --- a/client/js/app/src/app/pages/querybuilder/Components/Buttons/ShowQueryButton.jsx +++ /dev/null @@ -1,29 +0,0 @@ -import React, { useContext, useState } from 'react'; -import { QueryContext } from '../Contexts/QueryContext'; -import SimpleButton from './SimpleButton'; - -export default function ShowQueryButton() { - const { query, showQuery, setShowQuery } = useContext(QueryContext); - - const handleClick = () => { - setShowQuery(!showQuery); - }; - - return ( - <> - <SimpleButton className="showJSON" onClick={handleClick}> - Show query JSON - </SimpleButton> - {showQuery && ( - <textarea - id="jsonquery" - className="responsebox" - readOnly - cols="70" - rows="15" - value={query} - ></textarea> - )} - </> - ); -} diff --git a/client/js/app/src/app/pages/querybuilder/Components/Buttons/SimpleButton.jsx b/client/js/app/src/app/pages/querybuilder/Components/Buttons/SimpleButton.jsx deleted file mode 100644 index a153c9577e4..00000000000 --- a/client/js/app/src/app/pages/querybuilder/Components/Buttons/SimpleButton.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; - -export default function SimpleButton({ onClick, children, className, id }) { - return ( - <button id={id} className={className} onClick={onClick}> - {children} - </button> - ); -} diff --git a/client/js/app/src/app/pages/querybuilder/Components/Contexts/QueryBuilderProvider.jsx b/client/js/app/src/app/pages/querybuilder/Components/Contexts/QueryBuilderProvider.jsx new file mode 100644 index 00000000000..2eebbb9b8b5 --- /dev/null +++ b/client/js/app/src/app/pages/querybuilder/Components/Contexts/QueryBuilderProvider.jsx @@ -0,0 +1,146 @@ +import React, { useReducer } from 'react'; +import { createContext, useContextSelector } from 'use-context-selector'; +import { cloneDeep, last } from 'lodash'; +import parameters from 'app/pages/querybuilder/parameters'; + +let _dispatch; +const root = { type: { children: parameters } }; +const context = createContext(null); + +export const ACTION = Object.freeze({ + SET_QUERY: 0, + SET_HTTP: 1, + + INPUT_ADD: 10, + INPUT_UPDATE: 11, + INPUT_REMOVE: 12, +}); + +function inputsToJson(inputs) { + return Object.fromEntries( + inputs.map(({ children, input, type: { name, type } }) => [ + name, + children ? inputsToJson(children) : parseInput(input, type), + ]) + ); +} + +function jsonToInputs(json, parent) { + return Object.entries(json).map(([key, value], i) => { + const node = { + id: parent.id ? `${parent.id}.${i}` : i.toString(), + type: parent.type.children[key], + parent, + }; + if (typeof value === 'object') { + node.input = ''; + node.children = jsonToInputs(value, node); + } else node.input = value.toString(); + return node; + }); +} + +function parseInput(input, type) { + if (type === 'Integer' || type === 'Long') return parseInt(input); + if (type === 'Float') return parseFloat(input); + if (type === 'Boolean') return input.toLowerCase() === 'true'; + return input; +} + +function inputAdd(query, { id: parentId, type: typeName }) { + const inputs = cloneDeep(query.children); + const parent = parentId ? findInput(inputs, parentId) : query; + + const nextId = + parseInt(last(last(parent.children)?.id?.split('.')) ?? -1) + 1; + const id = parentId ? `${parentId}.${nextId}` : nextId.toString(); + const type = parent.type.children[typeName]; + + parent.children.push( + Object.assign( + { id, input: '', type, parent }, + type.children && { children: [] } + ) + ); + return { ...query, children: inputs }; +} + +function inputUpdate(query, { id, ...props }) { + const keys = Object.keys(props); + if (keys.length !== 1) + throw new Error(`Expected to update exactly 1 input prop, got: ${keys}`); + if (!['input', 'type'].includes(keys[0])) + throw new Error(`Cannot update key ${keys[0]}`); + + const inputs = cloneDeep(query.children); + const node = Object.assign(findInput(inputs, id), props); + if (node.type.children) node.children = []; + else delete node.children; + return { ...query, children: inputs }; +} + +function findInput(inputs, id, Delete = false) { + let end = -1; + while ((end = id.indexOf('.', end + 1)) > 0) + inputs = inputs.find((input) => input.id === id.substring(0, end)).children; + const index = inputs.findIndex((input) => input.id === id); + return Delete ? inputs.splice(index, 1)[0] : inputs[index]; +} + +function reducer(state, action) { + const result = preReducer(state, action); + if (state.query.children !== result.query.children) { + const json = inputsToJson(result.query.children); + result.query.input = JSON.stringify(json, null, 4); + } + + return result; +} +function preReducer(state, { action, data }) { + switch (action) { + case ACTION.SET_QUERY: { + try { + const children = jsonToInputs(JSON.parse(data), root); + return { ...state, query: { ...root, children } }; + } catch (error) { + alert(`Failed to parse query: ${error}`); // TODO: Change to toast + return state; + } + } + case ACTION.SET_HTTP: + return { ...state, http: data }; + + case ACTION.INPUT_ADD: + return { ...state, query: inputAdd(state.query, data) }; + case ACTION.INPUT_UPDATE: + return { ...state, query: inputUpdate(state.query, data) }; + case ACTION.INPUT_REMOVE: { + const inputs = cloneDeep(state.query.children); + findInput(inputs, data, true); + return { ...state, query: { ...state.query, children: inputs } }; + } + + default: + throw new Error(`Unknown action ${action}`); + } +} + +export function QueryBuilderProvider({ children }) { + const [value, dispatch] = useReducer( + reducer, + { http: {}, query: { ...root, input: '', children: [] } }, + (s) => reducer(s, { action: ACTION.SET_QUERY, data: '{"yql":""}' }) + ); + _dispatch = dispatch; + + return <context.Provider value={value}>{children}</context.Provider>; +} + +export function useQueryBuilderContext(selector) { + const func = typeof selector === 'string' ? (c) => c[selector] : selector; + return useContextSelector(context, func); +} + +export function dispatch(action, data) { + _dispatch({ action, data }); +} diff --git a/client/js/app/src/app/pages/querybuilder/Components/Contexts/QueryContext.jsx b/client/js/app/src/app/pages/querybuilder/Components/Contexts/QueryContext.jsx deleted file mode 100644 index 00644c21d41..00000000000 --- a/client/js/app/src/app/pages/querybuilder/Components/Contexts/QueryContext.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import React, { createContext, useState } from 'react'; - -export const QueryContext = createContext(); - -export const QueryProvider = (prop) => { - const [query, setQuery] = useState(''); - const [showQuery, setShowQuery] = useState(false); - - return ( - <QueryContext.Provider value={{ query, setQuery, showQuery, setShowQuery }}> - {prop.children} - </QueryContext.Provider> - ); -}; 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 deleted file mode 100644 index bc21ea81d9a..00000000000 --- a/client/js/app/src/app/pages/querybuilder/Components/Contexts/QueryInputContext.jsx +++ /dev/null @@ -1,160 +0,0 @@ -import React, { useState, createContext } from 'react'; - -export const QueryInputContext = createContext(); - -export const QueryInputProvider = (prop) => { - // This is the id of the newest QueryInput, gets updated each time a new one is added - const [id, setId] = useState(1); - - // These are the methods that can be chosen in a QueryInput - const levelZeroParameters = { - yql: { name: 'yql', type: 'String', hasChildren: false }, - hits: { name: 'hits', type: 'Integer', hasChildren: false }, - offset: { name: 'offset', type: 'Integer', hasChildren: false }, - queryProfile: { name: 'queryProfile', type: 'String', hasChildren: false }, - noCache: { name: 'noCache', type: 'Boolean', hasChildren: false }, - groupingSessionCache: { - name: 'groupingSessionCache', - type: 'Boolean', - hasChildren: false, - }, - 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 }, - explainlevel: { name: 'explainlevel', type: 'Integer', hasChildren: false }, - model: { name: 'model', type: 'Parent', hasChildren: true }, - ranking: { name: 'ranking', type: 'Parent', hasChildren: true }, - collapse: { name: 'collapse', type: 'Parent', hasChildren: true }, - collapsesize: { name: 'collapsesize', type: 'Integer', hasChildren: false }, - collapsefield: { - name: 'collapsefield', - type: 'String', - hasChildren: false, - }, - presentation: { name: 'presentation', type: 'Parent', hasChildren: true }, - pos: { name: 'pos', type: 'Parent', hasChildren: true }, - streaming: { name: 'streaming', type: 'Parent', hasChildren: true }, - rules: { name: 'rules', type: 'Parent', hasChildren: true }, - recall: { name: 'recall', type: 'List', hasChildren: false }, - user: { name: 'user', type: 'String', hasChildren: false }, - metrics: { name: 'metrics', type: 'Parent', hasChildren: true }, - }; - - // Children of the levelZeroParameters that have child attributes - const childMap = { - collapse: { - summary: { name: 'summary', type: 'String', hasChildren: false }, - }, - metrics: { - ignore: { name: 'ignore', type: 'Boolean', hasChildren: false }, - }, - model: { - defaultIndex: { - name: 'defaultIndex', - type: 'String', - hasChildren: false, - }, - encoding: { name: 'encoding', type: 'String', hasChildren: false }, - language: { name: 'language', type: 'String', hasChildren: false }, - queryString: { name: 'queryString', type: 'String', hasChildren: false }, - restrict: { name: 'restrict', type: 'List', hasChildren: false }, - searchPath: { name: 'searchPath', type: 'String', hasChildren: false }, - sources: { name: 'sources', type: 'List', hasChildren: false }, - type: { name: 'type', type: 'String', hasChildren: false }, - }, - pos: { - ll: { name: 'll', type: 'String', hasChildren: false }, - radius: { name: 'radius', type: 'String', hasChildren: false }, - bb: { name: 'bb', type: 'List', hasChildren: false }, - attribute: { name: 'attribute', type: 'String', hasChildren: false }, - }, - presentation: { - bolding: { name: 'bolding', type: 'Boolean', hasChildren: false }, - format: { name: 'format', type: 'String', hasChildren: false }, - summary: { name: 'summary', type: 'String', hasChildren: false }, - template: { name: 'template', type: 'String', hasChildren: false }, - timing: { name: 'timing', type: 'Boolean', hasChildren: false }, - }, - ranking: { - location: { name: 'location', type: 'String', hasChildren: false }, - features: { name: 'features', type: 'String', hasChildren: false }, - listFeatures: { - name: 'listFeatures', - type: 'Boolean', - hasChildren: false, - }, - profile: { name: 'profile', type: 'String', hasChildren: false }, - properties: { name: 'properties', type: 'String', hasChildren: false }, - sorting: { name: 'sorting', type: 'String', hasChildren: false }, - freshness: { name: 'freshness', type: 'String', hasChildren: false }, - queryCache: { name: 'queryCache', type: 'Boolean', hasChildren: false }, - matchPhase: { name: 'matchPhase', type: 'Parent', hasChildren: true }, - }, - ranking_matchPhase: { - maxHits: { name: 'maxHits', type: 'Long', hasChildren: false }, - attribute: { name: 'attribute', type: 'String', hasChildren: false }, - ascending: { name: 'ascending', type: 'Boolean', hasChildren: false }, - diversity: { name: 'diversity', type: 'Parent', hasChildren: true }, - }, - ranking_matchPhase_diversity: { - attribute: { name: 'attribute', type: 'String', hasChildren: false }, - minGroups: { name: 'minGroups', type: 'Long', hasChildren: false }, - }, - rules: { - off: { name: 'off', type: 'Boolean', hasChildren: false }, - rulebase: { name: 'rulebase', type: 'String', hasChildren: false }, - }, - streaming: { - userid: { name: 'userid', type: 'Integer', hasChildren: false }, - groupname: { name: 'groupname', type: 'String', hasChildren: false }, - selection: { name: 'selection', type: 'String', hasChildren: false }, - priority: { name: 'priority', type: 'String', hasChildren: false }, - maxbucketspervisitor: { - name: 'maxbucketspervisitor', - type: 'Integer', - hasChildren: false, - }, - }, - trace: { - timestamps: { name: 'timestamps', type: 'Boolean', hasChildren: false }, - }, - tracelevel: { - rules: { name: 'rules', type: 'Integer', hasChildren: false }, - }, - }; - - const firstChoice = levelZeroParameters[Object.keys(levelZeroParameters)[0]]; - - const [inputs, setInputs] = useState([ - { - id: '1', - type: firstChoice.name, - typeof: firstChoice.type, - input: '', - hasChildren: false, - children: [], - }, - ]); - - const [selectedItems, setSelectedItems] = useState([]); - - return ( - <QueryInputContext.Provider - value={{ - inputs, - setInputs, - id, - setId, - levelZeroParameters, - childMap, - selectedItems, - setSelectedItems, - }} - > - {prop.children} - </QueryInputContext.Provider> - ); -}; diff --git a/client/js/app/src/app/pages/querybuilder/Components/Contexts/ResponseContext.jsx b/client/js/app/src/app/pages/querybuilder/Components/Contexts/ResponseContext.jsx deleted file mode 100644 index 54f5fc955fd..00000000000 --- a/client/js/app/src/app/pages/querybuilder/Components/Contexts/ResponseContext.jsx +++ /dev/null @@ -1,13 +0,0 @@ -import React, { createContext, useState } from 'react'; - -export const ResponseContext = createContext(); - -export const ResponseProvider = (prop) => { - const [response, setResponse] = useState(''); - - return ( - <ResponseContext.Provider value={{ response, setResponse }}> - {prop.children} - </ResponseContext.Provider> - ); -}; diff --git a/client/js/app/src/app/pages/querybuilder/Components/Navigation/CustomNavbar.jsx b/client/js/app/src/app/pages/querybuilder/Components/Navigation/CustomNavbar.jsx deleted file mode 100644 index 6ffeae4caed..00000000000 --- a/client/js/app/src/app/pages/querybuilder/Components/Navigation/CustomNavbar.jsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import { Navbar, Container, Nav } from 'react-bootstrap'; - -function CustomNavbar() { - return ( - <Navbar collapseOnSelect variant="default" expand="sm"> - <Container> - <Navbar.Brand href="https://vespa.ai"> - Vespa. Big data. Real time. - </Navbar.Brand> - <Navbar.Toggle aria-controls="responsive-navbar-nav" /> - <Navbar.Collapse id="responsive-navbar-nav"> - <Nav> - <Nav.Link href="https://blog.vespa.ai/">Blog</Nav.Link> - <Nav.Link variant="link" href="https://twitter.com/vespaengine"> - Twitter - </Nav.Link> - <Nav.Link variant="link" href="https://docs.vespa.ai"> - Docs - </Nav.Link> - <Nav.Link variant="link" href="https://github.com/vespa-engine"> - GitHub - </Nav.Link> - <Nav.Link - variant="link" - href="https://docs.vespa.ai/en/getting-started.html" - > - Get Started Now - </Nav.Link> - </Nav> - </Navbar.Collapse> - </Container> - </Navbar> - ); -} - -export default CustomNavbar; diff --git a/client/js/app/src/app/pages/querybuilder/Components/Navigation/Footer.jsx b/client/js/app/src/app/pages/querybuilder/Components/Navigation/Footer.jsx deleted file mode 100644 index b110bce943d..00000000000 --- a/client/js/app/src/app/pages/querybuilder/Components/Navigation/Footer.jsx +++ /dev/null @@ -1,68 +0,0 @@ -import React from 'react'; -import { Table } from 'react-bootstrap'; - -export default function Footer() { - return ( - <footer> - <Table borderless size="sm"> - <thead className="footer-title"> - <tr> - <th>Resources</th> - <th>Contact</th> - <th>Community</th> - </tr> - </thead> - <tbody> - <tr> - <th> - <a href="https://docs.vespa.ai/en/vespa-quick-start.html"> - Getting Started - </a> - </th> - <th> - <a href="https://twitter.com/vespaengine">Twitter</a> - </th> - <th> - <a href="https://github.com/vespa-engine/vespa/blob/master/CONTRIBUTING.md"> - Contributing - </a> - </th> - </tr> - <tr> - <th> - <a href="https://docs.vespa.ai">Documentation</a> - </th> - <th> - <a href="mailto:info@vespa.ai">info@vespa.ai</a> - </th> - <th> - <a href="https://stackoverflow.com/questions/tagged/vespa"> - Stack Overflow - </a> - </th> - </tr> - <tr> - <th> - <a href="https://github.com/vespa-engine/vespa">Open source</a> - </th> - <th> - <a href="https://github.com/vespa-engine/vespa/issues">Issues</a> - </th> - <th> - <a href="https://gitter.im/vespa-engine/Lobby">Gitter</a> - </th> - </tr> - </tbody> - </Table> - <div className="credits"> - <span>Copyright Yahoo</span> - Licensed under{' '} - <a href="https://github.com/vespa-engine/vespa/blob/master/LICENSE"> - Apache License 2.0 - </a> - , <a href="https://github.com/y7kim/agency-jekyll-theme">Theme</a> by - Rick K. - </div> - </footer> - ); -} diff --git a/client/js/app/src/app/pages/querybuilder/Components/Text/Info.jsx b/client/js/app/src/app/pages/querybuilder/Components/Text/Info.jsx deleted file mode 100644 index d921c53a602..00000000000 --- a/client/js/app/src/app/pages/querybuilder/Components/Text/Info.jsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import { OverlayTrigger, Popover } from 'react-bootstrap'; -import image from '../../assets/img/information.svg'; - -export default function Info({ - id, - className = 'tip', - height = 15, - width = 15, -}) { - const popover = ( - <Popover id={`inf${id}`}> - <Popover.Header as="h3">Popover right</Popover.Header> - <Popover.Body>Content</Popover.Body> - </Popover> - ); - - return ( - <> - <OverlayTrigger - placement="right" - delay={{ show: 250, hide: 400 }} - overlay={popover} - > - <span> - <img - src={image} - height={height} - width={width} - className="information" - alt="Missing" - /> - </span> - </OverlayTrigger> - </> - ); -} 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 deleted file mode 100644 index 88bd545282a..00000000000 --- a/client/js/app/src/app/pages/querybuilder/Components/Text/QueryDropDownForm.jsx +++ /dev/null @@ -1,75 +0,0 @@ -import React, { useContext, useState } from 'react'; -import { QueryInputContext } from '../Contexts/QueryInputContext'; -import SimpleDropDownForm from './SimpleDropDownForm'; - -export default function QueryDropdownForm({ - choices, - id, - child = false, - initial, -}) { - const { - inputs, - setInputs, - levelZeroParameters, - childMap, - selectedItems, - setSelectedItems, - } = useContext(QueryInputContext); - const [choice, setChoice] = useState(); - - /** - * Update the state of inputs to reflect the method chosen from the dropdown. - * If the prevoiusly chosen method had children they are removed. - * @param {Event} e Event containing the new type. - */ - const updateType = (e) => { - e.preventDefault(); - const newType = e.target.value; - const newInputs = inputs.slice(); - let currentId = id.substring(0, 1); - let index = newInputs.findIndex((element) => element.id === currentId); - if (child) { - 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); - let child = children[index]; - parentTypes = parentTypes + '_' + child.type; - childChoices = childMap[parentTypes]; - children = child.children; - } - index = children.findIndex((element) => element.id === id); - children[index].type = newType; - children[index].hasChildren = childChoices[newType].hasChildren; - children[index].children = []; - children[index].typeof = childChoices[newType].type; - setSelectedItems([...selectedItems, newType]); - } else { - newInputs[index].type = newType; - let hasChildren = levelZeroParameters[newType].hasChildren; - newInputs[index].hasChildren = hasChildren; - newInputs[index].children = []; - newInputs[index].typeof = levelZeroParameters[newType].type; - setSelectedItems([...selectedItems, newType]); - } - setInputs(newInputs); - setChoice(newType); - }; - - //TODO: do not display options that have been chosen - - return ( - <SimpleDropDownForm - id={id} - 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 bd41edd6008..5f5a9fdbd89 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 @@ -1,79 +1,91 @@ -import React, { useContext } from 'react'; -import SimpleButton from '../Buttons/SimpleButton'; -import SimpleForm from './SimpleForm'; +import React from 'react'; import { OverlayTrigger, Tooltip } from 'react-bootstrap'; -import { QueryInputContext } from '../Contexts/QueryInputContext'; -import QueryDropdownForm from './QueryDropDownForm'; -import AddPropertyButton from '../Buttons/AddPropertyButton'; -import QueryInputChild from './QueryInputChild'; +import SimpleDropDownForm from 'app/pages/querybuilder/Components/Text/SimpleDropDownForm'; +import { + ACTION, + dispatch, + useQueryBuilderContext, +} from 'app/pages/querybuilder/Components/Contexts/QueryBuilderProvider'; export default function QueryInput() { - const { inputs, setInputs, levelZeroParameters } = - useContext(QueryInputContext); - - function removeRow(id) { - const newList = inputs.filter((item) => item.id !== id); - setInputs(newList); - } - - const updateInput = (e) => { - e.preventDefault(); - const fid = e.target.id.replace('v', ''); - const newInputs = inputs.slice(); - const index = newInputs.findIndex((element) => element.id === fid); - newInputs[index].input = e.target.value; - setInputs(newInputs); - }; + const { children, type } = useQueryBuilderContext('query'); + return <Inputs type={type.children} inputs={children} />; +} - const setPlaceholder = (id) => { - try { - const index = inputs.findIndex((element) => element.id === id); - return inputs[index].typeof; - } catch (error) { - console.log(error); - } - }; +function Inputs({ id, type, inputs }) { + const usedTypes = inputs.map(({ type }) => type.name); + const remainingTypes = Object.fromEntries( + Object.entries(type).filter(([name]) => !usedTypes.includes(name)) + ); + const firstRemaining = Object.keys(remainingTypes)[0]; + return ( + <> + {inputs.map(({ id, input, type, children }) => ( + <Input + key={id} + types={remainingTypes} + {...{ id, input, type, children }} + /> + ))} + {firstRemaining && <AddPropertyButton id={id} type={firstRemaining} />} + </> + ); +} - const inputList = inputs.map((value) => { - return ( - <div key={value.id + value.typeof} id={value.id} className="queryinput"> - <QueryDropdownForm - choices={levelZeroParameters} - id={value.id} - initial={value.type} +function Input({ id, input, types, type, children }) { + return ( + <div className="queryinput"> + <SimpleDropDownForm + onChange={({ target }) => + dispatch(ACTION.INPUT_UPDATE, { + id, + type: types[target.value], + }) + } + options={{ [type.name]: type, ...types }} + value={type.name} + /> + {children ? ( + <Inputs id={id} type={type.children} inputs={children} /> + ) : ( + <input + size="30" + onChange={({ target }) => + dispatch(ACTION.INPUT_UPDATE, { + id, + input: target.value, + }) + } + placeholder={type.type} + value={input} /> - {value.hasChildren ? ( - <> - <AddPropertyButton id={value.id} /> - <QueryInputChild id={value.id} /> - </> - ) : ( - <SimpleForm - id={`v${value.id}`} - size="30" - onChange={updateInput} - placeholder={setPlaceholder(value.id)} - initial={value.input} - /> - )} - <OverlayTrigger - placement="right" - delay={{ show: 250, hide: 400 }} - overlay={<Tooltip id="button-tooltip">Remove row</Tooltip>} - > - <span> - <SimpleButton - id={`b${value.id}`} - className="removeRow" - onClick={() => removeRow(value.id)} - children="-" - ></SimpleButton> - </span> - </OverlayTrigger> - <br /> - </div> - ); - }); + )} + <OverlayTrigger + placement="right" + delay={{ show: 250, hide: 400 }} + overlay={<Tooltip id="button-tooltip">Remove row</Tooltip>} + > + <span> + <button + className="removeRow" + onClick={() => dispatch(ACTION.INPUT_REMOVE, id)} + > + - + </button> + </span> + </OverlayTrigger> + <br /> + </div> + ); +} - return <>{inputList}</>; +function AddPropertyButton({ id, type }) { + return ( + <button + className="addpropsbutton" + onClick={() => dispatch(ACTION.INPUT_ADD, { id, type })} + > + + Add property + </button> + ); } 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 deleted file mode 100644 index 4ab2a074214..00000000000 --- a/client/js/app/src/app/pages/querybuilder/Components/Text/QueryInputChild.jsx +++ /dev/null @@ -1,201 +0,0 @@ -import React, { useContext } from 'react'; -import AddPropertyButton from '../Buttons/AddPropertyButton'; -import { QueryInputContext } from '../Contexts/QueryInputContext'; -import QueryDropdownForm from './QueryDropDownForm'; -import SimpleForm from './SimpleForm'; -import { OverlayTrigger, Tooltip } from 'react-bootstrap'; -import SimpleButton from '../Buttons/SimpleButton'; - -export default function QueryInputChild({ id }) { - const { inputs, setInputs, childMap } = useContext(QueryInputContext); - - let index = inputs.findIndex((element) => element.id === id); - let childArray = inputs[index].children; - let currentTypes = inputs[index].type; - - /** - * Update the state of inputs to reflect what is written into the form. - * @param {Event} e Event containing the new input. - */ - const updateInput = (e) => { - e.preventDefault(); - let newInputs = inputs.slice(); - let iterId = e.target.id.replace('v', ''); - let currentId = iterId.substring(0, 1); - let index = newInputs.findIndex((element) => element.id === currentId); - let children = newInputs[index].children; - const traversedChildren = traverseChildren(iterId, children, ''); - children = traversedChildren.children; - index = children.findIndex((element) => element.id === iterId); - children[index].input = e.target.value; - setInputs(newInputs); - }; - - /** - * Returns a placeholder text for a SimpleForm component. - * @param {String} id The id of the SimpleForm component. - * @returns {String} The placeholder text - */ - const setPlaceHolder = (id) => { - let currentId = id.substring(0, 1); - let index = inputs.findIndex((element) => element.id === currentId); - let combinedType = inputs[index].type; - let children = inputs[index].children; - if (id.length > 3) { - const traversedChildren = traverseChildren(id, children, combinedType); - combinedType = traversedChildren.combinedType; - children = traversedChildren.children; - const currentChoice = childMap[combinedType]; - index = children.findIndex((element) => element.id === id); - combinedType = children[index].type; - return currentChoice[combinedType].type; - } else { - const currentChoice = childMap[combinedType]; - index = children.findIndex((element) => element.id === id); - combinedType = children[index].type; - return currentChoice[combinedType].type; - } - }; - - /** - * Removes the row with the provided id. - * @param {String} id Id of row. - */ - const removeRow = (id) => { - let newInputs = inputs.slice(); - let currentId = id.substring(0, 1); - let index = newInputs.findIndex((element) => element.id === currentId); - let children = newInputs[index].children; - const traversedChildren = traverseChildren(id, children, ''); - index = traversedChildren.children.findIndex((element) => element === id); - traversedChildren.children.splice(index, 1); - setInputs(newInputs); - }; - - /** - * Traverses the children until a child with the provided id is reached. - * @param {String} id Id of the innermost child. - * @param {Array} children Array containing serveral child objects. - * @param {String} combinedType The combined type of all traversed children - * @returns {Object} An object containing the children of the child with the provided id and the combined type. - */ - function traverseChildren(id, children, combinedType) { - let currentId; - let index; - for (let i = 3; i < id.length; i += 2) { - currentId = id.substring(0, i); - index = children.findIndex((element) => element.id === currentId); - combinedType = combinedType + '_' + children[index].type; - children = children[index].children; - } - return { children: children, combinedType: combinedType }; - } - - 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 ? ( - <> - <AddPropertyButton id={child.id} /> - </> - ) : ( - <SimpleForm - id={`v${child.id}`} - size="30" - onChange={updateInput} - placeholder={setPlaceHolder(child.id)} - inital={child.input} - /> - )} - <OverlayTrigger - placement="right" - delay={{ show: 250, hide: 400 }} - overlay={<Tooltip id="button-tooltip">Remove row</Tooltip>} - > - <span> - <SimpleButton - id={`b${child.id}`} - className="removeRow" - onClick={() => removeRow(child.id)} - children="-" - ></SimpleButton> - </span> - </OverlayTrigger> - <br /> - <Child - type={currentTypes + '_' + child.type} - child={child} - onChange={updateInput} - placeholder={setPlaceHolder} - removeRow={removeRow} - /> - </div> - ); - }); - - return <>{inputList}</>; -} - -function Child({ child, type, onChange, placeholder, removeRow }) { - const { childMap } = useContext(QueryInputContext); - - const nestedChildren = (child.children || []).map((child) => { - return ( - <div key={child.id}> - <QueryDropdownForm - choices={childMap[type]} - id={child.id} - child={true} - initial={child.type} - /> - {child.hasChildren ? ( - <> - <AddPropertyButton id={child.id} /> - </> - ) : ( - <SimpleForm - id={`v${child.id}`} - size="30" - onChange={onChange} - placeholder={placeholder(child.id)} - initial={child.input} - /> - )} - <OverlayTrigger - placement="right" - delay={{ show: 250, hide: 400 }} - overlay={<Tooltip id="button-tooltip">Remove row</Tooltip>} - > - <span> - <SimpleButton - id={`b${child.id}`} - className="removeRow" - onClick={() => removeRow(child.id)} - children="-" - ></SimpleButton> - </span> - </OverlayTrigger> - <br /> - <Child - child={child} - id={child.id} - type={type + '_' + child.type} - onChange={onChange} - placeholder={placeholder} - removeRow={removeRow} - /> - </div> - ); - }); - - return <>{nestedChildren}</>; -} 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 deleted file mode 100644 index dac98271965..00000000000 --- a/client/js/app/src/app/pages/querybuilder/Components/Text/ResponseBox.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { useContext } from 'react'; -import { ResponseContext } from '../Contexts/ResponseContext'; - -export default function ResponseBox() { - const { response } = useContext(ResponseContext); - - return ( - <textarea - id="responsetext" - className="responsebox" - readOnly - cols="70" - rows="25" - value={response} - /> - ); -} 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 a3714f27fb5..303bc8bfc83 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,24 +1,32 @@ -import React, { useContext, useEffect, useState } from 'react'; -import SimpleButton from '../Buttons/SimpleButton'; -import { QueryInputContext } from '../Contexts/QueryInputContext'; -import { ResponseContext } from '../Contexts/ResponseContext'; -import { QueryContext } from '../Contexts/QueryContext'; -import SimpleForm from './SimpleForm'; +import React, { useState } from 'react'; import SimpleDropDownForm from './SimpleDropDownForm'; +import { + ACTION, + dispatch, + useQueryBuilderContext, +} from 'app/pages/querybuilder/Components/Contexts/QueryBuilderProvider'; + +function send(method, url, query) { + dispatch(ACTION.SET_HTTP, { loading: true }); + fetch(url, { + method, + headers: { 'Content-Type': 'application/json;charset=utf-8' }, + body: query, + }) + .then((response) => response.json()) + .then((result) => + dispatch(ACTION.SET_HTTP, { + response: JSON.stringify(result, null, 4), + }) + ) + .catch((error) => dispatch(ACTION.SET_HTTP, { error })); +} export default function SendQuery() { - const { inputs } = useContext(QueryInputContext); - const { setResponse } = useContext(ResponseContext); - const { showQuery, setQuery } = useContext(QueryContext); - const messageMethods = { post: { name: 'POST' }, get: { name: 'GET' } }; const [method, setMethod] = useState(messageMethods.post.name); const [url, setUrl] = useState('http://localhost:8080/search/'); - - useEffect(() => { - const query = buildJSON(inputs, {}); - setQuery(JSON.stringify(query, undefined, 4)); - }, [showQuery]); + const query = useQueryBuilderContext((ctx) => ctx.query.input); const updateMethod = (e) => { e.preventDefault(); @@ -26,80 +34,23 @@ export default function SendQuery() { setMethod(newMethod); }; - function handleClick() { - const json = buildJSON(inputs, {}); - send(json); - } - - async function send(json) { - let responses = await fetch(url, { - method: method, - headers: { 'Content-Type': 'application/json;charset=utf-8' }, - body: JSON.stringify(json), - }); - if (responses.ok) { - let result = await responses.json(); - let resultObject = JSON.stringify(result, undefined, 4); - setResponse(resultObject); - } - } - - function buildJSON(inputs, json) { - let queryJson = json; - for (let i = 0; i < inputs.length; i++) { - let current = inputs[i]; - let key = current.type; - if (current.hasChildren) { - let child = {}; - child = buildJSON(current.children, child); - queryJson[key] = child; - } else { - queryJson[key] = parseInput(current.input, current.typeof); - } - } - return queryJson; - } - - function parseInput(input, type) { - switch (type) { - case 'Integer': - case 'Long': - return parseInt(input); - - case 'Float': - return parseFloat(input); - - case 'Boolean': - return input.toLowerCase() === 'true' ? true : false; - - default: - return input; - } - } - - const updateUrl = (e) => { - const newUrl = e.target.value; - setUrl(newUrl); - }; - return ( <> <SimpleDropDownForm - choices={messageMethods} - id="method" + options={messageMethods} + value={method} className="methodselector" onChange={updateMethod} /> - <SimpleForm - id="url" - className="textbox" - initial={url} + <input size="30" - onChange={updateUrl} + className="textbox" + value={url} + onChange={({ target }) => setUrl(target.value)} /> - <SimpleButton id="send" className="button" onClick={handleClick}> + <button className="button" onClick={() => send(method, url, query)}> Send - </SimpleButton> + </button> </> ); } 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 94c6c01b619..99342a5ae81 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 @@ -1,44 +1,18 @@ -import React, { useContext, useEffect } from 'react'; -import { QueryInputContext } from '../Contexts/QueryInputContext'; +import React from 'react'; export default function SimpleDropDownForm({ - choices, - id, - className = 'input', - onChange, + options, value, - initial, + className = 'input', + ...props }) { - const { selectedItems } = useContext(QueryInputContext); - - //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) - ); - useEffect(() => { - filtered = Object.keys(choices).filter( - (choice) => !selectedItems.includes(choice) - ); - }, [selectedItems]); - - const options = Object.keys(choices).map((choice) => { - return ( - <option className="options" key={choice} value={choices[choice].name}> - {choices[choice].name} - </option> - ); - }); - return ( - <form id={id}> - <select - className={className} - id={id} - defaultValue={initial} - onChange={onChange} - > - {options} - </select> - </form> + <select className={className} {...props} value={value}> + {Object.values(options).map(({ name }) => ( + <option className="options" key={name} value={name}> + {name} + </option> + ))} + </select> ); } diff --git a/client/js/app/src/app/pages/querybuilder/Components/Text/SimpleForm.jsx b/client/js/app/src/app/pages/querybuilder/Components/Text/SimpleForm.jsx deleted file mode 100644 index bb6aaa13529..00000000000 --- a/client/js/app/src/app/pages/querybuilder/Components/Text/SimpleForm.jsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import { useState } from 'react'; - -export default function SimpleForm({ - id, - className = 'propvalue', - initial, - size = '20', - onChange, - placeholder, -}) { - SimpleForm.defaultProps = { - onChange: handleChange, - }; - const [input, setValue] = useState(initial); - - function handleChange(e) { - setValue(e.target.value); - } - - return ( - <form className={className} id={id}> - <input - size={size} - type="text" - id={id} - className={className} - defaultValue={initial} - onChange={onChange} - placeholder={placeholder} - /> - </form> - ); -} diff --git a/client/js/app/src/app/pages/querybuilder/Components/Text/TextBox.jsx b/client/js/app/src/app/pages/querybuilder/Components/Text/TextBox.jsx deleted file mode 100644 index 022b250da7c..00000000000 --- a/client/js/app/src/app/pages/querybuilder/Components/Text/TextBox.jsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; - -export default function TextBox({ id, className, children }) { - return ( - <p className={className} id={id}> - {children} - </p> - ); -} diff --git a/client/js/app/src/app/pages/querybuilder/parameters.jsx b/client/js/app/src/app/pages/querybuilder/parameters.jsx new file mode 100644 index 00000000000..6557cfc0ea0 --- /dev/null +++ b/client/js/app/src/app/pages/querybuilder/parameters.jsx @@ -0,0 +1,130 @@ +export default { + yql: { name: 'yql', type: 'String' }, + hits: { name: 'hits', type: 'Integer' }, + offset: { name: 'offset', type: 'Integer' }, + queryProfile: { name: 'queryProfile', type: 'String' }, + noCache: { name: 'noCache', type: 'Boolean' }, + groupingSessionCache: { name: 'groupingSessionCache', type: 'Boolean' }, + searchChain: { name: 'searchChain', type: 'String' }, + timeout: { name: 'timeout', type: 'Float' }, + trace: { + name: 'trace', + type: 'Parent', + children: { + timestamps: { name: 'timestamps', type: 'Boolean' }, + }, + }, + tracelevel: { + name: 'tracelevel', + type: 'Parent', + children: { + rules: { name: 'rules', type: 'Integer' }, + }, + }, + traceLevel: { name: 'traceLevel', type: 'Integer' }, + explainLevel: { name: 'explainLevel', type: 'Integer' }, + explainlevel: { name: 'explainlevel', type: 'Integer' }, + model: { + name: 'model', + type: 'Parent', + children: { + defaultIndex: { name: 'defaultIndex', type: 'String' }, + encoding: { name: 'encoding', type: 'String' }, + language: { name: 'language', type: 'String' }, + queryString: { name: 'queryString', type: 'String' }, + restrict: { name: 'restrict', type: 'List' }, + searchPath: { name: 'searchPath', type: 'String' }, + sources: { name: 'sources', type: 'List' }, + type: { name: 'type', type: 'String' }, + }, + }, + ranking: { + name: 'ranking', + type: 'Parent', + children: { + location: { name: 'location', type: 'String' }, + features: { name: 'features', type: 'String' }, + listFeatures: { name: 'listFeatures', type: 'Boolean' }, + profile: { name: 'profile', type: 'String' }, + properties: { name: 'properties', type: 'String' }, + sorting: { name: 'sorting', type: 'String' }, + freshness: { name: 'freshness', type: 'String' }, + queryCache: { name: 'queryCache', type: 'Boolean' }, + matchPhase: { + name: 'matchPhase', + type: 'Parent', + children: { + maxHits: { name: 'maxHits', type: 'Long' }, + attribute: { name: 'attribute', type: 'String' }, + ascending: { name: 'ascending', type: 'Boolean' }, + diversity: { + name: 'diversity', + type: 'Parent', + children: { + attribute: { name: 'attribute', type: 'String' }, + minGroups: { name: 'minGroups', type: 'Long' }, + }, + }, + }, + }, + }, + }, + collapse: { + name: 'collapse', + type: 'Parent', + children: { + summary: { name: 'summary', type: 'String' }, + }, + }, + collapsesize: { name: 'collapsesize', type: 'Integer' }, + collapsefield: { name: 'collapsefield', type: 'String' }, + presentation: { + name: 'presentation', + type: 'Parent', + children: { + bolding: { name: 'bolding', type: 'Boolean' }, + format: { name: 'format', type: 'String' }, + summary: { name: 'summary', type: 'String' }, + template: { name: 'template', type: 'String' }, + timing: { name: 'timing', type: 'Boolean' }, + }, + }, + pos: { + name: 'pos', + type: 'Parent', + children: { + ll: { name: 'll', type: 'String' }, + radius: { name: 'radius', type: 'String' }, + bb: { name: 'bb', type: 'List' }, + attribute: { name: 'attribute', type: 'String' }, + }, + }, + streaming: { + name: 'streaming', + type: 'Parent', + children: { + userid: { name: 'userid', type: 'Integer' }, + groupname: { name: 'groupname', type: 'String' }, + selection: { name: 'selection', type: 'String' }, + priority: { name: 'priority', type: 'String' }, + maxbucketspervisitor: { name: 'maxbucketspervisitor', type: 'Integer' }, + }, + }, + rules: { + name: 'rules', + type: 'Parent', + children: { + off: { name: 'off', type: 'Boolean' }, + rulebase: { name: 'rulebase', type: 'String' }, + }, + }, + recall: { name: 'recall', type: 'List' }, + user: { name: 'user', type: 'String' }, + metrics: { + name: 'metrics', + type: 'Parent', + children: { + ignore: { name: 'ignore', type: 'Boolean' }, + }, + }, +}; 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 42a4ed829c3..b101e8639fb 100644 --- a/client/js/app/src/app/pages/querybuilder/query-builder.jsx +++ b/client/js/app/src/app/pages/querybuilder/query-builder.jsx @@ -1,53 +1,58 @@ import React from 'react'; import QueryInput from './Components/Text/QueryInput'; -import TextBox from './Components/Text/TextBox'; -import AddQueryInput from './Components/Buttons/AddQueryInputButton'; -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 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 { + QueryBuilderProvider, + useQueryBuilderContext, +} from 'app/pages/querybuilder/Components/Contexts/QueryBuilderProvider'; import '../../styles/agency.css'; import '../../styles/vespa.css'; -export function QueryBuilder() { +function QueryBox() { + const query = useQueryBuilderContext((ctx) => ctx.query.input); + return <textarea readOnly cols="70" rows="15" value={query}></textarea>; +} + +function ResponseBox() { + const response = useQueryBuilderContext((ctx) => ctx.http.response); return ( <> - <header> - <div className="intro container"> - <TextBox className={'intro-lead-in'}>Vespa Search Engine</TextBox> - <TextBox className={'intro-long'}> - Select the method for sending a request and construct a query. - </TextBox> - <ResponseProvider> - <QueryProvider> - <QueryInputProvider> - <SendQuery /> - <br /> - <div id="request"> - <QueryInput /> - </div> - <br /> - <AddQueryInput /> - <br /> - <PasteJSONButton /> - </QueryInputProvider> - <ShowQueryButton /> - </QueryProvider> - <TextBox className="response">Response</TextBox> - <ResponseBox /> - <CopyResponseButton /> - <DownloadJSONButton>Download response as JSON</DownloadJSONButton> - </ResponseProvider> + <textarea readOnly cols="70" rows="25" value={response} /> + <CopyResponseButton /> + <DownloadJSONButton response={response}> + Download in Jeager format + </DownloadJSONButton> + </> + ); +} + +export function QueryBuilder() { + return ( + <header> + <div className="intro container"> + <p className="intro-lead-in">Vespa Search Engine</p> + <p className="intro-long"> + Select the method for sending a request and construct a query. + </p> + <QueryBuilderProvider> + <SendQuery /> <br /> + <div id="request"> + <QueryInput /> + </div> <br /> - </div> - </header> - </> + <PasteJSONButton /> + <QueryBox /> + <p className="response">Response</p> + <ResponseBox /> + </QueryBuilderProvider> + <br /> + <br /> + </div> + </header> ); } 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 c700b73ebba..c3212c70c8a 100644 --- a/client/js/app/src/app/pages/querytracer/query-tracer.jsx +++ b/client/js/app/src/app/pages/querytracer/query-tracer.jsx @@ -1,14 +1,9 @@ -import React, { useContext } from 'react'; +import React, { useState } from 'react'; import DownloadJSONButton from '../querybuilder/Components/Buttons/DownloadJSONButton'; -import { ResponseContext } from '../querybuilder/Components/Contexts/ResponseContext'; import { Container } from 'app/components'; export function QueryTracer() { - const { response, setResponse } = useContext(ResponseContext); - - const updateResponse = (e) => { - setResponse(e.target.value); - }; + const [response, setResponse] = useState(''); return ( <Container> @@ -16,10 +11,10 @@ export function QueryTracer() { cols="70" rows="25" value={response} - onChange={updateResponse} + onChange={({ target }) => setResponse(target.value)} ></textarea> - <DownloadJSONButton> - Convert to Jeager format and download trace + <DownloadJSONButton response={response}> + Download in Jeager format </DownloadJSONButton> </Container> ); diff --git a/client/js/app/yarn.lock b/client/js/app/yarn.lock index 2261621f31b..6ef7814c807 100644 --- a/client/js/app/yarn.lock +++ b/client/js/app/yarn.lock @@ -1798,6 +1798,11 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash@^4: + version "4.17.21" + resolved "https://registry.npm.ouryahoo.com:4443/npm-registry/api/npm/npm-registry/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -2168,9 +2173,9 @@ react-refresh@^0.13.0: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.13.0.tgz#cbd01a4482a177a5da8d44c9755ebb1f26d5a1c1" integrity sha512-XP8A9BT0CpRBD+NYLLeIhld/RqG9+gktUjW1FkE+Vm7OCinbG1SshcK5tb9ls4kzvjZr9mOQc7HYgBngEyPAXg== -react-router-dom@^6.3.0: +react-router-dom@^6: version "6.3.0" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.3.0.tgz#a0216da813454e521905b5fa55e0e5176123f43d" + resolved "https://registry.npm.ouryahoo.com:4443/npm-registry/api/npm/npm-registry/react-router-dom/-/react-router-dom-6.3.0.tgz#a0216da813454e521905b5fa55e0e5176123f43d" integrity sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw== dependencies: history "^5.2.0" @@ -2464,6 +2469,11 @@ use-composed-ref@^1.3.0: resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.3.0.tgz#3d8104db34b7b264030a9d916c5e94fbe280dbda" integrity sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ== +use-context-selector@^1: + version "1.4.1" + resolved "https://registry.npm.ouryahoo.com:4443/npm-registry/api/npm/npm-registry/use-context-selector/-/use-context-selector-1.4.1.tgz#eb96279965846b72915d7f899b8e6ef1d768b0ae" + integrity sha512-Io2ArvcRO+6MWIhkdfMFt+WKQX+Vb++W8DS2l03z/Vw/rz3BclKpM0ynr4LYGyU85Eke+Yx5oIhTY++QR0ZDoA== + use-isomorphic-layout-effect@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb" |