diff --git a/web/vtadmin/src/components/NavRail.tsx b/web/vtadmin/src/components/NavRail.tsx index 9863ec5d547..0dc401da6e1 100644 --- a/web/vtadmin/src/components/NavRail.tsx +++ b/web/vtadmin/src/components/NavRail.tsx @@ -18,17 +18,20 @@ import { Link, NavLink } from 'react-router-dom'; import style from './NavRail.module.scss'; import logo from '../img/vitess-icon-color.svg'; -import { useClusters, useGates, useKeyspaces, useTableDefinitions, useTablets, useWorkflows } from '../hooks/api'; +import { useClusters, useGates, useKeyspaces, useSchemas, useTablets, useWorkflows } from '../hooks/api'; import { Icon, Icons } from './Icon'; +import { getTableDefinitions } from '../util/tableDefinitions'; export const NavRail = () => { const { data: clusters = [] } = useClusters(); const { data: keyspaces = [] } = useKeyspaces(); const { data: gates = [] } = useGates(); - const { data: schemas = [] } = useTableDefinitions(); + const { data: schemas = [] } = useSchemas(); const { data: tablets = [] } = useTablets(); const { data: workflows = [] } = useWorkflows(); + const tds = React.useMemo(() => getTableDefinitions(schemas), [schemas]); + return (
@@ -57,7 +60,7 @@ export const NavRail = () => {
  • - +
  • diff --git a/web/vtadmin/src/components/routes/Schemas.tsx b/web/vtadmin/src/components/routes/Schemas.tsx index 82691ac71e7..583944f2709 100644 --- a/web/vtadmin/src/components/routes/Schemas.tsx +++ b/web/vtadmin/src/components/routes/Schemas.tsx @@ -17,9 +17,10 @@ import { orderBy } from 'lodash-es'; import * as React from 'react'; import { Link } from 'react-router-dom'; -import { useTableDefinitions } from '../../hooks/api'; +import { useSchemas } from '../../hooks/api'; import { useDocumentTitle } from '../../hooks/useDocumentTitle'; import { filterNouns } from '../../util/filterNouns'; +import { getTableDefinitions } from '../../util/tableDefinitions'; import { Button } from '../Button'; import { DataCell } from '../dataTable/DataCell'; import { DataTable } from '../dataTable/DataTable'; @@ -30,11 +31,13 @@ import style from './Schemas.module.scss'; export const Schemas = () => { useDocumentTitle('Schemas'); - const { data = [] } = useTableDefinitions(); + const { data = [] } = useSchemas(); const [filter, setFilter] = React.useState(''); const filteredData = React.useMemo(() => { - const mapped = data.map((d) => ({ + const tableDefinitions = getTableDefinitions(data); + + const mapped = tableDefinitions.map((d) => ({ cluster: d.cluster?.name, clusterID: d.cluster?.id, keyspace: d.keyspace, diff --git a/web/vtadmin/src/hooks/api.ts b/web/vtadmin/src/hooks/api.ts index b49b3317eae..1cc4f2d4fc6 100644 --- a/web/vtadmin/src/hooks/api.ts +++ b/web/vtadmin/src/hooks/api.ts @@ -86,44 +86,6 @@ export const useWorkflows = (...args: Parameters) = return { data: workflows, ...query }; }; -export interface TableDefinition { - cluster?: pb.Schema['cluster']; - keyspace?: pb.Schema['keyspace']; - // The [0] index is a typescript quirk to infer the type of - // an entry in an array, and therefore the type of ALL entries - // in the array (not just the first one). - tableDefinition?: pb.Schema['table_definitions'][0]; -} - -/** - * useTableDefinitions is a helper hook for when a flattened list - * of table definitions (across all keyspaces and clusters) is required, - * instead of the default vtadmin-api/Vitess grouping of schemas by keyspace. - * - * Under the hood, this calls the useSchemas hook and therefore uses - * the same query cache. - */ -export const useTableDefinitions = (...args: Parameters) => { - const { data, ...query } = useSchemas(...args); - - if (!Array.isArray(data)) { - return { data, ...query }; - } - - const tds = data.reduce((acc: TableDefinition[], schema: pb.Schema) => { - (schema.table_definitions || []).forEach((td) => { - acc.push({ - cluster: schema.cluster, - keyspace: schema.keyspace, - tableDefinition: td, - }); - }); - return acc; - }, []); - - return { ...query, data: tds }; -}; - /** * useSchema is a query hook that fetches a single schema for the given parameters. */ diff --git a/web/vtadmin/src/util/tableDefinitions.test.ts b/web/vtadmin/src/util/tableDefinitions.test.ts new file mode 100644 index 00000000000..ebf12b86785 --- /dev/null +++ b/web/vtadmin/src/util/tableDefinitions.test.ts @@ -0,0 +1,132 @@ +/** + * Copyright 2021 The Vitess Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { vtadmin as pb } from '../proto/vtadmin'; +import { getTableDefinitions, TableDefinition } from './tableDefinitions'; + +describe('getTableDefinitions', () => { + const tests: { + name: string; + input: pb.Schema[] | null | undefined; + expected: TableDefinition[]; + }[] = [ + { + name: 'handles empty arrays', + input: [], + expected: [], + }, + { + name: 'handles undefined input', + input: undefined, + expected: [], + }, + { + name: 'handles null input', + input: null, + expected: [], + }, + { + name: 'extracts table definitions and sizes', + input: [ + pb.Schema.create({ + cluster: { id: 'c1', name: 'cluster1' }, + keyspace: 'fauna', + table_definitions: [{ name: 'cats' }, { name: 'dogs' }], + table_sizes: { + cats: { row_count: 1234, data_length: 4321 }, + dogs: { row_count: 5678, data_length: 8765 }, + }, + }), + pb.Schema.create({ + cluster: { id: 'c2', name: 'cluster2' }, + keyspace: 'flora', + table_definitions: [{ name: 'trees' }, { name: 'flowers' }], + table_sizes: { + flowers: { row_count: 1234, data_length: 4321 }, + trees: { row_count: 5678, data_length: 8765 }, + }, + }), + ], + expected: [ + { + cluster: { id: 'c1', name: 'cluster1' }, + keyspace: 'fauna', + tableDefinition: { name: 'cats' }, + tableSize: { row_count: 1234, data_length: 4321 }, + }, + { + cluster: { id: 'c1', name: 'cluster1' }, + keyspace: 'fauna', + tableDefinition: { name: 'dogs' }, + tableSize: { row_count: 5678, data_length: 8765 }, + }, + { + cluster: { id: 'c2', name: 'cluster2' }, + keyspace: 'flora', + tableDefinition: { name: 'trees' }, + tableSize: { row_count: 5678, data_length: 8765 }, + }, + { + cluster: { id: 'c2', name: 'cluster2' }, + keyspace: 'flora', + tableDefinition: { name: 'flowers' }, + tableSize: { row_count: 1234, data_length: 4321 }, + }, + ], + }, + { + name: 'handles when a table has a definition but no defined size', + input: [ + pb.Schema.create({ + cluster: { id: 'c1', name: 'cluster1' }, + keyspace: 'fauna', + table_definitions: [{ name: 'cats' }], + }), + ], + expected: [ + { + cluster: { id: 'c1', name: 'cluster1' }, + keyspace: 'fauna', + tableDefinition: { name: 'cats' }, + }, + ], + }, + { + name: 'handles when a table defines sizes but not a definition', + input: [ + pb.Schema.create({ + cluster: { id: 'c1', name: 'cluster1' }, + keyspace: 'fauna', + table_sizes: { + cats: { row_count: 1234, data_length: 4321 }, + }, + }), + ], + expected: [ + { + cluster: { id: 'c1', name: 'cluster1' }, + keyspace: 'fauna', + tableDefinition: { name: 'cats' }, + tableSize: { row_count: 1234, data_length: 4321 }, + }, + ], + }, + ]; + + test.each(tests.map(Object.values))('%s', (name: string, input: pb.Schema[], expected: TableDefinition[]) => { + const result = getTableDefinitions(input); + expect(result).toEqual(expected); + }); +}); diff --git a/web/vtadmin/src/util/tableDefinitions.ts b/web/vtadmin/src/util/tableDefinitions.ts new file mode 100644 index 00000000000..ba367e25f54 --- /dev/null +++ b/web/vtadmin/src/util/tableDefinitions.ts @@ -0,0 +1,61 @@ +/** + * Copyright 2021 The Vitess Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { vtadmin as pb } from '../proto/vtadmin'; + +export interface TableDefinition { + cluster?: pb.Schema['cluster']; + keyspace?: pb.Schema['keyspace']; + // The [0] index is a typescript quirk to infer the type of + // an entry in an array, and therefore the type of ALL entries + // in the array (not just the first one). + tableDefinition?: pb.Schema['table_definitions'][0]; + tableSize?: pb.Schema['table_sizes'][0]; +} + +/** + * getTableDefinitions is a helper function for transforming an array of Schemas + * into a flat array of table definitions. + */ +export const getTableDefinitions = (schemas: pb.Schema[] | null | undefined): TableDefinition[] => { + return (schemas || []).reduce((acc: TableDefinition[], schema: pb.Schema) => { + // Index table definitions in this Schema by name, since we necessarily loop twice + const sts: { [tableName: string]: TableDefinition } = {}; + + (schema.table_definitions || []).forEach((td) => { + if (!td.name) return; + sts[td.name] = { + cluster: schema.cluster, + keyspace: schema.keyspace, + tableDefinition: td, + }; + }); + + Object.entries(schema.table_sizes || {}).forEach(([tableName, tableSize]) => { + // Include tables that have size/rows defined but do not have a table definition. + if (!(tableName in sts)) { + sts[tableName] = { + cluster: schema.cluster, + keyspace: schema.keyspace, + tableDefinition: { name: tableName }, + }; + } + + sts[tableName].tableSize = tableSize; + }); + + return acc.concat(Object.values(sts)); + }, []); +};