Skip to content

Commit 1e68b4e

Browse files
committed
Refactor identity provider form to declarative approach
1 parent 5fad0dd commit 1e68b4e

8 files changed

Lines changed: 695 additions & 546 deletions

File tree

packages/localizations/src/en-US.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -535,13 +535,16 @@ export const enUS: LocalizationResource = {
535535
modes: {
536536
title: 'Fill in your Google Workspace application details',
537537
ariaLabel: 'Configuration ',
538-
metadataUpload: 'Add via metadata',
538+
metadataFile: 'Add via metadata',
539539
manual: 'Configure manually',
540540
},
541-
metadataUpload: {
541+
metadataFile: {
542542
label: 'IdP metadata',
543-
uploadFile: 'Upload file',
544543
description: 'In your Google Workspace app, download the IdP metadata and upload it below.',
544+
uploadFile: 'Upload file',
545+
replaceFile: 'Replace file',
546+
removeFile: 'Remove file',
547+
fileUploaded: 'File uploaded',
545548
},
546549
manual: {
547550
description: 'In your Google Workspace app, retrieve these values.',

packages/shared/src/types/localization.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1589,13 +1589,16 @@ export type __internal_LocalizationResource = {
15891589
modes: {
15901590
title: LocalizationValue;
15911591
ariaLabel: LocalizationValue;
1592-
metadataUpload: LocalizationValue;
1592+
metadataFile: LocalizationValue;
15931593
manual: LocalizationValue;
15941594
};
1595-
metadataUpload: {
1595+
metadataFile: {
15961596
label: LocalizationValue;
1597-
uploadFile: LocalizationValue;
15981597
description: LocalizationValue;
1598+
uploadFile: LocalizationValue;
1599+
replaceFile: LocalizationValue;
1600+
removeFile: LocalizationValue;
1601+
fileUploaded: LocalizationValue;
15991602
};
16001603
manual: {
16011604
description: LocalizationValue;

packages/ui/src/components/ConfigureSSO/steps/ConfigureStep/saml/SamlCustomConfigureSteps.tsx

Lines changed: 133 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type JSX } from 'react';
1+
import React, { type JSX } from 'react';
22

33
import { Col, descriptors, Heading, localizationKeys, Text } from '@/customizables';
44
import { ClipboardInput } from '@/elements/ClipboardInput';
@@ -12,8 +12,16 @@ import { Step } from '../../../elements/Step';
1212
import { useWizard, Wizard } from '../../../elements/Wizard';
1313
import { InnerStepCounter } from '../../../elements/Wizard/InnerStepCounter';
1414
import { AttributeMappingTable, type AttributeMappingTableConfig } from './shared/AttributeMappingTable';
15-
import { IdentityProviderMetadataForm } from './shared/IdentityProviderMetadataForm';
16-
import { useIdentityProviderMetadataForm } from './shared/useIdentityProviderMetadataForm';
15+
import {
16+
applySamlSubmitError,
17+
buildSamlConfigurationPayload,
18+
IdentityProviderConfigurationForm,
19+
type IdentityProviderConfigurationFormProps,
20+
} from './shared/IdentityProviderConfigurationForm';
21+
import {
22+
IdentityProviderConfigurationModes,
23+
type IdpConfigurationMode,
24+
} from './shared/IdentityProviderConfigurationModes';
1725

1826
export const SamlCustomConfigureSteps = (): JSX.Element => {
1927
return (
@@ -252,42 +260,113 @@ const SamlCustomAssignUsersStep = (): JSX.Element => {
252260
);
253261
};
254262

263+
const CUSTOM_SAML_IDP_MODES = ['metadataUrl', 'manual'] as const satisfies readonly IdpConfigurationMode[];
264+
255265
const SamlCustomIdentityProviderMetadataStep = (): JSX.Element => {
256266
const card = useCardState();
257267
const { goNext, goPrev, isFirstStep } = useWizard();
258268
const { enterpriseConnection, updateEnterpriseConnection } = useConfigureSSO();
259269

260-
const controller = useIdentityProviderMetadataForm({
261-
metadataUrl: {
262-
label: localizationKeys('configureSSO.configureStep.samlCustom.identityProviderMetadataStep.metadataUrl.label'),
263-
placeholder: localizationKeys(
264-
'configureSSO.configureStep.samlCustom.identityProviderMetadataStep.metadataUrl.placeholder',
265-
),
266-
},
267-
manual: {
268-
signOnUrl: {
269-
label: localizationKeys(
270-
'configureSSO.configureStep.samlCustom.identityProviderMetadataStep.manual.signOnUrl.label',
271-
),
272-
placeholder: localizationKeys(
273-
'configureSSO.configureStep.samlCustom.identityProviderMetadataStep.manual.signOnUrl.placeholder',
274-
),
275-
},
276-
issuer: {
277-
label: localizationKeys(
278-
'configureSSO.configureStep.samlCustom.identityProviderMetadataStep.manual.issuer.label',
279-
),
280-
placeholder: localizationKeys(
281-
'configureSSO.configureStep.samlCustom.identityProviderMetadataStep.manual.issuer.placeholder',
282-
),
283-
},
284-
signingCertificateLabel: localizationKeys(
285-
'configureSSO.configureStep.samlCustom.identityProviderMetadataStep.manual.signingCertificate.label',
286-
),
287-
},
270+
const samlConnection = enterpriseConnection?.samlConnection;
271+
const hasExistingConfig = Boolean(
272+
samlConnection?.idpSsoUrl ||
273+
samlConnection?.idpEntityId ||
274+
samlConnection?.idpCertificate ||
275+
samlConnection?.idpMetadataUrl,
276+
);
277+
const existingCertPresent = Boolean(samlConnection?.idpCertificate);
278+
279+
const [mode, setMode] = React.useState<IdpConfigurationMode>(hasExistingConfig ? 'manual' : 'metadataUrl');
280+
const [certFile, setCertFile] = React.useState<File | null>(null);
281+
282+
const metadataUrlField = useFormControl('idpMetadataUrl', samlConnection?.idpMetadataUrl ?? '', {
283+
type: 'text',
284+
label: localizationKeys('configureSSO.configureStep.samlCustom.identityProviderMetadataStep.metadataUrl.label'),
285+
placeholder: localizationKeys(
286+
'configureSSO.configureStep.samlCustom.identityProviderMetadataStep.metadataUrl.placeholder',
287+
),
288+
isRequired: true,
289+
});
290+
291+
const signOnUrlField = useFormControl('idpSsoUrl', samlConnection?.idpSsoUrl ?? '', {
292+
type: 'text',
293+
label: localizationKeys(
294+
'configureSSO.configureStep.samlCustom.identityProviderMetadataStep.manual.signOnUrl.label',
295+
),
296+
placeholder: localizationKeys(
297+
'configureSSO.configureStep.samlCustom.identityProviderMetadataStep.manual.signOnUrl.placeholder',
298+
),
299+
isRequired: true,
300+
});
301+
302+
const issuerField = useFormControl('idpEntityId', samlConnection?.idpEntityId ?? '', {
303+
type: 'text',
304+
label: localizationKeys('configureSSO.configureStep.samlCustom.identityProviderMetadataStep.manual.issuer.label'),
305+
placeholder: localizationKeys(
306+
'configureSSO.configureStep.samlCustom.identityProviderMetadataStep.manual.issuer.placeholder',
307+
),
308+
isRequired: true,
288309
});
289310

290-
const canSubmit = !card.isLoading && controller.isValid;
311+
const certificateField = useFormControl('idpCertificate', '', {
312+
type: 'text',
313+
label: localizationKeys(
314+
'configureSSO.configureStep.samlCustom.identityProviderMetadataStep.manual.signingCertificate.label',
315+
),
316+
isRequired: true,
317+
});
318+
319+
const trimmedMetadataUrl = metadataUrlField.value.trim();
320+
const trimmedSignOnUrl = signOnUrlField.value.trim();
321+
const trimmedIssuer = issuerField.value.trim();
322+
const hasCert = certFile !== null || existingCertPresent;
323+
324+
const isValid =
325+
mode === 'metadataUrl'
326+
? trimmedMetadataUrl.length > 0
327+
: trimmedSignOnUrl.length > 0 && trimmedIssuer.length > 0 && hasCert;
328+
329+
const canSubmit = isValid && !card.isLoading;
330+
331+
const formProps: IdentityProviderConfigurationFormProps =
332+
mode === 'metadataUrl'
333+
? {
334+
mode: 'metadataUrl',
335+
form: { field: metadataUrlField },
336+
labels: {
337+
description: localizationKeys(
338+
'configureSSO.configureStep.samlCustom.identityProviderMetadataStep.metadataUrl.description',
339+
),
340+
},
341+
}
342+
: {
343+
mode: 'manual',
344+
form: {
345+
signOnUrlField,
346+
issuerField,
347+
certificateField,
348+
certFile,
349+
onCertFileChange: setCertFile,
350+
existingCertPresent,
351+
},
352+
labels: {
353+
description: localizationKeys(
354+
'configureSSO.configureStep.samlCustom.identityProviderMetadataStep.manual.description',
355+
),
356+
uploadFile: localizationKeys(
357+
'configureSSO.configureStep.samlCustom.identityProviderMetadataStep.manual.signingCertificate.uploadFile',
358+
),
359+
replaceFile: localizationKeys(
360+
'configureSSO.configureStep.samlCustom.identityProviderMetadataStep.manual.signingCertificate.replaceFile',
361+
),
362+
removeFile: localizationKeys(
363+
'configureSSO.configureStep.samlCustom.identityProviderMetadataStep.manual.signingCertificate.removeFile',
364+
),
365+
fileUploaded: localizationKeys(
366+
'configureSSO.configureStep.samlCustom.identityProviderMetadataStep.manual.signingCertificate.fileUploaded',
367+
),
368+
},
369+
};
291370

292371
const handleContinue = async (): Promise<void> => {
293372
if (!enterpriseConnection || !canSubmit) {
@@ -298,11 +377,20 @@ const SamlCustomIdentityProviderMetadataStep = (): JSX.Element => {
298377
card.setLoading();
299378

300379
try {
301-
const saml = await controller.buildSamlPayload();
380+
const saml = await buildSamlConfigurationPayload({
381+
mode,
382+
metadataUrl: { value: metadataUrlField.value },
383+
manual: { signOnUrl: signOnUrlField.value, issuer: issuerField.value, certFile },
384+
});
385+
302386
await updateEnterpriseConnection(enterpriseConnection.id, { saml });
303387
void goNext();
304388
} catch (err) {
305-
controller.applySubmitError(err, card);
389+
if (mode === 'metadataUrl') {
390+
applySamlSubmitError(err, card, metadataUrlField);
391+
} else {
392+
applySamlSubmitError(err, card, signOnUrlField, [issuerField, certificateField]);
393+
}
306394
} finally {
307395
card.setIdle();
308396
}
@@ -323,47 +411,26 @@ const SamlCustomIdentityProviderMetadataStep = (): JSX.Element => {
323411
'configureSSO.configureStep.samlCustom.identityProviderMetadataStep.modes.title',
324412
)}
325413
/>
326-
<IdentityProviderMetadataForm
327-
controller={controller}
328-
modes={{
414+
<IdentityProviderConfigurationModes
415+
modes={CUSTOM_SAML_IDP_MODES}
416+
value={mode}
417+
onChange={next => {
418+
card.setError(undefined);
419+
setMode(next);
420+
}}
421+
labels={{
329422
ariaLabel: localizationKeys(
330423
'configureSSO.configureStep.samlCustom.identityProviderMetadataStep.modes.ariaLabel',
331424
),
332-
metadataUrlLabel: localizationKeys(
425+
metadataUrl: localizationKeys(
333426
'configureSSO.configureStep.samlCustom.identityProviderMetadataStep.modes.metadataUrl',
334427
),
335-
manualLabel: localizationKeys(
428+
manual: localizationKeys(
336429
'configureSSO.configureStep.samlCustom.identityProviderMetadataStep.modes.manual',
337430
),
338431
}}
339-
metadataUrl={{
340-
description: localizationKeys(
341-
'configureSSO.configureStep.samlCustom.identityProviderMetadataStep.metadataUrl.description',
342-
),
343-
}}
344-
manual={{
345-
description: localizationKeys(
346-
'configureSSO.configureStep.samlCustom.identityProviderMetadataStep.manual.description',
347-
),
348-
signingCertificate: {
349-
label: localizationKeys(
350-
'configureSSO.configureStep.samlCustom.identityProviderMetadataStep.manual.signingCertificate.label',
351-
),
352-
uploadFile: localizationKeys(
353-
'configureSSO.configureStep.samlCustom.identityProviderMetadataStep.manual.signingCertificate.uploadFile',
354-
),
355-
replaceFile: localizationKeys(
356-
'configureSSO.configureStep.samlCustom.identityProviderMetadataStep.manual.signingCertificate.replaceFile',
357-
),
358-
removeFile: localizationKeys(
359-
'configureSSO.configureStep.samlCustom.identityProviderMetadataStep.manual.signingCertificate.removeFile',
360-
),
361-
fileUploaded: localizationKeys(
362-
'configureSSO.configureStep.samlCustom.identityProviderMetadataStep.manual.signingCertificate.fileUploaded',
363-
),
364-
},
365-
}}
366432
/>
433+
<IdentityProviderConfigurationForm {...formProps} />
367434
</Step.Section>
368435
</Step.Body>
369436

0 commit comments

Comments
 (0)