UNPKG

chrome-devtools-frontend

Version:
239 lines (209 loc) • 8.82 kB
// Copyright 2025 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Common from '../../core/common/common.js'; import type * as SDK from '../../core/sdk/sdk.js'; import * as GreenDev from '../greendev/greendev.js'; import {AnnotationType} from './AnnotationType.js'; export interface BaseAnnotationData { id: number; type: AnnotationType; message: string; // Sometimes the anchor for an annotation is not known, but is provided using a // string id instead (which can be converted to a proper `anchor`). lookupId: string; // Sometimes we want annotations to anchor to a particular string on the page. anchorToString?: string; } export interface ElementsAnnotationData extends BaseAnnotationData { type: AnnotationType.ELEMENT_NODE; anchor?: SDK.DOMModel.DOMNode; } export interface NetworkRequestAnnotationData extends BaseAnnotationData { type: AnnotationType.NETWORK_REQUEST; anchor?: SDK.NetworkRequest.NetworkRequest; } export interface NetworkRequestDetailsAnnotationData extends BaseAnnotationData { type: AnnotationType.NETWORK_REQUEST_SUBPANEL_HEADERS; anchor?: SDK.NetworkRequest.NetworkRequest; } export const enum Events { ANNOTATION_ADDED = 'AnnotationAdded', ANNOTATION_DELETED = 'AnnotationDeleted', ALL_ANNOTATIONS_DELETED = 'AllAnnotationsDeleted', } export interface EventTypes { [Events.ANNOTATION_ADDED]: BaseAnnotationData; [Events.ANNOTATION_DELETED]: {id: number}; [Events.ALL_ANNOTATIONS_DELETED]: void; } export class AnnotationRepository { static #instance: AnnotationRepository|null = null; static #hasRepliedGreenDevDisabled = false; static #hasShownFlagWarning = false; #events = new Common.ObjectWrapper.ObjectWrapper<EventTypes>(); #annotationData: BaseAnnotationData[] = []; #nextId = 0; static instance(): AnnotationRepository { if (!AnnotationRepository.#instance) { AnnotationRepository.#instance = new AnnotationRepository(); } return AnnotationRepository.#instance; } static annotationsEnabled(): boolean { const enabled = GreenDev.Prototypes.instance().isEnabled('aiAnnotations'); // TODO(finnur): Fix race when Repository is created before feature flags have been set properly. if (!enabled) { this.#hasRepliedGreenDevDisabled = true; } else if (this.#hasRepliedGreenDevDisabled && !this.#hasShownFlagWarning) { console.warn( 'Flag controlling GreenDev has flipped from false to true. ' + 'Only some callers will expect GreenDev to be enabled, which can lead to unexpected results.'); this.#hasShownFlagWarning = true; } return Boolean(enabled); } addEventListener<T extends keyof EventTypes>( eventType: T, listener: (arg0: Common.EventTarget.EventTargetEvent<EventTypes[T]>) => void, thisObject?: Object): Common.EventTarget.EventDescriptor<EventTypes, T> { if (!AnnotationRepository.annotationsEnabled()) { console.warn('Received request to add event listener with annotations disabled'); } return this.#events.addEventListener(eventType, listener, thisObject); } getAnnotationDataByType(type: AnnotationType): BaseAnnotationData[] { if (!AnnotationRepository.annotationsEnabled()) { console.warn('Received query for annotation types with annotations disabled'); return []; } const annotations = this.#annotationData.filter(annotation => annotation.type === type); return annotations; } getAnnotationDataById(id: number): BaseAnnotationData|undefined { if (!AnnotationRepository.annotationsEnabled()) { console.warn('Received query for annotation type with annotations disabled'); return undefined; } return this.#annotationData.find(annotation => annotation.id === id); } #getExistingAnnotation(type: AnnotationType, anchor?: SDK.DOMModel.DOMNode|SDK.NetworkRequest.NetworkRequest|string): BaseAnnotationData|undefined { const annotations = this.getAnnotationDataByType(type); const annotation = annotations.find(annotation => { if (typeof anchor === 'string') { return annotation.lookupId === anchor; } switch (type) { case AnnotationType.ELEMENT_NODE: { const elementAnnotation = annotation as ElementsAnnotationData; return elementAnnotation.anchor === anchor; } case AnnotationType.NETWORK_REQUEST_SUBPANEL_HEADERS: { const networkRequestDetailsAnnotation = annotation as NetworkRequestDetailsAnnotationData; return networkRequestDetailsAnnotation.anchor === anchor; } default: console.warn('[AnnotationRepository] Unknown AnnotationType', type); return false; } }); return annotation; } #updateExistingAnnotationLabel( label: string, type: AnnotationType, anchor?: SDK.DOMModel.DOMNode|SDK.NetworkRequest.NetworkRequest|string): boolean { const annotation = this.#getExistingAnnotation(type, anchor); if (annotation) { // TODO(finnur): This should work for annotations that have not been displayed yet, // but we need to also notify the AnnotationManager for those that have been shown. annotation.message = label; return true; } return false; } addElementsAnnotation( label: string, anchor?: SDK.DOMModel.DOMNode|string, anchorToString?: string, ): void { if (!AnnotationRepository.annotationsEnabled()) { console.warn('Received annotation registration with annotations disabled'); return; } if (this.#updateExistingAnnotationLabel(label, AnnotationType.ELEMENT_NODE, anchor)) { return; } const annotationData: ElementsAnnotationData = { id: this.#nextId++, type: AnnotationType.ELEMENT_NODE, message: label, lookupId: typeof anchor === 'string' ? anchor : '', anchor: typeof anchor !== 'string' ? anchor : undefined, anchorToString, }; this.#annotationData.push(annotationData); // eslint-disable-next-line no-console console.log('[AnnotationRepository] Added element annotation:', label, { annotationData, annotations: this.#annotationData.length, }); this.#events.dispatchEventToListeners(Events.ANNOTATION_ADDED, annotationData); } addNetworkRequestAnnotation( label: string, anchor?: SDK.NetworkRequest.NetworkRequest|string, anchorToString?: string, ): void { if (!AnnotationRepository.annotationsEnabled()) { console.warn('Received annotation registration with annotations disabled'); return; } // We only need to update the NETWORK_REQUEST_SUBPANEL_HEADERS because the // NETWORK_REQUEST Annotation has no meaningful label. if (this.#updateExistingAnnotationLabel(label, AnnotationType.NETWORK_REQUEST_SUBPANEL_HEADERS, anchor)) { return; } const annotationData: NetworkRequestAnnotationData = { id: this.#nextId++, type: AnnotationType.NETWORK_REQUEST, message: '', lookupId: typeof anchor === 'string' ? anchor : '', anchor: typeof anchor !== 'string' ? anchor : undefined, anchorToString, }; this.#annotationData.push(annotationData); // eslint-disable-next-line no-console console.log('[AnnotationRepository] Added annotation:', label, { annotationData, annotations: this.#annotationData.length, }); this.#events.dispatchEventToListeners(Events.ANNOTATION_ADDED, annotationData); const annotationDetailsData: NetworkRequestDetailsAnnotationData = { id: this.#nextId++, type: AnnotationType.NETWORK_REQUEST_SUBPANEL_HEADERS, message: label, lookupId: typeof anchor === 'string' ? anchor : '', anchor: typeof anchor !== 'string' ? anchor : undefined, anchorToString, }; this.#annotationData.push(annotationDetailsData); this.#events.dispatchEventToListeners(Events.ANNOTATION_ADDED, annotationDetailsData); } deleteAllAnnotations(): void { this.#annotationData = []; this.#events.dispatchEventToListeners(Events.ALL_ANNOTATIONS_DELETED); // eslint-disable-next-line no-console console.log('[AnnotationRepository] Deleting all annotations'); } deleteAnnotation(id: number): void { const index = this.#annotationData.findIndex(annotation => annotation.id === id); if (index === -1) { console.warn(`[AnnotationRepository] Could not find annotation with id ${id}`); return; } this.#annotationData.splice(index, 1); this.#events.dispatchEventToListeners(Events.ANNOTATION_DELETED, {id}); // eslint-disable-next-line no-console console.log(`[AnnotationRepository] Deleted annotation with id ${id}`); } }