chrome-devtools-frontend
Version:
Chrome DevTools UI
376 lines (341 loc) • 14.3 kB
text/typescript
// 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)}
·
<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();
}
}