console: Reset track tables search on input clear button [dsf-231]

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/8485
GitOrigin-RevId: 7b17c41c294b2ddffcdf6981e25196e79214777d
This commit is contained in:
Matthew Goodwin 2023-03-24 14:24:17 -05:00 committed by hasura-bot
parent b2f683f56d
commit 3d071f96b9
8 changed files with 109 additions and 194 deletions

View File

@ -2,9 +2,9 @@ import React from 'react';
import * as Tabs from '@radix-ui/react-tabs';
import { useTables } from './hooks/useTables';
import { TrackedTables } from './components/TrackedTables';
import { UntrackedTables } from './components/UntrackedTables';
import { TableList } from './components/TableList';
import Skeleton from 'react-loading-skeleton';
import { TrackableTable } from './types';
const classNames = {
selected:
@ -17,6 +17,24 @@ interface Props {
dataSourceName: string;
}
const groupTables = (tables: TrackableTable[]) => {
const trackedTables: TrackableTable[] = [];
const untrackedTables: TrackableTable[] = [];
if (tables) {
//doing this in one loop to reduce the overhead for large data sets
tables.forEach(t => {
if (t.is_tracked) {
trackedTables.push(t);
} else {
untrackedTables.push(t);
}
});
}
return { trackedTables, untrackedTables };
};
export const TrackTables = ({ dataSourceName }: Props) => {
const [tab, setTab] = React.useState<'tracked' | 'untracked'>('untracked');
@ -24,6 +42,11 @@ export const TrackTables = ({ dataSourceName }: Props) => {
dataSourceName,
});
const { trackedTables, untrackedTables } = React.useMemo(
() => groupTables(data ?? []),
[data]
);
if (isLoading)
return (
<div className="px-md">
@ -33,9 +56,6 @@ export const TrackTables = ({ dataSourceName }: Props) => {
if (!data) return <div className="px-md">Something went wrong</div>;
const trackedTables = data.filter(({ is_tracked }) => is_tracked);
const untrackedTables = data.filter(({ is_tracked }) => !is_tracked);
return (
<Tabs.Root
defaultValue="untracked"
@ -74,10 +94,15 @@ export const TrackTables = ({ dataSourceName }: Props) => {
</Tabs.List>
<Tabs.Content value="tracked" className="px-md">
<TrackedTables dataSourceName={dataSourceName} tables={trackedTables} />
<TableList
mode={'track'}
dataSourceName={dataSourceName}
tables={trackedTables}
/>
</Tabs.Content>
<Tabs.Content value="untracked" className="px-md">
<UntrackedTables
<TableList
mode={'untrack'}
dataSourceName={dataSourceName}
tables={untrackedTables}
/>

View File

@ -1,36 +1,38 @@
import React from 'react';
import { FaSearch } from 'react-icons/fa';
import { z } from 'zod';
import { Button } from '../../../../new-components/Button';
import { InputField, SimpleForm } from '../../../../new-components/Form';
import { Input } from '../../../../new-components/Form';
type SearchBarProps = {
onSubmit: (searchText: string) => void;
onSearch: (searchText: string) => void;
};
const schema = z.object({
searchText: z.string().optional(),
});
export const SearchBar = (props: SearchBarProps) => {
const { onSubmit } = props;
export const SearchBar = ({ onSearch }: SearchBarProps) => {
const timer = React.useRef<ReturnType<typeof setTimeout> | null>(null);
const [value, setValue] = React.useState('');
return (
<SimpleForm
schema={schema}
onSubmit={data => {
onSubmit(data.searchText ?? '');
}}
>
<div className="flex gap-2">
<InputField
name="searchText"
icon={<FaSearch />}
iconPosition="start"
noErrorPlaceholder
clearButton
/>
<Button type="submit">Search</Button>
</div>
</SimpleForm>
<div className="flex gap-2">
<Input
name="searchText"
icon={<FaSearch />}
iconPosition="start"
noErrorPlaceholder
clearButton
fieldProps={{ value: value }}
onChange={e => {
setValue(e.target.value);
if (timer.current) clearTimeout(timer.current);
timer.current = setTimeout(() => {
onSearch(e.target.value);
}, 20);
}}
onClearButtonClick={() => {
setValue('');
onSearch('');
}}
/>
</div>
);
};

View File

@ -1,31 +1,34 @@
import React, { useState } from 'react';
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa';
import { useTrackTable } from '../..';
import { Badge } from '../../../../new-components/Badge';
import { Button } from '../../../../new-components/Button';
import { CardedTable } from '../../../../new-components/CardedTable';
import { IndicatorCard } from '../../../../new-components/IndicatorCard';
import React, { useState } from 'react';
import { useCheckRows } from '../hooks/useCheckRows';
import { TrackableTable } from '../types';
import { TableRow } from './TableRow';
import {
DEFAULT_PAGE_NUMBER,
DEFAULT_PAGE_SIZE,
DEFAULT_PAGE_SIZES,
} from '../constants';
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa';
import { useCheckRows } from '../hooks/useCheckRows';
import { TrackableTable } from '../types';
import { paginate, search } from '../utils';
import { SearchBar } from './SearchBar';
import { Badge } from '../../../../new-components/Badge';
import { TableRow } from './TableRow';
interface TrackTableProps {
interface TableListProps {
dataSourceName: string;
tables: TrackableTable[];
mode: 'track' | 'untrack';
}
export const TrackedTables = (props: TrackTableProps) => {
export const TableList = (props: TableListProps) => {
const { mode, dataSourceName, tables } = props;
const [pageNumber, setPageNumber] = useState(DEFAULT_PAGE_NUMBER);
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
const [searchText, setSearchText] = useState('');
const filteredTables = search(props.tables, searchText);
const filteredTables = search(tables, searchText);
const checkboxRef = React.useRef<HTMLInputElement>(null);
@ -37,19 +40,27 @@ export const TrackedTables = (props: TrackTableProps) => {
checkboxRef.current.indeterminate = inputStatus === 'indeterminate';
}, [inputStatus]);
const { untrackTables, loading } = useTrackTable(props.dataSourceName);
const { untrackTables, trackTables, loading } = useTrackTable(dataSourceName);
const onClick = () => {
untrackTables(
filteredTables.filter(({ name }) => checkedIds.includes(name))
const tables = filteredTables.filter(({ name }) =>
checkedIds.includes(name)
);
if (mode === 'track') {
untrackTables(tables);
} else {
trackTables(tables);
}
reset();
};
if (!props.tables.length) {
if (!tables.length) {
return (
<div className="space-y-4">
<IndicatorCard>No tracked tables found</IndicatorCard>
<IndicatorCard>{`No ${
mode === 'track' ? 'tracked' : 'untracked'
} tables found`}</IndicatorCard>
</div>
);
}
@ -65,15 +76,17 @@ export const TrackedTables = (props: TrackTableProps) => {
isLoading={loading}
loadingText="Please Wait"
>
Untrack Selected ({checkedIds.length})
{`${mode === 'track' ? 'Untrack' : 'Track'} Selected (${
checkedIds.length
})`}
</Button>
<span className="border-r border-slate-300"></span>
<div className="flex gap-2">
<SearchBar onSubmit={data => setSearchText(data)} />
<SearchBar onSearch={data => setSearchText(data)} />
{searchText.length ? (
<Badge>{filteredTables.length} results found</Badge>
) : null}
</div>{' '}
</div>
</div>
<div className="flex gap-1">
@ -126,7 +139,7 @@ export const TrackedTables = (props: TrackTableProps) => {
<TableRow
key={table.id}
table={table}
dataSourceName={props.dataSourceName}
dataSourceName={dataSourceName}
checked={checkedIds.includes(table.id)}
reset={reset}
onChange={() => onCheck(table.id)}

View File

@ -1,136 +0,0 @@
import React, { useState } from 'react';
import { Button } from '../../../../new-components/Button';
import { CardedTable } from '../../../../new-components/CardedTable';
import { IndicatorCard } from '../../../../new-components/IndicatorCard';
import { useTrackTable } from '../..';
import { useCheckRows } from '../hooks/useCheckRows';
import { TrackableTable } from '../types';
import { TableRow } from './TableRow';
import { DEFAULT_PAGE_SIZES } from '../constants';
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa';
import { DEFAULT_PAGE_NUMBER, DEFAULT_PAGE_SIZE } from '../constants';
import { paginate, search } from '../utils';
import { SearchBar } from './SearchBar';
import { Badge } from '../../../../new-components/Badge';
interface TrackTableProps {
dataSourceName: string;
tables: TrackableTable[];
}
export const UntrackedTables = (props: TrackTableProps) => {
const [pageNumber, setPageNumber] = useState(DEFAULT_PAGE_NUMBER);
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
const [searchText, setSearchText] = useState('');
const filteredTables = search(props.tables, searchText);
const checkboxRef = React.useRef<HTMLInputElement>(null);
const { checkedIds, onCheck, allChecked, toggleAll, reset, inputStatus } =
useCheckRows(filteredTables || []);
React.useEffect(() => {
if (!checkboxRef.current) return;
checkboxRef.current.indeterminate = inputStatus === 'indeterminate';
}, [inputStatus]);
const { trackTables, loading } = useTrackTable(props.dataSourceName);
const onClick = () => {
trackTables(props.tables.filter(({ name }) => checkedIds.includes(name)));
reset();
};
if (!props.tables.length) {
return (
<div className="space-y-4">
<IndicatorCard>No untracked tables found</IndicatorCard>
</div>
);
}
return (
<div className="space-y-4">
<div className="flex justify-between space-x-4">
<div className="flex gap-5">
<Button
mode="primary"
disabled={!checkedIds.length}
onClick={onClick}
isLoading={loading}
loadingText="Please Wait"
>
Track Selected ({checkedIds.length})
</Button>
<span className="border-r border-slate-300"></span>
<div className="flex gap-2">
<SearchBar onSubmit={data => setSearchText(data)} />
{searchText.length ? (
<Badge>{filteredTables.length} results found</Badge>
) : null}
</div>
</div>
<div className="flex gap-1">
<Button
icon={<FaAngleLeft />}
onClick={() => setPageNumber(pageNumber - 1)}
disabled={pageNumber === 1}
/>
<select
value={pageSize}
onChange={e => {
setPageSize(Number(e.target.value));
}}
className="block w-full max-w-xl h-8 min-h-full shadow-sm rounded pl-3 pr-6 py-0.5 border border-gray-300 hover:border-gray-400 focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-yellow-200 focus-visible:border-yellow-400"
>
{DEFAULT_PAGE_SIZES.map(_pageSize => (
<option key={_pageSize} value={_pageSize}>
Show {_pageSize} tables
</option>
))}
</select>
<Button
icon={<FaAngleRight />}
onClick={() => setPageNumber(pageNumber + 1)}
disabled={pageNumber >= filteredTables.length / pageSize}
/>
</div>
</div>
<CardedTable.Table>
<CardedTable.TableHead>
<CardedTable.TableHeadRow>
<th className="w-0 bg-gray-50 px-sm text-sm font-semibold text-muted uppercase tracking-wider border-r">
<input
ref={checkboxRef}
type="checkbox"
className="cursor-pointer
rounded border shadow-sm border-gray-400 hover:border-gray-500 focus:ring-yellow-400"
checked={allChecked}
onChange={toggleAll}
/>
</th>
<CardedTable.TableHeadCell>Object</CardedTable.TableHeadCell>
<CardedTable.TableHeadCell>Type</CardedTable.TableHeadCell>
<CardedTable.TableHeadCell>Actions</CardedTable.TableHeadCell>
</CardedTable.TableHeadRow>
</CardedTable.TableHead>
<CardedTable.TableBody>
{paginate(filteredTables, pageSize, pageNumber).map(table => (
<TableRow
key={table.id}
table={table}
dataSourceName={props.dataSourceName}
checked={checkedIds.includes(table.id)}
reset={reset}
onChange={() => onCheck(table.id)}
/>
))}
</CardedTable.TableBody>
</CardedTable.Table>
</div>
);
};

View File

@ -135,3 +135,9 @@ Untrack.play = async ({ canvasElement }) => {
timeout: 2000,
});
};
export const MassiveTableAmount = TrackedTables.bind({});
MassiveTableAmount.parameters = {
msw: handlers(1000000),
};

View File

@ -87,7 +87,7 @@ function isUntrackTable(arg: any) {
return arg.type === 'postgres_untrack_table';
}
const runSQLResponse: RunSQLResponse = {
const runSQLResponse = (size = 1700): RunSQLResponse => ({
result_type: 'TuplesOk',
result: [
['table_name', 'table_schema', 'table_type'],
@ -109,9 +109,9 @@ const runSQLResponse: RunSQLResponse = {
['InvoiceLineView', 'public', 'VIEW'],
['InvoiceView', 'public', 'VIEW'],
['TrackView', 'public', 'VIEW'],
...createTables(1700),
...createTables(size),
],
};
});
function createTables(count: number) {
const tables = [];
@ -121,7 +121,7 @@ function createTables(count: number) {
return tables;
}
export const handlers = () => [
export const handlers = (amountOfTables = 1700) => [
rest.post(`http://localhost:8080/v1/metadata`, async (req, res, ctx) => {
const body = (await req.json()) as TMigration['query'];
if (isTrackOrUntrackTable(body)) {
@ -137,6 +137,6 @@ export const handlers = () => [
return res(ctx.json({ ...metadata }));
}),
rest.post(`http://localhost:8080/v2/query`, (req, res, ctx) => {
return res(ctx.json(runSQLResponse));
return res(ctx.json(runSQLResponse(amountOfTables)));
}),
];

View File

@ -12,7 +12,6 @@ export const paginate = <T>(
export const search = (tables: TrackableTable[], searchText: string) => {
if (!searchText.length) return tables;
console.log(searchText, tables);
return tables.filter(table =>
table.name.toLowerCase().includes(searchText.toLowerCase())
);

View File

@ -68,6 +68,10 @@ export type InputFieldProps<T extends z.infer<Schema>> =
* Renders a button to clear the input onClick
*/
clearButton?: boolean;
/**
* Handler for when a user presses the clear button, and the state is cleared.
*/
onClear?: () => void;
/**
* The input field classes
*/
@ -87,6 +91,7 @@ export const InputField = <T extends z.infer<Schema>>({
inputTransform,
renderDescriptionLineBreaks = false,
clearButton,
onClear,
inputClassName,
fieldProps = {},
...wrapperProps
@ -129,6 +134,7 @@ export const InputField = <T extends z.infer<Schema>>({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
setValue(name, '');
onClear?.();
};
return (