|
({ padding: `${t.space.$10} 0`, flex: 1 })}
+ sx={t => ({
+ padding: `${t.space.$10} 0`,
+ flex: 1,
+ gap: t.space.$1,
+ textAlign: 'center',
+ })}
>
-
+
+
|
@@ -373,6 +372,7 @@ const TestResultsTable = ({
rows.map(row => (
setSelectedTestRun(row)}
sx={t => ({
cursor: 'pointer',
@@ -560,6 +560,7 @@ const ParsedUserInfoSection = ({
return (
({
@@ -622,6 +623,7 @@ const FullMessageBlock = ({ message }: { message: string }): JSX.Element => {
localizationKey={localizationKeys('configureSSO.testConfigurationStep.testRunDetails.runDetails.fullMessage')}
/>
{
/>
({
margin: 0,
@@ -682,6 +685,8 @@ const TestRunStatusCell = ({ testRun }: { testRun: EnterpriseConnectionTestRunRe
if (testRun.status === 'success') {
return (
@@ -690,6 +695,8 @@ const TestRunStatusCell = ({ testRun }: { testRun: EnterpriseConnectionTestRunRe
if (testRun.status === 'failed') {
return (
@@ -697,26 +704,26 @@ const TestRunStatusCell = ({ testRun }: { testRun: EnterpriseConnectionTestRunRe
}
return (
);
};
-type CopyTestUrlButtonProps = {
+type OpenTestUrlButtonProps = {
onTestRunCreated?: (testUrl: string) => void;
};
-const CopyTestUrlButton = ({ onTestRunCreated }: CopyTestUrlButtonProps): JSX.Element => {
+const OpenTestUrlButton = ({ onTestRunCreated }: OpenTestUrlButtonProps): JSX.Element => {
const { user } = useUser();
const card = useCardState();
const { enterpriseConnection } = useConfigureSSO();
- const [testUrl, setTestUrl] = useState('');
const [isCreatingTestRun, setIsCreatingTestRun] = useState(false);
- const { onCopy, hasCopied } = useClipboard(testUrl);
- const createTestRun = () => {
+ const openTestRun = () => {
if (!user || !enterpriseConnection) {
return;
}
@@ -726,9 +733,10 @@ const CopyTestUrlButton = ({ onTestRunCreated }: CopyTestUrlButtonProps): JSX.El
user
.createEnterpriseConnectionTestRun(enterpriseConnection.id)
.then(({ url }) => {
- setTestUrl(url);
- onCopy(url);
onTestRunCreated?.(url);
+ // `noopener,noreferrer` so the IdP can't reach back into the dashboard
+ // via `window.opener` once it lands the SAML response.
+ window.open(url, '_blank', 'noopener,noreferrer');
})
.catch(err => handleError(err as Error, [], card.setError))
.finally(() => setIsCreatingTestRun(false));
@@ -736,15 +744,14 @@ const CopyTestUrlButton = ({ onTestRunCreated }: CopyTestUrlButtonProps): JSX.El
return (
);
diff --git a/packages/ui/src/components/ConfigureSSO/steps/TestRunHowToFixSection.tsx b/packages/ui/src/components/ConfigureSSO/steps/TestRunHowToFixSection.tsx
index 1b08d8d12e0..bbfc44fd3d1 100644
--- a/packages/ui/src/components/ConfigureSSO/steps/TestRunHowToFixSection.tsx
+++ b/packages/ui/src/components/ConfigureSSO/steps/TestRunHowToFixSection.tsx
@@ -1,4 +1,4 @@
-import { Box, Flex, Heading, Icon, Link, localizationKeys, Span, Text } from '@/customizables';
+import { Box, descriptors, Flex, Heading, Icon, Link, localizationKeys, Span, Text } from '@/customizables';
import { ArrowRightIcon } from '@/icons';
import type { LocalizationKey } from '../../../localization';
@@ -75,6 +75,7 @@ export const TestRunHowToFixSection = ({ errorCode }: TestRunHowToFixSectionProp
return (
({
@@ -104,6 +105,7 @@ export const TestRunHowToFixSection = ({ errorCode }: TestRunHowToFixSectionProp
{
({ gap: t.space.$3, alignItems: 'center', flex: 1 })}
>
({
width: t.sizes.$8,
@@ -72,6 +74,7 @@ export const VerifyDomainStep = (): JSX.Element => {
sx={t => ({ textAlign: 'center', maxWidth: t.sizes.$94 })}
>
({ fontSize: t.fontSizes.$lg, textWrap: 'balance' })}
localizationKey={localizationKeys('configureSSO.verifyEmailDomainStep.domainTaken.title', {
@@ -79,6 +82,7 @@ export const VerifyDomainStep = (): JSX.Element => {
})}
/>
({
@@ -243,6 +249,8 @@ export const ProvideEmailStep = ({ emailAddressRef, preExistingEmailIdRef }: Pro
})}
>
({
width: t.sizes.$8,
@@ -252,6 +260,8 @@ export const ProvideEmailStep = ({ emailAddressRef, preExistingEmailIdRef }: Pro
/>
({ fontSize: t.fontSizes.$lg, fontWeight: t.fontWeights.$bold })}
localizationKey={localizationKeys('configureSSO.verifyEmailDomainStep.addEmailAddress.formTitle')}
@@ -263,12 +273,16 @@ export const ProvideEmailStep = ({ emailAddressRef, preExistingEmailIdRef }: Pro
sx={{ width: '100%' }}
>
{card.error ? (
({ color: t.colors.$danger500, fontSize: t.fontSizes.$sm, textAlign: 'start' })}
@@ -313,7 +328,6 @@ export const EnterVerificationCodeStep = ({
emailAddressRef: React.MutableRefObject;
}): JSX.Element | null => {
const { user } = useUser();
- const { provider, createEnterpriseConnection } = useConfigureSSO();
const card = useCardState();
const { goNext, goPrev } = useWizard();
const primaryEmailAddress = user?.primaryEmailAddress;
@@ -357,18 +371,6 @@ export const EnterVerificationCodeStep = ({
}
}
- if (!provider) {
- void goNext();
- return;
- }
-
- try {
- await createEnterpriseConnection(provider, emailToVerify);
- } catch (err) {
- handleError(err as Error, [], card.setError);
- return;
- }
-
void goNext();
},
});
@@ -398,11 +400,15 @@ export const EnterVerificationCodeStep = ({
>
({ fontSize: t.fontSizes.$sm })}
localizationKey={localizationKeys('configureSSO.verifyEmailDomainStep.emailCode.formTitle')}
/>
({
width: t.sizes.$8,
@@ -454,6 +464,8 @@ const EmailAlreadyVerified = ({ emailAddress }: { emailAddress: string }): JSX.E
sx={t => ({ textAlign: 'center', maxWidth: t.sizes.$66 })}
>
({ fontSize: t.fontSizes.$lg, fontWeight: t.fontWeights.$bold })}
localizationKey={localizationKeys('configureSSO.verifyEmailDomainStep.emailCode.verified.title')}
@@ -463,6 +475,8 @@ const EmailAlreadyVerified = ({ emailAddress }: { emailAddress: string }): JSX.E
sx={{ flex: 1 }}
>
{
const { wrapper } = await createFixtures();
renderStep(wrapper);
- expect(screen.getByRole('heading', { name: 'Select provider' })).toBeInTheDocument();
- expect(screen.getByText('Select your identity provider')).toBeInTheDocument();
+ expect(screen.getByRole('heading', { name: 'Select your identity provider' })).toBeInTheDocument();
+ expect(screen.getByText(/We.*ll guide you through the detailed setup process next\./)).toBeInTheDocument();
});
it('renders both SAML provider radios with their labels', async () => {
diff --git a/packages/ui/src/customizables/elementDescriptors.ts b/packages/ui/src/customizables/elementDescriptors.ts
index 9f228b833cc..55d3863b094 100644
--- a/packages/ui/src/customizables/elementDescriptors.ts
+++ b/packages/ui/src/customizables/elementDescriptors.ts
@@ -544,11 +544,66 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([
'configureSSOStepperItemLabel',
'configureSSOStepperSeparator',
'configureSSOStep',
+ 'configureSSOStepHeader',
+ 'configureSSOStepHeaderTitle',
+ 'configureSSOStepHeaderDescription',
+ 'configureSSOStepBody',
+ 'configureSSOStepSection',
'configureSSOStepCounter',
'configureSSOFooter',
'configureSSOFooterPreviousButton',
'configureSSOFooterContinueButton',
+ 'configureSSOProviderGroup',
+ 'configureSSOProviderGroupLabel',
+ 'configureSSOProviderGrid',
+ 'configureSSOProviderCard',
+ 'configureSSOProviderCardIcon',
+ 'configureSSOProviderCardLabel',
+
+ 'configureSSOVerifyDomainErrorRoot',
+ 'configureSSOVerifyDomainErrorIcon',
+ 'configureSSOVerifyDomainErrorTitle',
+ 'configureSSOVerifyDomainErrorSubtitle',
+ 'configureSSOEmailVerificationForm',
+ 'configureSSOEmailVerificationIcon',
+ 'configureSSOEmailVerificationTitle',
+ 'configureSSOEmailVerificationSubtitle',
+ 'configureSSOEmailVerificationInput',
+ 'configureSSOEmailVerificationError',
+
+ 'configureSSOInstructionsHeading',
+ 'configureSSOInstructionsList',
+ 'configureSSOInstructionsListItem',
+ 'configureSSOAttributeMappingTable',
+ 'configureSSOAttributeMappingBadge',
+ 'configureSSOCertificateUploadButton',
+ 'configureSSOCertificateFileBadge',
+ 'configureSSOCertificateFileName',
+ 'configureSSOCertificateRemoveButton',
+
+ 'configureSSOTestUrlOpenButton',
+ 'configureSSOTestRefreshButton',
+ 'configureSSOTestResultsTable',
+ 'configureSSOTestResultsRow',
+ 'configureSSOTestResultsEmpty',
+ 'configureSSOTestResultsLoadingSpinner',
+ 'configureSSOTestRunStatusBadge',
+ 'configureSSOTestRunFullMessage',
+ 'configureSSOTestRunFullMessageCopyButton',
+ 'configureSSOTestRunHowToFixSection',
+ 'configureSSOTestRunHowToFixDocsLink',
+ 'configureSSOTestRunParsedUserInfo',
+ 'configureSSOTestError',
+
+ 'configureSSOConfirmationStatusBadge',
+ 'configureSSOConfirmationDomainLink',
+ 'configureSSOConfirmationConfigDetailsLabel',
+ 'configureSSOConfirmationConfigDetailsValue',
+ 'configureSSOConfirmationConfigDetailsLink',
+ 'configureSSOConfirmationReconfigureButton',
+ 'configureSSOConfirmationResetButton',
+
'web3SolanaWalletButtonsRoot',
'web3SolanaWalletButtons',
'web3SolanaWalletButtonsIconButton',
diff --git a/packages/ui/src/hooks/index.ts b/packages/ui/src/hooks/index.ts
index 796773dd1d4..dcbc9e9f64a 100644
--- a/packages/ui/src/hooks/index.ts
+++ b/packages/ui/src/hooks/index.ts
@@ -16,4 +16,5 @@ export * from './usePrefersReducedMotion';
export * from './useSafeState';
export * from './useScrollLock';
export * from './useSearchInput';
+export * from './useSpinDelay';
export * from './useTotalEnabledAuthMethods';
diff --git a/packages/ui/src/hooks/useSpinDelay.ts b/packages/ui/src/hooks/useSpinDelay.ts
new file mode 100644
index 00000000000..00c3b8d4562
--- /dev/null
+++ b/packages/ui/src/hooks/useSpinDelay.ts
@@ -0,0 +1,66 @@
+import { useEffect, useRef } from 'react';
+
+import { useSafeState } from './useSafeState';
+
+type UseSpinDelayOptions = {
+ /**
+ * The amount of time (in ms) to wait before reflecting `value === true`. If `value` flips back
+ * to `false` before this elapses, the flag is never set to `true` (the spinner is skipped).
+ *
+ * @default 0
+ */
+ delay?: number;
+ /**
+ * Once the flag becomes `true`, it stays `true` for at least this long (in ms) even if the
+ * underlying `value` returns to `false` earlier. Prevents the spinner from flickering on fast
+ * operations.
+ *
+ * @default 425
+ */
+ minDuration?: number;
+};
+
+const DEFAULT_DELAY = 0;
+// 425ms is the default used by the dashboard (`apps/dashboard/app/hooks/use-pending-hooks.ts`),
+// chosen by trial-and-error as the threshold above which a spinner feels intentional rather
+// than a flicker.
+const DEFAULT_MIN_DURATION = 425;
+
+/**
+ * Smooths a transient boolean flag (typically a loading/fetching state) so the consumer never
+ * shows a spinner that flickers on and off within a single frame.
+ *
+ * @example
+ * const isFetching = useSomeQuery();
+ * const showSpinner = useSpinDelay(isFetching, { delay: 0, minDuration: 425 });
+ * return showSpinner ? : ;
+ */
+export function useSpinDelay(
+ value: boolean,
+ { delay = DEFAULT_DELAY, minDuration = DEFAULT_MIN_DURATION }: UseSpinDelayOptions = {},
+): boolean {
+ const [displayed, setDisplayed] = useSafeState(false);
+ const shownAtRef = useRef(null);
+
+ useEffect(() => {
+ if (value && !displayed) {
+ const timeout = setTimeout(() => {
+ shownAtRef.current = Date.now();
+ setDisplayed(true);
+ }, delay);
+ return () => clearTimeout(timeout);
+ }
+
+ if (!value && displayed) {
+ const elapsed = shownAtRef.current != null ? Date.now() - shownAtRef.current : minDuration;
+ const remaining = Math.max(0, minDuration - elapsed);
+ const timeout = setTimeout(() => {
+ shownAtRef.current = null;
+ setDisplayed(false);
+ }, remaining);
+ return () => clearTimeout(timeout);
+ }
+ }, [value, displayed, delay, minDuration, setDisplayed]);
+
+ return displayed;
+}
diff --git a/packages/ui/src/internal/appearance.ts b/packages/ui/src/internal/appearance.ts
index e558f3af36c..41a80cdbf8b 100644
--- a/packages/ui/src/internal/appearance.ts
+++ b/packages/ui/src/internal/appearance.ts
@@ -680,11 +680,66 @@ export type ElementsConfig = {
configureSSOStepperItemLabel: WithOptions;
configureSSOStepperSeparator: WithOptions;
configureSSOStep: WithOptions;
+ configureSSOStepHeader: WithOptions;
+ configureSSOStepHeaderTitle: WithOptions;
+ configureSSOStepHeaderDescription: WithOptions;
+ configureSSOStepBody: WithOptions;
+ configureSSOStepSection: WithOptions;
configureSSOStepCounter: WithOptions;
configureSSOFooter: WithOptions;
configureSSOFooterPreviousButton: WithOptions;
configureSSOFooterContinueButton: WithOptions;
+ configureSSOProviderGroup: WithOptions;
+ configureSSOProviderGroupLabel: WithOptions;
+ configureSSOProviderGrid: WithOptions;
+ configureSSOProviderCard: WithOptions;
+ configureSSOProviderCardIcon: WithOptions;
+ configureSSOProviderCardLabel: WithOptions;
+
+ configureSSOVerifyDomainErrorRoot: WithOptions;
+ configureSSOVerifyDomainErrorIcon: WithOptions;
+ configureSSOVerifyDomainErrorTitle: WithOptions;
+ configureSSOVerifyDomainErrorSubtitle: WithOptions;
+ configureSSOEmailVerificationForm: WithOptions;
+ configureSSOEmailVerificationIcon: WithOptions;
+ configureSSOEmailVerificationTitle: WithOptions;
+ configureSSOEmailVerificationSubtitle: WithOptions;
+ configureSSOEmailVerificationInput: WithOptions;
+ configureSSOEmailVerificationError: WithOptions;
+
+ configureSSOInstructionsHeading: WithOptions;
+ configureSSOInstructionsList: WithOptions;
+ configureSSOInstructionsListItem: WithOptions;
+ configureSSOAttributeMappingTable: WithOptions;
+ configureSSOAttributeMappingBadge: WithOptions;
+ configureSSOCertificateUploadButton: WithOptions;
+ configureSSOCertificateFileBadge: WithOptions;
+ configureSSOCertificateFileName: WithOptions;
+ configureSSOCertificateRemoveButton: WithOptions;
+
+ configureSSOTestUrlOpenButton: WithOptions;
+ configureSSOTestRefreshButton: WithOptions;
+ configureSSOTestResultsTable: WithOptions;
+ configureSSOTestResultsRow: WithOptions;
+ configureSSOTestResultsEmpty: WithOptions;
+ configureSSOTestResultsLoadingSpinner: WithOptions;
+ configureSSOTestRunStatusBadge: WithOptions;
+ configureSSOTestRunFullMessage: WithOptions;
+ configureSSOTestRunFullMessageCopyButton: WithOptions;
+ configureSSOTestRunHowToFixSection: WithOptions;
+ configureSSOTestRunHowToFixDocsLink: WithOptions;
+ configureSSOTestRunParsedUserInfo: WithOptions;
+ configureSSOTestError: WithOptions;
+
+ configureSSOConfirmationStatusBadge: WithOptions;
+ configureSSOConfirmationDomainLink: WithOptions;
+ configureSSOConfirmationConfigDetailsLabel: WithOptions;
+ configureSSOConfirmationConfigDetailsValue: WithOptions;
+ configureSSOConfirmationConfigDetailsLink: WithOptions;
+ configureSSOConfirmationReconfigureButton: WithOptions;
+ configureSSOConfirmationResetButton: WithOptions;
+
web3SolanaWalletButtonsRoot: WithOptions;
web3SolanaWalletButtons: WithOptions;
web3SolanaWalletButtonsIconButton: WithOptions;