aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLeandro Alves <ldalves@gmail.com>2022-08-10 13:41:23 +0200
committerGitHub <noreply@github.com>2022-08-10 13:41:23 +0200
commit35b76f6daf216b3d6e158576b05e4e914d58ae13 (patch)
tree4f6d2a209789b5868268da0d3934a15e8dbe590b
parentce083110a5834b83751f267abc285741cfcbc36e (diff)
parentcfb2e225f980af5c065e7d969245af262dfb5d52 (diff)
Merge pull request #23619 from vespa-engine/freva/refactor
Refactor querybuilder
-rw-r--r--client/js/app/package.json4
-rw-r--r--client/js/app/src/app/main.jsx5
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Buttons/AddPropertyButton.jsx46
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Buttons/AddQueryInputButton.jsx48
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Buttons/CopyResponseButton.jsx21
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Buttons/DownloadJSONButton.jsx9
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Buttons/ImageButton.jsx7
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Buttons/OverlayImageButton.jsx38
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Buttons/PasteJSONButton.jsx69
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Buttons/ShowQueryButton.jsx29
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Buttons/SimpleButton.jsx9
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Contexts/QueryBuilderProvider.jsx146
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Contexts/QueryContext.jsx14
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Contexts/QueryInputContext.jsx160
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Contexts/ResponseContext.jsx13
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Navigation/CustomNavbar.jsx37
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Navigation/Footer.jsx68
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Text/Info.jsx37
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Text/QueryDropDownForm.jsx75
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Text/QueryInput.jsx154
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Text/QueryInputChild.jsx201
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Text/ResponseBox.jsx17
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Text/SendQuery.jsx111
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Text/SimpleDropDownForm.jsx48
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Text/SimpleForm.jsx34
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Text/TextBox.jsx9
-rw-r--r--client/js/app/src/app/pages/querybuilder/parameters.jsx130
-rw-r--r--client/js/app/src/app/pages/querybuilder/query-builder.jsx73
-rw-r--r--client/js/app/src/app/pages/querytracer/query-tracer.jsx5
-rw-r--r--client/js/app/yarn.lock14
30 files changed, 478 insertions, 1153 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..88af68910a8 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,10 +1,9 @@
-import React, { useContext } from 'react';
-import { ResponseContext } from '../Contexts/ResponseContext';
+import React from 'react';
import transform from '../../TransformVespaTrace';
-import SimpleButton from './SimpleButton';
+import { useQueryBuilderContext } from 'app/pages/querybuilder/Components/Contexts/QueryBuilderProvider';
export default function DownloadJSONButton({ children }) {
- const { response } = useContext(ResponseContext);
+ const response = useQueryBuilderContext((ctx) => ctx.http.response);
const transformResponse = (response) => {
return transform(response);
@@ -40,5 +39,5 @@ export default function DownloadJSONButton({ children }) {
}
};
- 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..808dab3a8c8
--- /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) {
+ // TODO: Display error somehow
+ 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..c0da870823e 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,52 @@
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';
+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 <textarea readOnly cols="70" rows="25" value={response} />;
+}
+
export function QueryBuilder() {
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>
+ <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 />
+ <CopyResponseButton />
+ <DownloadJSONButton>Download response as JSON</DownloadJSONButton>
+ </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..7d9d32fe1b7 100644
--- a/client/js/app/src/app/pages/querytracer/query-tracer.jsx
+++ b/client/js/app/src/app/pages/querytracer/query-tracer.jsx
@@ -1,10 +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 { response, setResponse } = useState('');
const updateResponse = (e) => {
setResponse(e.target.value);
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"