aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorErlend <erlendniko@hotmail.com>2022-07-01 13:30:34 +0200
committerErlend <erlendniko@hotmail.com>2022-07-01 13:30:34 +0200
commit790a5e068de378bb0ce1dbaa118c9cd469825b90 (patch)
tree8a3e5a9ab75bebba0b1c16fc1871a46717156cbd
parente4265f852d20675d873489973b3745580d2b3c5b (diff)
parent57d155a72acfeef2e0f8efb7cf53579334dec045 (diff)
Merge branch 'enikolaisen/querybuilder-js'
-rw-r--r--client/js/app/package.json1
-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/ImageButton.jsx25
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Buttons/OverlayImageButton.jsx40
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Buttons/PasteJSONButton.jsx75
-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/QueryContext.jsx14
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Contexts/QueryInputContext.jsx159
-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.jsx67
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Text/QueryInput.jsx74
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Text/QueryInputChild.jsx194
-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.jsx108
-rw-r--r--client/js/app/src/app/pages/querybuilder/Components/Text/SimpleDropDownForm.jsx38
-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/assets/img/Vespa-V2.pngbin0 -> 29139 bytes
-rw-r--r--client/js/app/src/app/pages/querybuilder/assets/img/VespaIcon.pngbin0 -> 19776 bytes
-rw-r--r--client/js/app/src/app/pages/querybuilder/assets/img/copy.svg7
-rw-r--r--client/js/app/src/app/pages/querybuilder/assets/img/down-arrow.svg1
-rw-r--r--client/js/app/src/app/pages/querybuilder/assets/img/features-help.pngbin0 -> 2554 bytes
-rw-r--r--client/js/app/src/app/pages/querybuilder/assets/img/information.svg10
-rw-r--r--client/js/app/src/app/pages/querybuilder/assets/img/paste.svg6
-rw-r--r--client/js/app/src/app/pages/querybuilder/assets/img/reload.svg6
-rw-r--r--client/js/app/src/app/pages/querybuilder/query-builder.jsx71
-rw-r--r--client/js/app/src/app/styles/agency.css895
-rw-r--r--client/js/app/src/app/styles/vespa.css752
-rw-r--r--client/js/app/yarn.lock132
34 files changed, 3014 insertions, 8 deletions
diff --git a/client/js/app/package.json b/client/js/app/package.json
index 8ea59b0cc35..1230741e1f2 100644
--- a/client/js/app/package.json
+++ b/client/js/app/package.json
@@ -11,6 +11,7 @@
},
"dependencies": {
"react": "^18",
+ "react-bootstrap": "^2.4.0",
"react-dom": "^18"
},
"devDependencies": {
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
new file mode 100644
index 00000000000..47b5a67875b
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/Components/Buttons/AddPropertyButton.jsx
@@ -0,0 +1,46 @@
+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
new file mode 100644
index 00000000000..8aeaff971bf
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/Components/Buttons/AddQueryInputButton.jsx
@@ -0,0 +1,48 @@
+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/ImageButton.jsx b/client/js/app/src/app/pages/querybuilder/Components/Buttons/ImageButton.jsx
new file mode 100644
index 00000000000..f620146bea5
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/Components/Buttons/ImageButton.jsx
@@ -0,0 +1,25 @@
+import React from 'react';
+
+export default function ImageButton({
+ onClick,
+ children,
+ className,
+ id,
+ image,
+ height = '15',
+ width = '15',
+ style,
+}) {
+ return (
+ <button id={id} className={className} onClick={onClick}>
+ <img
+ src={image}
+ height={height}
+ width={width}
+ style={style}
+ alt="Missing"
+ />
+ {children}
+ </button>
+ );
+}
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
new file mode 100644
index 00000000000..2cb9c7d6e9a
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/Components/Buttons/OverlayImageButton.jsx
@@ -0,0 +1,40 @@
+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
new file mode 100644
index 00000000000..73dce637500
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/Components/Buttons/PasteJSONButton.jsx
@@ -0,0 +1,75 @@
+import React, { useContext, useState } from 'react';
+import ImageButton from './ImageButton';
+import pasteImage from '../../assets/img/paste.svg';
+import { QueryInputContext } from '../Contexts/QueryInputContext';
+
+export default function PasteJSONButton() {
+ const { inputs, setInputs, id, setId, levelZeroParameters, childMap } =
+ useContext(QueryInputContext);
+ const [paste, setPaste] = useState(false);
+
+ const handleClick = (e) => {
+ alert('Button is non-functional');
+ // setPaste(true);
+ // window.addEventListener('paste', handlePaste)
+ };
+
+ const handlePaste = (e) => {
+ setPaste(false);
+ 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);
+ setId(1);
+ const newInputs = buildFromJSON(json, id);
+ setInputs(newInputs);
+ } catch (error) {
+ alert('Could not parse JSON, with error-message: \n\n' + error.message);
+ }
+ };
+
+ const buildFromJSON = (json, id) => {
+ let newInputs = [];
+ let keys = Object.keys(json);
+ for (let i = 0; i < keys.length; i++) {
+ let childId = 1;
+ let newInput = { id: id, type: keys[i] };
+ if (json[keys[i]] === Object) {
+ newInput['typeof'] = 'Parent';
+ newInput['input'] = '';
+ newInput['hasChildren'] = true;
+ let tempId = id + '.' + childId;
+ childId += 1;
+ newInput['children'] = buildFromJSON(json[keys[i]], tempId);
+ } else {
+ newInput['typeof'] = levelZeroParameters[keys[i]].type;
+ newInput['input'] = json[keys[i]];
+ newInput['hasChildren'] = false;
+ newInput['children'] = [];
+ }
+ setId(id + 1);
+ console.log(newInput);
+ newInputs.push(newInput);
+ }
+ return newInputs;
+ };
+
+ return (
+ <>
+ <ImageButton
+ id="pasteJSON"
+ className="pasteJSON"
+ image={pasteImage}
+ style={{ marginTop: '-2px', marginRight: '3px' }}
+ onClick={handleClick}
+ >
+ {paste ? 'Press CMD + V' : 'Paste JSON'}
+ </ImageButton>
+ </>
+ );
+}
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
new file mode 100644
index 00000000000..789fc387b38
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/Components/Buttons/ShowQueryButton.jsx
@@ -0,0 +1,29 @@
+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
new file mode 100644
index 00000000000..a153c9577e4
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/Components/Buttons/SimpleButton.jsx
@@ -0,0 +1,9 @@
+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/QueryContext.jsx b/client/js/app/src/app/pages/querybuilder/Components/Contexts/QueryContext.jsx
new file mode 100644
index 00000000000..00644c21d41
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/Components/Contexts/QueryContext.jsx
@@ -0,0 +1,14 @@
+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
new file mode 100644
index 00000000000..064bf51795b
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/Components/Contexts/QueryInputContext.jsx
@@ -0,0 +1,159 @@
+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 },
+ 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
new file mode 100644
index 00000000000..54f5fc955fd
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/Components/Contexts/ResponseContext.jsx
@@ -0,0 +1,13 @@
+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
new file mode 100644
index 00000000000..6ffeae4caed
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/Components/Navigation/CustomNavbar.jsx
@@ -0,0 +1,37 @@
+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
new file mode 100644
index 00000000000..b110bce943d
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/Components/Navigation/Footer.jsx
@@ -0,0 +1,68 @@
+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
new file mode 100644
index 00000000000..d921c53a602
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/Components/Text/Info.jsx
@@ -0,0 +1,37 @@
+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
new file mode 100644
index 00000000000..d9a3417752c
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/Components/Text/QueryDropDownForm.jsx
@@ -0,0 +1,67 @@
+import { QueryInputContext } from '../Contexts/QueryInputContext';
+import React, { useCallback, useContext, useState } from 'react';
+import SimpleDropDownForm from './SimpleDropDownForm';
+
+export default function QueryDropdownForm({ choices, id, child = false }) {
+ 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];
+ 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}
+ ></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
new file mode 100644
index 00000000000..c5410ae7c4a
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/Components/Text/QueryInput.jsx
@@ -0,0 +1,74 @@
+import React, { useContext } from 'react';
+import SimpleButton from '../Buttons/SimpleButton';
+import SimpleForm from './SimpleForm';
+import { OverlayTrigger, Tooltip } from 'react-bootstrap';
+import { QueryInputContext } from '../Contexts/QueryInputContext';
+import QueryDropdownForm from './QueryDropDownForm';
+import AddPropertyButton from '../Buttons/AddPropertyButton';
+import QueryInputChild from './QueryInputChild';
+
+export default function QueryInput() {
+ const { inputs, setInputs, levelZeroParameters, childMap } =
+ 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 setPlaceholder = (id) => {
+ try {
+ const index = inputs.findIndex((element) => element.id === id);
+ return inputs[index].typeof;
+ } catch (error) {
+ console.log(error);
+ }
+ };
+
+ const inputList = inputs.map((value) => {
+ return (
+ <div key={value.id} id={value.id} className="queryinput">
+ <QueryDropdownForm choices={levelZeroParameters} id={value.id} />
+ {value.hasChildren ? (
+ <>
+ <AddPropertyButton id={value.id} />
+ <QueryInputChild id={value.id} />
+ </>
+ ) : (
+ <SimpleForm
+ id={`v${value.id}`}
+ size="30"
+ onChange={updateInput}
+ placeholder={setPlaceholder(value.id)}
+ />
+ )}
+ <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>
+ );
+ });
+
+ return <>{inputList}</>;
+}
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
new file mode 100644
index 00000000000..0440d6ef1ba
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/Components/Text/QueryInputChild.jsx
@@ -0,0 +1,194 @@
+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}>
+ <QueryDropdownForm
+ choices={childMap[currentTypes]}
+ id={child.id}
+ child={true}
+ />
+ {child.hasChildren ? (
+ <>
+ <AddPropertyButton id={child.id} />
+ </>
+ ) : (
+ <SimpleForm
+ id={`v${child.id}`}
+ size="30"
+ onChange={updateInput}
+ placeholder={setPlaceHolder(child.id)}
+ />
+ )}
+ <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}
+ />
+ {child.hasChildren ? (
+ <>
+ <AddPropertyButton id={child.id} />
+ </>
+ ) : (
+ <SimpleForm
+ id={`v${child.id}`}
+ size="30"
+ onChange={onChange}
+ placeholder={placeholder(child.id)}
+ />
+ )}
+ <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
new file mode 100644
index 00000000000..08c65238434
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/Components/Text/ResponseBox.jsx
@@ -0,0 +1,17 @@
+import React, { useContext, useEffect, useState } 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
new file mode 100644
index 00000000000..9ee370de7c9
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/Components/Text/SendQuery.jsx
@@ -0,0 +1,108 @@
+import React, { useContext, useEffect, useState } from 'react';
+import SimpleDropDownForm from './SimpleDropDownForm';
+import SimpleButton from '../Buttons/SimpleButton';
+import SimpleForm from './SimpleForm';
+import { QueryInputContext } from '../Contexts/QueryInputContext';
+import { ResponseContext } from '../Contexts/ResponseContext';
+import { QueryContext } from '../Contexts/QueryContext';
+
+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 updateMethod = (e) => {
+ e.preventDefault();
+ const newMethod = e.target.value;
+ 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);
+ break;
+
+ case 'Float':
+ return parseFloat(input);
+ break;
+
+ case 'Boolean':
+ return input.toLowerCase() === 'true' ? true : false;
+ break;
+
+ default:
+ return input;
+ }
+ }
+
+ const updateUrl = (e) => {
+ const newUrl = e.target.value;
+ setUrl(newUrl);
+ };
+
+ return (
+ <>
+ <SimpleDropDownForm
+ choices={messageMethods}
+ id="method"
+ className="methodselector"
+ onChange={updateMethod}
+ />
+ <SimpleForm
+ id="url"
+ className="textbox"
+ initial={url}
+ size="30"
+ onChange={updateUrl}
+ />
+ <SimpleButton id="send" className="button" onClick={handleClick}>
+ Send
+ </SimpleButton>
+ </>
+ );
+}
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
new file mode 100644
index 00000000000..01288cea44f
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/Components/Text/SimpleDropDownForm.jsx
@@ -0,0 +1,38 @@
+import React, { useContext, useEffect } from 'react';
+import { QueryInputContext } from '../Contexts/QueryInputContext';
+
+export default function SimpleDropDownForm({
+ choices,
+ id,
+ className = 'input',
+ onChange,
+ value,
+}) {
+ const { selectedItems } = useContext(QueryInputContext);
+
+ //FIXME: using the filtered list to render options results in dropdown not changing the displayed selection to what was actually selected.
+ 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} value={value} onChange={onChange}>
+ {options}
+ </select>
+ </form>
+ );
+}
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
new file mode 100644
index 00000000000..bb6aaa13529
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/Components/Text/SimpleForm.jsx
@@ -0,0 +1,34 @@
+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
new file mode 100644
index 00000000000..022b250da7c
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/Components/Text/TextBox.jsx
@@ -0,0 +1,9 @@
+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/assets/img/Vespa-V2.png b/client/js/app/src/app/pages/querybuilder/assets/img/Vespa-V2.png
new file mode 100644
index 00000000000..ac87f8e94d0
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/assets/img/Vespa-V2.png
Binary files differ
diff --git a/client/js/app/src/app/pages/querybuilder/assets/img/VespaIcon.png b/client/js/app/src/app/pages/querybuilder/assets/img/VespaIcon.png
new file mode 100644
index 00000000000..33063432c20
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/assets/img/VespaIcon.png
Binary files differ
diff --git a/client/js/app/src/app/pages/querybuilder/assets/img/copy.svg b/client/js/app/src/app/pages/querybuilder/assets/img/copy.svg
new file mode 100644
index 00000000000..eada154413a
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/assets/img/copy.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 488.3 488.3" style="enable-background:new 0 0 488.3 488.3;" xml:space="preserve" width="512px" height="512px" class=""><g><g>
+ <g>
+ <path d="M314.25,85.4h-227c-21.3,0-38.6,17.3-38.6,38.6v325.7c0,21.3,17.3,38.6,38.6,38.6h227c21.3,0,38.6-17.3,38.6-38.6V124 C352.75,102.7,335.45,85.4,314.25,85.4z M325.75,449.6c0,6.4-5.2,11.6-11.6,11.6h-227c-6.4,0-11.6-5.2-11.6-11.6V124 c0-6.4,5.2-11.6,11.6-11.6h227c6.4,0,11.6,5.2,11.6,11.6V449.6z" data-original="#000000" class="active-path" data-old_color="#F8F5F5" fill="#FCFCFC"/>
+ <path d="M401.05,0h-227c-21.3,0-38.6,17.3-38.6,38.6c0,7.5,6,13.5,13.5,13.5s13.5-6,13.5-13.5c0-6.4,5.2-11.6,11.6-11.6h227 c6.4,0,11.6,5.2,11.6,11.6v325.7c0,6.4-5.2,11.6-11.6,11.6c-7.5,0-13.5,6-13.5,13.5s6,13.5,13.5,13.5c21.3,0,38.6-17.3,38.6-38.6 V38.6C439.65,17.3,422.35,0,401.05,0z" data-original="#000000" class="active-path" data-old_color="#F8F5F5" fill="#FCFCFC"/>
+ </g>
+</g></g> </svg>
diff --git a/client/js/app/src/app/pages/querybuilder/assets/img/down-arrow.svg b/client/js/app/src/app/pages/querybuilder/assets/img/down-arrow.svg
new file mode 100644
index 00000000000..d78d2f9c17f
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/assets/img/down-arrow.svg
@@ -0,0 +1 @@
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 36 36"><defs><style>.cls-1{clip-path:url(#clip-path);}.cls-2{clip-path:url(#clip-path-2);}.cls-3{clip-path:url(#clip-path-3);}</style><clipPath id="clip-path"><path d="M18,4.09A13.91,13.91,0,1,1,4.09,18,13.91,13.91,0,0,1,18,4.09ZM18,35A17,17,0,1,0,6,30,17,17,0,0,0,18,35Z"/></clipPath><clipPath id="clip-path-2"><rect x="-723.82" y="-837.15" width="1483.64" height="3802.85" fill="#FFFFFF"/></clipPath><clipPath id="clip-path-3"><path d="M26,13.83a1.55,1.55,0,0,0-2.16,0L18,19.75l-5.88-6A1.54,1.54,0,0,0,9.93,16L18,24.18,26.07,16a1.57,1.57,0,0,0,0-2.19Z"/></clipPath></defs><title>down-arrow</title><path d="M18,4.09A13.91,13.91,0,1,1,4.09,18,13.91,13.91,0,0,1,18,4.09ZM18,35A17,17,0,1,0,6,30,17,17,0,0,0,18,35Z"/><g class="cls-1"><rect x="-723.82" y="-837.15" width="1483.64" height="3802.85" fill="#FFFFFF"/><g class="cls-2"><rect x="-4.15" y="-4.15" width="44.3" height="44.3" fill="#FFFFFF"/></g></g><path d="M26,13.83a1.55,1.55,0,0,0-2.16,0L18,19.75l-5.88-6A1.54,1.54,0,0,0,9.93,16L18,24.18,26.07,16a1.57,1.57,0,0,0,0-2.19Z"/><g class="cls-3"><rect x="-723.82" y="-837.15" width="1483.64" height="3802.85" fill="#FFFFFF"/><g class="cls-2"><rect x="4.35" y="8.21" width="27.3" height="21.12" fill="#FFFFFF"/></g></g></svg>
diff --git a/client/js/app/src/app/pages/querybuilder/assets/img/features-help.png b/client/js/app/src/app/pages/querybuilder/assets/img/features-help.png
new file mode 100644
index 00000000000..65702f8b91f
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/assets/img/features-help.png
Binary files differ
diff --git a/client/js/app/src/app/pages/querybuilder/assets/img/information.svg b/client/js/app/src/app/pages/querybuilder/assets/img/information.svg
new file mode 100644
index 00000000000..da42cf2caf6
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/assets/img/information.svg
@@ -0,0 +1,10 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve" width="512px" height="512px"><g><g>
+ <g>
+ <g>
+ <circle cx="256" cy="378.5" r="25" data-original="#000000" class="active-path" data-old_color="#898989" fill="#767474"/>
+ <path d="M256,0C114.516,0,0,114.497,0,256c0,141.484,114.497,256,256,256c141.484,0,256-114.497,256-256 C512,114.516,397.503,0,256,0z M256,472c-119.377,0-216-96.607-216-216c0-119.377,96.607-216,216-216 c119.377,0,216,96.607,216,216C472,375.377,375.393,472,256,472z" data-original="#000000" class="active-path" data-old_color="#898989" fill="#767474"/>
+ <path d="M256,128.5c-44.112,0-80,35.888-80,80c0,11.046,8.954,20,20,20s20-8.954,20-20c0-22.056,17.944-40,40-40 c22.056,0,40,17.944,40,40c0,22.056-17.944,40-40,40c-11.046,0-20,8.954-20,20v50c0,11.046,8.954,20,20,20 c11.046,0,20-8.954,20-20v-32.531c34.466-8.903,60-40.26,60-77.469C336,164.388,300.112,128.5,256,128.5z" data-original="#000000" class="active-path" data-old_color="#898989" fill="#767474"/>
+ </g>
+ </g>
+</g></g> </svg>
diff --git a/client/js/app/src/app/pages/querybuilder/assets/img/paste.svg b/client/js/app/src/app/pages/querybuilder/assets/img/paste.svg
new file mode 100644
index 00000000000..b2edac680bf
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/assets/img/paste.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" width="512px" height="512px" viewBox="0 0 561 561" style="enable-background:new 0 0 561 561;" xml:space="preserve" class=""><g><g>
+ <g id="content-paste">
+ <path d="M459,51H351.9c-10.2-30.6-38.25-51-71.4-51c-33.15,0-61.2,20.4-71.4,51H102c-28.05,0-51,22.95-51,51v408 c0,28.05,22.95,51,51,51h357c28.05,0,51-22.95,51-51V102C510,73.95,487.05,51,459,51z M280.5,51c15.3,0,25.5,10.2,25.5,25.5 S295.8,102,280.5,102S255,91.8,255,76.5S265.2,51,280.5,51z M459,510H102V102h51v76.5h255V102h51V510z" data-original="#000000" class="active-path" data-old_color="#F9F6F6" fill="#FBF9F9"/>
+ </g>
+</g></g> </svg>
diff --git a/client/js/app/src/app/pages/querybuilder/assets/img/reload.svg b/client/js/app/src/app/pages/querybuilder/assets/img/reload.svg
new file mode 100644
index 00000000000..c5381f9f232
--- /dev/null
+++ b/client/js/app/src/app/pages/querybuilder/assets/img/reload.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve" width="512px" height="512px" class=""><g><g>
+ <g>
+ <path d="M482.195,226.196C482.195,101.471,380.725,0,256.001,0S29.805,101.471,29.805,226.196c0,7.409,6.007,13.416,13.416,13.416 s13.416-6.008,13.416-13.416c0-109.93,89.434-199.363,199.363-199.363s199.363,89.434,199.363,199.363 c0,109.928-89.434,199.362-199.363,199.362h-23.276l33.282-37.255c4.937-5.525,4.458-14.007-1.067-18.944 c-5.525-4.937-14.008-4.457-18.944,1.068l-47.576,53.255c-7.788,8.718-7.788,21.866,0,30.584l47.576,53.255 c2.651,2.968,6.322,4.478,10.01,4.478c3.181,0,6.375-1.126,8.934-3.41c5.526-4.937,6.004-13.419,1.067-18.944l-33.282-37.255 h23.276C380.725,452.39,482.195,350.919,482.195,226.196z" data-original="#000000" class="active-path" data-old_color="#F0EDED" fill="#F3F2F2"/>
+ </g>
+</g></g> </svg>
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 fe158fb5b98..472ec1efe49 100644
--- a/client/js/app/src/app/pages/querybuilder/query-builder.jsx
+++ b/client/js/app/src/app/pages/querybuilder/query-builder.jsx
@@ -1,6 +1,71 @@
-import React from 'react';
-import { Container } from 'app/components';
+import React, { useContext } from 'react';
+import SimpleButton from './Components/Buttons/SimpleButton';
+import QueryInput from './Components/Text/QueryInput';
+import TextBox from './Components/Text/TextBox';
+import ImageButton from './Components/Buttons/ImageButton';
+import OverlayImageButton from './Components/Buttons/OverlayImageButton';
+import AddQueryInput from './Components/Buttons/AddQueryInputButton';
+import { QueryInputProvider } from './Components/Contexts/QueryInputContext';
+import SendQuery from './Components/Text/SendQuery';
+import {
+ ResponseContext,
+ ResponseProvider,
+} from './Components/Contexts/ResponseContext';
+import ResponseBox from './Components/Text/ResponseBox';
+
+import copyImage from './assets/img/copy.svg';
+
+import '../../styles/agency.css';
+import '../../styles/vespa.css';
+import ShowQueryButton from './Components/Buttons/ShowQueryButton';
+import { QueryProvider } from './Components/Contexts/QueryContext';
+import PasteJSONButton from './Components/Buttons/PasteJSONButton';
+
+//import 'bootstrap/dist/css/bootstrap.min.css'; //TODO: Find out how to get this css
export function QueryBuilder() {
- return <Container>query builder</Container>;
+ 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 />
+ </ResponseProvider>
+ <OverlayImageButton
+ className="intro-copy"
+ image={copyImage}
+ height="30"
+ width="30"
+ tooltip="Copy"
+ onClick={() => {
+ alert('Button is non-functional');
+ }}
+ >
+ Copy
+ </OverlayImageButton>
+ <br />
+ <br />
+ </div>
+ </header>
+ </>
+ );
}
diff --git a/client/js/app/src/app/styles/agency.css b/client/js/app/src/app/styles/agency.css
new file mode 100644
index 00000000000..e17809338c6
--- /dev/null
+++ b/client/js/app/src/app/styles/agency.css
@@ -0,0 +1,895 @@
+/*!
+ * Start Bootstrap - Agency Bootstrap Theme (http://startbootstrap.com)
+ * Code licensed under the Apache License v2.0.
+ * For details, see http://www.apache.org/licenses/LICENSE-2.0.
+ */
+:root {
+ --primary: #188fff;
+ --secondary: #003abc;
+ --secondary-dark: #333;
+ --muted: #777;
+
+ --fontprimary: HelveticaNeue, Helvetica, Arial, sans-serif;
+ --fontsecondary: HelveticaNeue-Thin, Helvetica, Arial, sans-serif;
+}
+
+body {
+ overflow-x: hidden;
+ font-family: HelveticaNeue, Helvetica, Arial, sans-serif;
+ color: var(--secondary-dark);
+}
+
+.text-muted {
+ color: var(--muted);
+}
+
+.text-primary {
+ color: HelveticaNeue, Helvetica, Arial, sans-serif;
+}
+
+p {
+ font-size: 14px;
+ line-height: 1.75;
+}
+
+p.large {
+ font-size: 16px;
+}
+
+a,
+a:hover,
+a:focus,
+a:active,
+a.active {
+ outline: 0;
+}
+
+a {
+ color: HelveticaNeue, Helvetica, Arial, sans-serif;
+}
+
+a:hover,
+a:focus,
+a:active,
+a.active {
+ color: HelveticaNeue-Thin, Helvetica, Arial, sans-serif;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ text-transform: uppercase;
+ font-family: HelveticaNeue, Helvetica, Arial, sans-serif;
+ font-weight: 700;
+}
+
+.img-centered {
+ margin: 0 auto;
+}
+
+.bg-light-gray {
+ background-color: #f7f7f7;
+}
+
+.bg-darkest-gray {
+ background-color: #222;
+}
+
+.btn-primary {
+ border-color: HelveticaNeue, Helvetica, Arial, sans-serif;
+ text-transform: uppercase;
+ font-family: HelveticaNeue, Helvetica, Arial, sans-serif;
+ font-weight: 700;
+ color: #fff;
+ background-color: HelveticaNeue, Helvetica, Arial, sans-serif;
+}
+
+.btn-primary:hover,
+.btn-primary:focus,
+.btn-primary:active,
+.btn-primary.active,
+.open .dropdown-toggle.btn-primary {
+ border-color: HelveticaNeue, Helvetica, Arial, sans-serif;
+ color: #fff;
+ background-color: HelveticaNeue-Thin, Helvetica, Arial, sans-serif;
+}
+
+.btn-primary:active,
+.btn-primary.active,
+.open .dropdown-toggle.btn-primary {
+ background-image: none;
+}
+
+.btn-primary.disabled,
+.btn-primary[disabled],
+fieldset[disabled] .btn-primary,
+.btn-primary.disabled:hover,
+.btn-primary[disabled]:hover,
+fieldset[disabled] .btn-primary:hover,
+.btn-primary.disabled:focus,
+.btn-primary[disabled]:focus,
+fieldset[disabled] .btn-primary:focus,
+.btn-primary.disabled:active,
+.btn-primary[disabled]:active,
+fieldset[disabled] .btn-primary:active,
+.btn-primary.disabled.active,
+.btn-primary[disabled].active,
+fieldset[disabled] .btn-primary.active {
+ border-color: HelveticaNeue, Helvetica, Arial, sans-serif;
+ background-color: HelveticaNeue, Helvetica, Arial, sans-serif;
+}
+
+.btn-primary .badge {
+ color: HelveticaNeue, Helvetica, Arial, sans-serif;
+ background-color: #fff;
+}
+
+.btn-xl {
+ padding: 20px 40px;
+ border-color: HelveticaNeue, Helvetica, Arial, sans-serif;
+ border-radius: 3px;
+ text-transform: uppercase;
+ font-family: HelveticaNeue, Helvetica, Arial, sans-serif;
+ font-size: 18px;
+ font-weight: 700;
+ color: #fff;
+ background-color: HelveticaNeue, Helvetica, Arial, sans-serif;
+}
+
+.btn-xl:hover,
+.btn-xl:focus,
+.btn-xl:active,
+.btn-xl.active,
+.open .dropdown-toggle.btn-xl {
+ border-color: HelveticaNeue, Helvetica, Arial, sans-serif;
+ color: #fff;
+ background-color: HelveticaNeue-Thin, Helvetica, Arial, sans-serif;
+}
+
+.btn-xl:active,
+.btn-xl.active,
+.open .dropdown-toggle.btn-xl {
+ background-image: none;
+}
+
+.btn-xl.disabled,
+.btn-xl[disabled],
+fieldset[disabled] .btn-xl,
+.btn-xl.disabled:hover,
+.btn-xl[disabled]:hover,
+fieldset[disabled] .btn-xl:hover,
+.btn-xl.disabled:focus,
+.btn-xl[disabled]:focus,
+fieldset[disabled] .btn-xl:focus,
+.btn-xl.disabled:active,
+.btn-xl[disabled]:active,
+fieldset[disabled] .btn-xl:active,
+.btn-xl.disabled.active,
+.btn-xl[disabled].active,
+fieldset[disabled] .btn-xl.active {
+ border-color: HelveticaNeue, Helvetica, Arial, sans-serif;
+ background-color: HelveticaNeue, Helvetica, Arial, sans-serif;
+}
+
+.btn-xl .badge {
+ color: HelveticaNeue, Helvetica, Arial, sans-serif;
+ background-color: #fff;
+}
+
+.navbar-default {
+ border-color: transparent;
+ background-color: #222;
+}
+
+.navbar-default .navbar-brand {
+ font-family: 'Kaushan Script', 'Helvetica Neue', Helvetica, Arial, cursive;
+ color: HelveticaNeue, Helvetica, Arial, sans-serif;
+}
+
+.navbar-default .navbar-brand:hover,
+.navbar-default .navbar-brand:focus,
+.navbar-default .navbar-brand:active,
+.navbar-default .navbar-brand.active {
+ color: HelveticaNeue-Thin, Helvetica, Arial, sans-serif;
+}
+
+.navbar-default .navbar-collapse {
+ border-color: rgba(255, 255, 255, 0.02);
+}
+
+.navbar-default .navbar-toggle {
+ border-color: HelveticaNeue, Helvetica, Arial, sans-serif;
+ background-color: HelveticaNeue, Helvetica, Arial, sans-serif;
+}
+
+.navbar-default .navbar-toggle .icon-bar {
+ background-color: #fff;
+}
+
+.navbar-default .navbar-toggle:hover,
+.navbar-default .navbar-toggle:focus {
+ background-color: HelveticaNeue, Helvetica, Arial, sans-serif;
+}
+
+.navbar-default .nav li a {
+ text-transform: uppercase;
+ font-family: HelveticaNeue, Helvetica, Arial, sans-serif;
+ font-weight: 400;
+ letter-spacing: 1px;
+ color: #fff;
+ -webkit-transition: all ease 0.35s;
+ -moz-transition: all ease 0.35s;
+ transition: all ease 0.35s;
+}
+
+.navbar-default .nav li a:hover,
+.navbar-default .nav li a:focus {
+ outline: 0;
+ color: HelveticaNeue, Helvetica, Arial, sans-serif;
+}
+
+.navbar-default .navbar-nav > .active > a {
+ border-radius: 0;
+ color: #fff;
+ background-color: HelveticaNeue, Helvetica, Arial, sans-serif;
+}
+
+.navbar-default .navbar-nav > .active > a:hover,
+.navbar-default .navbar-nav > .active > a:focus {
+ color: #fff;
+ background-color: HelveticaNeue-Thin, Helvetica, Arial, sans-serif;
+}
+
+@media (min-width: 768px) {
+ .navbar-default {
+ padding: 25px 0;
+ border: 0;
+ background-color: transparent;
+ -webkit-transition: padding 0.3s;
+ -moz-transition: padding 0.3s;
+ transition: padding 0.3s;
+ }
+
+ .navbar-default .navbar-brand {
+ font-size: 2em;
+ -webkit-transition: all 0.3s;
+ -moz-transition: all 0.3s;
+ transition: all 0.3s;
+ }
+
+ .navbar-default .navbar-nav > .active > a {
+ border-radius: 3px;
+ }
+
+ .navbar-default.navbar-shrink {
+ padding: 10px 0;
+ background-color: #222;
+ }
+
+ .navbar-default.navbar-shrink .navbar-brand {
+ font-size: 1.5em;
+ }
+}
+
+header {
+ text-align: center;
+ color: #fff;
+ background-attachment: scroll;
+ /*background-image: url('../img/header-bg.png');*/
+ background-position: center center;
+ background-repeat: none;
+ -webkit-background-size: cover;
+ -moz-background-size: cover;
+ background-size: cover;
+ -o-background-size: cover;
+}
+
+header .intro-text {
+ padding-top: 100px;
+ padding-bottom: 50px;
+}
+
+header .intro-text .intro-lead-in {
+ margin-bottom: 25px;
+ font-family: HelveticaNeue-Thin, Helvetica, Arial, sans-serif;
+ font-size: 22px;
+ font-style: italic;
+ line-height: 22px;
+}
+
+header .intro-text .intro-heading {
+ margin-bottom: 25px;
+ text-transform: uppercase;
+ font-family: HelveticaNeue, Helvetica, Arial, sans-serif;
+ font-size: 50px;
+ font-weight: 700;
+ line-height: 50px;
+}
+
+@media (min-width: 768px) {
+ header .intro-text {
+ padding-top: 300px;
+ padding-bottom: 200px;
+ }
+
+ header .intro-text .intro-lead-in {
+ margin-bottom: 25px;
+ font-family: HelveticaNeue-Thin, Helvetica, Arial, sans-serif;
+ font-size: 40px;
+ font-style: italic;
+ line-height: 40px;
+ }
+
+ header .intro-text .intro-heading {
+ margin-bottom: 50px;
+ text-transform: uppercase;
+ font-family: HelveticaNeue, Helvetica, Arial, sans-serif;
+ font-size: 75px;
+ font-weight: 700;
+ line-height: 75px;
+ }
+}
+
+section {
+ padding: 100px 0;
+}
+
+section h2.section-heading {
+ margin-top: 0;
+ margin-bottom: 15px;
+ font-size: 40px;
+}
+
+section h3.section-subheading {
+ margin-bottom: 75px;
+ text-transform: none;
+ font-family: HelveticaNeue-Thin, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ font-style: italic;
+ font-weight: 400;
+}
+
+@media (min-width: 768px) {
+ section {
+ padding: 150px 0;
+ }
+}
+
+.service-heading {
+ margin: 15px 0;
+ text-transform: none;
+}
+
+#portfolio .portfolio-item {
+ right: 0;
+ margin: 0 0 15px;
+}
+
+#portfolio .portfolio-item .portfolio-link {
+ display: block;
+ position: relative;
+ margin: 0 auto;
+ max-width: 400px;
+}
+
+#portfolio .portfolio-item .portfolio-link .portfolio-hover {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ opacity: 0;
+ -webkit-transition: all ease 0.5s;
+ -moz-transition: all ease 0.5s;
+ transition: all ease 0.5s;
+ background: rgba(254, 209, 54, 0.9); /* Fallback when no plugin support */
+ background: rgba(var(--primary) | hex_to_rgb | join: ','}}, 0.9);
+}
+
+#portfolio .portfolio-item .portfolio-link .portfolio-hover:hover {
+ opacity: 1;
+}
+
+#portfolio
+ .portfolio-item
+ .portfolio-link
+ .portfolio-hover
+ .portfolio-hover-content {
+ position: absolute;
+ top: 50%;
+ width: 100%;
+ height: 20px;
+ margin-top: -12px;
+ text-align: center;
+ font-size: 20px;
+ color: #fff;
+}
+
+#portfolio
+ .portfolio-item
+ .portfolio-link
+ .portfolio-hover
+ .portfolio-hover-content
+ i {
+ margin-top: -12px;
+}
+
+#portfolio
+ .portfolio-item
+ .portfolio-link
+ .portfolio-hover
+ .portfolio-hover-content
+ h3,
+#portfolio
+ .portfolio-item
+ .portfolio-link
+ .portfolio-hover
+ .portfolio-hover-content
+ h4 {
+ margin: 0;
+}
+
+#portfolio .portfolio-item .portfolio-caption {
+ margin: 0 auto;
+ padding: 25px;
+ max-width: 400px;
+ text-align: center;
+ background-color: #fff;
+}
+
+#portfolio .portfolio-item .portfolio-caption h4 {
+ margin: 0;
+ text-transform: none;
+}
+
+#portfolio .portfolio-item .portfolio-caption p {
+ margin: 0;
+ font-family: HelveticaNeue-Thin, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ font-style: italic;
+}
+
+#portfolio * {
+ z-index: 2;
+}
+
+@media (min-width: 767px) {
+ #portfolio .portfolio-item {
+ margin: 0 0 30px;
+ }
+}
+
+.timeline {
+ position: relative;
+ padding: 0;
+ list-style: none;
+}
+
+.timeline:before {
+ content: '';
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 40px;
+ width: 2px;
+ margin-left: -1.5px;
+ background-color: #f1f1f1;
+}
+
+.timeline > li {
+ position: relative;
+ margin-bottom: 50px;
+ min-height: 50px;
+}
+
+.timeline > li:before,
+.timeline > li:after {
+ content: ' ';
+ display: table;
+}
+
+.timeline > li:after {
+ clear: both;
+}
+
+.timeline > li .timeline-panel {
+ float: right;
+ position: relative;
+ width: 100%;
+ padding: 0 20px 0 100px;
+ text-align: left;
+}
+
+.timeline > li .timeline-panel:before {
+ right: auto;
+ left: -15px;
+ border-right-width: 15px;
+ border-left-width: 0;
+}
+
+.timeline > li .timeline-panel:after {
+ right: auto;
+ left: -14px;
+ border-right-width: 14px;
+ border-left-width: 0;
+}
+
+.timeline > li .timeline-image {
+ z-index: 100;
+ position: absolute;
+ left: 0;
+ width: 80px;
+ height: 80px;
+ margin-left: 0;
+ border: 7px solid #f1f1f1;
+ border-radius: 100%;
+ text-align: center;
+ color: #fff;
+ background-color: HelveticaNeue, Helvetica, Arial, sans-serif;
+}
+
+.timeline > li .timeline-image h4 {
+ margin-top: 12px;
+ font-size: 10px;
+ line-height: 14px;
+}
+
+.timeline > li.timeline-inverted > .timeline-panel {
+ float: right;
+ padding: 0 20px 0 100px;
+ text-align: left;
+}
+
+.timeline > li.timeline-inverted > .timeline-panel:before {
+ right: auto;
+ left: -15px;
+ border-right-width: 15px;
+ border-left-width: 0;
+}
+
+.timeline > li.timeline-inverted > .timeline-panel:after {
+ right: auto;
+ left: -14px;
+ border-right-width: 14px;
+ border-left-width: 0;
+}
+
+.timeline > li:last-child {
+ margin-bottom: 0;
+}
+
+.timeline .timeline-heading h4 {
+ margin-top: 0;
+ color: inherit;
+}
+
+.timeline .timeline-heading h4.subheading {
+ text-transform: none;
+}
+
+.timeline .timeline-body > p,
+.timeline .timeline-body > ul {
+ margin-bottom: 0;
+}
+
+@media (min-width: 768px) {
+ .timeline:before {
+ left: 50%;
+ }
+
+ .timeline > li {
+ margin-bottom: 100px;
+ min-height: 100px;
+ }
+
+ .timeline > li .timeline-panel {
+ float: left;
+ width: 41%;
+ padding: 0 20px 20px 30px;
+ text-align: right;
+ }
+
+ .timeline > li .timeline-image {
+ left: 50%;
+ width: 100px;
+ height: 100px;
+ margin-left: -50px;
+ }
+
+ .timeline > li .timeline-image h4 {
+ margin-top: 16px;
+ font-size: 13px;
+ line-height: 18px;
+ }
+
+ .timeline > li.timeline-inverted > .timeline-panel {
+ float: right;
+ padding: 0 30px 20px 20px;
+ text-align: left;
+ }
+}
+
+@media (min-width: 992px) {
+ .timeline > li {
+ min-height: 150px;
+ }
+
+ .timeline > li .timeline-panel {
+ padding: 0 20px 20px;
+ }
+
+ .timeline > li .timeline-image {
+ width: 150px;
+ height: 150px;
+ margin-left: -75px;
+ }
+
+ .timeline > li .timeline-image h4 {
+ margin-top: 30px;
+ font-size: 18px;
+ line-height: 26px;
+ }
+
+ .timeline > li.timeline-inverted > .timeline-panel {
+ padding: 0 20px 20px;
+ }
+}
+
+@media (min-width: 1200px) {
+ .timeline > li {
+ min-height: 170px;
+ }
+
+ .timeline > li .timeline-panel {
+ padding: 0 20px 20px 100px;
+ }
+
+ .timeline > li .timeline-image {
+ width: 170px;
+ height: 170px;
+ margin-left: -85px;
+ }
+
+ .timeline > li .timeline-image h4 {
+ margin-top: 40px;
+ }
+
+ .timeline > li.timeline-inverted > .timeline-panel {
+ padding: 0 100px 20px 20px;
+ }
+}
+
+.team-member {
+ margin-bottom: 50px;
+ text-align: center;
+}
+
+.team-member img {
+ margin: 0 auto;
+ border: 7px solid #fff;
+}
+
+.team-member h4 {
+ margin-top: 25px;
+ margin-bottom: 0;
+ text-transform: none;
+}
+
+.team-member p {
+ margin-top: 0;
+}
+
+aside.clients img {
+ margin: 50px auto;
+}
+
+section#contact {
+ background-color: #222;
+ /* background-image: url('../img/map-image.png'); */
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+section#contact .section-heading {
+ color: #fff;
+}
+
+section#contact .form-group {
+ margin-bottom: 25px;
+}
+
+section#contact .form-group input,
+section#contact .form-group textarea {
+ padding: 20px;
+}
+
+section#contact .form-group input.form-control {
+ height: auto;
+}
+
+section#contact .form-group textarea.form-control {
+ height: 236px;
+}
+
+section#contact .form-control:focus {
+ border-color: HelveticaNeue, Helvetica, Arial, sans-serif;
+ box-shadow: none;
+}
+
+section#contact::-webkit-input-placeholder {
+ text-transform: uppercase;
+ font-family: HelveticaNeue, Helvetica, Arial, sans-serif;
+ font-weight: 700;
+ color: #bbb;
+}
+
+section#contact:-moz-placeholder {
+ text-transform: uppercase;
+ font-family: HelveticaNeue, Helvetica, Arial, sans-serif;
+ font-weight: 700;
+ color: #bbb;
+}
+
+section#contact::-moz-placeholder {
+ text-transform: uppercase;
+ font-family: HelveticaNeue, Helvetica, Arial, sans-serif;
+ font-weight: 700;
+ color: #bbb;
+}
+
+section#contact:-ms-input-placeholder {
+ text-transform: uppercase;
+ font-family: HelveticaNeue, Helvetica, Arial, sans-serif;
+ font-weight: 700;
+ color: #bbb;
+}
+
+section#contact .text-danger {
+ color: #e74c3c;
+}
+
+footer {
+ padding: 25px 0;
+ text-align: center;
+}
+
+footer span.copyright {
+ text-transform: uppercase;
+ text-transform: none;
+ font-family: HelveticaNeue, Helvetica, Arial, sans-serif;
+ line-height: 40px;
+}
+
+footer ul.quicklinks {
+ margin-bottom: 0;
+ text-transform: uppercase;
+ text-transform: none;
+ font-family: HelveticaNeue, Helvetica, Arial, sans-serif;
+ line-height: 40px;
+}
+
+ul.social-buttons {
+ margin-bottom: 0;
+}
+
+ul.social-buttons li a {
+ display: block;
+ width: 40px;
+ height: 40px;
+ border-radius: 100%;
+ font-size: 20px;
+ line-height: 40px;
+ outline: 0;
+ color: #fff;
+ background-color: #222;
+ -webkit-transition: all 0.3s;
+ -moz-transition: all 0.3s;
+ transition: all 0.3s;
+}
+
+ul.social-buttons li a:hover,
+ul.social-buttons li a:focus,
+ul.social-buttons li a:active {
+ background-color: HelveticaNeue, Helvetica, Arial, sans-serif;
+}
+
+.btn:focus,
+.btn:active,
+.btn.active,
+.btn:active:focus {
+ outline: 0;
+}
+
+.portfolio-modal .modal-content {
+ padding: 100px 0;
+ min-height: 100%;
+ border: 0;
+ border-radius: 0;
+ text-align: center;
+ background-clip: border-box;
+ -webkit-box-shadow: none;
+ box-shadow: none;
+}
+
+.portfolio-modal .modal-content h2 {
+ margin-bottom: 15px;
+ font-size: 3em;
+}
+
+.portfolio-modal .modal-content p {
+ margin-bottom: 30px;
+}
+
+.portfolio-modal .modal-content p.item-intro {
+ margin: 20px 0 30px;
+ font-family: HelveticaNeue-Thin, Helvetica, Arial, sans-serif;
+ font-size: 16px;
+ font-style: italic;
+}
+
+.portfolio-modal .modal-content ul.list-inline {
+ margin-top: 0;
+ margin-bottom: 30px;
+}
+
+.portfolio-modal .modal-content img {
+ margin-bottom: 30px;
+}
+
+.portfolio-modal .close-modal {
+ position: absolute;
+ top: 25px;
+ right: 25px;
+ width: 75px;
+ height: 75px;
+ background-color: transparent;
+ cursor: pointer;
+}
+
+.portfolio-modal .close-modal:hover {
+ opacity: 0.3;
+}
+
+.portfolio-modal .close-modal .lr {
+ z-index: 1051;
+ width: 1px;
+ height: 75px;
+ margin-left: 35px;
+ background-color: #222;
+ -webkit-transform: rotate(45deg);
+ -ms-transform: rotate(45deg);
+ transform: rotate(45deg);
+}
+
+.portfolio-modal .close-modal .lr .rl {
+ z-index: 1052;
+ width: 1px;
+ height: 75px;
+ background-color: #222;
+ -webkit-transform: rotate(90deg);
+ -ms-transform: rotate(90deg);
+ transform: rotate(90deg);
+}
+
+::-moz-selection {
+ text-shadow: none;
+ background: HelveticaNeue, Helvetica, Arial, sans-serif;
+}
+
+::selection {
+ text-shadow: none;
+ background: HelveticaNeue, Helvetica, Arial, sans-serif;
+}
+
+img::selection {
+ background: 0 0;
+}
+
+img::-moz-selection {
+ background: 0 0;
+}
+
+body {
+ webkit-tap-highlight-color: HelveticaNeue, Helvetica, Arial, sans-serif;
+}
diff --git a/client/js/app/src/app/styles/vespa.css b/client/js/app/src/app/styles/vespa.css
new file mode 100644
index 00000000000..4017f0275c3
--- /dev/null
+++ b/client/js/app/src/app/styles/vespa.css
@@ -0,0 +1,752 @@
+/**
+* Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+* Based on vespa.css from https://github.com/vespa-engine/frontpage for header- and footer-elements
+*/
+:root {
+ --primary: #188fff;
+ --secondary: #003abc;
+ --secondary-dark: #333;
+ --muted: #777;
+
+ --fontprimary: 'Hind Madurai', Helvetica, Arial, sans-serif;
+ --fontsecondary: 'Hind Madurai', Helvetica, Arial, sans-serif;
+}
+
+.bg-light-blue {
+ background-image: linear-gradient(
+ -1deg,
+ rgba(63, 157, 216, 0.08) 0%,
+ rgba(163, 195, 215, 0.08) 97%
+ );
+}
+
+.yql {
+}
+
+.yql:focus {
+ outline: none;
+}
+
+.information {
+ margin-left: -40px;
+ margin-right: 28px;
+ margin-top: -2.5px;
+ opacity: 0.6;
+ transition: transform 0.1s; /* Animation */
+}
+
+.information:hover {
+ transform: scale(1.05);
+}
+
+/*** Tooltips! ***/
+.tip {
+ visibility: visible;
+ border-bottom: 1px dotted;
+ position: relative;
+ cursor: help;
+ text-decoration: none;
+ color: #fff !important;
+}
+.tip span {
+ display: none;
+ z-index: 100;
+ position: absolute;
+ padding: 0.6em;
+ padding-left: 1em;
+ top: 1.5em;
+ left: 2.4em;
+ width: 15em;
+ background-color: #4da2d6;
+ border: 1px solid #ffffff;
+ border-radius: 0.5em;
+}
+
+.tip span td {
+ text-align: left;
+ vertical-align: top;
+ padding-left: 5px;
+}
+
+.tip span a {
+ text-decoration: none;
+ color: white;
+}
+
+.tip:hover span {
+ display: inline-block;
+}
+.sg-question-set,
+.sg-type-radio ul.sg-list-vertical li,
+.sg-type-checkbox ul.sg-list-vertical li,
+.sg-question-options,
+.sg-type-radio-likert .sg-question-options,
+.sg-type-table .sg-question-options,
+.sg-instructions {
+ overflow: visible;
+}
+
+section h2.section-heading,
+section h2.section-subheading,
+section h3.section-heading,
+section h3.section-subheading {
+ text-transform: none;
+ font-family: 'Hind Madurai', Helvetica, Arial, sans-serif;
+ font-weight: normal;
+ font-style: normal;
+}
+
+section h2.section-heading,
+section h3.section-heading {
+ font-size: 50px;
+ color: #3f9dd8;
+ margin-bottom: 5px;
+}
+
+section h2.section-subheading,
+section h3.section-subheading {
+ line-height: 26px;
+ margin-top: 0;
+}
+
+header .help-title {
+ color: #ffc43c;
+ text-transform: uppercase;
+ font-weight: bold;
+}
+
+.text-muted {
+ color: #303030;
+}
+
+#banner {
+ background-color: #336699;
+ color: #ffffff;
+ font-weight: bold;
+ padding-bottom: 2px;
+ padding-top: 2px;
+ width: 100%;
+}
+
+#banner a {
+ color: #ffffff;
+ text-decoration: underline;
+}
+
+.navbar-default {
+ background-color: #005a8e;
+ padding: 0;
+ border-bottom: 2px solid rgba(255, 255, 255, 0.2);
+}
+
+.navbar-default .navbar-header {
+ margin-left: 13px;
+}
+
+.navbar-default .navbar-brand {
+ background: transparent url('https://vespa.ai/assets/vespa-logo.png')
+ no-repeat;
+ background-size: contain;
+ direction: ltr;
+ text-indent: -9000px;
+ height: 28px;
+ width: 100px;
+ display: inline-block;
+ font-family: 'Hind Madurai', Helvetica, Arial, sans-serif;
+ font-weight: bold;
+ color: var(--primary);
+ margin-top: 16px;
+ margin-left: 10px;
+}
+
+.navbar-nav .nav-link {
+ font-weight: bold;
+ font-size: 14px;
+ letter-spacing: 1px;
+ color: #ffffff;
+ text-transform: none;
+ font-family: 'Hind Madurai', Helvetica, Arial, sans-serif;
+}
+
+.navbar-default .navbar-nav > a:hover,
+.navbar-default .navbar-nav > .active > a,
+.navbar-default .navbar-nav > .active > a:hover {
+ color: #ffc43c;
+}
+
+.navbar-default .navbar-toggler-icon {
+ background-image: url('data:image/svg+xml,%3csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 30 30%27%3e%3cpath stroke=%27rgba%280, 0, 0, 0.55%29%27 stroke-linecap=%27round%27 stroke-miterlimit=%2710%27 stroke-width=%272%27 d=%27M4 7h22M4 15h22M4 23h22%27/%3e%3c/svg%3e');
+}
+
+.navbar-default .navbar-toggler {
+ margin-right: 45px;
+ border: 1px solid;
+ border-color: #1a242f;
+}
+.navbar-toggler:hover,
+.navbar-default .navbar-toggler:focus {
+ background-color: #1a242f;
+ box-shadow: 0 0 0 0;
+}
+
+.navbar-toggle {
+ margin-right: 45px;
+}
+
+/* Trick to prevent anchored links to hide heading behind navbar */
+section[id]:before {
+ display: block;
+ content: ' ';
+ margin-top: -80px;
+ height: 100px;
+ visibility: hidden;
+}
+
+header {
+ /*background-image: linear-gradient(0deg, #98C1DB 7%, #3F9DD8 100%); */
+ background-color: #005a8e;
+ min-height: 1150px;
+}
+
+@media (max-height: 1150px) {
+ header {
+ min-height: 100vh;
+ }
+}
+
+header .intro {
+ margin-top: -80px;
+ max-width: 350px;
+ padding-top: 10px;
+ padding-bottom: 50px;
+}
+
+header .intro-logo {
+ max-width: 120px;
+ padding-bottom: 30px;
+}
+
+header .intro-button {
+ box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.5);
+ padding-left: 50px;
+ padding-right: 50px;
+}
+
+header .intro-long {
+ line-height: 27px;
+ font-size: 16px;
+ padding-bottom: 20px;
+}
+
+header .intro-param {
+ line-height: 27px;
+ font-size: 16px;
+ padding-bottom: 0px;
+ margin-bottom: 5px;
+}
+header .response {
+ line-height: 27px;
+ font-size: 20px;
+ padding-top: 10px;
+ padding-bottom: 8px;
+}
+
+header .methodselector {
+ font-size: 15px;
+ color: #fff;
+ background-color: #99c1da;
+ width: 70px;
+ height: 25px;
+ border-width: 0px;
+ -o-transition: 0.3s;
+ -ms-transition: 0.3s;
+ -moz-transition: 0.3s;
+ -webkit-transition: 0.3s;
+ transition: 0.3s;
+}
+header .intro-help:hover {
+ background-color: #4ea2d6 !important;
+}
+header .methodselector:hover {
+ background-color: #79b4d8;
+}
+header .button:hover {
+ background-color: #79b4d8;
+}
+header .removeRow:hover {
+ background-color: #79b4d8;
+}
+header .addRow:hover {
+ background-color: #79b4d8;
+}
+header .showJSON:hover {
+ background-color: #79b4d8;
+}
+header .copyURL:hover {
+ background-color: #79b4d8;
+}
+header .copyJSON:hover {
+ background-color: #79b4d8;
+}
+header .methodselector:focus {
+ outline: none;
+}
+header .textbox:focus {
+ outline: none;
+}
+header .input:focus {
+ outline: none;
+}
+header .responsebox::selection {
+ background-color: grey;
+}
+header .textbox::selection {
+ background-color: grey;
+}
+header .input::selection {
+ background-color: grey;
+}
+header .propvalue::selection {
+ background-color: grey;
+}
+header .responsebox:focus {
+ outline: none;
+}
+header .responseinfo:focus {
+ outline: none;
+}
+header .propvalue:focus {
+ outline: none;
+}
+header .addpropsbutton:focus {
+ outline: none;
+}
+
+header .input {
+ color: #000;
+ width: 190px;
+ border-width: 0px;
+ margin-right: 2px;
+}
+
+header .input2 {
+ color: #000;
+ width: 175px;
+ border-width: 0px;
+ margin-right: 2px;
+}
+
+header .textbox {
+ color: #000;
+ padding-left: 2px;
+}
+
+header .responsebox {
+ color: #000;
+ background-color: #fff;
+}
+
+header .responseinfo {
+ color: #d6e6f0;
+ text-align: center;
+ border-width: 0px;
+ background-color: transparent;
+}
+
+header .propvalue {
+ color: #000;
+ background-color: #fff;
+ width: 175px;
+ border-width: 0px;
+ margin-bottom: 3px;
+}
+
+header .propvalue::-webkit-input-placeholder {
+ /* Safari, Chrome(, Opera?) */
+ color: gray;
+ font-style: italic;
+}
+header .propvalue:-moz-placeholder {
+ /* Firefox 18- */
+ color: gray;
+ font-style: italic;
+}
+header .propvalue:-moz-placeholder {
+ /* Firefox 19+ */
+ color: gray;
+ font-style: italic;
+}
+header .propvalue-ms-input-placeholder {
+ /* IE (10+?) */
+ color: gray;
+ font-style: italic;
+}
+
+header .button {
+ font-size: 15px;
+ color: #fff;
+ background-color: #99c1da;
+ width: 70px;
+ height: 25px;
+ border-width: 0px;
+ border-radius: 5px;
+ padding: 0px;
+}
+header .button:focus {
+ outline: none;
+}
+
+header .addpropsbutton {
+ font-size: 15px;
+ color: #fff;
+ background-color: #99c1da;
+ width: 175px;
+ height: 23px;
+ border-width: 0px;
+ border-radius: 5px;
+ padding: 0px;
+ margin-left: 2px;
+ margin-bottom: 3px;
+}
+header .addRow {
+ font-size: 15px;
+ color: #fff;
+ background-color: #99c1da;
+ width: 70px;
+ height: 25px;
+ border-width: 0px;
+ border-radius: 5px;
+ padding: 0px;
+}
+header .addRow:focus {
+ outline: none;
+}
+
+header .removeRow {
+ font-size: 16px;
+ color: #fff;
+ background-color: #99c1da;
+ width: 35px;
+ height: 23px;
+ border-width: 0px;
+ border-radius: 5px;
+ padding: 0px;
+ padding-bottom: 2px;
+ margin-left: 3px;
+ text-align: center;
+ line-height: normal;
+}
+header .removeRow:focus {
+ outline: none;
+}
+
+header .json:focus {
+ outline: none;
+}
+header .copyJSON:focus {
+ outline: none;
+}
+header .showJSON:focus {
+ outline: none;
+}
+header .copyURL:focus {
+ outline: none;
+}
+header .pasteJSON:focus {
+ outline: none;
+}
+
+header .copyJSON {
+ font-size: 15px;
+ color: #fff;
+ background-color: #99c1da;
+ width: 130px;
+ height: 25px;
+ border-width: 0px;
+ border-radius: 5px;
+ padding-left: 1px;
+ margin-top: 10px;
+ margin-bottom: 20px;
+ margin-right: 2px;
+ display: none;
+}
+
+header .showJSON {
+ font-size: 15px;
+ color: #fff;
+ background-color: #99c1da;
+ width: 140px;
+ height: 25px;
+ border-width: 0px;
+ border-radius: 5px;
+ padding: 0px;
+ margin-top: 5px;
+ margin-bottom: 10px;
+}
+
+header .pasteJSON {
+ font-size: 15px;
+ color: #fff;
+ background-color: #99c1da;
+ width: 140px;
+ height: 25px;
+ border-width: 0px;
+ border-radius: 5px;
+ padding-left: 0px;
+ padding-bottom: 2px;
+ margin-top: 35px;
+ margin-bottom: 10px;
+}
+
+header .copyURL {
+ font-size: 15px;
+ color: #fff;
+ background-color: #99c1da;
+ width: 130px;
+ height: 25px;
+ border-width: 0px;
+ border-radius: 5px;
+ padding-left: 1px;
+ margin-top: 25px;
+ margin-bottom: 10px;
+ margin-left: 2px;
+ display: none;
+}
+
+header .intro .intro-lead-in {
+ font-style: normal;
+ font-size: 22px;
+ line-height: 50px;
+ margin-bottom: 25px;
+ font-family: 'Hind Madurai', Helvetica, Arial, sans-serif;
+}
+
+header .intro .intro-heading {
+ font-style: normal;
+}
+
+header .intro-button {
+ box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.5);
+}
+
+header .intro .btn-xl {
+ background: #ffffff;
+ border: 1px solid #3f9dd8;
+ border-radius: 2px;
+ font-size: 20px;
+ color: #3f9dd8;
+ font-weight: normal;
+ text-transform: none;
+ font-family: 'Hind Madurai', Helvetica, Arial, sans-serif;
+}
+
+header span {
+ display: inline-block;
+ position: inherit;
+ margin-top: 10px;
+ /*padding-left: 50px;*/
+ transform: translateX(-50%);
+ background: transparent;
+ border: none !important;
+ font-size: 0;
+}
+
+header .intro-copy {
+ background: transparent;
+ border: none !important;
+ font-size: 0;
+}
+
+header .intro-help {
+ display: inline-block;
+ margin-top: 15px;
+ width: 134px;
+ height: 45px;
+ background-color: transparent;
+ border: none !important;
+ font-size: 0;
+
+ border-top-left-radius: 30%;
+ border-top-right-radius: 29%;
+ border-bottom-right-radius: 24%;
+ border-bottom-left-radius: 26%;
+}
+
+header .intro-refresh {
+ display: inline-block;
+ margin-top: 15px;
+ /* transform: translateX(-50%); */
+ background: transparent;
+ border: none !important;
+ font-size: 0;
+}
+
+@media (min-height: 1150px) {
+ header .intro-down-arrow {
+ /* Hard code top: 1150 (height of top section) - 20 (bottom) - 36 (image height) = 1094 */
+ top: 1094px;
+ }
+}
+
+@media (max-height: 825px) {
+ header .intro-down-arrow {
+ display: none;
+ }
+}
+
+@media (min-width: 768px) {
+ header .intro {
+ padding-top: 200px;
+ padding-bottom: 200px;
+ max-width: 620px;
+ }
+
+ header .intro-logo {
+ max-width: 238px;
+ padding-bottom: 70px;
+ }
+
+ header .intro .intro-lead-in {
+ font-size: 51px;
+ font-style: normal;
+ line-height: 50px;
+ }
+
+ header .intro .intro-long {
+ margin-left: 80px;
+ margin-right: 80px;
+ }
+
+ header .intro .intro-heading {
+ margin-bottom: 25px;
+ font-size: 90px;
+ font-weight: 600;
+ }
+}
+
+section {
+ padding: 50px 0;
+}
+
+section h3.section-subheading {
+ margin-bottom: 50px;
+ font-weight: normal;
+}
+
+@media (min-width: 768px) {
+ section {
+ padding: 50px 0;
+ }
+}
+
+footer .row {
+ font-size: 12px;
+ text-align: left;
+}
+
+footer .row h4 {
+ font-size: 14px;
+ padding-left: 40px;
+}
+
+footer .row .quicklinks {
+ line-height: 25px;
+ list-style: none;
+ text-align: left;
+}
+
+footer .credits {
+ font-size: 10px;
+ text-align: center;
+}
+
+#description p {
+ font-weight: 300;
+ font-size: 25px;
+}
+
+footer {
+ font-size: 14px;
+ color: #ffffff;
+ background-color: #3f9dd8;
+ line-height: 22px;
+ letter-spacing: 0;
+ text-align: left;
+ display: flex;
+ flex-wrap: wrap;
+}
+
+footer .footer-row {
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ flex-wrap: wrap;
+}
+
+footer quicklinks {
+ font-size: 14px;
+ color: #ffffff;
+}
+
+footer ul.quicklinks {
+ margin: 0;
+ padding: 0;
+}
+
+footer .footer-title {
+ color: #ffc43c;
+ text-transform: uppercase;
+ font-weight: bold;
+}
+
+footer ul.quicklinks a {
+ font-size: 14px;
+ color: #ffffff;
+}
+
+footer .quicklink-section {
+ padding: 0;
+ min-width: 120px;
+}
+
+footer .credits {
+ float: left;
+ font-size: 12px;
+ font-weight: normal;
+ text-align: right;
+ font-family: 'Hind Madurai', Helvetica, Arial, sans-serif;
+}
+
+footer .credits a {
+ color: #ffffff;
+}
+
+footer .credits span {
+ color: #ffc43c;
+}
+
+footer a {
+ text-decoration: none;
+}
+
+footer .table {
+ width: fit-content;
+ margin-right: auto;
+ margin-left: auto;
+ float: left;
+ margin-bottom: 0em;
+}
+
+footer .table tbody th {
+ font-size: 13px;
+ padding-right: 2em;
+}
+
+footer table thead {
+ color: #ffc43c;
+}
+
+footer table tbody a {
+ color: #ffffff;
+}
diff --git a/client/js/app/yarn.lock b/client/js/app/yarn.lock
index f1cee14e50e..04399707958 100644
--- a/client/js/app/yarn.lock
+++ b/client/js/app/yarn.lock
@@ -463,7 +463,7 @@
clsx "^1.1.1"
csstype "3.0.9"
-"@popperjs/core@^2.9.3":
+"@popperjs/core@^2.10.1", "@popperjs/core@^2.9.3":
version "2.11.5"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.5.tgz#db5a11bf66bdab39569719555b0f76e138d7bd64"
integrity sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==
@@ -558,6 +558,45 @@
dependencies:
"@babel/runtime" "^7.13.10"
+"@reach/router@^1":
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/@reach/router/-/router-1.3.4.tgz#d2574b19370a70c80480ed91f3da840136d10f8c"
+ integrity sha512-+mtn9wjlB9NN2CNnnC/BRYtwdKBfSyyasPYraNAyvaV1occr/5NnB4CVzjEZipNHwYebQwcndGUmpFzxAUoqSA==
+ dependencies:
+ create-react-context "0.3.0"
+ invariant "^2.2.3"
+ prop-types "^15.6.1"
+ react-lifecycles-compat "^3.0.4"
+
+"@react-aria/ssr@^3.0.1":
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/@react-aria/ssr/-/ssr-3.2.0.tgz#88460384b43204f91c972d5b0de24ee44d6a2984"
+ integrity sha512-wwJFdkl+Q8NU5yJ4NvdAOqx5LM3QtUVoSjuK7Ey8jZ4WS4bB0EqT3Kr3IInBs257HzZ5nXCiKXKE4NGXXuIRWA==
+ dependencies:
+ "@babel/runtime" "^7.6.2"
+
+"@restart/hooks@^0.4.0", "@restart/hooks@^0.4.6":
+ version "0.4.7"
+ resolved "https://registry.yarnpkg.com/@restart/hooks/-/hooks-0.4.7.tgz#d79ca6472c01ce04389fc73d4a79af1b5e33cd39"
+ integrity sha512-ZbjlEHcG+FQtpDPHd7i4FzNNvJf2enAwZfJbpM8CW7BhmOAbsHpZe3tsHwfQUrBuyrxWqPYp2x5UMnilWcY22A==
+ dependencies:
+ dequal "^2.0.2"
+
+"@restart/ui@^1.2.0":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@restart/ui/-/ui-1.2.0.tgz#fb90251aa25f99b41ccedc78a91d2a15f3c5e0fb"
+ integrity sha512-oIh2t3tG8drZtZ9SlaV5CY6wGsUViHk8ZajjhcI+74IQHyWy+AnxDv8rJR5wVgsgcgrPBUvGNkC1AEdcGNPaLQ==
+ dependencies:
+ "@babel/runtime" "^7.13.16"
+ "@popperjs/core" "^2.10.1"
+ "@react-aria/ssr" "^3.0.1"
+ "@restart/hooks" "^0.4.0"
+ "@types/warning" "^3.0.0"
+ dequal "^2.0.2"
+ dom-helpers "^5.2.0"
+ uncontrollable "^7.2.1"
+ warning "^4.0.3"
+
"@rollup/pluginutils@^4.2.1":
version "4.2.1"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d"
@@ -588,6 +627,13 @@
dependencies:
"@types/react" "*"
+"@types/react-transition-group@^4.4.4":
+ version "4.4.4"
+ resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.4.tgz#acd4cceaa2be6b757db61ed7b432e103242d163e"
+ integrity sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug==
+ dependencies:
+ "@types/react" "*"
+
"@types/react@*":
version "18.0.13"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.13.tgz#0f5bd24a5f26593e04e450fe85ff43f51c1524ff"
@@ -597,7 +643,7 @@
"@types/scheduler" "*"
csstype "^3.0.2"
-"@types/react@^18":
+"@types/react@>=16.9.11", "@types/react@^18":
version "18.0.14"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.14.tgz#e016616ffff51dba01b04945610fe3671fdbe06d"
integrity sha512-x4gGuASSiWmo0xjDLpm5mPb52syZHJx02VKbqUKdLmKtAwIh63XClGsiTI1K6DO5q7ox4xAsQrU+Gl3+gGXF9Q==
@@ -611,6 +657,11 @@
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
+"@types/warning@^3.0.0":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@types/warning/-/warning-3.0.0.tgz#0d2501268ad8f9962b740d387c4654f5f8e23e52"
+ integrity sha512-t/Tvs5qR47OLOr+4E9ckN8AmP2Tf16gWq+/qA4iUGS/OOyHVO8wv2vjJuX8SNOUTJyWb+2t7wJm6cXILFnOROA==
+
"@vitejs/plugin-react@^1":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-1.3.2.tgz#2fcf0b6ce9bcdcd4cec5c760c199779d5657ece1"
@@ -782,6 +833,11 @@ chalk@^4.0.0:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
+classnames@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e"
+ integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==
+
clsx@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
@@ -876,6 +932,11 @@ define-properties@^1.1.3, define-properties@^1.1.4:
has-property-descriptors "^1.0.0"
object-keys "^1.1.1"
+dequal@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.2.tgz#85ca22025e3a87e65ef75a7a437b35284a7e319d"
+ integrity sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==
+
doctrine@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
@@ -890,6 +951,14 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
+dom-helpers@^5.0.1, dom-helpers@^5.2.0, dom-helpers@^5.2.1:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
+ integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==
+ dependencies:
+ "@babel/runtime" "^7.8.7"
+ csstype "^3.0.2"
+
electron-to-chromium@^1.4.147:
version "1.4.158"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.158.tgz#abbdaaf64676bfa4bc0307522125db34424a0ada"
@@ -1546,6 +1615,13 @@ internal-slot@^1.0.3:
has "^1.0.3"
side-channel "^1.0.4"
+invariant@^2.2.3, invariant@^2.2.4:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
+ integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
+ dependencies:
+ loose-envify "^1.0.0"
+
is-bigint@^1.0.1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3"
@@ -2011,7 +2087,15 @@ pretty-quick@^3:
mri "^1.1.5"
multimatch "^4.0.0"
-prop-types@^15.8.1:
+prop-types-extra@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/prop-types-extra/-/prop-types-extra-1.1.1.tgz#58c3b74cbfbb95d304625975aa2f0848329a010b"
+ integrity sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==
+ dependencies:
+ react-is "^16.3.2"
+ warning "^4.0.0"
+
+prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -2033,6 +2117,24 @@ punycode@^2.1.0:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+react-bootstrap@^2.4.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/react-bootstrap/-/react-bootstrap-2.4.0.tgz#99bf9656e2e7a23ae1ae135d18fd5ad7c344b416"
+ integrity sha512-dn599jNK1Fg5GGjJH+lQQDwELVzigh/MdusKpB/0el+sCjsO5MZDH5gRMmBjRhC+vb7VlCDr6OXffPIDSkNMLw==
+ dependencies:
+ "@babel/runtime" "^7.17.2"
+ "@restart/hooks" "^0.4.6"
+ "@restart/ui" "^1.2.0"
+ "@types/react-transition-group" "^4.4.4"
+ classnames "^2.3.1"
+ dom-helpers "^5.2.1"
+ invariant "^2.2.4"
+ prop-types "^15.8.1"
+ prop-types-extra "^1.1.0"
+ react-transition-group "^4.4.2"
+ uncontrollable "^7.2.1"
+ warning "^4.0.3"
+
react-dom@^18:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
@@ -2046,7 +2148,7 @@ react-fast-compare@^3.0.1:
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
-react-is@^16.13.1, react-is@^16.7.0:
+react-is@^16.13.1, react-is@^16.3.2, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -2088,6 +2190,16 @@ react-textarea-autosize@^8.3.2:
use-composed-ref "^1.3.0"
use-latest "^1.2.1"
+react-transition-group@^4.4.2:
+ version "4.4.2"
+ resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470"
+ integrity sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==
+ dependencies:
+ "@babel/runtime" "^7.5.5"
+ dom-helpers "^5.0.1"
+ loose-envify "^1.4.0"
+ prop-types "^15.6.2"
+
react@^18:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
@@ -2328,6 +2440,16 @@ unbox-primitive@^1.0.2:
has-symbols "^1.0.3"
which-boxed-primitive "^1.0.2"
+uncontrollable@^7.2.1:
+ version "7.2.1"
+ resolved "https://registry.yarnpkg.com/uncontrollable/-/uncontrollable-7.2.1.tgz#1fa70ba0c57a14d5f78905d533cf63916dc75738"
+ integrity sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==
+ dependencies:
+ "@babel/runtime" "^7.6.3"
+ "@types/react" ">=16.9.11"
+ invariant "^2.2.4"
+ react-lifecycles-compat "^3.0.4"
+
uri-js@^4.2.2:
version "4.4.1"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
@@ -2357,7 +2479,7 @@ v8-compile-cache@^2.0.3:
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
-vite@^2:
+vite@^2.9.12:
version "2.9.12"
resolved "https://registry.yarnpkg.com/vite/-/vite-2.9.12.tgz#b1d636b0a8ac636afe9d83e3792d4895509a941b"
integrity sha512-suxC36dQo9Rq1qMB2qiRorNJtJAdxguu5TMvBHOc/F370KvqAe9t48vYp+/TbPKRNrMh/J55tOUmkuIqstZaew==