Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/extensions/browser/extensionFeaturesTab.ts
5251 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
7
import { $, append, clearNode, addDisposableListener, EventType } from '../../../../base/browser/dom.js';
8
import { Emitter, Event } from '../../../../base/common/event.js';
9
import { ExtensionIdentifier, IExtensionManifest } from '../../../../platform/extensions/common/extensions.js';
10
import { Orientation, Sizing, SplitView } from '../../../../base/browser/ui/splitview/splitview.js';
11
import { IExtensionFeatureDescriptor, Extensions, IExtensionFeaturesRegistry, IExtensionFeatureRenderer, IExtensionFeaturesManagementService, IExtensionFeatureTableRenderer, IExtensionFeatureMarkdownRenderer, ITableData, IRenderedData, IExtensionFeatureMarkdownAndTableRenderer } from '../../../services/extensionManagement/common/extensionFeatures.js';
12
import { Registry } from '../../../../platform/registry/common/platform.js';
13
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
14
import { localize } from '../../../../nls.js';
15
import { WorkbenchList } from '../../../../platform/list/browser/listService.js';
16
import { getExtensionId } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js';
17
import { IListRenderer, IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js';
18
import { Button } from '../../../../base/browser/ui/button/button.js';
19
import { defaultButtonStyles, defaultKeybindingLabelStyles } from '../../../../platform/theme/browser/defaultStyles.js';
20
import { getErrorMessage } from '../../../../base/common/errors.js';
21
import { PANEL_SECTION_BORDER } from '../../../common/theme.js';
22
import { IThemeService, Themable } from '../../../../platform/theme/common/themeService.js';
23
import { DomScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js';
24
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
25
import { ThemeIcon } from '../../../../base/common/themables.js';
26
import Severity from '../../../../base/common/severity.js';
27
import { errorIcon, infoIcon, warningIcon } from './extensionsIcons.js';
28
import { SeverityIcon } from '../../../../base/browser/ui/severityIcon/severityIcon.js';
29
import { KeybindingLabel } from '../../../../base/browser/ui/keybindingLabel/keybindingLabel.js';
30
import { OS } from '../../../../base/common/platform.js';
31
import { IMarkdownString, MarkdownString, isMarkdownString } from '../../../../base/common/htmlContent.js';
32
import { Color } from '../../../../base/common/color.js';
33
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
34
import { Codicon } from '../../../../base/common/codicons.js';
35
import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';
36
import { ResolvedKeybinding } from '../../../../base/common/keybindings.js';
37
import { asCssVariable } from '../../../../platform/theme/common/colorUtils.js';
38
import { foreground, chartAxis, chartGuide, chartLine } from '../../../../platform/theme/common/colorRegistry.js';
39
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
40
import { IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js';
41
42
interface IExtensionFeatureElementRenderer extends IExtensionFeatureRenderer {
43
type: 'element';
44
render(manifest: IExtensionManifest): IRenderedData<HTMLElement>;
45
}
46
47
class RuntimeStatusMarkdownRenderer extends Disposable implements IExtensionFeatureElementRenderer {
48
49
static readonly ID = 'runtimeStatus';
50
readonly type = 'element';
51
52
constructor(
53
@IExtensionService private readonly extensionService: IExtensionService,
54
@IHoverService private readonly hoverService: IHoverService,
55
@IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService,
56
@IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService,
57
) {
58
super();
59
}
60
61
shouldRender(manifest: IExtensionManifest): boolean {
62
const extensionId = new ExtensionIdentifier(getExtensionId(manifest.publisher, manifest.name));
63
if (!this.extensionService.extensions.some(e => ExtensionIdentifier.equals(e.identifier, extensionId))) {
64
return false;
65
}
66
return !!manifest.main || !!manifest.browser;
67
}
68
69
render(manifest: IExtensionManifest): IRenderedData<HTMLElement> {
70
const disposables = new DisposableStore();
71
const extensionId = new ExtensionIdentifier(getExtensionId(manifest.publisher, manifest.name));
72
const emitter = disposables.add(new Emitter<HTMLElement>());
73
disposables.add(this.extensionService.onDidChangeExtensionsStatus(e => {
74
if (e.some(extension => ExtensionIdentifier.equals(extension, extensionId))) {
75
emitter.fire(this.createElement(manifest, disposables));
76
}
77
}));
78
disposables.add(this.extensionFeaturesManagementService.onDidChangeAccessData(e => emitter.fire(this.createElement(manifest, disposables))));
79
return {
80
onDidChange: emitter.event,
81
data: this.createElement(manifest, disposables),
82
dispose: () => disposables.dispose()
83
};
84
}
85
86
private createElement(manifest: IExtensionManifest, disposables: DisposableStore): HTMLElement {
87
const container = $('.runtime-status');
88
const extensionId = new ExtensionIdentifier(getExtensionId(manifest.publisher, manifest.name));
89
const status = this.extensionService.getExtensionsStatus()[extensionId.value];
90
if (this.extensionService.extensions.some(extension => ExtensionIdentifier.equals(extension.identifier, extensionId))) {
91
const data = new MarkdownString();
92
data.appendMarkdown(`### ${localize('activation', "Activation")}\n\n`);
93
if (status.activationTimes) {
94
if (status.activationTimes.activationReason.startup) {
95
data.appendMarkdown(`Activated on Startup: \`${status.activationTimes.activateCallTime}ms\``);
96
} else {
97
data.appendMarkdown(`Activated by \`${status.activationTimes.activationReason.activationEvent}\` event: \`${status.activationTimes.activateCallTime}ms\``);
98
}
99
} else {
100
data.appendMarkdown('Not yet activated');
101
}
102
this.renderMarkdown(data, container, disposables);
103
}
104
const features = Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).getExtensionFeatures();
105
for (const feature of features) {
106
const accessData = this.extensionFeaturesManagementService.getAccessData(extensionId, feature.id);
107
if (accessData) {
108
this.renderMarkdown(new MarkdownString(`\n ### ${localize('label', "{0} Usage", feature.label)}\n\n`), container, disposables);
109
if (accessData.accessTimes.length) {
110
const description = append(container,
111
$('.feature-chart-description',
112
undefined,
113
localize('chartDescription', "There were {0} {1} requests from this extension in the last 30 days.", accessData?.accessTimes.length, feature.accessDataLabel ?? feature.label)));
114
description.style.marginBottom = '8px';
115
this.renderRequestsChart(container, accessData.accessTimes, disposables);
116
}
117
const status = accessData?.current?.status;
118
if (status) {
119
const data = new MarkdownString();
120
if (status?.severity === Severity.Error) {
121
data.appendMarkdown(`$(${errorIcon.id}) ${status.message}\n\n`);
122
}
123
if (status?.severity === Severity.Warning) {
124
data.appendMarkdown(`$(${warningIcon.id}) ${status.message}\n\n`);
125
}
126
if (data.value) {
127
this.renderMarkdown(data, container, disposables);
128
}
129
}
130
}
131
}
132
if (status.runtimeErrors.length || status.messages.length) {
133
const data = new MarkdownString();
134
if (status.runtimeErrors.length) {
135
data.appendMarkdown(`\n ### ${localize('uncaught errors', "Uncaught Errors ({0})", status.runtimeErrors.length)}\n`);
136
for (const error of status.runtimeErrors) {
137
data.appendMarkdown(`$(${Codicon.error.id})&nbsp;${getErrorMessage(error)}\n\n`);
138
}
139
}
140
if (status.messages.length) {
141
data.appendMarkdown(`\n ### ${localize('messaages', "Messages ({0})", status.messages.length)}\n`);
142
for (const message of status.messages) {
143
data.appendMarkdown(`$(${(message.type === Severity.Error ? Codicon.error : message.type === Severity.Warning ? Codicon.warning : Codicon.info).id})&nbsp;${message.message}\n\n`);
144
}
145
}
146
if (data.value) {
147
this.renderMarkdown(data, container, disposables);
148
}
149
}
150
return container;
151
}
152
153
private renderMarkdown(markdown: IMarkdownString, container: HTMLElement, disposables: DisposableStore): void {
154
const { element } = disposables.add(this.markdownRendererService.render({
155
value: markdown.value,
156
isTrusted: markdown.isTrusted,
157
supportThemeIcons: true
158
}));
159
append(container, element);
160
}
161
162
private renderRequestsChart(container: HTMLElement, accessTimes: Date[], disposables: DisposableStore): void {
163
const width = 450;
164
const height = 250;
165
const margin = { top: 0, right: 4, bottom: 20, left: 4 };
166
const innerWidth = width - margin.left - margin.right;
167
const innerHeight = height - margin.top - margin.bottom;
168
169
const chartContainer = append(container, $('.feature-chart-container'));
170
chartContainer.style.position = 'relative';
171
172
const tooltip = append(chartContainer, $('.feature-chart-tooltip'));
173
tooltip.style.position = 'absolute';
174
tooltip.style.width = '0px';
175
tooltip.style.height = '0px';
176
177
let maxCount = 100;
178
const map = new Map<string, number>();
179
for (const accessTime of accessTimes) {
180
const day = `${accessTime.getDate()} ${accessTime.toLocaleString('default', { month: 'short' })}`;
181
map.set(day, (map.get(day) ?? 0) + 1);
182
maxCount = Math.max(maxCount, map.get(day)!);
183
}
184
185
const now = new Date();
186
type Point = { x: number; y: number; date: string; count: number };
187
const points: Point[] = [];
188
for (let i = 0; i <= 30; i++) {
189
const date = new Date(now);
190
date.setDate(now.getDate() - (30 - i));
191
const dateString = `${date.getDate()} ${date.toLocaleString('default', { month: 'short' })}`;
192
const count = map.get(dateString) ?? 0;
193
const x = (i / 30) * innerWidth;
194
const y = innerHeight - (count / maxCount) * innerHeight;
195
points.push({ x, y, date: dateString, count });
196
}
197
198
const chart = append(chartContainer, $('.feature-chart'));
199
const svg = append(chart, $.SVG('svg'));
200
svg.setAttribute('width', `${width}px`);
201
svg.setAttribute('height', `${height}px`);
202
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
203
204
const g = $.SVG('g');
205
g.setAttribute('transform', `translate(${margin.left},${margin.top})`);
206
svg.appendChild(g);
207
208
const xAxisLine = $.SVG('line');
209
xAxisLine.setAttribute('x1', '0');
210
xAxisLine.setAttribute('y1', `${innerHeight}`);
211
xAxisLine.setAttribute('x2', `${innerWidth}`);
212
xAxisLine.setAttribute('y2', `${innerHeight}`);
213
xAxisLine.setAttribute('stroke', asCssVariable(chartAxis));
214
xAxisLine.setAttribute('stroke-width', '1px');
215
g.appendChild(xAxisLine);
216
217
for (let i = 1; i <= 30; i += 7) {
218
const date = new Date(now);
219
date.setDate(now.getDate() - (30 - i));
220
const dateString = `${date.getDate()} ${date.toLocaleString('default', { month: 'short' })}`;
221
const x = (i / 30) * innerWidth;
222
223
// Add vertical line
224
const tick = $.SVG('line');
225
tick.setAttribute('x1', `${x}`);
226
tick.setAttribute('y1', `${innerHeight}`);
227
tick.setAttribute('x2', `${x}`);
228
tick.setAttribute('y2', `${innerHeight + 10}`);
229
tick.setAttribute('stroke', asCssVariable(chartAxis));
230
tick.setAttribute('stroke-width', '1px');
231
g.appendChild(tick);
232
233
const ruler = $.SVG('line');
234
ruler.setAttribute('x1', `${x}`);
235
ruler.setAttribute('y1', `0`);
236
ruler.setAttribute('x2', `${x}`);
237
ruler.setAttribute('y2', `${innerHeight}`);
238
ruler.setAttribute('stroke', asCssVariable(chartGuide));
239
ruler.setAttribute('stroke-width', '1px');
240
g.appendChild(ruler);
241
242
const xAxisDate = $.SVG('text');
243
xAxisDate.setAttribute('x', `${x}`);
244
xAxisDate.setAttribute('y', `${height}`); // Adjusted y position to be within the SVG view port
245
xAxisDate.setAttribute('text-anchor', 'middle');
246
xAxisDate.setAttribute('fill', asCssVariable(foreground));
247
xAxisDate.setAttribute('font-size', '10px');
248
xAxisDate.textContent = dateString;
249
g.appendChild(xAxisDate);
250
}
251
252
const line = $.SVG('polyline');
253
line.setAttribute('fill', 'none');
254
line.setAttribute('stroke', asCssVariable(chartLine));
255
line.setAttribute('stroke-width', `2px`);
256
line.setAttribute('points', points.map(p => `${p.x},${p.y}`).join(' '));
257
g.appendChild(line);
258
259
const highlightCircle = $.SVG('circle');
260
highlightCircle.setAttribute('r', `4px`);
261
highlightCircle.style.display = 'none';
262
g.appendChild(highlightCircle);
263
264
const hoverDisposable = disposables.add(new MutableDisposable<IDisposable>());
265
const mouseMoveListener = (event: MouseEvent): void => {
266
const rect = svg.getBoundingClientRect();
267
const mouseX = event.clientX - rect.left - margin.left;
268
269
let closestPoint: Point | undefined;
270
let minDistance = Infinity;
271
272
points.forEach(point => {
273
const distance = Math.abs(point.x - mouseX);
274
if (distance < minDistance) {
275
minDistance = distance;
276
closestPoint = point;
277
}
278
});
279
280
if (closestPoint) {
281
highlightCircle.setAttribute('cx', `${closestPoint.x}`);
282
highlightCircle.setAttribute('cy', `${closestPoint.y}`);
283
highlightCircle.style.display = 'block';
284
tooltip.style.left = `${closestPoint.x + 24}px`;
285
tooltip.style.top = `${closestPoint.y + 14}px`;
286
hoverDisposable.value = this.hoverService.showInstantHover({
287
content: new MarkdownString(`${closestPoint.date}: ${closestPoint.count} requests`),
288
target: tooltip,
289
appearance: {
290
showPointer: true,
291
skipFadeInAnimation: true,
292
}
293
});
294
} else {
295
hoverDisposable.value = undefined;
296
}
297
};
298
disposables.add(addDisposableListener(svg, EventType.MOUSE_MOVE, mouseMoveListener));
299
300
const mouseLeaveListener = () => {
301
highlightCircle.style.display = 'none';
302
hoverDisposable.value = undefined;
303
};
304
disposables.add(addDisposableListener(svg, EventType.MOUSE_LEAVE, mouseLeaveListener));
305
}
306
}
307
308
309
interface ILayoutParticipant {
310
layout(height?: number, width?: number): void;
311
}
312
313
const runtimeStatusFeature = {
314
id: RuntimeStatusMarkdownRenderer.ID,
315
label: localize('runtime', "Runtime Status"),
316
access: {
317
canToggle: false
318
},
319
renderer: new SyncDescriptor(RuntimeStatusMarkdownRenderer),
320
};
321
322
export class ExtensionFeaturesTab extends Themable {
323
324
readonly domNode: HTMLElement;
325
326
private readonly featureView = this._register(new MutableDisposable<ExtensionFeatureView>());
327
private featureViewDimension?: { height?: number; width?: number };
328
329
private readonly layoutParticipants: ILayoutParticipant[] = [];
330
private readonly extensionId: ExtensionIdentifier;
331
332
constructor(
333
private readonly manifest: IExtensionManifest,
334
private readonly feature: string | undefined,
335
@IThemeService themeService: IThemeService,
336
@IInstantiationService private readonly instantiationService: IInstantiationService
337
) {
338
super(themeService);
339
340
this.extensionId = new ExtensionIdentifier(getExtensionId(manifest.publisher, manifest.name));
341
this.domNode = $('div.subcontent.feature-contributions');
342
this.create();
343
}
344
345
layout(height?: number, width?: number): void {
346
this.layoutParticipants.forEach(participant => participant.layout(height, width));
347
}
348
349
private create(): void {
350
const features = this.getFeatures();
351
if (features.length === 0) {
352
append($('.no-features'), this.domNode).textContent = localize('noFeatures', "No features contributed.");
353
return;
354
}
355
356
const splitView = this._register(new SplitView<number>(this.domNode, {
357
orientation: Orientation.HORIZONTAL,
358
proportionalLayout: true
359
}));
360
this.layoutParticipants.push({
361
layout: (height: number, width: number) => {
362
splitView.el.style.height = `${height - 14}px`;
363
splitView.layout(width);
364
}
365
});
366
367
const featuresListContainer = $('.features-list-container');
368
const list = this._register(this.createFeaturesList(featuresListContainer));
369
list.splice(0, list.length, features);
370
371
const featureViewContainer = $('.feature-view-container');
372
this._register(list.onDidChangeSelection(e => {
373
const feature = e.elements[0];
374
if (feature) {
375
this.showFeatureView(feature, featureViewContainer);
376
}
377
}));
378
379
const index = this.feature ? features.findIndex(f => f.id === this.feature) : 0;
380
list.setSelection([index === -1 ? 0 : index]);
381
382
splitView.addView({
383
onDidChange: Event.None,
384
element: featuresListContainer,
385
minimumSize: 100,
386
maximumSize: Number.POSITIVE_INFINITY,
387
layout: (width, _, height) => {
388
featuresListContainer.style.width = `${width}px`;
389
list.layout(height, width);
390
}
391
}, 200, undefined, true);
392
393
splitView.addView({
394
onDidChange: Event.None,
395
element: featureViewContainer,
396
minimumSize: 500,
397
maximumSize: Number.POSITIVE_INFINITY,
398
layout: (width, _, height) => {
399
featureViewContainer.style.width = `${width}px`;
400
this.featureViewDimension = { height, width };
401
this.layoutFeatureView();
402
}
403
}, Sizing.Distribute, undefined, true);
404
405
splitView.style({
406
separatorBorder: this.theme.getColor(PANEL_SECTION_BORDER)!
407
});
408
}
409
410
private createFeaturesList(container: HTMLElement): WorkbenchList<IExtensionFeatureDescriptor> {
411
const renderer = this.instantiationService.createInstance(ExtensionFeatureItemRenderer, this.extensionId);
412
const delegate = new ExtensionFeatureItemDelegate();
413
const list = this.instantiationService.createInstance(WorkbenchList, 'ExtensionFeaturesList', append(container, $('.features-list-wrapper')), delegate, [renderer], {
414
multipleSelectionSupport: false,
415
setRowLineHeight: false,
416
horizontalScrolling: false,
417
accessibilityProvider: {
418
getAriaLabel(extensionFeature: IExtensionFeatureDescriptor | null): string {
419
return extensionFeature?.label ?? '';
420
},
421
getWidgetAriaLabel(): string {
422
return localize('extension features list', "Extension Features");
423
}
424
},
425
openOnSingleClick: true
426
}) as WorkbenchList<IExtensionFeatureDescriptor>;
427
return list;
428
}
429
430
private layoutFeatureView(): void {
431
this.featureView.value?.layout(this.featureViewDimension?.height, this.featureViewDimension?.width);
432
}
433
434
private showFeatureView(feature: IExtensionFeatureDescriptor, container: HTMLElement): void {
435
if (this.featureView.value?.feature.id === feature.id) {
436
return;
437
}
438
clearNode(container);
439
this.featureView.value = this.instantiationService.createInstance(ExtensionFeatureView, this.extensionId, this.manifest, feature);
440
container.appendChild(this.featureView.value.domNode);
441
this.layoutFeatureView();
442
}
443
444
private getFeatures(): IExtensionFeatureDescriptor[] {
445
const features = Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry)
446
.getExtensionFeatures().filter(feature => {
447
const renderer = this.getRenderer(feature);
448
const shouldRender = renderer?.shouldRender(this.manifest);
449
renderer?.dispose();
450
return shouldRender;
451
}).sort((a, b) => a.label.localeCompare(b.label));
452
453
const renderer = this.getRenderer(runtimeStatusFeature);
454
if (renderer?.shouldRender(this.manifest)) {
455
features.splice(0, 0, runtimeStatusFeature);
456
}
457
renderer?.dispose();
458
return features;
459
}
460
461
private getRenderer(feature: IExtensionFeatureDescriptor): IExtensionFeatureRenderer | undefined {
462
return feature.renderer ? this.instantiationService.createInstance(feature.renderer) : undefined;
463
}
464
465
}
466
467
interface IExtensionFeatureItemTemplateData {
468
readonly label: HTMLElement;
469
readonly disabledElement: HTMLElement;
470
readonly statusElement: HTMLElement;
471
readonly disposables: DisposableStore;
472
}
473
474
class ExtensionFeatureItemDelegate implements IListVirtualDelegate<IExtensionFeatureDescriptor> {
475
getHeight() { return 22; }
476
getTemplateId() { return 'extensionFeatureDescriptor'; }
477
}
478
479
class ExtensionFeatureItemRenderer implements IListRenderer<IExtensionFeatureDescriptor, IExtensionFeatureItemTemplateData> {
480
481
readonly templateId = 'extensionFeatureDescriptor';
482
483
constructor(
484
private readonly extensionId: ExtensionIdentifier,
485
@IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService
486
) { }
487
488
renderTemplate(container: HTMLElement): IExtensionFeatureItemTemplateData {
489
container.classList.add('extension-feature-list-item');
490
const label = append(container, $('.extension-feature-label'));
491
const disabledElement = append(container, $('.extension-feature-disabled-label'));
492
disabledElement.textContent = localize('revoked', "No Access");
493
const statusElement = append(container, $('.extension-feature-status'));
494
return { label, disabledElement, statusElement, disposables: new DisposableStore() };
495
}
496
497
renderElement(element: IExtensionFeatureDescriptor, index: number, templateData: IExtensionFeatureItemTemplateData) {
498
templateData.disposables.clear();
499
templateData.label.textContent = element.label;
500
templateData.disabledElement.style.display = element.id === runtimeStatusFeature.id || this.extensionFeaturesManagementService.isEnabled(this.extensionId, element.id) ? 'none' : 'inherit';
501
502
templateData.disposables.add(this.extensionFeaturesManagementService.onDidChangeEnablement(({ extension, featureId, enabled }) => {
503
if (ExtensionIdentifier.equals(extension, this.extensionId) && featureId === element.id) {
504
templateData.disabledElement.style.display = enabled ? 'none' : 'inherit';
505
}
506
}));
507
508
const statusElementClassName = templateData.statusElement.className;
509
const updateStatus = () => {
510
const accessData = this.extensionFeaturesManagementService.getAccessData(this.extensionId, element.id);
511
if (accessData?.current?.status) {
512
templateData.statusElement.style.display = 'inherit';
513
templateData.statusElement.className = `${statusElementClassName} ${SeverityIcon.className(accessData.current.status.severity)}`;
514
} else {
515
templateData.statusElement.style.display = 'none';
516
}
517
};
518
updateStatus();
519
templateData.disposables.add(this.extensionFeaturesManagementService.onDidChangeAccessData(({ extension, featureId }) => {
520
if (ExtensionIdentifier.equals(extension, this.extensionId) && featureId === element.id) {
521
updateStatus();
522
}
523
}));
524
}
525
526
disposeElement(element: IExtensionFeatureDescriptor, index: number, templateData: IExtensionFeatureItemTemplateData): void {
527
templateData.disposables.dispose();
528
}
529
530
disposeTemplate(templateData: IExtensionFeatureItemTemplateData) {
531
templateData.disposables.dispose();
532
}
533
534
}
535
536
class ExtensionFeatureView extends Disposable {
537
538
readonly domNode: HTMLElement;
539
private readonly layoutParticipants: ILayoutParticipant[] = [];
540
541
constructor(
542
private readonly extensionId: ExtensionIdentifier,
543
private readonly manifest: IExtensionManifest,
544
readonly feature: IExtensionFeatureDescriptor,
545
@IInstantiationService private readonly instantiationService: IInstantiationService,
546
@IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService,
547
@IDialogService private readonly dialogService: IDialogService,
548
@IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService,
549
) {
550
super();
551
552
this.domNode = $('.extension-feature-content');
553
this.create(this.domNode);
554
}
555
556
private create(content: HTMLElement): void {
557
const header = append(content, $('.feature-header'));
558
const title = append(header, $('.feature-title'));
559
title.textContent = this.feature.label;
560
561
if (this.feature.access.canToggle) {
562
const actionsContainer = append(header, $('.feature-actions'));
563
const button = new Button(actionsContainer, defaultButtonStyles);
564
this.updateButtonLabel(button);
565
this._register(this.extensionFeaturesManagementService.onDidChangeEnablement(({ extension, featureId }) => {
566
if (ExtensionIdentifier.equals(extension, this.extensionId) && featureId === this.feature.id) {
567
this.updateButtonLabel(button);
568
}
569
}));
570
this._register(button.onDidClick(async () => {
571
const enabled = this.extensionFeaturesManagementService.isEnabled(this.extensionId, this.feature.id);
572
const confirmationResult = await this.dialogService.confirm({
573
title: localize('accessExtensionFeature', "Enable '{0}' Feature", this.feature.label),
574
message: enabled
575
? localize('disableAccessExtensionFeatureMessage', "Would you like to revoke '{0}' extension to access '{1}' feature?", this.manifest.displayName ?? this.extensionId.value, this.feature.label)
576
: localize('enableAccessExtensionFeatureMessage', "Would you like to allow '{0}' extension to access '{1}' feature?", this.manifest.displayName ?? this.extensionId.value, this.feature.label),
577
custom: true,
578
primaryButton: enabled ? localize('revoke', "Revoke Access") : localize('grant', "Allow Access"),
579
cancelButton: localize('cancel', "Cancel"),
580
});
581
if (confirmationResult.confirmed) {
582
this.extensionFeaturesManagementService.setEnablement(this.extensionId, this.feature.id, !enabled);
583
}
584
}));
585
}
586
587
const body = append(content, $('.feature-body'));
588
589
const bodyContent = $('.feature-body-content');
590
const scrollableContent = this._register(new DomScrollableElement(bodyContent, {}));
591
append(body, scrollableContent.getDomNode());
592
this.layoutParticipants.push({ layout: () => scrollableContent.scanDomNode() });
593
scrollableContent.scanDomNode();
594
595
if (this.feature.description) {
596
const description = append(bodyContent, $('.feature-description'));
597
description.textContent = this.feature.description;
598
}
599
600
const accessData = this.extensionFeaturesManagementService.getAccessData(this.extensionId, this.feature.id);
601
if (accessData?.current?.status) {
602
append(bodyContent, $('.feature-status', undefined,
603
$(`span${ThemeIcon.asCSSSelector(accessData.current.status.severity === Severity.Error ? errorIcon : accessData.current.status.severity === Severity.Warning ? warningIcon : infoIcon)}`, undefined),
604
$('span', undefined, accessData.current.status.message)));
605
}
606
607
const featureContentElement = append(bodyContent, $('.feature-content'));
608
if (this.feature.renderer) {
609
const renderer = this.instantiationService.createInstance<IExtensionFeatureRenderer>(this.feature.renderer);
610
if (renderer.type === 'table') {
611
this.renderTableData(featureContentElement, <IExtensionFeatureTableRenderer>renderer);
612
} else if (renderer.type === 'markdown') {
613
this.renderMarkdownData(featureContentElement, <IExtensionFeatureMarkdownRenderer>renderer);
614
} else if (renderer.type === 'markdown+table') {
615
this.renderMarkdownAndTableData(featureContentElement, <IExtensionFeatureMarkdownAndTableRenderer>renderer);
616
} else if (renderer.type === 'element') {
617
this.renderElementData(featureContentElement, <IExtensionFeatureElementRenderer>renderer);
618
}
619
}
620
}
621
622
private updateButtonLabel(button: Button): void {
623
button.label = this.extensionFeaturesManagementService.isEnabled(this.extensionId, this.feature.id) ? localize('revoke', "Revoke Access") : localize('enable', "Allow Access");
624
}
625
626
private renderTableData(container: HTMLElement, renderer: IExtensionFeatureTableRenderer): void {
627
const tableData = this._register(renderer.render(this.manifest));
628
const tableDisposable = this._register(new MutableDisposable());
629
if (tableData.onDidChange) {
630
this._register(tableData.onDidChange(data => {
631
clearNode(container);
632
tableDisposable.value = this.renderTable(data, container);
633
}));
634
}
635
tableDisposable.value = this.renderTable(tableData.data, container);
636
}
637
638
private renderTable(tableData: ITableData, container: HTMLElement): IDisposable {
639
const disposables = new DisposableStore();
640
append(container,
641
$('table', undefined,
642
$('tr', undefined,
643
...tableData.headers.map(header => $('th', undefined, header))
644
),
645
...tableData.rows
646
.map(row => {
647
return $('tr', undefined,
648
...row.map(rowData => {
649
if (typeof rowData === 'string') {
650
return $('td', undefined, $('p', undefined, rowData));
651
}
652
const data = Array.isArray(rowData) ? rowData : [rowData];
653
return $('td', undefined, ...data.map(item => {
654
const result: Node[] = [];
655
if (isMarkdownString(rowData)) {
656
const element = $('', undefined);
657
this.renderMarkdown(rowData, element);
658
result.push(element);
659
} else if (item instanceof ResolvedKeybinding) {
660
const element = $('');
661
const kbl = disposables.add(new KeybindingLabel(element, OS, defaultKeybindingLabelStyles));
662
kbl.set(item);
663
result.push(element);
664
} else if (item instanceof Color) {
665
result.push($('span', { class: 'colorBox', style: 'background-color: ' + Color.Format.CSS.format(item) }, ''));
666
result.push($('code', undefined, Color.Format.CSS.formatHex(item)));
667
}
668
return result;
669
}).flat());
670
})
671
);
672
})));
673
return disposables;
674
}
675
676
private renderMarkdownAndTableData(container: HTMLElement, renderer: IExtensionFeatureMarkdownAndTableRenderer): void {
677
const markdownAndTableData = this._register(renderer.render(this.manifest));
678
if (markdownAndTableData.onDidChange) {
679
this._register(markdownAndTableData.onDidChange(data => {
680
clearNode(container);
681
this.renderMarkdownAndTable(data, container);
682
}));
683
}
684
this.renderMarkdownAndTable(markdownAndTableData.data, container);
685
}
686
687
private renderMarkdownData(container: HTMLElement, renderer: IExtensionFeatureMarkdownRenderer): void {
688
container.classList.add('markdown');
689
const markdownData = this._register(renderer.render(this.manifest));
690
if (markdownData.onDidChange) {
691
this._register(markdownData.onDidChange(data => {
692
clearNode(container);
693
this.renderMarkdown(data, container);
694
}));
695
}
696
this.renderMarkdown(markdownData.data, container);
697
}
698
699
private renderMarkdown(markdown: IMarkdownString, container: HTMLElement): void {
700
const { element } = this._register(this.markdownRendererService.render({
701
value: markdown.value,
702
isTrusted: markdown.isTrusted,
703
supportThemeIcons: true
704
}));
705
append(container, element);
706
}
707
708
private renderMarkdownAndTable(data: Array<IMarkdownString | ITableData>, container: HTMLElement): void {
709
for (const markdownOrTable of data) {
710
if (isMarkdownString(markdownOrTable)) {
711
const element = $('', undefined);
712
this.renderMarkdown(markdownOrTable, element);
713
append(container, element);
714
} else {
715
const tableElement = append(container, $('table'));
716
this.renderTable(markdownOrTable, tableElement);
717
}
718
}
719
}
720
721
private renderElementData(container: HTMLElement, renderer: IExtensionFeatureElementRenderer): void {
722
const elementData = this._register(renderer.render(this.manifest));
723
if (elementData.onDidChange) {
724
this._register(elementData.onDidChange(data => {
725
clearNode(container);
726
container.appendChild(data);
727
}));
728
}
729
container.appendChild(elementData.data);
730
}
731
732
layout(height?: number, width?: number): void {
733
this.layoutParticipants.forEach(p => p.layout(height, width));
734
}
735
736
}
737
738