// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import './critical_error_page.js';
import './hardware_error_page.js';
import './onboarding_choose_destination_page.js';
import './onboarding_choose_wipe_device_page.js';
import './onboarding_choose_wp_disable_method_page.js';
import './onboarding_enter_rsu_wp_disable_code_page.js';
import './onboarding_landing_page.js';
import './onboarding_network_page.js';
import './onboarding_select_components_page.js';
import './onboarding_update_page.js';
import './onboarding_wait_for_manual_wp_disable_page.js';
import './onboarding_wp_disable_complete_page.js';
import './reboot_page.js';
import './reimaging_calibration_failed_page.js';
import './reimaging_calibration_run_page.js';
import './reimaging_calibration_setup_page.js';
import './reimaging_device_information_page.js';
import './reimaging_firmware_update_page.js';
import './reimaging_provisioning_page.js';
import './shimless_3p_diagnostics.js';
import './shimless_rma_shared.css.js';
import './splash_screen.js';
import './wrapup_finalize_page.js';
import './wrapup_repair_complete_page.js';
import './wrapup_restock_page.js';
import './wrapup_wait_for_manual_wp_enable_page.js';
import 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
import {CrDialogElement} from 'chrome://resources/ash/common/cr_elements/cr_dialog/cr_dialog.js';
import {I18nMixin} from 'chrome://resources/ash/common/cr_elements/i18n_mixin.js';
import {assert} from 'chrome://resources/js/assert.js';
import {FilePath} from 'chrome://resources/mojo/mojo/public/mojom/base/file_path.mojom-webui.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {CriticalErrorPage} from './critical_error_page.js';
import {CLICK_EXIT_BUTTON, CLICK_NEXT_BUTTON, DISABLE_ALL_BUTTONS, DISABLE_NEXT_BUTTON, DisableAllButtonsEvent, DisableNextButtonEvent, ENABLE_ALL_BUTTONS, FATAL_HARDWARE_ERROR, FatalHardwareEvent, OPEN_LOGS_DIALOG, SET_NEXT_BUTTON_LABEL, SetNextButtonLabelEvent, TRANSITION_STATE, TransitionStateEvent} from './events.js';
import {HardwareErrorPage} from './hardware_error_page.js';
import {getShimlessRmaService} from './mojo_interface_provider.js';
import {OnboardingChooseDestinationPageElement} from './onboarding_choose_destination_page.js';
import {OnboardingChooseWipeDevicePage} from './onboarding_choose_wipe_device_page.js';
import {OnboardingChooseWpDisableMethodPage} from './onboarding_choose_wp_disable_method_page.js';
import {OnboardingEnterRsuWpDisableCodePage} from './onboarding_enter_rsu_wp_disable_code_page.js';
import {OnboardingLandingPage} from './onboarding_landing_page.js';
import {OnboardingNetworkPage} from './onboarding_network_page.js';
import {OnboardingSelectComponentsPageElement} from './onboarding_select_components_page.js';
import {OnboardingUpdatePageElement} from './onboarding_update_page.js';
import {OnboardingWaitForManualWpDisablePage} from './onboarding_wait_for_manual_wp_disable_page.js';
import {OnboardingWpDisableCompletePage} from './onboarding_wp_disable_complete_page.js';
import {RebootPage} from './reboot_page.js';
import {ReimagingCalibrationFailedPage} from './reimaging_calibration_failed_page.js';
import {ReimagingCalibrationRunPage} from './reimaging_calibration_run_page.js';
import {ReimagingCalibrationSetupPage} from './reimaging_calibration_setup_page.js';
import {ReimagingDeviceInformationPage} from './reimaging_device_information_page.js';
import {UpdateRoFirmwarePage} from './reimaging_firmware_update_page.js';
import {ReimagingProvisioningPage} from './reimaging_provisioning_page.js';
import {Shimless3pDiagnostics} from './shimless_3p_diagnostics.js';
import {getTemplate} from './shimless_rma.html.js';
import {ErrorObserverReceiver, ExternalDiskStateObserverReceiver, RmadErrorCode, ShimlessRmaServiceInterface, State, StateResult} from './shimless_rma.mojom-webui.js';
import {SplashScreen} from './splash_screen.js';
import {WrapupFinalizePage} from './wrapup_finalize_page.js';
import {WrapupRepairCompletePage} from './wrapup_repair_complete_page.js';
import {WrapupRestockPage} from './wrapup_restock_page.js';
import {WrapupWaitForManualWpEnablePage} from './wrapup_wait_for_manual_wp_enable_page.js';
declare global {
interface WindowEventMap {
[TRANSITION_STATE]: TransitionStateEvent;
[DISABLE_NEXT_BUTTON]: DisableNextButtonEvent;
[FATAL_HARDWARE_ERROR]: FatalHardwareEvent;
[DISABLE_ALL_BUTTONS]: DisableAllButtonsEvent;
[DISABLE_NEXT_BUTTON]: DisableNextButtonEvent;
[SET_NEXT_BUTTON_LABEL]: SetNextButtonLabelEvent;
}
}
export interface SaveLogResponse {
savePath: FilePath;
error: RmadErrorCode;
}
/**
* Enum for the state of USB used for saving logs. The states are transitioned
* through as the user plugs in a USB then attempts to save the log.
*/
enum UsbLogState {
USB_UNPLUGGED = 0,
USB_READY = 1,
SAVING_LOGS = 2,
LOG_SAVE_SUCCESS = 3,
LOG_SAVE_FAIL = 4,
}
// TODO(b/315002705): Replace this type with a mapped type that can infer
// which shimless custom element is returned by `loadComponent`.
export type ShimlessCustomElementType = HTMLElement&{
confirmExitButtonClicked?: boolean,
hidden?: boolean,
errorCode?: RmadErrorCode,
getStartedButtonClicked?: boolean,
allButtonsDisabled?: boolean,
onNextButtonClick?: () => Promise<{stateResult: StateResult}>,
onExitButtonClick?: () => Promise<{stateResult: StateResult}>,
};
/**
* The starting USB state for the logs dialog.
*/
const DEFAULT_USB_LOG_STATE: UsbLogState = UsbLogState.USB_READY;
/**
* Enum for button states.
*/
export enum ButtonState {
VISIBLE = 'visible',
DISABLED = 'disabled',
HIDDEN = 'hidden',
}
const HEADER_FOOTER_HEIGHT_PX = 80;
const OOBE_LARGE_SCREEN_WIDTH_PX = 80;
interface PageInfo {
componentIs: string;
requiresReloadWhenShown?: boolean;
buttonNext: ButtonState;
buttonNextLabelKey?: string|null;
buttonExitLabelKey?: string|null;
buttonExit: ButtonState;
buttonBack: ButtonState;
}
export const StateComponentMapping: {[key in State]: PageInfo} = {
// It is assumed that if state is kUnknown the error is kRmaNotRequired.
[State.kUnknown]: {
componentIs: CriticalErrorPage.is,
requiresReloadWhenShown: false,
buttonNext: ButtonState.HIDDEN,
buttonExit: ButtonState.HIDDEN,
buttonBack: ButtonState.HIDDEN,
},
[State.kWelcomeScreen]: {
componentIs: OnboardingLandingPage.is,
requiresReloadWhenShown: false,
buttonNext: ButtonState.HIDDEN,
buttonNextLabelKey: 'getStartedButtonLabel',
buttonExit: ButtonState.HIDDEN,
buttonBack: ButtonState.HIDDEN,
},
[State.kConfigureNetwork]: {
componentIs: OnboardingNetworkPage.is,
requiresReloadWhenShown: false,
buttonNext: ButtonState.DISABLED,
buttonNextLabelKey: 'skipButtonLabel',
buttonExit: ButtonState.VISIBLE,
buttonBack: ButtonState.VISIBLE,
},
[State.kUpdateOs]: {
componentIs: OnboardingUpdatePageElement.is,
requiresReloadWhenShown: false,
buttonNext: ButtonState.DISABLED,
buttonNextLabelKey: 'skipButtonLabel',
buttonExit: ButtonState.VISIBLE,
buttonBack: ButtonState.VISIBLE,
},
[State.kSelectComponents]: {
componentIs: OnboardingSelectComponentsPageElement.is,
requiresReloadWhenShown: true,
buttonNext: ButtonState.DISABLED,
buttonExit: ButtonState.VISIBLE,
buttonBack: ButtonState.VISIBLE,
},
[State.kChooseDestination]: {
componentIs: OnboardingChooseDestinationPageElement.is,
requiresReloadWhenShown: false,
buttonNext: ButtonState.DISABLED,
buttonExit: ButtonState.VISIBLE,
buttonBack: ButtonState.VISIBLE,
},
[State.kChooseWipeDevice]: {
componentIs: OnboardingChooseWipeDevicePage.is,
requiresReloadWhenShown: false,
buttonNext: ButtonState.DISABLED,
buttonExit: ButtonState.VISIBLE,
buttonBack: ButtonState.VISIBLE,
},
[State.kChooseWriteProtectDisableMethod]: {
componentIs:
OnboardingChooseWpDisableMethodPage.is,
requiresReloadWhenShown: false,
buttonNext: ButtonState.DISABLED,
buttonExit: ButtonState.VISIBLE,
buttonBack: ButtonState.VISIBLE,
},
[State.kEnterRSUWPDisableCode]: {
componentIs:
OnboardingEnterRsuWpDisableCodePage.is,
requiresReloadWhenShown: true,
buttonNext: ButtonState.DISABLED,
buttonExit: ButtonState.VISIBLE,
buttonBack: ButtonState.VISIBLE,
},
[State.kWaitForManualWPDisable]: {
componentIs:
OnboardingWaitForManualWpDisablePage.is,
requiresReloadWhenShown: true,
buttonNext: ButtonState.HIDDEN,
buttonExit: ButtonState.VISIBLE,
buttonBack: ButtonState.VISIBLE,
},
[State.kWPDisableComplete]: {
componentIs: OnboardingWpDisableCompletePage.is,
requiresReloadWhenShown: false,
buttonNext: ButtonState.DISABLED,
buttonExit: ButtonState.VISIBLE,
buttonBack: ButtonState.HIDDEN,
},
[State.kUpdateRoFirmware]: {
componentIs: UpdateRoFirmwarePage.is,
requiresReloadWhenShown: false,
buttonNext: ButtonState.HIDDEN,
buttonExit: ButtonState.HIDDEN,
buttonBack: ButtonState.HIDDEN,
},
[State.kUpdateDeviceInformation]: {
componentIs: ReimagingDeviceInformationPage.is,
requiresReloadWhenShown: false,
buttonNext: ButtonState.DISABLED,
buttonExit: ButtonState.HIDDEN,
buttonBack: ButtonState.HIDDEN,
},
[State.kCheckCalibration]: {
componentIs: ReimagingCalibrationFailedPage.is,
requiresReloadWhenShown: true,
buttonNext: ButtonState.DISABLED,
buttonExitLabelKey: 'calibrationFailedSkipCalibrationButtonLabel',
buttonExit: ButtonState.VISIBLE,
buttonBack: ButtonState.VISIBLE,
},
[State.kRunCalibration]: {
componentIs: ReimagingCalibrationRunPage.is,
requiresReloadWhenShown: true,
buttonNext: ButtonState.DISABLED,
buttonExit: ButtonState.HIDDEN,
buttonBack: ButtonState.HIDDEN,
},
[State.kSetupCalibration]: {
componentIs: ReimagingCalibrationSetupPage.is,
requiresReloadWhenShown: true,
buttonNext: ButtonState.DISABLED,
buttonExit: ButtonState.HIDDEN,
buttonBack: ButtonState.VISIBLE,
},
[State.kProvisionDevice]: {
componentIs: ReimagingProvisioningPage.is,
requiresReloadWhenShown: true,
buttonNext: ButtonState.HIDDEN,
buttonExit: ButtonState.HIDDEN,
buttonBack: ButtonState.HIDDEN,
},
[State.kWaitForManualWPEnable]: {
componentIs:
WrapupWaitForManualWpEnablePage.is,
requiresReloadWhenShown: true,
buttonNext: ButtonState.HIDDEN,
buttonExit: ButtonState.HIDDEN,
buttonBack: ButtonState.HIDDEN,
},
[State.kRestock]: {
componentIs: WrapupRestockPage.is,
requiresReloadWhenShown: false,
buttonNext: ButtonState.HIDDEN,
buttonExit: ButtonState.HIDDEN,
buttonBack: ButtonState.VISIBLE,
},
[State.kFinalize]: {
componentIs: WrapupFinalizePage.is,
buttonNext: ButtonState.HIDDEN,
buttonExit: ButtonState.HIDDEN,
buttonBack: ButtonState.HIDDEN,
},
[State.kRepairComplete]: {
componentIs: WrapupRepairCompletePage.is,
requiresReloadWhenShown: false,
buttonNext: ButtonState.HIDDEN,
buttonExit: ButtonState.HIDDEN,
buttonBack: ButtonState.HIDDEN,
},
[State.kHardwareError]: {
componentIs: HardwareErrorPage.is,
requiresReloadWhenShown: false,
buttonNext: ButtonState.HIDDEN,
buttonExit: ButtonState.HIDDEN,
buttonBack: ButtonState.HIDDEN,
},
[State.kReboot]: {
componentIs: RebootPage.is,
requiresReloadWhenShown: false,
buttonNext: ButtonState.HIDDEN,
buttonExit: ButtonState.HIDDEN,
buttonBack: ButtonState.HIDDEN,
},
};
/**
* @fileoverview
* 'shimless-rma' is the main page for the shimless rma process modal dialog.
*/
const ShimlessRmaBase = I18nMixin(PolymerElement);
export class ShimlessRma extends ShimlessRmaBase {
static get is() {
return 'shimless-rma' as const;
}
static get template() {
return getTemplate();
}
static get properties() {
return {
/**
* Current PageInfo based on current state
*/
currentPage: {
reflectToAttribute: true,
type: Object,
value: {
componentIs: SplashScreen.is,
requiresReloadWhenShown: false,
buttonNext: ButtonState.HIDDEN,
buttonExit: ButtonState.HIDDEN,
buttonBack: ButtonState.HIDDEN,
},
},
shimlessRmaService: {
type: Object,
value: {},
},
/**
* Used to disable all buttons while waiting for long running mojo API
* calls to complete. Also controls the busy state overlay.
*/
allButtonsDisabled: {
type: Boolean,
value: true,
reflectToAttribute: true,
},
/**
* Show busy state overlay while waiting for the service response.
*/
showBusyStateOverlay: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
/**
* After the next button is clicked, true until the next state is
* processed.
*/
nextButtonClicked: {
type: Boolean,
value: false,
},
/**
* After the back button is clicked, true until the next state is
* processed.
*/
backButtonClicked: {
type: Boolean,
value: false,
},
/**
* After the exit button is clicked, true until the next state is
* processed.
*/
confirmExitButtonClicked: {
type: Boolean,
value: false,
},
log: {
type: String,
value: '',
},
/**
* Tracks the current status of the USB and log saving.
*/
usbLogState: {
type: Number,
value: DEFAULT_USB_LOG_STATE,
},
logSavedStatusText: {
type: String,
value: '',
},
};
}
protected currentPage: PageInfo;
protected allButtonsDisabled: boolean;
protected showBusyStateOverlay: boolean;
protected nextButtonClicked: boolean;
protected backButtonClicked: boolean;
protected confirmExitButtonClicked: boolean;
protected log: string;
protected usbLogState: UsbLogState;
protected logSavedStatusText: string;
shimlessRmaService: ShimlessRmaServiceInterface = getShimlessRmaService();
errorObserverReceiver: ErrorObserverReceiver;
externalDiskStateReceiver: ExternalDiskStateObserverReceiver;
transitionState: (e: TransitionStateEvent) => void;
disableNextButtonCallback: (e: DisableNextButtonEvent) => void;
enableAllButtonsCallback: () => void;
disableAllButtonsCallback: (e: DisableAllButtonsEvent) => void;
exitButtonCallback: () => void;
nextButtonCallback: () => void;
setNextButtonLabelCallback: (e: SetNextButtonLabelEvent) => void;
fatalHardwareErrorCallback: (e: FatalHardwareEvent) => void;
openLogsDialogCallback: () => void;
onKeyDownCallback: (e: KeyboardEvent) => void;
constructor() {
super();
this.errorObserverReceiver = new ErrorObserverReceiver(this);
this.shimlessRmaService.observeError(
this.errorObserverReceiver.$.bindNewPipeAndPassRemote());
this.externalDiskStateReceiver =
new ExternalDiskStateObserverReceiver(this);
this.shimlessRmaService.observeExternalDiskState(
this.externalDiskStateReceiver.$.bindNewPipeAndPassRemote());
/**
* transitionState is used by page elements to trigger state transition
* functions and switching to the next page without using the 'Next' button.
*/
this.transitionState = (e: TransitionStateEvent): void => {
this.setAllButtonsState(
/* shouldDisableButtons= */ true, /* showBusyStateOverlay= */ true);
e.detail().then((stateResult) => this.processStateResult(stateResult));
};
/**
* The disableNextButton callback is used by page elements to control the
* disabled state of the 'Next' button.
*/
this.disableNextButtonCallback = (e: DisableNextButtonEvent): void => {
this.currentPage.buttonNext =
e.detail ? ButtonState.DISABLED : ButtonState.VISIBLE;
// Allow polymer to observe the changed state.
this.notifyPath('currentPage.buttonNext');
};
/**
* The enableAllButtons callback is used by page elements to enable all
* buttons.
*/
this.enableAllButtonsCallback = (): void => {
this.setAllButtonsState(
/* shouldDisableButtons= */ false, /* showBusyStateOverlay= */ false);
};
/**
* The disableAllButtons callback is used by page elements to disable all
* buttons and optionally show a busy overlay.
*/
this.disableAllButtonsCallback = (e: DisableAllButtonsEvent): void => {
this.setAllButtonsState(
/* shouldDisableButtons= */ true, e.detail.showBusyStateOverlay);
};
/**
* The exitButtonCallback callback is used by the landing page to create
* its own Exit button in the left pane.
*/
this.exitButtonCallback = (): void => {
this.onExitButtonClicked();
};
/**
* The nextButtonCallback callback is used by the landing page to simulate
* the next button being clicked.
*/
this.nextButtonCallback = (): void => {
this.onNextButtonClicked();
};
/**
* The setNextButtonLabelCallback callback is used by page elements to set
* the text label for the 'Next' button.
*/
this.setNextButtonLabelCallback = (e: SetNextButtonLabelEvent): void => {
this.currentPage.buttonNextLabelKey = e.detail;
this.notifyPath('currentPage.buttonNextLabelKey');
};
/**
* The fatalHardwareErrorCallback callback is used by the finalization
* page and the provisioning page to tell the app that there is a fatal
* hardware error.
*/
this.fatalHardwareErrorCallback = (event: FatalHardwareEvent): void => {
const errorState = {
stateResult: {
state: State.kHardwareError,
canExit: false,
canGoBack: false,
error: event.detail.fatalErrorCode,
},
};
this.showState(errorState);
};
/**
* Opens the logs dialog.
*/
this.openLogsDialogCallback = (): void => {
this.openLogsDialog();
};
this.onKeyDownCallback = (event: KeyboardEvent): void => {
this.handleKeyboardShortcut(event);
};
}
override connectedCallback() {
super.connectedCallback();
window.addEventListener(TRANSITION_STATE, this.transitionState);
window.addEventListener(
DISABLE_NEXT_BUTTON, this.disableNextButtonCallback);
window.addEventListener(
SET_NEXT_BUTTON_LABEL, this.setNextButtonLabelCallback);
window.addEventListener(
DISABLE_ALL_BUTTONS, this.disableAllButtonsCallback);
window.addEventListener(ENABLE_ALL_BUTTONS, this.enableAllButtonsCallback);
window.addEventListener(CLICK_EXIT_BUTTON, this.exitButtonCallback);
window.addEventListener(CLICK_NEXT_BUTTON, this.nextButtonCallback);
window.addEventListener(
FATAL_HARDWARE_ERROR, this.fatalHardwareErrorCallback);
window.addEventListener(OPEN_LOGS_DIALOG, this.openLogsDialogCallback);
window.addEventListener('keydown', this.onKeyDownCallback);
}
override disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener(TRANSITION_STATE, this.transitionState);
window.removeEventListener(
DISABLE_NEXT_BUTTON, this.disableNextButtonCallback);
window.removeEventListener(
SET_NEXT_BUTTON_LABEL, this.setNextButtonLabelCallback);
window.removeEventListener(
DISABLE_ALL_BUTTONS, this.disableAllButtonsCallback);
window.removeEventListener(
ENABLE_ALL_BUTTONS, this.enableAllButtonsCallback);
window.removeEventListener(CLICK_EXIT_BUTTON, this.exitButtonCallback);
window.removeEventListener(CLICK_NEXT_BUTTON, this.nextButtonCallback);
window.removeEventListener(
FATAL_HARDWARE_ERROR, this.fatalHardwareErrorCallback);
window.removeEventListener(OPEN_LOGS_DIALOG, this.openLogsDialogCallback);
window.removeEventListener('keydown', this.onKeyDownCallback);
}
override ready() {
super.ready();
this.style.setProperty(
'--header-footer-height', `${HEADER_FOOTER_HEIGHT_PX}px`);
const screenWidth = window.innerWidth;
// TODO(b/315002705): Replace integers with variables for
// `containerHorizontalPadding` calculation.
const containerHorizontalPadding =
screenWidth > OOBE_LARGE_SCREEN_WIDTH_PX ? ((screenWidth - 1040) / 2) :
(screenWidth * .08);
this.style.setProperty(
'--container-horizontal-padding', `${containerHorizontalPadding}px`);
const contentContainerWidth =
screenWidth - (containerHorizontalPadding * 2);
this.style.setProperty(
'--content-container-width', `${contentContainerWidth}px`);
const screenHeight = window.innerHeight;
const containerVerticalPadding = screenHeight * .06;
this.style.setProperty(
'--container-vertical-padding', `${containerVerticalPadding}px`);
const contentContainerHeight = screenHeight -
(containerVerticalPadding * 2) - (HEADER_FOOTER_HEIGHT_PX * 2);
this.style.setProperty(
'--content-container-height', `${contentContainerHeight}px`);
const splashComponent = this.loadComponent(this.currentPage.componentIs);
splashComponent.hidden = false;
// Get the initial state.
this.shimlessRmaService.getCurrentState().then(
(stateResult: {stateResult: StateResult}) => {
this.processStateResult(stateResult);
});
}
private processStateResult(stateResult: {stateResult: StateResult}): void {
// Do not show the state screen if the critical error screen was shown.
if (this.handleStandardAndCriticalError(stateResult.stateResult.error)) {
return;
}
// This is a special case for showing the reboot page when the platform
// sends the error code for expecting a reboot or a shut down.
if (stateResult.stateResult.error === RmadErrorCode.kExpectReboot ||
stateResult.stateResult.error === RmadErrorCode.kExpectShutdown) {
const rebootState = {
stateResult: {
state: State.kReboot,
canExit: false,
canGoBack: false,
error: stateResult.stateResult.error,
},
};
this.showState(rebootState);
return;
}
this.showState(stateResult);
}
onError(error: RmadErrorCode): void {
this.handleStandardAndCriticalError(error);
}
/**
* Returns true if the critical error screen was displayed.
*/
private handleStandardAndCriticalError(error: RmadErrorCode): boolean {
// Critical error - expected to be in RMA.
if (error === RmadErrorCode.kRmaNotRequired) {
const errorState = {
stateResult: {
state: State.kUnknown,
canExit: false,
canGoBack: false,
error: RmadErrorCode.kRmaNotRequired,
},
};
this.showState(errorState);
return true;
}
return false;
}
private showState({stateResult}: {stateResult: StateResult}): void {
// Reset clicked variables to hide the spinners.
this.nextButtonClicked = false;
this.backButtonClicked = false;
this.confirmExitButtonClicked = false;
const nextStatePageInfo: PageInfo =
StateComponentMapping[stateResult.state];
assert(nextStatePageInfo);
if (this.currentPage.requiresReloadWhenShown) {
this.removeComponent(this.currentPage.componentIs);
}
// Only perform the below actions if the page needs to change or reload.
const shouldLoadNextPage = this.currentPage !== nextStatePageInfo ||
this.currentPage.requiresReloadWhenShown;
if (shouldLoadNextPage) {
this.hideAllComponents();
// Set the next page as the current page.
this.currentPage = nextStatePageInfo;
if (!stateResult.canExit) {
// The calibration failed page is a special case because the Exit button
// is used as the Skip Calibration button. So we don't want to
// acknowledge `canExit` here.
if (this.currentPage.componentIs !== ReimagingCalibrationFailedPage.is) {
this.currentPage.buttonExit = ButtonState.HIDDEN;
}
}
if (!stateResult.canGoBack) {
this.currentPage.buttonBack = ButtonState.HIDDEN;
}
// Load the next page so it's visible.
const currentPageComponent =
this.loadComponent(this.currentPage.componentIs);
currentPageComponent.hidden = false;
currentPageComponent.errorCode = stateResult.error;
this.notifyPath('currentPage.buttonNext');
this.notifyPath('currentPage.buttonExit');
this.notifyPath('currentPage.buttonBack');
// A special case for the landing page, which has its own navigation
// buttons.
currentPageComponent.getStartedButtonClicked = false;
currentPageComponent.confirmExitButtonClicked = false;
}
this.setAllButtonsState(
/* shouldDisableButtons= */ false, /* showBusyStateOverlay= */ false);
}
/**
* Utility method to bulk hide all contents.
*/
hideAllComponents(): void {
const components = this.shadowRoot!.querySelectorAll('.shimless-content');
Array.from(components).map((c) => (c as HTMLElement).hidden = true);
}
private removeComponent(componentIs: string): void {
const currentPageComponent =
this.shadowRoot!.querySelector(`#${componentIs}`);
assert(!!currentPageComponent);
currentPageComponent.remove();
}
private loadComponent(componentIs: string): ShimlessCustomElementType {
const alreadyLoadedComponent =
this.shadowRoot!.querySelector<ShimlessCustomElementType>(
`#${componentIs}`);
if (alreadyLoadedComponent) {
return alreadyLoadedComponent;
}
const shimlessBody = this.shadowRoot!.querySelector('#contentContainer');
assert(shimlessBody);
const component =
document.createElement(componentIs) as ShimlessCustomElementType;
component.setAttribute('id', componentIs);
component.setAttribute('class', 'shimless-content');
component.hidden = true;
shimlessBody.appendChild(component);
return component;
}
protected isButtonHidden(button: ButtonState): boolean {
return button === ButtonState.HIDDEN;
}
protected isButtonDisabled(button: ButtonState): boolean {
return (button === ButtonState.DISABLED) || this.allButtonsDisabled;
}
protected setAllButtonsState(
shouldDisableButtons: boolean, showBusyStateOverlay: boolean): void {
// `showBusyStateOverlay` should only be true when disabling all buttons.
assert(!showBusyStateOverlay || shouldDisableButtons);
this.allButtonsDisabled = shouldDisableButtons;
this.showBusyStateOverlay = showBusyStateOverlay;
const component = this.loadComponent(this.currentPage.componentIs);
if (!component) {
return;
}
component!.allButtonsDisabled = this.allButtonsDisabled;
}
updateButtonState(buttonName: string, buttonState: ButtonState): void {
assert(this.currentPage.hasOwnProperty(buttonName));
this.set(`currentPage.${buttonName}`, buttonState);
}
protected onBackButtonClicked(): void {
this.backButtonClicked = true;
this.setAllButtonsState(
/* shouldDisableButtons= */ true, /* showBusyStateOverlay= */ true);
this.shimlessRmaService.transitionPreviousState().then(
(stateResult: {stateResult: StateResult}) =>
this.processStateResult(stateResult));
}
protected onNextButtonClicked(): void {
const page = this.loadComponent(this.currentPage.componentIs);
assert(page, 'Could not find page ' + this.currentPage.componentIs);
assert(
page!.onNextButtonClick,
'No onNextButtonClick for ' + this.currentPage.componentIs);
assert(
typeof page!.onNextButtonClick === 'function',
'onNextButtonClick not a function for ' + this.currentPage.componentIs);
this.nextButtonClicked = true;
this.setAllButtonsState(
/* shouldDisableButtons= */ true, /* showBusyStateOverlay= */ true);
page!.onNextButtonClick()
.then((stateResult) => {
this.processStateResult(stateResult);
})
.catch((_err: Error) => {
this.nextButtonClicked = false;
this.setAllButtonsState(
/* shouldDisableButtons= */ false,
/* showBusyStateOverlay= */ false);
});
}
protected onExitButtonClicked(): void {
const page = this.loadComponent(this.currentPage.componentIs);
assert(page);
// Don't show the exit dialog if it's on calibration failed page.
if (page.onExitButtonClick) {
// A special case for the calibration failed page, where the skip button
// replaces the exit button.
page.onExitButtonClick()
.then((stateResult) => {
this.processStateResult(stateResult);
})
.catch((_err: Error) => {
this.confirmExitButtonClicked = false;
this.setAllButtonsState(
/* shouldDisableButtons= */ false,
/* showBusyStateOverlay= */ false);
});
} else {
const dialog: CrDialogElement|null =
this.shadowRoot!.querySelector('#exitDialog');
assert(dialog);
dialog.showModal();
}
}
protected onConfirmExitButtonClicked(): void {
this.confirmExitButtonClicked = true;
this.closeDialog();
// Show exit button spinner on the landing page
const currentPageComponent =
this.loadComponent(this.currentPage.componentIs);
currentPageComponent!.confirmExitButtonClicked = true;
this.setAllButtonsState(
/* shouldDisableButtons= */ true, /* showBusyStateOverlay= */ true);
this.shimlessRmaService.abortRma().then((result) => {
this.confirmExitButtonClicked = false;
this.handleStandardAndCriticalError(result.error);
});
}
protected closeDialog(): void {
const dialog: CrDialogElement|null =
this.shadowRoot!.querySelector('#exitDialog');
assert(dialog);
dialog.close();
}
private getNextButtonLabel(): string {
return this.i18n(
this.currentPage.buttonNextLabelKey ?
this.currentPage.buttonNextLabelKey :
'nextButtonLabel');
}
protected getExitButtonLabel(): string {
return this.i18n(
this.currentPage.buttonExitLabelKey ?
this.currentPage.buttonExitLabelKey :
'exitButtonLabel');
}
protected openLogsDialog(): void {
this.shimlessRmaService.getLog().then(
(res: {log: string, error: RmadErrorCode}) => this.log = res.log);
const dialog: CrDialogElement|null =
this.shadowRoot!.querySelector('#logsDialog');
assert(dialog);
if (!dialog.open) {
dialog.showModal();
}
}
protected launch3pDiagnostics(): void {
if (this.allButtonsDisabled) {
return;
}
const diagnostics: Shimless3pDiagnostics|null =
this.shadowRoot!.querySelector('#shimless3pDiagnostics');
assert(diagnostics);
diagnostics.launch3pDiagnostics();
}
private saveLog(): void {
this.shimlessRmaService.saveLog().then((result: SaveLogResponse) => {
if (result.error === RmadErrorCode.kOk) {
this.logSavedStatusText =
this.i18n('rmaLogsSaveSuccessText', result.savePath.path);
this.usbLogState = UsbLogState.LOG_SAVE_SUCCESS;
} else if (result.error === RmadErrorCode.kUsbNotFound) {
this.logSavedStatusText = this.i18n('rmaLogsSaveUsbNotFound');
this.usbLogState = UsbLogState.LOG_SAVE_FAIL;
} else {
this.logSavedStatusText = this.i18n('rmaLogsSaveFailText');
this.usbLogState = UsbLogState.LOG_SAVE_FAIL;
}
});
}
protected onSaveLogClick(): void {
this.saveLog();
}
protected retrySaveLogs(): void {
this.saveLog();
}
protected closeLogsDialog(): void {
const dialog: CrDialogElement|null =
this.shadowRoot!.querySelector('#logsDialog');
assert(dialog);
dialog.close();
// Reset the USB state back to the default.
this.usbLogState = DEFAULT_USB_LOG_STATE;
}
/**
* Implements ExternalDiskStateObserver.onExternalDiskStateChanged()
*/
onExternalDiskStateChanged(detected: boolean): void {
if (!detected) {
this.usbLogState = UsbLogState.USB_UNPLUGGED;
return;
}
if (this.usbLogState === UsbLogState.USB_UNPLUGGED) {
this.usbLogState = UsbLogState.USB_READY;
}
}
protected shouldShowSaveToUsbButton(): boolean {
return this.usbLogState === UsbLogState.USB_READY;
}
protected shouldShowLogSaveAttemptContainer(): boolean {
return this.usbLogState === UsbLogState.LOG_SAVE_SUCCESS ||
this.usbLogState === UsbLogState.LOG_SAVE_FAIL;
}
protected shouldShowRetryButton(): boolean {
return this.usbLogState === UsbLogState.LOG_SAVE_FAIL;
}
protected shouldShowLogUsbMessageContainer(): boolean {
return this.usbLogState === UsbLogState.USB_UNPLUGGED;
}
protected getSaveLogResultIcon(): string {
switch (this.usbLogState) {
case UsbLogState.LOG_SAVE_SUCCESS:
return 'shimless-icon:check';
case UsbLogState.LOG_SAVE_FAIL:
return 'shimless-icon:warning';
default:
return '';
}
}
private handleKeyboardShortcut(event: KeyboardEvent): void {
// Handle `Alt + Shift + {key}` shortcuts.
if (event.altKey && event.shiftKey) {
switch (event.key.toLowerCase()) {
case 'l':
this.openLogsDialog();
break;
case 'd':
this.launch3pDiagnostics();
break;
}
}
}
}
declare global {
interface HTMLElementTagNameMap {
[ShimlessRma.is]: ShimlessRma;
}
}
customElements.define(ShimlessRma.is, ShimlessRma);