Skip to content

Commit

Permalink
Add Search/Query toggle UI (#738)
Browse files Browse the repository at this point in the history
Rename Domain Workflows Filters to Header
Add SegmentedControl to switch between Search and Query
Add Query Input and Run button for Query view
Add Query Label component with configurable Tooltip
Add Tooltip config
  • Loading branch information
adhityamamallan authored Nov 22, 2024
1 parent ccca673 commit 396707b
Show file tree
Hide file tree
Showing 10 changed files with 362 additions and 30 deletions.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { render, screen, userEvent } from '@/test-utils/rtl';

import * as usePageFiltersModule from '@/components/page-filters/hooks/use-page-filters';
import { type Props as PageFiltersToggleProps } from '@/components/page-filters/page-filters-toggle/page-filters-toggle.types';

import { mockDomainWorkflowsQueryParamsValues } from '../../__fixtures__/domain-workflows-query-params';
import DomainWorkflowsHeader from '../domain-workflows-header';

jest.mock(
'@/components/page-filters/page-filters-search/page-filters-search',
() => jest.fn(() => <div>Filter search</div>)
);

jest.mock(
'@/components/page-filters/page-filters-fields/page-filters-fields',
() => jest.fn(() => <div>Filter fields</div>)
);

jest.mock(
'@/components/page-filters/page-filters-toggle/page-filters-toggle',
() =>
jest.fn((props: PageFiltersToggleProps) => (
<button onClick={props.onClick}>Filter toggle</button>
))
);

jest.mock(
'../../domain-workflows-query-input/domain-workflows-query-input',
() => jest.fn(() => <div>Query input</div>)
);

const mockSetQueryParams = jest.fn();
const mockResetAllFilters = jest.fn();
const mockActiveFiltersCount = 2;
jest.mock('@/components/page-filters/hooks/use-page-filters', () =>
jest.fn(() => ({
resetAllFilters: mockResetAllFilters,
activeFiltersCount: mockActiveFiltersCount,
queryParams: mockDomainWorkflowsQueryParamsValues,
setQueryParams: mockSetQueryParams,
}))
);

describe(DomainWorkflowsHeader.name, () => {
it('renders segmented control', async () => {
render(<DomainWorkflowsHeader />);

expect(await screen.findByText('Search')).toBeInTheDocument();
expect(await screen.findByText('Query')).toBeInTheDocument();
});

it('renders page search and filters button when input type is search', async () => {
render(<DomainWorkflowsHeader />);

expect(await screen.findByText('Filter search')).toBeInTheDocument();
expect(await screen.findByText('Filter toggle')).toBeInTheDocument();
});

it('renders page filters when filter toggle is clicked', async () => {
const user = userEvent.setup();
render(<DomainWorkflowsHeader />);

const filterToggle = await screen.findByText('Filter toggle');
await user.click(filterToggle);

expect(await screen.findByText('Filter fields')).toBeInTheDocument();
});

it('renders query input when input type is query', async () => {
jest.spyOn(usePageFiltersModule, 'default').mockReturnValueOnce({
resetAllFilters: mockResetAllFilters,
activeFiltersCount: mockActiveFiltersCount,
queryParams: {
...mockDomainWorkflowsQueryParamsValues,
inputType: 'query',
},
setQueryParams: mockSetQueryParams,
});

render(<DomainWorkflowsHeader />);

expect(await screen.findByText('Query')).toBeInTheDocument();
});

it('toggles input type when segmented control is used', async () => {
const user = userEvent.setup();
render(<DomainWorkflowsHeader />);

const queryButton = await screen.findByText('Search');
await user.click(queryButton);

expect(mockSetQueryParams).toHaveBeenCalledWith(
{ inputType: 'search' },
{ pageRerender: true, replace: false }
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { styled as createStyled, type Theme } from 'baseui';
import {
type SegmentOverrides,
type SegmentedControlOverrides,
} from 'baseui/segmented-control';
import { type StyleObject } from 'styletron-react';

export const styled = {
HeaderContainer: createStyled('div', ({ $theme }: { $theme: Theme }) => ({
marginTop: $theme.sizing.scale950,
marginBottom: $theme.sizing.scale900,
})),
InputContainer: createStyled('div', ({ $theme }: { $theme: Theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: $theme.sizing.scale500,
marginBottom: $theme.sizing.scale500,
[$theme.mediaQuery.medium]: {
flexDirection: 'row',
},
})),
};

export const overrides = {
inputToggle: {
Root: {
style: ({ $theme }: { $theme: Theme }): StyleObject => ({
flex: '1 0 auto',
height: $theme.sizing.scale950,
padding: $theme.sizing.scale0,
borderRadius: $theme.borders.radius300,
width: '100%',
...$theme.typography.ParagraphSmall,
[$theme.mediaQuery.medium]: {
width: 'auto',
},
}),
},
SegmentList: {
style: ({ $theme }: { $theme: Theme }): StyleObject => ({
height: $theme.sizing.scale950,
...$theme.typography.ParagraphSmall,
}),
},
Active: {
style: ({ $theme }: { $theme: Theme }): StyleObject => ({
height: $theme.sizing.scale900,
top: 0,
}),
},
} satisfies SegmentedControlOverrides,
inputToggleSegment: {
Segment: {
style: ({ $theme }: { $theme: Theme }): StyleObject => ({
height: $theme.sizing.scale900,
whiteSpace: 'nowrap',
}),
},
} satisfies SegmentOverrides,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
'use client';
import { useState } from 'react';

import { Segment, SegmentedControl } from 'baseui/segmented-control';

import usePageFilters from '@/components/page-filters/hooks/use-page-filters';
import PageFiltersFields from '@/components/page-filters/page-filters-fields/page-filters-fields';
import PageFiltersSearch from '@/components/page-filters/page-filters-search/page-filters-search';
import PageFiltersToggle from '@/components/page-filters/page-filters-toggle/page-filters-toggle';
import domainPageQueryParamsConfig from '@/views/domain-page/config/domain-page-query-params.config';

import domainWorkflowsFiltersConfig from '../config/domain-workflows-filters.config';
import DomainWorkflowsQueryInput from '../domain-workflows-query-input/domain-workflows-query-input';

import { overrides, styled } from './domain-workflows-header.styles';

export default function DomainWorkflowsHeader() {
const [areFiltersShown, setAreFiltersShown] = useState(false);

const { resetAllFilters, activeFiltersCount, queryParams, setQueryParams } =
usePageFilters({
pageFiltersConfig: domainWorkflowsFiltersConfig,
pageQueryParamsConfig: domainPageQueryParamsConfig,
});

return (
<styled.HeaderContainer>
<styled.InputContainer>
<SegmentedControl
activeKey={queryParams.inputType}
onChange={({ activeKey }) => {
setQueryParams(
{
inputType: activeKey === 'query' ? 'query' : 'search',
},
{ replace: false, pageRerender: true }
);
}}
overrides={overrides.inputToggle}
>
<Segment
overrides={overrides.inputToggleSegment}
key="search"
label="Search"
/>
<Segment
overrides={overrides.inputToggleSegment}
key="query"
// TODO @adhitya.mamallan - replace this with the label tooltip component
label="Query"
/>
</SegmentedControl>
{queryParams.inputType === 'query' ? (
<DomainWorkflowsQueryInput
value={queryParams.query}
setValue={(v) => setQueryParams({ query: v })}
/>
) : (
<>
<PageFiltersSearch
pageQueryParamsConfig={domainPageQueryParamsConfig}
searchQueryParamKey="search"
searchPlaceholder="Search for Workflow ID, Run ID, or Workflow Type"
/>
<PageFiltersToggle
isActive={areFiltersShown}
onClick={() => {
setAreFiltersShown((value) => !value);
}}
activeFiltersCount={activeFiltersCount}
/>
</>
)}
</styled.InputContainer>
{queryParams.inputType === 'search' && areFiltersShown && (
<PageFiltersFields
pageFiltersConfig={domainWorkflowsFiltersConfig}
resetAllFilters={resetAllFilters}
queryParams={queryParams}
setQueryParams={setQueryParams}
/>
)}
</styled.HeaderContainer>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react';

import { render, screen, userEvent, waitFor } from '@/test-utils/rtl';

import DomainWorkflowsQueryInput from '../domain-workflows-query-input';

describe(DomainWorkflowsQueryInput.name, () => {
it('renders as expected', async () => {
setup({});

expect(await screen.findByRole('textbox')).toBeInTheDocument();
expect(await screen.findByText('Run Query')).toBeInTheDocument();
});

it('renders as expected when loaded with a start value', async () => {
setup({ startValue: 'test_query' });

const textbox = await screen.findByRole('textbox');
await waitFor(() => expect(textbox).toHaveValue('test_query'));
expect(await screen.findByText('Rerun Query')).toBeInTheDocument();
});

it('calls setValue and changes text when the Run Query button is clicked', async () => {
const { mockSetValue, user } = setup({});

const textbox = await screen.findByRole('textbox');
await user.type(textbox, 'mock_query');
await user.click(await screen.findByText('Run Query'));

expect(mockSetValue).toHaveBeenCalledWith('mock_query');
});
});

function setup({ startValue }: { startValue?: string }) {
const mockSetValue = jest.fn();
const user = userEvent.setup();
render(
<DomainWorkflowsQueryInput
value={startValue ?? ''}
setValue={mockSetValue}
/>
);

return { mockSetValue, user };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { type Theme } from 'baseui';
import { type ButtonOverrides } from 'baseui/button';
import { type InputOverrides } from 'baseui/input';
import { type StyleObject } from 'styletron-react';

export const overrides = {
input: {
Root: {
style: ({ $theme }: { $theme: Theme }): StyleObject => ({
height: $theme.sizing.scale950,
}),
},
Input: {
style: ({ $theme }: { $theme: Theme }): StyleObject => ({
...$theme.typography.MonoParagraphXSmall,
}),
},
} satisfies InputOverrides,
runButton: {
Root: {
style: ({ $theme }: { $theme: Theme }): StyleObject => ({
whiteSpace: 'nowrap',
height: $theme.sizing.scale950,
...$theme.typography.LabelSmall,
}),
},
} satisfies ButtonOverrides,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React, { useEffect, useState } from 'react';

import { Button } from 'baseui/button';
import { Input } from 'baseui/input';
import { MdPlayArrow, MdCode, MdRefresh } from 'react-icons/md';

import { overrides } from './domain-workflows-query-input.styles';
import { type Props } from './domain-workflows-query-input.types';

export default function DomainWorkflowsQueryInput({ value, setValue }: Props) {
const [queryText, setQueryText] = useState<string>('');

useEffect(() => {
setQueryText(value);
}, [value]);

const isQueryUnchanged = value && value === queryText;

return (
<>
<Input
value={queryText}
onChange={(event) => {
setQueryText(event.target.value);
}}
startEnhancer={() => <MdCode />}
overrides={overrides.input}
placeholder="Filter workflows using a custom query"
clearable
clearOnEscape
/>
<Button
onClick={() => setValue(queryText || undefined)}
overrides={overrides.runButton}
startEnhancer={isQueryUnchanged ? <MdRefresh /> : <MdPlayArrow />}
>
{isQueryUnchanged ? 'Rerun Query' : 'Run Query'}
</Button>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type Props = {
value: string;
setValue: (v: string | undefined) => void;
};
Loading

0 comments on commit 396707b

Please sign in to comment.