diff --git a/web/vtadmin/src/api/http.ts b/web/vtadmin/src/api/http.ts index aa3200a72c2..937dd6cf080 100644 --- a/web/vtadmin/src/api/http.ts +++ b/web/vtadmin/src/api/http.ts @@ -182,6 +182,20 @@ export const fetchTablets = async () => }, }); +export interface FetchVSchemaParams { + clusterID: string; + keyspace: string; +} + +export const fetchVSchema = async ({ clusterID, keyspace }: FetchVSchemaParams) => { + const { result } = await vtfetch(`/api/vschema/${clusterID}/${keyspace}`); + + const err = pb.VSchema.verify(result); + if (err) throw Error(err); + + return pb.VSchema.create(result); +}; + export const fetchWorkflows = async () => { const { result } = await vtfetch(`/api/workflows`); diff --git a/web/vtadmin/src/components/routes/Schema.module.scss b/web/vtadmin/src/components/routes/Schema.module.scss index 0e1595e0fe4..c2744e73843 100644 --- a/web/vtadmin/src/components/routes/Schema.module.scss +++ b/web/vtadmin/src/components/routes/Schema.module.scss @@ -44,6 +44,11 @@ } } +.container { + display: grid; + grid-gap: 16px; +} + .panel { border: solid 1px var(--colorScaffoldingHighlight); border-radius: 6px; @@ -71,3 +76,20 @@ display: block; font-size: 5.6rem; } + +.skCol { + // Minimize column width + width: 1%; +} + +.skBadge { + border: solid 1px var(--colorPrimary50); + border-radius: 6px; + color: var(--colorPrimary); + display: inline-block; + font-size: var(--fontSizeSmall); + margin: 0 8px; + padding: 4px 8px; + text-transform: uppercase; + white-space: nowrap; +} diff --git a/web/vtadmin/src/components/routes/Schema.tsx b/web/vtadmin/src/components/routes/Schema.tsx index c943452964f..d0588372249 100644 --- a/web/vtadmin/src/components/routes/Schema.tsx +++ b/web/vtadmin/src/components/routes/Schema.tsx @@ -17,9 +17,10 @@ import * as React from 'react'; import { Link, useParams } from 'react-router-dom'; import style from './Schema.module.scss'; -import { useSchema } from '../../hooks/api'; +import { useSchema, useVSchema } from '../../hooks/api'; import { Code } from '../Code'; import { useDocumentTitle } from '../../hooks/useDocumentTitle'; +import { getVindexesForTable } from '../../util/vschemas'; interface RouteParams { clusterID: string; @@ -29,10 +30,11 @@ interface RouteParams { export const Schema = () => { const { clusterID, keyspace, table } = useParams(); - const { data, error, isError, isLoading, isSuccess } = useSchema({ clusterID, keyspace, table }); - useDocumentTitle(`${table} (${keyspace})`); + const { data, error, isError, isLoading, isSuccess } = useSchema({ clusterID, keyspace, table }); + const vschemaQuery = useVSchema({ clusterID, keyspace }); + const tableDefinition = React.useMemo( () => data && Array.isArray(data.table_definitions) @@ -41,6 +43,11 @@ export const Schema = () => { [data, table] ); + const tableVindexes = React.useMemo( + () => (vschemaQuery.data ? getVindexesForTable(vschemaQuery.data, table) : []), + [vschemaQuery.data, table] + ); + const is404 = isSuccess && !tableDefinition; return ( @@ -98,6 +105,64 @@ export const Schema = () => {

Table Definition

+ + {!!tableVindexes.length && ( +
+

Vindexes

+

+ A Vindex provides a way to map a column value to a keyspace ID. Since each shard in + Vitess covers a range of keyspace ID values, this mapping can be used to identify which + shard contains a row.{' '} + + Learn more about Vindexes in the Vitess documentation. + +

+ + + + + + + + + + + + {tableVindexes.map((v, vdx) => { + const columns = v.column ? [v.column] : v.columns; + return ( + + + + + + + + ); + })} + +
VindexColumnsTypeParams +
{v.name}{(columns || []).join(', ')}{v.meta?.type} + {v.meta?.params ? ( + Object.entries(v.meta.params).map(([k, val]) => ( +
+ {k}: {val} +
+ )) + ) : ( + + N/A + + )} +
+ {vdx === 0 && Sharding Key} +
+
+ )} )} diff --git a/web/vtadmin/src/hooks/api.ts b/web/vtadmin/src/hooks/api.ts index 1cc4f2d4fc6..4591993fab0 100644 --- a/web/vtadmin/src/hooks/api.ts +++ b/web/vtadmin/src/hooks/api.ts @@ -22,6 +22,8 @@ import { FetchSchemaParams, fetchSchemas, fetchTablets, + fetchVSchema, + FetchVSchemaParams, fetchWorkflow, fetchWorkflows, } from '../api/http'; @@ -105,6 +107,13 @@ export const useSchema = (params: FetchSchemaParams, options?: UseQueryOptions

| undefined) => { + return useQuery(['vschema', params], () => fetchVSchema(params)); +}; + /** * useWorkflow is a query hook that fetches a single workflow for the given parameters. */ diff --git a/web/vtadmin/src/util/vschemas.test.ts b/web/vtadmin/src/util/vschemas.test.ts new file mode 100644 index 00000000000..1a6e151ed88 --- /dev/null +++ b/web/vtadmin/src/util/vschemas.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 * as vs from './vschemas'; + +describe('getVindexesForTable', () => { + const tests: { + name: string; + input: Parameters; + expected: ReturnType; + }[] = [ + { + name: 'should return column vindexes', + input: [ + pb.VSchema.create({ + v_schema: { + tables: { + customer: { + column_vindexes: [{ column: 'customer_id', name: 'hash' }], + }, + }, + vindexes: { + hash: { type: 'hash' }, + }, + }, + }), + 'customer', + ], + expected: [ + { + column: 'customer_id', + name: 'hash', + meta: { type: 'hash' }, + }, + ], + }, + { + name: 'should return column vindexes + metadata', + input: [ + pb.VSchema.create({ + v_schema: { + tables: { + dogs: { + column_vindexes: [ + { column: 'id', name: 'hash' }, + { name: 'dogs_domain_vdx', columns: ['domain', 'is_good_dog'] }, + ], + }, + }, + vindexes: { + hash: { type: 'hash' }, + dogs_domain_vdx: { + type: 'lookup_hash', + owner: 'dogs', + params: { + from: 'domain,is_good_dog', + table: 'dogs_domain_idx', + to: 'id', + }, + }, + }, + }, + }), + 'dogs', + ], + expected: [ + { + column: 'id', + name: 'hash', + meta: { type: 'hash' }, + }, + { + columns: ['domain', 'is_good_dog'], + name: 'dogs_domain_vdx', + meta: { + owner: 'dogs', + params: { + from: 'domain,is_good_dog', + table: 'dogs_domain_idx', + to: 'id', + }, + type: 'lookup_hash', + }, + }, + ], + }, + { + name: 'should handle vschemas where the given table is not defined', + input: [ + pb.VSchema.create({ + v_schema: { + tables: { + customer: { + column_vindexes: [{ column: 'customer_id', name: 'hash' }], + }, + }, + vindexes: { + hash: { type: 'hash' }, + }, + }, + }), + 'does-not-exist', + ], + expected: [], + }, + ]; + + test.each(tests.map(Object.values))( + '%s', + ( + name: string, + input: Parameters, + expected: ReturnType + ) => { + const result = vs.getVindexesForTable(...input); + expect(result).toEqual(expected); + } + ); +}); diff --git a/web/vtadmin/src/util/vschemas.ts b/web/vtadmin/src/util/vschemas.ts new file mode 100644 index 00000000000..bbbd0c5cfe9 --- /dev/null +++ b/web/vtadmin/src/util/vschemas.ts @@ -0,0 +1,29 @@ +/** + * 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'; + +/** + * getVindexesForTable returns Vindexes + Vindex metadata for the given table. + */ +export const getVindexesForTable = (vschema: pb.VSchema, tableName: string) => { + const table = (vschema.v_schema?.tables || {})[tableName]; + if (!table) return []; + + return (table.column_vindexes || []).map((cv) => ({ + ...cv, + meta: cv.name ? (vschema.v_schema?.vindexes || {})[cv.name] : null, + })); +};