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