diff --git a/web/vtadmin/src/components/App.tsx b/web/vtadmin/src/components/App.tsx index 15d18b4921a..fb656060431 100644 --- a/web/vtadmin/src/components/App.tsx +++ b/web/vtadmin/src/components/App.tsx @@ -119,7 +119,7 @@ export const App = () => { - + diff --git a/web/vtadmin/src/components/placeholders/QueryLoadingPlaceholder.tsx b/web/vtadmin/src/components/placeholders/QueryLoadingPlaceholder.tsx index acdc68bf0d0..abb4bbe72e9 100644 --- a/web/vtadmin/src/components/placeholders/QueryLoadingPlaceholder.tsx +++ b/web/vtadmin/src/components/placeholders/QueryLoadingPlaceholder.tsx @@ -18,13 +18,17 @@ import { UseQueryResult } from 'react-query'; import { Spinner } from '../Spinner'; interface Props { - query: UseQueryResult; + query?: UseQueryResult; + queries?: UseQueryResult[]; } /** * QueryLoadingPlaceholder is a straightforward component that displays a loading - * message when given a query from a useQueryHook. To simplify its use, this component - * takes care of hiding itself when `props.query` is in any state other than "loading". + * message when given one or more queries as returned from useQueryHook. + * + * To simplify its use, this component takes care of hiding itself when all of its + * queries have completed loading. + * * It's perfectly fine to render it this: * * @@ -34,16 +38,22 @@ interface Props { * {query.isLoading && } */ export const QueryLoadingPlaceholder: React.FC = (props) => { - if (!props.query.isLoading) { + const queries = props.queries || []; + if (props.query) { + queries.push(props.query); + } + + const anyLoading = queries.some((q) => q.isLoading); + if (!anyLoading) { return null; } + const maxFailureCount = Math.max(...queries.map((q) => q.failureCount)); + return (
-
- {props.query.failureCount > 2 ? 'Still loading...' : 'Loading...'} -
+
{maxFailureCount > 2 ? 'Still loading...' : 'Loading...'}
); }; diff --git a/web/vtadmin/src/components/routes/Clusters.tsx b/web/vtadmin/src/components/routes/Clusters.tsx index b40892f4d01..5d2cf439a9b 100644 --- a/web/vtadmin/src/components/routes/Clusters.tsx +++ b/web/vtadmin/src/components/routes/Clusters.tsx @@ -24,14 +24,15 @@ import { DataCell } from '../dataTable/DataCell'; import { ContentContainer } from '../layout/ContentContainer'; import { WorkspaceHeader } from '../layout/WorkspaceHeader'; import { WorkspaceTitle } from '../layout/WorkspaceTitle'; +import { QueryLoadingPlaceholder } from '../placeholders/QueryLoadingPlaceholder'; export const Clusters = () => { useDocumentTitle('Clusters'); - const { data } = useClusters(); + const clustersQuery = useClusters(); const rows = React.useMemo(() => { - return orderBy(data, ['name']); - }, [data]); + return orderBy(clustersQuery.data, ['name']); + }, [clustersQuery.data]); const renderRows = (rows: pb.Cluster[]) => rows.map((cluster, idx) => ( @@ -50,6 +51,7 @@ export const Clusters = () => {
+
diff --git a/web/vtadmin/src/components/routes/Gates.tsx b/web/vtadmin/src/components/routes/Gates.tsx index 42907875dc9..354d4507a3e 100644 --- a/web/vtadmin/src/components/routes/Gates.tsx +++ b/web/vtadmin/src/components/routes/Gates.tsx @@ -26,15 +26,16 @@ import { DataTable } from '../dataTable/DataTable'; import { ContentContainer } from '../layout/ContentContainer'; import { WorkspaceHeader } from '../layout/WorkspaceHeader'; import { WorkspaceTitle } from '../layout/WorkspaceTitle'; +import { QueryLoadingPlaceholder } from '../placeholders/QueryLoadingPlaceholder'; export const Gates = () => { useDocumentTitle('Gates'); - const { data } = useGates(); + const gatesQuery = useGates(); const { value: filter, updateValue: updateFilter } = useSyncedURLParam('filter'); const rows = React.useMemo(() => { - const mapped = (data || []).map((g) => ({ + const mapped = (gatesQuery.data || []).map((g) => ({ cell: g.cell, cluster: g.cluster?.name, hostname: g.hostname, @@ -43,7 +44,7 @@ export const Gates = () => { })); const filtered = filterNouns(filter, mapped); return orderBy(filtered, ['cluster', 'pool', 'hostname', 'cell']); - }, [data, filter]); + }, [gatesQuery.data, filter]); const renderRows = (gates: typeof rows) => gates.map((gate, idx) => ( @@ -72,6 +73,7 @@ export const Gates = () => { value={filter || ''} /> + ); diff --git a/web/vtadmin/src/components/routes/Keyspaces.tsx b/web/vtadmin/src/components/routes/Keyspaces.tsx index 6d9ef9158be..88e45885f64 100644 --- a/web/vtadmin/src/components/routes/Keyspaces.tsx +++ b/web/vtadmin/src/components/routes/Keyspaces.tsx @@ -33,15 +33,16 @@ import KeyspaceActions from './keyspaces/KeyspaceActions'; import { ReadOnlyGate } from '../ReadOnlyGate'; import { isReadOnlyMode } from '../../util/env'; import { Link } from 'react-router-dom'; +import { QueryLoadingPlaceholder } from '../placeholders/QueryLoadingPlaceholder'; export const Keyspaces = () => { useDocumentTitle('Keyspaces'); const { value: filter, updateValue: updateFilter } = useSyncedURLParam('filter'); - const { data } = useKeyspaces(); + const keyspacesQuery = useKeyspaces(); const ksRows = React.useMemo(() => { - const mapped = (data || []).map((k) => { + const mapped = (keyspacesQuery.data || []).map((k) => { const shardsByState = getShardsByState(k); return { @@ -54,7 +55,7 @@ export const Keyspaces = () => { }); const filtered = filterNouns(filter, mapped); return orderBy(filtered, ['cluster', 'name']); - }, [data, filter]); + }, [keyspacesQuery.data, filter]); const renderRows = (rows: typeof ksRows) => rows.map((row, idx) => ( @@ -114,6 +115,7 @@ export const Keyspaces = () => { data={ksRows} renderRows={renderRows} /> + diff --git a/web/vtadmin/src/components/routes/Schemas.tsx b/web/vtadmin/src/components/routes/Schemas.tsx index 6d397e9a3a2..fff3bb8b2db 100644 --- a/web/vtadmin/src/components/routes/Schemas.tsx +++ b/web/vtadmin/src/components/routes/Schemas.tsx @@ -30,6 +30,7 @@ import { ContentContainer } from '../layout/ContentContainer'; import { WorkspaceHeader } from '../layout/WorkspaceHeader'; import { WorkspaceTitle } from '../layout/WorkspaceTitle'; import { KeyspaceLink } from '../links/KeyspaceLink'; +import { QueryLoadingPlaceholder } from '../placeholders/QueryLoadingPlaceholder'; import { HelpTooltip } from '../tooltip/HelpTooltip'; const TABLE_COLUMNS = [ @@ -62,11 +63,11 @@ const TABLE_COLUMNS = [ export const Schemas = () => { useDocumentTitle('Schemas'); - const { data = [] } = useSchemas(); + const schemasQuery = useSchemas(); const { value: filter, updateValue: updateFilter } = useSyncedURLParam('filter'); const filteredData = React.useMemo(() => { - const tableDefinitions = getTableDefinitions(data); + const tableDefinitions = getTableDefinitions(schemasQuery.data); const mapped = tableDefinitions.map((d) => ({ cluster: d.cluster?.name, @@ -78,7 +79,7 @@ export const Schemas = () => { const filtered = filterNouns(filter, mapped); return orderBy(filtered, ['cluster', 'keyspace', 'table']); - }, [data, filter]); + }, [schemasQuery.data, filter]); const renderRows = (rows: typeof filteredData) => rows.map((row, idx) => { @@ -120,6 +121,7 @@ export const Schemas = () => { value={filter || ''} /> + ); diff --git a/web/vtadmin/src/components/routes/Tablets.tsx b/web/vtadmin/src/components/routes/Tablets.tsx index 70c5bdf4c4c..b1bf246d532 100644 --- a/web/vtadmin/src/components/routes/Tablets.tsx +++ b/web/vtadmin/src/components/routes/Tablets.tsx @@ -37,6 +37,7 @@ import { ShardLink } from '../links/ShardLink'; import InfoDropdown from './tablets/InfoDropdown'; import { isReadOnlyMode } from '../../util/env'; import { ReadOnlyGate } from '../ReadOnlyGate'; +import { QueryLoadingPlaceholder } from '../placeholders/QueryLoadingPlaceholder'; const COLUMNS = ['Keyspace', 'Shard', 'Alias', 'Type', 'Tablet State', 'Hostname']; if (!isReadOnlyMode()) { @@ -47,12 +48,16 @@ export const Tablets = () => { useDocumentTitle('Tablets'); const { value: filter, updateValue: updateFilter } = useSyncedURLParam('filter'); - const { data = [] } = useTablets(); - const { data: keyspaces = [], ...ksQuery } = useKeyspaces(); + + const tabletsQuery = useTablets(); + const keyspacesQuery = useKeyspaces(); + const queries = [tabletsQuery, keyspacesQuery]; + + const { data: keyspaces = [], ...ksQuery } = keyspacesQuery; const filteredData = React.useMemo(() => { - return formatRows(data, keyspaces, filter); - }, [data, filter, keyspaces]); + return formatRows(tabletsQuery.data, keyspaces, filter); + }, [tabletsQuery.data, filter, keyspaces]); const renderRows = React.useCallback( (rows: typeof filteredData) => { @@ -119,6 +124,7 @@ export const Tablets = () => { value={filter || ''} /> + ); diff --git a/web/vtadmin/src/components/routes/Vtctlds.tsx b/web/vtadmin/src/components/routes/Vtctlds.tsx index 331702abdad..26efd8498b4 100644 --- a/web/vtadmin/src/components/routes/Vtctlds.tsx +++ b/web/vtadmin/src/components/routes/Vtctlds.tsx @@ -24,9 +24,12 @@ import { DataTable } from '../dataTable/DataTable'; import { ContentContainer } from '../layout/ContentContainer'; import { WorkspaceHeader } from '../layout/WorkspaceHeader'; import { WorkspaceTitle } from '../layout/WorkspaceTitle'; +import { QueryLoadingPlaceholder } from '../placeholders/QueryLoadingPlaceholder'; export const Vtctlds = () => { - const { data: vtctlds = [], ...q } = useVtctlds(); + const vtctldsQuery = useVtctlds(); + const { data: vtctlds = [] } = vtctldsQuery; + const { value: filter, updateValue: updateFilter } = useSyncedURLParam('filter'); const data = useMemo(() => { @@ -81,9 +84,7 @@ export const Vtctlds = () => { value={filter || ''} /> - - {/* TODO skeleton placeholder */} - {!!q.isLoading &&
Loading
} + ); diff --git a/web/vtadmin/src/components/routes/Workflows.tsx b/web/vtadmin/src/components/routes/Workflows.tsx index 479056604a0..1682e8dc0d8 100644 --- a/web/vtadmin/src/components/routes/Workflows.tsx +++ b/web/vtadmin/src/components/routes/Workflows.tsx @@ -33,14 +33,17 @@ import { WorkspaceTitle } from '../layout/WorkspaceTitle'; import { DataFilter } from '../dataTable/DataFilter'; import { Tooltip } from '../tooltip/Tooltip'; import { KeyspaceLink } from '../links/KeyspaceLink'; +import { QueryLoadingPlaceholder } from '../placeholders/QueryLoadingPlaceholder'; +import { UseQueryResult } from 'react-query'; export const Workflows = () => { useDocumentTitle('Workflows'); - const { data } = useWorkflows(); + const workflowsQuery = useWorkflows(); + const { value: filter, updateValue: updateFilter } = useSyncedURLParam('filter'); const sortedData = React.useMemo(() => { - const mapped = (data || []).map((workflow) => ({ + const mapped = (workflowsQuery.data || []).map((workflow) => ({ clusterID: workflow.cluster?.id, clusterName: workflow.cluster?.name, keyspace: workflow.keyspace, @@ -54,7 +57,7 @@ export const Workflows = () => { })); const filtered = filterNouns(filter, mapped); return orderBy(filtered, ['name', 'clusterName', 'source', 'target']); - }, [data, filter]); + }, [workflowsQuery.data, filter]); const renderRows = (rows: typeof sortedData) => rows.map((row, idx) => { @@ -150,6 +153,8 @@ export const Workflows = () => { data={sortedData} renderRows={renderRows} /> + + );