UNPKG

chrome-devtools-frontend

Version:
376 lines (341 loc) 14.3 kB
// 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 '../../../ui/components/settings/settings.js'; import '../../../ui/components/tooltips/tooltips.js'; import * as Common from '../../../core/common/common.js'; import * as Host from '../../../core/host/host.js'; import * as i18n from '../../../core/i18n/i18n.js'; import type * as Platform from '../../../core/platform/platform.js'; import * as SDK from '../../../core/sdk/sdk.js'; import * as Badges from '../../../models/badges/badges.js'; import * as Buttons from '../../../ui/components/buttons/buttons.js'; import type * as SettingsComponents from '../../../ui/components/settings/settings.js'; import * as UI from '../../../ui/legacy/legacy.js'; import * as Lit from '../../../ui/lit/lit.js'; import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js'; import * as PanelCommon from '../../common/common.js'; import * as PanelUtils from '../../utils/utils.js'; import syncSectionStyles from './syncSection.css.js'; const UIStrings = { /** * @description Text shown to the user in the Settings UI. 'This setting' refers * to a checkbox that is disabled. */ syncDisabled: 'To turn this setting on, you must enable Chrome sync.', /** * @description Text shown to the user in the Settings UI. Explains why the checkbox * for saving DevTools settings to the user's Google account is inactive. */ preferencesSyncDisabled: 'You need to first enable saving `Chrome` settings in your `Google` account.', /** * @description Label for the account email address. Shown in the DevTools Settings UI in * front of the email address currently used for Chrome Sync. */ signedIn: 'Signed into Chrome as:', /** * @description Label for the account settings. Shown in the DevTools Settings UI in * case the user is not logged in to Chrome. */ notSignedIn: 'You\'re not signed into Chrome.', /** * @description Label for the Google Developer Program profile status that corresponds to * standard plan (No subscription). */ gdpStandardPlan: 'Standard plan', /** * @description Label for the Google Developer Program subscription status that corresponds to * `PREMIUM_ANNUAL` plan. */ gdpPremiumSubscription: 'Premium', /** * @description Label for the Google Developer Program subscription status that corresponds to * `PRO_ANNUAL` plan. */ gdpProSubscription: 'Pro', /** * @description Label for the Google Developer Program subscription status that corresponds * to a plan not known by the client. */ gdpUnknownSubscription: 'Unknown plan', /** * @description Label for Sign-Up button for the Google Developer Program profiles. */ signUp: 'Sign up', /** * @description Link text for opening the Google Developer Program profile page. */ viewProfile: 'View profile', /** * @description Text for tooltip shown on hovering over "Relevant Data" in the disclaimer text for AI code completion. */ tooltipDisclaimerText: 'When you qualify for a badge, the badge’s identifier and the type of activity you did to earn it are sent to Google', /** * @description Text for the data notice right after the settings checkbox. */ relevantData: 'Relevant data', /** * @description Text for the data notice right after the settings checkbox. * @example {Relevant data} PH1 */ dataDisclaimer: '({PH1} is sent to Google)', } as const; const str_ = i18n.i18n.registerUIStrings('panels/settings/components/SyncSection.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); const i18nTemplate = Lit.i18nTemplate.bind(undefined, str_); const {html, render, Directives: {ref}} = Lit; function getGdpSubscriptionText(profile: Host.GdpClient.Profile): Platform.UIString.LocalizedString { if (!profile.activeSubscription || profile.activeSubscription.subscriptionStatus !== Host.GdpClient.SubscriptionStatus.ENABLED) { return i18nString(UIStrings.gdpStandardPlan); } switch (profile.activeSubscription.subscriptionTier) { case Host.GdpClient.SubscriptionTier.PREMIUM_ANNUAL: case Host.GdpClient.SubscriptionTier.PREMIUM_MONTHLY: return i18nString(UIStrings.gdpPremiumSubscription); case Host.GdpClient.SubscriptionTier.PRO_ANNUAL: case Host.GdpClient.SubscriptionTier.PRO_MONTHLY: return i18nString(UIStrings.gdpProSubscription); default: return i18nString(UIStrings.gdpUnknownSubscription); } } // clang-format off const DEFAULT_VIEW = (input: ViewInput, output: ViewOutput, target: HTMLElement): void => { const renderSettingCheckboxIfNeeded = (): Lit.LitTemplate => { if (!input.syncInfo.accountEmail) { return Lit.nothing; } const warningText = input.warningType === WarningType.SYNC_DISABLED ? i18nString(UIStrings.syncDisabled) : i18nString(UIStrings.preferencesSyncDisabled); return html` <div class="setting-checkbox-container"> <setting-checkbox class="setting-checkbox" .data=${{ setting: input.syncSetting } as SettingsComponents.SettingCheckbox.SettingCheckboxData}> </setting-checkbox> ${input.warningType ? html` <devtools-button aria-details="settings-sync-info" .iconName=${'info'} .variant=${Buttons.Button.Variant.ICON} .size=${Buttons.Button.Size.SMALL} @click=${input.onWarningClick}> </devtools-button> <devtools-tooltip id="settings-sync-info" variant="rich"> ${warningText} </devtools-tooltip>`: Lit.nothing} </div> `; }; const renderAccountInfo = (): Lit.LitTemplate => { if (!input.syncInfo.accountEmail) { return html` <div class="not-signed-in">${i18nString(UIStrings.notSignedIn)}</div> `; } return html` <div class="account-info"> <img class="account-avatar" src="data:image/png;base64, ${input.syncInfo.accountImage}" alt="Account avatar" /> <div class="account-email"> <span>${i18nString(UIStrings.signedIn)}</span> <span>${input.syncInfo.accountEmail}</span> </div> </div>`; }; const renderGdpSectionIfNeeded = (): Lit.LitTemplate => { if (!input.isEligibleToCreateGdpProfile && !input.gdpProfile) { return Lit.nothing; } const hasReceiveBadgesCheckbox = Host.GdpClient.isBadgesEnabled() && input.receiveBadgesSetting; const renderBrand = (): Lit.LitTemplate => { return html` <div class="gdp-profile-header"> <div class="gdp-logo" role="img" aria-label="Google Developer Program"></div> </div> `; }; return html` <div class="gdp-profile-container" .jslog=${VisualLogging.section().context('gdp-profile')}> <div class="divider"></div> ${input.gdpProfile ? html` <div class="gdp-profile-details-content"> ${renderBrand()} <div class="plan-details"> ${getGdpSubscriptionText(input.gdpProfile)} &nbsp;·&nbsp; <x-link .jslog=${VisualLogging.link().track({click: true, keydown: 'Enter|Space'}).context('view-profile')} class="link" href=${Host.GdpClient.GOOGLE_DEVELOPER_PROGRAM_PROFILE_LINK}> ${i18nString(UIStrings.viewProfile)} </x-link></div> ${hasReceiveBadgesCheckbox ? html` <div class="setting-container" ${ref(el => { output.highlightReceiveBadgesSetting = () => { if (el) { PanelUtils.PanelUtils.highlightElement(el as HTMLElement); } }; })}> <setting-checkbox class="setting-checkbox" .data=${{setting: input.receiveBadgesSetting} as SettingsComponents.SettingCheckbox.SettingCheckboxData} @change=${(e: Event) => input.onReceiveBadgesSettingClick(e)}> </setting-checkbox> <span>${i18nTemplate(UIStrings.dataDisclaimer, {PH1: html` <span class="link" tabindex="0" aria-details="gdp-profile-tooltip"> ${i18nString(UIStrings.relevantData)}</span> <devtools-tooltip id="gdp-profile-tooltip" variant="rich"> <div class="tooltip-content" tabindex="0"> ${i18nString(UIStrings.tooltipDisclaimerText)}</div> </devtools-tooltip>`})} </span> </div> ` : Lit.nothing} </div> ` : html` <div class="gdp-profile-sign-up-content"> ${renderBrand()} <devtools-button @click=${input.onSignUpClick} .jslogContext=${'open-sign-up-dialog'} .variant=${Buttons.Button.Variant.OUTLINED}> ${i18nString(UIStrings.signUp)} </devtools-button> </div> `} </div> `; }; render(html` <style>${syncSectionStyles}</style> <fieldset> ${renderAccountInfo()} ${renderSettingCheckboxIfNeeded()} ${renderGdpSectionIfNeeded()} </fieldset> `, target); }; // clang-format on type View = typeof DEFAULT_VIEW; export const enum WarningType { SYNC_DISABLED = 'SYNC_DISABLED', PREFERENCES_SYNC_DISABLED = 'PREFERENCES_SYNC_DISABLED', } export interface SyncSectionData { syncInfo: Host.InspectorFrontendHostAPI.SyncInformation; syncSetting: Common.Settings.Setting<boolean>; receiveBadgesSetting: Common.Settings.Setting<boolean>; } export interface ViewInput { syncInfo: Host.InspectorFrontendHostAPI.SyncInformation; syncSetting: Common.Settings.Setting<boolean>; receiveBadgesSetting?: Common.Settings.Setting<boolean>; isEligibleToCreateGdpProfile: boolean; gdpProfile?: Host.GdpClient.Profile; onSignUpClick: () => void; onReceiveBadgesSettingClick: (e: Event) => void; onWarningClick: (e: Event) => void; warningType?: WarningType; } export interface ViewOutput { highlightReceiveBadgesSetting?: () => void; } export class SyncSection extends UI.Widget.Widget { #syncInfo: Host.InspectorFrontendHostAPI.SyncInformation = {isSyncActive: false}; #syncSetting: Common.Settings.Setting<boolean>; #receiveBadgesSetting: Common.Settings.Setting<boolean>; #isEligibleToCreateGdpProfile = false; #gdpProfile?: Host.GdpClient.Profile; #view: View; #viewOutput: ViewOutput = {}; constructor(element?: HTMLElement, view: View = DEFAULT_VIEW) { super(element); this.#view = view; this.#receiveBadgesSetting = Common.Settings.Settings.instance().moduleSetting('receive-gdp-badges'); this.#syncSetting = Common.Settings.moduleSetting('sync-preferences') as Common.Settings.Setting<boolean>; } override wasShown(): void { super.wasShown(); this.requestUpdate(); } set syncInfo(syncInfo: Host.InspectorFrontendHostAPI.SyncInformation) { this.#syncInfo = syncInfo; this.requestUpdate(); // Trigger fetching GDP profile if the user is signed in. if (syncInfo.accountEmail) { void this.#fetchGdpDetails(); } } async highlightReceiveBadgesSetting(): Promise<void> { this.requestUpdate(); await this.updateComplete; this.#viewOutput.highlightReceiveBadgesSetting?.(); } override performUpdate(): void { // TODO: this should not probably happen in render, instead, the setting // should be disabled. const checkboxDisabled = !this.#syncInfo.isSyncActive || !this.#syncInfo.arePreferencesSynced; this.#syncSetting?.setDisabled(checkboxDisabled); let warningType: WarningType|undefined; if (!this.#syncInfo.isSyncActive) { warningType = WarningType.SYNC_DISABLED; } else if (!this.#syncInfo.arePreferencesSynced) { warningType = WarningType.PREFERENCES_SYNC_DISABLED; } const viewInput: ViewInput = { syncInfo: this.#syncInfo, syncSetting: this.#syncSetting, receiveBadgesSetting: this.#receiveBadgesSetting, gdpProfile: this.#gdpProfile, isEligibleToCreateGdpProfile: Host.GdpClient.isGdpProfilesAvailable() && this.#isEligibleToCreateGdpProfile, onSignUpClick: this.#onSignUpClick.bind(this), onReceiveBadgesSettingClick: this.#onReceiveBadgesSettingClick.bind(this), onWarningClick: this.#onWarningClick.bind(this), warningType, }; this.#view(viewInput, this.#viewOutput, this.contentElement); } #onSignUpClick(): void { PanelCommon.GdpSignUpDialog.show({onSuccess: (this.#fetchGdpDetails.bind(this))}); } #onReceiveBadgesSettingClick(e: Event): void { const settingCheckbox = e.target as SettingsComponents.SettingCheckbox.SettingCheckbox; void Badges.UserBadges.instance().initialize().then(() => { if (!settingCheckbox.checked) { return; } Badges.UserBadges.instance().recordAction(Badges.BadgeAction.RECEIVE_BADGES_SETTING_ENABLED); }); } #onWarningClick(event: Event): void { const rootTarget = SDK.TargetManager.TargetManager.instance().rootTarget(); if (rootTarget === null) { return; } // TODO: investigate if /advance link is alive const warningLink = !this.#syncInfo.isSyncActive ? ('chrome://settings/syncSetup' as Platform.DevToolsPath.UrlString) : ('chrome://settings/syncSetup/advanced' as Platform.DevToolsPath.UrlString); void rootTarget.targetAgent().invoke_createTarget({url: warningLink}).then(result => { if (result.getError()) { Host.InspectorFrontendHost.InspectorFrontendHostInstance.openInNewTab(warningLink); } }); event.consume(); } async #fetchGdpDetails(): Promise<void> { if (!Host.GdpClient.isGdpProfilesAvailable()) { return; } const getProfileResponse = await Host.GdpClient.GdpClient.instance().getProfile(); if (!getProfileResponse) { return; } this.#gdpProfile = getProfileResponse.profile ?? undefined; this.#isEligibleToCreateGdpProfile = getProfileResponse.isEligible; this.requestUpdate(); } }