explorer: Add tabs for block program and account stats (#15702)
This commit is contained in:
@ -46,8 +46,10 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path={"/block/:id"}
|
path={["/block/:id", "/block/:id/:tab"]}
|
||||||
render={({ match }) => <BlockDetailsPage slot={match.params.id} />}
|
render={({ match }) => (
|
||||||
|
<BlockDetailsPage slot={match.params.id} tab={match.params.tab} />
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
|
@ -8,6 +8,8 @@ import {
|
|||||||
ProgramDataAccountInfo,
|
ProgramDataAccountInfo,
|
||||||
} from "validators/accounts/upgradeable-program";
|
} from "validators/accounts/upgradeable-program";
|
||||||
import { Slot } from "components/common/Slot";
|
import { Slot } from "components/common/Slot";
|
||||||
|
import { addressLabel } from "utils/tx";
|
||||||
|
import { useCluster } from "providers/cluster";
|
||||||
|
|
||||||
export function UpgradeableProgramSection({
|
export function UpgradeableProgramSection({
|
||||||
account,
|
account,
|
||||||
@ -19,6 +21,8 @@ export function UpgradeableProgramSection({
|
|||||||
programData: ProgramDataAccountInfo;
|
programData: ProgramDataAccountInfo;
|
||||||
}) {
|
}) {
|
||||||
const refresh = useFetchAccountInfo();
|
const refresh = useFetchAccountInfo();
|
||||||
|
const { cluster } = useCluster();
|
||||||
|
const label = addressLabel(account.pubkey.toBase58(), cluster);
|
||||||
return (
|
return (
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
@ -41,6 +45,12 @@ export function UpgradeableProgramSection({
|
|||||||
<Address pubkey={account.pubkey} alignRight raw />
|
<Address pubkey={account.pubkey} alignRight raw />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{label && (
|
||||||
|
<tr>
|
||||||
|
<td>Address Label</td>
|
||||||
|
<td className="text-lg-right">{label}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
<tr>
|
<tr>
|
||||||
<td>Balance (SOL)</td>
|
<td>Balance (SOL)</td>
|
||||||
<td className="text-lg-right text-uppercase">
|
<td className="text-lg-right text-uppercase">
|
||||||
|
110
explorer/src/components/block/BlockAccountsCard.tsx
Normal file
110
explorer/src/components/block/BlockAccountsCard.tsx
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { ConfirmedBlock, PublicKey } from "@solana/web3.js";
|
||||||
|
import { Address } from "components/common/Address";
|
||||||
|
|
||||||
|
type AccountStats = {
|
||||||
|
reads: number;
|
||||||
|
writes: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PAGE_SIZE = 25;
|
||||||
|
|
||||||
|
export function BlockAccountsCard({ block }: { block: ConfirmedBlock }) {
|
||||||
|
const [numDisplayed, setNumDisplayed] = React.useState(10);
|
||||||
|
const totalTransactions = block.transactions.length;
|
||||||
|
|
||||||
|
const accountStats = React.useMemo(() => {
|
||||||
|
const statsMap = new Map<string, AccountStats>();
|
||||||
|
block.transactions.forEach((tx) => {
|
||||||
|
const txSet = new Map<string, boolean>();
|
||||||
|
tx.transaction.instructions.forEach((ix) => {
|
||||||
|
ix.keys.forEach((key) => {
|
||||||
|
const address = key.pubkey.toBase58();
|
||||||
|
txSet.set(address, key.isWritable);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
txSet.forEach((isWritable, address) => {
|
||||||
|
const stats = statsMap.get(address) || { reads: 0, writes: 0 };
|
||||||
|
if (isWritable) {
|
||||||
|
stats.writes++;
|
||||||
|
} else {
|
||||||
|
stats.reads++;
|
||||||
|
}
|
||||||
|
statsMap.set(address, stats);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const accountEntries = [];
|
||||||
|
for (let entry of statsMap) {
|
||||||
|
accountEntries.push(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
accountEntries.sort((a, b) => {
|
||||||
|
const aCount = a[1].reads + a[1].writes;
|
||||||
|
const bCount = b[1].reads + b[1].writes;
|
||||||
|
if (aCount < bCount) return 1;
|
||||||
|
if (aCount > bCount) return -1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return accountEntries;
|
||||||
|
}, [block]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header align-items-center">
|
||||||
|
<h3 className="card-header-title">Block Account Usage</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="table-responsive mb-0">
|
||||||
|
<table className="table table-sm table-nowrap card-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="text-muted">Account</th>
|
||||||
|
<th className="text-muted">Read-Write Count</th>
|
||||||
|
<th className="text-muted">Read-Only Count</th>
|
||||||
|
<th className="text-muted">Total Count</th>
|
||||||
|
<th className="text-muted">% of Transactions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{accountStats
|
||||||
|
.slice(0, numDisplayed)
|
||||||
|
.map(([address, { writes, reads }]) => {
|
||||||
|
return (
|
||||||
|
<tr key={address}>
|
||||||
|
<td>
|
||||||
|
<Address pubkey={new PublicKey(address)} link />
|
||||||
|
</td>
|
||||||
|
<td>{writes}</td>
|
||||||
|
<td>{reads}</td>
|
||||||
|
<td>{writes + reads}</td>
|
||||||
|
<td>
|
||||||
|
{((100 * (writes + reads)) / totalTransactions).toFixed(
|
||||||
|
2
|
||||||
|
)}
|
||||||
|
%
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{accountStats.length > numDisplayed && (
|
||||||
|
<div className="card-footer">
|
||||||
|
<button
|
||||||
|
className="btn btn-primary w-100"
|
||||||
|
onClick={() =>
|
||||||
|
setNumDisplayed((displayed) => displayed + PAGE_SIZE)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Load More
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -4,7 +4,11 @@ import { ErrorCard } from "components/common/ErrorCard";
|
|||||||
import { Signature } from "components/common/Signature";
|
import { Signature } from "components/common/Signature";
|
||||||
import bs58 from "bs58";
|
import bs58 from "bs58";
|
||||||
|
|
||||||
|
const PAGE_SIZE = 25;
|
||||||
|
|
||||||
export function BlockHistoryCard({ block }: { block: ConfirmedBlock }) {
|
export function BlockHistoryCard({ block }: { block: ConfirmedBlock }) {
|
||||||
|
const [numDisplayed, setNumDisplayed] = React.useState(PAGE_SIZE);
|
||||||
|
|
||||||
if (block.transactions.length === 0) {
|
if (block.transactions.length === 0) {
|
||||||
return <ErrorCard text="This block has no transactions" />;
|
return <ErrorCard text="This block has no transactions" />;
|
||||||
}
|
}
|
||||||
@ -24,7 +28,7 @@ export function BlockHistoryCard({ block }: { block: ConfirmedBlock }) {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="list">
|
<tbody className="list">
|
||||||
{block.transactions.map((tx, i) => {
|
{block.transactions.slice(0, numDisplayed).map((tx, i) => {
|
||||||
let statusText;
|
let statusText;
|
||||||
let statusClass;
|
let statusClass;
|
||||||
let signature: React.ReactNode;
|
let signature: React.ReactNode;
|
||||||
@ -60,6 +64,19 @@ export function BlockHistoryCard({ block }: { block: ConfirmedBlock }) {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{block.transactions.length > numDisplayed && (
|
||||||
|
<div className="card-footer">
|
||||||
|
<button
|
||||||
|
className="btn btn-primary w-100"
|
||||||
|
onClick={() =>
|
||||||
|
setNumDisplayed((displayed) => displayed + PAGE_SIZE)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Load More
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -7,8 +7,19 @@ import { Slot } from "components/common/Slot";
|
|||||||
import { ClusterStatus, useCluster } from "providers/cluster";
|
import { ClusterStatus, useCluster } from "providers/cluster";
|
||||||
import { BlockHistoryCard } from "./BlockHistoryCard";
|
import { BlockHistoryCard } from "./BlockHistoryCard";
|
||||||
import { BlockRewardsCard } from "./BlockRewardsCard";
|
import { BlockRewardsCard } from "./BlockRewardsCard";
|
||||||
|
import { ConfirmedBlock } from "@solana/web3.js";
|
||||||
|
import { NavLink } from "react-router-dom";
|
||||||
|
import { clusterPath } from "utils/url";
|
||||||
|
import { BlockProgramsCard } from "./BlockProgramsCard";
|
||||||
|
import { BlockAccountsCard } from "./BlockAccountsCard";
|
||||||
|
|
||||||
export function BlockOverviewCard({ slot }: { slot: number }) {
|
export function BlockOverviewCard({
|
||||||
|
slot,
|
||||||
|
tab,
|
||||||
|
}: {
|
||||||
|
slot: number;
|
||||||
|
tab?: string;
|
||||||
|
}) {
|
||||||
const confirmedBlock = useBlock(slot);
|
const confirmedBlock = useBlock(slot);
|
||||||
const fetchBlock = useFetchBlock();
|
const fetchBlock = useFetchBlock();
|
||||||
const { status } = useCluster();
|
const { status } = useCluster();
|
||||||
@ -46,12 +57,6 @@ export function BlockOverviewCard({ slot }: { slot: number }) {
|
|||||||
<Slot slot={slot} />
|
<Slot slot={slot} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td className="w-100">Parent Slot</td>
|
|
||||||
<td className="text-lg-right text-monospace">
|
|
||||||
<Slot slot={block.parentSlot} link />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td className="w-100">Blockhash</td>
|
<td className="w-100">Blockhash</td>
|
||||||
<td className="text-lg-right text-monospace">
|
<td className="text-lg-right text-monospace">
|
||||||
@ -59,16 +64,96 @@ export function BlockOverviewCard({ slot }: { slot: number }) {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td className="w-100">Previous Blockhash</td>
|
<td className="w-100">Parent Slot</td>
|
||||||
|
<td className="text-lg-right text-monospace">
|
||||||
|
<Slot slot={block.parentSlot} link />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="w-100">Parent Blockhash</td>
|
||||||
<td className="text-lg-right text-monospace">
|
<td className="text-lg-right text-monospace">
|
||||||
<span>{block.previousBlockhash}</span>
|
<span>{block.previousBlockhash}</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="w-100">Total Transactions</td>
|
||||||
|
<td className="text-lg-right text-monospace">
|
||||||
|
<span>{block.transactions.length}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</TableCardBody>
|
</TableCardBody>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BlockRewardsCard block={block} />
|
<MoreSection block={block} slot={slot} tab={tab} />
|
||||||
<BlockHistoryCard block={block} />
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const TABS: Tab[] = [
|
||||||
|
{
|
||||||
|
slug: "history",
|
||||||
|
title: "Transactions",
|
||||||
|
path: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "rewards",
|
||||||
|
title: "Rewards",
|
||||||
|
path: "/rewards",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "programs",
|
||||||
|
title: "Programs",
|
||||||
|
path: "/programs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "accounts",
|
||||||
|
title: "Accounts",
|
||||||
|
path: "/accounts",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
type MoreTabs = "history" | "rewards" | "programs" | "accounts";
|
||||||
|
|
||||||
|
type Tab = {
|
||||||
|
slug: MoreTabs;
|
||||||
|
title: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function MoreSection({
|
||||||
|
slot,
|
||||||
|
block,
|
||||||
|
tab,
|
||||||
|
}: {
|
||||||
|
slot: number;
|
||||||
|
block: ConfirmedBlock;
|
||||||
|
tab?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="container">
|
||||||
|
<div className="header">
|
||||||
|
<div className="header-body pt-0">
|
||||||
|
<ul className="nav nav-tabs nav-overflow header-tabs">
|
||||||
|
{TABS.map(({ title, slug, path }) => (
|
||||||
|
<li key={slug} className="nav-item">
|
||||||
|
<NavLink
|
||||||
|
className="nav-link"
|
||||||
|
to={clusterPath(`/block/${slot}${path}`)}
|
||||||
|
exact
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{tab === undefined && <BlockHistoryCard block={block} />}
|
||||||
|
{tab === "rewards" && <BlockRewardsCard block={block} />}
|
||||||
|
{tab === "accounts" && <BlockAccountsCard block={block} />}
|
||||||
|
{tab === "programs" && <BlockProgramsCard block={block} />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
109
explorer/src/components/block/BlockProgramsCard.tsx
Normal file
109
explorer/src/components/block/BlockProgramsCard.tsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { ConfirmedBlock, PublicKey } from "@solana/web3.js";
|
||||||
|
import { Address } from "components/common/Address";
|
||||||
|
import { TableCardBody } from "components/common/TableCardBody";
|
||||||
|
|
||||||
|
export function BlockProgramsCard({ block }: { block: ConfirmedBlock }) {
|
||||||
|
const totalTransactions = block.transactions.length;
|
||||||
|
const txFrequency = new Map<string, number>();
|
||||||
|
const ixFrequency = new Map<string, number>();
|
||||||
|
|
||||||
|
let totalInstructions = 0;
|
||||||
|
block.transactions.forEach((tx) => {
|
||||||
|
totalInstructions += tx.transaction.instructions.length;
|
||||||
|
const programUsed = new Set<string>();
|
||||||
|
const trackProgramId = (programId: PublicKey) => {
|
||||||
|
const programAddress = programId.toBase58();
|
||||||
|
programUsed.add(programAddress);
|
||||||
|
const frequency = ixFrequency.get(programAddress);
|
||||||
|
ixFrequency.set(programAddress, frequency ? frequency + 1 : 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
tx.transaction.instructions.forEach((ix, index) => {
|
||||||
|
trackProgramId(ix.programId);
|
||||||
|
tx.meta?.innerInstructions?.forEach((inner) => {
|
||||||
|
if (inner.index !== index) return;
|
||||||
|
totalInstructions += inner.instructions.length;
|
||||||
|
inner.instructions.forEach((innerIx) => {
|
||||||
|
if (innerIx.programIdIndex >= ix.keys.length) return;
|
||||||
|
trackProgramId(ix.keys[innerIx.programIdIndex].pubkey);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
programUsed.forEach((programId) => {
|
||||||
|
const frequency = txFrequency.get(programId);
|
||||||
|
txFrequency.set(programId, frequency ? frequency + 1 : 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const programEntries = [];
|
||||||
|
for (let entry of txFrequency) {
|
||||||
|
programEntries.push(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
programEntries.sort((a, b) => {
|
||||||
|
if (a[1] < b[1]) return 1;
|
||||||
|
if (a[1] > b[1]) return -1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header align-items-center">
|
||||||
|
<h3 className="card-header-title">Block Program Stats</h3>
|
||||||
|
</div>
|
||||||
|
<TableCardBody>
|
||||||
|
<tr>
|
||||||
|
<td className="w-100">Unique Programs Count</td>
|
||||||
|
<td className="text-lg-right text-monospace">
|
||||||
|
{programEntries.length}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td className="w-100">Total Instructions</td>
|
||||||
|
<td className="text-lg-right text-monospace">
|
||||||
|
{totalInstructions}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</TableCardBody>
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header align-items-center">
|
||||||
|
<h3 className="card-header-title">Block Programs</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="table-responsive mb-0">
|
||||||
|
<table className="table table-sm table-nowrap card-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="text-muted">Program</th>
|
||||||
|
<th className="text-muted">Transaction Count</th>
|
||||||
|
<th className="text-muted">% of Total</th>
|
||||||
|
<th className="text-muted">Instruction Count</th>
|
||||||
|
<th className="text-muted">% of Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{programEntries.map(([programId, txFreq]) => {
|
||||||
|
const ixFreq = ixFrequency.get(programId) as number;
|
||||||
|
return (
|
||||||
|
<tr key={programId}>
|
||||||
|
<td>
|
||||||
|
<Address pubkey={new PublicKey(programId)} link />
|
||||||
|
</td>
|
||||||
|
<td>{txFreq}</td>
|
||||||
|
<td>{((100 * txFreq) / totalTransactions).toFixed(2)}%</td>
|
||||||
|
<td>{ixFreq}</td>
|
||||||
|
<td>{((100 * ixFreq) / totalInstructions).toFixed(2)}%</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -6,9 +6,9 @@ import { BlockOverviewCard } from "components/block/BlockOverviewCard";
|
|||||||
// IE11 doesn't support Number.MAX_SAFE_INTEGER
|
// IE11 doesn't support Number.MAX_SAFE_INTEGER
|
||||||
const MAX_SAFE_INTEGER = 9007199254740991;
|
const MAX_SAFE_INTEGER = 9007199254740991;
|
||||||
|
|
||||||
type Props = { slot: string };
|
type Props = { slot: string; tab?: string };
|
||||||
|
|
||||||
export function BlockDetailsPage({ slot }: Props) {
|
export function BlockDetailsPage({ slot, tab }: Props) {
|
||||||
const slotNumber = Number(slot);
|
const slotNumber = Number(slot);
|
||||||
let output = <ErrorCard text={`Block ${slot} is not valid`} />;
|
let output = <ErrorCard text={`Block ${slot} is not valid`} />;
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ export function BlockDetailsPage({ slot }: Props) {
|
|||||||
slotNumber < MAX_SAFE_INTEGER &&
|
slotNumber < MAX_SAFE_INTEGER &&
|
||||||
slotNumber % 1 === 0
|
slotNumber % 1 === 0
|
||||||
) {
|
) {
|
||||||
output = <BlockOverviewCard slot={slotNumber} />;
|
output = <BlockOverviewCard slot={slotNumber} tab={tab} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -109,14 +109,14 @@ export const SYSVAR_IDS = {
|
|||||||
export function addressLabel(
|
export function addressLabel(
|
||||||
address: string,
|
address: string,
|
||||||
cluster: Cluster,
|
cluster: Cluster,
|
||||||
tokenRegistry: KnownTokenMap
|
tokenRegistry?: KnownTokenMap
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
return (
|
return (
|
||||||
PROGRAM_NAME_BY_ID[address] ||
|
PROGRAM_NAME_BY_ID[address] ||
|
||||||
LOADER_IDS[address] ||
|
LOADER_IDS[address] ||
|
||||||
SYSVAR_IDS[address] ||
|
SYSVAR_IDS[address] ||
|
||||||
SYSVAR_ID[address] ||
|
SYSVAR_ID[address] ||
|
||||||
tokenRegistry.get(address)?.tokenName ||
|
tokenRegistry?.get(address)?.tokenName ||
|
||||||
SerumMarketRegistry.get(address, cluster)
|
SerumMarketRegistry.get(address, cluster)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
"downlevelIteration": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
Reference in New Issue
Block a user