diff options
author | Valerij Fredriksen <freva@users.noreply.github.com> | 2022-10-31 09:29:16 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-10-31 09:29:16 +0100 |
commit | 42d67fa4e4fd247296b1e99bcfe44e1fdcb50834 (patch) | |
tree | 2e4da8e825af7d5a91ca728ab1a9cc23ef069b2a | |
parent | db6c0474107a847db494fbb3152e8179182ae768 (diff) | |
parent | 7760654b5e8e0b17838686b3b409be1f7c23b90c (diff) |
Merge pull request #24647 from vespa-engine/freva/qb-input
QueryBuilder: Use different input types depending on parameter type
3 files changed, 204 insertions, 221 deletions
diff --git a/client/js/app/src/app/pages/querybuilder/context/parameters.jsx b/client/js/app/src/app/pages/querybuilder/context/parameters.jsx index 9803d824d53..377d60bfcd7 100644 --- a/client/js/app/src/app/pages/querybuilder/context/parameters.jsx +++ b/client/js/app/src/app/pages/querybuilder/context/parameters.jsx @@ -1,200 +1,125 @@ +function param(name, type, props = {}) { + let children; + if (Array.isArray(type)) { + children = Object.fromEntries(type.map((child) => [child.name, child])); + type = 'Parent'; + } + return Object.assign({ name, type }, children && { children }, props); +} + // https://docs.vespa.ai/en/reference/query-api-reference.html -export default { - yql: { name: 'yql', type: 'String' }, +export default param('root', [ + param('yql', 'String'), // Native Execution Parameters - hits: { name: 'hits', type: 'Integer' }, - offset: { name: 'offset', type: 'Integer' }, - queryProfile: { name: 'queryProfile', type: 'String' }, - groupingSessionCache: { name: 'groupingSessionCache', type: 'Boolean' }, - searchChain: { name: 'searchChain', type: 'String' }, - timeout: { name: 'timeout', type: 'Double' }, - noCache: { name: 'noCache', type: 'Boolean' }, + param('hits', 'Integer', { min: 0, default: 10 }), + param('offset', 'Integer', { min: 0, default: 0 }), + param('queryProfile', 'String', { default: 'default' }), + param('groupingSessionCache', 'Boolean', { default: true }), + param('searchChain', 'String', { default: 'default' }), + param('timeout', 'Float', { min: 0, default: 0.5 }), + param('noCache', 'Boolean', { default: false }), // Query Model - model: { - name: 'model', - type: 'Parent', - children: { - defaultIndex: { name: 'defaultIndex', type: 'String' }, - encoding: { name: 'encoding', type: 'String' }, - filter: { name: 'filter', type: 'String' }, - locale: { name: 'locale', type: 'String' }, - language: { name: 'language', type: 'String' }, - queryString: { name: 'queryString', type: 'String' }, - restrict: { name: 'restrict', type: 'String' }, - searchPath: { name: 'searchPath', type: 'String' }, - sources: { name: 'sources', type: 'String' }, - type: { name: 'type', type: 'String' }, - }, - }, + param('model', [ + param('defaultIndex', 'String', { default: 'default' }), + param('encoding', 'String', { default: 'utf-8' }), + param('filter', 'String'), + param('locale', 'String'), + param('language', 'String'), + param('queryString', 'String'), + param('restrict', 'String'), + param('searchPath', 'String'), + param('sources', 'String'), + param('type', 'String'), + ]), // Ranking - ranking: { - name: 'ranking', - type: 'Parent', - children: { - location: { name: 'location', type: 'String' }, - features: { name: 'features', type: 'Parent', children: 'String' }, - listFeatures: { name: 'listFeatures', type: 'Boolean' }, - profile: { name: 'profile', type: 'String' }, - properties: { name: 'properties', type: 'String' }, - sorting: { name: 'sorting', type: 'String' }, - freshness: { name: 'freshness', type: 'String' }, - queryCache: { name: 'queryCache', type: 'Boolean' }, - rerankCount: { name: 'rerankCount', type: 'Integer' }, - matching: { - name: 'matching', - type: 'Parent', - children: { - numThreadsPerSearch: { name: 'numThreadsPerSearch', type: 'Integer' }, - minHitsPerThread: { name: 'minHitsPerThread', type: 'Integer' }, - numSearchPartitions: { name: 'numSearchPartitions', type: 'Integer' }, - termwiseLimit: { name: 'termwiseLimit', type: 'Float' }, - postFilterThreshold: { name: 'postFilterThreshold', type: 'Float' }, - approximateThreshold: { - name: 'approximateThreshold', - type: 'Float', - }, - }, - }, - matchPhase: { - name: 'matchPhase', - type: 'Parent', - children: { - attribute: { name: 'attribute', type: 'String' }, - maxHits: { name: 'maxHits', type: 'Integer' }, - ascending: { name: 'ascending', type: 'Boolean' }, - diversity: { - name: 'diversity', - type: 'Parent', - children: { - attribute: { name: 'attribute', type: 'String' }, - minGroups: { name: 'minGroups', type: 'Integer' }, - }, - }, - }, - }, - }, - }, + param('ranking', [ + param('location', 'String'), + param('features', 'Parent', { children: 'String' }), + param('listFeatures', 'Boolean', { default: false }), + param('profile', 'String', { default: 'default' }), + param('properties', 'String', { children: 'String' }), + param('softtimeout', [ + param('enable', 'Boolean', { default: true }), + param('factor', 'Float', { min: 0, max: 1, default: 0.7 }), + ]), + param('sorting', 'String'), + param('freshness', 'String'), + param('queryCache', 'Boolean', { default: false }), + param('rerankCount', 'Integer', { min: 0 }), + param('matching', [ + param('numThreadsPerSearch', 'Integer', { min: 0 }), + param('minHitsPerThread', 'Integer', { min: 0 }), + param('numSearchPartitions', 'Integer', { min: 0 }), + param('termwiseLimit', 'Float', { min: 0, max: 1 }), + param('postFilterThreshold', 'Float', { min: 0, max: 1 }), + param('approximateThreshold', 'Float', { min: 0, max: 1 }), + ]), + param('matchPhase', [ + param('attribute', 'String'), + param('maxHits', 'Integer', { min: 0 }), + param('ascending', 'Boolean'), + param('diversity', [ + param('attribute', 'String'), + param('minGroups', 'Integer', { min: 0 }), + ]), + ]), + ]), // Grouping - collapsesize: { name: 'collapsesize', type: 'Integer' }, - collapsefield: { name: 'collapsefield', type: 'String' }, - collapse: { - name: 'collapse', - type: 'Parent', - children: { - summary: { name: 'summary', type: 'String' }, - }, - }, - grouping: { - name: 'grouping', - type: 'Parent', - children: { - defaultMaxGroups: { name: 'defaultMaxGroups', type: 'Integer' }, - defaultMaxHits: { name: 'defaultMaxHits', type: 'Integer' }, - globalMaxGroups: { name: 'globalMaxGroups', type: 'Integer' }, - defaultPrecisionFactor: { - name: 'defaultPrecisionFactor', - type: 'Float', - }, - }, - }, + param('collapsesize', 'Integer', { min: 1, default: 1 }), + param('collapsefield', 'String'), + param('collapse', [param('summary', 'String')]), + param('grouping', [ + param('defaultMaxGroups', 'Integer', { min: -1, default: 10 }), + param('defaultMaxHits', 'Integer', { min: -1, default: 10 }), + param('globalMaxGroups', 'Integer', { min: -1, default: 10000 }), + param('defaultPrecisionFactor', 'Float', { min: 0, default: 2.0 }), + ]), // Presentation - presentation: { - name: 'presentation', - type: 'Parent', - children: { - bolding: { name: 'bolding', type: 'Boolean' }, - format: { - name: 'format', - type: 'Parent', - children: { - tensors: { name: 'tensors', type: 'String' }, - }, - }, - template: { name: 'template', type: 'String' }, - summary: { name: 'summary', type: 'String' }, - timing: { name: 'timing', type: 'Boolean' }, - }, - }, + param('presentation', [ + param('bolding', 'Boolean', { default: true }), + param('format', 'String', { default: 'default' }), + param('template', 'String'), + param('summary', 'String'), + param('timing', 'Boolean', { default: false }), + ]), // Tracing - trace: { - name: 'trace', - type: 'Parent', - children: { - level: { name: 'level', type: 'Integer' }, - explainLevel: { name: 'explainLevel', type: 'Integer' }, - profileDepth: { name: 'profileDepth', type: 'Integer' }, - timestamps: { name: 'timestamps', type: 'Boolean' }, - query: { name: 'query', type: 'Boolean' }, - }, - }, + param('trace', [ + param('level', 'Integer', { min: 1 }), + param('explainLevel', 'Integer', { min: 1 }), + param('profileDepth', 'Integer', { min: 1 }), + param('timestamps', 'Boolean', { default: false }), + param('query', 'Boolean', { default: true }), + ]), // Semantic Rules - rules: { - name: 'rules', - type: 'Parent', - children: { - off: { name: 'off', type: 'Boolean' }, - rulebase: { name: 'rulebase', type: 'String' }, - }, - }, - tracelevel: { - name: 'tracelevel', - type: 'Parent', - children: { - rules: { name: 'rules', type: 'Integer' }, - }, - }, + param('rules', [ + param('off', 'Boolean', { default: true }), + param('rulebase', 'String'), + ]), + param('tracelevel', [param('rules', 'Integer', { min: 0 })]), // Dispatch - dispatch: { - name: 'dispatch', - type: 'Parent', - children: { - topKProbability: { name: 'topKProbability', type: 'Float' }, - }, - }, + param('dispatch', [param('topKProbability', 'Float', { min: 0, max: 1 })]), // Other - recall: { name: 'recall', type: 'String' }, - user: { name: 'user', type: 'String' }, - hitcountestimate: { name: 'hitcountestimate', type: 'Boolean' }, - metrics: { - name: 'metrics', - type: 'Parent', - children: { - ignore: { name: 'ignore', type: 'Boolean' }, - }, - }, - weakAnd: { - name: 'weakAnd', - type: 'Parent', - children: { - replace: { name: 'replace', type: 'Boolean' }, - }, - }, - wand: { - name: 'wand', - type: 'Parent', - children: { - hits: { name: 'hits', type: 'Integer' }, - }, - }, + param('recall', 'String'), + param('user', 'String'), + param('hitcountestimate', 'Boolean', { default: false }), + param('metrics', [param('ignore', 'Boolean', { default: false })]), + param('weakAnd', [param('replace', 'Boolean', { default: false })]), + param('wand', [param('hits', 'Integer', { default: 100 })]), - streaming: { - name: 'streaming', - type: 'Parent', - children: { - userid: { name: 'userid', type: 'Integer' }, - groupname: { name: 'groupname', type: 'String' }, - selection: { name: 'selection', type: 'String' }, - priority: { name: 'priority', type: 'String' }, - maxbucketspervisitor: { name: 'maxbucketspervisitor', type: 'Integer' }, - }, - }, -}; + param('streaming', [ + param('userid', 'Integer'), + param('groupname', 'String'), + param('selection', 'String'), + param('priority', 'String'), + param('maxbucketspervisitor', 'Integer'), + ]), +]).children; diff --git a/client/js/app/src/app/pages/querybuilder/context/query-builder-provider.jsx b/client/js/app/src/app/pages/querybuilder/context/query-builder-provider.jsx index df9b8a105a0..23c4af165f0 100644 --- a/client/js/app/src/app/pages/querybuilder/context/query-builder-provider.jsx +++ b/client/js/app/src/app/pages/querybuilder/context/query-builder-provider.jsx @@ -106,7 +106,11 @@ function inputAdd(params, { id: parentId, type: typeName }) { ? { name: typeName, type: parent.type.children } : parent.type.children[typeName]; - parent.value.push({ id, value: type.children ? [] : '', type }); + parent.value.push({ + id, + value: type.children ? [] : type.default?.toString() ?? '', + type, + }); return cloned; } @@ -120,8 +124,8 @@ function inputUpdate(params, { id, type, value }) { typeof parent.type.children === 'string' ? { name: type, type: parent.type.children } : parent.type.children[type]; - if ((node.type.children != null) !== (newType.children != null)) - node.value = newType.children ? [] : ''; + if (node.type.type !== newType.type.type) + node.value = newType.children ? [] : newType.default?.toString() ?? ''; node.type = newType; } if (value != null) node.value = value; diff --git a/client/js/app/src/app/pages/querybuilder/query-filters/query-filters.jsx b/client/js/app/src/app/pages/querybuilder/query-filters/query-filters.jsx index 668164b54ee..45ebb3bab70 100644 --- a/client/js/app/src/app/pages/querybuilder/query-filters/query-filters.jsx +++ b/client/js/app/src/app/pages/querybuilder/query-filters/query-filters.jsx @@ -6,6 +6,7 @@ import { Button, Box, Stack, + Switch, Badge, Group, } from '@mantine/core'; @@ -25,47 +26,100 @@ function AddProperty(props) { ); } +function Property({ id, type, types }) { + if (types) + return ( + <Select + sx={{ flex: 1 }} + data={Object.values({ [type.name]: type, ...types }).map( + ({ name }) => name + )} + onChange={(type) => dispatch(ACTION.INPUT_UPDATE, { id, type })} + value={type.name} + searchable + /> + ); + + return ( + <TextInput + sx={{ flex: 1 }} + onChange={(event) => + dispatch(ACTION.INPUT_UPDATE, { + id, + type: event.currentTarget.value, + }) + } + placeholder="String" + value={type.name} + /> + ); +} + +function Value({ id, type, value }) { + if (type.children) return null; + + if (type.type === 'Boolean') + return ( + <Switch + sx={{ flex: 1 }} + onLabel="true" + offLabel="false" + size="xl" + checked={value === 'true'} + onChange={(event) => + dispatch(ACTION.INPUT_UPDATE, { + id, + value: event.currentTarget.checked.toString(), + }) + } + /> + ); + + const props = { value, placeholder: type.type }; + if (type.type === 'Integer' || type.type === 'Float') { + props.type = 'number'; + let range; + if (type.min != null) { + props.min = type.min; + range = `[${props.min}, `; + } else range = '(-∞, '; + if (type.max != null) { + props.max = type.max; + range += props.max + ']'; + } else range += '∞)'; + props.placeholder += ` in ${range}`; + + if (type.type === 'Float' && type.min != null && type.max != null) + props.step = (type.max - type.min) / 100; + + if (parseFloat(value) < type.min || parseFloat(value) > type.max) + props.error = `Must be within ${range}`; + } + + return ( + <TextInput + sx={{ flex: 1 }} + onChange={(event) => + dispatch(ACTION.INPUT_UPDATE, { + id, + value: event.currentTarget.value, + }) + } + {...props} + /> + ); +} + function Input({ id, value, types, type }) { return ( <> - <Box sx={{ display: 'flex', alignItems: 'center', gap: '5px' }}> - {types ? ( - <Select - sx={{ flex: 1 }} - data={Object.values({ [type.name]: type, ...types }).map( - ({ name }) => name - )} - onChange={(type) => dispatch(ACTION.INPUT_UPDATE, { id, type })} - value={type.name} - searchable - /> - ) : ( - <TextInput - sx={{ flex: 1 }} - onChange={(event) => - dispatch(ACTION.INPUT_UPDATE, { - id, - type: event.currentTarget.value, - }) - } - placeholder="String" - value={type.name} - /> - )} - {!type.children && ( - <TextInput - sx={{ flex: 1 }} - onChange={(event) => - dispatch(ACTION.INPUT_UPDATE, { - id, - value: event.currentTarget.value, - }) - } - placeholder={type.type} - value={value} - /> - )} - <ActionIcon onClick={() => dispatch(ACTION.INPUT_REMOVE, id)}> + <Box sx={{ display: 'flex', gap: '5px' }}> + <Property {...{ id, type, types }} /> + <Value {...{ id, type, value }} /> + <ActionIcon + sx={{ marginTop: 5 }} + onClick={() => dispatch(ACTION.INPUT_REMOVE, id)} + > <Icon name="circle-minus" /> </ActionIcon> </Box> |