diff --git a/apps/tangle-cloud/src/components/tangleCloudTable/TangleCloudTable.tsx b/apps/tangle-cloud/src/components/tangleCloudTable/TangleCloudTable.tsx index 2377fe5cb..ec2adef90 100644 --- a/apps/tangle-cloud/src/components/tangleCloudTable/TangleCloudTable.tsx +++ b/apps/tangle-cloud/src/components/tangleCloudTable/TangleCloudTable.tsx @@ -10,6 +10,7 @@ const GLASS_CONTAINER_CLASS = export const TangleCloudTable = ({ title, + hideTitle = false, data, isLoading, error, @@ -28,17 +29,18 @@ export const TangleCloudTable = ({ ? `${errorMessage.slice(0, 137)}...` : errorMessage : 'Indexer or network request failed.'; + const hasTitle = !hideTitle; - const titleElement = ( + const titleElement = hasTitle ? ( {title} - ); + ) : undefined; if (isLoading) { return (
- {titleElement} + {hasTitle ? titleElement : null} ({ } icon={loadingTableProps?.icon ?? '๐Ÿ”„'} className={twMerge( - 'w-full !border-0 !rounded-none !p-0 mt-3', + 'w-full !border-0 !rounded-none !p-0', + hasTitle ? 'mt-3' : null, loadingTableProps?.className, )} /> @@ -60,7 +63,7 @@ export const TangleCloudTable = ({ if (error) { return (
- {titleElement} + {hasTitle ? titleElement : null} ({ (hasRetry ? { onClick: () => void onRetry() } : undefined) } className={twMerge( - 'w-full !border-0 !rounded-none !p-0 mt-3', + 'w-full !border-0 !rounded-none !p-0', + hasTitle ? 'mt-3' : null, errorTableProps?.className, )} /> @@ -89,7 +93,7 @@ export const TangleCloudTable = ({ if (isEmpty) { return (
- {titleElement} + {hasTitle ? titleElement : null} ({ description={emptyTableProps?.description ?? 'No data found'} icon={emptyTableProps?.icon ?? '๐Ÿ”'} className={twMerge( - 'w-full !border-0 !rounded-none !p-0 mt-3', + 'w-full !border-0 !rounded-none !p-0', + hasTitle ? 'mt-3' : null, emptyTableProps?.className, )} /> @@ -107,19 +112,22 @@ export const TangleCloudTable = ({ return ( ); }; diff --git a/apps/tangle-cloud/src/components/tangleCloudTable/type.ts b/apps/tangle-cloud/src/components/tangleCloudTable/type.ts index 58e3284f6..1efad71b5 100644 --- a/apps/tangle-cloud/src/components/tangleCloudTable/type.ts +++ b/apps/tangle-cloud/src/components/tangleCloudTable/type.ts @@ -5,6 +5,7 @@ import { RowData, type useReactTable } from '@tanstack/react-table'; export interface TangleCloudTableProps { title: string; + hideTitle?: boolean; data: T[]; isLoading: boolean; error: Error | null; diff --git a/apps/tangle-cloud/src/pages/operators/manage/components/SlashingSummaryCards.tsx b/apps/tangle-cloud/src/pages/operators/manage/components/SlashingSummaryCards.tsx new file mode 100644 index 000000000..98194bd74 --- /dev/null +++ b/apps/tangle-cloud/src/pages/operators/manage/components/SlashingSummaryCards.tsx @@ -0,0 +1,83 @@ +import { + SlashDisputeEligibility, + SlashProposal, +} from '@tangle-network/tangle-shared-ui/data/graphql'; +import { Card, Typography } from '@tangle-network/ui-components'; +import { formatDateTime, formatTimeRemaining } from '../utils'; + +interface SlashingSummaryCardsProps { + activeRegistrationsCount: number; + activeAgainstMeCount: number; + myActiveProposalCount: number; + nearestPendingSlash: SlashProposal | null; + nearestPendingSlashEligibility: SlashDisputeEligibility | null; +} + +const SlashingSummaryCards = ({ + activeRegistrationsCount, + activeAgainstMeCount, + myActiveProposalCount, + nearestPendingSlash, + nearestPendingSlashEligibility, +}: SlashingSummaryCardsProps) => { + return ( + <> +
+ + + Active Registrations + + + {activeRegistrationsCount} + + + + + + Active Against You + + + {activeAgainstMeCount} + + + + + + My Active Proposals + + + {myActiveProposalCount} + + +
+ + {nearestPendingSlash && nearestPendingSlashEligibility ? ( + +
+
+ + Nearest Dispute Deadline Against You + +
+ + Slash #{nearestPendingSlash.id.toString()}{' '} + expires at{' '} + {formatDateTime(nearestPendingSlash.executeAfter)} + + + Time remaining:{' '} + {formatTimeRemaining( + nearestPendingSlashEligibility.secondsUntilDeadline, + )} + + + ) : null} + + ); +}; + +export default SlashingSummaryCards; diff --git a/apps/tangle-cloud/src/pages/operators/manage/components/SlashingTabsTable.tsx b/apps/tangle-cloud/src/pages/operators/manage/components/SlashingTabsTable.tsx new file mode 100644 index 000000000..f6e971bfa --- /dev/null +++ b/apps/tangle-cloud/src/pages/operators/manage/components/SlashingTabsTable.tsx @@ -0,0 +1,135 @@ +import { SlashProposal } from '@tangle-network/tangle-shared-ui/data/graphql'; +import { + Button, + Card, + TabContent, + Typography, +} from '@tangle-network/ui-components'; +import { TableAndChartTabs } from '@tangle-network/ui-components/components/TableAndChartTabs'; +import { useReactTable } from '@tanstack/react-table'; +import { TangleCloudTable } from '../../../../components/tangleCloudTable/TangleCloudTable'; +import { SLASHING_TAB_ICONS, SLASHING_TABS, SlashingTab } from '../constants'; + +interface SlashingTabsTableProps { + selectedSlashingTab: SlashingTab; + onSlashingTabChange: (tab: SlashingTab) => void; + onOpenProposeModal: () => void; + myProposals: SlashProposal[]; + againstMe: SlashProposal[]; + loadingSlash: boolean; + slashError: Error | null; + refetchSlashProposals: () => void; + myProposalsTable: ReturnType>; + againstMeTable: ReturnType>; + executeError: string | null; + onDismissExecuteError: () => void; +} + +const SlashingTabsTable = ({ + selectedSlashingTab, + onSlashingTabChange, + onOpenProposeModal, + myProposals, + againstMe, + loadingSlash, + slashError, + refetchSlashProposals, + myProposalsTable, + againstMeTable, + executeError, + onDismissExecuteError, +}: SlashingTabsTableProps) => { + return ( + +
+ + Slashing Management + + +
+ + {executeError ? ( + +
+ + {executeError} + + +
+
+ ) : null} + + onSlashingTabChange(tab as SlashingTab)} + className="space-y-5" + enableAdvancedDivider + > + + + + + + + + +
+ ); +}; + +export default SlashingTabsTable; diff --git a/apps/tangle-cloud/src/pages/operators/manage/components/modals/CancelSlashModal.tsx b/apps/tangle-cloud/src/pages/operators/manage/components/modals/CancelSlashModal.tsx new file mode 100644 index 000000000..360a50830 --- /dev/null +++ b/apps/tangle-cloud/src/pages/operators/manage/components/modals/CancelSlashModal.tsx @@ -0,0 +1,112 @@ +import { + SlashActionPermissions, + SlashProposal, +} from '@tangle-network/tangle-shared-ui/data/graphql'; +import { + Modal, + ModalBody, + ModalContent, + ModalFooterActions, + ModalHeader, + Typography, +} from '@tangle-network/ui-components'; + +interface CancelSlashModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + selectedSlash: SlashProposal | null; + selectedSlashPermissions: SlashActionPermissions | null; + cancelReason: string; + onCancelReasonChange: (value: string) => void; + minCancelReasonLength: number; + trimmedCancelReasonLength: number; + canSubmitCancel: boolean; + isSubmitting: boolean; + onConfirm: () => void; + errorMessage: string | null; + onDismissError: () => void; +} + +const CancelSlashModal = ({ + open, + onOpenChange, + selectedSlash, + selectedSlashPermissions, + cancelReason, + onCancelReasonChange, + minCancelReasonLength, + trimmedCancelReasonLength, + canSubmitCancel, + isSubmitting, + onConfirm, + errorMessage, + onDismissError, +}: CancelSlashModalProps) => { + return ( + + + Cancel Slash Proposal + + + Cancel slash proposal #{selectedSlash?.id.toString()} + + + Admin-only action. Authorization is validated on-chain at submission + time. + +
+ + Reason for cancellation + +