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] Display shard state on Tablets view + extract tablet utilities #7999

Merged
merged 2 commits into from
Apr 30, 2021
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
32 changes: 32 additions & 0 deletions web/vtadmin/src/components/pips/ShardServingPip.tsx
Original file line number Diff line number Diff line change
@@ -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.)
Comment on lines +21 to +24
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it will make your life easier (or more pleasant!) I'm pretty sure we can change the JSON marshaling in the http adapter to not omit fields with zero values

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh that's good to know! Definitely not a blocker but seems useful for boolean + numeric values... perhaps one day I will change it, but in the meantime it doesn't really matter much. I made a card anyway: https://github.com/vitessio/vitess/projects/12#card-60142070 😎

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(That said, this made me think of a small change I want to make, so I'll do that real quick.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

......... okay, so I did the change and it turns out I like it way less, so I think this is fine to merge as-is. Once this unit test passes anyway, haha.

isLoading?: boolean | null | undefined;
isServing?: boolean | null | undefined;
}

export const ShardServingPip = ({ isLoading, isServing }: Props) => {
let state: PipState = isServing ? 'success' : 'danger';
return <Pip state={isLoading ? null : state} />;
};
133 changes: 67 additions & 66 deletions web/vtadmin/src/components/routes/Tablets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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) => (
<tr key={tdx}>
<DataCell>
<div>{t.keyspace}</div>
<div className="font-size-small text-color-secondary">{t.cluster}</div>
</DataCell>
<DataCell>{t.shard}</DataCell>
<DataCell className="white-space-nowrap">
<TabletServingPip state={t._raw.state} /> {t.type}
</DataCell>
<DataCell>{t.state}</DataCell>
<DataCell>{t.alias}</DataCell>
<DataCell>{t.hostname}</DataCell>
</tr>
));
}, []);
const renderRows = React.useCallback(
(rows: typeof filteredData) => {
return rows.map((t, tdx) => (
<tr key={tdx}>
<DataCell>
<div>{t.keyspace}</div>
<div className="font-size-small text-color-secondary">{t.cluster}</div>
</DataCell>
<DataCell>
<ShardServingPip isLoading={ksQuery.isLoading} isServing={t.isShardServing} /> {t.shard}
{ksQuery.isSuccess && (
<div className="font-size-small text-color-secondary white-space-nowrap">
{!t.isShardServing && 'NOT SERVING'}
</div>
)}
</DataCell>
<DataCell className="white-space-nowrap">
<TabletServingPip state={t._raw.state} /> {t.type}
</DataCell>
<DataCell>{t.state}</DataCell>
<DataCell>{t.alias}</DataCell>
<DataCell>{t.hostname}</DataCell>
</tr>
));
},
[ksQuery.isLoading, ksQuery.isSuccess]
);

return (
<div className="max-width-content">
Expand All @@ -73,63 +86,51 @@ export const Tablets = () => {
</Button>
</div>
<DataTable
columns={['Keyspace', 'Shard', 'Type', 'State', 'Alias', 'Hostname']}
columns={['Keyspace', 'Shard', 'Type', 'Tablet State', 'Alias', 'Hostname']}
data={filteredData}
renderRows={renderRows}
/>
</div>
);
};

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']);
};
46 changes: 46 additions & 0 deletions web/vtadmin/src/util/tablets.ts
Original file line number Diff line number Diff line change
@@ -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 extends pb.ITablet>(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];