diff --git a/web/vtadmin/src/api/http.ts b/web/vtadmin/src/api/http.ts index 6f85a82e78d..3f75330d240 100644 --- a/web/vtadmin/src/api/http.ts +++ b/web/vtadmin/src/api/http.ts @@ -421,6 +421,20 @@ export const fetchVSchema = async ({ clusterID, keyspace }: FetchVSchemaParams) return pb.VSchema.create(result); }; +export interface FetchTransactionParams { + clusterID: string; + dtid: string; +} + +export const fetchTransaction = async ({ clusterID, dtid }: FetchTransactionParams) => { + const { result } = await vtfetch(`/api/transaction/${clusterID}/${dtid}/info`); + + const err = vtctldata.GetTransactionInfoResponse.verify(result); + if (err) throw Error(err); + + return vtctldata.GetTransactionInfoResponse.create(result); +}; + export interface FetchTransactionsParams { clusterID: string; keyspace: string; diff --git a/web/vtadmin/src/components/App.tsx b/web/vtadmin/src/components/App.tsx index 505ea7e64eb..ef27a35dc95 100644 --- a/web/vtadmin/src/components/App.tsx +++ b/web/vtadmin/src/components/App.tsx @@ -42,6 +42,7 @@ import { Topology } from './routes/topology/Topology'; import { ClusterTopology } from './routes/topology/ClusterTopology'; import { CreateMoveTables } from './routes/createWorkflow/CreateMoveTables'; import { Transactions } from './routes/Transactions'; +import { Transaction } from './routes/transaction/Transaction'; import { CreateReshard } from './routes/createWorkflow/CreateReshard'; import { CreateMaterialize } from './routes/createWorkflow/CreateMaterialize'; @@ -143,6 +144,10 @@ export const App = () => { + + + + diff --git a/web/vtadmin/src/components/links/TransactionLink.tsx b/web/vtadmin/src/components/links/TransactionLink.tsx new file mode 100644 index 00000000000..32d8b4a94bd --- /dev/null +++ b/web/vtadmin/src/components/links/TransactionLink.tsx @@ -0,0 +1,46 @@ +/** + * 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 React from 'react'; +import { Link } from 'react-router-dom'; + +interface Props { + className?: string; + clusterID: string | null | undefined; + dtid: string | null | undefined; +} + +export const TransactionLink: React.FunctionComponent = ({ + children, + className, + clusterID, + dtid, + ...props +}) => { + if (!clusterID || !dtid) { + return {children}; + } + + const to = { + pathname: `/transaction/${clusterID}/${dtid}`, + }; + + return ( + + {children} + + ); +}; diff --git a/web/vtadmin/src/components/routes/Transactions.tsx b/web/vtadmin/src/components/routes/Transactions.tsx index 10945403596..5f1745b1de1 100644 --- a/web/vtadmin/src/components/routes/Transactions.tsx +++ b/web/vtadmin/src/components/routes/Transactions.tsx @@ -32,9 +32,11 @@ import { orderBy } from 'lodash-es'; import { ReadOnlyGate } from '../ReadOnlyGate'; import TransactionActions from './transactions/TransactionActions'; import { isReadOnlyMode } from '../../util/env'; +import {TransactionLink} from "../links/TransactionLink"; +import * as React from "react"; -const COLUMNS = ['ID', 'State', 'Participants', 'Time Created', 'Actions']; -const READ_ONLY_COLUMNS = ['ID', 'State', 'Participants', 'Time Created']; +export const COLUMNS = ['ID', 'State', 'Participants', 'Time Created', 'Actions']; +export const READ_ONLY_COLUMNS = ['ID', 'State', 'Participants', 'Time Created']; const ABANDON_AGE_OPTIONS = [ { @@ -90,7 +92,9 @@ export const Transactions = () => { return ( -
{row.dtid}
+ +
{row.dtid}
+
{formatTransactionState(row)}
diff --git a/web/vtadmin/src/components/routes/transaction/Transaction.tsx b/web/vtadmin/src/components/routes/transaction/Transaction.tsx new file mode 100644 index 00000000000..a4f04bd629b --- /dev/null +++ b/web/vtadmin/src/components/routes/transaction/Transaction.tsx @@ -0,0 +1,185 @@ +/** + * 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 { useTransaction } from '../../../hooks/api'; +import { DataCell } from '../../dataTable/DataCell'; +import { DataTable } from '../../dataTable/DataTable'; +import { ContentContainer } from '../../layout/ContentContainer'; +import { WorkspaceHeader } from '../../layout/WorkspaceHeader'; +import { WorkspaceTitle } from '../../layout/WorkspaceTitle'; +import { QueryLoadingPlaceholder } from '../../placeholders/QueryLoadingPlaceholder'; +import { useDocumentTitle } from '../../../hooks/useDocumentTitle'; +import { query, vtctldata} from '../../../proto/vtadmin'; +import { formatTransactionState } from '../../../util/transactions'; +import { ShardLink } from '../../links/ShardLink'; +import { formatDateTime, formatRelativeTimeInSeconds } from '../../../util/time'; +import { ReadOnlyGate } from '../../ReadOnlyGate'; +import TransactionActions from '../transactions/TransactionActions'; +import { isReadOnlyMode } from '../../../util/env'; +import {useParams} from "react-router"; +import style from "../keyspace/Keyspace.module.scss"; +import {Link} from "react-router-dom"; +import {NavCrumbs} from "../../layout/NavCrumbs"; +import {READ_ONLY_COLUMNS} from "../Transactions" +import {COLUMNS} from "../Transactions" +import * as React from "react"; +import {TransactionLink} from "../../links/TransactionLink"; + +interface RouteParams { + clusterID: string; + dtid: string; +} + +export const SHARD_STATE_COLUMNS = ['Shard', 'State', 'Message', 'Time Created', 'Statements']; + +export const Transaction = () => { + const { clusterID, dtid } = useParams(); + + useDocumentTitle(`${dtid} (${clusterID})`); + + const transactionQuery = useTransaction({ clusterID, dtid }); + + const { data: transaction } = transactionQuery; + + if (transactionQuery.error) { + return ( +
+ 😰 +

An error occurred

+ {(transactionQuery.error as any).response?.error?.message || transactionQuery.error?.message} +

+ ← All Unresolved Transactions +

+
+ ); + } + + if (!transactionQuery.isLoading && !transaction) { + return ( +
+ 😖 +

Transaction not found

+

+ ← All Unresolved Transactions +

+
+ ); + } + + const renderMetadataRow = (rows: query.ITransactionMetadata[]) => { + return rows.map((row) => { + return ( + + + +
{row.dtid}
+
+
+ +
{formatTransactionState(row)}
+
+ + {row.participants?.map((participant) => { + const shard = `${participant.keyspace}/${participant.shard}`; + return ( + + {shard} + + ); + })} + + +
{formatDateTime(row.time_created)}
+
+ {formatRelativeTimeInSeconds(row.time_created)} +
+
+ + + + + + + ); + }); + }; + + const renderShardStateRow = (rows: vtctldata.IShardTransactionState[]) => { + return rows.map((row) => { + return ( + + +
{row.shard}
+
+ +
{row.state}
+
+ +
{row.message}
+
+ +
{formatDateTime(row.time_created)}
+
+ {formatRelativeTimeInSeconds(row.time_created)} +
+
+ + {row.statements} + + + ); + }); + }; + + return ( +
+ + + ← All Unresolved Transactions + + + {dtid} + +
+ + Cluster: {clusterID} + +
+
+ + + + + + +
+ ); +}; diff --git a/web/vtadmin/src/hooks/api.ts b/web/vtadmin/src/hooks/api.ts index 375fcd7a959..9261f4f0eb0 100644 --- a/web/vtadmin/src/hooks/api.ts +++ b/web/vtadmin/src/hooks/api.ts @@ -82,6 +82,8 @@ import { createMoveTables, startWorkflow, stopWorkflow, + FetchTransactionParams, + fetchTransaction, FetchTransactionsParams, fetchTransactions, completeMoveTables, @@ -427,6 +429,16 @@ export const useTransactions = ( return useQuery(['transactions', params], () => fetchTransactions(params), { ...options }); }; +/** + * useTransaction is a query hook that fetches the details of a transaction for the given dtid. + */ +export const useTransaction = ( + params: FetchTransactionParams, + options?: UseQueryOptions | undefined +) => { + return useQuery(['transaction', params], () => fetchTransaction(params), { ...options }); +}; + /** * useConcludeTransaction is a mutate hook that concludes a transaction. */