Skip to content

Commit

Permalink
Initialize Queries UI (#665)
Browse files Browse the repository at this point in the history
* Init commit

* more commit

* Add more changes

* Add tests

* clean up code

* Init queries UI

* Add unit tests

* Fix unsaved merge conflict

* Fix tests and merge conflicts again

* update snapshot

* Fix snapshot

* Reorder some props in tile

* resolve comments

* Fix tests

* Fix tests and make design responsive

* Remove snapshots

* Update tests for tile input
  • Loading branch information
adhityamamallan committed Sep 18, 2024
1 parent 6de34e0 commit d38ba48
Show file tree
Hide file tree
Showing 15 changed files with 536 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Suspense } from 'react';

import { userEvent } from '@testing-library/user-event';
import { HttpResponse } from 'msw';

import { render, screen, act } from '@/test-utils/rtl';
Expand All @@ -8,11 +9,44 @@ import { type FetchWorkflowQueryTypesResponse } from '@/route-handlers/fetch-wor

import WorkflowQueriesLoader from '../workflow-queries-loader';

jest.mock('../../workflow-queries-tile/workflow-queries-tile', () =>
jest.fn(({ name, onClick, runQuery }) => (
<div onClick={onClick}>
<div>Mock tile: {name}</div>
<button onClick={runQuery}>Run</button>
</div>
))
);

jest.mock(
'../../workflow-queries-result-json/workflow-queries-result-json',
() =>
jest.fn(({ data }) => (
<div>
<div>Mock JSON</div>
<div>{JSON.stringify(data)}</div>
</div>
))
);

