Skip to content

Commit

Permalink
feat: add vtadmin page for transaction information
Browse files Browse the repository at this point in the history
Signed-off-by: Manan Gupta <[email protected]>
  • Loading branch information
GuptaManan100 committed Nov 6, 2024
1 parent 3c9fb2c commit d575918
Show file tree
Hide file tree
Showing 6 changed files with 269 additions and 3 deletions.
14 changes: 14 additions & 0 deletions web/vtadmin/src/api/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions web/vtadmin/src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -143,6 +144,10 @@ export const App = () => {
<Transactions />
</Route>

<Route path="/transaction/:clusterID/:dtid">
<Transaction />
</Route>

<Route path="/topology/:clusterID">
<ClusterTopology />
</Route>
Expand Down
46 changes: 46 additions & 0 deletions web/vtadmin/src/components/links/TransactionLink.tsx
Original file line number Diff line number Diff line change
@@ -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<Props> = ({
children,
className,
clusterID,
dtid,
...props
}) => {
if (!clusterID || !dtid) {
return <span className={className}>{children}</span>;
}

const to = {
pathname: `/transaction/${clusterID}/${dtid}`,
};

return (
<Link className={className} to={to}>
{children}
</Link>
);
};
10 changes: 7 additions & 3 deletions web/vtadmin/src/components/routes/Transactions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
{
Expand Down Expand Up @@ -90,7 +92,9 @@ export const Transactions = () => {
return (
<tr key={row.dtid}>
<DataCell>
<div>{row.dtid}</div>
<TransactionLink clusterID={params.clusterID} dtid={row.dtid}>
<div className="font-bold">{row.dtid}</div>
</TransactionLink>
</DataCell>
<DataCell>
<div>{formatTransactionState(row)}</div>
Expand Down
185 changes: 185 additions & 0 deletions web/vtadmin/src/components/routes/transaction/Transaction.tsx
Original file line number Diff line number Diff line change
@@ -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<RouteParams>();

useDocumentTitle(`${dtid} (${clusterID})`);

const transactionQuery = useTransaction({ clusterID, dtid });

const { data: transaction } = transactionQuery;

if (transactionQuery.error) {
return (
<div className={style.placeholder}>
<span className={style.errorEmoji}>😰</span>
<h1>An error occurred</h1>
<code>{(transactionQuery.error as any).response?.error?.message || transactionQuery.error?.message}</code>
<p>
<Link to="/transactions">← All Unresolved Transactions</Link>
</p>
</div>
);
}

if (!transactionQuery.isLoading && !transaction) {
return (
<div className={style.placeholder}>
<span className={style.errorEmoji}>😖</span>
<h1>Transaction not found</h1>
<p>
<Link to="/transactions">← All Unresolved Transactions</Link>
</p>
</div>
);
}

const renderMetadataRow = (rows: query.ITransactionMetadata[]) => {
return rows.map((row) => {
return (
<tr key={row.dtid}>
<DataCell>
<TransactionLink clusterID={clusterID} dtid={row.dtid}>
<div className="font-bold">{row.dtid}</div>
</TransactionLink>
</DataCell>
<DataCell>
<div>{formatTransactionState(row)}</div>
</DataCell>
<DataCell>
{row.participants?.map((participant) => {
const shard = `${participant.keyspace}/${participant.shard}`;
return (
<ShardLink
clusterID={clusterID}
keyspace={participant.keyspace}
shard={participant.shard}
>
{shard}
</ShardLink>
);
})}
</DataCell>
<DataCell>
<div className="font-sans whitespace-nowrap">{formatDateTime(row.time_created)}</div>
<div className="font-sans text-sm text-secondary">
{formatRelativeTimeInSeconds(row.time_created)}
</div>
</DataCell>
<ReadOnlyGate>
<DataCell>
<TransactionActions
refetchTransactions={transactionQuery.refetch}
clusterID={clusterID as string}
dtid={row.dtid as string}
/>
</DataCell>
</ReadOnlyGate>
</tr>
);
});
};

const renderShardStateRow = (rows: vtctldata.IShardTransactionState[]) => {
return rows.map((row) => {
return (
<tr key={row.shard}>
<DataCell>
<div className="font-bold">{row.shard}</div>
</DataCell>
<DataCell>
<div>{row.state}</div>
</DataCell>
<DataCell>
<div>{row.message}</div>
</DataCell>
<DataCell>
<div className="font-sans whitespace-nowrap">{formatDateTime(row.time_created)}</div>
<div className="font-sans text-sm text-secondary">
{formatRelativeTimeInSeconds(row.time_created)}
</div>
</DataCell>
<DataCell>
{row.statements}
</DataCell>
</tr>
);
});
};

return (
<div>
<WorkspaceHeader className="mb-0">
<NavCrumbs>
<Link to="/transactions">← All Unresolved Transactions</Link>
</NavCrumbs>

<WorkspaceTitle className="font-mono">{dtid}</WorkspaceTitle>

<div className={style.headingMeta}>
<span>
Cluster: <code>{clusterID}</code>
</span>
</div>
</WorkspaceHeader>

<ContentContainer>
<DataTable
columns={isReadOnlyMode() ? READ_ONLY_COLUMNS : COLUMNS}
data={transaction && transaction.metadata? [transaction.metadata]:[]}
renderRows={renderMetadataRow}
/>
<DataTable
columns={SHARD_STATE_COLUMNS}
data={transaction && transaction.shard_states? transaction.shard_states:[]}
renderRows={renderShardStateRow}
/>
<QueryLoadingPlaceholder query={transactionQuery} />
</ContentContainer>
</div>
);
};
12 changes: 12 additions & 0 deletions web/vtadmin/src/hooks/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ import {
createMoveTables,
startWorkflow,
stopWorkflow,
FetchTransactionParams,
fetchTransaction,
FetchTransactionsParams,
fetchTransactions,
completeMoveTables,
Expand Down Expand Up @@ -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<vtctldata.GetTransactionInfoResponse, Error> | undefined
) => {
return useQuery(['transaction', params], () => fetchTransaction(params), { ...options });
};

/**
* useConcludeTransaction is a mutate hook that concludes a transaction.
*/
Expand Down

0 comments on commit d575918

Please sign in to comment.