blob: 4c455b21e53afadac1afdbed4f6184078d84d671 [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as i18n from '../../../core/i18n/i18n.js';
import * as Protocol from '../../../generated/protocol.js';
import * as Adorners from '../../../ui/components/adorners/adorners.js';
import * as ComponentHelpers from '../../../ui/components/helpers/helpers.js';
import * as TreeOutline from '../../../ui/components/tree_outline/tree_outline.js';
import * as LitHtml from '../../../ui/lit-html/lit-html.js';
import badgeStyles from './badge.css.js';
import originTrialTokenRowsStyles from './originTrialTokenRows.css.js';
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',
};
const str_ = i18n.i18n.registerUIStrings('panels/application/components/OriginTrialTreeView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export interface BadgeData {
badgeContent: string;
style: 'error'|'success'|'secondary';
}
export class Badge extends HTMLElement {
static readonly litTagName = LitHtml.literal`devtools-resources-origin-trial-tree-view-badge`;
readonly #shadow = this.attachShadow({mode: 'open'});
#adorner = new Adorners.Adorner.Adorner();
set data(data: BadgeData) {
this.#render(data);
}
connectedCallback(): void {
this.#shadow.adoptedStyleSheets = [badgeStyles];
}
#render(data: BadgeData): void {
const adornerContent = document.createElement('span');
adornerContent.textContent = data.badgeContent;
this.#adorner.data = {
name: 'badge',
content: adornerContent,
};
this.#adorner.classList.add(`badge-${data.style}`);
LitHtml.render(
LitHtml.html`
${this.#adorner}
`,
this.#shadow, {host: this});
}
}
ComponentHelpers.CustomElements.defineComponent('devtools-resources-origin-trial-tree-view-badge', Badge);
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 constructOriginTrialTree(originTrial: Protocol.Page.OriginTrial): TreeNode<OriginTrialTreeNodeData> {
return {
treeNodeData: originTrial,
id: 'OriginTrialTreeNode#' + originTrial.trialName,
children: async(): Promise<TreeNode<OriginTrialTreeNodeData>[]> => originTrial.tokensWithStatus.length > 1 ?
originTrial.tokensWithStatus.map(constructTokenNode) :
constructTokenDetailsNodes(originTrial.tokensWithStatus[0]),
renderer: (node: TreeNode<OriginTrialTreeNodeData>): LitHtml.TemplateResult => {
const trial = node.treeNodeData as Protocol.Page.OriginTrial;
const tokenCountBadge = LitHtml.html`
<${Badge.litTagName} .data=${{
badgeContent: i18nString(UIStrings.tokens, {PH1: trial.tokensWithStatus.length}),
style: 'secondary',
} as BadgeData}></${Badge.litTagName}>
`;
return LitHtml.html`
${trial.trialName}
<${Badge.litTagName} .data=${{
badgeContent: trial.status,
style: trial.status === Protocol.Page.OriginTrialStatus.Enabled ? 'success' : 'error',
} as BadgeData}></${Badge.litTagName}>
${trial.tokensWithStatus.length > 1 ? tokenCountBadge : LitHtml.nothing}
`;
},
};
}
function constructTokenNode(token: Protocol.Page.OriginTrialTokenWithStatus): TreeNode<OriginTrialTreeNodeData> {
return {
treeNodeData: token.status,
id: 'TokenNode#' + token.rawTokenText,
children: async(): Promise<TreeNode<OriginTrialTreeNodeData>[]> => constructTokenDetailsNodes(token),
renderer: (node: TreeNode<OriginTrialTreeNodeData>, state: {isExpanded: boolean}): LitHtml.TemplateResult => {
const tokenStatus = node.treeNodeData as string;
const statusBadge = LitHtml.html`
<${Badge.litTagName} .data=${{
badgeContent: tokenStatus,
style: tokenStatus === Protocol.Page.OriginTrialTokenStatus.Success ? 'success' : 'error',
} as BadgeData}></${Badge.litTagName}>
`;
// Only display token status for convenience when the node is not expanded.
return LitHtml.html`${i18nString(UIStrings.token)} ${state.isExpanded ? LitHtml.nothing : statusBadge}`;
},
};
}
interface TokenField {
name: string;
value: LitHtml.TemplateResult;
}
function renderTokenDetails(node: TreeNode<OriginTrialTreeNodeData>): LitHtml.TemplateResult {
return LitHtml.html`
<${OriginTrialTokenRows.litTagName} .data=${{node: node} as OriginTrialTokenRowsData}>
</${OriginTrialTokenRows.litTagName}>
`;
}
function constructTokenDetailsNodes(token: Protocol.Page.OriginTrialTokenWithStatus):
TreeNode<OriginTrialTreeNodeData>[] {
return [
{
treeNodeData: token,
id: 'TokenDetailsNode#' + token.rawTokenText,
renderer: renderTokenDetails,
},
constructRawTokenTextNode(token.rawTokenText),
];
}
function constructRawTokenTextNode(tokenText: string): TreeNode<OriginTrialTreeNodeData> {
return {
treeNodeData: i18nString(UIStrings.rawTokenText),
id: 'TokenRawTextContainerNode#' + tokenText,
children: async(): Promise<TreeNode<OriginTrialTreeNodeData>[]> => [{
treeNodeData: tokenText,
id: 'TokenRawTextNode#' + tokenText,
renderer: (data: TreeNode<OriginTrialTreeNodeData>): LitHtml.TemplateResult => {
const tokenText = data.treeNodeData as string;
return LitHtml.html`
<div style="overflow-wrap: break-word;">
${tokenText}
</div>
`;
},
}],
};
}
function defaultRenderer(node: TreeNode<OriginTrialTreeNodeData>): LitHtml.TemplateResult {
return LitHtml.html`${String(node.treeNodeData)}`;
}
export interface OriginTrialTokenRowsData {
node: TreeNode<OriginTrialTreeNodeData>;
}
export class OriginTrialTokenRows extends HTMLElement {
static readonly litTagName = LitHtml.literal`devtools-resources-origin-trial-token-rows`;
readonly #shadow = this.attachShadow({mode: 'open'});
#tokenWithStatus: Protocol.Page.OriginTrialTokenWithStatus|null = null;
#parsedTokenDetails: TokenField[] = [];
#dateFormatter: Intl.DateTimeFormat = new Intl.DateTimeFormat(
i18n.DevToolsLocale.DevToolsLocale.instance().locale,
{dateStyle: 'long', timeStyle: 'long'},
);
set data(data: OriginTrialTokenRowsData) {
this.#tokenWithStatus = data.node.treeNodeData as Protocol.Page.OriginTrialTokenWithStatus;
this.#setTokenFields();
}
connectedCallback(): void {
this.#shadow.adoptedStyleSheets = [originTrialTokenRowsStyles];
this.#render();
}
#renderTokenField = (fieldValue: string, hasError?: boolean): LitHtml.TemplateResult => LitHtml.html`
<div class=${LitHtml.Directives.ifDefined(hasError ? 'error-text' : undefined)}>
${fieldValue}
</div>`;
#setTokenFields(): void {
if (!this.#tokenWithStatus?.parsedToken) {
return;
}
this.#parsedTokenDetails = [
{
name: i18nString(UIStrings.origin),
value: this.#renderTokenField(
this.#tokenWithStatus.parsedToken.origin,
this.#tokenWithStatus.status === Protocol.Page.OriginTrialTokenStatus.WrongOrigin),
},
{
name: i18nString(UIStrings.expiryTime),
value: this.#renderTokenField(
this.#dateFormatter.format(this.#tokenWithStatus.parsedToken.expiryTime * 1000),
this.#tokenWithStatus.status === Protocol.Page.OriginTrialTokenStatus.Expired),
},
{
name: i18nString(UIStrings.usageRestriction),
value: this.#renderTokenField(this.#tokenWithStatus.parsedToken.usageRestriction),
},
{
name: i18nString(UIStrings.isThirdParty),
value: this.#renderTokenField(this.#tokenWithStatus.parsedToken.isThirdParty.toString()),
},
{
name: i18nString(UIStrings.matchSubDomains),
value: this.#renderTokenField(this.#tokenWithStatus.parsedToken.matchSubDomains.toString()),
},
];
if (this.#tokenWithStatus.status === Protocol.Page.OriginTrialTokenStatus.UnknownTrial) {
this.#parsedTokenDetails = [
{
name: i18nString(UIStrings.trialName),
value: this.#renderTokenField(this.#tokenWithStatus.parsedToken.trialName),
},
...this.#parsedTokenDetails,
];
}
}
#render(): void {
if (!this.#tokenWithStatus) {
return;
}
const tokenDetails: TokenField[] = [
{
name: i18nString(UIStrings.status),
value: LitHtml.html`
<${Badge.litTagName} .data=${{
badgeContent: this.#tokenWithStatus.status,
style: this.#tokenWithStatus.status === Protocol.Page.OriginTrialTokenStatus.Success ? 'success' : 'error',
} as BadgeData}></${Badge.litTagName}>`,
},
...this.#parsedTokenDetails,
];
const tokenDetailRows = tokenDetails.map((field: TokenField): LitHtml.TemplateResult => {
return LitHtml.html`
<div class="key">${field.name}</div>
<div class="value">${field.value}</div>
`;
});
LitHtml.render(
LitHtml.html`
<div class="content">
${tokenDetailRows}
</div>
`,
this.#shadow, {host: this});
}
}
ComponentHelpers.CustomElements.defineComponent('devtools-resources-origin-trial-token-rows', OriginTrialTokenRows);
export interface OriginTrialTreeViewData {
trials: Protocol.Page.OriginTrial[];
}
export class OriginTrialTreeView extends HTMLElement {
static readonly litTagName = LitHtml.literal`devtools-resources-origin-trial-tree-view`;
readonly #shadow = this.attachShadow({mode: 'open'});
set data(data: OriginTrialTreeViewData) {
this.#render(data.trials);
}
#render(trials: Protocol.Page.OriginTrial[]): void {
if (!trials.length) {
return;
}
LitHtml.render(
LitHtml.html`
<${TreeOutline.TreeOutline.TreeOutline.litTagName} .data=${{
tree: trials.map(constructOriginTrialTree),
defaultRenderer,
} as TreeOutline.TreeOutline.TreeOutlineData<OriginTrialTreeNodeData>}>
</${TreeOutline.TreeOutline.TreeOutline.litTagName}>
`,
this.#shadow, {host: this});
}
}
ComponentHelpers.CustomElements.defineComponent('devtools-resources-origin-trial-tree-view', OriginTrialTreeView);
declare global {
interface HTMLElementTagNameMap {
'devtools-resources-origin-trial-tree-view': OriginTrialTreeView;
'devtools-resources-origin-trial-token-rows': OriginTrialTokenRows;
'devtools-resources-origin-trial-tree-view-badge': Badge;
}
}