Path: blob/main/src/vs/workbench/contrib/extensions/browser/extensionFeaturesTab.ts
5251 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 { getErrorMessage } from '../../../../base/common/errors.js';20import { PANEL_SECTION_BORDER } from '../../../common/theme.js';21import { IThemeService, Themable } from '../../../../platform/theme/common/themeService.js';22import { DomScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js';23import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';24import { ThemeIcon } from '../../../../base/common/themables.js';25import Severity from '../../../../base/common/severity.js';26import { errorIcon, infoIcon, warningIcon } from './extensionsIcons.js';27import { SeverityIcon } from '../../../../base/browser/ui/severityIcon/severityIcon.js';28import { KeybindingLabel } from '../../../../base/browser/ui/keybindingLabel/keybindingLabel.js';29import { OS } from '../../../../base/common/platform.js';30import { IMarkdownString, MarkdownString, isMarkdownString } from '../../../../base/common/htmlContent.js';31import { Color } from '../../../../base/common/color.js';32import { IExtensionService } from '../../../services/extensions/common/extensions.js';33import { Codicon } from '../../../../base/common/codicons.js';34import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';35import { ResolvedKeybinding } from '../../../../base/common/keybindings.js';36import { asCssVariable } from '../../../../platform/theme/common/colorUtils.js';37import { foreground, chartAxis, chartGuide, chartLine } from '../../../../platform/theme/common/colorRegistry.js';38import { IHoverService } from '../../../../platform/hover/browser/hover.js';39import { IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js';4041interface IExtensionFeatureElementRenderer extends IExtensionFeatureRenderer {42type: 'element';43render(manifest: IExtensionManifest): IRenderedData<HTMLElement>;44}4546class RuntimeStatusMarkdownRenderer extends Disposable implements IExtensionFeatureElementRenderer {4748static readonly ID = 'runtimeStatus';49readonly type = 'element';5051constructor(52@IExtensionService private readonly extensionService: IExtensionService,53@IHoverService private readonly hoverService: IHoverService,54@IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService,55@IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService,56) {57super();58}5960shouldRender(manifest: IExtensionManifest): boolean {61const extensionId = new ExtensionIdentifier(getExtensionId(manifest.publisher, manifest.name));62if (!this.extensionService.extensions.some(e => ExtensionIdentifier.equals(e.identifier, extensionId))) {63return false;64}65return !!manifest.main || !!manifest.browser;66}6768render(manifest: IExtensionManifest): IRenderedData<HTMLElement> {69const disposables = new DisposableStore();70const extensionId = new ExtensionIdentifier(getExtensionId(manifest.publisher, manifest.name));71const emitter = disposables.add(new Emitter<HTMLElement>());72disposables.add(this.extensionService.onDidChangeExtensionsStatus(e => {73if (e.some(extension => ExtensionIdentifier.equals(extension, extensionId))) {74emitter.fire(this.createElement(manifest, disposables));75}76}));77disposables.add(this.extensionFeaturesManagementService.onDidChangeAccessData(e => emitter.fire(this.createElement(manifest, disposables))));78return {79onDidChange: emitter.event,80data: this.createElement(manifest, disposables),81dispose: () => disposables.dispose()82};83}8485private createElement(manifest: IExtensionManifest, disposables: DisposableStore): HTMLElement {86const container = $('.runtime-status');87const extensionId = new ExtensionIdentifier(getExtensionId(manifest.publisher, manifest.name));88const status = this.extensionService.getExtensionsStatus()[extensionId.value];89if (this.extensionService.extensions.some(extension => ExtensionIdentifier.equals(extension.identifier, extensionId))) {90const data = new MarkdownString();91data.appendMarkdown(`### ${localize('activation', "Activation")}\n\n`);92if (status.activationTimes) {93if (status.activationTimes.activationReason.startup) {94data.appendMarkdown(`Activated on Startup: \`${status.activationTimes.activateCallTime}ms\``);95} else {96data.appendMarkdown(`Activated by \`${status.activationTimes.activationReason.activationEvent}\` event: \`${status.activationTimes.activateCallTime}ms\``);97}98} else {99data.appendMarkdown('Not yet activated');100}101this.renderMarkdown(data, container, disposables);102}103const features = Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).getExtensionFeatures();104for (const feature of features) {105const accessData = this.extensionFeaturesManagementService.getAccessData(extensionId, feature.id);106if (accessData) {107this.renderMarkdown(new MarkdownString(`\n ### ${localize('label', "{0} Usage", feature.label)}\n\n`), container, disposables);108if (accessData.accessTimes.length) {109const description = append(container,110$('.feature-chart-description',111undefined,112localize('chartDescription', "There were {0} {1} requests from this extension in the last 30 days.", accessData?.accessTimes.length, feature.accessDataLabel ?? feature.label)));113description.style.marginBottom = '8px';114this.renderRequestsChart(container, accessData.accessTimes, disposables);115}116const status = accessData?.current?.status;117if (status) {118const data = new MarkdownString();119if (status?.severity === Severity.Error) {120data.appendMarkdown(`$(${errorIcon.id}) ${status.message}\n\n`);121}122if (status?.severity === Severity.Warning) {123data.appendMarkdown(`$(${warningIcon.id}) ${status.message}\n\n`);124}125if (data.value) {126this.renderMarkdown(data, container, disposables);127}128}129}130}131if (status.runtimeErrors.length || status.messages.length) {132const data = new MarkdownString();133if (status.runtimeErrors.length) {134data.appendMarkdown(`\n ### ${localize('uncaught errors', "Uncaught Errors ({0})", status.runtimeErrors.length)}\n`);135for (const error of status.runtimeErrors) {136data.appendMarkdown(`$(${Codicon.error.id}) ${getErrorMessage(error)}\n\n`);137}138}139if (status.messages.length) {140data.appendMarkdown(`\n ### ${localize('messaages', "Messages ({0})", status.messages.length)}\n`);141for (const message of status.messages) {142data.appendMarkdown(`$(${(message.type === Severity.Error ? Codicon.error : message.type === Severity.Warning ? Codicon.warning : Codicon.info).id}) ${message.message}\n\n`);143}144}145if (data.value) {146this.renderMarkdown(data, container, disposables);147}148}149return container;150}151152private renderMarkdown(markdown: IMarkdownString, container: HTMLElement, disposables: DisposableStore): void {153const { element } = disposables.add(this.markdownRendererService.render({154value: markdown.value,155isTrusted: markdown.isTrusted,156supportThemeIcons: true157}));158append(container, element);159}160161private renderRequestsChart(container: HTMLElement, accessTimes: Date[], disposables: DisposableStore): void {162const width = 450;163const height = 250;164const margin = { top: 0, right: 4, bottom: 20, left: 4 };165const innerWidth = width - margin.left - margin.right;166const innerHeight = height - margin.top - margin.bottom;167168const chartContainer = append(container, $('.feature-chart-container'));169chartContainer.style.position = 'relative';170171const tooltip = append(chartContainer, $('.feature-chart-tooltip'));172tooltip.style.position = 'absolute';173tooltip.style.width = '0px';174tooltip.style.height = '0px';175176let maxCount = 100;177const map = new Map<string, number>();178for (const accessTime of accessTimes) {179const day = `${accessTime.getDate()} ${accessTime.toLocaleString('default', { month: 'short' })}`;180map.set(day, (map.get(day) ?? 0) + 1);181maxCount = Math.max(maxCount, map.get(day)!);182}183184const now = new Date();185type Point = { x: number; y: number; date: string; count: number };186const points: Point[] = [];187for (let i = 0; i <= 30; i++) {188const date = new Date(now);189date.setDate(now.getDate() - (30 - i));190const dateString = `${date.getDate()} ${date.toLocaleString('default', { month: 'short' })}`;191const count = map.get(dateString) ?? 0;192const x = (i / 30) * innerWidth;193const y = innerHeight - (count / maxCount) * innerHeight;194points.push({ x, y, date: dateString, count });195}196197const chart = append(chartContainer, $('.feature-chart'));198const svg = append(chart, $.SVG('svg'));199svg.setAttribute('width', `${width}px`);200svg.setAttribute('height', `${height}px`);201svg.setAttribute('viewBox', `0 0 ${width} ${height}`);202203const g = $.SVG('g');204g.setAttribute('transform', `translate(${margin.left},${margin.top})`);205svg.appendChild(g);206207const xAxisLine = $.SVG('line');208xAxisLine.setAttribute('x1', '0');209xAxisLine.setAttribute('y1', `${innerHeight}`);210xAxisLine.setAttribute('x2', `${innerWidth}`);211xAxisLine.setAttribute('y2', `${innerHeight}`);212xAxisLine.setAttribute('stroke', asCssVariable(chartAxis));213xAxisLine.setAttribute('stroke-width', '1px');214g.appendChild(xAxisLine);215216for (let i = 1; i <= 30; i += 7) {217const date = new Date(now);218date.setDate(now.getDate() - (30 - i));219const dateString = `${date.getDate()} ${date.toLocaleString('default', { month: 'short' })}`;220const x = (i / 30) * innerWidth;221222// Add vertical line223const tick = $.SVG('line');224tick.setAttribute('x1', `${x}`);225tick.setAttribute('y1', `${innerHeight}`);226tick.setAttribute('x2', `${x}`);227tick.setAttribute('y2', `${innerHeight + 10}`);228tick.setAttribute('stroke', asCssVariable(chartAxis));229tick.setAttribute('stroke-width', '1px');230g.appendChild(tick);231232const ruler = $.SVG('line');233ruler.setAttribute('x1', `${x}`);234ruler.setAttribute('y1', `0`);235ruler.setAttribute('x2', `${x}`);236ruler.setAttribute('y2', `${innerHeight}`);237ruler.setAttribute('stroke', asCssVariable(chartGuide));238ruler.setAttribute('stroke-width', '1px');239g.appendChild(ruler);240241const xAxisDate = $.SVG('text');242xAxisDate.setAttribute('x', `${x}`);243xAxisDate.setAttribute('y', `${height}`); // Adjusted y position to be within the SVG view port244xAxisDate.setAttribute('text-anchor', 'middle');245xAxisDate.setAttribute('fill', asCssVariable(foreground));246xAxisDate.setAttribute('font-size', '10px');247xAxisDate.textContent = dateString;248g.appendChild(xAxisDate);249}250251const line = $.SVG('polyline');252line.setAttribute('fill', 'none');253line.setAttribute('stroke', asCssVariable(chartLine));254line.setAttribute('stroke-width', `2px`);255line.setAttribute('points', points.map(p => `${p.x},${p.y}`).join(' '));256g.appendChild(line);257258const highlightCircle = $.SVG('circle');259highlightCircle.setAttribute('r', `4px`);260highlightCircle.style.display = 'none';261g.appendChild(highlightCircle);262263const hoverDisposable = disposables.add(new MutableDisposable<IDisposable>());264const mouseMoveListener = (event: MouseEvent): void => {265const rect = svg.getBoundingClientRect();266const mouseX = event.clientX - rect.left - margin.left;267268let closestPoint: Point | undefined;269let minDistance = Infinity;270271points.forEach(point => {272const distance = Math.abs(point.x - mouseX);273if (distance < minDistance) {274minDistance = distance;275closestPoint = point;276}277});278279if (closestPoint) {280highlightCircle.setAttribute('cx', `${closestPoint.x}`);281highlightCircle.setAttribute('cy', `${closestPoint.y}`);282highlightCircle.style.display = 'block';283tooltip.style.left = `${closestPoint.x + 24}px`;284tooltip.style.top = `${closestPoint.y + 14}px`;285hoverDisposable.value = this.hoverService.showInstantHover({286content: new MarkdownString(`${closestPoint.date}: ${closestPoint.count} requests`),287target: tooltip,288appearance: {289showPointer: true,290skipFadeInAnimation: true,291}292});293} else {294hoverDisposable.value = undefined;295}296};297disposables.add(addDisposableListener(svg, EventType.MOUSE_MOVE, mouseMoveListener));298299const mouseLeaveListener = () => {300highlightCircle.style.display = 'none';301hoverDisposable.value = undefined;302};303disposables.add(addDisposableListener(svg, EventType.MOUSE_LEAVE, mouseLeaveListener));304}305}306307308interface ILayoutParticipant {309layout(height?: number, width?: number): void;310}311312const runtimeStatusFeature = {313id: RuntimeStatusMarkdownRenderer.ID,314label: localize('runtime', "Runtime Status"),315access: {316canToggle: false317},318renderer: new SyncDescriptor(RuntimeStatusMarkdownRenderer),319};320321export class ExtensionFeaturesTab extends Themable {322323readonly domNode: HTMLElement;324325private readonly featureView = this._register(new MutableDisposable<ExtensionFeatureView>());326private featureViewDimension?: { height?: number; width?: number };327328private readonly layoutParticipants: ILayoutParticipant[] = [];329private readonly extensionId: ExtensionIdentifier;330331constructor(332private readonly manifest: IExtensionManifest,333private readonly feature: string | undefined,334@IThemeService themeService: IThemeService,335@IInstantiationService private readonly instantiationService: IInstantiationService336) {337super(themeService);338339this.extensionId = new ExtensionIdentifier(getExtensionId(manifest.publisher, manifest.name));340this.domNode = $('div.subcontent.feature-contributions');341this.create();342}343344layout(height?: number, width?: number): void {345this.layoutParticipants.forEach(participant => participant.layout(height, width));346}347348private create(): void {349const features = this.getFeatures();350if (features.length === 0) {351append($('.no-features'), this.domNode).textContent = localize('noFeatures', "No features contributed.");352return;353}354355const splitView = this._register(new SplitView<number>(this.domNode, {356orientation: Orientation.HORIZONTAL,357proportionalLayout: true358}));359this.layoutParticipants.push({360layout: (height: number, width: number) => {361splitView.el.style.height = `${height - 14}px`;362splitView.layout(width);363}364});365366const featuresListContainer = $('.features-list-container');367const list = this._register(this.createFeaturesList(featuresListContainer));368list.splice(0, list.length, features);369370const featureViewContainer = $('.feature-view-container');371this._register(list.onDidChangeSelection(e => {372const feature = e.elements[0];373if (feature) {374this.showFeatureView(feature, featureViewContainer);375}376}));377378const index = this.feature ? features.findIndex(f => f.id === this.feature) : 0;379list.setSelection([index === -1 ? 0 : index]);380381splitView.addView({382onDidChange: Event.None,383element: featuresListContainer,384minimumSize: 100,385maximumSize: Number.POSITIVE_INFINITY,386layout: (width, _, height) => {387featuresListContainer.style.width = `${width}px`;388list.layout(height, width);389}390}, 200, undefined, true);391392splitView.addView({393onDidChange: Event.None,394element: featureViewContainer,395minimumSize: 500,396maximumSize: Number.POSITIVE_INFINITY,397layout: (width, _, height) => {398featureViewContainer.style.width = `${width}px`;399this.featureViewDimension = { height, width };400this.layoutFeatureView();401}402}, Sizing.Distribute, undefined, true);403404splitView.style({405separatorBorder: this.theme.getColor(PANEL_SECTION_BORDER)!406});407}408409private createFeaturesList(container: HTMLElement): WorkbenchList<IExtensionFeatureDescriptor> {410const renderer = this.instantiationService.createInstance(ExtensionFeatureItemRenderer, this.extensionId);411const delegate = new ExtensionFeatureItemDelegate();412const list = this.instantiationService.createInstance(WorkbenchList, 'ExtensionFeaturesList', append(container, $('.features-list-wrapper')), delegate, [renderer], {413multipleSelectionSupport: false,414setRowLineHeight: false,415horizontalScrolling: false,416accessibilityProvider: {417getAriaLabel(extensionFeature: IExtensionFeatureDescriptor | null): string {418return extensionFeature?.label ?? '';419},420getWidgetAriaLabel(): string {421return localize('extension features list', "Extension Features");422}423},424openOnSingleClick: true425}) as WorkbenchList<IExtensionFeatureDescriptor>;426return list;427}428429private layoutFeatureView(): void {430this.featureView.value?.layout(this.featureViewDimension?.height, this.featureViewDimension?.width);431}432433private showFeatureView(feature: IExtensionFeatureDescriptor, container: HTMLElement): void {434if (this.featureView.value?.feature.id === feature.id) {435return;436}437clearNode(container);438this.featureView.value = this.instantiationService.createInstance(ExtensionFeatureView, this.extensionId, this.manifest, feature);439container.appendChild(this.featureView.value.domNode);440this.layoutFeatureView();441}442443private getFeatures(): IExtensionFeatureDescriptor[] {444const features = Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry)445.getExtensionFeatures().filter(feature => {446const renderer = this.getRenderer(feature);447const shouldRender = renderer?.shouldRender(this.manifest);448renderer?.dispose();449return shouldRender;450}).sort((a, b) => a.label.localeCompare(b.label));451452const renderer = this.getRenderer(runtimeStatusFeature);453if (renderer?.shouldRender(this.manifest)) {454features.splice(0, 0, runtimeStatusFeature);455}456renderer?.dispose();457return features;458}459460private getRenderer(feature: IExtensionFeatureDescriptor): IExtensionFeatureRenderer | undefined {461return feature.renderer ? this.instantiationService.createInstance(feature.renderer) : undefined;462}463464}465466interface IExtensionFeatureItemTemplateData {467readonly label: HTMLElement;468readonly disabledElement: HTMLElement;469readonly statusElement: HTMLElement;470readonly disposables: DisposableStore;471}472473class ExtensionFeatureItemDelegate implements IListVirtualDelegate<IExtensionFeatureDescriptor> {474getHeight() { return 22; }475getTemplateId() { return 'extensionFeatureDescriptor'; }476}477478class ExtensionFeatureItemRenderer implements IListRenderer<IExtensionFeatureDescriptor, IExtensionFeatureItemTemplateData> {479480readonly templateId = 'extensionFeatureDescriptor';481482constructor(483private readonly extensionId: ExtensionIdentifier,484@IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService485) { }486487renderTemplate(container: HTMLElement): IExtensionFeatureItemTemplateData {488container.classList.add('extension-feature-list-item');489const label = append(container, $('.extension-feature-label'));490const disabledElement = append(container, $('.extension-feature-disabled-label'));491disabledElement.textContent = localize('revoked', "No Access");492const statusElement = append(container, $('.extension-feature-status'));493return { label, disabledElement, statusElement, disposables: new DisposableStore() };494}495496renderElement(element: IExtensionFeatureDescriptor, index: number, templateData: IExtensionFeatureItemTemplateData) {497templateData.disposables.clear();498templateData.label.textContent = element.label;499templateData.disabledElement.style.display = element.id === runtimeStatusFeature.id || this.extensionFeaturesManagementService.isEnabled(this.extensionId, element.id) ? 'none' : 'inherit';500501templateData.disposables.add(this.extensionFeaturesManagementService.onDidChangeEnablement(({ extension, featureId, enabled }) => {502if (ExtensionIdentifier.equals(extension, this.extensionId) && featureId === element.id) {503templateData.disabledElement.style.display = enabled ? 'none' : 'inherit';504}505}));506507const statusElementClassName = templateData.statusElement.className;508const updateStatus = () => {509const accessData = this.extensionFeaturesManagementService.getAccessData(this.extensionId, element.id);510if (accessData?.current?.status) {511templateData.statusElement.style.display = 'inherit';512templateData.statusElement.className = `${statusElementClassName} ${SeverityIcon.className(accessData.current.status.severity)}`;513} else {514templateData.statusElement.style.display = 'none';515}516};517updateStatus();518templateData.disposables.add(this.extensionFeaturesManagementService.onDidChangeAccessData(({ extension, featureId }) => {519if (ExtensionIdentifier.equals(extension, this.extensionId) && featureId === element.id) {520updateStatus();521}522}));523}524525disposeElement(element: IExtensionFeatureDescriptor, index: number, templateData: IExtensionFeatureItemTemplateData): void {526templateData.disposables.dispose();527}528529disposeTemplate(templateData: IExtensionFeatureItemTemplateData) {530templateData.disposables.dispose();531}532533}534535class ExtensionFeatureView extends Disposable {536537readonly domNode: HTMLElement;538private readonly layoutParticipants: ILayoutParticipant[] = [];539540constructor(541private readonly extensionId: ExtensionIdentifier,542private readonly manifest: IExtensionManifest,543readonly feature: IExtensionFeatureDescriptor,544@IInstantiationService private readonly instantiationService: IInstantiationService,545@IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService,546@IDialogService private readonly dialogService: IDialogService,547@IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService,548) {549super();550551this.domNode = $('.extension-feature-content');552this.create(this.domNode);553}554555private create(content: HTMLElement): void {556const header = append(content, $('.feature-header'));557const title = append(header, $('.feature-title'));558title.textContent = this.feature.label;559560if (this.feature.access.canToggle) {561const actionsContainer = append(header, $('.feature-actions'));562const button = new Button(actionsContainer, defaultButtonStyles);563this.updateButtonLabel(button);564this._register(this.extensionFeaturesManagementService.onDidChangeEnablement(({ extension, featureId }) => {565if (ExtensionIdentifier.equals(extension, this.extensionId) && featureId === this.feature.id) {566this.updateButtonLabel(button);567}568}));569this._register(button.onDidClick(async () => {570const enabled = this.extensionFeaturesManagementService.isEnabled(this.extensionId, this.feature.id);571const confirmationResult = await this.dialogService.confirm({572title: localize('accessExtensionFeature', "Enable '{0}' Feature", this.feature.label),573message: enabled574? localize('disableAccessExtensionFeatureMessage', "Would you like to revoke '{0}' extension to access '{1}' feature?", this.manifest.displayName ?? this.extensionId.value, this.feature.label)575: localize('enableAccessExtensionFeatureMessage', "Would you like to allow '{0}' extension to access '{1}' feature?", this.manifest.displayName ?? this.extensionId.value, this.feature.label),576custom: true,577primaryButton: enabled ? localize('revoke', "Revoke Access") : localize('grant', "Allow Access"),578cancelButton: localize('cancel', "Cancel"),579});580if (confirmationResult.confirmed) {581this.extensionFeaturesManagementService.setEnablement(this.extensionId, this.feature.id, !enabled);582}583}));584}585586const body = append(content, $('.feature-body'));587588const bodyContent = $('.feature-body-content');589const scrollableContent = this._register(new DomScrollableElement(bodyContent, {}));590append(body, scrollableContent.getDomNode());591this.layoutParticipants.push({ layout: () => scrollableContent.scanDomNode() });592scrollableContent.scanDomNode();593594if (this.feature.description) {595const description = append(bodyContent, $('.feature-description'));596description.textContent = this.feature.description;597}598599const accessData = this.extensionFeaturesManagementService.getAccessData(this.extensionId, this.feature.id);600if (accessData?.current?.status) {601append(bodyContent, $('.feature-status', undefined,602$(`span${ThemeIcon.asCSSSelector(accessData.current.status.severity === Severity.Error ? errorIcon : accessData.current.status.severity === Severity.Warning ? warningIcon : infoIcon)}`, undefined),603$('span', undefined, accessData.current.status.message)));604}605606const featureContentElement = append(bodyContent, $('.feature-content'));607if (this.feature.renderer) {608const renderer = this.instantiationService.createInstance<IExtensionFeatureRenderer>(this.feature.renderer);609if (renderer.type === 'table') {610this.renderTableData(featureContentElement, <IExtensionFeatureTableRenderer>renderer);611} else if (renderer.type === 'markdown') {612this.renderMarkdownData(featureContentElement, <IExtensionFeatureMarkdownRenderer>renderer);613} else if (renderer.type === 'markdown+table') {614this.renderMarkdownAndTableData(featureContentElement, <IExtensionFeatureMarkdownAndTableRenderer>renderer);615} else if (renderer.type === 'element') {616this.renderElementData(featureContentElement, <IExtensionFeatureElementRenderer>renderer);617}618}619}620621private updateButtonLabel(button: Button): void {622button.label = this.extensionFeaturesManagementService.isEnabled(this.extensionId, this.feature.id) ? localize('revoke', "Revoke Access") : localize('enable', "Allow Access");623}624625private renderTableData(container: HTMLElement, renderer: IExtensionFeatureTableRenderer): void {626const tableData = this._register(renderer.render(this.manifest));627const tableDisposable = this._register(new MutableDisposable());628if (tableData.onDidChange) {629this._register(tableData.onDidChange(data => {630clearNode(container);631tableDisposable.value = this.renderTable(data, container);632}));633}634tableDisposable.value = this.renderTable(tableData.data, container);635}636637private renderTable(tableData: ITableData, container: HTMLElement): IDisposable {638const disposables = new DisposableStore();639append(container,640$('table', undefined,641$('tr', undefined,642...tableData.headers.map(header => $('th', undefined, header))643),644...tableData.rows645.map(row => {646return $('tr', undefined,647...row.map(rowData => {648if (typeof rowData === 'string') {649return $('td', undefined, $('p', undefined, rowData));650}651const data = Array.isArray(rowData) ? rowData : [rowData];652return $('td', undefined, ...data.map(item => {653const result: Node[] = [];654if (isMarkdownString(rowData)) {655const element = $('', undefined);656this.renderMarkdown(rowData, element);657result.push(element);658} else if (item instanceof ResolvedKeybinding) {659const element = $('');660const kbl = disposables.add(new KeybindingLabel(element, OS, defaultKeybindingLabelStyles));661kbl.set(item);662result.push(element);663} else if (item instanceof Color) {664result.push($('span', { class: 'colorBox', style: 'background-color: ' + Color.Format.CSS.format(item) }, ''));665result.push($('code', undefined, Color.Format.CSS.formatHex(item)));666}667return result;668}).flat());669})670);671})));672return disposables;673}674675private renderMarkdownAndTableData(container: HTMLElement, renderer: IExtensionFeatureMarkdownAndTableRenderer): void {676const markdownAndTableData = this._register(renderer.render(this.manifest));677if (markdownAndTableData.onDidChange) {678this._register(markdownAndTableData.onDidChange(data => {679clearNode(container);680this.renderMarkdownAndTable(data, container);681}));682}683this.renderMarkdownAndTable(markdownAndTableData.data, container);684}685686private renderMarkdownData(container: HTMLElement, renderer: IExtensionFeatureMarkdownRenderer): void {687container.classList.add('markdown');688const markdownData = this._register(renderer.render(this.manifest));689if (markdownData.onDidChange) {690this._register(markdownData.onDidChange(data => {691clearNode(container);692this.renderMarkdown(data, container);693}));694}695this.renderMarkdown(markdownData.data, container);696}697698private renderMarkdown(markdown: IMarkdownString, container: HTMLElement): void {699const { element } = this._register(this.markdownRendererService.render({700value: markdown.value,701isTrusted: markdown.isTrusted,702supportThemeIcons: true703}));704append(container, element);705}706707private renderMarkdownAndTable(data: Array<IMarkdownString | ITableData>, container: HTMLElement): void {708for (const markdownOrTable of data) {709if (isMarkdownString(markdownOrTable)) {710const element = $('', undefined);711this.renderMarkdown(markdownOrTable, element);712append(container, element);713} else {714const tableElement = append(container, $('table'));715this.renderTable(markdownOrTable, tableElement);716}717}718}719720private renderElementData(container: HTMLElement, renderer: IExtensionFeatureElementRenderer): void {721const elementData = this._register(renderer.render(this.manifest));722if (elementData.onDidChange) {723this._register(elementData.onDidChange(data => {724clearNode(container);725container.appendChild(data);726}));727}728container.appendChild(elementData.data);729}730731layout(height?: number, width?: number): void {732this.layoutParticipants.forEach(p => p.layout(height, width));733}734735}736737738