Skip to content

Commit eb04dff

Browse files
talissoncostaclaude
andcommitted
refactor(saml): keep the highlighted XML editor, pin it with language
Swapping the IdP metadata field to InputGroup lost syntax highlighting, which is the reason SAML used ValueEditor in the first place: an administrator pastes a metadata document and has to be able to read it. Reverted, so SAML keeps the editor it has on main. `onlyOneLang` goes instead. A caller that pins `language` has nothing to switch to, so pinning now hides the format row and the copy control, which is all that boolean ever did. The SAML call site loses one prop rather than gaining one. The InvalidJson story went with it: the warning renders against the active format label, so a pinned story has nowhere to show it. The browser test added in this PR covers that path instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent ea90fdb commit eb04dff

3 files changed

Lines changed: 41 additions & 28 deletions

File tree

frontend/documentation/components/ValueEditor.stories.tsx

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ export const Json: Story = {
5151
render: () => (
5252
<Interactive
5353
label='Value'
54-
language='json'
5554
initialValue='{ "colour": "blue", "size": 12 }'
5655
/>
5756
),
@@ -76,13 +75,6 @@ export const ValueArrivesAfterMount: Story = {
7675
render: () => <LateLoading />,
7776
}
7877

79-
// Invalid JSON surfaces a warning against the active language label.
80-
export const InvalidJson: Story = {
81-
render: () => (
82-
<Interactive label='Value' language='json' initialValue='{ "colour": ' />
83-
),
84-
}
85-
8678
export const CodeMedium: Story = {
8779
render: () => (
8880
<Interactive
@@ -100,6 +92,18 @@ export const Disabled: Story = {
10092
),
10193
}
10294

95+
// A pinned format hides the row, since there is nothing to switch to. This is
96+
// what the SAML IdP metadata field renders.
97+
export const XmlOnly: Story = {
98+
render: () => (
99+
<Interactive
100+
label='IdP metadata XML'
101+
language='xml'
102+
initialValue={'<EntityDescriptor entityID="https://example.com" />'}
103+
/>
104+
),
105+
}
106+
103107
// The multivariate control value carries a weight chip and a tooltip, so it is
104108
// the widest label this component gets. Label and format buttons share one flex
105109
// row, so they compress rather than overlap.

frontend/web/components/ValueEditor/ValueEditor.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@ const ValueEditor: FC<ValueEditorProps> = ({
7676
setLanguage(next)
7777
}
7878

79-
const showControls = !disabled
79+
// A caller that pins the format has nothing to switch, so the row goes, and
80+
// copy goes with it as it always has.
81+
const showControls = !disabled && !languageProp
8082

8183
return (
8284
<div

frontend/web/components/pages/organisation-settings/tabs/sso/saml/modals/CreateSAML.tsx

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import FieldLabel from 'components/base/forms/FieldLabel'
33
import InputGroup from 'components/base/forms/InputGroup'
44
import Utils from 'common/utils/utils'
55
import Switch from 'components/Switch'
6+
import ValueEditor from 'components/ValueEditor'
67
import {
78
useCreateSamlConfigurationMutation,
89
useUpdateSamlConfigurationMutation,
@@ -15,6 +16,8 @@ import { Req } from 'common/types/requests'
1516
import ErrorMessage from 'components/ErrorMessage'
1617
import { getStore } from 'common/store'
1718
import XMLUpload from 'components/XMLUpload'
19+
import { IonIcon } from '@ionic/react'
20+
import { cloudDownloadOutline } from 'ionicons/icons'
1821
import Tabs from 'components/navigation/TabMenu/Tabs'
1922
import TabItem from 'components/navigation/TabMenu/TabItem'
2023
import { AttributeName } from 'common/types/responses'
@@ -160,17 +163,30 @@ const CreateSAML: FC<CreateSAML> = ({ organisationId, samlName }) => {
160163
(data &&
161164
((data.name && !data.idp_metadata_xml) ||
162165
data.idp_metadata_xml))) && (
163-
<InputGroup
164-
title='IdP metadata XML'
165-
textarea
166-
className='full-width mb-0'
167-
data-test='idpMetadataXml'
168-
inputProps={{ name: 'idpMetadataXml' }}
169-
value={metadataXml || data?.idp_metadata_xml || ''}
170-
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) =>
171-
setMetadataXml(Utils.safeParseEventValue(event))
166+
<ValueEditor
167+
label='IdP metadata XML'
168+
labelAfter={
169+
data?.idp_metadata_xml && (
170+
<div className='clickable' onClick={downloadIDPMetadata}>
171+
<Tooltip
172+
title={
173+
<IonIcon
174+
className='icon-action'
175+
icon={cloudDownloadOutline}
176+
style={{ fontSize: '18px' }}
177+
/>
178+
}
179+
place='right'
180+
>
181+
Download IDP Metadata
182+
</Tooltip>
183+
</div>
184+
)
172185
}
173-
placeholder="e.g. '<xml>time<xml>' "
186+
className='full-width'
187+
value={metadataXml || data?.idp_metadata_xml}
188+
onChange={setMetadataXml}
189+
language='xml'
174190
/>
175191
)}
176192
<Row className='or-divider my-1'>
@@ -204,15 +220,6 @@ const CreateSAML: FC<CreateSAML> = ({ organisationId, samlName }) => {
204220
</div>
205221
)}
206222
<div className='text-right py-2'>
207-
{!!data?.idp_metadata_xml && (
208-
<Button
209-
theme='secondary'
210-
onClick={downloadIDPMetadata}
211-
className='mr-2'
212-
>
213-
Download IdP Metadata
214-
</Button>
215-
)}
216223
{isEdit && (
217224
<Button
218225
disabled={isLoading}

0 commit comments

Comments
 (0)