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];