vitess.io/vitess@v0.16.2/web/vtadmin/src/components/routes/Tablets.tsx (about) 1 /** 2 * Copyright 2021 The Vitess Authors. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 import * as React from 'react'; 17 18 import { useKeyspaces, useTablets } from '../../hooks/api'; 19 import { vtadmin as pb } from '../../proto/vtadmin'; 20 import { orderBy } from 'lodash-es'; 21 import { useDocumentTitle } from '../../hooks/useDocumentTitle'; 22 import { DataTable } from '../dataTable/DataTable'; 23 import { filterNouns } from '../../util/filterNouns'; 24 import { DataCell } from '../dataTable/DataCell'; 25 import { TabletServingPip } from '../pips/TabletServingPip'; 26 import { useSyncedURLParam } from '../../hooks/useSyncedURLParam'; 27 import { formatAlias, formatDisplayType, formatState, formatType } from '../../util/tablets'; 28 import { ShardServingPip } from '../pips/ShardServingPip'; 29 import { ContentContainer } from '../layout/ContentContainer'; 30 import { WorkspaceHeader } from '../layout/WorkspaceHeader'; 31 import { WorkspaceTitle } from '../layout/WorkspaceTitle'; 32 import { DataFilter } from '../dataTable/DataFilter'; 33 import { KeyspaceLink } from '../links/KeyspaceLink'; 34 import { TabletLink } from '../links/TabletLink'; 35 import { ExternalTabletLink } from '../links/ExternalTabletLink'; 36 import { ShardLink } from '../links/ShardLink'; 37 import InfoDropdown from './tablets/InfoDropdown'; 38 import { isReadOnlyMode } from '../../util/env'; 39 import { ReadOnlyGate } from '../ReadOnlyGate'; 40 import { QueryLoadingPlaceholder } from '../placeholders/QueryLoadingPlaceholder'; 41 42 const COLUMNS = ['Keyspace', 'Shard', 'Alias', 'Type', 'Tablet State', 'Hostname']; 43 if (!isReadOnlyMode()) { 44 COLUMNS.push('Actions'); 45 } 46 47 export const Tablets = () => { 48 useDocumentTitle('Tablets'); 49 50 const { value: filter, updateValue: updateFilter } = useSyncedURLParam('filter'); 51 52 const tabletsQuery = useTablets(); 53 const keyspacesQuery = useKeyspaces(); 54 const queries = [tabletsQuery, keyspacesQuery]; 55 56 const { data: keyspaces = [], ...ksQuery } = keyspacesQuery; 57 58 const filteredData = React.useMemo(() => { 59 return formatRows(tabletsQuery.data, keyspaces, filter); 60 }, [tabletsQuery.data, filter, keyspaces]); 61 62 const renderRows = React.useCallback( 63 (rows: typeof filteredData) => { 64 return rows.map((t, tdx) => ( 65 <tr key={tdx}> 66 <DataCell> 67 <KeyspaceLink clusterID={t._raw.cluster?.id} name={t.keyspace}> 68 <div>{t.keyspace}</div> 69 <div className="text-sm text-secondary">{t.cluster}</div> 70 </KeyspaceLink> 71 </DataCell> 72 <DataCell> 73 <ShardLink 74 className="whitespace-nowrap" 75 clusterID={t._raw.cluster?.id} 76 keyspace={t.keyspace} 77 shard={t.shard} 78 > 79 <ShardServingPip isLoading={ksQuery.isLoading} isServing={t.isShardServing} /> {t.shard} 80 {ksQuery.isSuccess && ( 81 <div className="text-sm text-secondary whitespace-nowrap"> 82 {!t.isShardServing && 'NOT SERVING'} 83 </div> 84 )} 85 </ShardLink> 86 </DataCell> 87 <DataCell> 88 <TabletLink alias={t.alias} className="font-bold" clusterID={t._raw.cluster?.id}> 89 {t.alias} 90 </TabletLink> 91 </DataCell> 92 <DataCell className="whitespace-nowrap">{t.type}</DataCell> 93 94 <DataCell> 95 <TabletServingPip state={t._raw.state} /> {t.state} 96 </DataCell> 97 98 <DataCell> 99 <ExternalTabletLink fqdn={`//${t._raw.FQDN}`}>{t.hostname}</ExternalTabletLink> 100 </DataCell> 101 102 <ReadOnlyGate> 103 <DataCell> 104 <InfoDropdown alias={t.alias as string} clusterID={t._raw.cluster?.id as string} /> 105 </DataCell> 106 </ReadOnlyGate> 107 </tr> 108 )); 109 }, 110 [ksQuery.isLoading, ksQuery.isSuccess] 111 ); 112 113 return ( 114 <div> 115 <WorkspaceHeader> 116 <WorkspaceTitle>Tablets</WorkspaceTitle> 117 </WorkspaceHeader> 118 <ContentContainer> 119 <DataFilter 120 autoFocus 121 onChange={(e) => updateFilter(e.target.value)} 122 onClear={() => updateFilter('')} 123 placeholder="Filter tablets" 124 value={filter || ''} 125 /> 126 <DataTable columns={COLUMNS} data={filteredData} renderRows={renderRows} /> 127 <QueryLoadingPlaceholder queries={queries} /> 128 </ContentContainer> 129 </div> 130 ); 131 }; 132 133 export const formatRows = ( 134 tablets: pb.Tablet[] | null | undefined, 135 keyspaces: pb.Keyspace[] | null | undefined, 136 filter: string | null | undefined 137 ) => { 138 if (!tablets) return []; 139 140 // Properties prefixed with "_" are hidden and included for filtering only. 141 // They also won't work as keys in key:value searches, e.g., you cannot 142 // search for `_keyspaceShard:customers/20-40`, by design, mostly because it's 143 // unexpected and a little weird to key on properties that you can't see. 144 const mapped = tablets.map((t) => { 145 const keyspace = (keyspaces || []).find( 146 (k) => k.cluster?.id === t.cluster?.id && k.keyspace?.name === t.tablet?.keyspace 147 ); 148 149 const shardName = t.tablet?.shard; 150 const shard = shardName ? keyspace?.shards[shardName] : null; 151 152 return { 153 alias: formatAlias(t.tablet?.alias), 154 cluster: t.cluster?.name, 155 hostname: t.tablet?.hostname, 156 isShardServing: shard?.shard?.is_primary_serving, 157 keyspace: t.tablet?.keyspace, 158 shard: shardName, 159 state: formatState(t), 160 type: formatDisplayType(t), 161 _raw: t, 162 _keyspaceShard: `${t.tablet?.keyspace}/${t.tablet?.shard}`, 163 // Include the unformatted type so (string) filtering by "primary" works 164 // even if "primary" is what we display, and what we use for key:value searches. 165 _rawType: formatType(t), 166 // Always sort primary tablets first, then sort alphabetically by type, etc. 167 _typeSortOrder: formatDisplayType(t) === 'PRIMARY' ? 1 : 2, 168 }; 169 }); 170 const filtered = filterNouns(filter, mapped); 171 return orderBy(filtered, ['cluster', 'keyspace', 'shard', '_typeSortOrder', 'type', 'alias']); 172 };