Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Search/Query toggle UI #738

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

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
Loading