vitess.io/vitess@v0.16.2/web/vtadmin/src/components/routes/keyspace/KeyspaceShards.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 React, { useMemo } from 'react'; 17 import { isEmpty, orderBy } from 'lodash'; 18 19 import style from './KeyspaceShards.module.scss'; 20 import { topodata, vtadmin as pb } from '../../../proto/vtadmin'; 21 import { useTablets } from '../../../hooks/api'; 22 import { formatAlias, formatType } from '../../../util/tablets'; 23 import { DataTable } from '../../dataTable/DataTable'; 24 import { DataCell } from '../../dataTable/DataCell'; 25 import { ShardServingPip } from '../../pips/ShardServingPip'; 26 import { TabletServingPip } from '../../pips/TabletServingPip'; 27 import { DataFilter } from '../../dataTable/DataFilter'; 28 import { useSyncedURLParam } from '../../../hooks/useSyncedURLParam'; 29 import { filterNouns } from '../../../util/filterNouns'; 30 import { TabletLink } from '../../links/TabletLink'; 31 import { ShardLink } from '../../links/ShardLink'; 32 import { getShardSortRange } from '../../../util/keyspaces'; 33 import { Pip } from '../../pips/Pip'; 34 import { Tooltip } from '../../tooltip/Tooltip'; 35 import { QueryLoadingPlaceholder } from '../../placeholders/QueryLoadingPlaceholder'; 36 37 interface Props { 38 keyspace: pb.Keyspace | null | undefined; 39 } 40 41 const TABLE_COLUMNS = ['Shard', 'Primary Serving?', 'Tablets', 'Primary Tablet']; 42 43 export const KeyspaceShards = ({ keyspace }: Props) => { 44 const tq = useTablets(); 45 const { data: tablets = [] } = tq; 46 47 const { value: filter, updateValue: updateFilter } = useSyncedURLParam('shardFilter'); 48 49 const data = useMemo(() => { 50 if (!keyspace || tq.isLoading) { 51 return []; 52 } 53 54 const keyspaceTablets = tablets.filter( 55 (t) => t.cluster?.id === keyspace.cluster?.id && t.tablet?.keyspace === keyspace.keyspace?.name 56 ); 57 58 const mapped = Object.values(keyspace?.shards).map((shard) => { 59 const sortRange = getShardSortRange(shard.name || ''); 60 61 const shardTablets = keyspaceTablets.filter((t) => t.tablet?.shard === shard.name); 62 63 const primaryTablet = shardTablets.find((t) => t.tablet?.type === topodata.TabletType.PRIMARY); 64 65 return { 66 keyspace: shard.keyspace, 67 isPrimaryServing: shard.shard?.is_primary_serving, 68 name: shard.name, 69 primaryAlias: formatAlias(primaryTablet?.tablet?.alias), 70 primaryHostname: primaryTablet?.tablet?.hostname, 71 // "_" prefix excludes the property name from k/v filtering 72 _primaryTablet: primaryTablet, 73 _sortStart: sortRange.start, 74 _sortEnd: sortRange.end, 75 _tabletsByType: countTablets(shardTablets), 76 }; 77 }); 78 79 const filtered = filterNouns(filter, mapped); 80 81 return orderBy(filtered, ['_sortStart', '_sortEnd']); 82 }, [filter, keyspace, tablets, tq.isLoading]); 83 84 const renderRows = React.useCallback( 85 (rows: typeof data) => { 86 return rows.map((row) => { 87 return ( 88 <tr key={row.name}> 89 <DataCell> 90 <ShardLink 91 className="font-bold" 92 clusterID={keyspace?.cluster?.id} 93 keyspace={keyspace?.keyspace?.name} 94 shard={row.name} 95 > 96 {row.keyspace}/{row.name} 97 </ShardLink> 98 </DataCell> 99 <DataCell> 100 <ShardServingPip isServing={row.isPrimaryServing} />{' '} 101 {row.isPrimaryServing ? 'SERVING' : 'NOT SERVING'} 102 </DataCell> 103 <DataCell> 104 {!isEmpty(row._tabletsByType) ? ( 105 <div className={style.counts}> 106 {Object.keys(row._tabletsByType) 107 .sort() 108 .map((tabletType) => { 109 const tt = row._tabletsByType[tabletType]; 110 const allSuccess = tt.serving === tt.total; 111 const tooltip = allSuccess 112 ? `${tt.serving}/${tt.total} ${tabletType} serving` 113 : `${tt.total - tt.serving}/${tt.total} ${tabletType} not serving`; 114 115 return ( 116 <span key={tabletType}> 117 <Tooltip text={tooltip}> 118 <span> 119 <Pip state={allSuccess ? 'success' : 'danger'} /> 120 </span> 121 </Tooltip>{' '} 122 {tt.total} {tabletType} 123 </span> 124 ); 125 })} 126 </div> 127 ) : ( 128 <span className="text-secondary">No tablets</span> 129 )} 130 </DataCell> 131 <DataCell> 132 {row._primaryTablet ? ( 133 <div> 134 <TabletLink alias={row.primaryAlias} clusterID={keyspace?.cluster?.id}> 135 <TabletServingPip state={row._primaryTablet.state} /> {row.primaryAlias} 136 </TabletLink> 137 <div className="text-sm text-secondary">{row.primaryHostname}</div> 138 </div> 139 ) : ( 140 <span className="text-secondary">No primary tablet</span> 141 )} 142 </DataCell> 143 </tr> 144 ); 145 }); 146 }, 147 [keyspace?.cluster?.id, keyspace?.keyspace?.name] 148 ); 149 150 if (!keyspace) { 151 return null; 152 } 153 154 return ( 155 <div className={style.container}> 156 <QueryLoadingPlaceholder query={tq} /> 157 <DataFilter 158 autoFocus 159 onChange={(e) => updateFilter(e.target.value)} 160 onClear={() => updateFilter('')} 161 placeholder="Filter shards" 162 value={filter || ''} 163 /> 164 165 <DataTable columns={TABLE_COLUMNS} data={data} renderRows={renderRows} /> 166 </div> 167 ); 168 }; 169 170 interface TabletCounts { 171 // tabletType is the stringified/display version of the 172 // topodata.TabletType enum. 173 [tabletType: string]: { 174 // The number of serving tablets for this type. 175 serving: number; 176 // The total number of tablets for this type. 177 total: number; 178 }; 179 } 180 181 const countTablets = (tablets: pb.Tablet[]): TabletCounts => { 182 return tablets.reduce((acc, t) => { 183 // If t.tablet.type is truly an undefined/null/otherwise invalid 184 // value (i.e,. not in the proto, which should in theory never happen), 185 // then call that "UNDEFINED" so as not to co-opt the existing "UNKNOWN" 186 // tablet state. 187 const ft = formatType(t) || 'UNDEFINED'; 188 if (!(ft in acc)) acc[ft] = { serving: 0, total: 0 }; 189 190 acc[ft].total++; 191 192 if (t.state === pb.Tablet.ServingState.SERVING) { 193 acc[ft].serving++; 194 } 195 196 return acc; 197 }, {} as TabletCounts); 198 };