diff --git a/web/vtadmin/src/components/routes/workflow/Workflow.tsx b/web/vtadmin/src/components/routes/workflow/Workflow.tsx index 4cbd827ee30..2d9c639cee0 100644 --- a/web/vtadmin/src/components/routes/workflow/Workflow.tsx +++ b/web/vtadmin/src/components/routes/workflow/Workflow.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Link, Redirect, Route, Switch, useParams, useRouteMatch } from 'react-router-dom'; +import { Link, Redirect, Route, Switch, useLocation, useParams, useRouteMatch } from 'react-router-dom'; import style from './Workflow.module.scss'; @@ -30,6 +30,7 @@ import { TabContainer } from '../../tabs/TabContainer'; import { Tab } from '../../tabs/Tab'; import { getStreams } from '../../../util/workflows'; import { Code } from '../../Code'; +import { ShardLink } from '../../links/ShardLink'; interface RouteParams { clusterID: string; @@ -40,12 +41,21 @@ interface RouteParams { export const Workflow = () => { const { clusterID, keyspace, name } = useParams(); const { path, url } = useRouteMatch(); + const location = useLocation(); useDocumentTitle(`${name} (${keyspace})`); const { data } = useWorkflow({ clusterID, keyspace, name }); const streams = getStreams(data); + const detailsURL = `${url}/details`; + const detailsTab = location.pathname === detailsURL; + + let isReshard = false; + if (data && data.workflow) { + isReshard = data.workflow.workflow_type === 'Reshard'; + } + return (
@@ -54,28 +64,66 @@ export const Workflow = () => { {name} + {isReshard && ( +
+
+ + {data?.workflow?.source?.shards?.length! > 1 ? 'Source Shards: ' : 'Source Shard: '} + {data?.workflow?.source?.shards?.map((shard) => ( + + + {`${keyspace}/${shard}`} + + + ))} + + + {data?.workflow?.target?.shards?.length! > 1 ? 'Target Shards: ' : 'Target Shard: '} + {data?.workflow?.target?.shards?.map((shard) => ( + + + {`${keyspace}/${shard}`} + + + ))} + +
+
+ )}
Cluster: {clusterID} - Target keyspace:{' '} + Target Keyspace:{' '} {keyspace}
-
- Scroll To Streams -
+ {detailsTab && ( +
+ Scroll To Streams +
+ )}
- + diff --git a/web/vtadmin/src/components/routes/workflow/WorkflowDetails.tsx b/web/vtadmin/src/components/routes/workflow/WorkflowDetails.tsx index 93023274b27..1ab82e5261d 100644 --- a/web/vtadmin/src/components/routes/workflow/WorkflowDetails.tsx +++ b/web/vtadmin/src/components/routes/workflow/WorkflowDetails.tsx @@ -19,11 +19,13 @@ import React, { useMemo } from 'react'; import { Link } from 'react-router-dom'; import { useWorkflow, useWorkflowStatus, useWorkflows } from '../../../hooks/api'; -import { formatDateTime } from '../../../util/time'; +import { formatDateTimeShort } from '../../../util/time'; import { TableCopyState, formatStreamKey, getReverseWorkflow, + getStreamSource, + getStreamTarget, getStreams, getTableCopyStates, } from '../../../util/workflows'; @@ -32,6 +34,10 @@ import { vtctldata } from '../../../proto/vtadmin'; import { DataCell } from '../../dataTable/DataCell'; import { StreamStatePip } from '../../pips/StreamStatePip'; import { ThrottleThresholdSeconds } from '../Workflows'; +import { ShardLink } from '../../links/ShardLink'; +import { Tooltip } from '../../tooltip/Tooltip'; +import { TabletLink } from '../../links/TabletLink'; +import { formatAlias } from '../../../util/tablets'; interface Props { clusterID: string; @@ -45,7 +51,7 @@ const LOG_COLUMNS = ['Type', 'State', 'Updated At', 'Message', 'Count']; const TABLE_COPY_STATE_COLUMNS = ['Table Name', 'Total Bytes', 'Bytes Copied', 'Total Rows', 'Rows Copied']; -const STREAM_COLUMNS = ['Stream', 'State', 'Message', 'Transaction Timestamp', 'Database Name']; +const STREAM_COLUMNS = ['Stream', 'Source Shard', 'Target Shard', 'Message', 'Transaction Timestamp', 'Database Name']; export const WorkflowDetails = ({ clusterID, keyspace, name }: Props) => { const { data: workflowData } = useWorkflow({ clusterID, keyspace, name }); @@ -112,15 +118,20 @@ export const WorkflowDetails = ({ clusterID, keyspace, name }: Props) => { const renderSummaryRows = (rows: (typeof workflowSummary)[]) => { return rows.map((row) => { const reverseWorkflow = row.reverseWorkflow; + let maxVReplicationLag = '-'; + if (row.workflowData && row.workflowData.workflow?.max_v_replication_lag) { + maxVReplicationLag = `${row.workflowData.workflow?.max_v_replication_lag}`; + if (maxVReplicationLag === '1') { + maxVReplicationLag += ' second'; + } else { + maxVReplicationLag += ' seconds'; + } + } return ( {row.streamSummary ? row.streamSummary : '-'} {row.workflowStatus ? row.workflowStatus.traffic_state : '-'} - - {row.workflowData && row.workflowData.workflow?.max_v_replication_lag - ? `${row.workflowData.workflow?.max_v_replication_lag}` - : '-'} - + {maxVReplicationLag} {reverseWorkflow ? ( { const renderStreamRows = (rows: typeof streams) => { return rows.map((row) => { + const source = getStreamSource(row); + const target = getStreamTarget(row, keyspace); const href = row.tablet && row.id ? `/workflow/${clusterID}/${keyspace}/${name}/stream/${row.tablet.cell}/${row.tablet.uid}/${row.id}` @@ -151,12 +164,22 @@ export const WorkflowDetails = ({ clusterID, keyspace, name }: Props) => { return ( - {' '} + + + {' '} + + {row.key}
- Updated {formatDateTime(row.time_updated?.seconds)} + Tablet{' '} + + {formatAlias(row.tablet)} + +
+
+ Updated {formatDateTimeShort(row.time_updated?.seconds)}
{isThrottled ? (
@@ -165,11 +188,32 @@ export const WorkflowDetails = ({ clusterID, keyspace, name }: Props) => {
) : null}
- {rowState} + + {source ? ( + + {source} + + ) : ( + '-' + )} + + + {target ? ( + + {target} + + ) : ( + '-' + )} + {row.message ? row.message : '-'} {row.transaction_timestamp && row.transaction_timestamp.seconds - ? formatDateTime(row.transaction_timestamp.seconds) + ? `${formatDateTimeShort(row.transaction_timestamp.seconds)}` : '-'} {row.db_name} @@ -189,7 +233,7 @@ export const WorkflowDetails = ({ clusterID, keyspace, name }: Props) => { {`${row.type}`} {`${row.state}`} - {`${formatDateTime(parseInt(`${row.updated_at?.seconds}`, 10))}`} + {`${formatDateTimeShort(parseInt(`${row.updated_at?.seconds}`, 10))}`} {message} {`${row.count}`} @@ -219,7 +263,7 @@ export const WorkflowDetails = ({ clusterID, keyspace, name }: Props) => { }; return ( -
+
{ pageSize={1} title="Summary" /> + { /> )}

Recent Logs

- {streams.map((stream) => ( -
- -
- ))} + {streams.length <= 8 ? ( + streams.map((stream) => ( +
+ +
+ )) + ) : ( + Recent logs from streams are not displayed due to the large number of shards. + )}
); }; diff --git a/web/vtadmin/src/components/routes/workflow/WorkflowStreams.tsx b/web/vtadmin/src/components/routes/workflow/WorkflowStreams.tsx index ef521406e3d..27696b68b93 100644 --- a/web/vtadmin/src/components/routes/workflow/WorkflowStreams.tsx +++ b/web/vtadmin/src/components/routes/workflow/WorkflowStreams.tsx @@ -14,22 +14,10 @@ * limitations under the License. */ -import { groupBy, orderBy } from 'lodash-es'; -import React, { useMemo } from 'react'; -import { Link } from 'react-router-dom'; +import React from 'react'; -import { useWorkflow } from '../../../hooks/api'; -import { formatAlias } from '../../../util/tablets'; -import { formatDateTime } from '../../../util/time'; -import { formatStreamKey, getStreams, getStreamSource, getStreamTarget } from '../../../util/workflows'; -import { DataCell } from '../../dataTable/DataCell'; -import { DataTable } from '../../dataTable/DataTable'; -import { TabletLink } from '../../links/TabletLink'; -import { StreamStatePip } from '../../pips/StreamStatePip'; import { WorkflowStreamsLagChart } from '../../charts/WorkflowStreamsLagChart'; -import { ShardLink } from '../../links/ShardLink'; import { env } from '../../../util/env'; -import { ThrottleThresholdSeconds } from '../Workflows'; interface Props { clusterID: string; @@ -37,83 +25,7 @@ interface Props { name: string; } -const COLUMNS = ['Stream', 'Source', 'Target', 'Tablet']; - export const WorkflowStreams = ({ clusterID, keyspace, name }: Props) => { - const { data } = useWorkflow({ clusterID, keyspace, name }); - - const streams = useMemo(() => { - const rows = getStreams(data).map((stream) => ({ - key: formatStreamKey(stream), - ...stream, - })); - - return orderBy(rows, 'streamKey'); - }, [data]); - - const streamsByState = groupBy(streams, 'state'); - - const renderRows = (rows: typeof streams) => { - return rows.map((row) => { - const href = - row.tablet && row.id - ? `/workflow/${clusterID}/${keyspace}/${name}/stream/${row.tablet.cell}/${row.tablet.uid}/${row.id}` - : null; - - const source = getStreamSource(row); - const target = getStreamTarget(row, keyspace); - var isThrottled = - Number(row?.throttler_status?.time_throttled?.seconds) > Date.now() / 1000 - ThrottleThresholdSeconds; - const rowState = isThrottled ? 'Throttled' : row.state; - return ( - - - {' '} - - {row.key} - -
- Updated {formatDateTime(row.time_updated?.seconds)} -
- {isThrottled ? ( -
- Throttled: - in {row.throttler_status?.component_throttled} -
- ) : null} -
- - {source ? ( - - {source} - - ) : ( - N/A - )} - - - {target ? ( - - {target} - - ) : ( - N/A - )} - - - - {formatAlias(row.tablet)} - - - - ); - }); - }; - return (
{env().VITE_ENABLE_EXPERIMENTAL_TABLET_DEBUG_VARS && ( @@ -122,29 +34,6 @@ export const WorkflowStreams = ({ clusterID, keyspace, name }: Props) => { )} - -

- Streams -

- {/* TODO(doeg): add a protobuf enum for this (https://github.com/vitessio/vitess/projects/12#card-60190340) */} - {['Error', 'Copying', 'Running', 'Stopped'].map((streamState) => { - if (!Array.isArray(streamsByState[streamState])) { - return null; - } - - return ( -
- -
- ); - })}
); }; diff --git a/web/vtadmin/src/util/time.ts b/web/vtadmin/src/util/time.ts index 5e7c723c3d8..c9f0017d011 100644 --- a/web/vtadmin/src/util/time.ts +++ b/web/vtadmin/src/util/time.ts @@ -40,3 +40,7 @@ export const formatRelativeTime = (timestamp: number | Long | null | undefined): const u = parse(timestamp); return u ? u.fromNow() : null; }; + +export const formatDateTimeShort = (timestamp: number | Long | null | undefined): string | null => { + return format(timestamp, 'MM/DD/YY HH:mm:ss Z'); +};