| // 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); |
| } |
| } |