diff --git a/web/vtadmin/src/components/routes/workflow/Workflow.tsx b/web/vtadmin/src/components/routes/workflow/Workflow.tsx index a81901786d8..4cbd827ee30 100644 --- a/web/vtadmin/src/components/routes/workflow/Workflow.tsx +++ b/web/vtadmin/src/components/routes/workflow/Workflow.tsx @@ -24,6 +24,7 @@ import { WorkspaceTitle } from '../../layout/WorkspaceTitle'; import { useDocumentTitle } from '../../../hooks/useDocumentTitle'; import { KeyspaceLink } from '../../links/KeyspaceLink'; import { WorkflowStreams } from './WorkflowStreams'; +import { WorkflowDetails } from './WorkflowDetails'; import { ContentContainer } from '../../layout/ContentContainer'; import { TabContainer } from '../../tabs/TabContainer'; import { Tab } from '../../tabs/Tab'; @@ -74,6 +75,7 @@ export const Workflow = () => { + @@ -82,6 +84,10 @@ export const Workflow = () => { + + + + diff --git a/web/vtadmin/src/components/routes/workflow/WorkflowDetails.tsx b/web/vtadmin/src/components/routes/workflow/WorkflowDetails.tsx new file mode 100644 index 00000000000..81fe30243ee --- /dev/null +++ b/web/vtadmin/src/components/routes/workflow/WorkflowDetails.tsx @@ -0,0 +1,155 @@ +/** + * Copyright 2024 The Vitess Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { groupBy, orderBy } from "lodash-es"; +import React, { useMemo } from "react"; +import { Link } from "react-router-dom"; + +import { useWorkflow, useWorkflows } from "../../../hooks/api"; +import { formatAlias } from "../../../util/tablets"; +import { formatDateTime, formatRelativeTime } from "../../../util/time"; +import { + formatStreamKey, + getReverseWorkflow, + getStreams, + getStreamSource, + getStreamTarget, +} from "../../../util/workflows"; +import { DataTable } from "../../dataTable/DataTable"; +import { vtctldata } from "../../../proto/vtadmin"; +import { DataCell } from "../../dataTable/DataCell"; +import { StreamStatePip } from "../../pips/StreamStatePip"; +import { ThrottleThresholdSeconds } from "../Workflows"; +import { workerData } from "worker_threads"; + +interface Props { + clusterID: string; + keyspace: string; + name: string; +} + +export const WorkflowDetails = ({ clusterID, keyspace, name }: Props) => { + const { data } = useWorkflow({ clusterID, keyspace, name }); + + const { data: workflowsData = [] } = useWorkflows(); + + const streams = useMemo(() => { + const rows = getStreams(data).map((stream) => ({ + key: formatStreamKey(stream), + ...stream, + })); + + return orderBy(rows, "streamKey"); + }, [data]); + + const renderRows = (rows: vtctldata.Workflow.Stream.ILog[]) => { + return rows.map((row) => { + let message = row.message ? `${row.message}` : "-"; + // TODO(@beingnoble03): Investigate how message can be parsed and displayed to JSON in case of "Stream Created" + if (row.type == "Stream Created") { + message = "-"; + } + return ( + + {`${row.type}`} + {`${row.state}`} + {`${formatDateTime( + parseInt(`${row.updated_at?.seconds}`, 10) + )}`} + {message} + {`${row.count}`} + + ); + }); + }; + + const reverseWorkflow = getReverseWorkflow(workflowsData, data); + + return ( +
+ {reverseWorkflow && ( +
+

Reverse Workflow

+
+ + {reverseWorkflow.workflow?.name} + +
+

+ Keyspace
+ + {`${reverseWorkflow.keyspace}`} + +

+ {reverseWorkflow.workflow?.max_v_replication_lag && ( +

+ Max VReplication Lag
+ {`${reverseWorkflow.workflow?.max_v_replication_lag}`} +

+ )} +
+ )} +

Streams

+ {streams.map((stream) => { + const href = + stream.tablet && stream.id + ? `/workflow/${clusterID}/${keyspace}/${name}/stream/${stream.tablet.cell}/${stream.tablet.uid}/${stream.id}` + : null; + + var isThrottled = + Number(stream.throttler_status?.time_throttled?.seconds) > + Date.now() / 1000 - ThrottleThresholdSeconds; + const streamState = isThrottled ? "Throttled" : stream.state; + return ( +
+
+ {" "} + {`${stream.key}`} +
+

+ State
+ {streamState} +

+ {isThrottled && ( +

+ Component Throttled
+ {stream.throttler_status?.component_throttled} +

+ )} + {streamState == "Running" && + data?.workflow?.max_v_replication_lag && ( +

+ VReplication Lag
+ {`${data?.workflow?.max_v_replication_lag}`} +

+ )} + +
+ ); + })} +
+ ); +}; diff --git a/web/vtadmin/src/util/workflows.ts b/web/vtadmin/src/util/workflows.ts index 66e8d765961..7c9a487e864 100644 --- a/web/vtadmin/src/util/workflows.ts +++ b/web/vtadmin/src/util/workflows.ts @@ -94,3 +94,23 @@ export const getStreamTablets = (workflow: W | null | un return [...aliases]; }; + +/** + * getReverseWorkflow returns the reverse workflow of `originalWorkflow` by looking for the '_reverse' + * suffix and the source and target keyspace from all `workflows` list. + */ +export const getReverseWorkflow = ( + workflows: W[], + originalWorkflow: W | undefined | null +): W | undefined => { + if (!originalWorkflow) return; + const originalWorkflowName = originalWorkflow.workflow?.name!; + let reverseWorkflowName = originalWorkflowName.concat("_reverse"); + if (originalWorkflowName.endsWith("_reverse")) { + reverseWorkflowName = originalWorkflowName.split("_reverse")[0]; + } + return workflows.find((workflow) => + workflow.workflow?.name === reverseWorkflowName && + workflow.workflow?.source?.keyspace === originalWorkflow.workflow?.target?.keyspace && + workflow.workflow?.target?.keyspace === originalWorkflow.workflow?.source?.keyspace); +};