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