Path: blob/main/extensions/copilot/test/simulation/workbench/components/localModeToolbar.tsx
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Badge, Button, Checkbox, ComboboxOpenChangeData, ComboboxOpenEvents, Dropdown, Input, Option, OptionOnSelectData, Select, SelectionEvents, Text, Tooltip } from '@fluentui/react-components';6import { Play16Regular, Stop16Regular } from '@fluentui/react-icons';7import * as mobx from 'mobx';8import * as mobxlite from 'mobx-react-lite';9import * as React from 'react';10import { useCallback, useEffect, useMemo, useRef, useState } from 'react';11import { InitArgs } from '../initArgs';12import { CacheMode, RunnerOptions } from '../stores/runnerOptions';13import { SimulationRunsProvider } from '../stores/simulationBaseline';14import { SimulationRunner, StateKind } from '../stores/simulationRunner';15import { SimulationTestsProvider } from '../stores/simulationTestsProvider';16import { useLocalStorageState } from '../stores/storage';17import { BaselineJSONPicker } from './baselineJSONPicker';18import { CompareAgainstRunPicker } from './compareAgainstRunPicker';19import { CurrentRunPicker } from './currentRunPicker';20import {21createAnnotationFilter,22createBaselineChangedFilter,23createCacheMissesFilter,24createFailuresFilter,25createFilterer,26createGrepFilter,27createRanTestsFilter28} from './filterUtils';29import { TestFilterer } from './testFilterer';3031/**32* Interface for filter parameters used in filtering tests33*/34interface FilterParams {35grep: string;36showBaselineJSONChangedOnly: boolean;37showFailedOnly: boolean;38showWithCacheMissesOnly: boolean;39showOnlyRanTests: boolean;40showOnlyTestsWithAnnotations: boolean;41selectedAnnotations: string[];42}4344/**45* Type for Fluent UI select option event46*/47interface SelectOptionEvent {48value: string;49}5051// Hook for asynchronously applying filters52const useAsyncFilter = (53filterParams: FilterParams,54debounceMs: number,55createFilter: (params: FilterParams) => TestFilterer,56onFilterChange: (filter: TestFilterer | undefined) => void57) => {58// Track if component is mounted to prevent state updates after unmount59const isMounted = useRef(true);60// Store the latest filter parameters61const latestFilterParams = useRef(filterParams);62// Track if a filter operation is in progress63const isFilteringRef = useRef(false);64// Track if there's a pending filter operation65const hasPendingFilterRef = useRef(false);66// Add a state to indicate filtering is in progress (could be used for UI feedback)67const [isFiltering, setIsFiltering] = useState(false);6869// Update the latest filter params when they change70useEffect(() => {71latestFilterParams.current = filterParams;72}, [filterParams]);7374// Clean up when component unmounts75useEffect(() => {76return () => {77isMounted.current = false;78};79}, []);8081// Function to perform the actual filter creation asynchronously82const applyFilter = useCallback(() => {83if (!isMounted.current) { return; }8485// If already filtering, mark as pending and return86if (isFilteringRef.current) {87hasPendingFilterRef.current = true;88return;89}9091isFilteringRef.current = true;92setIsFiltering(true);9394// Use setTimeout to move filter creation off the main thread95setTimeout(() => {96// Create a local copy of the latest filter params to avoid race conditions97const params = latestFilterParams.current;9899// Use requestAnimationFrame to schedule filter application for the next frame100requestAnimationFrame(() => {101try {102// Create the filter103const newFilter = createFilter(params);104105// Schedule the filter change callback for the next frame to prevent UI blocking106requestAnimationFrame(() => {107if (isMounted.current) {108onFilterChange(newFilter);109isFilteringRef.current = false;110setIsFiltering(false);111112// If there's a pending filter request, process it113if (hasPendingFilterRef.current) {114hasPendingFilterRef.current = false;115applyFilter();116}117}118});119} catch (error) {120console.error('Error applying filter:', error);121if (isMounted.current) {122isFilteringRef.current = false;123setIsFiltering(false);124125// If there's a pending filter request, process it126if (hasPendingFilterRef.current) {127hasPendingFilterRef.current = false;128applyFilter();129}130}131}132});133}, 0); // Use 0ms timeout to defer to the next event loop tick134}, [createFilter, onFilterChange]);135136// Set up a debounced effect to apply the filter137useEffect(() => {138const debounceTimer = setTimeout(applyFilter, debounceMs);139return () => clearTimeout(debounceTimer);140}, [filterParams, applyFilter, debounceMs]);141142return { isFiltering };143};144145type Props = {146initArgs: InitArgs | undefined;147runner: SimulationRunner;148runnerOptions: RunnerOptions;149simulationRunsProvider: SimulationRunsProvider;150simulationTestsProvider: SimulationTestsProvider;151onFiltererChange: (filter: TestFilterer | undefined) => void;152};153154export const LocalModeToolbar = mobxlite.observer(155({156initArgs,157runner,158runnerOptions,159simulationRunsProvider,160simulationTestsProvider,161onFiltererChange,162}: Props) => {163164// filtering165const [showBaselineJSONChangedOnly, setShowBaselineJSONChangedOnly] = useLocalStorageState('baselineJSONChangedOnly', undefined, false);166const [showFailedOnly, setShowFailedOnly] = useLocalStorageState('showFailedOnly', undefined, false);167const [showOnlyRanTests, setShowOnlyRanTests] = useLocalStorageState('showOnlyRanTests', undefined, false);168const [showWithCacheMissesOnly, setShowWithCacheMissesOnly] = React.useState(false);169const [showOnlyTestsWithAnnotations, setShowOnlyTestsWithAnnotations] = useLocalStorageState('showOnlyTestsWithAnnotations', undefined, false);170const [selectedAnnotations, setSelectedAnnotations] = useLocalStorageState<string[]>('selectedAnnotations', undefined, []);171172// Create filter parameters object that will be processed asynchronously173const filterParams = useMemo(() => ({174grep: runnerOptions.grep.value,175showBaselineJSONChangedOnly,176showFailedOnly,177showWithCacheMissesOnly,178showOnlyRanTests,179showOnlyTestsWithAnnotations,180selectedAnnotations181}), [182runnerOptions.grep.value,183showBaselineJSONChangedOnly,184showFailedOnly,185showWithCacheMissesOnly,186showOnlyRanTests,187showOnlyTestsWithAnnotations,188selectedAnnotations189]);190191// Callback for creating a filter instance - memoized to prevent recreations192const createFilter = useCallback((params: FilterParams): TestFilterer => {193const predicates = [];194195// Add active filter predicates196predicates.push(createGrepFilter(params.grep));197198if (params.showBaselineJSONChangedOnly) {199predicates.push(createBaselineChangedFilter());200}201202if (params.showFailedOnly) {203predicates.push(createFailuresFilter());204}205206if (params.showWithCacheMissesOnly) {207predicates.push(createCacheMissesFilter());208}209210if (params.showOnlyRanTests) {211predicates.push(createRanTestsFilter());212}213214if (params.showOnlyTestsWithAnnotations && params.selectedAnnotations.length > 0) {215predicates.push(createAnnotationFilter(new Set(params.selectedAnnotations)));216}217218return createFilterer(predicates);219}, []);220221// Memoize the filter change callback222const handleFilterChange = useCallback((filter: TestFilterer | undefined) => {223onFiltererChange(filter);224}, [onFiltererChange]);225226// Use the async filter hook with a longer debounce time (500ms)227const { isFiltering } = useAsyncFilter(228filterParams,229500, // Increased from 300ms to 500ms230createFilter,231handleFilterChange232);233234const isSimulationRunning = runner.state.kind === StateKind.Running;235236const [knownAnnotations, setKnownAnnotations] = React.useState<string[]>([]);237238// Memoize event handlers to prevent recreations on each render239const updateKnownAnnotations = useCallback(() => {240const newAnnotations = new Set<string>(knownAnnotations);241for (const test of simulationTestsProvider.tests) {242if (test.runnerStatus) {243for (const run of test.runnerStatus.runs) {244for (const annotation of run.annotations) {245newAnnotations.add(annotation.label);246}247}248}249}250setKnownAnnotations([...newAnnotations].sort());251}, [knownAnnotations, simulationTestsProvider.tests]);252253const handleRunStopButtonClick = useCallback(() => {254mobx.runInAction(() => {255if (isSimulationRunning) {256runner.stopRunning();257} else {258runner.startRunning({259grep: runnerOptions.grep.value,260cacheMode: runnerOptions.cacheMode.value,261n: parseInt(runnerOptions.n.value),262noFetch: runnerOptions.noFetch.value,263additionalArgs: runnerOptions.additionalArgs.value,264});265}266});267}, [isSimulationRunning, runner, runnerOptions]);268269// Memoize checkbox handlers270const handleBaselineJSONChangedOnlyChange = useCallback(() => {271setShowBaselineJSONChangedOnly(!showBaselineJSONChangedOnly);272}, [showBaselineJSONChangedOnly, setShowBaselineJSONChangedOnly]);273274const handleShowFailedOnlyChange = useCallback(() => {275setShowFailedOnly(!showFailedOnly);276}, [showFailedOnly, setShowFailedOnly]);277278const handleShowWithCacheMissesOnlyChange = useCallback(() => {279setShowWithCacheMissesOnly(!showWithCacheMissesOnly);280}, [showWithCacheMissesOnly]);281282const handleShowOnlyRanTestsChange = useCallback(() => {283setShowOnlyRanTests(!showOnlyRanTests);284}, [showOnlyRanTests, setShowOnlyRanTests]);285286const handleShowOnlyTestsWithAnnotationsChange = useCallback(() => {287setShowOnlyTestsWithAnnotations(!showOnlyTestsWithAnnotations);288}, [showOnlyTestsWithAnnotations, setShowOnlyTestsWithAnnotations]);289290const handleOptionSelect = useCallback((_e: SelectionEvents, o: OptionOnSelectData) => {291setSelectedAnnotations(o.selectedOptions);292setShowOnlyTestsWithAnnotations(o.selectedOptions.length > 0);293}, [setSelectedAnnotations, setShowOnlyTestsWithAnnotations]);294295const handleOpenChange = useCallback((_e: ComboboxOpenEvents, o: ComboboxOpenChangeData) => {296if (o.open) {297updateKnownAnnotations();298}299}, [updateKnownAnnotations]);300301const handleGrepChange = useCallback((e: React.FormEvent<HTMLInputElement>) => {302mobx.runInAction(() => {303runnerOptions.grep.value = (e.target as HTMLInputElement).value;304});305}, [runnerOptions.grep]);306307const handleNRunsChange = useCallback((e: React.FormEvent<HTMLInputElement>) => {308runnerOptions.n.value = (e.target as HTMLInputElement).value;309}, [runnerOptions.n]);310311const handleNoFetchChange = useCallback(() => {312mobx.runInAction(() => {313runnerOptions.noFetch.value = !runnerOptions.noFetch.value;314});315}, [runnerOptions.noFetch]);316317const handleExtraArgsChange = useCallback((e: React.FormEvent<HTMLInputElement>) => {318mobx.runInAction(() => {319runnerOptions.additionalArgs.value = (e.target as HTMLInputElement).value;320});321}, [runnerOptions.additionalArgs]);322323const handleUpdateBaselineJSON = useCallback(() => {324simulationTestsProvider.baselineJSONProvider.updateRootBaselineJSON();325}, [simulationTestsProvider.baselineJSONProvider]);326327const handleRunFromDiskChange = useCallback((selected: string | undefined) => {328const name = selected ?? '';329runner.setSelectedRunFromDisk(name);330}, [runner]);331332const handleCacheModeChange = useCallback((_e: React.FormEvent<HTMLSelectElement>, option: SelectOptionEvent) => {333runnerOptions.cacheMode.value = option.value as CacheMode;334}, [runnerOptions.cacheMode]);335336return (337<div338className='toolbar'339style={{340display: 'flex',341flexDirection: 'column',342justifyContent: 'space-between',343}}344>345<div style={{ display: 'flex', justifyContent: 'space-between' }}>346<div style={{ display: 'flex', alignItems: 'center' }}>347<CurrentRunPicker348simulationRunsProvider={simulationRunsProvider}349disabled={isSimulationRunning}350onChange={handleRunFromDiskChange}351outputFolderName={runner.selectedRun}352/>353<CompareAgainstRunPicker354simulationRunsProvider={simulationRunsProvider}355/>356<BaselineJSONPicker testsProvider={simulationTestsProvider} />357</div>358</div>359<div style={{ height: '8px' }} />360<div>361<div>Configure new run</div>362<div style={{ display: 'flex', gap: '10px' }}>363<Input364id='grep'365size='small'366placeholder='grep'367title='Filter by test name'368value={runnerOptions.grep.value}369onChange={handleGrepChange}370style={{ width: '300px', maxWidth: '25vw' }}371/>372<Input373id='nRuns'374size='small'375style={{ width: '40px' }}376placeholder='N'377title='Specify number of runs per each test'378value={runnerOptions.n.value}379onChange={handleNRunsChange}380/>381<Select onChange={handleCacheModeChange} defaultValue={runnerOptions.cacheMode.value}>382<option value={CacheMode.Default} title='Use cache if available'>Use Cache</option>383<option value={CacheMode.Disable} title='Do not use cache'>No Cache</option>384<option value={CacheMode.Require} title='Use cache, fail if not available'>Require Cache</option>385</Select>386<Tooltip relationship='label' content={'Do not send requests to the model endpoint (uses cache but doesn\'t write to it) (useful to make sure prompts are unchanged by observing cache misses)'}>387<Checkbox388label='No fetch'389defaultChecked={runnerOptions.noFetch.value}390onChange={handleNoFetchChange}391/>392</Tooltip>393<Input394id='extraArgs'395size='small'396style={{ width: '300px' }}397placeholder='Extra args, e.g., --parallelism=10 --require-cache'398value={runnerOptions.additionalArgs.value}399onInput={handleExtraArgsChange}400/>401<Button402size='small' appearance={isSimulationRunning ? 'secondary' : 'primary'}403icon={isSimulationRunning ? <Stop16Regular /> : <Play16Regular />}404iconPosition='before'405onClick={handleRunStopButtonClick}406style={{ width: '69px' }}407>408{isSimulationRunning ? 'Stop' : 'Run'}409</Button>410{411runner.selectedRun !== '' && runner.state.kind !== StateKind.Running &&412<Button size='small' onClick={handleUpdateBaselineJSON}> Update baseline.json </Button>413}414</div>415</div>416<div style={{ height: '8px' }} />417<div>418<Tooltip content={'Only modify tests displayed. Tests will be run even if not in list below'} relationship={'label'}>419<Text>View Filters {isFiltering && '(Filtering...)'}</Text>420</Tooltip>421<div style={{ display: 'flex', alignItems: 'center' }}>422<Checkbox423className='showBaselineJSONChanged'424label='Show baseline.json changed only'425checked={showBaselineJSONChangedOnly}426onChange={handleBaselineJSONChangedOnlyChange}427/>428<Checkbox429className='showFailedOnly'430label='Show with failures only'431checked={showFailedOnly}432onChange={handleShowFailedOnlyChange}433/>434<Checkbox435label='Show with cache misses only'436checked={showWithCacheMissesOnly}437onChange={handleShowWithCacheMissesOnlyChange}438/>439<Checkbox440label='Show ran tests only'441checked={showOnlyRanTests}442onChange={handleShowOnlyRanTestsChange}443/>444<Checkbox445label='Show with annotations only:'446checked={showOnlyTestsWithAnnotations}447onChange={handleShowOnlyTestsWithAnnotationsChange}448/>449<Dropdown450multiselect451size='small'452placeholder='Select annotations'453defaultValue={selectedAnnotations.length ? selectedAnnotations.join(', ') : undefined}454defaultSelectedOptions={selectedAnnotations}455onOptionSelect={handleOptionSelect}456onOpenChange={handleOpenChange}457>458{knownAnnotations.map((option) => (459<Option key={option} text={option}>460<Badge key={option} shape='square' appearance='outline' size='small'>{option}</Badge>461</Option>462))}463</Dropdown>464</div>465</div>466</div >467);468}469);470471472