Skip to content

Commit

Permalink
feat(HMS-2301): add query search for instance types select
Browse files Browse the repository at this point in the history
  • Loading branch information
amirfefer committed Aug 28, 2023
1 parent 5967648 commit d80a834
Show file tree
Hide file tree
Showing 9 changed files with 512 additions and 365 deletions.
738 changes: 399 additions & 339 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@unleash/proxy-client-react": "^3.5.2",
"axios": "1.4.0",
"classnames": "^2.3.1",
"jsonata": "^2.0.3",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-query": "^3.39.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ describe('InstanceTypesSelect', () => {
const items = await screen.findAllByLabelText(/^Instance Type/);
expect(items).toHaveLength(1);
});
test('filter with a query', async () => {
const query = 'vcpus > 2 and cores > 2';
const dropdown = await mountSelectAndClick();
await userEvent.type(dropdown, query);
const items = await screen.findAllByLabelText(/^Instance Type/);
expect(items).toHaveLength(1);
});
});
});

Expand Down
54 changes: 39 additions & 15 deletions src/Components/InstanceTypesSelect/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { Spinner, Select, SelectOption, TextInput } from '@patternfly/react-core
import { useQuery } from 'react-query';
import { fetchInstanceTypesList } from '../../API';
import { useWizardContext } from '../Common/WizardContext';
import { evaluateQuery } from '../../Utils/querySearch';
import _throttle from 'lodash/throttle';