describe(WorkflowQueriesLoader.name, () => {
it('WIP: renders query types without error', async () => {
it('renders without error', async () => {
await setup({});

expect(await screen.findByText(/__query_types/)).toBeInTheDocument();
expect(await screen.findByText(/__open_sessions/)).toBeInTheDocument();
});

it('runs query and updates JSON', async () => {
const { user } = await setup({});

const queryRunButtons = await screen.findAllByRole('button');
expect(queryRunButtons).toHaveLength(2);

await user.click(queryRunButtons[1]);

expect(
await screen.findByText(/{"name":"__open_sessions"}/)
).toBeInTheDocument();
});

it('does not render if the initial call fails', async () => {
Expand All @@ -32,6 +66,8 @@ describe(WorkflowQueriesLoader.name, () => {
});

async function setup({ error }: { error?: boolean }) {
const user = userEvent.setup();

render(
<Suspense>
<WorkflowQueriesLoader
Expand Down Expand Up @@ -64,4 +100,6 @@ async function setup({ error }: { error?: boolean }) {
],
}
);

return { user };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { styled as createStyled } from 'baseui';

export const styled = {
PageContainer: createStyled('div', ({ $theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: $theme.sizing.scale900,
[$theme.mediaQuery.medium]: {
flexDirection: 'row',
},
})),
QueriesSidebar: createStyled('div', ({ $theme }) => ({
[$theme.mediaQuery.medium]: {
flex: '1 0 300px',
},
maxWidth: '450px',
display: 'flex',
flexDirection: 'column',
rowGap: $theme.sizing.scale600,
})),
QueryResultView: createStyled('div', {
flex: '1 0 300px',
}),
};
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
'use client';
import React from 'react';
import React, { useState } from 'react';

import { useSuspenseQuery } from '@tanstack/react-query';
import { useQueries, useSuspenseQuery } from '@tanstack/react-query';

import PageSection from '@/components/page-section/page-section';
import { type FetchWorkflowQueryTypesResponse } from '@/route-handlers/fetch-workflow-query-types/fetch-workflow-query-types.types';
import request from '@/utils/request';
import { type RequestError } from '@/utils/request/request-error';

import WorkflowQueriesResultJson from '../workflow-queries-result-json/workflow-queries-result-json';
import WorkflowQueriesTile from '../workflow-queries-tile/workflow-queries-tile';

import { styled } from './workflow-queries-loader.styles';
import { type Props } from './workflow-queries-loader.types';

export default function WorkflowQueriesLoader(props: Props) {
Expand All @@ -26,5 +29,50 @@ export default function WorkflowQueriesLoader(props: Props) {
).then((res) => res.json()),
});

return <PageSection>{JSON.stringify(queryTypes)}</PageSection>;
const [selectedQueryIndex, setSelectedQueryIndex] = useState<number>(-1);
const [inputs, setInputs] = useState<Record<string, string | undefined>>({});

// TODO: when the queries are ready, add their generic types here
const queries = useQueries({
queries: queryTypes.map((name) => ({
queryKey: [name, inputs[name]] as const,
queryFn: ({
queryKey: [name, input],
}: {
queryKey: readonly [string, string | undefined];
}) => {
// TODO: add the actual query here
return { name, input };
},
enabled: false,
})),
});

return (
<styled.PageContainer>
<styled.QueriesSidebar>
{queryTypes.map((name, index) => (
<WorkflowQueriesTile
key={name}
name={name}
input={inputs[name]}
onChangeInput={(v) =>
setInputs((oldInputs) => ({ ...oldInputs, [name]: v }))
}
isSelected={index === selectedQueryIndex}
onClick={() => setSelectedQueryIndex(index)}
runQuery={queries[index].refetch}
queryStatus={queries[index].status}
/>
))}
</styled.QueriesSidebar>
<styled.QueryResultView>
<WorkflowQueriesResultJson
data={queries[selectedQueryIndex]?.data}
error={queries[selectedQueryIndex]?.error}
loading={queries[selectedQueryIndex]?.isFetching}
/>
</styled.QueryResultView>
</styled.PageContainer>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from 'react';

import copy from 'copy-to-clipboard';

import { render, fireEvent, screen, act } from '@/test-utils/rtl';

import WorkflowQueriesResultJson from '../workflow-queries-result-json';

jest.mock('copy-to-clipboard', jest.fn);

jest.mock('@/components/pretty-json/pretty-json', () =>
jest.fn(({ json }) => (
<div>
<div>PrettyJson Mock</div>
<div>{JSON.stringify(json)}</div>
</div>
))
);

describe(WorkflowQueriesResultJson.name, () => {
it('renders correctly with initial props', () => {
setup({});

expect(screen.getByText('PrettyJson Mock')).toBeInTheDocument();
expect(screen.getByText(/dataJson/)).toBeInTheDocument();
});

it('copies JSON to clipboard', () => {
const inputData = { input: 'dataJson' };
setup({ data: inputData });

const copyButton = screen.getByRole('button');
fireEvent.click(copyButton);

expect(copy).toHaveBeenCalledWith(JSON.stringify(inputData, null, '\t'));
});

it('show tooltip for 1 second and remove it', () => {
jest.useFakeTimers();

setup({});

const copyButton = screen.getByRole('button');
fireEvent.click(copyButton);
const visibleTooltip = screen.getByText('Copied');
expect(visibleTooltip).toBeInTheDocument();

act(() => {
jest.advanceTimersByTime(1000 + 500); // hide + animation duration
});

// Ensure the tooltip is hidden after 1000ms
const hiddenTooltip = screen.queryByText('Copied');
expect(hiddenTooltip).not.toBeInTheDocument();

jest.useRealTimers();
});
});

function setup({
data = { input: 'dataJson' },
error = undefined,
loading = false,
}: {
data?: any;
error?: any;
loading?: boolean;
}) {
render(
<WorkflowQueriesResultJson data={data} error={error} loading={loading} />
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { styled as createStyled } from 'baseui';

export const styled = {
ViewContainer: createStyled('div', ({ $theme }) => ({
display: 'flex',
flexDirection: 'row',
alignItems: 'flex-start',
justifyContent: 'space-between',
gap: $theme.sizing.scale600,
padding: $theme.sizing.scale600,
backgroundColor: $theme.colors.backgroundSecondary,
borderRadius: $theme.borders.radius300,
})),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
'use client';
import React, { useEffect, useState } from 'react';

import { Button, KIND as BUTTON_KIND, SHAPE, SIZE } from 'baseui/button';
import { ACCESSIBILITY_TYPE, Tooltip } from 'baseui/tooltip';
import copy from 'copy-to-clipboard';
import { MdCopyAll } from 'react-icons/md';

import PrettyJson from '@/components/pretty-json/pretty-json';

import { styled } from './workflow-queries-result-json.styles';
import { type Props } from './workflow-queries-result-json.types';

export default function WorkflowQueriesResultJson(props: Props) {
const [showTooltip, setShowTooltip] = useState(false);

useEffect(() => {
if (showTooltip) {
const timer = setTimeout(() => {
setShowTooltip(false);
}, 1000);
return () => clearTimeout(timer);
}
}, [showTooltip]);

return (
<styled.ViewContainer>
<PrettyJson json={props.data} />
<Tooltip
animateOutTime={400}
isOpen={showTooltip}
showArrow
placement="bottom"
accessibilityType={ACCESSIBILITY_TYPE.tooltip}
content={() => <>Copied</>}
>
<Button
onClick={() => {
copy(JSON.stringify(props.data, null, '\t'));
setShowTooltip(true);
}}
size={SIZE.compact}
shape={SHAPE.pill}
kind={BUTTON_KIND.secondary}
>
<MdCopyAll />
</Button>
</Tooltip>
</styled.ViewContainer>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// TODO: add proper types here when adding queries
export type Props = {
data: any;
error: any;
loading: boolean;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';

import { userEvent } from '@testing-library/user-event';

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

import WorkflowQueriesTileInput from '../workflow-queries-tile-input';

describe(WorkflowQueriesTileInput.name, () => {
afterEach(() => {
jest.clearAllMocks();
});

it('renders correctly', () => {
render(<WorkflowQueriesTileInput value="" onChange={jest.fn()} />);

const textbox = screen.getByRole('textbox');
expect(textbox).toBeInTheDocument();
});

it('renders correctly with a non-empty value', () => {
render(
<WorkflowQueriesTileInput value="test value" onChange={jest.fn()} />
);

const textbox = screen.getByRole('textbox');
expect(textbox).toHaveTextContent('test value');
});

it('calls onChange when typed into', async () => {
const user = userEvent.setup();
const mockOnChange = jest.fn();

render(<WorkflowQueriesTileInput value="" onChange={mockOnChange} />);

await user.type(screen.getByRole('textbox'), 'a');

expect(mockOnChange).toHaveBeenCalledWith('a');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { type Theme } from 'baseui';
import { type TextareaOverrides } from 'baseui/textarea';
import { type StyleObject } from 'styletron-react';

export const overrides = {
textarea: {
Input: {
style: ({ $theme }: { $theme: Theme }): StyleObject => ({
...$theme.typography.MonoParagraphXSmall,
}),
},
} satisfies TextareaOverrides,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';

import { Textarea } from 'baseui/textarea';

import { overrides } from './workflow-queries-tile-input.styles';
import { type Props } from './workflow-queries-tile-input.types';

export default function WorkflowQueriesTileInput(props: Props) {
return (
<Textarea
value={props.value}
onChange={(e) => props.onChange(e.target.value)}
overrides={overrides.textarea}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type Props = {
value: string;
onChange: (value: string) => void;
};
Loading

0 comments on commit d38ba48

Please sign in to comment.