UNPKG

chrome-devtools-frontend

Version:
342 lines (308 loc) • 12.5 kB
// Copyright 2024 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Common from '../../core/common/common.js'; import * as i18n from '../../core/i18n/i18n.js'; import type * as Platform from '../../core/platform/platform.js'; import * as Protocol from '../../generated/protocol.js'; import type {Icon} from '../../ui/kit/kit.js'; import * as UI from '../../ui/legacy/legacy.js'; import {html, nothing, render, type TemplateResult} from '../../ui/lit/lit.js'; import lockIconStyles from './lockIcon.css.js'; import { createHighlightedUrl, getSecurityStateIconForDetailedView, getSecurityStateIconForOverview, OriginGroup, } from './SecurityPanel.js'; import sidebarStyles from './sidebar.css.js'; const UIStrings = { /** * @description Section title for the Security panel's sidebar. */ security: 'Security', /** * @description Title of the tree item in the Security panel sidebar representing the main origin. */ mainOrigin: 'Main origin', /** * @description Title of the tree group in the Security panel sidebar for non-secure origins. */ nonsecureOrigins: 'Non-secure origins', /** * @description Title of the tree group in the Security panel sidebar for secure origins. */ secureOrigins: 'Secure origins', /** * @description Title of the tree group in the Security panel sidebar for unknown or canceled origins. */ unknownCanceled: 'Unknown / canceled', /** * @description Title of the overview tree item in the Security panel sidebar. */ overview: 'Overview', /** * @description Message shown in the Security panel sidebar prompting the user to reload the page to record origins. */ reloadToViewDetails: 'Reload to view details', } as const; const str_ = i18n.i18n.registerUIStrings('panels/security/SecurityPanelSidebar.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export interface ViewInput { mainOrigin: string|null; origins: Map<Platform.DevToolsPath.UrlString, Protocol.Security.SecurityState>; originsHidden: boolean; showOriginsUnconditionally: boolean; overviewSecurityState: Protocol.Security.SecurityState; selectedElementId: string; } export interface ViewOutput { onElementSelected: (id: string) => void; onShowOrigin: (origin: Platform.DevToolsPath.UrlString|null) => void; } export type View = (input: ViewInput, output: ViewOutput, target: HTMLElement) => void; export const DEFAULT_VIEW: View = (input, output, target) => { const renderIcon = (icon: Icon): TemplateResult => html`${icon}`; const renderOrigin = (origin: Platform.DevToolsPath.UrlString, state: Protocol.Security.SecurityState): TemplateResult => { const icon = getSecurityStateIconForDetailedView(state, `security-property security-property-${state}`); const selected = input.selectedElementId === origin; // clang-format off return html` <li role="treeitem" class="security-sidebar-tree-item" title=${origin} ?selected=${selected} @click=${() => { output.onElementSelected(origin); output.onShowOrigin(origin); }}> ${renderIcon(icon)} ${createHighlightedUrl(origin, state)} </li> `; // clang-format on }; const getGroupTitle = (group: OriginGroup): string => { switch (group) { case OriginGroup.MainOrigin: return i18nString(UIStrings.mainOrigin); case OriginGroup.NonSecure: return i18nString(UIStrings.nonsecureOrigins); case OriginGroup.Secure: return i18nString(UIStrings.secureOrigins); case OriginGroup.Unknown: return i18nString(UIStrings.unknownCanceled); } }; const getGroupIcon = (group: OriginGroup, mainOriginState = Protocol.Security.SecurityState.Unknown): Icon|null => { switch (group) { case OriginGroup.MainOrigin: return getSecurityStateIconForOverview(mainOriginState, `lock-icon lock-icon-${mainOriginState}`); case OriginGroup.NonSecure: return getSecurityStateIconForDetailedView(Protocol.Security.SecurityState.Insecure, `lock-icon lock-icon-${Protocol.Security.SecurityState.Insecure}`); case OriginGroup.Secure: return getSecurityStateIconForDetailedView(Protocol.Security.SecurityState.Secure, `lock-icon lock-icon-${Protocol.Security.SecurityState.Secure}`); case OriginGroup.Unknown: return getSecurityStateIconForDetailedView(Protocol.Security.SecurityState.Unknown, `lock-icon lock-icon-${Protocol.Security.SecurityState.Unknown}`); } }; const originsByGroup = new Map<OriginGroup, Array<{origin: Platform.DevToolsPath.UrlString, state: Protocol.Security.SecurityState}>>(); for (const group of Object.values(OriginGroup)) { originsByGroup.set(group, []); } let mainOriginState = Protocol.Security.SecurityState.Unknown; const overviewIcon = getSecurityStateIconForOverview(input.overviewSecurityState, 'lock-icon lock-icon-' + input.overviewSecurityState); for (const [origin, state] of input.origins) { if (origin === input.mainOrigin) { originsByGroup.get(OriginGroup.MainOrigin)?.push({origin, state}); mainOriginState = state; } else { switch (state) { case Protocol.Security.SecurityState.Secure: originsByGroup.get(OriginGroup.Secure)?.push({origin, state}); break; case Protocol.Security.SecurityState.Unknown: originsByGroup.get(OriginGroup.Unknown)?.push({origin, state}); break; default: originsByGroup.get(OriginGroup.NonSecure)?.push({origin, state}); break; } } } const renderGroup = (group: OriginGroup): TemplateResult => { const origins = originsByGroup.get(group) || []; const title = getGroupTitle(group); const icon = getGroupIcon(group, mainOriginState); const isHidden = input.originsHidden || (!input.showOriginsUnconditionally && group !== OriginGroup.MainOrigin && origins.length === 0); const showReloadMessage = group === OriginGroup.MainOrigin && input.origins.size === 0; // clang-format off return html` <li role="treeitem" class="security-sidebar-origins" open ?hidden=${isHidden}> ${icon ? renderIcon(icon) : nothing} <span class="security-sidebar-origins-title">${title}</span> <ul role="group" aria-label=${title}> ${showReloadMessage ? html` <li role="treeitem" class="security-main-view-reload-message" aria-disabled="true"> ${i18nString(UIStrings.reloadToViewDetails)} </li> ` : nothing} ${origins.map(o => renderOrigin(o.origin, o.state))} </ul> </li> `; // clang-format on }; // clang-format off render(html` <devtools-tree class="security-sidebar" navigation-variant .template=${html` <ul role="tree"> <li role="treeitem" class="security-group-list-item" aria-level="3" aria-label=${i18nString(UIStrings.security)}> ${i18nString(UIStrings.security)} <ul role="group"> <li role="treeitem" class="security-main-view-sidebar-tree-item" title=${i18nString(UIStrings.overview)} ?selected=${input.selectedElementId === 'overview'} @click=${() => { output.onElementSelected('overview'); output.onShowOrigin(null); }}> ${renderIcon(overviewIcon)} <span title=${i18nString(UIStrings.overview)}>${i18nString(UIStrings.overview)}</span> </li> ${Object.values(OriginGroup).map(group => renderGroup(group))} </ul> </li> </ul> `}></devtools-tree> `, target); // clang-format on }; export class SecurityPanelSidebar extends UI.Widget.VBox { readonly #securitySidebarLastItemSetting: Common.Settings.Setting<string>; #mainOrigin: string|null = null; #origins = new Map<Platform.DevToolsPath.UrlString, Protocol.Security.SecurityState>(); #originsHidden = false; #showOriginsUnconditionally = false; #overviewSecurityState = Protocol.Security.SecurityState.Unknown; #selectedElementId = 'overview'; readonly #view: View; #onShowOrigin?: (origin: Platform.DevToolsPath.UrlString|null) => void; constructor(element?: HTMLElement, view: View = DEFAULT_VIEW) { super(element); this.#view = view; this.registerRequiredCSS(lockIconStyles, sidebarStyles); this.#securitySidebarLastItemSetting = Common.Settings.Settings.instance().createSetting('security-last-selected-element-path', ''); } set onShowOrigin(callback: (origin: Platform.DevToolsPath.UrlString|null) => void) { this.#onShowOrigin = callback; } override wasShown(): void { super.wasShown(); this.showLastSelectedElement(); } showLastSelectedElement(): void { this.#selectedElementId = 'overview'; this.#securitySidebarLastItemSetting.set('overview'); this.requestUpdate(); this.#onShowOrigin?.(null); } toggleOriginsList(hidden: boolean): void { this.#originsHidden = hidden; this.#showOriginsUnconditionally = !hidden; this.requestUpdate(); } addOrigin(origin: Platform.DevToolsPath.UrlString, securityState: Protocol.Security.SecurityState): void { this.#origins.set(origin, securityState); this.requestUpdate(); } setMainOrigin(origin: string): void { this.#mainOrigin = origin; this.requestUpdate(); } get mainOrigin(): string|null { return this.#mainOrigin; } updateOrigin(origin: Platform.DevToolsPath.UrlString, securityState: Protocol.Security.SecurityState): void { this.#origins.set(origin, securityState); this.requestUpdate(); } updateOverviewSecurityState(securityState: Protocol.Security.SecurityState): void { this.#overviewSecurityState = securityState; this.requestUpdate(); } clearOrigins(): void { this.#origins.clear(); this.requestUpdate(); } // Used in web tests elementsByOrigin(): Map<string, { select: (omitFocus?: boolean, selectedByUser?: boolean) => void, showElement: () => void, origin: () => string, securityState: () => Protocol.Security.SecurityState | undefined, }> { const map = new Map<string, { select: (omitFocus?: boolean, selectedByUser?: boolean) => void, showElement: () => void, origin: () => string, securityState: () => Protocol.Security.SecurityState | undefined, }>(); for (const [origin, state] of this.#origins.entries()) { const element = { select: () => { this.#selectedElementId = origin; this.#securitySidebarLastItemSetting.set(origin); this.requestUpdate(); this.#onShowOrigin?.(origin); }, showElement: () => { this.#onShowOrigin?.(origin); }, origin: () => origin, securityState: () => state, }; map.set(origin, element); } return map; } set selectedOrigin(origin: Platform.DevToolsPath.UrlString|null) { this.#selectedElementId = origin ?? 'overview'; this.#securitySidebarLastItemSetting.set(this.#selectedElementId); this.requestUpdate(); this.#onShowOrigin?.(origin); } get selectedOrigin(): string { return this.#selectedElementId; } override performUpdate(): void { const input: ViewInput = { mainOrigin: this.#mainOrigin, origins: this.#origins, originsHidden: this.#originsHidden, showOriginsUnconditionally: this.#showOriginsUnconditionally, overviewSecurityState: this.#overviewSecurityState, selectedElementId: this.#selectedElementId, }; const output: ViewOutput = { onElementSelected: (id: string) => { this.#selectedElementId = id; this.#securitySidebarLastItemSetting.set(id); this.requestUpdate(); }, onShowOrigin: (origin: Platform.DevToolsPath.UrlString|null) => { this.#onShowOrigin?.(origin); }, }; this.#view(input, output, this.contentElement); } }