Skip to content

Commit a0bba81

Browse files
feat(compass-aggregation): adds a search input for filtering use cases in aggregation sidepanel - COMPASS-6665 (#4344)
1 parent 2fd8292 commit a0bba81

File tree

9 files changed

+485
-144
lines changed

9 files changed

+485
-144
lines changed

package-lock.json

Lines changed: 294 additions & 82 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/compass-aggregations/src/components/aggregation-side-panel/index.spec.tsx

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import React from 'react';
22
import type { ComponentProps } from 'react';
33
import { AggregationSidePanel } from './index';
4-
import { render, screen } from '@testing-library/react';
4+
import { cleanup, render, screen } from '@testing-library/react';
5+
import userEvent from '@testing-library/user-event';
56
import { expect } from 'chai';
67
import configureStore from '../../../test/configure-store';
78
import { Provider } from 'react-redux';
89
import sinon from 'sinon';
10+
import { STAGE_WIZARD_USE_CASES } from './stage-wizard-use-cases';
911

1012
const renderAggregationSidePanel = (
1113
props: Partial<ComponentProps<typeof AggregationSidePanel>> = {}
@@ -22,26 +24,78 @@ const renderAggregationSidePanel = (
2224
};
2325

2426
describe('aggregation side panel', function () {
27+
afterEach(cleanup);
28+
2529
describe('header', function () {
2630
it('renders title', function () {
2731
renderAggregationSidePanel();
2832
expect(screen.getByText('Stage Wizard')).to.exist;
2933
});
34+
3035
it('renders close button', function () {
3136
renderAggregationSidePanel();
3237
expect(screen.getByLabelText('Hide Side Panel')).to.exist;
3338
});
39+
3440
it('calls onCloseSidePanel when close button is clicked', function () {
3541
const onCloseSidePanel = sinon.spy();
3642
renderAggregationSidePanel({ onCloseSidePanel });
3743
screen.getByLabelText('Hide Side Panel').click();
3844
expect(onCloseSidePanel).to.have.been.calledOnce;
3945
});
40-
it('calls onSelectUseCase when a use case is clicked', function () {
41-
const onSelectUseCase = sinon.spy();
42-
renderAggregationSidePanel({ onSelectUseCase });
43-
screen.getByTestId('use-case-sort').click();
44-
expect(onSelectUseCase).to.have.been.calledOnceWith('sort', '$sort');
45-
});
46+
});
47+
48+
it('renders a search input', function () {
49+
renderAggregationSidePanel();
50+
expect(screen.getByRole('search')).to.not.throw;
51+
});
52+
53+
it('renders all the usecases', function () {
54+
renderAggregationSidePanel();
55+
expect(
56+
screen
57+
.getByTestId('side-panel-content')
58+
.querySelectorAll('[data-testid^="use-case-"]')
59+
).to.have.lengthOf(STAGE_WIZARD_USE_CASES.length);
60+
});
61+
62+
it('renders usecases filtered by search text matching the title of the usecases', function () {
63+
renderAggregationSidePanel();
64+
const searchBox = screen.getByPlaceholderText(/How can we help\?/i);
65+
userEvent.type(searchBox, 'Sort');
66+
expect(
67+
screen
68+
.getByTestId('side-panel-content')
69+
.querySelectorAll('[data-testid^="use-case-"]')
70+
).to.have.lengthOf(1);
71+
expect(screen.getByTestId('use-case-sort')).to.not.throw;
72+
});
73+
74+
it('renders usecases filtered by search text matching the stage operator of the usecases', function () {
75+
renderAggregationSidePanel();
76+
const searchBox = screen.getByPlaceholderText(/How can we help\?/i);
77+
userEvent.type(searchBox, 'lookup');
78+
expect(
79+
screen
80+
.getByTestId('side-panel-content')
81+
.querySelectorAll('[data-testid^="use-case-"]')
82+
).to.have.lengthOf(1);
83+
expect(screen.getByTestId('use-case-lookup')).to.not.throw;
84+
85+
userEvent.clear(searchBox);
86+
userEvent.type(searchBox, '$lookup');
87+
expect(
88+
screen
89+
.getByTestId('side-panel-content')
90+
.querySelectorAll('[data-testid^="use-case-"]')
91+
).to.have.lengthOf(1);
92+
expect(screen.getByTestId('use-case-lookup')).to.not.throw;
93+
});
94+
95+
it('calls onSelectUseCase when a use case is clicked', function () {
96+
const onSelectUseCase = sinon.spy();
97+
renderAggregationSidePanel({ onSelectUseCase });
98+
screen.getByTestId('use-case-sort').click();
99+
expect(onSelectUseCase).to.have.been.calledOnceWith('sort', '$sort');
46100
});
47101
});

packages/compass-aggregations/src/components/aggregation-side-panel/index.tsx

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback } from 'react';
1+
import React, { useCallback, useMemo, useState } from 'react';
22
import {
33
Body,
44
css,
@@ -9,14 +9,17 @@ import {
99
palette,
1010
spacing,
1111
useDarkMode,
12+
SearchInput,
1213
} from '@mongodb-js/compass-components';
1314
import { connect } from 'react-redux';
15+
import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging';
16+
1417
import { toggleSidePanel } from '../../modules/side-panel';
15-
import { STAGE_WIZARD_USE_CASES, UseCaseList } from './stage-wizard-use-cases';
18+
import { STAGE_WIZARD_USE_CASES } from './stage-wizard-use-cases';
1619
import { FeedbackLink } from './feedback-link';
1720
import { addWizard } from '../../modules/pipeline-builder/stage-editor';
21+
import { UseCaseCard } from './stage-wizard-use-cases';
1822

19-
import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging';
2023
const { track } = createLoggerAndTelemetry('COMPASS-AGGREGATIONS-UI');
2124

2225
const containerStyles = css({
@@ -71,8 +74,24 @@ export const AggregationSidePanel = ({
7174
onCloseSidePanel,
7275
onSelectUseCase,
7376
}: AggregationSidePanelProps) => {
77+
const [searchText, setSearchText] = useState<string>('');
7478
const darkMode = useDarkMode();
7579

80+
const filteredUseCases = useMemo(() => {
81+
return STAGE_WIZARD_USE_CASES.filter(({ title, stageOperator }) => {
82+
const escapedSearchText = searchText.replace('$', '\\$');
83+
const matchRegex = new RegExp(escapedSearchText, 'gi');
84+
return title.match(matchRegex) || stageOperator.match(matchRegex);
85+
});
86+
}, [searchText]);
87+
88+
const handleSearchTextChange = useCallback(
89+
(e: React.ChangeEvent<HTMLInputElement>) => {
90+
setSearchText(e.target.value);
91+
},
92+
[setSearchText]
93+
);
94+
7695
const onSelect = useCallback(
7796
(id: string) => {
7897
const useCase = STAGE_WIZARD_USE_CASES.find(
@@ -86,7 +105,7 @@ export const AggregationSidePanel = ({
86105
drag_and_drop: false,
87106
});
88107
},
89-
[track]
108+
[onSelectUseCase]
90109
);
91110

92111
return (
@@ -110,8 +129,22 @@ export const AggregationSidePanel = ({
110129
<Icon glyph="X" />
111130
</IconButton>
112131
</div>
113-
<div className={contentStyles}>
114-
<UseCaseList onSelect={onSelect} />
132+
<SearchInput
133+
value={searchText}
134+
onChange={handleSearchTextChange}
135+
placeholder="How can we help?"
136+
aria-label="How can we help?"
137+
/>
138+
<div className={contentStyles} data-testid="side-panel-content">
139+
{filteredUseCases.map((useCase) => (
140+
<UseCaseCard
141+
key={useCase.id}
142+
id={useCase.id}
143+
title={useCase.title}
144+
stageOperator={useCase.stageOperator}
145+
onSelect={() => onSelect(useCase.id)}
146+
/>
147+
))}
115148
<FeedbackLink />
116149
</div>
117150
</KeylineCard>

packages/compass-aggregations/src/components/aggregation-side-panel/stage-wizard-use-cases/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import UseCaseList from './use-case-list';
1+
import UseCaseCard from './use-case-card';
22
import SortUseCase from './sort/sort';
33
import LookupUseCase from './lookup/lookup';
44
import ProjectUseCase from './project/project';
@@ -73,4 +73,4 @@ export const STAGE_WIZARD_USE_CASES: StageWizardUseCase[] = [
7373
},
7474
];
7575

76-
export { UseCaseList };
76+
export { UseCaseCard };
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import React from 'react';
2+
import { render, screen, cleanup } from '@testing-library/react';
3+
import userEvent from '@testing-library/user-event';
4+
import Sinon from 'sinon';
5+
import { expect } from 'chai';
6+
import UseCaseCard from './use-case-card';
7+
import { STAGE_WIZARD_USE_CASES } from '.';
8+
9+
describe('UseCaseCard', function () {
10+
afterEach(cleanup);
11+
12+
it('should render a card for provided usecase', function () {
13+
const useCase = STAGE_WIZARD_USE_CASES[0];
14+
render(
15+
<UseCaseCard
16+
id={useCase.id}
17+
title={useCase.title}
18+
stageOperator={useCase.stageOperator}
19+
onSelect={Sinon.spy()}
20+
/>
21+
);
22+
expect(screen.getByTestId(`use-case-${useCase.id}`)).to.not.throw;
23+
});
24+
25+
it('should call onSelect when a usecase is selected', function () {
26+
const onSelectSpy = Sinon.spy();
27+
const useCase = STAGE_WIZARD_USE_CASES[0];
28+
render(
29+
<UseCaseCard
30+
id={useCase.id}
31+
title={useCase.title}
32+
stageOperator={useCase.stageOperator}
33+
onSelect={onSelectSpy}
34+
/>
35+
);
36+
userEvent.click(screen.getByTestId(`use-case-${useCase.id}`));
37+
expect(onSelectSpy).to.be.called;
38+
});
39+
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import React from 'react';
2+
import {
3+
KeylineCard,
4+
Body,
5+
Link,
6+
css,
7+
spacing,
8+
} from '@mongodb-js/compass-components';
9+
10+
import { getStageHelpLink } from '../../../utils/stage';
11+
import type { StageWizardUseCase } from '.';
12+
13+
const cardStyles = css({
14+
cursor: 'pointer',
15+
padding: spacing[3],
16+
});
17+
18+
const cardTitleStyles = css({
19+
display: 'inline',
20+
marginRight: spacing[2],
21+
});
22+
23+
type UseCaseCardProps = {
24+
onSelect: () => void;
25+
} & Pick<StageWizardUseCase, 'id' | 'title' | 'stageOperator'>;
26+
27+
const UseCaseCard = ({
28+
id,
29+
title,
30+
stageOperator,
31+
onSelect,
32+
}: UseCaseCardProps) => {
33+
return (
34+
<KeylineCard
35+
data-testid={`use-case-${id}`}
36+
onClick={onSelect}
37+
className={cardStyles}
38+
>
39+
<Body className={cardTitleStyles}>{title}</Body>
40+
<Link target="_blank" href={getStageHelpLink(stageOperator) as string}>
41+
{stageOperator}
42+
</Link>
43+
</KeylineCard>
44+
);
45+
};
46+
47+
export default UseCaseCard;

packages/compass-aggregations/src/components/aggregation-side-panel/stage-wizard-use-cases/use-case-list.tsx

Lines changed: 0 additions & 47 deletions
This file was deleted.

packages/compass-components/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"@leafygreen-ui/tokens": "^1.4.1",
7171
"@leafygreen-ui/tooltip": "^9.0.2",
7272
"@leafygreen-ui/typography": "^15.2.0",
73+
"@leafygreen-ui/search-input": "^2.0.3",
7374
"@react-aria/interactions": "^3.9.1",
7475
"@react-aria/tooltip": "^3.2.1",
7576
"@react-aria/utils": "^3.13.1",

packages/compass-components/src/components/leafygreen.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import {
4848
import { Tabs, Tab } from '@leafygreen-ui/tabs';
4949
import TextArea from '@leafygreen-ui/text-area';
5050
import TextInput from '@leafygreen-ui/text-input';
51+
import { SearchInput } from '@leafygreen-ui/search-input';
5152
import {
5253
default as Toast,
5354
Variant as ToastVariant,
@@ -147,4 +148,5 @@ export {
147148
Label,
148149
Link,
149150
Description,
151+
SearchInput,
150152
};

0 commit comments

Comments
 (0)