diff --git a/web/vtadmin/src/components/pips/ShardServingPip.tsx b/web/vtadmin/src/components/pips/ShardServingPip.tsx new file mode 100644 index 00000000000..75906152263 --- /dev/null +++ b/web/vtadmin/src/components/pips/ShardServingPip.tsx @@ -0,0 +1,32 @@ +/** + * 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 { Pip, PipState } from './Pip'; + +interface Props { + // If shard status is still pending, a neutral pip is used. + // The distinction between "is loading" and "is_master_serving is undefined" + // is useful, as it's common for the shard `shard.is_master_serving` to be + // excluded from API responses for non-serving shards (instead of being + // explicitly false.) + isLoading?: boolean | null | undefined; + isServing?: boolean | null | undefined; +} + +export const ShardServingPip = ({ isLoading, isServing }: Props) => { + let state: PipState = isServing ? 'success' : 'danger'; + return ; +}; diff --git a/web/vtadmin/src/components/routes/Tablets.tsx b/web/vtadmin/src/components/routes/Tablets.tsx index ee33f066896..55337727445 100644 --- a/web/vtadmin/src/components/routes/Tablets.tsx +++ b/web/vtadmin/src/components/routes/Tablets.tsx @@ -15,9 +15,9 @@ */ import * as React from 'react'; -import { useTablets } from '../../hooks/api'; -import { vtadmin as pb, topodata } from '../../proto/vtadmin'; -import { invertBy, orderBy } from 'lodash-es'; +import { useKeyspaces, useTablets } from '../../hooks/api'; +import { vtadmin as pb } from '../../proto/vtadmin'; +import { orderBy } from 'lodash-es'; import { useDocumentTitle } from '../../hooks/useDocumentTitle'; import { DataTable } from '../dataTable/DataTable'; import { TextInput } from '../TextInput'; @@ -28,34 +28,47 @@ import { Button } from '../Button'; import { DataCell } from '../dataTable/DataCell'; import { TabletServingPip } from '../pips/TabletServingPip'; import { useSyncedURLParam } from '../../hooks/useSyncedURLParam'; +import { formatAlias, formatDisplayType, formatState, formatType } from '../../util/tablets'; +import { ShardServingPip } from '../pips/ShardServingPip'; export const Tablets = () => { useDocumentTitle('Tablets'); const { value: filter, updateValue: updateFilter } = useSyncedURLParam('filter'); const { data = [] } = useTablets(); + const { data: keyspaces = [], ...ksQuery } = useKeyspaces(); const filteredData = React.useMemo(() => { - return formatRows(data, filter); - }, [data, filter]); + return formatRows(data, keyspaces, filter); + }, [data, filter, keyspaces]); - const renderRows = React.useCallback((rows: typeof filteredData) => { - return rows.map((t, tdx) => ( - - -
{t.keyspace}
-
{t.cluster}
-
- {t.shard} - - {t.type} - - {t.state} - {t.alias} - {t.hostname} - - )); - }, []); + const renderRows = React.useCallback( + (rows: typeof filteredData) => { + return rows.map((t, tdx) => ( + + +
{t.keyspace}
+
{t.cluster}
+
+ + {t.shard} + {ksQuery.isSuccess && ( +
+ {!t.isShardServing && 'NOT SERVING'} +
+ )} +
+ + {t.type} + + {t.state} + {t.alias} + {t.hostname} + + )); + }, + [ksQuery.isLoading, ksQuery.isSuccess] + ); return (
@@ -73,7 +86,7 @@ export const Tablets = () => {
@@ -81,55 +94,43 @@ export const Tablets = () => { ); }; -const SERVING_STATES = Object.keys(pb.Tablet.ServingState); - -// TABLET_TYPES maps numeric tablet types back to human readable strings. -// Note that topodata.TabletType allows duplicate values: specifically, -// both RDONLY (new name) and BATCH (old name) share the same numeric value. -// So, we make the assumption that if there are duplicate keys, we will -// always take the first value. -const TABLET_TYPES = Object.entries(invertBy(topodata.TabletType)).reduce((acc, [k, vs]) => { - acc[k] = vs[0]; - return acc; -}, {} as { [k: string]: string }); - -const formatAlias = (t: pb.Tablet) => - t.tablet?.alias?.cell && t.tablet?.alias?.uid && `${t.tablet.alias.cell}-${t.tablet.alias.uid}`; - -const formatType = (t: pb.Tablet) => { - return t.tablet?.type && TABLET_TYPES[t.tablet?.type]; -}; - -const formatDisplayType = (t: pb.Tablet) => { - const tt = formatType(t); - return tt === 'MASTER' ? 'PRIMARY' : tt; -}; - -const formatState = (t: pb.Tablet) => t.state && SERVING_STATES[t.state]; - -export const formatRows = (tablets: pb.Tablet[] | null | undefined, filter: string | null | undefined) => { +export const formatRows = ( + tablets: pb.Tablet[] | null | undefined, + keyspaces: pb.Keyspace[] | null | undefined, + filter: string | null | undefined +) => { if (!tablets) return []; // Properties prefixed with "_" are hidden and included for filtering only. // They also won't work as keys in key:value searches, e.g., you cannot // search for `_keyspaceShard:customers/20-40`, by design, mostly because it's // unexpected and a little weird to key on properties that you can't see. - const mapped = tablets.map((t) => ({ - cluster: t.cluster?.name, - keyspace: t.tablet?.keyspace, - shard: t.tablet?.shard, - alias: formatAlias(t), - hostname: t.tablet?.hostname, - type: formatDisplayType(t), - state: formatState(t), - _raw: t, - _keyspaceShard: `${t.tablet?.keyspace}/${t.tablet?.shard}`, - // Include the unformatted type so (string) filtering by "master" works - // even if "primary" is what we display, and what we use for key:value searches. - _rawType: formatType(t), - // Always sort primary tablets first, then sort alphabetically by type, etc. - _typeSortOrder: formatDisplayType(t) === 'PRIMARY' ? 1 : 2, - })); + const mapped = tablets.map((t) => { + const keyspace = (keyspaces || []).find( + (k) => k.cluster?.id === t.cluster?.id && k.keyspace?.name === t.tablet?.keyspace + ); + + const shardName = t.tablet?.shard; + const shard = shardName ? keyspace?.shards[shardName] : null; + + return { + alias: formatAlias(t), + cluster: t.cluster?.name, + hostname: t.tablet?.hostname, + isShardServing: shard?.shard?.is_master_serving, + keyspace: t.tablet?.keyspace, + shard: shardName, + state: formatState(t), + type: formatDisplayType(t), + _raw: t, + _keyspaceShard: `${t.tablet?.keyspace}/${t.tablet?.shard}`, + // Include the unformatted type so (string) filtering by "master" works + // even if "primary" is what we display, and what we use for key:value searches. + _rawType: formatType(t), + // Always sort primary tablets first, then sort alphabetically by type, etc. + _typeSortOrder: formatDisplayType(t) === 'PRIMARY' ? 1 : 2, + }; + }); const filtered = filterNouns(filter, mapped); return orderBy(filtered, ['cluster', 'keyspace', 'shard', '_typeSortOrder', 'type', 'alias']); }; diff --git a/web/vtadmin/src/util/tablets.ts b/web/vtadmin/src/util/tablets.ts new file mode 100644 index 00000000000..16228d1ff5a --- /dev/null +++ b/web/vtadmin/src/util/tablets.ts @@ -0,0 +1,46 @@ +/** + * 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 { invertBy } from 'lodash-es'; +import { topodata, vtadmin as pb } from '../proto/vtadmin'; + +/** + * TABLET_TYPES maps numeric tablet types back to human readable strings. + * Note that topodata.TabletType allows duplicate values: specifically, + * both RDONLY (new name) and BATCH (old name) share the same numeric value. + * So, we make the assumption that if there are duplicate keys, we will + * always take the first value. + */ +export const TABLET_TYPES = Object.entries(invertBy(topodata.TabletType)).reduce((acc, [k, vs]) => { + acc[k] = vs[0]; + return acc; +}, {} as { [k: string]: string }); + +/** + * formatAlias formats a tablet.alias object as a single string, The Vitess Way™. + */ +export const formatAlias = (t: T) => + t.tablet?.alias?.cell && t.tablet?.alias?.uid ? `${t.tablet.alias.cell}-${t.tablet.alias.uid}` : null; + +export const formatType = (t: pb.Tablet) => t.tablet?.type && TABLET_TYPES[t.tablet?.type]; + +export const formatDisplayType = (t: pb.Tablet) => { + const tt = formatType(t); + return tt === 'MASTER' ? 'PRIMARY' : tt; +}; + +export const SERVING_STATES = Object.keys(pb.Tablet.ServingState); + +export const formatState = (t: pb.Tablet) => t.state && SERVING_STATES[t.state];