blob: e3f7399f22daae7289679fb14523a5d1fb1c3a96 [file]
// Copyright 2026 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/components/data_grid/data_grid.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as Lit from '../../ui/lit/lit.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
import subAppsViewStyles from './subAppsView.css.js';
const {html} = Lit;
const UIStrings = {
/**
* @description Heading label for Sub Apps view
*/
subApps: 'Sub apps',
/**
* @description Heading label for Sibling Sub Apps view
*/
siblingSubApps: 'Sibling sub apps',
/**
* @description Column header for the sub-app name
*/
name: 'Name',
/**
* @description Column header for the sub-app scope
*/
scope: 'Scope',
/**
* @description Column header when Manifest ID and Start URL are identical across all sub-apps
*/
manifestIdAndStartUrl: 'Manifest ID / Start URL',
/**
* @description Column header for Manifest ID
*/
manifestId: 'Manifest ID',
/**
* @description Column header for Start URL
*/
startUrl: 'Start URL',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/application/SubAppsView.ts', UIStrings);
export const i18nString: i18n.LocalizeString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export interface SubAppEntry {
name: string;
scope: string;
manifestId: string;
startUrl: string;
}
export interface SubAppsViewInput {
subApps: SubAppEntry[];
isSiblingSubApps?: boolean;
}
/**
* Sub apps always share the parent app's origin, so, like the manifest's
* Start URL field, only the path (plus any query or fragment) is shown. The
* full URL is kept in the cell's tooltip. Falls back to the original string if
* it is not a valid absolute URL.
*/
function stripOrigin(url: string): string {
try {
const parsed = new URL(url);
return parsed.pathname + parsed.search + parsed.hash;
} catch {
return url;
}
}
type View = (input: SubAppsViewInput, output: undefined, target: HTMLElement) => void;
export const DEFAULT_VIEW: View = (input, output, target) => {
const hasDistinctManifestId =
input.subApps.some(subApp => subApp.manifestId && subApp.startUrl && subApp.manifestId !== subApp.startUrl);
const headingText = input.isSiblingSubApps ? i18nString(UIStrings.siblingSubApps) : i18nString(UIStrings.subApps);
// clang-format off
Lit.render(html`
<style>${subAppsViewStyles}</style>
<div jslog=${VisualLogging.pane('sub-apps')}>
<span class="heading">${headingText}</span>
<devtools-data-grid striped inline role="grid" aria-label=${headingText}>
<table>
<tr>
<th id="name" weight="10" sortable>${i18nString(UIStrings.name)}</th>
<th id="scope" weight="10" sortable>${i18nString(UIStrings.scope)}</th>
${hasDistinctManifestId ? html`
<th id="manifest-id" weight="10" sortable>${i18nString(UIStrings.manifestId)}</th>
<th id="start-url" weight="10" sortable>${i18nString(UIStrings.startUrl)}</th>
` : html`
<th id="manifest-id-start-url" weight="10" sortable>${i18nString(UIStrings.manifestIdAndStartUrl)}</th>
`}
</tr>
${input.subApps.map(subApp => html`
<tr>
<td>${subApp.name}</td>
<td title=${subApp.scope}>${stripOrigin(subApp.scope)}</td>
${hasDistinctManifestId ? html`
<td title=${subApp.manifestId}>${stripOrigin(subApp.manifestId)}</td>
<td title=${subApp.startUrl}>${stripOrigin(subApp.startUrl)}</td>
` : html`
<td title=${subApp.manifestId || subApp.startUrl}>${stripOrigin(subApp.manifestId || subApp.startUrl)}</td>
`}
</tr>
`)}
</table>
</devtools-data-grid>
</div>
`, target);
// clang-format on
};
export class SubAppsView extends UI.Widget.VBox {
#subApps: SubAppEntry[] = [];
#isSiblingSubApps = false;
#view: View;
constructor(element?: HTMLElement, view: View = DEFAULT_VIEW) {
super(element);
this.#view = view;
}
get subApps(): SubAppEntry[] {
return this.#subApps;
}
set subApps(subApps: SubAppEntry[]) {
if (subApps === this.#subApps) {
return;
}
this.#subApps = subApps;
this.requestUpdate();
}
get isSiblingSubApps(): boolean {
return this.#isSiblingSubApps;
}
set isSiblingSubApps(isSiblingSubApps: boolean) {
if (isSiblingSubApps === this.#isSiblingSubApps) {
return;
}
this.#isSiblingSubApps = isSiblingSubApps;
this.requestUpdate();
}
override async performUpdate(): Promise<void> {
this.#view({subApps: this.#subApps, isSiblingSubApps: this.#isSiblingSubApps}, undefined, this.contentElement);
}
}