Skip to content

Commit d2f4608

Browse files
committed
feat(onboarding-ui): clarify rename reconnect flow
- Purpose: make the server-rename success flow clearer while the browser reconnects on the new host. - Before: the summary dialog closed immediately into a redirect/reload with no dedicated transition state, and the rename notice used stronger visual emphasis with longer copy. - Problem: users could feel like the page jumped unexpectedly or worry that a reload/sign-in prompt meant onboarding progress was lost. - Change: add a neutral informational alert to the result dialog, show a loading state while waiting for the redirect/reload handoff, and tighten the rename messaging. - How: reuse the existing loading component during post-confirm transition, store a rename follow-up message for the success modal, and extend the summary-step tests to cover the new reconnect UX.
1 parent 0f8c95c commit d2f4608

3 files changed

Lines changed: 106 additions & 18 deletions

File tree

web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,9 @@ vi.mock('@unraid/ui', () => ({
149149
props: ['items', 'type', 'collapsible', 'class'],
150150
template: `<div data-testid="accordion"><template v-for="item in items" :key="item.value"><slot name="trigger" :item="item" :open="false" /><slot name="content" :item="item" :open="false" /></template></div>`,
151151
},
152+
Spinner: {
153+
template: '<div data-testid="spinner" />',
154+
},
152155
}));
153156

