From ce1ac3066f724e1eee0c7b60d24a883e8bab35c7 Mon Sep 17 00:00:00 2001 From: Puneet Bharti Date: Mon, 20 Jul 2026 11:00:55 +0000 Subject: [PATCH] SV UI: warn when disabled config fields change in proposals (#5650) Show a warning and highlight on proposal details when a SetConfig vote alters UI-locked fields (emergency/API path). Also wrap long pending config values in the create form so synchronizer IDs fit. Signed-off-by: Puneet Bharti --- .../proposal-details-content.test.tsx | 85 +++++++++++++++++++ .../form-components/ConfigField.tsx | 69 ++++++++++----- .../governance/ConfigValuesChanges.tsx | 37 ++++++-- .../governance/ProposalDetailsContent.tsx | 26 +++++- 4 files changed, 188 insertions(+), 29 deletions(-) diff --git a/apps/sv/frontend/src/__tests__/governance/proposal-details-content.test.tsx b/apps/sv/frontend/src/__tests__/governance/proposal-details-content.test.tsx index b79df961b7..7ab545460a 100644 --- a/apps/sv/frontend/src/__tests__/governance/proposal-details-content.test.tsx +++ b/apps/sv/frontend/src/__tests__/governance/proposal-details-content.test.tsx @@ -422,6 +422,91 @@ describe('Proposal Details Content', () => { expect(jsonDiffsToggle).toHaveAttribute('aria-expanded', 'false'); expect(screen.queryByText('JSON')).not.toBeInTheDocument(); expect(screen.getByTestId('json-diffs-details')).not.toBeVisible(); + + expect( + screen.queryByTestId('proposal-details-disabled-fields-warning') + ).not.toBeInTheDocument(); + }); + + test('should warn when disabled fields were altered in an amulet rules config proposal', () => { + const amuletRulesConfigDetails = { + actionName: 'Set Amulet Rules Config', + action: 'CRARC_SetConfig', + proposal: { + configChanges: [ + { + fieldName: 'transferConfigCreateFee', + label: 'Transfer (Create Fee)', + currentValue: '0.03', + newValue: '0.04', + }, + { + fieldName: 'decentralizedSynchronizerActiveSynchronizer', + label: 'The currently active synchronizer', + currentValue: 'global-domain::12', + newValue: 'global-domain::13', + isId: true, + disabled: true, + }, + ], + }, + } as ProposalDetails; + + render( + + + + ); + + const warning = screen.getByTestId('proposal-details-disabled-fields-warning'); + expect(warning).toBeInTheDocument(); + expect(warning.textContent).toMatch(/Disabled fields have been altered in this vote proposal/); + + const changes = screen.getAllByTestId('config-change'); + expect(changes[1]).toHaveAttribute('data-disabled', 'true'); + expect(within(changes[1]).getByTestId('config-change-disabled-label')).toHaveTextContent( + 'Disabled field' + ); + }); + + test('should warn when disabled fields were altered in a dso rules config proposal', () => { + const dsoRulesConfigDetails = { + actionName: 'Set DSO Rules Configuration', + action: 'SRARC_SetConfig', + proposal: { + configChanges: [ + { + fieldName: 'decentralizedSynchronizerActiveSynchronizerId', + label: 'Decentralized synchronizer: Active synchronizer identifier', + currentValue: 'global-domain::12', + newValue: 'global-domain::13', + isId: true, + disabled: true, + }, + ], + }, + } as ProposalDetails; + + render( + + + + ); + + expect(screen.getByTestId('proposal-details-disabled-fields-warning')).toBeInTheDocument(); + expect(screen.getByTestId('config-change-disabled-label')).toHaveTextContent('Disabled field'); }); test('should render dso rules config changes', () => { diff --git a/apps/sv/frontend/src/components/form-components/ConfigField.tsx b/apps/sv/frontend/src/components/form-components/ConfigField.tsx index 61a0216eaa..45b011c6b2 100644 --- a/apps/sv/frontend/src/components/form-components/ConfigField.tsx +++ b/apps/sv/frontend/src/components/form-components/ConfigField.tsx @@ -72,10 +72,12 @@ export const ConfigField: React.FC = props => { sx={{ display: 'flex', justifyContent: 'space-between', - alignItems: 'center', + alignItems: 'flex-start', + gap: 2, + minWidth: 0, }} > - + {configChange.label} @@ -89,9 +91,10 @@ export const ConfigField: React.FC = props => { - + = props => { Current Configuration: {configChange.currentValue} @@ -140,24 +149,46 @@ export const PendingConfigDisplay: React.FC = ({ pend effectiveDate === 'Threshold' ? 'at Threshold' : dayjs(effectiveDate).fromNow(); return ( - - Pending Configuration: {pendingValue}
- This{' '} - - pending configuration - {' '} - will go into effect {effectiveText} -
+ Pending Configuration:{' '} + + {pendingValue} + +
+ + This{' '} + + pending configuration + {' '} + will go into effect {effectiveText} + +
); }; diff --git a/apps/sv/frontend/src/components/governance/ConfigValuesChanges.tsx b/apps/sv/frontend/src/components/governance/ConfigValuesChanges.tsx index 530e0d4b1c..5151f5705a 100644 --- a/apps/sv/frontend/src/components/governance/ConfigValuesChanges.tsx +++ b/apps/sv/frontend/src/components/governance/ConfigValuesChanges.tsx @@ -31,17 +31,36 @@ export const ConfigValuesChanges: React.FC = props => {changes.map((change, index) => ( - - {change.label} - + + + {change.label} + + {change.disabled && ( + + Disabled field + + )} + {change.currentValue && ( <> diff --git a/apps/sv/frontend/src/components/governance/ProposalDetailsContent.tsx b/apps/sv/frontend/src/components/governance/ProposalDetailsContent.tsx index 06ca0387df..3e1cf13269 100644 --- a/apps/sv/frontend/src/components/governance/ProposalDetailsContent.tsx +++ b/apps/sv/frontend/src/components/governance/ProposalDetailsContent.tsx @@ -8,7 +8,7 @@ import { } from '@daml.js/splice-dso-governance/lib/Splice/DsoRules'; import { ContractId } from '@daml/types'; import { ChevronLeft, Edit } from '@mui/icons-material'; -import { Box, Button, Divider, Stack, Tab, Tabs, Typography } from '@mui/material'; +import { Alert, Box, Button, Divider, Stack, Tab, Tabs, Typography } from '@mui/material'; import React, { PropsWithChildren, useEffect, useMemo, useRef, useState } from 'react'; import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime'; @@ -20,6 +20,7 @@ import { } from '@canton-network/splice-common-frontend'; import { Link as RouterLink } from 'react-router'; import { + ConfigChange, ProposalDetails, ProposalVote, ProposalVotingInformation, @@ -35,6 +36,11 @@ import { CopyableIdentifier, CopyableUrl, MemberIdentifier, VoteStats } from '.. import { useQuery } from '@tanstack/react-query'; import { useSvAdminClient } from '../../contexts/SvAdminServiceContext'; +/** True when a proposal changed fields that are locked/disabled in the create UI (e.g. emergency API). */ +export function hasAlteredDisabledFields(changes: ConfigChange[]): boolean { + return changes.some(c => c.disabled && c.currentValue !== c.newValue); +} + dayjs.extend(relativeTime); export interface ProposalDetailsContentProps { @@ -246,6 +252,15 @@ export const ProposalDetailsContent: React.FC = pro {proposalDetails.action === 'CRARC_SetConfig' && ( <> + {hasAlteredDisabledFields(proposalDetails.proposal.configChanges) && ( + + Disabled fields have been altered in this vote proposal. + + )} } @@ -267,6 +282,15 @@ export const ProposalDetailsContent: React.FC = pro {proposalDetails.action === 'SRARC_SetConfig' && ( <> + {hasAlteredDisabledFields(proposalDetails.proposal.configChanges) && ( + + Disabled fields have been altered in this vote proposal. + + )} }