vitess.io/vitess@v0.16.2/web/vtadmin/src/components/routes/Workflows.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 { groupBy, orderBy } from 'lodash-es';
    17  import * as React from 'react';
    18  import { Link } from 'react-router-dom';
    19  
    20  import style from './Workflows.module.scss';
    21  import { useWorkflows } from '../../hooks/api';
    22  import { useDocumentTitle } from '../../hooks/useDocumentTitle';
    23  import { DataCell } from '../dataTable/DataCell';
    24  import { DataTable } from '../dataTable/DataTable';
    25  import { useSyncedURLParam } from '../../hooks/useSyncedURLParam';
    26  import { filterNouns } from '../../util/filterNouns';
    27  import { getStreams, getTimeUpdated } from '../../util/workflows';
    28  import { formatDateTime, formatRelativeTime } from '../../util/time';
    29  import { StreamStatePip } from '../pips/StreamStatePip';
    30  import { ContentContainer } from '../layout/ContentContainer';
    31  import { WorkspaceHeader } from '../layout/WorkspaceHeader';
    32  import { WorkspaceTitle } from '../layout/WorkspaceTitle';
    33  import { DataFilter } from '../dataTable/DataFilter';
    34  import { Tooltip } from '../tooltip/Tooltip';
    35  import { KeyspaceLink } from '../links/KeyspaceLink';
    36  import { QueryLoadingPlaceholder } from '../placeholders/QueryLoadingPlaceholder';
    37  import { UseQueryResult } from 'react-query';
    38  
    39  export const Workflows = () => {
    40      useDocumentTitle('Workflows');
    41      const workflowsQuery = useWorkflows();
    42  
    43      const { value: filter, updateValue: updateFilter } = useSyncedURLParam('filter');
    44  
    45      const sortedData = React.useMemo(() => {
    46          const mapped = (workflowsQuery.data || []).map((workflow) => ({
    47              clusterID: workflow.cluster?.id,
    48              clusterName: workflow.cluster?.name,
    49              keyspace: workflow.keyspace,
    50              name: workflow.workflow?.name,
    51              source: workflow.workflow?.source?.keyspace,
    52              sourceShards: workflow.workflow?.source?.shards,
    53              streams: groupBy(getStreams(workflow), 'state'),
    54              target: workflow.workflow?.target?.keyspace,
    55              targetShards: workflow.workflow?.target?.shards,
    56              timeUpdated: getTimeUpdated(workflow),
    57              workflowType: workflow.workflow?.workflow_type,
    58              workflowSubType: workflow.workflow?.workflow_sub_type,
    59          }));
    60          const filtered = filterNouns(filter, mapped);
    61          return orderBy(filtered, ['name', 'clusterName', 'source', 'target']);
    62      }, [workflowsQuery.data, filter]);
    63  
    64      const renderRows = (rows: typeof sortedData) =>
    65          rows.map((row, idx) => {
    66              const href =
    67                  row.clusterID && row.keyspace && row.name
    68                      ? `/workflow/${row.clusterID}/${row.keyspace}/${row.name}`
    69                      : null;
    70  
    71              return (
    72                  <tr key={idx}>
    73                      <DataCell>
    74                          <div className="font-bold">{href ? <Link to={href}>{row.name}</Link> : row.name}</div>
    75                          {row.workflowType && (
    76                              <div className="text-secondary text-success-200">
    77                                  {row.workflowType}
    78                                  {row.workflowSubType && row.workflowSubType !== 'None' && (
    79                                      <span className="text-sm">{' (' + row.workflowSubType + ')'}</span>
    80                                  )}
    81                              </div>
    82                          )}
    83                          <div className="text-sm text-secondary">{row.clusterName}</div>
    84                      </DataCell>
    85                      <DataCell>
    86                          {row.source ? (
    87                              <>
    88                                  <KeyspaceLink clusterID={row.clusterID} name={row.source}>
    89                                      {row.source}
    90                                  </KeyspaceLink>
    91                                  <div className={style.shardList}>{(row.sourceShards || []).join(', ')}</div>
    92                              </>
    93                          ) : (
    94                              <span className="text-secondary">N/A</span>
    95                          )}
    96                      </DataCell>
    97                      <DataCell>
    98                          {row.target ? (
    99                              <>
   100                                  <KeyspaceLink clusterID={row.clusterID} name={row.target}>
   101                                      {row.target}
   102                                  </KeyspaceLink>
   103                                  <div className={style.shardList}>{(row.targetShards || []).join(', ')}</div>
   104                              </>
   105                          ) : (
   106                              <span className="text-secondary">N/A</span>
   107                          )}
   108                      </DataCell>
   109  
   110                      <DataCell>
   111                          <div className={style.streams}>
   112                              {/* TODO(doeg): add a protobuf enum for this (https://github.com/vitessio/vitess/projects/12#card-60190340) */}
   113                              {['Error', 'Copying', 'Running', 'Stopped'].map((streamState) => {
   114                                  if (streamState in row.streams) {
   115                                      const streamCount = row.streams[streamState].length;
   116                                      const tooltip = [
   117                                          streamCount,
   118                                          streamState === 'Error' ? 'failed' : streamState.toLocaleLowerCase(),
   119                                          streamCount === 1 ? 'stream' : 'streams',
   120                                      ].join(' ');
   121  
   122                                      return (
   123                                          <Tooltip key={streamState} text={tooltip}>
   124                                              <span className={style.stream}>
   125                                                  <StreamStatePip state={streamState} /> {streamCount}
   126                                              </span>
   127                                          </Tooltip>
   128                                      );
   129                                  }
   130                                  return (
   131                                      <span key={streamState} className={style.streamPlaceholder}>
   132                                          -
   133                                      </span>
   134                                  );
   135                              })}
   136                          </div>
   137                      </DataCell>
   138  
   139                      <DataCell>
   140                          <div className="font-sans whitespace-nowrap">{formatDateTime(row.timeUpdated)}</div>
   141                          <div className="font-sans text-sm text-secondary">{formatRelativeTime(row.timeUpdated)}</div>
   142                      </DataCell>
   143                  </tr>
   144              );
   145          });
   146  
   147      return (
   148          <div>
   149              <WorkspaceHeader>
   150                  <WorkspaceTitle>Workflows</WorkspaceTitle>
   151              </WorkspaceHeader>
   152              <ContentContainer>
   153                  <DataFilter
   154                      autoFocus
   155                      onChange={(e) => updateFilter(e.target.value)}
   156                      onClear={() => updateFilter('')}
   157                      placeholder="Filter workflows"
   158                      value={filter || ''}
   159                  />
   160  
   161                  <DataTable
   162                      columns={['Workflow', 'Source', 'Target', 'Streams', 'Last Updated']}
   163                      data={sortedData}
   164                      renderRows={renderRows}
   165                  />
   166  
   167                  <QueryLoadingPlaceholder query={workflowsQuery as UseQueryResult} />
   168              </ContentContainer>
   169          </div>
   170      );
   171  };