154157
vi.mock('@/components/Onboarding/components/OnboardingConsole.vue', () => ({
@@ -247,7 +250,9 @@ const setupApolloMocks = () => {
247250
};
248251

249252
const mountComponent = (props: Record<string, unknown> = {}) => {
250-
const onComplete = vi.fn();
253+
const onComplete =
254+
(props.onComplete as (() => void | Promise<void>) | undefined) ??
255+
vi.fn<() => void | Promise<void>>();
251256
const wrapper = mount(OnboardingSummaryStep, {
252257
props: {
253258
draft: {
@@ -312,6 +317,7 @@ const mountComponent = (props: Record<string, unknown> = {}) => {
312317
vm.showBootDriveWarningDialog ? 'Confirm Drive Wipe' : '',
313318
vm.showApplyResultDialog ? vm.applyResultTitle : '',
314319
vm.showApplyResultDialog ? vm.applyResultMessage : '',
320+
vm.showApplyResultDialog ? (vm.applyResultFollowUpMessage ?? '') : '',
315321
]
316322
.filter(Boolean)
317323
.join(' ');
@@ -327,6 +333,7 @@ interface SummaryVm {
327333
showBootDriveWarningDialog: boolean;
328334
applyResultTitle: string;
329335
applyResultMessage: string;
336+
applyResultFollowUpMessage: string | null;
330337
applyResultSeverity: 'success' | 'warning' | 'error';
331338
handleBootDriveWarningConfirm: () => Promise<void>;
332339
handleBootDriveWarningCancel: () => void;
@@ -1091,6 +1098,12 @@ describe('OnboardingSummaryStep', () => {
10911098
sysModel: undefined,
10921099
});
10931100
expect(onComplete).not.toHaveBeenCalled();
1101+
expect(getSummaryVm(wrapper).applyResultFollowUpMessage).toContain(
1102+
'Your server name has been updated. The page may reload or prompt you to sign in again.'
1103+
);
1104+
expect(wrapper.text()).toContain(
1105+
'Your server name has been updated. The page may reload or prompt you to sign in again.'
1106+
);
10941107

10951108
await clickButtonByText(wrapper, 'OK');
10961109

@@ -1113,6 +1126,50 @@ describe('OnboardingSummaryStep', () => {
11131126
expect(mockLocation.reload).not.toHaveBeenCalled();
11141127
});
11151128

1129+
it('shows a loading state while waiting to reconnect after a successful server rename', async () => {
1130+
draftStore.serverName = 'Newtower';
1131+
updateServerIdentityMock.mockResolvedValue({
1132+
data: {
1133+
updateServerIdentity: {
1134+
id: 'local',
1135+
name: 'Newtower',
1136+
comment: '',
1137+
defaultUrl: 'https://Newtower.local:4443',
1138+
},
1139+
},
1140+
});
1141+
1142+
let resolveOnComplete: (() => void) | undefined;
1143+
const onComplete = vi.fn(
1144+
() =>
1145+
new Promise<void>((resolve) => {
1146+
resolveOnComplete = resolve;
1147+
})
1148+
);
1149+
1150+
const { wrapper } = mountComponent({ onComplete });
1151+
1152+
await clickApply(wrapper);
1153+
1154+
const confirmPromise = getSummaryVm(wrapper).handleApplyResultConfirm();
1155+
await flushPromises();
1156+
1157+
expect(wrapper.find('[data-testid="onboarding-loading-state"]').exists()).toBe(true);
1158+
expect(wrapper.text()).toContain('Refreshing your connection');
1159+
expect(wrapper.text()).toContain(
1160+
'Your server name has been updated. The page may reload or prompt you to sign in again.'
1161+
);
1162+
expect(mockLocation.replace).not.toHaveBeenCalled();
1163+
expect(mockLocation.reload).not.toHaveBeenCalled();
1164+
1165+
resolveOnComplete?.();
1166+
await confirmPromise;
1167+
await flushPromises();
1168+
1169+
expect(onComplete).toHaveBeenCalledTimes(1);
1170+
expect(mockLocation.replace).toHaveBeenCalledWith('https://newtower.local:4443/');
1171+
});
1172+
11161173
it('reloads the current page instead of redirecting when the user is on an IP-based URL', async () => {
11171174
draftStore.serverName = 'Newtower';
11181175
mockLocation.hostname = '192.168.1.2';

web/src/components/Onboarding/steps/OnboardingSummaryStep.vue

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,10 @@ const showBootDriveWarningDialog = ref(false);
188188
const applyResultTitle = ref('');
189189
const applyResultMessage = ref('');
190190
const applyResultSeverity = ref<'success' | 'warning' | 'error'>('success');
191+
const applyResultFollowUpMessage = ref<string | null>(null);
191192
const shouldReloadAfterApplyResult = ref(false);
192193
const redirectUrlAfterApplyResult = ref<string | null>(null);
194+
const isTransitioningAfterApplyResult = ref(false);
193195
const summaryT = (key: string, values?: Record<string, unknown>) =>
194196
t(`onboarding.summaryStep.${key}`, values ?? {});
195197
@@ -608,6 +610,8 @@ const handleComplete = async () => {
608610
isProcessing.value = true;
609611
error.value = null;
610612
logs.value = []; // Clear logs
613+
applyResultFollowUpMessage.value = null;
614+
isTransitioningAfterApplyResult.value = false;
611615
shouldReloadAfterApplyResult.value = false;
612616
redirectUrlAfterApplyResult.value = null;
613617
@@ -694,6 +698,7 @@ const handleComplete = async () => {
694698
shouldRetryNetworkMutations
695699
);
696700
if (serverNameChanged) {
701+
applyResultFollowUpMessage.value = summaryT('result.renameFollowUpMessage');
697702
if (useReturnedDefaultUrlAfterRename) {
698703
const defaultUrl = result?.data?.updateServerIdentity?.defaultUrl;
699704
if (!defaultUrl) {
@@ -1091,22 +1096,27 @@ const handleComplete = async () => {
10911096
10921097
const handleApplyResultConfirm = async () => {
10931098
showApplyResultDialog.value = false;
1094-
await Promise.resolve(props.onComplete());
1095-
10961099
if (!shouldReloadAfterApplyResult.value) {
1100+
await Promise.resolve(props.onComplete());
10971101
return;
10981102
}
10991103
1100-
shouldReloadAfterApplyResult.value = false;
1104+
isTransitioningAfterApplyResult.value = true;
11011105
const redirectUrl = redirectUrlAfterApplyResult.value;
1106+
shouldReloadAfterApplyResult.value = false;
11021107
redirectUrlAfterApplyResult.value = null;
1103-
await nextTick();
1104-
if (redirectUrl) {
1105-
location.replace(redirectUrl);
1106-
return;
1107-
}
1108+
try {
1109+
await Promise.resolve(props.onComplete());
1110+
await nextTick();
1111+
if (redirectUrl) {
1112+
location.replace(redirectUrl);
1113+
return;
1114+
}
11081115
1109-
location.reload();
1116+
location.reload();
1117+
} finally {
1118+
isTransitioningAfterApplyResult.value = false;
1119+
}
11101120
};
11111121
11121122
const handleApplyClick = async () => {
@@ -1136,9 +1146,15 @@ const handleBack = () => {
11361146
<template>
11371147
<div class="mx-auto w-full max-w-4xl px-4 pb-4 md:px-8">
11381148
<OnboardingLoadingState
1139-
v-if="props.isSavingStep"
1140-
:title="t('onboarding.loading.title')"
1141-
:description="t('onboarding.loading.description')"
1149+
v-if="props.isSavingStep || isTransitioningAfterApplyResult"
1150+
:title="
1151+
isTransitioningAfterApplyResult ? summaryT('transition.title') : t('onboarding.loading.title')
1152+
"
1153+
:description="
1154+
isTransitioningAfterApplyResult
1155+
? summaryT('transition.description')
1156+
: t('onboarding.loading.description')
1157+
"
11421158
/>
11431159

11441160
<div v-else class="bg-elevated border-muted rounded-xl border p-6 text-left shadow-sm md:p-10">
@@ -1421,12 +1437,24 @@ const handleBack = () => {
14211437
: 'z-50 max-w-md',
14221438
}"
14231439
>
1424-
<template v-if="showDiagnosticLogsInResultDialog" #body>
1440+
<template v-if="showDiagnosticLogsInResultDialog || applyResultFollowUpMessage" #body>
14251441
<div class="space-y-3">
1426-
<h4 class="text-sm font-semibold tracking-wide uppercase">
1427-
{{ t('onboarding.summaryStep.diagnosticLogs') }}
1428-
</h4>
1429-
<OnboardingConsole :logs="logs" :title="t('onboarding.summaryStep.onboardingDiagnostics')" />
1442+
<UAlert
1443+
v-if="applyResultFollowUpMessage"
1444+
color="neutral"
1445+
variant="subtle"
1446+
:description="applyResultFollowUpMessage"
1447+
icon="i-heroicons-information-circle"
1448+
/>
1449+
<template v-if="showDiagnosticLogsInResultDialog">
1450+
<h4 class="text-sm font-semibold tracking-wide uppercase">
1451+
{{ t('onboarding.summaryStep.diagnosticLogs') }}
1452+
</h4>
1453+
<OnboardingConsole
1454+
:logs="logs"
1455+
:title="t('onboarding.summaryStep.onboardingDiagnostics')"
1456+
/>
1457+
</template>
14301458
</div>
14311459
</template>
14321460
<template #footer>

web/src/locales/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,8 +341,11 @@
341341
"onboarding.summaryStep.result.noChangesMessage": "There were no onboarding updates to apply, so nothing was changed.",
342342
"onboarding.summaryStep.result.successTitle": "Setup Applied",
343343
"onboarding.summaryStep.result.successMessage": "Your onboarding settings were applied successfully.",
344+
"onboarding.summaryStep.result.renameFollowUpMessage": "Your server name has been updated. The page may reload or prompt you to sign in again. Your onboarding progress will not be affected.",
344345
"onboarding.summaryStep.result.failedTitle": "Setup Failed",
345346
"onboarding.summaryStep.result.failedMessage": "An unexpected error interrupted onboarding. Review the logs below and share them with support.",
347+
"onboarding.summaryStep.transition.title": "Refreshing your connection",
348+
"onboarding.summaryStep.transition.description": "Your server name has been updated. The page may reload or prompt you to sign in again. Your onboarding progress will not be affected.",
346349
"onboarding.licenseStep.title": "Unraid OS License",
347350
"onboarding.licenseStep.description": "Ready for activation. Click below to manage your license and server registration in the Unraid Account App.",
348351
"onboarding.licenseStep.status.registered": "Registered",

0 commit comments

Comments
 (0)