chrome-devtools-frontend
Version:
Chrome DevTools UI
334 lines (300 loc) • 10.7 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/kit/kit.js';
import '../../ui/legacy/legacy.js';
import '../../ui/components/adorners/adorners.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Protocol from '../../generated/protocol.js';
import type * as TreeOutline from '../../ui/components/tree_outline/tree_outline.js';
import * as UI from '../../ui/legacy/legacy.js';
import {Directives, html, type LitTemplate, nothing, render, type TemplateResult} from '../../ui/lit/lit.js';
import originTrialTokenRowsStyles from './originTrialTokenRows.css.js';
import originTrialTreeViewStyles from './originTrialTreeView.css.js';
const {classMap} = Directives;
const {widgetConfig} = UI.Widget;
const UIStrings = {
/**
* @description Label for the 'origin' field in a parsed Origin Trial Token.
*/
origin: 'Origin',
/**
* @description Label for `trialName` field in a parsed Origin Trial Token.
* This field is only shown when token has unknown trial name as the token
* will be put into 'UNKNOWN' group.
*/
trialName: 'Trial Name',
/**
* @description Label for `expiryTime` field in a parsed Origin Trial Token.
*/
expiryTime: 'Expiry Time',
/**
* @description Label for `usageRestriction` field in a parsed Origin Trial Token.
*/
usageRestriction: 'Usage Restriction',
/**
* @description Label for `isThirdParty` field in a parsed Origin Trial Token.
*/
isThirdParty: 'Third Party',
/**
* @description Label for a field containing info about an Origin Trial Token's `matchSubDomains` field.
*An Origin Trial Token contains an origin URL. The `matchSubDomains` field describes whether the token
*only applies to the origin URL or to all subdomains of the origin URL as well.
*The field contains either 'true' or 'false'.
*/
matchSubDomains: 'Subdomain Matching',
/**
* @description Label for the raw(= encoded / not human-readable) Origin Trial Token.
*/
rawTokenText: 'Raw Token',
/**
* @description Label for `status` field in an Origin Trial Token.
*/
status: 'Token Status',
/**
* @description Label for tokenWithStatus node.
*/
token: 'Token',
/**
* @description Label for a badge showing the number of Origin Trial Tokens. This number is always greater than 1.
* @example {2} PH1
*/
tokens: '{PH1} tokens',
/**
* @description Label shown when there are no Origin Trial Tokens in the Frame view of the Application panel.
*/
noTrialTokens: 'No trial tokens',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/application/OriginTrialTreeView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
type TreeNode<DataType> = TreeOutline.TreeOutlineUtils.TreeNode<DataType>;
/**
* The Origin Trial Tree has 4 levels of content:
* - Origin Trial (has multiple Origin Trial tokens)
* - Origin Trial Token (has only 1 raw token text)
* - Fields in Origin Trial Token
* - Raw Origin Trial Token text (folded because the content is long)
**/
export type OriginTrialTreeNodeData = Protocol.Page.OriginTrial|Protocol.Page.OriginTrialTokenWithStatus|string;
function renderOriginTrialTree(originTrial: Protocol.Page.OriginTrial): LitTemplate {
const success = originTrial.status === Protocol.Page.OriginTrialStatus.Enabled;
// clang-format off
return html`
<li role="treeitem">
${originTrial.trialName}
<devtools-adorner class="badge-${success ? 'success' : 'error'}">
${originTrial.status}
</devtools-adorner>
${originTrial.tokensWithStatus.length > 1 ? html`
<devtools-adorner class="badge-secondary">
${i18nString(UIStrings.tokens, {PH1: originTrial.tokensWithStatus.length})}
</devtools-adorner>`
: nothing}
<ul role="group" hidden>
${originTrial.tokensWithStatus.length > 1 ?
originTrial.tokensWithStatus.map(renderTokenNode) :
renderTokenDetailsNodes(originTrial.tokensWithStatus[0])}
</ul>
</li>`;
// clang-format on
}
function renderTokenNode(token: Protocol.Page.OriginTrialTokenWithStatus): LitTemplate {
const success = token.status === Protocol.Page.OriginTrialTokenStatus.Success;
// Only display token status for convenience when the node is not expanded.
// clang-format off
return html`
<li role="treeitem">
${i18nString(UIStrings.token)}
<devtools-adorner class="token-status-badge badge-${success ? 'success' : 'error'}">
${token.status}
</devtools-adorner>
<ul role="group" hidden>
${renderTokenDetailsNodes(token)}
</ul>
</li>`;
}
interface TokenField {
name: string;
value: {text: string, hasError?: boolean};
}
function renderTokenDetails(token: Protocol.Page.OriginTrialTokenWithStatus): TemplateResult {
return html`
<li role="treeitem">
<devtools-widget .widgetConfig=${widgetConfig(OriginTrialTokenRows, {data: token})}>
</devtools-widget>
</li>`;
}
function renderTokenDetailsNodes(token: Protocol.Page.OriginTrialTokenWithStatus):
TemplateResult {
// clang-format off
return html`
${renderTokenDetails(token)}
${renderRawTokenTextNode(token.rawTokenText)}
`;
// clang-format on
}
function renderRawTokenTextNode(tokenText: string): LitTemplate {
// clang-format off
return html`
<li role="treeitem">
${i18nString(UIStrings.rawTokenText)}
<ul role="group" hidden>
<li role="treeitem">
<div style="overflow-wrap: break-word;">
${tokenText}
</div>
</li>
</ul>
</li>`;
// clang-format on
}
export interface OriginTrialTokenRowsData {
node: TreeNode<OriginTrialTreeNodeData>;
}
interface RowsViewInput {
tokenWithStatus: Protocol.Page.OriginTrialTokenWithStatus;
parsedTokenDetails: TokenField[];
}
type RowsView = (input: RowsViewInput, output: undefined, target: HTMLElement) => void;
const ROWS_DEFAULT_VIEW: RowsView = (input, _output, target) => {
const success = input.tokenWithStatus.status === Protocol.Page.OriginTrialTokenStatus.Success;
// clang-format off
render(html`
<style>
${originTrialTokenRowsStyles}
${originTrialTreeViewStyles}
</style>
<div class="content">
<div class="key">${i18nString(UIStrings.status)}</div>
<div class="value">
<devtools-adorner class="badge-${success ? 'success' : 'error'}">
${input.tokenWithStatus.status}
</devtools-adorner>
</div>
${input.parsedTokenDetails.map((field: TokenField) => html`
<div class="key">${field.name}</div>
<div class="value">
<div class=${classMap({'error-text': Boolean(field.value.hasError)})}>
${field.value.text}
</div>
</div>
`)}
</div>`, target);
// clang-format on
};
export class OriginTrialTokenRows extends UI.Widget.Widget {
#view: RowsView;
#tokenWithStatus: Protocol.Page.OriginTrialTokenWithStatus|null = null;
#parsedTokenDetails: TokenField[] = [];
#dateFormatter: Intl.DateTimeFormat = new Intl.DateTimeFormat(
i18n.DevToolsLocale.DevToolsLocale.instance().locale,
{dateStyle: 'long', timeStyle: 'long'},
);
constructor(element?: HTMLElement, view: RowsView = ROWS_DEFAULT_VIEW) {
super(element, {useShadowDom: true});
this.#view = view;
}
set data(data: Protocol.Page.OriginTrialTokenWithStatus) {
this.#tokenWithStatus = data;
this.#setTokenFields();
}
connectedCallback(): void {
this.requestUpdate();
}
#setTokenFields(): void {
if (!this.#tokenWithStatus?.parsedToken) {
return;
}
this.#parsedTokenDetails = [
{
name: i18nString(UIStrings.origin),
value: {
text: this.#tokenWithStatus.parsedToken.origin,
hasError: this.#tokenWithStatus.status === Protocol.Page.OriginTrialTokenStatus.WrongOrigin,
},
},
{
name: i18nString(UIStrings.expiryTime),
value: {
text: this.#dateFormatter.format(this.#tokenWithStatus.parsedToken.expiryTime * 1000),
hasError: this.#tokenWithStatus.status === Protocol.Page.OriginTrialTokenStatus.Expired
},
},
{
name: i18nString(UIStrings.usageRestriction),
value: {text: this.#tokenWithStatus.parsedToken.usageRestriction},
},
{
name: i18nString(UIStrings.isThirdParty),
value: {text: this.#tokenWithStatus.parsedToken.isThirdParty.toString()},
},
{
name: i18nString(UIStrings.matchSubDomains),
value: {text: this.#tokenWithStatus.parsedToken.matchSubDomains.toString()},
},
];
if (this.#tokenWithStatus.status === Protocol.Page.OriginTrialTokenStatus.UnknownTrial) {
this.#parsedTokenDetails = [
{
name: i18nString(UIStrings.trialName),
value: {text: this.#tokenWithStatus.parsedToken.trialName},
},
...this.#parsedTokenDetails,
];
}
this.requestUpdate();
}
override performUpdate(): void {
if (!this.#tokenWithStatus) {
return;
}
const viewInput: RowsViewInput = {
tokenWithStatus: this.#tokenWithStatus,
parsedTokenDetails: this.#parsedTokenDetails,
};
this.#view(viewInput, undefined, this.contentElement);
}
}
export interface OriginTrialTreeViewData {
trials: Protocol.Page.OriginTrial[];
}
type View = (input: OriginTrialTreeViewData, output: undefined, target: HTMLElement) => void;
const DEFAULT_VIEW: View = (input, _output, target) => {
if (!input.trials.length) {
// clang-format off
render(html`
<span class="status-badge">
<devtools-icon class="medium" name="clear"></devtools-icon>
<span>${i18nString(UIStrings.noTrialTokens)}</span>
</span>`, target);
// clang-format on
return;
}
// clang-format off
render(html`
<style>${originTrialTreeViewStyles}</style>
<devtools-tree .template=${html`
<style>${originTrialTreeViewStyles}</style>
<ul role="tree">
${input.trials.map(renderOriginTrialTree)}
</ul>
`}>
</devtools-tree>
`, target);
// clang-format on
};
export class OriginTrialTreeView extends UI.Widget.Widget {
#data: OriginTrialTreeViewData = {trials: []};
#view: View;
constructor(element?: HTMLElement, view: View = DEFAULT_VIEW) {
super(element, {useShadowDom: true});
this.#view = view;
}
set data(data: OriginTrialTreeViewData) {
this.#data = data;
this.requestUpdate();
}
override performUpdate(): void {
this.#view(this.#data, undefined, this.contentElement);
}
}