Skip to content

Commit

Permalink
Generalize ProviderSelection to Select
Browse files Browse the repository at this point in the history
  • Loading branch information
nelsonkopliku committed Nov 17, 2023
1 parent f1eae2d commit 32a26bf
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 56 deletions.
37 changes: 0 additions & 37 deletions assets/js/components/ChecksCatalog/ProviderSelection.stories.jsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,35 @@ import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid';

import classNames from 'classnames';

import ProviderLabel from '@components/ProviderLabel';
export const ALL_FILTER = 'all';

import { checkProviderExists } from '@components/ProviderLabel/ProviderLabel';
const addAllOption = (items) => [ALL_FILTER, ...items];
const defaultOnChange = () => {};
const defaultOptionRenderer = (item) => item;
const renderOption = (item, optionsName, optionRenderer) =>
item === ALL_FILTER ? `All ${optionsName}` : optionRenderer(item);

const displayOption = (provider) =>
checkProviderExists(provider) ? (
<ProviderLabel provider={provider} />
) : (
<span>All</span>
);

function ProviderSelection({ className, providers, selected, onChange }) {
function Select({
optionsName,
options,
selected,
optionRenderer = defaultOptionRenderer,
onChange = defaultOnChange,
className,
}) {
const allAptions = addAllOption(options);
const dropdownSelector = `${optionsName.replace(
/\s+/g,
''
)}-selection-dropdown`;
return (
<div className={classNames('w-64 pb-4', className)}>
<Listbox value={selected} onChange={onChange}>
<div className="relative mt-1">
<Listbox.Button className="cloud-provider-selection-dropdown relative w-full py-2 pl-3 pr-10 text-left bg-white rounded-lg shadow-md cursor-default focus:outline-none focus-visible:ring-2 focus-visible:ring-opacity-75 focus-visible:ring-white focus-visible:ring-offset-orange-300 focus-visible:ring-offset-2 focus-visible:border-indigo-500 sm:text-sm">
{displayOption(selected)}
<Listbox.Button
className={`${dropdownSelector} relative w-full py-2 pl-3 pr-10 text-left bg-white rounded-lg cursor-default border border-gray-300 sm:text-sm`}
>
{renderOption(selected, optionsName, optionRenderer)}
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<ChevronUpDownIcon
className="w-5 h-5 text-gray-400"
Expand All @@ -37,15 +48,15 @@ function ProviderSelection({ className, providers, selected, onChange }) {
leaveTo="opacity-0"
>
<Listbox.Options className="absolute w-full py-1 mt-1 overflow-auto text-base bg-white rounded-md shadow-lg max-h-60 ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm z-[1]">
{providers.map((provider, providerIdx) => (
{allAptions.map((option) => (
<Listbox.Option
key={/* eslint-disable */ providerIdx}
key={option}
className={({ active }) =>
`cursor-default select-none relative py-2 pl-10 pr-4 ${
active ? 'text-green-900 bg-green-100' : 'text-gray-900'
}`
}
value={provider}
value={option}
>
{({ selected: isSelected }) => (
<>
Expand All @@ -54,7 +65,7 @@ function ProviderSelection({ className, providers, selected, onChange }) {
isSelected ? 'font-medium' : 'font-normal'
}`}
>
{displayOption(provider)}
{renderOption(option, optionsName, optionRenderer)}
</span>
{isSelected ? (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-green-600">
Expand All @@ -73,4 +84,4 @@ function ProviderSelection({ className, providers, selected, onChange }) {
);
}

export default ProviderSelection;
export default Select;
95 changes: 95 additions & 0 deletions assets/js/components/Select/Select.stories.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React, { useState } from 'react';

import { PROVIDERS } from '@lib/model';
import ProviderLabel from '@components/ProviderLabel';
import Select from './Select';

export default {
title: 'Components/Select',
components: Select,
argTypes: {
optionsName: {
type: 'string',
description:
'The name of the options to be used in the "All `optionsName`" option',
control: {
type: 'text',
},
},
options: {
type: 'array',
description: 'The list of options to be rendered in the dropdown',
control: {
type: 'array',
},
},
selected: {
type: 'string',
description: 'The currently selected option',
control: {
type: 'text',
},
},
optionRenderer: {
description: 'A function to render each option in the dropdown',
table: {
type: { summary: '(item) => item' },
},
},
onChange: {
description: 'A function to be called when the selected option changes',
table: {
type: { summary: '() => {}' },
},
},
className: {
type: 'string',
description: 'Extra classes to be applied to the component',
control: {
type: 'text',
},
},
},
};

const providerOptionRenderer = (provider) => (
<ProviderLabel provider={provider} />
);

export function ProviderSelection() {
const [selected, setSelected] = useState('azure');

return (
<Select
optionsName="providers"
options={PROVIDERS}
selected={selected}
optionRenderer={providerOptionRenderer}
onChange={setSelected}
/>
);
}

const emojiOptions = ['foo', 'bar', 'baz', 'qux'];

const emojiOptionsToLabel = {
foo: '😁 Foo',
bar: '😛 Bar',
baz: '🤪 Baz',
qux: '🧐 Qux',
};
const itemsOptionRenderer = (item) => <span>{emojiOptionsToLabel[item]}</span>;

export function EmojiSelection() {
const [selected, setSelected] = useState('all');

return (
<Select
optionsName="emojis"
options={emojiOptions}
selected={selected}
optionRenderer={itemsOptionRenderer}
onChange={setSelected}
/>
);
}
99 changes: 99 additions & 0 deletions assets/js/components/Select/Select.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';

import userEvent from '@testing-library/user-event';
import Select from './Select';

describe('Select Component', () => {
it('should render the `all options` option as selected', () => {
const options = ['option1', 'option2', 'option3'];
render(<Select optionsName="foobars" options={options} selected="all" />);

options.forEach((option) => {
expect(screen.queryByText(option)).not.toBeInTheDocument();
});
expect(screen.getByRole('button')).toHaveTextContent('All foobars');
});

const someOptions = ['option1', 'option2', 'option3'];
it.each(someOptions)(
'should render the selected option',
(selectedOption) => {
render(
<Select
optionsName="foobars"
options={someOptions}
selected={selectedOption}
/>
);

someOptions
.filter((option) => option !== selectedOption)
.forEach((notSelectedOption) => {
expect(screen.queryByText(notSelectedOption)).not.toBeInTheDocument();
});
expect(screen.getByRole('button')).toHaveTextContent(selectedOption);
}
);

it('should render the options when clicked', async () => {
const user = userEvent.setup();
const options = ['option1', 'option2', 'option3'];

render(<Select optionsName="foobars" options={options} selected="all" />);

expect(screen.getByRole('button')).toHaveTextContent('All foobars');

await user.click(screen.getByText('All foobars'));

expect(screen.getAllByText('All foobars')).toHaveLength(2);

options.forEach((option) => {
expect(screen.getByText(option)).toBeInTheDocument();
});
});

it('should render options via a custom option renderer', async () => {
const user = userEvent.setup();
const options = ['option1', 'option2', 'option3'];

const optionRenderer = (option) => `custom ${option}`;

render(
<Select
optionsName="foobars"
options={options}
selected="all"
optionRenderer={optionRenderer}
/>
);

await user.click(screen.getByText('All foobars'));

options.forEach((option) => {
expect(screen.getByText(`custom ${option}`)).toBeInTheDocument();
});
});

it('should notify about a new option being selected', async () => {
const user = userEvent.setup();
const mockOnChange = jest.fn();

const options = ['option1', 'option2', 'option3'];

render(
<Select
optionsName="foobars"
options={options}
selected="all"
onChange={mockOnChange}
/>
);

await user.click(screen.getByText('All foobars'));
await user.click(screen.getByText('option2'));

expect(mockOnChange).toHaveBeenCalledWith('option2');
});
});
5 changes: 5 additions & 0 deletions assets/js/components/Select/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Select, { ALL_FILTER } from './Select';

export { ALL_FILTER };

export default Select;
4 changes: 2 additions & 2 deletions test/e2e/cypress/e2e/checks_catalog.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ context('Checks catalog', () => {
}
).as('request');

cy.get('.cloud-provider-selection-dropdown').click();
cy.get('.cloud-provider-selection-dropdown')
cy.get('.providers-selection-dropdown').click();
cy.get('.providers-selection-dropdown')
.get('span')
.contains(label)
.click();
Expand Down

0 comments on commit 32a26bf

Please sign in to comment.