const OPTIONS_PER_SCREEN = 3;
const sanitizeSearchValue = (str) => str.replace(/\\+$/, '');
Expand All @@ -13,8 +15,9 @@ const InstanceTypesSelect = ({ setValidation, architecture }) => {
const [isOpen, setIsOpen] = React.useState(false);
const [numOptions, setNumOptions] = React.useState(OPTIONS_PER_SCREEN);
const [filteredTypes, setFilteredTypes] = React.useState(null);
const [prevSearch, setPrevSearch] = React.useState('');
const [isTypeSupported, setTypeSupported] = React.useState(true);
const [searchValue, setSearchValue] = React.useState();

const {
isLoading,
error,
Expand Down Expand Up @@ -49,12 +52,16 @@ const InstanceTypesSelect = ({ setValidation, architecture }) => {
clearSelection();
} else {
const chosenInstanceType = instanceTypes.find((instanceType) => selection === instanceType.name);
setTypeSupported(chosenInstanceType.supported);
setWizardContext((prevState) => ({
...prevState,
chosenInstanceType: selection,
}));
chosenInstanceType.supported ? setValidation('success') : setValidation('warning');
if (chosenInstanceType) {
setTypeSupported(chosenInstanceType.supported);
setWizardContext((prevState) => ({
...prevState,
chosenInstanceType: selection,
}));
chosenInstanceType.supported ? setValidation('success') : setValidation('warning');
} else {
setSearchValue(selection);
}
setIsOpen(false);
}
};
Expand All @@ -69,13 +76,28 @@ const InstanceTypesSelect = ({ setValidation, architecture }) => {
setIsOpen(false);
};

const onFilter = (_e, inputValue) => {
const search = sanitizeSearchValue(inputValue);
if (prevSearch !== search) {
setNumOptions(OPTIONS_PER_SCREEN);
setPrevSearch(search);
setFilteredTypes(instanceTypes.filter((i) => i.name.toLowerCase().includes(search.toLowerCase())));
const queryFilter = async (search) => {
if (search.length > 0) {
const { error: queryError, result } = await evaluateQuery(search, instanceTypes);
if (queryError) {
setFilteredTypes([]);
} else if (Array.isArray(result)) {
setFilteredTypes(result);
} else if (result instanceof Object) {
setFilteredTypes([result]);
}
}
};

const onTypeaheadInputChange = (inputValue) => {
if (inputValue === '') {
setFilteredTypes(null);
return;
}
const search = sanitizeSearchValue(inputValue);
setNumOptions(OPTIONS_PER_SCREEN);
const throttledFilter = _throttle(queryFilter, 200);
throttledFilter(search);
};

const selectItemsMapper = (types, limit) => {
Expand Down Expand Up @@ -129,10 +151,12 @@ const InstanceTypesSelect = ({ setValidation, architecture }) => {
placeholderText="Select instance type"
maxHeight="180px"
isOpen={isOpen}
selections={chosenInstanceType}
selections={chosenInstanceType || searchValue}
onToggle={onToggle}
onSelect={onSelect}
onFilter={onFilter}
onFilter={() => {}}
isInputValuePersisted
onTypeaheadInputChanged={onTypeaheadInputChange}
{...(numOptions < types?.length && {
loadingVariant: {
text: `View more (${types.length - numOptions})`,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react';
import { Form, FormGroup, Popover, Title, Button } from '@patternfly/react-core';
import { Form, FormGroup, Popover, Title, Button, Text } from '@patternfly/react-core';
import { HelpIcon } from '@patternfly/react-icons';

import { AWS_PROVIDER } from '../../../../constants';
Expand Down Expand Up @@ -87,8 +87,16 @@ const AccountCustomizationsAWS = ({ setStepValidated, image }) => {
fieldId="aws-select-instance-types"
labelIcon={
<Popover
bodyContent="Select AWS instance type based on your computing,
memory, networking, or storage needs"
bodyContent={
<div>
Select AWS instance type based on your computing, memory, networking, or storage needs
<br />
<br />
<b>Tip:</b> You can filter by a query search, i.e:
<br />
<Text component="small">{'vcpus = 2 and cores < 4 and memory < 4000'}</Text>
</div>
}
>
<Button
ouiaId="instance_type_help"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react';
import { Form, FormGroup, Popover, Title, Button } from '@patternfly/react-core';
import { Form, FormGroup, Popover, Title, Button, Text } from '@patternfly/react-core';
import { HelpIcon } from '@patternfly/react-icons';

import { AZURE_PROVIDER } from '../../../../constants';
Expand Down Expand Up @@ -85,7 +85,18 @@ const AccountCustomizationsAzure = ({ setStepValidated, image }) => {
helperText={validations.types === 'warning' && 'The selected specification does not meet minimum requirements for this image'}
fieldId="azure-select-instance-size"
labelIcon={
<Popover headerContent={<div>Azure instance sizes</div>}>
<Popover
bodyContent={
<div>
Select Azure instance type based on your computing, memory, networking, or storage needs
<br />
<br />
<b>Tip:</b> You can filter by a query search, i.e:
<br />
<Text component="small">{'vcpus = 2 and storage > 30 and memory < 4000'}</Text>
</div>
}
>
<Button
ouiaId="machine_type_help"
type="button"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react';
import { Form, FormGroup, Popover, Title, Button } from '@patternfly/react-core';
import { Form, FormGroup, Popover, Title, Button, Text } from '@patternfly/react-core';
import { HelpIcon } from '@patternfly/react-icons';

import { GCP_PROVIDER } from '../../../../constants';
Expand Down Expand Up @@ -86,7 +86,18 @@ const AccountCustomizationsGCP = ({ setStepValidated, image }) => {
helperText={validations.types === 'warning' && 'The selected specification does not meet minimum requirements for this image'}
fieldId="gcp-select-machine-types"
labelIcon={
<Popover headerContent={<div>GCP machine types</div>}>
<Popover
bodyContent={
<div>
Select GCP instance type based on your computing, memory, networking, or storage needs
<br />
<br />
<b>Tip:</b> You can filter by a query search, i.e:
<br />
<Text component="small">{'vcpus = 2 and storage > 30 and memory < 4000'}</Text>
</div>
}
>
<Button
ouiaId="machine_type_help"
type="button"
Expand Down
25 changes: 25 additions & 0 deletions src/Utils/querySearch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const jsonata = require('jsonata');

const OPERATORS = ['<', '>', '='];

const parseQuery = (query) => {
query = query.replace('memory', 'memory_mib');
query = query.replace('storage', 'storage_gb');
if (OPERATORS.every((operator) => !query.includes(operator))) {
query = `name ~> /${query}/i`;
}
return `types[${query}]`;
};

export const evaluateQuery = async (query, data) => {
const q = parseQuery(query);
const d = { types: data };
try {
const expression = jsonata(q);
const result = await expression.evaluate(d);
if (!result) return { error: false, result: [] };
return { error: false, result };
} catch (e) {
return { error: true, result: e };
}
};
8 changes: 4 additions & 4 deletions src/mocks/fixtures/instanceTypes.fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const awsInstanceTypeList = {
name: 'm5dn.12xlarge',
vcpus: 48,
cores: 24,
memory: 196608,
memory_mib: 196608,
supported: true,
architecture: 'x86_64',
},
Expand All @@ -14,15 +14,15 @@ export const awsInstanceTypeList = {
name: 't4g.nano',
vcpus: 2,
cores: 2,
memory: 512,
memory_mib: 512,
architecture: 'arm64',
},
{
id: 3,
name: 't1.micro',
vcpus: 1,
cores: 2,
memory: 128,
memory_mib: 128,
supported: true,
architecture: 'x86_64',
},
Expand All @@ -31,7 +31,7 @@ export const awsInstanceTypeList = {
name: 't1.nano',
vcpus: 1,
cores: 1,
memory: 512,
memory_mib: 512,
architecture: 'x86_64',
supported: false,
},
Expand Down

0 comments on commit d80a834

Please sign in to comment.