| /** |
| * @module modules/test-picker-sk |
| * @description <h2><code>test-picker-sk</code></h2> |
| * |
| * A trace/test picker used to select a valid trace. |
| * This element will guide the user by providing the following: |
| * - Specific order in which fields must be filled. |
| * - Fields with dropdown menus to aid in selecting valid values for each param. |
| * - Indicator as to when a test is ready to be plotted. |
| * |
| * This Element also provides the option to populate all the fields |
| * using a given query. e.g.: |
| * |
| * populateFieldDataFromQuery( |
| * 'benchmark=a&bot=b&test=c&subtest1=&subtest2=d', |
| * ['benchmark', 'bot', 'test', 'subtest1', 'subtest2'] |
| * ) |
| * |
| * In the above case, fields will be filled in order of hierarchy until an |
| * empty value is reached. Since subtest1 is empty, it'll stop filling at |
| * subtest1, leaving subtest1 and subtest2 fields empty. |
| * |
| * @evt plot-button-clicked - Triggered when the Plot button is clicked. |
| * It will contain the currently populated query in the test picker in |
| * event.detail.query. |
| * |
| */ |
| import { html } from 'lit/html.js'; |
| import { define } from '../../../elements-sk/modules/define'; |
| import { jsonOrThrow } from '../../../infra-sk/modules/jsonOrThrow'; |
| import { ElementSk } from '../../../infra-sk/modules/ElementSk'; |
| import { ParamSet, fromParamSet, toParamSet } from '../../../infra-sk/modules/query'; |
| import { CheckOrRadio } from '../../../elements-sk/modules/checkbox-sk/checkbox-sk'; |
| |
| import { NextParamListHandlerRequest, NextParamListHandlerResponse } from '../json'; |
| import '../picker-field-sk'; |
| import { PickerFieldSk } from '../picker-field-sk/picker-field-sk'; |
| import { errorMessage } from '../../../elements-sk/modules/errorMessage'; |
| import '../../../elements-sk/modules/spinner-sk'; |
| |
| // The maximum number of matches before Plotting is enabled. |
| const PLOT_MAXIMUM: number = 200; |
| |
| const MAX_MESSAGE = 'Reduce Traces'; |
| |
| // Data Structure to keep track of field information. |
| class FieldInfo { |
| field: PickerFieldSk | null = null; // The field element itself. |
| |
| param: string = ''; // The label of the field. Must match a valid trace key in CDB. |
| |
| value: string[] = []; // The currently selected value in a field. |
| |
| splitBy: string[] = []; // Split item selected. |
| |
| index: number = 0; // Index of the field in the fieldData array. |
| |
| onValueChanged: ((e: Event) => void) | null = null; |
| |
| onSplitByChanged: ((e: Event) => void) | null = null; |
| } |
| |
| export class TestPickerSk extends ElementSk { |
| private _fieldData: FieldInfo[] = []; |
| |
| private _count: number = -1; |
| |
| private _containerDiv: Element | null = null; |
| |
| private _plotButton: HTMLButtonElement | null = null; |
| |
| private _graphDiv: Element | null = null; |
| |
| private _requestInProgress: boolean = false; |
| |
| private _currentIndex: number = 0; |
| |
| private _defaultParams: { [key: string]: string[] | null } = {}; |
| |
| private _autoAddTrace: boolean = false; |
| |
| private _readOnly: boolean = false; |
| |
| private _dataLoading: boolean = false; |
| |
| constructor() { |
| super(TestPickerSk.template); |
| } |
| |
| private static template = (ele: TestPickerSk) => html` |
| <div id="testPicker"> |
| <div id="fieldContainer"></div> |
| <div id="queryCount"> |
| <div class="test-picker-sk-matches-container"> |
| Traces: ${ele._requestInProgress ? '' : ele._count} |
| <spinner-sk ?active=${ele._requestInProgress}></spinner-sk> |
| </div> |
| <div id="plot-button-container"> |
| <div ?hidden="${!(ele._count > PLOT_MAXIMUM)}"> |
| <span id="max-message" style="margin-left:2px"> (${MAX_MESSAGE}) </span> |
| </div> |
| <button |
| id="plot-button" |
| @click=${ele.onPlotButtonClick} |
| disabled |
| title="Plot a graph on selected values."> |
| Plot |
| </button> |
| </div> |
| </div> |
| </div> |
| `; |
| |
| /** |
| * Called when the element is added to the DOM. |
| * Initializes references to DOM elements and renders the component. |
| */ |
| connectedCallback(): void { |
| super.connectedCallback(); |
| this._render(); |
| |
| this._containerDiv = this.querySelector('#fieldContainer'); |
| this._plotButton = this.querySelector('#plot-button'); |
| this._graphDiv = document.querySelector('#graphContainer'); |
| |
| window.addEventListener('data-loaded', () => { |
| this._dataLoading = false; |
| this.setReadOnly(false); |
| }); |
| } |
| |
| /** |
| * Adds a new PickerFieldSk element to the field container. |
| * This function fetches options for the new field from the backend, |
| * initializes and populates the field, focuses it, and sets up event |
| * listeners. |
| */ |
| private addChildField(readOnly: boolean) { |
| const currentIndex = this._currentIndex; |
| const currentFieldInfo = this._fieldData[currentIndex]; |
| const param = currentFieldInfo.param; |
| |
| const handler = (json: NextParamListHandlerResponse) => { |
| this.updateCount(json.count); |
| |
| if (param in json.paramset && json.paramset[param] !== null) { |
| const options = json.paramset[param].filter((option: string) => !option.includes('.')); |
| options.sort((a, b) => a.localeCompare(b, 'en', { sensitivity: 'base' })); |
| const field: PickerFieldSk = new PickerFieldSk(param); |
| currentFieldInfo.field = field; |
| this._containerDiv!.appendChild(field); |
| this.setReadOnly(readOnly); |
| |
| field!.label = param; |
| field!.options = options; |
| field.index = this._currentIndex; |
| const extraTests = json.paramset[param].filter((option: string) => option.includes('.')); |
| if (extraTests.length > 0) { |
| field!.options = options.concat(extraTests); |
| } |
| this._currentIndex += 1; |
| field!.focus(); |
| if (currentIndex !== 0) { |
| field!.openOverlay(); |
| } |
| |
| this.addValueUpdatedEventToField(currentIndex); |
| } |
| this._render(); |
| }; |
| this.callNextParamList(handler, currentIndex); |
| } |
| |
| /** |
| * Removes child fields from the field container starting from a given index. |
| * This function iterates through the `_fieldData` array from the current |
| * index down to the specified index, removing the corresponding |
| * `PickerFieldSk` elements from the DOM and resetting their values. |
| * It also dispatches a 'split-by-changed' event if a split field is removed. |
| * @param index The index from which to start removing child fields. |
| */ |
| private removeChildFields(index: number) { |
| while (this._currentIndex > index) { |
| const fieldInfo = this._fieldData[this._currentIndex]; |
| // Remove split if it was previously enabled. |
| if (fieldInfo.splitBy.length > 0) { |
| fieldInfo.field!.split = false; |
| this.dispatchEvent( |
| new CustomEvent('split-by-changed', { |
| detail: { |
| param: fieldInfo.param, |
| split: false, |
| }, |
| bubbles: true, |
| composed: true, |
| }) |
| ); |
| } |
| fieldInfo.value = []; |
| if (fieldInfo.field !== null && this._containerDiv?.contains(fieldInfo.field!)) { |
| this._containerDiv!.removeChild(fieldInfo.field!); |
| } |
| fieldInfo.field = null; |
| this._currentIndex -= 1; |
| } |
| this._render(); |
| } |
| |
| /** |
| * Sets the readonly property for all rendered fields. |
| * When `readonly` is true, all fields and the plot button are disabled. |
| * When `readonly` is false, all fields are enabled, and the plot button is |
| * enabled unless `autoAddTrace` is true. |
| * @param readonly - A boolean indicating whether the fields should be |
| * read-only. |
| */ |
| setReadOnly(readonly: boolean) { |
| if (this._readOnly === readonly) { |
| return; |
| } |
| const exploreMulti = document.querySelector('explore-multi-sk') as any; |
| if (exploreMulti && exploreMulti._dataLoading) { |
| readonly = true; |
| } |
| this._dataLoading = exploreMulti?._dataLoading; |
| this._readOnly = readonly; |
| this._fieldData.forEach((field) => { |
| if (readonly) { |
| field.field?.disable(); |
| this._plotButton!.disabled = true; |
| this._requestInProgress = true; |
| } else { |
| field.field?.enable(); |
| if (!this.autoAddTrace && !this._dataLoading) { |
| this._plotButton!.disabled = false; |
| } |
| this._requestInProgress = false; |
| } |
| }); |
| this._render(); |
| } |
| |
| get readOnly() { |
| return this._readOnly; |
| } |
| |
| /** |
| * Makes a POST request to the /_/nextParamList/ endpoint to fetch parameter |
| * lists. |
| * Updates the count and sets `_requestInProgress` to true during the request. |
| * Disables fields if multiple selections are not allowed. |
| * Re-enables fields and calls the handler function on success. |
| * On failure, removes child fields and displays an error message. |
| * @param handler - A callback function to handle the JSON response. |
| * @param index - The index of the current field. |
| */ |
| private callNextParamList(handler: (json: NextParamListHandlerResponse) => void, index: number) { |
| this.updateCount(-1); |
| this._requestInProgress = true; |
| // Allow multiple selections to continue. |
| if (!(this._fieldData[index].value.length > 1)) { |
| this.setReadOnly(true); |
| } |
| this._render(); |
| |
| const fieldData = this.createQueryFromFieldData(); |
| const body: NextParamListHandlerRequest = { |
| q: fieldData, |
| }; |
| |
| fetch('/_/nextParamList/', { |
| method: 'POST', |
| body: JSON.stringify(body), |
| headers: { |
| 'Content-Type': 'application/json', |
| }, |
| }) |
| .then(jsonOrThrow) |
| .then((json) => { |
| this._requestInProgress = false; |
| // Only re-enable when autoadd is false, and we have results or it is the initial |
| handler(json); |
| }) |
| .catch((msg: any) => { |
| this._requestInProgress = false; |
| this.setReadOnly(false); |
| // If the request fails, we remove child fields to reset. |
| this.removeChildFields(0); |
| errorMessage(msg); |
| }); |
| } |
| |
| /** |
| * Fetches the available options for a given field from the backend. |
| * After fetching, it populates the field's dropdown, updates the match count, |
| * focuses the field, and opens its overlay if it's not the first field. |
| * @param index - The index of the field in the `_fieldData` array. |
| */ |
| private fetchOptions(index: number) { |
| const fieldInfo = this._fieldData[index]; |
| const field = fieldInfo.field; |
| const param = fieldInfo.param; |
| |
| const handler = (json: NextParamListHandlerResponse) => { |
| if (param in json.paramset && json.paramset[param] !== null) { |
| const options = json.paramset[param].filter((option: string) => !option.includes('.')); |
| field!.options = options; |
| this.updateCount(json.count); |
| field!.focus(); |
| if (index !== 0) { |
| field!.openOverlay(); |
| } |
| this._render(); |
| } |
| }; |
| this.callNextParamList(handler, index); |
| } |
| |
| /** |
| * Handles the click event on the Plot button. |
| * Dispatches a 'plot-button-clicked' custom event with the current query. |
| */ |
| private onPlotButtonClick() { |
| const detail = { |
| query: this.createQueryFromFieldData(), |
| }; |
| this.dispatchEvent( |
| new CustomEvent('plot-button-clicked', { |
| detail: detail, |
| bubbles: true, |
| }) |
| ); |
| this._render(); |
| } |
| |
| /** |
| * Resets the test picker and populates the fields with an input query. |
| * This function parses the input query, initializes the field data based on |
| * the provided parameters, and then populates the fields with the |
| * corresponding selected values and available options. |
| * Note that calling this function will overwrite any current selections in |
| * the test picker. |
| * @param query - The query string to populate the fields from |
| * (e.g., 'benchmark=a&bot=b&test=c'). |
| * @param params - An array of parameter names defining the hierarchy of the |
| * fields. |
| * @param paramSet - A ParamSet object containing available options for each |
| * parameter. |
| */ |
| populateFieldDataFromQuery(query: string, params: string[], paramSet: ParamSet) { |
| const selectedParams: ParamSet = toParamSet(query); |
| if (paramSet) { |
| const paramKeys: string[] = Object.keys(paramSet).filter((key) => key in selectedParams); |
| this.initializeFieldData(paramKeys); |
| } else { |
| this.initializeFieldData(params); |
| } |
| for (let i = 0; i < this._fieldData.length; i++) { |
| const fieldInfo = this._fieldData[i]; |
| const param = fieldInfo.param; |
| const field: PickerFieldSk = new PickerFieldSk(param); |
| fieldInfo.field = field; |
| fieldInfo.field.index = i; |
| this._containerDiv!.appendChild(field); |
| |
| // Set selected items from the query |
| const selectedValue = selectedParams[fieldInfo.param] || []; |
| field.selectedItems = selectedValue; |
| fieldInfo.value = selectedValue; |
| |
| // If there are available options provided, use them. |
| if (paramSet && paramSet[param]) { |
| field.options = paramSet[param]; |
| } |
| field.index = i; |
| |
| // Add event listener for value changes |
| this.addValueUpdatedEventToField(i); |
| this.fetchExtraOptions(i); |
| |
| field.focus(); |
| this._render(); |
| } |
| } |
| |
| /** |
| * Populates the field data from a given ParamSet. |
| * This function initializes the field data based on the unique keys from |
| * both paramSets and paramSet, and then populates the fields with the |
| * corresponding values and options. |
| * If no parameters are provided in `paramSet`, `autoAddTrace` is set to true. |
| * @param paramSets - A ParamSet object containing the initial selected |
| * values for the fields. |
| * @param paramSet - A ParamSet object containing available options for each |
| * parameter. |
| */ |
| populateFieldDataFromParamSet(paramSets: ParamSet, paramSet: ParamSet) { |
| const uniqueParamKeys = [...new Set([...Object.keys(paramSets), ...Object.keys(paramSet)])]; |
| this.initializeFieldData(uniqueParamKeys); |
| this._currentIndex = 0; // Reset current index for proper field initialization |
| // If no params are provided, then chart is loaded with all traces. |
| if (Object.keys(paramSet).length === 0) { |
| this.autoAddTrace = true; |
| } |
| |
| for (let i = 0; i < this._fieldData.length; i++) { |
| const fieldInfo = this._fieldData[i]; |
| const param = fieldInfo.param; |
| fieldInfo.field = new PickerFieldSk(param); |
| // Combine options from both paramSets and paramSet for the current param. |
| const allOptions = [ |
| ...new Set([...(paramSets[param] || []), ...(paramSet[param] || [])]), |
| ].sort(); |
| const value = paramSets[param] || []; |
| if (value.length === 0) { |
| break; // Stop after the first field without a value |
| } |
| if (allOptions.length > 0) { |
| fieldInfo.field.options = allOptions; |
| fieldInfo.field.selectedItems = value; |
| fieldInfo.field.index = i; |
| fieldInfo.value = value; |
| } |
| this.fetchExtraOptions(i); |
| this._containerDiv!.appendChild(fieldInfo.field); |
| } |
| } |
| |
| /** |
| * Handles the toggle event of the auto-add trace checkbox. |
| * Sets the `autoAddTrace` property based on the checkbox's checked state. |
| * @param e The event object from the checkbox toggle. |
| */ |
| private onToggleCheckboxClick(e: Event): void { |
| this.autoAddTrace = (e.target as CheckOrRadio).checked; |
| // This prevents a double event from happening. |
| e.preventDefault(); |
| this._render(); |
| } |
| |
| /** |
| * Updates the graph based on the selected values. |
| * If `autoAddTrace` is false, no update occurs. |
| * If the trace count exceeds `PLOT_MAXIMUM`, an error message is displayed. |
| * Dispatches 'remove-trace' or 'add-to-graph' events based on changes. |
| * |
| * @param value The current selected values for the field. |
| * @param fieldInfo The FieldInfo object for the current field. |
| * @param removedValue The values that were removed from the selection. |
| */ |
| private updateGraph(value: string[], fieldInfo: FieldInfo, removedValue: string[]) { |
| // No valid data, so remove entire graph. |
| if (fieldInfo.index === 0 && value.length === 0) { |
| const detail = { |
| query: this.createQueryFromFieldData(), |
| param: fieldInfo.param, |
| value: value.length > 0 ? removedValue : value, |
| }; |
| this.dispatchEvent( |
| new CustomEvent('remove-trace', { |
| detail: detail, |
| bubbles: true, |
| composed: true, |
| }) |
| ); |
| return; |
| } |
| // Only update when autoAdd is ready and chart is active. |
| if (!this.autoAddTrace) { |
| return; |
| } |
| if (this._count > PLOT_MAXIMUM) { |
| // Show error message if there are too many traces. |
| errorMessage(MAX_MESSAGE); |
| return; |
| } |
| this.setReadOnly(true); |
| if (this._graphDiv !== null && this._graphDiv.children.length > 0) { |
| const detail = { |
| query: this.createQueryFromFieldData(), |
| param: fieldInfo.param, |
| value: value.length > 0 ? removedValue : value, |
| }; |
| if (removedValue.length > 0) { |
| // Remove item from chart, no need to requery. |
| this.dispatchEvent( |
| new CustomEvent('remove-trace', { |
| detail: detail, |
| bubbles: true, |
| composed: true, |
| }) |
| ); |
| return; |
| } |
| // Field was split, but not enough values so remove split. |
| if (fieldInfo.field!.split && value.length < 2) { |
| this.setSplitFields(fieldInfo.param, false); |
| this.dispatchEvent( |
| new CustomEvent('split-by-changed', { |
| detail: { |
| param: fieldInfo.param, |
| split: false, |
| }, |
| bubbles: true, |
| composed: true, |
| }) |
| ); |
| return; |
| } |
| this.dispatchEvent( |
| new CustomEvent('add-to-graph', { |
| detail: detail, |
| bubbles: true, |
| }) |
| ); |
| } |
| } |
| |
| private addValueUpdatedEventToField(index: number) { |
| const fieldInfo = this._fieldData[index]; |
| if (fieldInfo.field === null) { |
| return; |
| } |
| // Remove existing listeners if they exist. |
| if (fieldInfo.onValueChanged) { |
| fieldInfo.field.removeEventListener('value-changed', fieldInfo.onValueChanged); |
| } |
| if (fieldInfo.onSplitByChanged) { |
| fieldInfo.field.removeEventListener('split-by-changed', fieldInfo.onSplitByChanged); |
| } |
| |
| // Create and store the new listeners. |
| fieldInfo.onValueChanged = (e: Event) => { |
| const value = (e as CustomEvent).detail.value as string[]; |
| const checkboxSelected = (e as CustomEvent).detail.checkboxSelected as boolean; |
| |
| if (value === fieldInfo.field!.selectedItems && !checkboxSelected) { |
| return; |
| } |
| |
| if (value.length === 0) { |
| this.removeChildFields(index); |
| } |
| |
| const newValues = new Set(value); |
| const oldValues = new Set(fieldInfo.value); |
| const removed = [...oldValues].filter((x) => !newValues.has(x)); |
| |
| // Don't update graph if the first field is changed as it can overload |
| // the graph. |
| if ( |
| this._fieldData[0].param === fieldInfo.param && |
| this._fieldData[0].field!.selectedItems.length > 0 && |
| removed.length === 0 |
| ) { |
| fieldInfo.field!.selectedItems = this._fieldData[0].field!.selectedItems; |
| errorMessage('Unable to add more items to the first field.'); |
| return; |
| } |
| |
| if (fieldInfo.value !== value) { |
| fieldInfo.value = value; |
| } |
| |
| if (value.length !== fieldInfo.field!.selectedItems.length) { |
| // Selected Item Needs to be updated if the explore was removed. |
| fieldInfo.field!.selectedItems = value; |
| } |
| if (value.length > 1) { |
| this.setReadOnly(true); |
| } |
| this.updateGraph(value, fieldInfo, removed); |
| this.fetchExtraOptions(index); |
| }; |
| |
| fieldInfo.onSplitByChanged = (e: Event) => { |
| const param = (e as CustomEvent).detail.param; |
| const split = (e as CustomEvent).detail.split; |
| this.setSplitFields(param, split); |
| }; |
| |
| // Add the new listeners. |
| fieldInfo.field!.addEventListener('value-changed', fieldInfo.onValueChanged); |
| fieldInfo.field.addEventListener('split-by-changed', fieldInfo.onSplitByChanged); |
| } |
| |
| /** |
| * Sets the split property for a given parameter and enables/disables split |
| * options for other fields. |
| * @param param The parameter to set the split property for. |
| * @param split A boolean indicating whether to split or not. |
| */ |
| private setSplitFields(param: string, split: boolean) { |
| this.setReadOnly(true); |
| for (let i = 0; i < this._fieldData.length; i++) { |
| if (this._fieldData[i].param === param) { |
| (this._fieldData[i].field as PickerFieldSk).split = split; |
| // Set split values and disable all other params |
| if (split) { |
| this._fieldData[i].splitBy = [param]; |
| } else { |
| this._fieldData[i].splitBy = []; |
| } |
| } else { |
| // Enable or disable the rest of the Split options to avoid multiple |
| // splits from being attempted. |
| if (split) { |
| this._fieldData[i].field?.disableSplit(); |
| } else { |
| this._fieldData[i].field?.enableSplit(); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Fetches the values for a given field. |
| * |
| * When creating a new field, we need to talk to the backend to |
| * figure out which options the field can provide as valid options in |
| * its dropdown menu. |
| * |
| * Once options are fetched, the field will be populated. Its dropdown |
| * menu will be automatically opened, unless it is the first field. |
| * The match count is also updated. |
| * |
| * @param index |
| */ |
| private fetchExtraOptions(index: number) { |
| const handler = (json: NextParamListHandlerResponse) => { |
| const param = Object.keys(json.paramset)[0]; |
| const count: number = json.count || -1; |
| if (param !== undefined) { |
| for (let i = 0; i < this._fieldData.length; i++) { |
| const fieldInfo = this._fieldData[i]; |
| if (fieldInfo.param === param) { |
| if (fieldInfo.field === null) { |
| const field: PickerFieldSk = new PickerFieldSk(param); |
| fieldInfo.field = field; |
| this._containerDiv!.appendChild(field); |
| } |
| fieldInfo.field!.options = json.paramset[param].filter( |
| (option: string) => !option.includes('.') |
| ); |
| const extraTests = json.paramset[param].filter((option: string) => |
| option.includes('.') |
| ); |
| if (extraTests.length > 0) { |
| fieldInfo.field!.options = fieldInfo.field!.options.concat(extraTests); |
| } |
| fieldInfo.field.index = i; |
| fieldInfo.field!.focus(); |
| this.addValueUpdatedEventToField(i); |
| // Track the furthest index queried |
| if (this._currentIndex <= i) { |
| this._currentIndex = i; |
| this.updateCount(count); |
| } |
| break; |
| } |
| } |
| } else { |
| // No parameter, so last item. Update count. |
| this._currentIndex = this._fieldData.length - 1; |
| this.updateCount(count); |
| } |
| this._render(); |
| }; |
| this.callNextParamList(handler, index); |
| } |
| |
| createParamSetFromFieldData(): ParamSet { |
| const paramSet: ParamSet = {}; |
| if (this._fieldData[0].value === null) { |
| return {}; |
| } |
| |
| this._fieldData.forEach((fieldInfo) => { |
| if (fieldInfo.value !== null) { |
| paramSet[fieldInfo.param] = fieldInfo.value; |
| } |
| }); |
| |
| // If all fields are empty, don't add any defaults, which can potentially |
| // make the query slow. An empty query should be a fast retrieval. |
| if (Object.keys(paramSet).length === 0) { |
| return {}; |
| } |
| // If values are set in child values, but missing initial value, then exit. |
| if (this._fieldData[0].value.length === 0) { |
| return {}; |
| } |
| // Apply default values. |
| for (const defaultParamKey in this._defaultParams) { |
| if (!(defaultParamKey in paramSet)) { |
| paramSet[defaultParamKey] = this._defaultParams![defaultParamKey]!; |
| } |
| } |
| |
| return paramSet; |
| } |
| |
| /** |
| * Generates a query string from the currently selected field values. |
| * Includes default parameter values if they are not already specified in the |
| * selected fields. |
| * @returns A query string representing the selected field values. |
| */ |
| createQueryFromFieldData(): string { |
| return fromParamSet(this.createParamSetFromFieldData()); |
| } |
| |
| /** |
| * Generates a query string from the selected field values up to a specified |
| * index. |
| * Includes default parameter values if they are not already specified in the |
| * selected fields. |
| * @param index - The maximum index of the field to include in the query |
| * string. |
| * @returns A query string representing the selected field values up to the |
| * specified index. |
| */ |
| createQueryFromIndex(index: number): string { |
| const paramSet: ParamSet = {}; |
| |
| for (let i = 0; i <= index; i++) { |
| const fieldInfo = this._fieldData[i]; |
| if (fieldInfo.value !== null) { |
| paramSet[fieldInfo.param] = fieldInfo.value; |
| } |
| } |
| |
| // If all fields are empty, don't add any defaults, which can potentially |
| // make the query slow. An empty query should be a fast retrieval. |
| if (Object.keys(paramSet).length === 0) { |
| return ''; |
| } |
| |
| // Apply default values. |
| for (const defaultParamKey in this._defaultParams) { |
| if (!(defaultParamKey in paramSet)) { |
| paramSet[defaultParamKey] = this._defaultParams![defaultParamKey]!; |
| } |
| } |
| |
| return fromParamSet(paramSet); |
| } |
| |
| /** |
| * Updates the count of matching traces and controls the state of the Plot |
| * button. |
| * If `count` is -1, it indicates loading, disabling the plot button and |
| * setting its title to 'Loading...'. |
| * If `count` is greater than `PLOT_MAXIMUM` or less than or equal to 0, the |
| * plot button is disabled. |
| * Otherwise, the plot button is enabled, and `autoAddTrace` is set based on |
| * whether a graph is already loaded. |
| * @param count - The number of matching traces. |
| */ |
| private updateCount(count: number) { |
| this._plotButton!.disabled = true; |
| if (count === -1) { |
| // Loading new data, so disable plotting. |
| this._plotButton!.title = 'Loading...'; |
| // Still loading so |
| if (this._currentIndex > 0) { |
| this.setReadOnly(true); |
| } else { |
| this.setReadOnly(false); |
| } |
| this._count = 0; |
| return; |
| } |
| |
| this.setReadOnly(false); |
| this._count = count; |
| if (count > PLOT_MAXIMUM || count <= 0) { |
| // Disable plotting if there are too many or no traces. |
| this.autoAddTrace = false; |
| this._plotButton!.title = this._count > PLOT_MAXIMUM ? 'Too many traces.' : 'No traces.'; |
| this._plotButton!.disabled = true; |
| return; |
| } |
| if (this._graphDiv && this._graphDiv.children.length > 0) { |
| // Graph is already loaded, so allow new changes automatically. |
| this.autoAddTrace = true; |
| } else { |
| // No graph loaded yet, so allow plotting. |
| this._plotButton!.title = 'Plot graph'; |
| this.autoAddTrace = false; |
| this._plotButton!.disabled = false; |
| } |
| } |
| |
| /** |
| * Sets whether traces should be added automatically to the graph. |
| * If `autoAdd` is true, the plot button will be disabled and its title will |
| * indicate automatic addition. |
| * Otherwise, the plot button will be enabled and its title will indicate |
| * manual plotting. |
| * @param autoAdd - A boolean indicating whether to automatically add traces. |
| */ |
| set autoAddTrace(autoAdd: boolean) { |
| this._autoAddTrace = autoAdd; |
| if (this._plotButton !== null) { |
| if (this._count > 0) { |
| this._plotButton.disabled = autoAdd; |
| this._plotButton!.title = autoAdd ? 'Traces are added automatically' : 'Plot a graph'; |
| } |
| } |
| } |
| |
| /** |
| * Returns whether traces are automatically added to the graph. |
| * @returns A boolean indicating whether traces are automatically added. |
| */ |
| get autoAddTrace(): boolean { |
| return this._autoAddTrace; |
| } |
| |
| /** |
| * Returns true if the first field is loaded. |
| * |
| * This is used to determine if the test picker is ready to be used. |
| * If the first field is not loaded, then we are not ready. |
| * |
| * @returns true if the first field is loaded, false otherwise. |
| */ |
| isLoaded(): boolean { |
| // If the first field is not loaded, then we are not ready. |
| return ( |
| this._fieldData.length > 0 && |
| this._fieldData[0].field !== null && |
| this._fieldData[0].field.selectedItems.length > 0 |
| ); |
| } |
| |
| /** |
| * Initializes Test Picker from scratch. |
| * |
| * Initializes the fieldData structure based on params given, and |
| * renders the first field for the user. |
| * |
| * @param params - A list of params that'll be used to populate |
| * the field labels and query the DB. The order of the list establishes |
| * a hierarchy in which each field can be populated. |
| * |
| * @param defaultParams - A map of default param values to apply to test |
| * selections. For example, if defaultParams is { 'bot': ['linux-perf'] }, |
| * queries will automatically get "bot=linux-perf" appended. The exception |
| * is if bot is already specified in the query, then no defaults are applied. |
| */ |
| initializeTestPicker( |
| params: string[], |
| defaultParams: { [key: string]: string[] | null }, |
| readOnly: boolean |
| ) { |
| this._defaultParams = defaultParams; |
| this.initializeFieldData(params); |
| this.addChildField(readOnly); |
| this._render(); |
| } |
| |
| /** |
| * Resets data structures from scratch. |
| * Clears the field container DOM, resets the index, |
| * resets the fieldData structure, and initializes fieldData |
| * based on given params. |
| * @param params - An array of parameter names to initialize the field data |
| * with. |
| */ |
| private initializeFieldData(params: string[]) { |
| this._containerDiv!.replaceChildren(); |
| this._currentIndex = 0; |
| if (this._fieldData.length > 0) { |
| this._fieldData = this._fieldData.filter((fieldInfo) => { |
| if (params.includes(fieldInfo.param)) { |
| fieldInfo.field = null; |
| fieldInfo.value = []; |
| return true; |
| } |
| return false; |
| }); |
| } else { |
| this._fieldData = []; |
| params.forEach((param, i) => { |
| this._fieldData.push({ |
| field: null, |
| param: param, |
| value: [], |
| splitBy: [], |
| index: i, |
| onValueChanged: null, |
| onSplitByChanged: null, |
| }); |
| }); |
| } |
| if (this._graphDiv && this._graphDiv.children.length > 0) { |
| this.autoAddTrace = true; |
| } |
| } |
| |
| /** |
| * Removes an item from the chart. |
| * @param param The parameter of the item to remove. |
| * @param value The value of the item to remove. |
| */ |
| removeItemFromChart(param: string, value: string[]) { |
| // Find the field info for the given param. |
| const fieldInfo = this._fieldData.find((field) => field.param === param); |
| if (fieldInfo) { |
| const newValue = fieldInfo.value.filter((v) => !value.includes(v)); |
| // Update the selected items in the field. |
| if (fieldInfo.field && fieldInfo.onValueChanged) { |
| // Create a mock event to pass to the handler. |
| const mockEvent = new CustomEvent('value-changed', { |
| detail: { |
| value: newValue, |
| }, |
| }); |
| // Directly call the handler. |
| fieldInfo.onValueChanged(mockEvent); |
| } |
| } |
| } |
| } |
| |
| define('test-picker-sk', TestPickerSk); |