Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[vtadmin-web] Three small bugfixes in Tablets table around stable sort order, display type lookup, and filtering by type #7568

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions web/vtadmin/src/components/routes/Tablets.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* 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, topodata } from '../../proto/vtadmin';
import { formatRows } from './Tablets';

describe('Tablets', () => {
describe('filterRows', () => {
const tests: {
name: string;
filter: string | null;
tablets: pb.Tablet[] | null;
// Undefined `expected` keys will be ignored.
// Unfortunately, Partial<ReturnType<typeof formatRows>> does not
// work as expected + requires all property keys to be defined.
expected: { [k: string]: unknown }[];
}[] = [
{
name: 'empty tablets',
filter: null,
tablets: null,
expected: [],
},
{
name: 'sort by primary first, then other tablet types alphabetically',
filter: null,
tablets: [
pb.Tablet.create({
tablet: {
alias: {
cell: 'cell1',
uid: 4,
},
type: topodata.TabletType.BACKUP,
},
}),
pb.Tablet.create({
tablet: {
alias: {
cell: 'cell1',
uid: 2,
},
type: topodata.TabletType.REPLICA,
},
}),
pb.Tablet.create({
tablet: {
alias: {
cell: 'cell1',
uid: 3,
},
type: topodata.TabletType.MASTER,
},
}),
pb.Tablet.create({
tablet: {
alias: {
cell: 'cell1',
uid: 1,
},
type: topodata.TabletType.REPLICA,
},
}),
],
expected: [
{ alias: 'cell1-3', type: 'PRIMARY' },
{ alias: 'cell1-4', type: 'BACKUP' },
{ alias: 'cell1-1', type: 'REPLICA' },
{ alias: 'cell1-2', type: 'REPLICA' },
],
},
];

test.each(tests.map(Object.values))(
'%s',
(name: string, filter: string, tablets: pb.Tablet[], expected: { [k: string]: unknown }[]) => {
const result = formatRows(tablets, filter);
expect(result).toMatchObject(expected);
}
);
});
});
63 changes: 40 additions & 23 deletions web/vtadmin/src/components/routes/Tablets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import * as React from 'react';

import { useTablets } from '../../hooks/api';
import { vtadmin as pb, topodata } from '../../proto/vtadmin';
import { orderBy } from 'lodash-es';
import { invertBy, orderBy } from 'lodash-es';
import { useDocumentTitle } from '../../hooks/useDocumentTitle';
import { DataTable } from '../dataTable/DataTable';
import { TextInput } from '../TextInput';
Expand All @@ -33,34 +33,16 @@ export const Tablets = () => {
const { data = [] } = useTablets();

const filteredData = React.useMemo(() => {
// 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 = data.map((t) => ({
cluster: t.cluster?.name,
keyspace: t.tablet?.keyspace,
shard: t.tablet?.shard,
alias: formatAlias(t),
hostname: t.tablet?.hostname,
displayType: formatDisplayType(t),
state: formatState(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.
_type: formatType(t),
}));
const filtered = filterNouns(filter, mapped);
return orderBy(filtered, ['cluster', 'keyspace', 'shard', 'displayType']);
}, [filter, data]);
return formatRows(data, filter);
}, [data, filter]);

const renderRows = React.useCallback((rows: typeof filteredData) => {
return rows.map((t, tdx) => (
<tr key={tdx}>
<td>{t.cluster}</td>
<td>{t.keyspace}</td>
<td>{t.shard}</td>
<td>{t.displayType}</td>
<td>{t.type}</td>
<td>{t.state}</td>
<td>{t.alias}</td>
<td>{t.hostname}</td>
Expand Down Expand Up @@ -93,7 +75,16 @@ export const Tablets = () => {
};

const SERVING_STATES = Object.keys(pb.Tablet.ServingState);
const TABLET_TYPES = Object.keys(topodata.TabletType);

// 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}`;
Expand All @@ -108,3 +99,29 @@ const formatDisplayType = (t: pb.Tablet) => {
};

const formatState = (t: pb.Tablet) => t.state && SERVING_STATES[t.state];

export const formatRows = (tablets: pb.Tablet[] | null, filter: string) => {
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),
_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']);
};
2 changes: 1 addition & 1 deletion web/vtadmin/src/util/filterNouns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { KeyValueSearchToken, SearchTokenTypes, tokenizeSearch } from './tokeniz
/**
* `filterNouns` filters a list of nouns by a search string.
*/
export const filterNouns = <T extends { [k: string]: any }>(needle: string, haystack: T[]): T[] => {
export const filterNouns = <T extends { [k: string]: any }>(needle: string | null, haystack: T[]): T[] => {
if (!needle) return haystack;

const tokens = tokenizeSearch(needle);
Expand Down