Skip to content

Commit

Permalink
Add segmented control
Browse files Browse the repository at this point in the history
  • Loading branch information
adhityamamallan committed Nov 21, 2024
1 parent 96059f5 commit 46f0df0
Show file tree
Hide file tree
Showing 8 changed files with 377 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
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>)
);

jest.mock(
'../../domain-workflows-query-label/domain-workflows-query-label',
() => jest.fn(() => <div>Query label</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 label')).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 input')).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,54 @@
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('Run Query')).toBeInTheDocument();
});

it('calls setValue 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');
});

it('calls setValue with undefined when the input is cleared', async () => {
const { mockSetValue, user } = setup({ startValue: 'test_query' });

const textbox = await screen.findByRole('textbox');
await user.clear(textbox);

expect(mockSetValue).toHaveBeenCalledWith(undefined);
});
});

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,42 @@
import React, { useEffect, useState } from 'react';

import { Button } from 'baseui/button';
import { Input } from 'baseui/input';
import { MdPlayArrow, MdCode } 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(() => {
value && setQueryText(value);
}, [value]);

return (
<>
<Input
value={queryText}
onChange={(event) => {
setQueryText(event.target.value);
if (!event.target.value) {
setValue(undefined);
}
}}
startEnhancer={() => <MdCode />}
overrides={overrides.input}
placeholder="Write a custom query for workflows"
clearable
clearOnEscape
/>
<Button
onClick={() => queryText && setValue(queryText)}
overrides={overrides.runButton}
startEnhancer={<MdPlayArrow />}
>
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;
};
4 changes: 2 additions & 2 deletions src/views/domain-workflows/domain-workflows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import React from 'react';

import { type DomainPageTabContentProps } from '@/views/domain-page/domain-page-content/domain-page-content.types';

import DomainWorkflowsFilters from './domain-workflows-filters/domain-workflows-filters';
import DomainWorkflowsHeader from './domain-workflows-header/domain-workflows-header';
import DomainWorkflowsTable from './domain-workflows-table/domain-workflows-table';

export default function DomainWorkflows(props: DomainPageTabContentProps) {
return (
<>
<DomainWorkflowsFilters />
<DomainWorkflowsHeader />
<DomainWorkflowsTable domain={props.domain} cluster={props.cluster} />
</>
);
Expand Down

0 comments on commit 46f0df0

Please sign in to comment.