Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<Wrapper>
<ProposalDetailsContent
currentSvPartyId={voteRequest.votingInformation.requester}
contractId={voteRequest.contractId}
proposalDetails={amuletRulesConfigDetails}
votingInformation={voteRequest.votingInformation}
votes={voteRequest.votes}
/>
</Wrapper>
);

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(
<Wrapper>
<ProposalDetailsContent
currentSvPartyId={voteRequest.votingInformation.requester}
contractId={voteRequest.contractId}
proposalDetails={dsoRulesConfigDetails}
votingInformation={voteRequest.votingInformation}
votes={voteRequest.votes}
/>
</Wrapper>
);

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', () => {
Expand Down
69 changes: 50 additions & 19 deletions apps/sv/frontend/src/components/form-components/ConfigField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,12 @@ export const ConfigField: React.FC<ConfigFieldProps> = props => {
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
alignItems: 'flex-start',
gap: 2,
minWidth: 0,
}}
>
<Box>
<Box sx={{ minWidth: 0, flex: 1, pr: 1 }}>
<Typography variant="body1" data-testid={`config-label-${configChange.fieldName}`}>
{configChange.label}
</Typography>
Expand All @@ -89,9 +91,10 @@ export const ConfigField: React.FC<ConfigFieldProps> = props => {
</Typography>
</Box>

<Box sx={{ width: 250 }}>
<Box sx={{ width: 250, minWidth: 0, flexShrink: 0 }}>
<MuiTextField
{...textFieldProps}
fullWidth
// We choose empty string to represent fields that could be undefined because their values have not been set.
value={field.state.value?.value || ''}
onBlur={field.handleBlur}
Expand All @@ -107,7 +110,13 @@ export const ConfigField: React.FC<ConfigFieldProps> = props => {
<Typography
variant="caption"
color="text.secondary"
sx={{ mt: 0.5, display: 'block' }}
title={configChange.currentValue}
sx={{
mt: 0.5,
display: 'block',
overflowWrap: 'anywhere',
wordBreak: 'break-word',
}}
data-testid={`config-current-value-${configChange.fieldName}`}
>
Current Configuration: {configChange.currentValue}
Expand Down Expand Up @@ -140,24 +149,46 @@ export const PendingConfigDisplay: React.FC<PendingConfigDisplayProps> = ({ pend
effectiveDate === 'Threshold' ? 'at Threshold' : dayjs(effectiveDate).fromNow();

return (
<Typography
variant="caption"
color="text.secondary"
sx={{ mt: 0.5, display: 'block', textAlign: 'center' }}
<Box
sx={{ mt: 0.5, width: '100%', minWidth: 0 }}
data-testid={`config-pending-value-${fieldName}`}
>
Pending Configuration: <strong>{pendingValue}</strong> <br />
This{' '}
<RouterLink
to={`/governance/proposals/${proposalCid}`}
target="_blank"
rel="noopener noreferrer"
style={{ color: 'inherit' }}
<Typography
variant="caption"
color="text.secondary"
sx={{ display: 'block', textAlign: 'center' }}
>
pending configuration
</RouterLink>{' '}
will go into effect <strong>{effectiveText}</strong>
</Typography>
Pending Configuration:{' '}
<Box
component="strong"
title={pendingValue}
sx={{
display: 'inline',
overflowWrap: 'anywhere',
wordBreak: 'break-word',
fontWeight: 700,
}}
>
{pendingValue}
</Box>
</Typography>
<Typography
variant="caption"
color="text.secondary"
sx={{ display: 'block', textAlign: 'center', mt: 0.5 }}
>
This{' '}
<RouterLink
to={`/governance/proposals/${proposalCid}`}
target="_blank"
rel="noopener noreferrer"
style={{ color: 'inherit' }}
>
pending configuration
</RouterLink>{' '}
will go into effect <strong>{effectiveText}</strong>
</Typography>
</Box>
);
};

Expand Down
37 changes: 28 additions & 9 deletions apps/sv/frontend/src/components/governance/ConfigValuesChanges.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,36 @@ export const ConfigValuesChanges: React.FC<ConfigValuesChangesProps> = props =>
{changes.map((change, index) => (
<Box
key={index}
sx={{ display: 'flex', alignItems: 'center', gap: 2 }}
sx={{
display: 'flex',
alignItems: 'center',
gap: 2,
...(change.disabled && {
px: 1.5,
py: 1,
borderRadius: 1,
borderLeft: '3px solid',
borderColor: 'warning.main',
bgcolor: 'rgba(255, 167, 38, 0.08)',
}),
}}
data-testid="config-change"
data-disabled={change.disabled ? 'true' : undefined}
>
<Typography
variant="body1"
sx={{ minWidth: 200 }}
data-testid="config-change-field-label"
color={textColor}
>
{change.label}
</Typography>
<Box sx={{ minWidth: 200 }}>
<Typography variant="body1" data-testid="config-change-field-label" color={textColor}>
{change.label}
</Typography>
{change.disabled && (
<Typography
variant="caption"
color="warning.main"
data-testid="config-change-disabled-label"
>
Disabled field
</Typography>
)}
</Box>

{change.currentValue && (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -20,6 +20,7 @@ import {
} from '@canton-network/splice-common-frontend';
import { Link as RouterLink } from 'react-router';
import {
ConfigChange,
ProposalDetails,
ProposalVote,
ProposalVotingInformation,
Expand All @@ -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 {
Expand Down Expand Up @@ -246,6 +252,15 @@ export const ProposalDetailsContent: React.FC<ProposalDetailsContentProps> = pro

{proposalDetails.action === 'CRARC_SetConfig' && (
<>
{hasAlteredDisabledFields(proposalDetails.proposal.configChanges) && (
<Alert
severity="warning"
variant="outlined"
data-testid="proposal-details-disabled-fields-warning"
>
Disabled fields have been altered in this vote proposal.
</Alert>
)}
<DetailItem
label="Proposed Changes"
value={<ConfigValuesChanges changes={proposalDetails.proposal.configChanges} />}
Expand All @@ -267,6 +282,15 @@ export const ProposalDetailsContent: React.FC<ProposalDetailsContentProps> = pro

{proposalDetails.action === 'SRARC_SetConfig' && (
<>
{hasAlteredDisabledFields(proposalDetails.proposal.configChanges) && (
<Alert
severity="warning"
variant="outlined"
data-testid="proposal-details-disabled-fields-warning"
>
Disabled fields have been altered in this vote proposal.
</Alert>
)}
<DetailItem
label="Proposed Changes"
value={<ConfigValuesChanges changes={proposalDetails.proposal.configChanges} />}
Expand Down