Skip to content

Commit e262111

Browse files
committed
Implement DeploymentsDetailsModal
Remove unused token component + hook Modify DeploymentContext schema
1 parent 672124d commit e262111

File tree

12 files changed

+141
-190
lines changed

12 files changed

+141
-190
lines changed

src/features/deployerWallet/fund.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ import { logger } from '../../utils/logger';
2020
import { useMultiProvider } from '../chains/hooks';
2121
import { getChainDisplayName } from '../chains/utils';
2222

23+
const USER_REJECTED_ERROR = 'User rejected';
24+
const CHAIN_MISMATCH_ERROR = 'ChainMismatchError';
25+
2326
export function useFundDeployerAccount(
2427
chainName: ChainName,
2528
gasUnits: bigint,
@@ -86,16 +89,27 @@ async function executeTransfer({
8689
const amount = await getFundingAmount(chainName, gasUnits, multiProvider);
8790
await assertSenderBalance(sender, chainName, amount, multiProvider);
8891
const tx = await getFundingTx(deployerAddress, chainName, amount, multiProvider);
89-
const { hash, confirm } = await sendTransaction({
90-
tx,
91-
chainName,
92-
activeChainName,
93-
});
9492

95-
const txReceipt = await confirm();
96-
logger.debug(`Deployer funding tx confirmed on ${chainName}, hash: ${hash}`);
97-
toastTxSuccess(`Deployer funded on ${chainName}!`, hash, origin);
98-
return txReceipt;
93+
try {
94+
const { hash, confirm } = await sendTransaction({
95+
tx,
96+
chainName,
97+
activeChainName,
98+
});
99+
const txReceipt = await confirm();
100+
logger.debug(`Deployer funding tx confirmed on ${chainName}, hash: ${hash}`);
101+
toastTxSuccess(`Deployer funded on ${chainName}!`, hash, origin);
102+
return txReceipt;
103+
} catch (error: any) {
104+
const errorDetails = error.message || error.toString();
105+
if (errorDetails.includes(CHAIN_MISMATCH_ERROR)) {
106+
throw new Error(`Wallet must be connected to ${chainName}`);
107+
} else if (errorDetails.includes(USER_REJECTED_ERROR)) {
108+
throw new Error('User rejected transaction');
109+
} else {
110+
throw error;
111+
}
112+
}
99113
}
100114

101115
async function getFundingAmount(
Lines changed: 68 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
import { Modal } from '@hyperlane-xyz/widgets';
2-
import { useMemo } from 'react';
3-
import { formatTimestamp } from '../../utils/date';
1+
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react';
2+
import { toTitleCase } from '@hyperlane-xyz/utils';
3+
import { ChevronIcon, CopyButton, Modal } from '@hyperlane-xyz/widgets';
4+
import { stringify } from 'yaml';
5+
import { Color } from '../../styles/Color';
6+
import { useMultiProvider } from '../chains/hooks';
7+
import { getChainDisplayName } from '../chains/utils';
8+
import { DeploymentStatusIcon } from './DeploymentStatusIcon';
49
import { DeploymentContext } from './types';
510

611
export function DeploymentsDetailsModal({
@@ -12,45 +17,70 @@ export function DeploymentsDetailsModal({
1217
onClose: () => void;
1318
deployment: DeploymentContext;
1419
}) {
15-
const { status, timestamp } = deployment || {};
20+
const { id, status, timestamp, config, result } = deployment || {};
21+
const { chains, type } = config;
1622

17-
const date = useMemo(
18-
() => (timestamp ? formatTimestamp(timestamp) : formatTimestamp(new Date().getTime())),
19-
[timestamp],
20-
);
23+
const multiProvider = useMultiProvider();
24+
const chainNames = chains.map((c) => getChainDisplayName(multiProvider, c)).join(', ');
2125

2226
return (
23-
<Modal isOpen={isOpen} close={onClose} panelClassname="p-4 md:p-5 max-w-sm">
24-
{status}
25-
{date}
27+
<Modal isOpen={isOpen} close={onClose} panelClassname="p-4 md:p-5 max-w-[30rem] space-y-3">
28+
<div className="flex items-center justify-between">
29+
<h2 className="text-lg text-primary-500">{`Deployment #${id}`}</h2>
30+
<DeploymentStatusIcon status={status} size={22} />
31+
</div>
32+
<div className="grid grid-cols-2 gap-2.5">
33+
<DeploymentProperty name="Type" value={toTitleCase(type)} />
34+
<DeploymentProperty name="Status" value={toTitleCase(status)} />
35+
<DeploymentProperty name="Date Created" value={new Date(timestamp).toLocaleDateString()} />
36+
<DeploymentProperty name="Time Created" value={new Date(timestamp).toLocaleTimeString()} />
37+
<DeploymentProperty name="Chains" value={chainNames} />
38+
</div>
39+
<CollapsibleData data={config.config} label="Deployment Config" />
40+
{result && <CollapsibleData data={result} label="Deployment Result" />}
2641
</Modal>
2742
);
2843
}
44+
function DeploymentProperty({ name, value }: { name: string; value: string }) {
45+
return (
46+
<div>
47+
<label className="text-sm text-gray-600">{name}</label>
48+
<div className="truncate text-md">{value}</div>
49+
</div>
50+
);
51+
}
2952

30-
// TODO remove?
31-
// function DeploymentProperty({
32-
// name,
33-
// value,
34-
// url,
35-
// }: {
36-
// name: string;
37-
// value: string;
38-
// url?: string;
39-
// }) {
40-
// return (
41-
// <div>
42-
// <div className="flex items-center justify-between">
43-
// <label className="text-sm leading-normal tracking-wider text-gray-350">{name}</label>
44-
// <div className="flex items-center space-x-2">
45-
// {url && (
46-
// <a href={url} target="_blank" rel="noopener noreferrer">
47-
// <Image src={LinkIcon} width={14} height={14} alt="" />
48-
// </a>
49-
// )}
50-
// <CopyButton copyValue={value} width={14} height={14} className="opacity-40" />
51-
// </div>
52-
// </div>
53-
// <div className="mt-1 truncate text-sm leading-normal tracking-wider">{value}</div>
54-
// </div>
55-
// );
56-
// }
53+
function CollapsibleData({ data, label }: { data: any; label: string }) {
54+
const yamlConfig = stringify(data, { indent: 2 });
55+
56+
return (
57+
<Disclosure as="div">
58+
{({ open }) => (
59+
<>
60+
<DisclosureButton className="data-[open] flex items-center gap-2 text-sm text-gray-600">
61+
<span>{label}</span>
62+
<ChevronIcon
63+
width={11}
64+
height={7}
65+
direction={open ? 'n' : 's'}
66+
color={Color.gray['600']}
67+
className="pt-px"
68+
/>
69+
</DisclosureButton>
70+
<DisclosurePanel
71+
transition
72+
className="relative mt-1 overflow-x-auto rounded-md bg-primary-500/5 px-1.5 py-1 text-xs transition duration-200 data-[closed]:-translate-y-4 data-[closed]:opacity-0"
73+
>
74+
<pre>{yamlConfig}</pre>
75+
<CopyButton
76+
copyValue={yamlConfig}
77+
width={14}
78+
height={14}
79+
className="absolute right-2 top-2 opacity-60"
80+
/>
81+
</DisclosurePanel>
82+
</>
83+
)}
84+
</Disclosure>
85+
);
86+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { ErrorIcon, SpinnerIcon } from '@hyperlane-xyz/widgets';
2+
import { ConfirmedIcon } from '../../components/icons/ConfirmedIcon';
3+
import { LogsIcon } from '../../components/icons/LogsIcon';
4+
import { StopIcon } from '../../components/icons/StopIcon';
5+
import { Color } from '../../styles/Color';
6+
import { DeploymentStatus } from './types';
7+
8+
export function DeploymentStatusIcon({ status, size }: { status: DeploymentStatus; size: number }) {
9+
switch (status) {
10+
case DeploymentStatus.Configured:
11+
return <LogsIcon width={size - 3} height={size - 3} color={Color.primary['500']} />;
12+
case DeploymentStatus.Deploying:
13+
return <SpinnerIcon width={size} height={size} />;
14+
case DeploymentStatus.Complete:
15+
return <ConfirmedIcon width={size} height={size} color={Color.primary['500']} />;
16+
case DeploymentStatus.Cancelled:
17+
return <StopIcon width={size - 2} height={size - 2} color={Color.red['500']} />;
18+
case DeploymentStatus.Failed:
19+
default:
20+
return <ErrorIcon width={size} height={size} color={Color.red['500']} />;
21+
}
22+
}

src/features/deployment/hooks.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,13 @@ export function useCoreDeploymentConfig() {
2121
}
2222

2323
export function useDeploymentHistory() {
24-
return useStore((s) => ({
24+
const state = useStore((s) => ({
2525
deployments: s.deployments,
2626
addDeployment: s.addDeployment,
2727
updateDeploymentStatus: s.updateDeploymentStatus,
2828
}));
29+
return {
30+
...state,
31+
currentIndex: state.deployments.length - 1,
32+
};
2933
}

src/features/deployment/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ export const FinalDeploymentStatuses = [
2121
];
2222

2323
export interface DeploymentContext {
24+
id: number;
2425
timestamp: number;
2526
status: DeploymentStatus;
26-
type: DeploymentType;
2727
config: DeploymentConfig;
2828
result?: DeploymentResult;
2929
}

src/features/deployment/utils.ts

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,7 @@
1-
import { ErrorIcon, tryClipboardSet } from '@hyperlane-xyz/widgets';
2-
import { FC } from 'react';
1+
import { tryClipboardSet } from '@hyperlane-xyz/widgets';
32
import { toast } from 'react-toastify';
43
import { stringify } from 'yaml';
5-
import { ConfirmedIcon } from '../../components/icons/ConfirmedIcon';
6-
import { LogsIcon } from '../../components/icons/LogsIcon';
7-
import { StopIcon } from '../../components/icons/StopIcon';
8-
import { DeploymentConfig, DeploymentStatus } from './types';
9-
10-
export function getIconByDeploymentStatus(status: DeploymentStatus): FC<any> {
11-
switch (status) {
12-
case DeploymentStatus.Configured:
13-
return LogsIcon;
14-
case DeploymentStatus.Complete:
15-
return ConfirmedIcon;
16-
case DeploymentStatus.Failed:
17-
return ErrorIcon;
18-
case DeploymentStatus.Cancelled:
19-
return StopIcon;
20-
default:
21-
return ErrorIcon;
22-
}
23-
}
4+
import { DeploymentConfig } from './types';
245

256
export async function tryCopyConfig(deploymentConfig: DeploymentConfig | undefined) {
267
if (!deploymentConfig) return;

src/features/deployment/warp/WarpDeploymentDeploy.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import { useMultiProvider } from '../../chains/hooks';
1515
import { getChainDisplayName } from '../../chains/utils';
1616
import { useFundDeployerAccount } from '../../deployerWallet/fund';
1717
import { getDeployerAddressForProtocol, useTempDeployerWallets } from '../../deployerWallet/hooks';
18-
import { useWarpDeploymentConfig } from '../hooks';
18+
import { useDeploymentHistory, useWarpDeploymentConfig } from '../hooks';
19+
import { DeploymentStatus } from '../types';
1920

2021
enum DeployStep {
2122
FundDeployer,
@@ -181,10 +182,12 @@ function ExecuteDeploy() {
181182
}
182183

183184
function ButtonSection() {
185+
const { updateDeploymentStatus, currentIndex } = useDeploymentHistory();
184186
const { setPage } = useCardNav();
185187
const onClickCancel = () => {
186188
// TODO cancel in SDK if possible?
187189
toast.warn('Deployment cancelled');
190+
updateDeploymentStatus(currentIndex, DeploymentStatus.Cancelled);
188191
setPage(CardPage.WarpForm);
189192
};
190193

src/features/deployment/warp/WarpDeploymentReview.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { isSyntheticTokenType } from './utils';
2121

2222
// TODO move to widgets lib
2323
import InfoCircle from '../../../images/icons/info-circle.svg';
24-
import { DeploymentStatus, DeploymentType } from '../types';
24+
import { DeploymentStatus } from '../types';
2525
import { tryCopyConfig } from '../utils';
2626

2727
export function WarpDeploymentReview() {
@@ -196,9 +196,7 @@ function ButtonSection() {
196196
return;
197197
}
198198
addDeployment({
199-
timestamp: Date.now(),
200199
status: DeploymentStatus.Configured,
201-
type: DeploymentType.Warp,
202200
config: deploymentConfig,
203201
});
204202
setPage(CardPage.WarpDeploy);

src/features/store.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ export interface AppState {
4242

4343
// User history
4444
deployments: DeploymentContext[];
45-
addDeployment: (t: DeploymentContext) => void;
45+
addDeployment: (t: Omit<DeploymentContext, 'id' | 'timestamp'>) => void;
4646
updateDeploymentStatus: (i: number, s: DeploymentStatus) => void;
47-
failUnconfirmedDeployments: () => void;
47+
cancelPendingDeployments: () => void;
4848

4949
// Shared component state
5050
cardPage: CardPage;
@@ -104,7 +104,9 @@ export const useStore = create<AppState>()(
104104
// User history
105105
deployments: [],
106106
addDeployment: (t) => {
107-
set((state) => ({ deployments: [...state.deployments, t] }));
107+
const currentDeployments = get().deployments;
108+
const newDeployment = { ...t, id: currentDeployments.length + 1, timestamp: Date.now() };
109+
set({ deployments: [...currentDeployments, newDeployment] });
108110
},
109111
updateDeploymentStatus: (i, s) => {
110112
set((state) => {
@@ -116,12 +118,12 @@ export const useStore = create<AppState>()(
116118
};
117119
});
118120
},
119-
failUnconfirmedDeployments: () => {
121+
cancelPendingDeployments: () => {
120122
set((state) => ({
121123
deployments: state.deployments.map((t) =>
122124
FinalDeploymentStatuses.includes(t.status)
123125
? t
124-
: { ...t, status: DeploymentStatus.Failed },
126+
: { ...t, status: DeploymentStatus.Cancelled },
125127
),
126128
}));
127129
},
@@ -162,7 +164,7 @@ export const useStore = create<AppState>()(
162164
onRehydrateStorage: () => {
163165
logger.debug('Rehydrating state');
164166
return (state, error) => {
165-
state?.failUnconfirmedDeployments();
167+
state?.cancelPendingDeployments();
166168
if (error || !state) {
167169
logger.error('Error during hydration', error);
168170
return;

0 commit comments

Comments
 (0)