Path: blob/main/src/vs/workbench/contrib/extensions/browser/extensionFeaturesTab.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js';6import { $, append, clearNode, addDisposableListener, EventType } from '../../../../base/browser/dom.js';7import { Emitter, Event } from '../../../../base/common/event.js';8import { ExtensionIdentifier, IExtensionManifest } from '../../../../platform/extensions/common/extensions.js';9import { Orientation, Sizing, SplitView } from '../../../../base/browser/ui/splitview/splitview.js';10import { IExtensionFeatureDescriptor, Extensions, IExtensionFeaturesRegistry, IExtensionFeatureRenderer, IExtensionFeaturesManagementService, IExtensionFeatureTableRenderer, IExtensionFeatureMarkdownRenderer, ITableData, IRenderedData, IExtensionFeatureMarkdownAndTableRenderer } from '../../../services/extensionManagement/common/extensionFeatures.js';11import { Registry } from '../../../../platform/registry/common/platform.js';12import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';13import { localize } from '../../../../nls.js';14import { WorkbenchList } from '../../../../platform/list/browser/listService.js';15import { getExtensionId } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js';16import { IListRenderer, IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js';17import { Button } from '../../../../base/browser/ui/button/button.js';18import { defaultButtonStyles, defaultKeybindingLabelStyles } from '../../../../platform/theme/browser/defaultStyles.js';19import { renderMarkdown } from '../../../../base/browser/markdownRenderer.js';20import { getErrorMessage, onUnexpectedError } from '../../../../base/common/errors.js';21import { IOpenerService } from '../../../../platform/opener/common/opener.js';22import { PANEL_SECTION_BORDER } from '../../../common/theme.js';23import { IThemeService, Themable } from '../../../../platform/theme/common/themeService.js';24import { DomScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js';25import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';26import { ThemeIcon } from '../../../../base/common/themables.js';27import Severity from '../../../../base/common/severity.js';28import { errorIcon, infoIcon, warningIcon } from './extensionsIcons.js';29import { SeverityIcon } from '../../../../base/browser/ui/severityIcon/severityIcon.js';30import { KeybindingLabel } from '../../../../base/browser/ui/keybindingLabel/keybindingLabel.js';31import { OS } from '../../../../base/common/platform.js';32import { IMarkdownString, MarkdownString, isMarkdownString } from '../../../../base/common/htmlContent.js';33import { Color } from '../../../../base/common/color.js';34import { IExtensionService } from '../../../services/extensions/common/extensions.js';35import { Codicon } from '../../../../base/common/codicons.js';36import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';37import { ResolvedKeybinding } from '../../../../base/common/keybindings.js';38import { asCssVariable } from '../../../../platform/theme/common/colorUtils.js';39import { foreground, chartAxis, chartGuide, chartLine } from '../../../../platform/theme/common/colorRegistry.js';40import { IHoverService } from '../../../../platform/hover/browser/hover.js';4142interface IExtensionFeatureElementRenderer extends IExtensionFeatureRenderer {43type: 'element';44render(manifest: IExtensionManifest): IRenderedData<HTMLElement>;45}4647class RuntimeStatusMarkdownRenderer extends Disposable implements IExtensionFeatureElementRenderer {4849static readonly ID = 'runtimeStatus';50readonly type = 'element';5152constructor(53@IExtensionService private readonly extensionService: IExtensionService,54@IOpenerService private readonly openerService: IOpenerService,55@IHoverService private readonly hoverService: IHoverService,56@IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService,57) {58super();59}6061shouldRender(manifest: IExtensionManifest): boolean {62const extensionId = new ExtensionIdentifier(getExtensionId(manifest.publisher, manifest.name));63if (!this.extensionService.extensions.some(e => ExtensionIdentifier.equals(e.identifier, extensionId))) {64return false;65}66return !!manifest.main || !!manifest.browser;67}6869render(manifest: IExtensionManifest): IRenderedData<HTMLElement> {70const disposables = new DisposableStore();71const extensionId = new ExtensionIdentifier(getExtensionId(manifest.publisher, manifest.name));72const emitter = disposables.add(new Emitter<HTMLElement>());73disposables.add(this.extensionService.onDidChangeExtensionsStatus(e => {74if (e.some(extension => ExtensionIdentifier.equals(extension, extensionId))) {75emitter.fire(this.createElement(manifest, disposables));76}77}));78disposables.add(this.extensionFeaturesManagementService.onDidChangeAccessData(e => emitter.fire(this.createElement(manifest, disposables))));79return {80onDidChange: emitter.event,81data: this.createElement(manifest, disposables),82dispose: () => disposables.dispose()83};84}8586private createElement(manifest: IExtensionManifest, disposables: DisposableStore): HTMLElement {87const container = $('.runtime-status');88const extensionId = new ExtensionIdentifier(getExtensionId(manifest.publisher, manifest.name));89const status = this.extensionService.getExtensionsStatus()[extensionId.value];90if (this.extensionService.extensions.some(extension => ExtensionIdentifier.equals(extension.identifier, extensionId))) {91const data = new MarkdownString();92data.appendMarkdown(`### ${localize('activation', "Activation")}\n\n`);93if (status.activationTimes) {94if (status.activationTimes.activationReason.startup) {95data.appendMarkdown(`Activated on Startup: \`${status.activationTimes.activateCallTime}ms\``);96} else {97data.appendMarkdown(`Activated by \`${status.activationTimes.activationReason.activationEvent}\` event: \`${status.activationTimes.activateCallTime}ms\``);98}99} else {100data.appendMarkdown('Not yet activated');101}102this.renderMarkdown(data, container, disposables);103}104const features = Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).getExtensionFeatures();105for (const feature of features) {106const accessData = this.extensionFeaturesManagementService.getAccessData(extensionId, feature.id);107if (accessData) {108this.renderMarkdown(new MarkdownString(`\n ### ${localize('label', "{0} Usage", feature.label)}\n\n`), container, disposables);109if (accessData.accessTimes.length) {110const description = append(container,111$('.feature-chart-description',112undefined,113localize('chartDescription', "There were {0} {1} requests from this extension in the last 30 days.", accessData?.accessTimes.length, feature.accessDataLabel ?? feature.label)));114description.style.marginBottom = '8px';115this.renderRequestsChart(container, accessData.accessTimes, disposables);116}117const status = accessData?.current?.status;118if (status) {119const data = new MarkdownString();120if (status?.severity === Severity.Error) {121data.appendMarkdown(`$(${errorIcon.id}) ${status.message}\n\n`);122}123if (status?.severity === Severity.Warning) {124data.appendMarkdown(`$(${warningIcon.id}) ${status.message}\n\n`);125}126if (data.value) {127this.renderMarkdown(data, container, disposables);128}129}130}131}132if (status.runtimeErrors.length || status.messages.length) {133const data = new MarkdownString();134if (status.runtimeErrors.length) {135data.appendMarkdown(`\n ### ${localize('uncaught errors', "Uncaught Errors ({0})", status.runtimeErrors.length)}\n`);136for (const error of status.runtimeErrors) {137data.appendMarkdown(`$(${Codicon.error.id}) ${getErrorMessage(error)}\n\n`);138}139}140if (status.messages.length) {141data.appendMarkdown(`\n ### ${localize('messaages', "Messages ({0})", status.messages.length)}\n`);142for (const message of status.messages) {143data.appendMarkdown(`$(${(message.type === Severity.Error ? Codicon.error : message.type === Severity.Warning ? Codicon.warning : Codicon.info).id}) ${message.message}\n\n`);144}145}146if (data.value) {147this.renderMarkdown(data, container, disposables);148}149}150return container;151}152153private renderMarkdown(markdown: IMarkdownString, container: HTMLElement, disposables: DisposableStore): void {154const { element } = disposables.add(renderMarkdown(155{156value: markdown.value,157isTrusted: markdown.isTrusted,158supportThemeIcons: true159},160{161actionHandler: (content) => this.openerService.open(content, { allowCommands: !!markdown.isTrusted }).catch(onUnexpectedError),162}));163append(container, element);164}165166private renderRequestsChart(container: HTMLElement, accessTimes: Date[], disposables: DisposableStore): void {167const width = 450;168const height = 250;169const margin = { top: 0, right: 4, bottom: 20, left: 4 };170const innerWidth = width - margin.left - margin.right;171const innerHeight = height - margin.top - margin.bottom;172173const chartContainer = append(container, $('.feature-chart-container'));174chartContainer.style.position = 'relative';175176const tooltip = append(chartContainer, $('.feature-chart-tooltip'));177tooltip.style.position = 'absolute';178tooltip.style.width = '0px';179tooltip.style.height = '0px';180181let maxCount = 100;182const map = new Map<string, number>();183for (const accessTime of accessTimes) {184const day = `${accessTime.getDate()} ${accessTime.toLocaleString('default', { month: 'short' })}`;185map.set(day, (map.get(day) ?? 0) + 1);186maxCount = Math.max(maxCount, map.get(day)!);187}188189const now = new Date();190type Point = { x: number; y: number; date: string; count: number };191const points: Point[] = [];192for (let i = 0; i <= 30; i++) {193const date = new Date(now);194date.setDate(now.getDate() - (30 - i));195const dateString = `${date.getDate()} ${date.toLocaleString('default', { month: 'short' })}`;196const count = map.get(dateString) ?? 0;197const x = (i / 30) * innerWidth;198const y = innerHeight - (count / maxCount) * innerHeight;199points.push({ x, y, date: dateString, count });200}201202const chart = append(chartContainer, $('.feature-chart'));203const svg = append(chart, $.SVG('svg'));204svg.setAttribute('width', `${width}px`);205svg.setAttribute('height', `${height}px`);206svg.setAttribute('viewBox', `0 0 ${width} ${height}`);207208const g = $.SVG('g');209g.setAttribute('transform', `translate(${margin.left},${margin.top})`);210svg.appendChild(g);211212const xAxisLine = $.SVG('line');213xAxisLine.setAttribute('x1', '0');214xAxisLine.setAttribute('y1', `${innerHeight}`);215xAxisLine.setAttribute('x2', `${innerWidth}`);216xAxisLine.setAttribute('y2', `${innerHeight}`);217xAxisLine.setAttribute('stroke', asCssVariable(chartAxis));218xAxisLine.setAttribute('stroke-width', '1px');219g.appendChild(xAxisLine);220221for (let i = 1; i <= 30; i += 7) {222const date = new Date(now);223date.setDate(now.getDate() - (30 - i));224const dateString = `${date.getDate()} ${date.toLocaleString('default', { month: 'short' })}`;225const x = (i / 30) * innerWidth;226227// Add vertical line228const tick = $.SVG('line');229tick.setAttribute('x1', `${x}`);230tick.setAttribute('y1', `${innerHeight}`);231tick.setAttribute('x2', `${x}`);232tick.setAttribute('y2', `${innerHeight + 10}`);233tick.setAttribute('stroke', asCssVariable(chartAxis));234tick.setAttribute('stroke-width', '1px');235g.appendChild(tick);236237const ruler = $.SVG('line');238ruler.setAttribute('x1', `${x}`);239ruler.setAttribute('y1', `0`);240ruler.setAttribute('x2', `${x}`);241ruler.setAttribute('y2', `${innerHeight}`);242ruler.setAttribute('stroke', asCssVariable(chartGuide));243ruler.setAttribute('stroke-width', '1px');244g.appendChild(ruler);245246const xAxisDate = $.SVG('text');247xAxisDate.setAttribute('x', `${x}`);248xAxisDate.setAttribute('y', `${height}`); // Adjusted y position to be within the SVG view port249xAxisDate.setAttribute('text-anchor', 'middle');250xAxisDate.setAttribute('fill', asCssVariable(foreground));251xAxisDate.setAttribute('font-size', '10px');252xAxisDate.textContent = dateString;253g.appendChild(xAxisDate);254}255256const line = $.SVG('polyline');257line.setAttribute('fill', 'none');258line.setAttribute('stroke', asCssVariable(chartLine));259line.setAttribute('stroke-width', `2px`);260line.setAttribute('points', points.map(p => `${p.x},${p.y}`).join(' '));261g.appendChild(line);262263const highlightCircle = $.SVG('circle');264highlightCircle.setAttribute('r', `4px`);265highlightCircle.style.display = 'none';266g.appendChild(highlightCircle);267268const hoverDisposable = disposables.add(new MutableDisposable<IDisposable>());269const mouseMoveListener = (event: MouseEvent): void => {270const rect = svg.getBoundingClientRect();271const mouseX = event.clientX - rect.left - margin.left;272273let closestPoint: Point | undefined;274let minDistance = Infinity;275276points.forEach(point => {277const distance = Math.abs(point.x - mouseX);278if (distance < minDistance) {279minDistance = distance;280closestPoint = point;281}282});283284if (closestPoint) {285highlightCircle.setAttribute('cx', `${closestPoint.x}`);286highlightCircle.setAttribute('cy', `${closestPoint.y}`);287highlightCircle.style.display = 'block';288tooltip.style.left = `${closestPoint.x + 24}px`;289tooltip.style.top = `${closestPoint.y + 14}px`;290hoverDisposable.value = this.hoverService.showInstantHover({291content: new MarkdownString(`${closestPoint.date}: ${closestPoint.count} requests`),292target: tooltip,293appearance: {294showPointer: true,295skipFadeInAnimation: true,296}297});298} else {299hoverDisposable.value = undefined;300}301};302disposables.add(addDisposableListener(svg, EventType.MOUSE_MOVE, mouseMoveListener));303304const mouseLeaveListener = () => {305highlightCircle.style.display = 'none';306hoverDisposable.value = undefined;307};308disposables.add(addDisposableListener(svg, EventType.MOUSE_LEAVE, mouseLeaveListener));309}310}311312313interface ILayoutParticipant {314layout(height?: number, width?: number): void;315}316317const runtimeStatusFeature = {318id: RuntimeStatusMarkdownRenderer.ID,319label: localize('runtime', "Runtime Status"),320access: {321canToggle: false322},323renderer: new SyncDescriptor(RuntimeStatusMarkdownRenderer),324};325326export class ExtensionFeaturesTab extends Themable {327328readonly domNode: HTMLElement;329330private readonly featureView = this._register(new MutableDisposable<ExtensionFeatureView>());331private featureViewDimension?: { height?: number; width?: number };332333private readonly layoutParticipants: ILayoutParticipant[] = [];334private readonly extensionId: ExtensionIdentifier;335336constructor(337private readonly manifest: IExtensionManifest,338private readonly feature: string | undefined,339@IThemeService themeService: IThemeService,340@IInstantiationService private readonly instantiationService: IInstantiationService341) {342super(themeService);343344this.extensionId = new ExtensionIdentifier(getExtensionId(manifest.publisher, manifest.name));345this.domNode = $('div.subcontent.feature-contributions');346this.create();347}348349layout(height?: number, width?: number): void {350this.layoutParticipants.forEach(participant => participant.layout(height, width));351}352353private create(): void {354const features = this.getFeatures();355if (features.length === 0) {356append($('.no-features'), this.domNode).textContent = localize('noFeatures', "No features contributed.");357return;358}359360const splitView = this._register(new SplitView<number>(this.domNode, {361orientation: Orientation.HORIZONTAL,362proportionalLayout: true363}));364this.layoutParticipants.push({365layout: (height: number, width: number) => {366splitView.el.style.height = `${height - 14}px`;367splitView.layout(width);368}369});370371const featuresListContainer = $('.features-list-container');372const list = this._register(this.createFeaturesList(featuresListContainer));373list.splice(0, list.length, features);374375const featureViewContainer = $('.feature-view-container');376this._register(list.onDidChangeSelection(e => {377const feature = e.elements[0];378if (feature) {379this.showFeatureView(feature, featureViewContainer);380}381}));382383const index = this.feature ? features.findIndex(f => f.id === this.feature) : 0;384list.setSelection([index === -1 ? 0 : index]);385386splitView.addView({387onDidChange: Event.None,388element: featuresListContainer,389minimumSize: 100,390maximumSize: Number.POSITIVE_INFINITY,391layout: (width, _, height) => {392featuresListContainer.style.width = `${width}px`;393list.layout(height, width);394}395}, 200, undefined, true);396397splitView.addView({398onDidChange: Event.None,399element: featureViewContainer,400minimumSize: 500,401maximumSize: Number.POSITIVE_INFINITY,402layout: (width, _, height) => {403featureViewContainer.style.width = `${width}px`;404this.featureViewDimension = { height, width };405this.layoutFeatureView();406}407}, Sizing.Distribute, undefined, true);408409splitView.style({410separatorBorder: this.theme.getColor(PANEL_SECTION_BORDER)!411});412}413414private createFeaturesList(container: HTMLElement): WorkbenchList<IExtensionFeatureDescriptor> {415const renderer = this.instantiationService.createInstance(ExtensionFeatureItemRenderer, this.extensionId);416const delegate = new ExtensionFeatureItemDelegate();417const list = this.instantiationService.createInstance(WorkbenchList, 'ExtensionFeaturesList', append(container, $('.features-list-wrapper')), delegate, [renderer], {418multipleSelectionSupport: false,419setRowLineHeight: false,420horizontalScrolling: false,421accessibilityProvider: {422getAriaLabel(extensionFeature: IExtensionFeatureDescriptor | null): string {423return extensionFeature?.label ?? '';424},425getWidgetAriaLabel(): string {426return localize('extension features list', "Extension Features");427}428},429openOnSingleClick: true430}) as WorkbenchList<IExtensionFeatureDescriptor>;431return list;432}433434private layoutFeatureView(): void {435this.featureView.value?.layout(this.featureViewDimension?.height, this.featureViewDimension?.width);436}437438private showFeatureView(feature: IExtensionFeatureDescriptor, container: HTMLElement): void {439if (this.featureView.value?.feature.id === feature.id) {440return;441}442clearNode(container);443this.featureView.value = this.instantiationService.createInstance(ExtensionFeatureView, this.extensionId, this.manifest, feature);444container.appendChild(this.featureView.value.domNode);445this.layoutFeatureView();446}447448private getFeatures(): IExtensionFeatureDescriptor[] {449const features = Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry)450.getExtensionFeatures().filter(feature => {451const renderer = this.getRenderer(feature);452const shouldRender = renderer?.shouldRender(this.manifest);453renderer?.dispose();454return shouldRender;455}).sort((a, b) => a.label.localeCompare(b.label));456457const renderer = this.getRenderer(runtimeStatusFeature);458if (renderer?.shouldRender(this.manifest)) {459features.splice(0, 0, runtimeStatusFeature);460}461renderer?.dispose();462return features;463}464465private getRenderer(feature: IExtensionFeatureDescriptor): IExtensionFeatureRenderer | undefined {466return feature.renderer ? this.instantiationService.createInstance(feature.renderer) : undefined;467}468469}470471interface IExtensionFeatureItemTemplateData {472readonly label: HTMLElement;473readonly disabledElement: HTMLElement;474readonly statusElement: HTMLElement;475readonly disposables: DisposableStore;476}477478class ExtensionFeatureItemDelegate implements IListVirtualDelegate<IExtensionFeatureDescriptor> {479getHeight() { return 22; }480getTemplateId() { return 'extensionFeatureDescriptor'; }481}482483class ExtensionFeatureItemRenderer implements IListRenderer<IExtensionFeatureDescriptor, IExtensionFeatureItemTemplateData> {484485readonly templateId = 'extensionFeatureDescriptor';486487constructor(488private readonly extensionId: ExtensionIdentifier,489@IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService490) { }491492renderTemplate(container: HTMLElement): IExtensionFeatureItemTemplateData {493container.classList.add('extension-feature-list-item');494const label = append(container, $('.extension-feature-label'));495const disabledElement = append(container, $('.extension-feature-disabled-label'));496disabledElement.textContent = localize('revoked', "No Access");497const statusElement = append(container, $('.extension-feature-status'));498return { label, disabledElement, statusElement, disposables: new DisposableStore() };499}500501renderElement(element: IExtensionFeatureDescriptor, index: number, templateData: IExtensionFeatureItemTemplateData) {502templateData.disposables.clear();503templateData.label.textContent = element.label;504templateData.disabledElement.style.display = element.id === runtimeStatusFeature.id || this.extensionFeaturesManagementService.isEnabled(this.extensionId, element.id) ? 'none' : 'inherit';505506templateData.disposables.add(this.extensionFeaturesManagementService.onDidChangeEnablement(({ extension, featureId, enabled }) => {507if (ExtensionIdentifier.equals(extension, this.extensionId) && featureId === element.id) {508templateData.disabledElement.style.display = enabled ? 'none' : 'inherit';509}510}));511512const statusElementClassName = templateData.statusElement.className;513const updateStatus = () => {514const accessData = this.extensionFeaturesManagementService.getAccessData(this.extensionId, element.id);515if (accessData?.current?.status) {516templateData.statusElement.style.display = 'inherit';517templateData.statusElement.className = `${statusElementClassName} ${SeverityIcon.className(accessData.current.status.severity)}`;518} else {519templateData.statusElement.style.display = 'none';520}521};522updateStatus();523templateData.disposables.add(this.extensionFeaturesManagementService.onDidChangeAccessData(({ extension, featureId }) => {524if (ExtensionIdentifier.equals(extension, this.extensionId) && featureId === element.id) {525updateStatus();526}527}));528}529530disposeElement(element: IExtensionFeatureDescriptor, index: number, templateData: IExtensionFeatureItemTemplateData): void {531templateData.disposables.dispose();532}533534disposeTemplate(templateData: IExtensionFeatureItemTemplateData) {535templateData.disposables.dispose();536}537538}539540class ExtensionFeatureView extends Disposable {541542readonly domNode: HTMLElement;543private readonly layoutParticipants: ILayoutParticipant[] = [];544545constructor(546private readonly extensionId: ExtensionIdentifier,547private readonly manifest: IExtensionManifest,548readonly feature: IExtensionFeatureDescriptor,549@IOpenerService private readonly openerService: IOpenerService,550@IInstantiationService private readonly instantiationService: IInstantiationService,551@IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService,552@IDialogService private readonly dialogService: IDialogService,553) {554super();555556this.domNode = $('.extension-feature-content');557this.create(this.domNode);558}559560private create(content: HTMLElement): void {561const header = append(content, $('.feature-header'));562const title = append(header, $('.feature-title'));563title.textContent = this.feature.label;564565if (this.feature.access.canToggle) {566const actionsContainer = append(header, $('.feature-actions'));567const button = new Button(actionsContainer, defaultButtonStyles);568this.updateButtonLabel(button);569this._register(this.extensionFeaturesManagementService.onDidChangeEnablement(({ extension, featureId }) => {570if (ExtensionIdentifier.equals(extension, this.extensionId) && featureId === this.feature.id) {571this.updateButtonLabel(button);572}573}));574this._register(button.onDidClick(async () => {575const enabled = this.extensionFeaturesManagementService.isEnabled(this.extensionId, this.feature.id);576const confirmationResult = await this.dialogService.confirm({577title: localize('accessExtensionFeature', "Enable '{0}' Feature", this.feature.label),578message: enabled579? localize('disableAccessExtensionFeatureMessage', "Would you like to revoke '{0}' extension to access '{1}' feature?", this.manifest.displayName ?? this.extensionId.value, this.feature.label)580: localize('enableAccessExtensionFeatureMessage', "Would you like to allow '{0}' extension to access '{1}' feature?", this.manifest.displayName ?? this.extensionId.value, this.feature.label),581custom: true,582primaryButton: enabled ? localize('revoke', "Revoke Access") : localize('grant', "Allow Access"),583cancelButton: localize('cancel', "Cancel"),584});585if (confirmationResult.confirmed) {586this.extensionFeaturesManagementService.setEnablement(this.extensionId, this.feature.id, !enabled);587}588}));589}590591const body = append(content, $('.feature-body'));592593const bodyContent = $('.feature-body-content');594const scrollableContent = this._register(new DomScrollableElement(bodyContent, {}));595append(body, scrollableContent.getDomNode());596this.layoutParticipants.push({ layout: () => scrollableContent.scanDomNode() });597scrollableContent.scanDomNode();598599if (this.feature.description) {600const description = append(bodyContent, $('.feature-description'));601description.textContent = this.feature.description;602}603604const accessData = this.extensionFeaturesManagementService.getAccessData(this.extensionId, this.feature.id);605if (accessData?.current?.status) {606append(bodyContent, $('.feature-status', undefined,607$(`span${ThemeIcon.asCSSSelector(accessData.current.status.severity === Severity.Error ? errorIcon : accessData.current.status.severity === Severity.Warning ? warningIcon : infoIcon)}`, undefined),608$('span', undefined, accessData.current.status.message)));609}610611const featureContentElement = append(bodyContent, $('.feature-content'));612if (this.feature.renderer) {613const renderer = this.instantiationService.createInstance<IExtensionFeatureRenderer>(this.feature.renderer);614if (renderer.type === 'table') {615this.renderTableData(featureContentElement, <IExtensionFeatureTableRenderer>renderer);616} else if (renderer.type === 'markdown') {617this.renderMarkdownData(featureContentElement, <IExtensionFeatureMarkdownRenderer>renderer);618} else if (renderer.type === 'markdown+table') {619this.renderMarkdownAndTableData(featureContentElement, <IExtensionFeatureMarkdownAndTableRenderer>renderer);620} else if (renderer.type === 'element') {621this.renderElementData(featureContentElement, <IExtensionFeatureElementRenderer>renderer);622}623}624}625626private updateButtonLabel(button: Button): void {627button.label = this.extensionFeaturesManagementService.isEnabled(this.extensionId, this.feature.id) ? localize('revoke', "Revoke Access") : localize('enable', "Allow Access");628}629630private renderTableData(container: HTMLElement, renderer: IExtensionFeatureTableRenderer): void {631const tableData = this._register(renderer.render(this.manifest));632const tableDisposable = this._register(new MutableDisposable());633if (tableData.onDidChange) {634this._register(tableData.onDidChange(data => {635clearNode(container);636tableDisposable.value = this.renderTable(data, container);637}));638}639tableDisposable.value = this.renderTable(tableData.data, container);640}641642private renderTable(tableData: ITableData, container: HTMLElement): IDisposable {643const disposables = new DisposableStore();644append(container,645$('table', undefined,646$('tr', undefined,647...tableData.headers.map(header => $('th', undefined, header))648),649...tableData.rows650.map(row => {651return $('tr', undefined,652...row.map(rowData => {653if (typeof rowData === 'string') {654return $('td', undefined, $('p', undefined, rowData));655}656const data = Array.isArray(rowData) ? rowData : [rowData];657return $('td', undefined, ...data.map(item => {658const result: Node[] = [];659if (isMarkdownString(rowData)) {660const element = $('', undefined);661this.renderMarkdown(rowData, element);662result.push(element);663} else if (item instanceof ResolvedKeybinding) {664const element = $('');665const kbl = disposables.add(new KeybindingLabel(element, OS, defaultKeybindingLabelStyles));666kbl.set(item);667result.push(element);668} else if (item instanceof Color) {669result.push($('span', { class: 'colorBox', style: 'background-color: ' + Color.Format.CSS.format(item) }, ''));670result.push($('code', undefined, Color.Format.CSS.formatHex(item)));671}672return result;673}).flat());674})675);676})));677return disposables;678}679680private renderMarkdownAndTableData(container: HTMLElement, renderer: IExtensionFeatureMarkdownAndTableRenderer): void {681const markdownAndTableData = this._register(renderer.render(this.manifest));682if (markdownAndTableData.onDidChange) {683this._register(markdownAndTableData.onDidChange(data => {684clearNode(container);685this.renderMarkdownAndTable(data, container);686}));687}688this.renderMarkdownAndTable(markdownAndTableData.data, container);689}690691private renderMarkdownData(container: HTMLElement, renderer: IExtensionFeatureMarkdownRenderer): void {692container.classList.add('markdown');693const markdownData = this._register(renderer.render(this.manifest));694if (markdownData.onDidChange) {695this._register(markdownData.onDidChange(data => {696clearNode(container);697this.renderMarkdown(data, container);698}));699}700this.renderMarkdown(markdownData.data, container);701}702703private renderMarkdown(markdown: IMarkdownString, container: HTMLElement): void {704const { element } = this._register(renderMarkdown(705{706value: markdown.value,707isTrusted: markdown.isTrusted,708supportThemeIcons: true709},710{711actionHandler: (content) => this.openerService.open(content, { allowCommands: !!markdown.isTrusted }).catch(onUnexpectedError),712}));713append(container, element);714}715716private renderMarkdownAndTable(data: Array<IMarkdownString | ITableData>, container: HTMLElement): void {717for (const markdownOrTable of data) {718if (isMarkdownString(markdownOrTable)) {719const element = $('', undefined);720this.renderMarkdown(markdownOrTable, element);721append(container, element);722} else {723const tableElement = append(container, $('table'));724this.renderTable(markdownOrTable, tableElement);725}726}727}728729private renderElementData(container: HTMLElement, renderer: IExtensionFeatureElementRenderer): void {730const elementData = this._register(renderer.render(this.manifest));731if (elementData.onDidChange) {732this._register(elementData.onDidChange(data => {733clearNode(container);734container.appendChild(data);735}));736}737container.appendChild(elementData.data);738}739740layout(height?: number, width?: number): void {741this.layoutParticipants.forEach(p => p.layout(height, width));742}743744}745746747