diff --git a/webui/react/package-lock.json b/webui/react/package-lock.json index f293bbc4889..a949027202e 100644 --- a/webui/react/package-lock.json +++ b/webui/react/package-lock.json @@ -4032,12 +4032,12 @@ "license": "Python-2.0" }, "node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "dependencies": { - "deep-equal": "^2.0.5" + "dequal": "^2.0.3" } }, "node_modules/array-buffer-byte-length": { @@ -5342,34 +5342,6 @@ "node": ">=6" } }, - "node_modules/deep-equal": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.0.tgz", - "integrity": "sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "es-get-iterator": "^1.1.2", - "get-intrinsic": "^1.1.3", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.1", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/deep-is": { "version": "0.1.3", "dev": true, @@ -5799,26 +5771,6 @@ "node": ">= 0.4" } }, - "node_modules/es-get-iterator": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", - "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/es-iterator-helpers": { "version": "1.0.19", "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz", @@ -8042,22 +7994,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-array-buffer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", @@ -10304,22 +10240,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object-is": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object-keys": { "version": "1.1.1", "dev": true, @@ -13036,18 +12956,6 @@ "integrity": "sha512-JGUEaALvL0Mf6JCfYnJOTcobY+Nc7sG/TemDRBqCA0wEr4DER7zDchaaixTlmOxAjG1uRJmX82EQcxwTQTkqVA==", "dev": true }, - "node_modules/stop-iteration-iterator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", - "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", - "dev": true, - "dependencies": { - "internal-slot": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", diff --git a/webui/react/src/components/ColumnPickerMenu.tsx b/webui/react/src/components/ColumnPickerMenu.tsx index 89e195926a5..f5596581e01 100644 --- a/webui/react/src/components/ColumnPickerMenu.tsx +++ b/webui/react/src/components/ColumnPickerMenu.tsx @@ -183,7 +183,7 @@ const ColumnPickerTab: React.FC = ({ ); return ( -
+
vi.fn()); + +vi.mock('services/api', () => ({ + getProjectColumns: vi.fn().mockReturnValue([]), + getWorkspaces: vi.fn().mockResolvedValue({ workspaces: [] }), + resetUserSetting: () => Promise.resolve(), + searchExperiments, +})); + +vi.mock('stores/userSettings', async (importOriginal) => { + const userSettings = await import('stores/userSettings'); + const store = new userSettings.UserSettingsStore(); + + store.clear(); + + return { + ...(await importOriginal()), + default: store, + }; +}); + +vi.mock('hooks/useMobile', async (importOriginal) => { + return { + ...(await importOriginal()), + default: () => false, + }; +}); + +const user = userEvent.setup(); + +const setup = (numExperiments?: number) => { + searchExperiments.mockImplementation(() => { + return Promise.resolve({ + experiments: [], + pagination: { total: numExperiments ?? 0 }, + }); + }); + + render( + + + + + + + , + ); +}; + +describe('Searches', () => { + it('should display count', async () => { + setup(2); + expect(screen.getByText('Loading searches...')).toBeInTheDocument(); + + await waitFor(() => { + expect(screen.getByText('2 searches')).toBeInTheDocument(); + }); + }); + + it('should display empty state', async () => { + setup(); + expect(screen.getByText('Loading searches...')).toBeInTheDocument(); + + await waitFor(() => { + expect(screen.getByText('0 searches')).toBeInTheDocument(); + expect(screen.getByText('No Searches')).toBeInTheDocument(); + }); + }); + + it('should display column picker menu without tab selection', async () => { + setup(); + + await user.click(screen.getByTestId('columns-menu-button')); + expect(screen.queryByRole('tab')).not.toBeInTheDocument(); + expect(screen.getByTestId('column-picker-tab')).toBeInTheDocument(); + }); + + it('should have hidden filter to exclude single-trial experiments', () => { + setup(); + + expect(vi.mocked(searchExperiments)).toHaveBeenCalledWith( + expect.objectContaining({ + filter: expectedFilterString, + limit: defaultProjectSettings.pageLimit, + offset: 0, + projectId: projectMock.id, + sort: defaultProjectSettings.sortString, + }), + { signal: expect.any(AbortSignal) }, + ); + }); +}); diff --git a/webui/react/src/components/Searches/Searches.tsx b/webui/react/src/components/Searches/Searches.tsx index 56bca4888e9..b938ae2b01d 100644 --- a/webui/react/src/components/Searches/Searches.tsx +++ b/webui/react/src/components/Searches/Searches.tsx @@ -21,6 +21,7 @@ import DataGrid, { } from 'hew/DataGrid/DataGrid'; import { MenuItem } from 'hew/Dropdown'; import Icon from 'hew/Icon'; +import Link from 'hew/Link'; import Message from 'hew/Message'; import Pagination from 'hew/Pagination'; import Row from 'hew/Row'; @@ -31,7 +32,7 @@ import { useObservable } from 'micro-observables'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { v4 as uuidv4 } from 'uuid'; -import { Error, NoExperiments } from 'components/exceptions'; +import { Error } from 'components/exceptions'; import ExperimentActionDropdown from 'components/ExperimentActionDropdown'; import { FilterFormStore, ROOT_ID } from 'components/FilterForm/components/FilterFormStore'; import { @@ -55,6 +56,7 @@ import useMobile from 'hooks/useMobile'; import usePolling from 'hooks/usePolling'; import { useSettings } from 'hooks/useSettings'; import { useTypedParams } from 'hooks/useTypedParams'; +import { paths } from 'routes/utils'; import { getProjectColumns, searchExperiments } from 'services/api'; import { V1BulkExperimentFilters, @@ -873,7 +875,16 @@ const Searches: React.FC = ({ project }) => {
{!isLoading && experiments.length === 0 ? ( numFilters === 0 ? ( - + + Quick Start Guide + + } + description="Keep track of searches in a project by connecting up your code." + icon="experiment" + title="No Searches" + /> ) : ( )