mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 08:02:15 +03:00
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:
parent
b2f683f56d
commit
3d071f96b9
@ -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}
|
||||
/>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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)}
|
@ -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>
|
||||
);
|
||||
};
|
@ -135,3 +135,9 @@ Untrack.play = async ({ canvasElement }) => {
|
||||
timeout: 2000,
|
||||
});
|
||||
};
|
||||
|
||||
export const MassiveTableAmount = TrackedTables.bind({});
|
||||
|
||||
MassiveTableAmount.parameters = {
|
||||
msw: handlers(1000000),
|
||||
};
|
||||
|
@ -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)));
|
||||
}),
|
||||
];
|
||||
|
@ -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())
|
||||
);
|
||||
|
@ -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 (
|
||||
|
Loading…
Reference in New Issue
Block a user