Skip to content

Commit 7a34398

Browse files
committed
feat: enhance proposal modal with detailed change tracking for shards, feeds, and publishers
1 parent 850357e commit 7a34398

File tree

1 file changed

+266
-21
lines changed
  • governance/xc_admin/packages/xc_admin_frontend/components/programs

1 file changed

+266
-21
lines changed

governance/xc_admin/packages/xc_admin_frontend/components/programs/PythLazer.tsx

Lines changed: 266 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,51 @@ import {
88
} from '@pythnetwork/xc-admin-common'
99
import toast from 'react-hot-toast'
1010
import Loadbar from '../loaders/Loadbar'
11-
import { LazerState } from '@pythnetwork/xc-admin-common/src/programs/types'
11+
import Spinner from '../common/Spinner'
12+
import { LazerState, LazerFeed, LazerPublisher } from '@pythnetwork/xc-admin-common/src/programs/types'
13+
import { capitalizeFirstLetter } from '../../utils/capitalizeFirstLetter'
1214

1315
interface PythLazerProps {
1416
proposerServerUrl: string
1517
}
1618

19+
interface ShardChanges {
20+
prev?: {
21+
shardId: number
22+
shardName: string
23+
minRate: string
24+
}
25+
new: {
26+
shardId: number
27+
shardName: string
28+
minRate: string
29+
}
30+
}
31+
32+
interface FeedChanges {
33+
prev?: LazerFeed
34+
new?: LazerFeed
35+
}
36+
37+
interface PublisherChanges {
38+
prev?: LazerPublisher
39+
new?: LazerPublisher
40+
}
41+
42+
interface ShardChangesRowsProps {
43+
changes: ShardChanges
44+
}
45+
46+
interface FeedChangesRowsProps {
47+
changes: FeedChanges
48+
feedId: string
49+
}
50+
51+
interface PublisherChangesRowsProps {
52+
changes: PublisherChanges
53+
publisherId: string
54+
}
55+
1756
interface ModalContentProps {
1857
changes: Record<
1958
string,
@@ -26,6 +65,167 @@ interface ModalContentProps {
2665
isSendProposalButtonLoading: boolean
2766
}
2867

68+
const ShardChangesRows: React.FC<ShardChangesRowsProps> = ({ changes }) => {
69+
const isNewShard = !changes.prev && changes.new
70+
71+
return (
72+
<>
73+
{Object.entries(changes.new).map(([key, newValue]) =>
74+
(isNewShard || (changes.prev && changes.prev[key as keyof typeof changes.prev] !== newValue)) && (
75+
<tr key={key}>
76+
<td className="base16 py-4 pl-6 pr-2 lg:pl-6">
77+
{key
78+
.split(/(?=[A-Z])/)
79+
.join(' ')
80+
.split('_')
81+
.map((word) => capitalizeFirstLetter(word))
82+
.join(' ')}
83+
</td>
84+
<td className="base16 py-4 pl-1 pr-2 lg:pl-6">
85+
{!isNewShard && changes.prev ? (
86+
<>
87+
<s>{String(changes.prev[key as keyof typeof changes.prev])}</s>
88+
<br />
89+
</>
90+
) : null}
91+
{String(newValue)}
92+
</td>
93+
</tr>
94+
)
95+
)}
96+
</>
97+
)
98+
}
99+
100+
const FeedChangesRows: React.FC<FeedChangesRowsProps> = ({ changes, feedId }) => {
101+
const isNewFeed = !changes.prev && changes.new
102+
const isDeletedFeed = changes.prev && !changes.new
103+
104+
if (isDeletedFeed) {
105+
return (
106+
<tr>
107+
<td className="base16 py-4 pl-6 pr-2 lg:pl-6">Feed ID</td>
108+
<td className="base16 py-4 pl-1 pr-2 lg:pl-6">{feedId.replace('feed_', '')}</td>
109+
</tr>
110+
)
111+
}
112+
113+
if (!changes.new) return null
114+
115+
const renderMetadataChanges = () => {
116+
if (!changes.new?.metadata) return null
117+
118+
return Object.entries(changes.new.metadata).map(([key, newValue]) => {
119+
const prevValue = changes.prev?.metadata?.[key as keyof typeof changes.prev.metadata]
120+
const hasChanged = isNewFeed || prevValue !== newValue
121+
122+
if (!hasChanged) return null
123+
124+
return (
125+
<tr key={key}>
126+
<td className="base16 py-4 pl-6 pr-2 lg:pl-6">
127+
{key
128+
.split(/(?=[A-Z])/)
129+
.join(' ')
130+
.split('_')
131+
.map((word) => capitalizeFirstLetter(word))
132+
.join(' ')}
133+
</td>
134+
<td className="base16 py-4 pl-1 pr-2 lg:pl-6">
135+
{!isNewFeed && prevValue !== undefined ? (
136+
<>
137+
<s>{String(prevValue)}</s>
138+
<br />
139+
</>
140+
) : null}
141+
{String(newValue)}
142+
</td>
143+
</tr>
144+
)
145+
})
146+
}
147+
148+
const renderPendingActivationChanges = () => {
149+
if (changes.new?.pendingActivation !== undefined || changes.prev?.pendingActivation !== undefined) {
150+
const hasChanged = isNewFeed || changes.prev?.pendingActivation !== changes.new?.pendingActivation
151+
152+
if (hasChanged) {
153+
return (
154+
<tr key="pendingActivation">
155+
<td className="base16 py-4 pl-6 pr-2 lg:pl-6">Pending Activation</td>
156+
<td className="base16 py-4 pl-1 pr-2 lg:pl-6">
157+
{!isNewFeed && changes.prev?.pendingActivation ? (
158+
<>
159+
<s>{changes.prev.pendingActivation}</s>
160+
<br />
161+
</>
162+
) : null}
163+
{changes.new?.pendingActivation || 'None'}
164+
</td>
165+
</tr>
166+
)
167+
}
168+
}
169+
return null
170+
}
171+
172+
return (
173+
<>
174+
{renderMetadataChanges()}
175+
{renderPendingActivationChanges()}
176+
</>
177+
)
178+
}
179+
180+
const PublisherChangesRows: React.FC<PublisherChangesRowsProps> = ({ changes, publisherId }) => {
181+
const isNewPublisher = !changes.prev && changes.new
182+
const isDeletedPublisher = changes.prev && !changes.new
183+
184+
if (isDeletedPublisher) {
185+
return (
186+
<tr>
187+
<td className="base16 py-4 pl-6 pr-2 lg:pl-6">Publisher ID</td>
188+
<td className="base16 py-4 pl-1 pr-2 lg:pl-6">{publisherId.replace('publisher_', '')}</td>
189+
</tr>
190+
)
191+
}
192+
193+
if (!changes.new) return null
194+
195+
return (
196+
<>
197+
{Object.entries(changes.new).map(([key, newValue]) => {
198+
const prevValue = changes.prev?.[key as keyof LazerPublisher]
199+
const hasChanged = isNewPublisher || JSON.stringify(prevValue) !== JSON.stringify(newValue)
200+
201+
if (!hasChanged) return null
202+
203+
return (
204+
<tr key={key}>
205+
<td className="base16 py-4 pl-6 pr-2 lg:pl-6">
206+
{key
207+
.split(/(?=[A-Z])/)
208+
.join(' ')
209+
.split('_')
210+
.map((word) => capitalizeFirstLetter(word))
211+
.join(' ')}
212+
</td>
213+
<td className="base16 py-4 pl-1 pr-2 lg:pl-6">
214+
{!isNewPublisher && prevValue !== undefined ? (
215+
<>
216+
<s>{Array.isArray(prevValue) ? prevValue.join(', ') : String(prevValue)}</s>
217+
<br />
218+
</>
219+
) : null}
220+
{Array.isArray(newValue) ? newValue.join(', ') : String(newValue)}
221+
</td>
222+
</tr>
223+
)
224+
})}
225+
</>
226+
)
227+
}
228+
29229
const ModalContent: React.FC<ModalContentProps> = ({
30230
changes,
31231
onSendProposal,
@@ -35,23 +235,68 @@ const ModalContent: React.FC<ModalContentProps> = ({
35235
<>
36236
{Object.keys(changes).length > 0 ? (
37237
<table className="mb-10 w-full table-auto bg-darkGray text-left">
38-
<tbody>
39-
{Object.entries(changes).map(([key, change]) => (
40-
<tr key={key}>
41-
<td
42-
className="base16 py-4 pl-6 pr-2 font-bold lg:pl-6"
43-
colSpan={2}
44-
>
45-
{key}
46-
</td>
47-
<td className="py-3 pl-6 pr-1 lg:pl-6">
48-
<pre className="whitespace-pre-wrap rounded bg-gray-100 p-2 text-xs dark:bg-gray-700 dark:text-gray-300">
49-
{JSON.stringify(change, null, 2)}
50-
</pre>
51-
</td>
52-
</tr>
53-
))}
54-
</tbody>
238+
{Object.entries(changes).map(([key, change]) => {
239+
const { prev, new: newChanges } = change
240+
const isAddition = !prev && newChanges
241+
const isDeletion = prev && !newChanges
242+
243+
let title = key
244+
if (key === 'shard') {
245+
title = isAddition ? 'Add New Shard' : isDeletion ? 'Delete Shard' : 'Shard Configuration'
246+
} else if (key.startsWith('feed_')) {
247+
const feedId = key.replace('feed_', '')
248+
title = isAddition ? `Add New Feed (ID: ${feedId})` : isDeletion ? `Delete Feed (ID: ${feedId})` : `Feed ${feedId}`
249+
} else if (key.startsWith('publisher_')) {
250+
const publisherId = key.replace('publisher_', '')
251+
title = isAddition ? `Add New Publisher (ID: ${publisherId})` : isDeletion ? `Delete Publisher (ID: ${publisherId})` : `Publisher ${publisherId}`
252+
}
253+
254+
return (
255+
<tbody key={key}>
256+
<tr>
257+
<td
258+
className="base16 py-4 pl-6 pr-2 font-bold lg:pl-6"
259+
colSpan={2}
260+
>
261+
{title}
262+
</td>
263+
</tr>
264+
265+
{key === 'shard' && newChanges ? (
266+
<ShardChangesRows
267+
changes={{
268+
prev: prev as ShardChanges['prev'],
269+
new: newChanges as ShardChanges['new'],
270+
}}
271+
/>
272+
) : key.startsWith('feed_') ? (
273+
<FeedChangesRows
274+
feedId={key}
275+
changes={{
276+
prev: prev as LazerFeed,
277+
new: newChanges as LazerFeed,
278+
}}
279+
/>
280+
) : key.startsWith('publisher_') ? (
281+
<PublisherChangesRows
282+
publisherId={key}
283+
changes={{
284+
prev: prev as LazerPublisher,
285+
new: newChanges as LazerPublisher,
286+
}}
287+
/>
288+
) : null}
289+
290+
{Object.keys(changes).indexOf(key) !== Object.keys(changes).length - 1 ? (
291+
<tr>
292+
<td className="base16 py-4 pl-6 pr-6" colSpan={2}>
293+
<hr className="border-gray-700" />
294+
</td>
295+
</tr>
296+
) : null}
297+
</tbody>
298+
)
299+
})}
55300
</table>
56301
) : (
57302
<p className="mb-8 leading-6">No proposed changes.</p>
@@ -62,7 +307,7 @@ const ModalContent: React.FC<ModalContentProps> = ({
62307
onClick={onSendProposal}
63308
disabled={isSendProposalButtonLoading}
64309
>
65-
{isSendProposalButtonLoading ? 'Sending...' : 'Send Proposal'}
310+
{isSendProposalButtonLoading ? <Spinner /> : 'Send Proposal'}
66311
</button>
67312
)}
68313
</>
@@ -138,7 +383,7 @@ const PythLazer = ({
138383
openModal()
139384
} catch (error) {
140385
if (error instanceof Error) {
141-
toast.error(error.message)
386+
toast.error(capitalizeFirstLetter(error.message))
142387
}
143388
}
144389
}
@@ -161,7 +406,7 @@ const PythLazer = ({
161406
toast.success('Proposal sent successfully!')
162407
} catch (error) {
163408
if (error instanceof Error) {
164-
toast.error(error.message)
409+
toast.error(capitalizeFirstLetter(error.message))
165410
}
166411
} finally {
167412
setIsSendProposalButtonLoading(false)

0 commit comments

Comments
 (0)