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