chrome-devtools-frontend
Version:
Chrome DevTools UI
477 lines (424 loc) • 18.9 kB
text/typescript
// 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.
/* eslint-disable @devtools/no-imperative-dom-api */
import * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import * 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 {OriginTreeElement} from './OriginTreeElement.js';
import {
createHighlightedUrl,
getSecurityStateIconForDetailedView,
getSecurityStateIconForOverview,
OriginGroup,
} from './SecurityPanel.js';
import type {SecurityPanelSidebarTreeElement} from './SecurityPanelSidebarTreeElement.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}
=${() => {
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'}
=${() => {
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>;
readonly sidebarTree: UI.TreeOutline.TreeOutlineInShadow;
readonly #originGroupTitles: Map<OriginGroup, {title: string, icon?: Icon}>;
#originGroups: Map<OriginGroup, UI.TreeOutline.TreeElement>;
securityOverviewElement: OriginTreeElement;
readonly #elementsByOrigin: Map<string, OriginTreeElement>;
readonly #mainViewReloadMessage: UI.TreeOutline.TreeElement;
#mainOrigin: string|null;
constructor(element?: HTMLElement) {
super(element);
this.#securitySidebarLastItemSetting =
Common.Settings.Settings.instance().createSetting('security-last-selected-element-path', '');
this.#mainOrigin = null;
this.sidebarTree = new UI.TreeOutline.TreeOutlineInShadow(UI.TreeOutline.TreeVariant.NAVIGATION_TREE);
this.sidebarTree.registerRequiredCSS(lockIconStyles, sidebarStyles);
this.sidebarTree.element.classList.add('security-sidebar');
this.contentElement.appendChild(this.sidebarTree.element);
const securitySectionTitle = i18nString(UIStrings.security);
const securityTreeSection = this.#addSidebarSection(securitySectionTitle, 'security');
this.securityOverviewElement =
new OriginTreeElement('security-main-view-sidebar-tree-item', this.#renderTreeElement);
this.securityOverviewElement.tooltip = i18nString(UIStrings.overview);
securityTreeSection.appendChild(this.securityOverviewElement);
this.#originGroupTitles = new Map([
[OriginGroup.MainOrigin, {title: i18nString(UIStrings.mainOrigin)}],
[
OriginGroup.NonSecure,
{
title: i18nString(UIStrings.nonsecureOrigins),
icon: getSecurityStateIconForDetailedView(Protocol.Security.SecurityState.Insecure,
`lock-icon lock-icon-${Protocol.Security.SecurityState.Insecure}`),
},
],
[
OriginGroup.Secure,
{
title: i18nString(UIStrings.secureOrigins),
icon: getSecurityStateIconForDetailedView(Protocol.Security.SecurityState.Secure,
`lock-icon lock-icon-${Protocol.Security.SecurityState.Secure}`),
},
],
[
OriginGroup.Unknown,
{
title: i18nString(UIStrings.unknownCanceled),
icon: getSecurityStateIconForDetailedView(Protocol.Security.SecurityState.Unknown,
`lock-icon lock-icon-${Protocol.Security.SecurityState.Unknown}`),
},
],
]);
this.#originGroups = new Map();
for (const group of Object.values(OriginGroup)) {
const element = this.#createOriginGroupElement(this.#originGroupTitles.get(group)?.title as string,
this.#originGroupTitles.get(group)?.icon);
this.#originGroups.set(group, element);
securityTreeSection.appendChild(element);
}
this.#mainViewReloadMessage = new UI.TreeOutline.TreeElement(i18nString(UIStrings.reloadToViewDetails));
this.#mainViewReloadMessage.selectable = false;
this.#mainViewReloadMessage.listItemElement.classList.add('security-main-view-reload-message');
const treeElement = this.#originGroups.get(OriginGroup.MainOrigin);
(treeElement as UI.TreeOutline.TreeElement).appendChild(this.#mainViewReloadMessage);
this.#clearOriginGroups();
this.#elementsByOrigin = new Map();
this.element.addEventListener('update-sidebar-selection', (event: Event) => {
const id: string = (event as CustomEvent).detail.id;
this.#securitySidebarLastItemSetting.set(id);
});
this.showLastSelectedElement();
}
// Used in web tests
elementsByOrigin(): Map<string, OriginTreeElement> {
return this.#elementsByOrigin;
}
// Used in web tests
set selectedOrigin(origin: Platform.DevToolsPath.UrlString|string|null) {
if (origin) {
const originElement = this.#elementsByOrigin.get(origin);
if (originElement) {
if (this.sidebarTree.selectedTreeElement === originElement) {
originElement.showElement();
} else {
originElement.select(undefined, true);
}
}
} else if (this.sidebarTree.selectedTreeElement === this.securityOverviewElement) {
this.securityOverviewElement.showElement();
} else {
this.securityOverviewElement.select(undefined, true);
}
}
get selectedOrigin(): string {
const selected = this.sidebarTree.selectedTreeElement;
if (selected instanceof OriginTreeElement) {
return selected.origin() ?? 'overview';
}
return 'overview';
}
showLastSelectedElement(): void {
this.securityOverviewElement.select();
this.securityOverviewElement.showElement();
}
#addSidebarSection(title: string, jslogContext: string): UI.TreeOutline.TreeElement {
const treeElement = new UI.TreeOutline.TreeElement(title, true, jslogContext);
treeElement.listItemElement.classList.add('security-group-list-item');
treeElement.setCollapsible(false);
treeElement.selectable = false;
this.sidebarTree.appendChild(treeElement);
UI.ARIAUtils.markAsHeading(treeElement.listItemElement, 3);
UI.ARIAUtils.setLabel(treeElement.childrenListElement, title);
return treeElement;
}
#originGroupTitle(originGroup: OriginGroup): string {
return this.#originGroupTitles.get(originGroup)?.title as string;
}
#originGroupElement(originGroup: OriginGroup): UI.TreeOutline.TreeElement {
return this.#originGroups.get(originGroup) as UI.TreeOutline.TreeElement;
}
#createOriginGroupElement(originGroupTitle: string, originGroupIcon?: Icon): UI.TreeOutline.TreeElement {
const originGroup = new UI.TreeOutline.TreeElement(originGroupTitle, true);
originGroup.expand();
originGroup.listItemElement.classList.add('security-sidebar-origins');
if (originGroupIcon) {
originGroup.setLeadingIcons([originGroupIcon]);
}
UI.ARIAUtils.setLabel(originGroup.childrenListElement, originGroupTitle);
return originGroup;
}
toggleOriginsList(hidden: boolean): void {
for (const element of this.#originGroups.values()) {
element.hidden = hidden;
}
}
addOrigin(origin: Platform.DevToolsPath.UrlString, securityState: Protocol.Security.SecurityState): void {
this.#mainViewReloadMessage.hidden = true;
const originElement = new OriginTreeElement('security-sidebar-tree-item', this.#renderTreeElement, origin);
originElement.tooltip = origin;
this.#elementsByOrigin.set(origin, originElement);
this.updateOrigin(origin, securityState);
}
setMainOrigin(origin: string): void {
this.#mainOrigin = origin;
}
get mainOrigin(): string|null {
return this.#mainOrigin;
}
get originGroups(): Map<OriginGroup, UI.TreeOutline.TreeElement> {
return this.#originGroups;
}
updateOrigin(origin: string, securityState: Protocol.Security.SecurityState): void {
const originElement = this.#elementsByOrigin.get(origin) as OriginTreeElement;
originElement.setSecurityState(securityState);
let newParent: UI.TreeOutline.TreeElement;
if (origin === this.#mainOrigin) {
newParent = this.#originGroups.get(OriginGroup.MainOrigin) as UI.TreeOutline.TreeElement;
newParent.title = i18nString(UIStrings.mainOrigin);
if (securityState === Protocol.Security.SecurityState.Secure) {
newParent.setLeadingIcons(
[getSecurityStateIconForOverview(securityState, `lock-icon lock-icon-${securityState}`)]);
} else {
newParent.setLeadingIcons(
[getSecurityStateIconForOverview(securityState, `lock-icon lock-icon-${securityState}`)]);
}
UI.ARIAUtils.setLabel(newParent.childrenListElement, newParent.title);
} else {
switch (securityState) {
case Protocol.Security.SecurityState.Secure:
newParent = this.#originGroupElement(OriginGroup.Secure);
break;
case Protocol.Security.SecurityState.Unknown:
newParent = this.#originGroupElement(OriginGroup.Unknown);
break;
default:
newParent = this.#originGroupElement(OriginGroup.NonSecure);
break;
}
}
const oldParent = originElement.parent;
if (oldParent !== newParent) {
if (oldParent) {
oldParent.removeChild(originElement);
if (oldParent.childCount() === 0) {
oldParent.hidden = true;
}
}
newParent.appendChild(originElement);
newParent.hidden = false;
}
}
#clearOriginGroups(): void {
for (const [originGroup, originGroupElement] of this.#originGroups) {
if (originGroup === OriginGroup.MainOrigin) {
for (let i = originGroupElement.childCount() - 1; i > 0; i--) {
originGroupElement.removeChildAtIndex(i);
}
originGroupElement.title = this.#originGroupTitle(OriginGroup.MainOrigin);
originGroupElement.hidden = false;
this.#mainViewReloadMessage.hidden = false;
} else {
originGroupElement.removeChildren();
originGroupElement.hidden = true;
}
}
}
clearOrigins(): void {
this.#clearOriginGroups();
this.#elementsByOrigin.clear();
}
override focus(): void {
this.sidebarTree.focus();
}
#renderTreeElement(element: SecurityPanelSidebarTreeElement): void {
if (element instanceof OriginTreeElement) {
const securityState = element.securityState() ?? Protocol.Security.SecurityState.Unknown;
const isOverviewElement = element.listItemElement.classList.contains('security-main-view-sidebar-tree-item');
const icon = isOverviewElement ?
getSecurityStateIconForOverview(securityState, `lock-icon lock-icon-${securityState}`) :
getSecurityStateIconForDetailedView(securityState, `security-property security-property-${securityState}`);
element.setLeadingIcons([icon]);
if (isOverviewElement) {
element.title = i18nString(UIStrings.overview);
} else {
const elementTitle =
createHighlightedUrl(element.origin() ?? Platform.DevToolsPath.EmptyUrlString, securityState);
if (element.listItemElement.lastChild) {
element.listItemElement.removeChild(element.listItemElement.lastChild);
}
element.listItemElement.appendChild(elementTitle);
}
}
}
}