chrome-devtools-frontend
Version:
Chrome DevTools UI
342 lines (308 loc) • 12.5 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.
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);
}
}