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