diff --git a/.github/disabled_workflows/chromatic.yml b/.github/disabled_workflows/chromatic.yml deleted file mode 100644 index e7ff272038..0000000000 --- a/.github/disabled_workflows/chromatic.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Chromatic -on: - push: - branches: - - develop - - rel-* - - release-* - - github-actions-* - tags: - - v* - pull_request: - types: [opened, synchronize] - branches: - - develop - - rel-* - - release-* - paths: - - app/** - - .github/workflows/chromatic.yml - -jobs: - chromatic: - runs-on: ubuntu-20.04 - steps: - - name: Clone fiftyone - uses: actions/checkout@v1 - with: - submodules: true - - name: Fetch dependency cache - uses: actions/cache@v2 - with: - key: electron-cache - path: app/node_modules - - name: Install dependencies - working-directory: app - run: yarn - - name: Publish storybook - working-directory: app - run: yarn chromatic --project-token=${{ secrets.CHROMATIC_PROJECT_TOKEN }} - - uses: actions/upload-artifact@v2 - if: success() || failure() - with: - name: build-storybook-log - path: app/build-storybook.log diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml deleted file mode 100644 index 25cf3c178c..0000000000 --- a/.github/workflows/build-desktop.yml +++ /dev/null @@ -1,103 +0,0 @@ -name: Build Desktop App - -on: - push: - tags: - - desktop-v* - pull_request: - paths: - - app/** - - package/desktop/** - - .github/workflows/build-desktop.yml - -jobs: - build: - runs-on: ubuntu-22.04 - strategy: - fail-fast: false - matrix: - platform: - - mac-arm64 - - mac-x86_64 - - linux-x86_64 - - linux-aarch64 - - win-amd64 - steps: - - name: Clone fiftyone - uses: actions/checkout@v4 - with: - submodules: true - - name: Set up Python 3.8 - uses: actions/setup-python@v5 - with: - python-version: 3.8 - - name: Install dependencies - run: | - pip install --upgrade pip setuptools wheel build - - name: Cache Node Modules - id: node-cache - uses: actions/cache@v4 - with: - path: | - app/node_modules - app/.yarn/cache - key: node-modules-${{ hashFiles('app/yarn.lock') }} - - name: Install app - run: cd app && yarn install - - name: Package App (Non-Windows) - if: ${{ matrix.platform != 'win-amd64' }} - working-directory: app/packages/desktop - run: yarn package-${{ matrix.platform }} --publish never - - name: Package App (Windows) - if: ${{ matrix.platform == 'win-amd64' }} - working-directory: app/ - run: | - docker run --rm \ - --env-file <(env | grep -iE 'DEBUG|NODE_|ELECTRON_|YARN_|NPM_|CI|CIRCLE|TRAVIS_TAG|TRAVIS|TRAVIS_REPO_|TRAVIS_BUILD_|TRAVIS_BRANCH|TRAVIS_PULL_REQUEST_|APPVEYOR_|CSC_|GH_|GITHUB_|BT_|AWS_|STRIP|BUILD_') \ - --env ELECTRON_CACHE="/root/.cache/electron" \ - --env ELECTRON_BUILDER_CACHE="/root/.cache/electron-builder" \ - -v ${PWD}:/project \ - -v ~/.cache/electron:/root/.cache/electron \ - -v ~/.cache/electron-builder:/root/.cache/electron-builder \ - electronuserland/builder:wine \ - yarn workspace FiftyOne package-${{ matrix.platform }} --publish never - - name: Set environment - env: - RELEASE_TAG: ${{ github.ref }} - run: | - if [[ $RELEASE_TAG =~ ^refs\/tags\/desktop-v.*-rc\..*$ ]]; then - echo "RELEASE_VERSION=$(echo "${{ github.ref }}" | sed "s/^refs\/tags\/desktop-v//")" >> $GITHUB_ENV - fi - - name: Build wheel - working-directory: package/desktop - run: RELEASE_DIR=${PWD}/../../app/packages/desktop/release python -Im build -C="--build-option=--plat-name=${{ matrix.platform }}" - - name: Upload wheel - uses: actions/upload-artifact@v4 - with: - name: wheel-${{ matrix.platform }} - path: package/desktop/dist/*.whl - - publish: - runs-on: ubuntu-20.04 - needs: [build] - if: startsWith(github.ref, 'refs/tags/desktop-v') - steps: - - name: Download wheels - uses: actions/download-artifact@v4 - with: - path: downloads - - name: Install dependencies - run: | - pip3 install twine - - name: Set environment - env: - RELEASE_TAG: ${{ github.ref }} - run: | - echo "TWINE_PASSWORD=${{ secrets.FIFTYONE_PYPI_TOKEN }}" >> $GITHUB_ENV - echo "TWINE_REPOSITORY=pypi" >> $GITHUB_ENV - - name: Upload to pypi - env: - TWINE_USERNAME: __token__ - TWINE_NON_INTERACTIVE: 1 - run: | - python3 -m twine upload downloads/wheel-*/*.whl diff --git a/app/packages/app/src/pages/datasets/__generated__/DatasetPageQuery.graphql.ts b/app/packages/app/src/pages/datasets/__generated__/DatasetPageQuery.graphql.ts index 9eec3727f2..8135f4ce77 100644 --- a/app/packages/app/src/pages/datasets/__generated__/DatasetPageQuery.graphql.ts +++ b/app/packages/app/src/pages/datasets/__generated__/DatasetPageQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<6e185677e63db2c2bdb3bba97ae284d4>> * @lightSyntaxTransform * @nogrep */ @@ -1447,12 +1447,12 @@ return { ] }, "params": { - "cacheID": "5a1d173917e24798599325eebd2eacb6", + "cacheID": "0094ca7fca41c8d0515cef6e7a3ab4d0", "id": null, "metadata": {}, "name": "DatasetPageQuery", "operationKind": "query", - "text": "query DatasetPageQuery(\n $count: Int\n $cursor: String\n $name: String!\n $extendedView: BSONArray!\n $savedViewSlug: String\n $search: String = \"\"\n $view: BSONArray!\n) {\n config {\n colorBy\n colorPool\n colorscale\n multicolorKeypoints\n showSkeletons\n }\n colorscale\n dataset(name: $name, view: $extendedView, savedViewSlug: $savedViewSlug) {\n name\n defaultGroupSlice\n appConfig {\n colorScheme {\n id\n colorBy\n colorPool\n multicolorKeypoints\n opacity\n showSkeletons\n defaultMaskTargetsColors {\n intTarget\n color\n }\n defaultColorscale {\n name\n list {\n value\n color\n }\n rgb\n }\n colorscales {\n path\n name\n list {\n value\n color\n }\n rgb\n }\n fields {\n colorByAttribute\n fieldColor\n path\n valueColors {\n color\n value\n }\n maskTargetsColors {\n intTarget\n color\n }\n }\n labelTags {\n fieldColor\n valueColors {\n color\n value\n }\n }\n }\n }\n ...datasetFragment\n id\n }\n ...NavFragment\n ...savedViewsFragment\n ...configFragment\n ...stageDefinitionsFragment\n ...viewSchemaFragment\n}\n\nfragment Analytics on Query {\n context\n dev\n doNotTrack\n uid\n version\n}\n\nfragment NavDatasets on Query {\n datasets(search: $search, first: $count, after: $cursor) {\n total\n edges {\n cursor\n node {\n name\n id\n __typename\n }\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment NavFragment on Query {\n ...Analytics\n ...NavDatasets\n}\n\nfragment colorSchemeFragment on ColorScheme {\n id\n colorBy\n colorPool\n multicolorKeypoints\n opacity\n showSkeletons\n labelTags {\n fieldColor\n valueColors {\n color\n value\n }\n }\n defaultMaskTargetsColors {\n intTarget\n color\n }\n defaultColorscale {\n name\n list {\n value\n color\n }\n rgb\n }\n colorscales {\n path\n name\n list {\n value\n color\n }\n rgb\n }\n fields {\n colorByAttribute\n fieldColor\n path\n valueColors {\n color\n value\n }\n maskTargetsColors {\n intTarget\n color\n }\n }\n}\n\nfragment configFragment on Query {\n config {\n colorBy\n colorPool\n colorscale\n disableFrameFiltering\n gridZoom\n lightningThreshold\n loopVideos\n mediaFallback\n multicolorKeypoints\n notebookHeight\n plugins\n showConfidence\n showIndex\n showLabel\n showSkeletons\n showTooltip\n sidebarMode\n theme\n timezone\n useFrameNumber\n }\n colorscale\n}\n\nfragment datasetAppConfigFragment on DatasetAppConfig {\n colorScheme {\n ...colorSchemeFragment\n id\n }\n disableFrameFiltering\n gridMediaField\n mediaFields\n modalMediaField\n mediaFallback\n plugins\n sidebarMode\n}\n\nfragment datasetFragment on Dataset {\n createdAt\n datasetId\n groupField\n id\n info\n lastLoadedAt\n mediaType\n name\n parentMediaType\n version\n appConfig {\n ...datasetAppConfigFragment\n }\n brainMethods {\n key\n version\n timestamp\n viewStages\n config {\n cls\n embeddingsField\n method\n patchesField\n supportsPrompts\n type\n maxK\n supportsLeastSimilarity\n }\n }\n defaultMaskTargets {\n target\n value\n }\n defaultSkeleton {\n labels\n edges\n }\n evaluations {\n key\n version\n timestamp\n viewStages\n config {\n cls\n predField\n gtField\n }\n }\n groupMediaTypes {\n name\n mediaType\n }\n maskTargets {\n name\n targets {\n target\n value\n }\n }\n skeletons {\n name\n labels\n edges\n }\n ...estimatedCountsFragment\n ...frameFieldsFragment\n ...groupSliceFragment\n ...indexesFragment\n ...mediaFieldsFragment\n ...mediaTypeFragment\n ...sampleFieldsFragment\n ...sidebarGroupsFragment\n ...viewFragment\n}\n\nfragment estimatedCountsFragment on Dataset {\n estimatedFrameCount\n estimatedSampleCount\n}\n\nfragment frameFieldsFragment on Dataset {\n frameFields {\n ftype\n subfield\n embeddedDocType\n path\n dbField\n description\n info\n }\n}\n\nfragment groupSliceFragment on Dataset {\n defaultGroupSlice\n}\n\nfragment indexesFragment on Dataset {\n frameIndexes {\n name\n unique\n key {\n field\n type\n }\n wildcardProjection {\n fields\n inclusion\n }\n }\n sampleIndexes {\n name\n unique\n key {\n field\n type\n }\n wildcardProjection {\n fields\n inclusion\n }\n }\n}\n\nfragment mediaFieldsFragment on Dataset {\n name\n appConfig {\n gridMediaField\n mediaFields\n modalMediaField\n mediaFallback\n }\n sampleFields {\n path\n }\n}\n\nfragment mediaTypeFragment on Dataset {\n mediaType\n}\n\nfragment sampleFieldsFragment on Dataset {\n sampleFields {\n ftype\n subfield\n embeddedDocType\n path\n dbField\n description\n info\n }\n}\n\nfragment savedViewsFragment on Query {\n savedViews(datasetName: $name) {\n id\n datasetId\n name\n slug\n description\n color\n viewStages\n createdAt\n lastModifiedAt\n lastLoadedAt\n }\n}\n\nfragment sidebarGroupsFragment on Dataset {\n name\n appConfig {\n sidebarGroups {\n expanded\n paths\n name\n }\n }\n ...frameFieldsFragment\n ...sampleFieldsFragment\n}\n\nfragment stageDefinitionsFragment on Query {\n stageDefinitions {\n name\n params {\n name\n type\n default\n placeholder\n }\n }\n}\n\nfragment viewFragment on Dataset {\n stages(slug: $savedViewSlug, view: $view)\n viewCls\n viewName\n}\n\nfragment viewSchemaFragment on Query {\n schemaForViewStages(datasetName: $name, viewStages: $view) {\n fieldSchema {\n path\n ftype\n subfield\n embeddedDocType\n info\n description\n }\n frameFieldSchema {\n path\n ftype\n subfield\n embeddedDocType\n info\n description\n }\n }\n}\n" + "text": "query DatasetPageQuery(\n $count: Int\n $cursor: String\n $name: String!\n $extendedView: BSONArray!\n $savedViewSlug: String\n $search: String = \"\"\n $view: BSONArray!\n) {\n config {\n colorBy\n colorPool\n colorscale\n multicolorKeypoints\n showSkeletons\n }\n colorscale\n dataset(name: $name, view: $extendedView, savedViewSlug: $savedViewSlug) {\n name\n defaultGroupSlice\n appConfig {\n colorScheme {\n id\n colorBy\n colorPool\n multicolorKeypoints\n opacity\n showSkeletons\n defaultMaskTargetsColors {\n intTarget\n color\n }\n defaultColorscale {\n name\n list {\n value\n color\n }\n rgb\n }\n colorscales {\n path\n name\n list {\n value\n color\n }\n rgb\n }\n fields {\n colorByAttribute\n fieldColor\n path\n valueColors {\n color\n value\n }\n maskTargetsColors {\n intTarget\n color\n }\n }\n labelTags {\n fieldColor\n valueColors {\n color\n value\n }\n }\n }\n }\n ...datasetFragment\n id\n }\n ...NavFragment\n ...savedViewsFragment\n ...configFragment\n ...stageDefinitionsFragment\n ...viewSchemaFragment\n}\n\nfragment Analytics on Query {\n context\n dev\n doNotTrack\n uid\n version\n}\n\nfragment NavDatasets on Query {\n datasets(search: $search, first: $count, after: $cursor) {\n total\n edges {\n cursor\n node {\n name\n id\n __typename\n }\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment NavFragment on Query {\n ...Analytics\n ...NavDatasets\n}\n\nfragment colorSchemeFragment on ColorScheme {\n id\n colorBy\n colorPool\n multicolorKeypoints\n opacity\n showSkeletons\n labelTags {\n fieldColor\n valueColors {\n color\n value\n }\n }\n defaultMaskTargetsColors {\n intTarget\n color\n }\n defaultColorscale {\n name\n list {\n value\n color\n }\n rgb\n }\n colorscales {\n path\n name\n list {\n value\n color\n }\n rgb\n }\n fields {\n colorByAttribute\n fieldColor\n path\n valueColors {\n color\n value\n }\n maskTargetsColors {\n intTarget\n color\n }\n }\n}\n\nfragment configFragment on Query {\n config {\n colorBy\n colorPool\n colorscale\n disableFrameFiltering\n gridZoom\n lightningThreshold\n loopVideos\n mediaFallback\n multicolorKeypoints\n notebookHeight\n plugins\n showConfidence\n showIndex\n showLabel\n showSkeletons\n showTooltip\n sidebarMode\n theme\n timezone\n useFrameNumber\n }\n colorscale\n}\n\nfragment datasetAppConfigFragment on DatasetAppConfig {\n colorScheme {\n ...colorSchemeFragment\n id\n }\n disableFrameFiltering\n gridMediaField\n mediaFields\n modalMediaField\n mediaFallback\n plugins\n sidebarMode\n}\n\nfragment datasetFragment on Dataset {\n createdAt\n datasetId\n groupField\n id\n info\n lastLoadedAt\n mediaType\n name\n parentMediaType\n version\n appConfig {\n ...datasetAppConfigFragment\n }\n brainMethods {\n key\n version\n timestamp\n viewStages\n config {\n cls\n embeddingsField\n method\n patchesField\n supportsPrompts\n type\n maxK\n supportsLeastSimilarity\n }\n }\n defaultMaskTargets {\n target\n value\n }\n defaultSkeleton {\n labels\n edges\n }\n evaluations {\n key\n version\n timestamp\n viewStages\n config {\n cls\n predField\n gtField\n }\n }\n groupMediaTypes {\n name\n mediaType\n }\n maskTargets {\n name\n targets {\n target\n value\n }\n }\n skeletons {\n name\n labels\n edges\n }\n ...estimatedCountsFragment\n ...frameFieldsFragment\n ...groupSliceFragment\n ...indexesFragment\n ...mediaFieldsFragment\n ...mediaTypeFragment\n ...sampleFieldsFragment\n ...sidebarGroupsFragment\n ...viewFragment\n}\n\nfragment estimatedCountsFragment on Dataset {\n estimatedFrameCount\n estimatedSampleCount\n}\n\nfragment frameFieldsFragment on Dataset {\n frameFields {\n ftype\n subfield\n embeddedDocType\n path\n dbField\n description\n info\n }\n}\n\nfragment groupSliceFragment on Dataset {\n defaultGroupSlice\n}\n\nfragment indexesFragment on Dataset {\n frameIndexes {\n name\n unique\n key {\n field\n type\n }\n wildcardProjection {\n fields\n inclusion\n }\n }\n sampleIndexes {\n name\n unique\n key {\n field\n type\n }\n wildcardProjection {\n fields\n inclusion\n }\n }\n}\n\nfragment mediaFieldsFragment on Dataset {\n name\n appConfig {\n gridMediaField\n mediaFields\n modalMediaField\n mediaFallback\n }\n sampleFields {\n path\n }\n}\n\nfragment mediaTypeFragment on Dataset {\n mediaType\n}\n\nfragment sampleFieldsFragment on Dataset {\n sampleFields {\n ftype\n subfield\n embeddedDocType\n path\n dbField\n description\n info\n }\n}\n\nfragment savedViewsFragment on Query {\n savedViews(datasetName: $name) {\n id\n datasetId\n name\n slug\n description\n color\n viewStages\n createdAt\n lastModifiedAt\n lastLoadedAt\n }\n}\n\nfragment sidebarGroupsFragment on Dataset {\n datasetId\n appConfig {\n sidebarGroups {\n expanded\n paths\n name\n }\n }\n ...frameFieldsFragment\n ...sampleFieldsFragment\n}\n\nfragment stageDefinitionsFragment on Query {\n stageDefinitions {\n name\n params {\n name\n type\n default\n placeholder\n }\n }\n}\n\nfragment viewFragment on Dataset {\n stages(slug: $savedViewSlug, view: $view)\n viewCls\n viewName\n}\n\nfragment viewSchemaFragment on Query {\n schemaForViewStages(datasetName: $name, viewStages: $view) {\n fieldSchema {\n path\n ftype\n subfield\n embeddedDocType\n info\n description\n }\n frameFieldSchema {\n path\n ftype\n subfield\n embeddedDocType\n info\n description\n }\n }\n}\n" } }; })(); diff --git a/app/packages/components/src/components/PillButton/PillButton.tsx b/app/packages/components/src/components/PillButton/PillButton.tsx index a8ea7bf0a2..08476ac897 100644 --- a/app/packages/components/src/components/PillButton/PillButton.tsx +++ b/app/packages/components/src/components/PillButton/PillButton.tsx @@ -49,14 +49,14 @@ const PillButton = React.forwardRef( ); type PillButtonProps = { - onClick: (event: Event) => void; - id?: string; - open?: boolean; + arrow?: boolean; highlight?: boolean; - text?: string; icon?: JSX.Element; - arrow?: boolean; + id?: string; + onClick: (event: Event) => void; + open?: boolean; style?: React.CSSProperties; + text?: string; title: string; }; diff --git a/app/packages/core/src/components/Grid/Grid.tsx b/app/packages/core/src/components/Grid/Grid.tsx index e1d848b9e8..1ad019455e 100644 --- a/app/packages/core/src/components/Grid/Grid.tsx +++ b/app/packages/core/src/components/Grid/Grid.tsx @@ -1,9 +1,9 @@ import styles from "./Grid.module.css"; -import type { Lookers } from "@fiftyone/state"; - import { freeVideos } from "@fiftyone/looker"; -import Spotlight, { type ID } from "@fiftyone/spotlight"; +import type { ID } from "@fiftyone/spotlight"; +import Spotlight from "@fiftyone/spotlight"; +import type { Lookers } from "@fiftyone/state"; import * as fos from "@fiftyone/state"; import React, { useEffect, @@ -17,6 +17,7 @@ import { v4 as uuid } from "uuid"; import { gridCrop, gridSpacing, pageParameters } from "./recoil"; import useAt from "./useAt"; import useEscape from "./useEscape"; +import useRecords from "./useRecords"; import useRefreshers from "./useRefreshers"; import useSelect from "./useSelect"; import useSelectSample from "./useSelectSample"; @@ -26,19 +27,22 @@ import useThreshold from "./useThreshold"; function Grid() { const id = useMemo(() => uuid(), []); const lookerStore = useMemo(() => new WeakMap(), []); - const selectSample = useRef>(); - const [resizing, setResizing] = useState(false); - const spacing = useRecoilValue(gridSpacing); + const selectSample = useRef>(); const { pageReset, reset } = useRefreshers(); - const { get, set } = useAt(pageReset); + const [resizing, setResizing] = useState(false); const threshold = useThreshold(); - const { page, records, store } = useSpotlightPager({ - pageSelector: pageParameters, - zoomSelector: gridCrop, - }); + const records = useRecords(pageReset); + const { page, store } = useSpotlightPager( + { + pageSelector: pageParameters, + zoomSelector: gridCrop, + }, + records + ); + const { get, set } = useAt(pageReset); const lookerOptions = fos.useLookerOptions(false); const createLooker = fos.useCreateLooker(false, true, lookerOptions); @@ -71,7 +75,7 @@ function Grid() { const init = (l) => { l.addEventListener("selectthumbnail", ({ detail }: CustomEvent) => { - selectSample.current?.(records, detail); + selectSample.current?.(detail); }); lookerStore.set(id, l); l.attach(element, dimensions); @@ -97,7 +101,7 @@ function Grid() { store, threshold, ]); - selectSample.current = useSelectSample(); + selectSample.current = useSelectSample(records); useSelect(lookerOptions, lookerStore, spotlight); useLayoutEffect(() => { diff --git a/app/packages/core/src/components/Grid/useRecords.test.ts b/app/packages/core/src/components/Grid/useRecords.test.ts new file mode 100644 index 0000000000..28f49e7dff --- /dev/null +++ b/app/packages/core/src/components/Grid/useRecords.test.ts @@ -0,0 +1,23 @@ +import { act, renderHook } from "@testing-library/react-hooks"; +import { describe, expect, it } from "vitest"; +import useRecords from "./useRecords"; + +describe("useRecords", () => { + it("return new records when clear string changes", () => { + const { result, rerender } = renderHook( + (clear: string) => useRecords(clear), + { initialProps: "one" } + ); + expect(result.current.size).toBe(0); + + act(() => { + result.current.set("one", 1); + }); + + expect(result.current.size).toBe(1); + + rerender("two"); + + expect(result.current.size).toBe(0); + }); +}); diff --git a/app/packages/core/src/components/Grid/useRecords.ts b/app/packages/core/src/components/Grid/useRecords.ts new file mode 100644 index 0000000000..f25b5f12d6 --- /dev/null +++ b/app/packages/core/src/components/Grid/useRecords.ts @@ -0,0 +1,10 @@ +import { useMemo } from "react"; + +export type Records = Map; + +export default (clear: string) => { + return useMemo(() => { + clear; + return new Map(); + }, [clear]); +}; diff --git a/app/packages/core/src/components/Grid/useSelect.ts b/app/packages/core/src/components/Grid/useSelect.ts index 809f524f8d..8973c436d9 100644 --- a/app/packages/core/src/components/Grid/useSelect.ts +++ b/app/packages/core/src/components/Grid/useSelect.ts @@ -1,5 +1,5 @@ import type Spotlight from "@fiftyone/spotlight"; -import { ID } from "@fiftyone/spotlight"; +import type { ID } from "@fiftyone/spotlight"; import * as fos from "@fiftyone/state"; import { useEffect } from "react"; import { useRecoilValue } from "recoil"; diff --git a/app/packages/core/src/components/Grid/useSelectSample.test.ts b/app/packages/core/src/components/Grid/useSelectSample.test.ts new file mode 100644 index 0000000000..99385e738e --- /dev/null +++ b/app/packages/core/src/components/Grid/useSelectSample.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import { addRange, removeRange } from "./useSelectSample"; + +describe("range selection tests", () => { + it("adds a range, and includes selections without an index record", () => { + const result = addRange( + 2, + new Set(["0", "other"]), + new Map([ + ["0", 0], + ["1", 1], + ["2", 2], + ]) + ); + expect(result).toStrictEqual(new Set(["0", "1", "2", "other"])); + }); + + it("removes a range, and includes selections without an index record", () => { + const result = removeRange( + 1, + new Set(["0", "1", "other"]), + new Map([ + ["0", 0], + ["1", 1], + ]) + ); + expect(result).toStrictEqual(new Set(["other"])); + }); +}); diff --git a/app/packages/core/src/components/Grid/useSelectSample.ts b/app/packages/core/src/components/Grid/useSelectSample.ts index 82b2dd0509..5b5cb096fb 100644 --- a/app/packages/core/src/components/Grid/useSelectSample.ts +++ b/app/packages/core/src/components/Grid/useSelectSample.ts @@ -1,13 +1,12 @@ -import { ID } from "@fiftyone/spotlight"; +import type { ID } from "@fiftyone/spotlight"; import type { Sample } from "@fiftyone/state"; - import { selectedSampleObjects, selectedSamples, useSetSelected, } from "@fiftyone/state"; -import { MutableRefObject } from "react"; import { useRecoilCallback } from "recoil"; +import type { Records } from "./useRecords"; export interface SelectThumbnailData { shiftKey: boolean; @@ -16,23 +15,19 @@ export interface SelectThumbnailData { symbol: ID; } -type Records = MutableRefObject>; - -const argFact = (compareFn) => (array) => - array.map((el, idx) => [el, idx]).reduce(compareFn)[1]; - -const argMin = argFact((max, el) => (el[0] < max[0] ? el : max)); - -const addRange = (index: number, items: string[], records: Records) => { +export const addRange = ( + index: number, + selected: Set, + records: Records +) => { + // filter selections without an index record + const items = [...selected].filter((i) => records.has(i)); const reverse = Object.fromEntries( - Array.from(records.current.entries()).map(([k, v]) => [v, k]) - ); - - const min = argMin( - items.map((id) => Math.abs(records.current.get(id) - index)) + Array.from(records.entries()).map(([k, v]) => [v, k]) ); + const min = argMin(items.map((id) => Math.abs(get(records, id) - index))); - const close = records.current.get(items[min]); + const close = get(records, items[min]); const [start, end] = index < close ? [index, close] : [close, index]; @@ -40,26 +35,42 @@ const addRange = (index: number, items: string[], records: Records) => { .fill(0) .map((_, i) => reverse[i + start]); - return new Set([...items, ...added]); + return new Set([...selected, ...added]); }; -const removeRange = ( +const argFact = (compareFn) => (array) => + array.map((el, idx) => [el, idx]).reduce(compareFn)[1]; + +const argMin = argFact((max, el) => (el[0] < max[0] ? el : max)); + +const get = (records: Records, id: string) => { + const index = records.get(id); + if (index !== undefined) { + return index; + } + + throw new Error(`record '${id}' not found`); +}; + +export const removeRange = ( index: number, selected: Set, records: Records ) => { + // filter selections without an index record + const items = new Set([...selected].filter((i) => records.has(i))); const reverse = Object.fromEntries( - Array.from(records.current.entries()).map(([k, v]) => [v, k]) + Array.from(records.entries()).map(([k, v]) => [v, k]) ); let before = index; - while (selected.has(reverse[before])) { + while (items.has(reverse[before])) { before--; } before += 1; let after = index; - while (selected.has(reverse[after])) { + while (items.has(reverse[after])) { after++; } after -= 1; @@ -73,31 +84,44 @@ const removeRange = ( ? [before, index] : [index, after]; - return new Set( - Array.from(selected).filter( - (s) => records.current.get(s) < start || records.current.get(s) > end + const next = new Set( + Array.from(items).filter( + (s) => get(records, s) < start || get(records, s) > end ) ); + + for (const id of selected) { + if (records.has(id)) continue; + // not in index records so it was not removed, add it back + next.add(id); + } + + return next; }; -export default () => { +export default (records: Records) => { const setSelected = useSetSelected(); return useRecoilCallback( ({ set, snapshot }) => - async ( - records: Records, - { shiftKey, id: sampleId, sample, symbol }: SelectThumbnailData - ) => { - let selected = new Set(await snapshot.getPromise(selectedSamples)); + async ({ + shiftKey, + id: sampleId, + sample, + symbol, + }: SelectThumbnailData) => { + const current = new Set(await snapshot.getPromise(selectedSamples)); + let selected = new Set(current); const selectedObjects = new Map( await snapshot.getPromise(selectedSampleObjects) ); - const items = Array.from(selected); - const index = records.current.get(symbol.description); + const index = get(records, symbol.description); if (shiftKey && !selected.has(sampleId)) { - selected = addRange(index, items, records); + selected = new Set([ + ...selected, + ...addRange(index, selected, records), + ]); } else if (shiftKey) { selected = removeRange(index, selected, records); } else { @@ -116,6 +140,6 @@ export default () => { set(selectedSampleObjects, selectedObjects); setSelected(new Set(selected)); }, - [setSelected] + [records, setSelected] ); }; diff --git a/app/packages/core/src/components/Grid/useSpotlightPager.ts b/app/packages/core/src/components/Grid/useSpotlightPager.ts index 7b67fc0c45..a752139438 100644 --- a/app/packages/core/src/components/Grid/useSpotlightPager.ts +++ b/app/packages/core/src/components/Grid/useSpotlightPager.ts @@ -14,6 +14,7 @@ import { import type { RecoilValueReadOnly } from "recoil"; import { useRecoilCallback, useRecoilValue } from "recoil"; import type { Subscription } from "relay-runtime"; +import type { Records } from "./useRecords"; export const PAGE_SIZE = 20; @@ -45,15 +46,18 @@ const processSamplePageData = ( }); }; -const useSpotlightPager = ({ - pageSelector, - zoomSelector, -}: { - pageSelector: RecoilValueReadOnly< - (page: number, pageSize: number) => VariablesOf - >; - zoomSelector: RecoilValueReadOnly; -}) => { +const useSpotlightPager = ( + { + pageSelector, + zoomSelector, + }: { + pageSelector: RecoilValueReadOnly< + (page: number, pageSize: number) => VariablesOf + >; + zoomSelector: RecoilValueReadOnly; + }, + records: Records +) => { const environment = useRelayEnvironment(); const pager = useRecoilValue(pageSelector); const zoom = useRecoilValue(zoomSelector); @@ -62,7 +66,8 @@ const useSpotlightPager = ({ () => new WeakMap(), [] ); - const records = useRef(new Map()); + + const keys = useRef(new Set()); const page = useRecoilCallback( ({ snapshot }) => { @@ -89,8 +94,9 @@ const useSpotlightPager = ({ data, schema, zoom, - records.current + records ); + for (const item of items) keys.current.add(item.id.description); resolve({ items, @@ -110,16 +116,16 @@ const useSpotlightPager = ({ ); const refresher = useRecoilValue(fos.refresher); + useEffect(() => { refresher; - const current = records.current; const clear = () => { commitLocalUpdate(fos.getCurrentEnvironment(), (store) => { - for (const id of Array.from(current.keys())) { + for (const id of keys.current) { store.get(id)?.invalidateRecord(); } - current.clear(); }); + keys.current.clear(); }; const unsubscribe = foq.subscribe( diff --git a/app/packages/core/src/components/Modal/Sample.tsx b/app/packages/core/src/components/Modal/Sample.tsx index 7811e97389..9b8cd75ed5 100644 --- a/app/packages/core/src/components/Modal/Sample.tsx +++ b/app/packages/core/src/components/Modal/Sample.tsx @@ -5,6 +5,7 @@ import { modalSample, modalSampleId, useHoveredSample, + useKeyDown, } from "@fiftyone/state"; import React, { MutableRefObject, useCallback, useRef, useState } from "react"; import { RecoilValueReadOnly, useRecoilValue } from "recoil"; @@ -24,6 +25,12 @@ export const SampleWrapper = ({ const [hovering, setHovering] = useState(false); const timeout: MutableRefObject = useRef(null); + + useKeyDown("c", () => { + !(document.activeElement instanceof HTMLInputElement) && + setHovering((cur) => !cur); + }); + const clear = useCallback(() => { if (hoveringRef.current) return; timeout.current && clearTimeout(timeout.current); diff --git a/app/packages/core/src/components/Sidebar/Entries/GroupEntries.tsx b/app/packages/core/src/components/Sidebar/Entries/GroupEntries.tsx index 4d246ee5d6..52cd1d2118 100644 --- a/app/packages/core/src/components/Sidebar/Entries/GroupEntries.tsx +++ b/app/packages/core/src/components/Sidebar/Entries/GroupEntries.tsx @@ -32,10 +32,11 @@ import styled from "styled-components"; import Draggable from "./Draggable"; type PillEntry = { + dataCy?: string; + icon?: React.ReactNode; onClick: () => void; text: string; title: string; - icon?: React.ReactNode; }; const Pills = ({ entries }: { entries: PillEntry[] }) => { @@ -43,10 +44,12 @@ const Pills = ({ entries }: { entries: PillEntry[] }) => { return ( <> - {entries.map((data, i) => ( + {entries.map(({ dataCy, ...data }, i) => ( { padding: "0.25rem 0.5rem", margin: "0 0.25rem", }} - key={i} /> ))} @@ -306,24 +308,28 @@ export const PathGroupEntry = React.memo( count: useRecoilValue( numGroupFieldsFiltered({ modal, group: name }) ), - onClick: useClearFiltered(modal, name), + dataCy: `clear-filters-${name}`, icon: , + onClick: useClearFiltered(modal, name), + title: `Clear ${name} filters`, }, { count: useRecoilValue( numGroupFieldsVisible({ modal, group: name }) ), - onClick: useClearVisibility(modal, name), + dataCy: `clear-visibility-${name}`, icon: , - title: `Clear ${name} filters`, + onClick: useClearVisibility(modal, name), + title: `Clear ${name} visibility`, }, { count: useRecoilValue( numGroupFieldsActive({ modal, group: name }) ), - onClick: useClearActive(modal, name), + dataCy: `clear-shown-${name}`, icon: , + onClick: useClearActive(modal, name), title: `Clear shown ${name}`, }, ] diff --git a/app/packages/core/src/plugins/SchemaIO/components/TabsView.tsx b/app/packages/core/src/plugins/SchemaIO/components/TabsView.tsx index 45f572a30d..1fe5930b67 100644 --- a/app/packages/core/src/plugins/SchemaIO/components/TabsView.tsx +++ b/app/packages/core/src/plugins/SchemaIO/components/TabsView.tsx @@ -4,12 +4,14 @@ import HeaderView from "./HeaderView"; import HelpTooltip from "./HelpTooltip"; import RoundedTabs from "./RoundedTabs"; import { getComponentProps } from "../utils"; +import { useKey } from "../hooks"; export default function TabsView(props) { const { onChange, path, schema, data } = props; const { view = {}, default: defaultValue } = schema; const { choices = [], variant = "default" } = view; const [tab, setTab] = useState(data ?? (defaultValue || choices[0]?.value)); + const [_, setUserChanged] = useKey(path, schema, data, true); useEffect(() => { if (typeof onChange === "function") onChange(path, tab); @@ -30,7 +32,10 @@ export default function TabsView(props) { value={tab} variant="scrollable" scrollButtons="auto" - onChange={(e, value) => setTab(value)} + onChange={(e, value) => { + setTab(value); + setUserChanged(); + }} sx={{ borderBottom: 1, borderColor: "divider" }} {...getComponentProps(props, "tabs")} > diff --git a/app/packages/relay/src/fragments/__generated__/sidebarGroupsFragment.graphql.ts b/app/packages/relay/src/fragments/__generated__/sidebarGroupsFragment.graphql.ts index 5329712204..fa8b9a6b5e 100644 --- a/app/packages/relay/src/fragments/__generated__/sidebarGroupsFragment.graphql.ts +++ b/app/packages/relay/src/fragments/__generated__/sidebarGroupsFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<224f120cbee602f090fa7e189aa8a02f>> + * @generated SignedSource<<5824920101a6f1b350d67c862b128228>> * @lightSyntaxTransform * @nogrep */ @@ -18,7 +18,7 @@ export type sidebarGroupsFragment$data = { readonly paths: ReadonlyArray | null; }> | null; } | null; - readonly name: string; + readonly datasetId: string; readonly " $fragmentSpreads": FragmentRefs<"frameFieldsFragment" | "sampleFieldsFragment">; readonly " $fragmentType": "sidebarGroupsFragment"; }; @@ -27,21 +27,19 @@ export type sidebarGroupsFragment$key = { readonly " $fragmentSpreads": FragmentRefs<"sidebarGroupsFragment">; }; -const node: ReaderFragment = (function(){ -var v0 = { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "name", - "storageKey": null -}; -return { +const node: ReaderFragment = { "argumentDefinitions": [], "kind": "Fragment", "metadata": null, "name": "sidebarGroupsFragment", "selections": [ - (v0/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "datasetId", + "storageKey": null + }, { "alias": null, "args": null, @@ -72,7 +70,13 @@ return { "name": "paths", "storageKey": null }, - (v0/*: any*/) + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } ], "storageKey": null } @@ -93,8 +97,7 @@ return { "type": "Dataset", "abstractKey": null }; -})(); -(node as any).hash = "3613f2a33c910c4e85495621b1c6ae34"; +(node as any).hash = "9d94d4a73e0018e461741256d90cf037"; export default node; diff --git a/app/packages/relay/src/fragments/sidebarGroupsFragment.ts b/app/packages/relay/src/fragments/sidebarGroupsFragment.ts index a70ef29e44..563416ecc0 100644 --- a/app/packages/relay/src/fragments/sidebarGroupsFragment.ts +++ b/app/packages/relay/src/fragments/sidebarGroupsFragment.ts @@ -2,7 +2,7 @@ import { graphql } from "relay-runtime"; export default graphql` fragment sidebarGroupsFragment on Dataset { - name + datasetId appConfig { sidebarGroups { expanded diff --git a/app/packages/relay/src/graphQLSyncFragmentAtomFamily.ts b/app/packages/relay/src/graphQLSyncFragmentAtomFamily.ts index b46469ab60..8a6b113980 100644 --- a/app/packages/relay/src/graphQLSyncFragmentAtomFamily.ts +++ b/app/packages/relay/src/graphQLSyncFragmentAtomFamily.ts @@ -56,8 +56,8 @@ export function graphQLSyncFragmentAtomFamily< !fragmentOptions.sync || fragmentOptions.sync(params) ? [ ({ setSelf, trigger }: Parameters>[0]) => { - // recoil state should be initialized via RecoilRoot's initializeState - // during tests + // recoil state should be initialized via RecoilRoot's + // initializeState during tests if (isTest) return; if (trigger === "set") { @@ -94,7 +94,7 @@ export function graphQLSyncFragmentAtomFamily< try { for (let i = 0; i < fragmentOptions.fragments.length; i++) { const fragment = fragmentOptions.fragments[i]; - if (fragmentOptions.keys && fragmentOptions.keys[i]) { + if (fragmentOptions?.keys[i]) { // @ts-ignore data = data[fragmentOptions.keys[i]]; } diff --git a/app/packages/relay/src/queries/__generated__/datasetQuery.graphql.ts b/app/packages/relay/src/queries/__generated__/datasetQuery.graphql.ts index 9e2b89656c..fd3986736d 100644 --- a/app/packages/relay/src/queries/__generated__/datasetQuery.graphql.ts +++ b/app/packages/relay/src/queries/__generated__/datasetQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<96262fb79eee2ce6339035f8451bb291>> + * @generated SignedSource<<583e3c7d99629e90af513ae5486e15fc>> * @lightSyntaxTransform * @nogrep */ @@ -1332,12 +1332,12 @@ return { ] }, "params": { - "cacheID": "be41b4ffcbf98b9feb70969f1f4c70d4", + "cacheID": "c9afb8d4108cafe275b467c420b595a2", "id": null, "metadata": {}, "name": "datasetQuery", "operationKind": "query", - "text": "query datasetQuery(\n $extendedView: BSONArray!\n $name: String!\n $savedViewSlug: String\n $view: BSONArray!\n $workspaceSlug: String\n) {\n config {\n colorBy\n colorPool\n colorscale\n multicolorKeypoints\n showSkeletons\n }\n dataset(name: $name, view: $extendedView, savedViewSlug: $savedViewSlug) {\n name\n defaultGroupSlice\n viewName\n savedViewSlug\n appConfig {\n colorScheme {\n id\n colorBy\n colorPool\n multicolorKeypoints\n opacity\n showSkeletons\n defaultMaskTargetsColors {\n intTarget\n color\n }\n defaultColorscale {\n name\n list {\n value\n color\n }\n rgb\n }\n colorscales {\n path\n name\n list {\n value\n color\n }\n rgb\n }\n labelTags {\n fieldColor\n valueColors {\n value\n color\n }\n }\n fields {\n colorByAttribute\n fieldColor\n path\n maskTargetsColors {\n intTarget\n color\n }\n valueColors {\n color\n value\n }\n }\n }\n }\n workspace(slug: $workspaceSlug) {\n id\n child\n slug\n }\n ...datasetFragment\n id\n }\n ...savedViewsFragment\n ...configFragment\n ...stageDefinitionsFragment\n ...viewSchemaFragment\n}\n\nfragment colorSchemeFragment on ColorScheme {\n id\n colorBy\n colorPool\n multicolorKeypoints\n opacity\n showSkeletons\n labelTags {\n fieldColor\n valueColors {\n color\n value\n }\n }\n defaultMaskTargetsColors {\n intTarget\n color\n }\n defaultColorscale {\n name\n list {\n value\n color\n }\n rgb\n }\n colorscales {\n path\n name\n list {\n value\n color\n }\n rgb\n }\n fields {\n colorByAttribute\n fieldColor\n path\n valueColors {\n color\n value\n }\n maskTargetsColors {\n intTarget\n color\n }\n }\n}\n\nfragment configFragment on Query {\n config {\n colorBy\n colorPool\n colorscale\n disableFrameFiltering\n gridZoom\n lightningThreshold\n loopVideos\n mediaFallback\n multicolorKeypoints\n notebookHeight\n plugins\n showConfidence\n showIndex\n showLabel\n showSkeletons\n showTooltip\n sidebarMode\n theme\n timezone\n useFrameNumber\n }\n colorscale\n}\n\nfragment datasetAppConfigFragment on DatasetAppConfig {\n colorScheme {\n ...colorSchemeFragment\n id\n }\n disableFrameFiltering\n gridMediaField\n mediaFields\n modalMediaField\n mediaFallback\n plugins\n sidebarMode\n}\n\nfragment datasetFragment on Dataset {\n createdAt\n datasetId\n groupField\n id\n info\n lastLoadedAt\n mediaType\n name\n parentMediaType\n version\n appConfig {\n ...datasetAppConfigFragment\n }\n brainMethods {\n key\n version\n timestamp\n viewStages\n config {\n cls\n embeddingsField\n method\n patchesField\n supportsPrompts\n type\n maxK\n supportsLeastSimilarity\n }\n }\n defaultMaskTargets {\n target\n value\n }\n defaultSkeleton {\n labels\n edges\n }\n evaluations {\n key\n version\n timestamp\n viewStages\n config {\n cls\n predField\n gtField\n }\n }\n groupMediaTypes {\n name\n mediaType\n }\n maskTargets {\n name\n targets {\n target\n value\n }\n }\n skeletons {\n name\n labels\n edges\n }\n ...estimatedCountsFragment\n ...frameFieldsFragment\n ...groupSliceFragment\n ...indexesFragment\n ...mediaFieldsFragment\n ...mediaTypeFragment\n ...sampleFieldsFragment\n ...sidebarGroupsFragment\n ...viewFragment\n}\n\nfragment estimatedCountsFragment on Dataset {\n estimatedFrameCount\n estimatedSampleCount\n}\n\nfragment frameFieldsFragment on Dataset {\n frameFields {\n ftype\n subfield\n embeddedDocType\n path\n dbField\n description\n info\n }\n}\n\nfragment groupSliceFragment on Dataset {\n defaultGroupSlice\n}\n\nfragment indexesFragment on Dataset {\n frameIndexes {\n name\n unique\n key {\n field\n type\n }\n wildcardProjection {\n fields\n inclusion\n }\n }\n sampleIndexes {\n name\n unique\n key {\n field\n type\n }\n wildcardProjection {\n fields\n inclusion\n }\n }\n}\n\nfragment mediaFieldsFragment on Dataset {\n name\n appConfig {\n gridMediaField\n mediaFields\n modalMediaField\n mediaFallback\n }\n sampleFields {\n path\n }\n}\n\nfragment mediaTypeFragment on Dataset {\n mediaType\n}\n\nfragment sampleFieldsFragment on Dataset {\n sampleFields {\n ftype\n subfield\n embeddedDocType\n path\n dbField\n description\n info\n }\n}\n\nfragment savedViewsFragment on Query {\n savedViews(datasetName: $name) {\n id\n datasetId\n name\n slug\n description\n color\n viewStages\n createdAt\n lastModifiedAt\n lastLoadedAt\n }\n}\n\nfragment sidebarGroupsFragment on Dataset {\n name\n appConfig {\n sidebarGroups {\n expanded\n paths\n name\n }\n }\n ...frameFieldsFragment\n ...sampleFieldsFragment\n}\n\nfragment stageDefinitionsFragment on Query {\n stageDefinitions {\n name\n params {\n name\n type\n default\n placeholder\n }\n }\n}\n\nfragment viewFragment on Dataset {\n stages(slug: $savedViewSlug, view: $view)\n viewCls\n viewName\n}\n\nfragment viewSchemaFragment on Query {\n schemaForViewStages(datasetName: $name, viewStages: $view) {\n fieldSchema {\n path\n ftype\n subfield\n embeddedDocType\n info\n description\n }\n frameFieldSchema {\n path\n ftype\n subfield\n embeddedDocType\n info\n description\n }\n }\n}\n" + "text": "query datasetQuery(\n $extendedView: BSONArray!\n $name: String!\n $savedViewSlug: String\n $view: BSONArray!\n $workspaceSlug: String\n) {\n config {\n colorBy\n colorPool\n colorscale\n multicolorKeypoints\n showSkeletons\n }\n dataset(name: $name, view: $extendedView, savedViewSlug: $savedViewSlug) {\n name\n defaultGroupSlice\n viewName\n savedViewSlug\n appConfig {\n colorScheme {\n id\n colorBy\n colorPool\n multicolorKeypoints\n opacity\n showSkeletons\n defaultMaskTargetsColors {\n intTarget\n color\n }\n defaultColorscale {\n name\n list {\n value\n color\n }\n rgb\n }\n colorscales {\n path\n name\n list {\n value\n color\n }\n rgb\n }\n labelTags {\n fieldColor\n valueColors {\n value\n color\n }\n }\n fields {\n colorByAttribute\n fieldColor\n path\n maskTargetsColors {\n intTarget\n color\n }\n valueColors {\n color\n value\n }\n }\n }\n }\n workspace(slug: $workspaceSlug) {\n id\n child\n slug\n }\n ...datasetFragment\n id\n }\n ...savedViewsFragment\n ...configFragment\n ...stageDefinitionsFragment\n ...viewSchemaFragment\n}\n\nfragment colorSchemeFragment on ColorScheme {\n id\n colorBy\n colorPool\n multicolorKeypoints\n opacity\n showSkeletons\n labelTags {\n fieldColor\n valueColors {\n color\n value\n }\n }\n defaultMaskTargetsColors {\n intTarget\n color\n }\n defaultColorscale {\n name\n list {\n value\n color\n }\n rgb\n }\n colorscales {\n path\n name\n list {\n value\n color\n }\n rgb\n }\n fields {\n colorByAttribute\n fieldColor\n path\n valueColors {\n color\n value\n }\n maskTargetsColors {\n intTarget\n color\n }\n }\n}\n\nfragment configFragment on Query {\n config {\n colorBy\n colorPool\n colorscale\n disableFrameFiltering\n gridZoom\n lightningThreshold\n loopVideos\n mediaFallback\n multicolorKeypoints\n notebookHeight\n plugins\n showConfidence\n showIndex\n showLabel\n showSkeletons\n showTooltip\n sidebarMode\n theme\n timezone\n useFrameNumber\n }\n colorscale\n}\n\nfragment datasetAppConfigFragment on DatasetAppConfig {\n colorScheme {\n ...colorSchemeFragment\n id\n }\n disableFrameFiltering\n gridMediaField\n mediaFields\n modalMediaField\n mediaFallback\n plugins\n sidebarMode\n}\n\nfragment datasetFragment on Dataset {\n createdAt\n datasetId\n groupField\n id\n info\n lastLoadedAt\n mediaType\n name\n parentMediaType\n version\n appConfig {\n ...datasetAppConfigFragment\n }\n brainMethods {\n key\n version\n timestamp\n viewStages\n config {\n cls\n embeddingsField\n method\n patchesField\n supportsPrompts\n type\n maxK\n supportsLeastSimilarity\n }\n }\n defaultMaskTargets {\n target\n value\n }\n defaultSkeleton {\n labels\n edges\n }\n evaluations {\n key\n version\n timestamp\n viewStages\n config {\n cls\n predField\n gtField\n }\n }\n groupMediaTypes {\n name\n mediaType\n }\n maskTargets {\n name\n targets {\n target\n value\n }\n }\n skeletons {\n name\n labels\n edges\n }\n ...estimatedCountsFragment\n ...frameFieldsFragment\n ...groupSliceFragment\n ...indexesFragment\n ...mediaFieldsFragment\n ...mediaTypeFragment\n ...sampleFieldsFragment\n ...sidebarGroupsFragment\n ...viewFragment\n}\n\nfragment estimatedCountsFragment on Dataset {\n estimatedFrameCount\n estimatedSampleCount\n}\n\nfragment frameFieldsFragment on Dataset {\n frameFields {\n ftype\n subfield\n embeddedDocType\n path\n dbField\n description\n info\n }\n}\n\nfragment groupSliceFragment on Dataset {\n defaultGroupSlice\n}\n\nfragment indexesFragment on Dataset {\n frameIndexes {\n name\n unique\n key {\n field\n type\n }\n wildcardProjection {\n fields\n inclusion\n }\n }\n sampleIndexes {\n name\n unique\n key {\n field\n type\n }\n wildcardProjection {\n fields\n inclusion\n }\n }\n}\n\nfragment mediaFieldsFragment on Dataset {\n name\n appConfig {\n gridMediaField\n mediaFields\n modalMediaField\n mediaFallback\n }\n sampleFields {\n path\n }\n}\n\nfragment mediaTypeFragment on Dataset {\n mediaType\n}\n\nfragment sampleFieldsFragment on Dataset {\n sampleFields {\n ftype\n subfield\n embeddedDocType\n path\n dbField\n description\n info\n }\n}\n\nfragment savedViewsFragment on Query {\n savedViews(datasetName: $name) {\n id\n datasetId\n name\n slug\n description\n color\n viewStages\n createdAt\n lastModifiedAt\n lastLoadedAt\n }\n}\n\nfragment sidebarGroupsFragment on Dataset {\n datasetId\n appConfig {\n sidebarGroups {\n expanded\n paths\n name\n }\n }\n ...frameFieldsFragment\n ...sampleFieldsFragment\n}\n\nfragment stageDefinitionsFragment on Query {\n stageDefinitions {\n name\n params {\n name\n type\n default\n placeholder\n }\n }\n}\n\nfragment viewFragment on Dataset {\n stages(slug: $savedViewSlug, view: $view)\n viewCls\n viewName\n}\n\nfragment viewSchemaFragment on Query {\n schemaForViewStages(datasetName: $name, viewStages: $view) {\n fieldSchema {\n path\n ftype\n subfield\n embeddedDocType\n info\n description\n }\n frameFieldSchema {\n path\n ftype\n subfield\n embeddedDocType\n info\n description\n }\n }\n}\n" } }; })(); diff --git a/app/packages/state/src/hooks/useSetModalState.ts b/app/packages/state/src/hooks/useSetModalState.ts index e59f1bb26e..efa5ef05f3 100644 --- a/app/packages/state/src/hooks/useSetModalState.ts +++ b/app/packages/state/src/hooks/useSetModalState.ts @@ -10,6 +10,7 @@ import * as modalAtoms from "../recoil/modal"; import * as schemaAtoms from "../recoil/schema"; import * as selectors from "../recoil/selectors"; import * as sidebarAtoms from "../recoil/sidebar"; +import * as sidebarExpandedAtoms from "../recoil/sidebarExpanded"; const setModalFilters = async ({ snapshot, set }: CallbackInterface) => { const paths = await snapshot.getPromise( @@ -43,12 +44,17 @@ export default () => { selectors.appConfigOption({ key, modal: false }), ]; }), + + [dynamicGroupsViewMode(true), dynamicGroupsViewMode(false)], + + [atoms.cropToContent(true), atoms.cropToContent(false)], + [atoms.sortFilterResults(true), atoms.sortFilterResults(false)], + [groupAtoms.groupStatistics(true), groupAtoms.groupStatistics(false)], + [groupAtoms.modalGroupSlice, groupAtoms.groupSlice], [ schemaAtoms.activeFields({ modal: true }), schemaAtoms.activeFields({ modal: false }), ], - [atoms.cropToContent(true), atoms.cropToContent(false)], - [atoms.sortFilterResults(true), atoms.sortFilterResults(false)], [ sidebarAtoms.sidebarGroupsDefinition(true), sidebarAtoms.sidebarGroupsDefinition(false), @@ -56,10 +62,10 @@ export default () => { [sidebarAtoms.sidebarWidth(true), sidebarAtoms.sidebarWidth(false)], [sidebarAtoms.sidebarVisible(true), sidebarAtoms.sidebarVisible(false)], [sidebarAtoms.textFilter(true), sidebarAtoms.textFilter(false)], - - [groupAtoms.groupStatistics(true), groupAtoms.groupStatistics(false)], - [groupAtoms.modalGroupSlice, groupAtoms.groupSlice], - [dynamicGroupsViewMode(true), dynamicGroupsViewMode(false)], + [ + sidebarExpandedAtoms.sidebarExpandedStore(true), + sidebarExpandedAtoms.sidebarExpandedStore(false), + ], ]; const slice = await snapshot.getPromise(groupAtoms.groupSlice); diff --git a/app/packages/state/src/recoil/pathFilters/index.test.ts b/app/packages/state/src/recoil/pathFilters/index.test.ts new file mode 100644 index 0000000000..382a381a70 --- /dev/null +++ b/app/packages/state/src/recoil/pathFilters/index.test.ts @@ -0,0 +1,26 @@ +import { + DETECTION_FIELD, + DETECTIONS_FIELD, + KEYPOINT_FIELD, + KEYPOINTS_FIELD, +} from "@fiftyone/utilities"; +import { describe, expect, it } from "vitest"; +import { keypointFilter } from "."; + +describe("path filter handling", () => { + it("overrides keypoint array filters", () => { + const keypoint = keypointFilter("test", KEYPOINT_FIELD, () => false); + expect(keypoint({ test: [] })).toBe(true); + + const keypoints = keypointFilter("test", KEYPOINTS_FIELD, () => false); + expect(keypoints({ test: [] })).toBe(true); + }); + + it("does not override other label fields", () => { + const keypoint = keypointFilter("test", DETECTION_FIELD, () => false); + expect(keypoint({ test: [] })).toBe(false); + + const keypoints = keypointFilter("test", DETECTIONS_FIELD, () => false); + expect(keypoints({ test: [] })).toBe(false); + }); +}); diff --git a/app/packages/state/src/recoil/pathFilters/index.ts b/app/packages/state/src/recoil/pathFilters/index.ts index 8609bac0f3..490218066f 100644 --- a/app/packages/state/src/recoil/pathFilters/index.ts +++ b/app/packages/state/src/recoil/pathFilters/index.ts @@ -6,6 +6,8 @@ import { FRAME_NUMBER_FIELD, FRAME_SUPPORT_FIELD, INT_FIELD, + KEYPOINT_FIELD, + KEYPOINTS_FIELD, LABELS, LIST_FIELD, OBJECT_ID_FIELD, @@ -30,6 +32,8 @@ export * from "./numeric"; export * from "./string"; export * from "./utils"; +const KEYPOINT_TYPES = new Set([KEYPOINT_FIELD, KEYPOINTS_FIELD]); + const primitiveFilter = selectorFamily< (value: any) => boolean, { modal: boolean; path: string } @@ -104,7 +108,6 @@ export const pathFilter = selectorFamily({ if (path.startsWith("_")) return f; const field = get(schemaAtoms.field(path)); - const isKeypoints = path.includes("keypoints"); if (field && LABELS.includes(field.embeddedDocType)) { const expandedPath = get(schemaAtoms.expandPath(path)); @@ -114,23 +117,19 @@ export const pathFilter = selectorFamily({ ftype: VALID_PRIMITIVE_TYPES, }) ); + const docType = get(schemaAtoms.field(expandedPath)).embeddedDocType; const fs = labelFields.map(({ name, dbField }) => { const filter = get( primitiveFilter({ modal, path: `${expandedPath}.${name}` }) ); - return (value: unknown) => { - if (isKeypoints && typeof value[name] === "object") { - // keypoints ListFields - return () => true; - } - + return keypointFilter(name, docType, (value: unknown) => { const correctedValue = value[0] ? value[0] : value; return filter( correctedValue[name === "id" ? "id" : dbField || name] ); - }; + }); }); f[path] = (value: unknown) => { @@ -170,6 +169,24 @@ export const pathFilter = selectorFamily({ }, }); +export const keypointFilter = ( + name: string, + embeddedDocType: string, + filter: (value: unknown) => boolean +) => { + const isKeypoints = KEYPOINT_TYPES.has(embeddedDocType); + + if (!isKeypoints) { + return filter; + } + + return (value: unknown) => { + if (Array.isArray(value[name])) return true; + + return filter(value); + }; +}; + const matchesLabelTags = ( value: { tags: string[]; diff --git a/app/packages/state/src/recoil/sidebar.ts b/app/packages/state/src/recoil/sidebar.ts index 9c90789e6a..2981e6ba02 100644 --- a/app/packages/state/src/recoil/sidebar.ts +++ b/app/packages/state/src/recoil/sidebar.ts @@ -214,7 +214,7 @@ export const resolveGroups = ( }, {}); groups = groups.map((group) => { - return expanded[group.name] !== undefined + return typeof expanded[group.name] === "boolean" ? { ...group, expanded: expanded[group.name] } : { ...group }; }); @@ -316,51 +316,54 @@ const groupUpdater = ( }; }; -export const [resolveSidebarGroups, sidebarGroupsDefinition] = (() => { +export const sidebarGroupsDefinition = (() => { let configGroups: State.SidebarGroup[] = []; let current: State.SidebarGroup[] = []; - return [ - (sampleFields: StrictField[], frameFields: StrictField[]) => { - return resolveGroups(sampleFields, frameFields, current, configGroups); - }, - graphQLSyncFragmentAtomFamily< - sidebarGroupsFragment$key, - State.SidebarGroup[], - boolean - >( - { - fragments: [datasetFragment, sidebarGroupsFragment], - keys: ["dataset"], - sync: (modal) => !modal, - read: (data, prev) => { - configGroups = (data.appConfig?.sidebarGroups || []).map((group) => ({ - ...group, - paths: [...group.paths], - })); - current = resolveGroups( - collapseFields( - readFragment( - sampleFieldsFragment, - data as sampleFieldsFragment$key - ).sampleFields - ), - collapseFields( - readFragment(frameFieldsFragment, data as frameFieldsFragment$key) - .frameFields - ), - data.name === prev?.name ? current : [], - configGroups - ); + return graphQLSyncFragmentAtomFamily< + sidebarGroupsFragment$key, + State.SidebarGroup[], + boolean + >( + { + fragments: [datasetFragment, sidebarGroupsFragment], + keys: ["dataset"], + sync: (modal) => !modal, + read: (data, prev) => { + configGroups = (data.appConfig?.sidebarGroups || []).map((group) => ({ + ...group, + paths: [...group.paths], + })); + current = resolveGroups( + collapseFields( + readFragment(sampleFieldsFragment, data as sampleFieldsFragment$key) + .sampleFields + ), + collapseFields( + readFragment(frameFieldsFragment, data as frameFieldsFragment$key) + .frameFields + ), + data?.datasetId === prev?.datasetId ? current : [], + configGroups + ); - return current; - }, - default: [], + return current; }, - { - key: "sidebarGroupsDefinition", - } - ), - ]; + default: [], + }, + { + effects: (modal) => + modal + ? [] + : [ + ({ onSet }) => { + onSet((next) => { + current = next; + }); + }, + ], + key: "sidebarGroupsDefinition", + } + ); })(); export const sidebarGroups = selectorFamily< diff --git a/app/packages/state/src/recoil/sidebarExpanded.ts b/app/packages/state/src/recoil/sidebarExpanded.ts index 8d78cead6c..2c83c5f8cd 100644 --- a/app/packages/state/src/recoil/sidebarExpanded.ts +++ b/app/packages/state/src/recoil/sidebarExpanded.ts @@ -5,7 +5,7 @@ export const sidebarExpandedStore = atomFamily< { [key: string]: boolean }, boolean >({ - key: "sidebarExpanded", + key: "sidebarExpandedStore", default: {}, effects: [ ({ node }) => @@ -19,7 +19,7 @@ export const sidebarExpanded = selectorFamily< boolean, { path: string; modal: boolean } >({ - key: "granularSidebarExpanded", + key: "sidebarExpanded", get: (params) => ({ get }) => diff --git a/docs/source/plugins/developing_plugins.rst b/docs/source/plugins/developing_plugins.rst index d3f58432c3..a8bffe2ad2 100644 --- a/docs/source/plugins/developing_plugins.rst +++ b/docs/source/plugins/developing_plugins.rst @@ -189,9 +189,9 @@ fiftyone.yml ------------ All plugins must contain a `fiftyone.yml` or `fiftyone.yaml` file, which is -used to define the plugin's metadata, declare any operators that it exposes, -and declare any :ref:`secrets ` that it may require. The -following fields are available: +used to define the plugin's metadata, declare any operators and panels that it +exposes, and declare any :ref:`secrets ` that it may require. +The following fields are available: - `name` **(required)**: the name of the plugin - `author`: the author of the plugin diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index d175da6291..14ffa9a0be 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -3,6 +3,72 @@ FiftyOne Release Notes .. default-role:: code +FiftyOne Teams 2.0.1 +-------------------- +*Released September 6, 2024* + +Includes all updates from :ref:`FiftyOne 0.25.1 `, plus: + +- Optimized the `Manage > Access` page for datasets +- Added support for configuring a deployment to allow Guests to run custom + plugins +- Fixed a bug where dataset permissions assigned to + :ref:`groups ` were not correctly applied to users that do not + otherwise have access to the dataset +- Fixed a bug where a deployment's default user role as configured on the + `Security > Config` page would not be respected +- Fixed a bug that could cause 3D scenes stored in Azure to fail to load +- Fixed a bug that erroneously caused the currently selected samples to be + cleared when navigating between samples or closing the sample modal + +.. _release-notes-v0.25.1: + +FiftyOne 0.25.1 +--------------- +*Released September 6, 2024* + +App + +- Fixed an issue with sidebar state persistence when opening and closing the + sample modal + `#4745 `_ +- Fixed a bug with sample selection in the :ref:`Map panel ` + when the grid is reset + `#4739 `_ +- Fixed a bug when filtering |Keypoint| fields using the App sidebar + `#4735 `_ +- Fixed a bug when tagging in the sample modal with active sidebar filters + `#4723 `_ +- Disabled ``fiftyone-desktop`` builds until package size can be optimized + `#4746 `_ + +SDK + +- Added support for loading lists of TXT files in + :ref:`YOLOv5 format ` + `#4742 `_ +- Fixed a bug with the ``match_expr`` argument of + :meth:`group_by() ` + `#4754 `_ +- Fixed a regression when running inference with + :ref:`Ultralytics models ` that don't support track + IDs + `#4720 `_ + +Plugins + +- Fixed a bug that caused :class:`TabsView ` + components to erroneously reset to their default state + `#4732 `_ +- Fixed a bug where calling + :meth:`set_state() ` and + :meth:`set_data() ` to patch + state/data would inadvertently clobber other existing values + `#4753 `_ +- Fixed a spurious warning that would appear for delegated operations that + don't return outputs + `#4715 `_ + FiftyOne Teams 2.0.0 -------------------- *Released August 20, 2024* diff --git a/e2e-pw/src/oss/poms/modal/modal-sidebar.ts b/e2e-pw/src/oss/poms/modal/modal-sidebar.ts index c007165929..665c0eb1ee 100644 --- a/e2e-pw/src/oss/poms/modal/modal-sidebar.ts +++ b/e2e-pw/src/oss/poms/modal/modal-sidebar.ts @@ -13,6 +13,29 @@ export class ModalSidebarPom { this.locator = page.getByTestId("modal").getByTestId("sidebar"); } + async applyFilter(label: string) { + const selectionDiv = this.locator + .getByTestId("checkbox-" + label) + .getByTitle(label); + await selectionDiv.click({ force: true }); + } + + async applySearch(field: string, search: string) { + const input = this.locator.getByTestId(`selector-sidebar-search-${field}`); + await input.fill(search); + await input.press("Enter"); + } + + async clearGroupFilters(name: string) { + return this.locator.getByTestId(`clear-filters-${name}`).click(); + } + + async clickFieldDropdown(field: string) { + return this.locator + .getByTestId(`sidebar-field-arrow-enabled-${field}`) + .click(); + } + getSidebarEntry(key: string) { return this.locator.getByTestId(`sidebar-entry-${key}`); } @@ -48,6 +71,10 @@ export class ModalSidebarPom { return absPath; } + async toggleLabelCheckbox(field: string) { + await this.locator.getByTestId(`checkbox-${field}`).click(); + } + async toggleSidebarGroup(name: string) { await this.locator.getByTestId(`sidebar-group-entry-${name}`).click(); } diff --git a/e2e-pw/src/oss/specs/smoke-tests/quickstart.spec.ts b/e2e-pw/src/oss/specs/smoke-tests/quickstart.spec.ts index 8a2c6cab00..dd698518d0 100644 --- a/e2e-pw/src/oss/specs/smoke-tests/quickstart.spec.ts +++ b/e2e-pw/src/oss/specs/smoke-tests/quickstart.spec.ts @@ -85,4 +85,11 @@ test.describe("quickstart", () => { await gridRefresh; await expect(page.getByTestId("entry-counts")).toHaveText("1 sample"); }); + + test("sidebar persistence", async ({ grid, modal, sidebar }) => { + await sidebar.toggleSidebarGroup("PRIMITIVES"); + await grid.openFirstSample(); + await modal.close(); + await sidebar.asserter.assertSidebarGroupIsHidden("PRIMITIVES"); + }); }); diff --git a/e2e-pw/src/oss/specs/smoke-tests/tagger.spec.ts b/e2e-pw/src/oss/specs/smoke-tests/tagger.spec.ts index ee60ed422f..b3e901c33c 100644 --- a/e2e-pw/src/oss/specs/smoke-tests/tagger.spec.ts +++ b/e2e-pw/src/oss/specs/smoke-tests/tagger.spec.ts @@ -1,29 +1,29 @@ import { test as base, expect } from "src/oss/fixtures"; -import { GridActionsRowPom } from "src/oss/poms/action-row/grid-actions-row"; import { GridTaggerPom } from "src/oss/poms/action-row/tagger/grid-tagger"; import { GridPom } from "src/oss/poms/grid"; +import { ModalPom } from "src/oss/poms/modal"; import { SidebarPom } from "src/oss/poms/sidebar"; import { getUniqueDatasetNameWithPrefix } from "src/oss/utils"; const datasetName = getUniqueDatasetNameWithPrefix("smoke-quickstart"); const test = base.extend<{ - tagger: GridTaggerPom; - sidebar: SidebarPom; grid: GridPom; - gridActionsRow: GridActionsRowPom; + modal: ModalPom; + sidebar: SidebarPom; + tagger: GridTaggerPom; }>({ - tagger: async ({ page }, use) => { - await use(new GridTaggerPom(page)); + grid: async ({ page, eventUtils }, use) => { + await use(new GridPom(page, eventUtils)); + }, + modal: async ({ page, eventUtils }, use) => { + await use(new ModalPom(page, eventUtils)); }, sidebar: async ({ page }, use) => { await use(new SidebarPom(page)); }, - grid: async ({ page, eventUtils }, use) => { - await use(new GridPom(page, eventUtils)); - }, - gridActionsRow: async ({ page, eventUtils }, use) => { - await use(new GridActionsRowPom(page, eventUtils)); + tagger: async ({ page }, use) => { + await use(new GridTaggerPom(page)); }, }); @@ -39,10 +39,10 @@ test.describe("tag", () => { }); test("sample tag and label tag loads correct aggregation number on default view", async ({ - gridActionsRow, + grid, tagger, }) => { - await gridActionsRow.toggleTagSamplesOrLabels(); + await grid.actionsRow.toggleTagSamplesOrLabels(); await tagger.setActiveTaggerMode("sample"); const placeHolder = await tagger.getTagInputTextPlaceholder("sample"); expect(placeHolder.includes(" 5 ")).toBe(true); @@ -51,12 +51,11 @@ test.describe("tag", () => { const placeHolder2 = await tagger.getTagInputTextPlaceholder("label"); expect(placeHolder2.includes(" 143 ")).toBe(true); - await gridActionsRow.toggleTagSamplesOrLabels(); + await grid.actionsRow.toggleTagSamplesOrLabels(); }); - test("In grid, I can add a new sample tag to all new samples", async ({ + test("In grid, I can add a new sample tag to all samples", async ({ grid, - gridActionsRow, page, sidebar, tagger, @@ -66,7 +65,7 @@ test.describe("tag", () => { // mount eventListener const gridRefreshedEventPromise = grid.getWaitForGridRefreshPromise(); - await gridActionsRow.toggleTagSamplesOrLabels(); + await grid.actionsRow.toggleTagSamplesOrLabels(); await tagger.setActiveTaggerMode("sample"); await tagger.addNewTag("sample", "test1"); @@ -76,9 +75,8 @@ test.describe("tag", () => { await expect(bubble).toHaveCount(5); }); - test("In grid, I can add a new label tag to all new samples", async ({ + test("In grid, I can add a new label tag to all samples", async ({ grid, - gridActionsRow, page, sidebar, tagger, @@ -88,7 +86,7 @@ test.describe("tag", () => { // mount eventListener const gridRefreshedEventPromise = grid.getWaitForGridRefreshPromise(); - await gridActionsRow.toggleTagSamplesOrLabels(); + await grid.actionsRow.toggleTagSamplesOrLabels(); await tagger.setActiveTaggerMode("label"); await tagger.addNewTag("label", "labelTest"); @@ -100,4 +98,33 @@ test.describe("tag", () => { await expect(bubble1).toBeVisible(); await expect(bubble2).toBeVisible(); }); + + test("In modal, I can add a label tag to a filtered sample", async ({ + eventUtils, + grid, + modal, + page, + }) => { + await grid.openFirstSample(); + + await modal.sidebar.toggleLabelCheckbox("ground_truth"); + await expect(modal.looker).toHaveScreenshot("labels.png"); + + const entryExpandPromise = eventUtils.getEventReceivedPromiseForPredicate( + "animation-onRest", + () => true + ); + await modal.sidebar.clickFieldDropdown("predictions"); + await entryExpandPromise; + await modal.sidebar.applyFilter("bird"); + + await modal.looker.hover(); + + await modal.tagger.toggleOpen(); + await modal.tagger.addLabelTag("correct"); + + await modal.sidebar.clearGroupFilters("labels"); + await page.keyboard.press("c"); + await expect(modal.looker).toHaveScreenshot("labels.png"); + }); }); diff --git a/e2e-pw/src/oss/specs/smoke-tests/tagger.spec.ts-snapshots/labels-chromium-darwin.png b/e2e-pw/src/oss/specs/smoke-tests/tagger.spec.ts-snapshots/labels-chromium-darwin.png new file mode 100644 index 0000000000..3b0580e887 Binary files /dev/null and b/e2e-pw/src/oss/specs/smoke-tests/tagger.spec.ts-snapshots/labels-chromium-darwin.png differ diff --git a/e2e-pw/src/oss/specs/smoke-tests/tagger.spec.ts-snapshots/labels-chromium-linux.png b/e2e-pw/src/oss/specs/smoke-tests/tagger.spec.ts-snapshots/labels-chromium-linux.png new file mode 100644 index 0000000000..f065cde6a2 Binary files /dev/null and b/e2e-pw/src/oss/specs/smoke-tests/tagger.spec.ts-snapshots/labels-chromium-linux.png differ diff --git a/fiftyone/core/stages.py b/fiftyone/core/stages.py index 1bf6d080b6..da035331cf 100644 --- a/fiftyone/core/stages.py +++ b/fiftyone/core/stages.py @@ -3416,7 +3416,7 @@ def _make_flat_pipeline(self, sample_collection): ) if match_expr is not None: - pipeline.append({"$match": match_expr}) + pipeline.append({"$match": {"$expr": match_expr}}) if sort_expr is not None: order = -1 if self._reverse else 1 diff --git a/fiftyone/operators/delegated.py b/fiftyone/operators/delegated.py index 8e33a53af4..ff43dd597a 100644 --- a/fiftyone/operators/delegated.py +++ b/fiftyone/operators/delegated.py @@ -405,7 +405,8 @@ async def _execute_operator(self, doc): outputs = await resolve_type_with_context( request_params, "outputs" ) - outputs_schema = outputs.to_json() + if outputs is not None: + outputs_schema = outputs.to_json() except (AttributeError, Exception): logger.warning( "Failed to resolve output schema for the operation." diff --git a/fiftyone/operators/panel.py b/fiftyone/operators/panel.py index 6939976460..dabed73f52 100644 --- a/fiftyone/operators/panel.py +++ b/fiftyone/operators/panel.py @@ -212,13 +212,8 @@ def set(self, key, value=None): value (None): the value, if key is a string """ d = key if isinstance(key, dict) else {key: value} - - args = {} - for k, v in d.items(): - super().set(k, v) - pydash.set_(args, k, v) - - self._ctx.ops.patch_panel_state(args) + super().set(d) + self._ctx.ops.patch_panel_state(d) def clear(self): """Clears the panel state.""" @@ -251,13 +246,8 @@ def set(self, key, value=None): value (None): the value, if key is a string """ d = key if isinstance(key, dict) else {key: value} - - args = {} - for k, v in d.items(): - super().set(k, v) - pydash.set_(args, k, v) - - self._ctx.ops.patch_panel_data(args) + super().set(d) + self._ctx.ops.patch_panel_data(d) def get(self, key, default=None): raise WriteOnlyError("Panel data is write-only") diff --git a/fiftyone/server/routes/tag.py b/fiftyone/server/routes/tag.py index c299de218d..1b80a447b9 100644 --- a/fiftyone/server/routes/tag.py +++ b/fiftyone/server/routes/tag.py @@ -69,11 +69,7 @@ async def post(self, request: Request, data: dict): view = await fost.get_tag_view( dataset, stages=stages, - filters=filters, - extended_stages=extended, - labels=labels, - hidden_labels=hidden_labels, - sample_ids=sample_ids, + filters={}, sample_filter=SampleFilter( group=( GroupElementFilter(id=group_id, slices=slices) @@ -81,6 +77,7 @@ async def post(self, request: Request, data: dict): else None ) ), + sample_ids=sample_ids, target_labels=False, ) diff --git a/fiftyone/utils/ultralytics.py b/fiftyone/utils/ultralytics.py index 276dc7d038..c5473a669d 100644 --- a/fiftyone/utils/ultralytics.py +++ b/fiftyone/utils/ultralytics.py @@ -58,7 +58,7 @@ def _extract_track_ids(result): return ( result.boxes.id.detach().cpu().numpy().astype(int) if result.boxes.is_track - else [None] * len(result.boxes.conf.size(0)) + else [None] * result.boxes.conf.size(0) ) diff --git a/fiftyone/utils/yolo.py b/fiftyone/utils/yolo.py index 7f9840b144..8e5bc6f915 100644 --- a/fiftyone/utils/yolo.py +++ b/fiftyone/utils/yolo.py @@ -506,25 +506,25 @@ def setup(self): ) dataset_path = d.get("path", "") - data = fos.normpath(os.path.join(dataset_path, d[self.split])) + split_info = d[self.split] + if isinstance(split_info, str): + split_info = [split_info] + data_paths = [ + fos.normpath(os.path.join(dataset_path, si)) for si in split_info + ] classes = _parse_yolo_classes(d.get("names", None)) - if etau.is_str(data) and data.endswith(".txt"): - txt_path = _parse_yolo_v5_path(data, self.yaml_path) - image_paths = [ - _parse_yolo_v5_path(fos.normpath(p), txt_path) - for p in _read_file_lines(txt_path) - ] - else: - if etau.is_str(data): - data_dirs = [data] + image_paths = [] + for data_path in data_paths: + if etau.is_str(data_path) and data_path.endswith(".txt"): + txt_path = _parse_yolo_v5_path(data_path, self.yaml_path) + image_paths.extend( + _parse_yolo_v5_path(fos.normpath(p), txt_path) + for p in _read_file_lines(txt_path) + ) else: - data_dirs = data - - image_paths = [] - for data_dir in data_dirs: data_dir = fos.normpath( - _parse_yolo_v5_path(data_dir, self.yaml_path) + _parse_yolo_v5_path(data_path, self.yaml_path) ) image_paths.extend( etau.list_files(data_dir, abs_paths=True, recursive=True) diff --git a/tests/unittests/group_tests.py b/tests/unittests/group_tests.py index 9c7da7170e..65f1b73877 100644 --- a/tests/unittests/group_tests.py +++ b/tests/unittests/group_tests.py @@ -1978,6 +1978,15 @@ def test_flatten(self): ) self.assertListEqual(view.values("frame_number"), [1, 1]) + @drop_datasets + def test_match_expr(self): + dataset = _make_group_by_dataset() + + view = dataset.group_by( + "frame_number", flat=True, match_expr=(F().length() > 1) + ) + self.assertDictEqual(view.count_values("frame_number"), {1: 2, 2: 2}) + @drop_datasets def test_group_by_group_dataset(self): dataset = _make_group_by_group_dataset() diff --git a/tests/unittests/panels/panel_tests.py b/tests/unittests/panels/panel_tests.py index a55846b95c..ad829fbaf0 100644 --- a/tests/unittests/panels/panel_tests.py +++ b/tests/unittests/panels/panel_tests.py @@ -146,12 +146,6 @@ def test_panel_ref_data_setattr(mock_ctx): ) -def test_panel_ref_data_getattr_raises_write_only_error(mock_ctx): - data = PanelRefData(mock_ctx) - with pytest.raises(WriteOnlyError): - _ = data.test_key - - def test_panel_ref_data_clear(mock_ctx): data = PanelRefData(mock_ctx) data.test_key = "test_value" @@ -189,6 +183,39 @@ def test_panel_ref_set_state(mock_ctx): ) +def test_panel_ref_set_state_dict(mock_ctx): + panel_ref = PanelRef(mock_ctx) + panel_ref.set_state({"test_key": "test_value"}) + assert panel_ref._state._data["test_key"] == "test_value" + mock_ctx.ops.patch_panel_state.assert_called_with( + {"test_key": "test_value"} + ) + + +def test_panel_ref_set_state_deep_path(mock_ctx): + panel_ref = PanelRef(mock_ctx) + panel_ref.set_state("nested.key.path", "test_value") + assert panel_ref._state._data["nested"]["key"]["path"] == "test_value" + mock_ctx.ops.patch_panel_state.assert_called_with( + {"nested.key.path": "test_value"} + ) + + +def test_panel_ref_set_state_deep_path_dict(mock_ctx): + panel_ref = PanelRef(mock_ctx) + panel_ref.set_state({"nested.key.path": "test_value"}) + assert panel_ref._state._data["nested"]["key"]["path"] == "test_value" + mock_ctx.ops.patch_panel_state.assert_called_with( + {"nested.key.path": "test_value"} + ) + + +def test_panel_ref_get_data_raises_write_only_error(mock_ctx): + panel_ref = PanelRef(mock_ctx) + with pytest.raises(WriteOnlyError): + _ = panel_ref.data.get("test_key") + + def test_panel_ref_get_state(mock_ctx): panel_ref = PanelRef(mock_ctx) panel_ref.set_state("test_key", "test_value") @@ -202,3 +229,30 @@ def test_panel_ref_set_data(mock_ctx): mock_ctx.ops.patch_panel_data.assert_called_with( {"test_key": "test_value"} ) + + +def test_panel_ref_set_data_dict(mock_ctx): + panel_ref = PanelRef(mock_ctx) + panel_ref.set_data({"test_key": "test_value"}) + assert panel_ref._data._data["test_key"] == "test_value" + mock_ctx.ops.patch_panel_data.assert_called_with( + {"test_key": "test_value"} + ) + + +def test_panel_ref_set_data_deep_path(mock_ctx): + panel_ref = PanelRef(mock_ctx) + panel_ref.set_data("nested.key.path", "test_value") + assert panel_ref._data._data["nested"]["key"]["path"] == "test_value" + mock_ctx.ops.patch_panel_data.assert_called_with( + {"nested.key.path": "test_value"} + ) + + +def test_panel_ref_set_data_deep_path_dict(mock_ctx): + panel_ref = PanelRef(mock_ctx) + panel_ref.set_data({"nested.key.path": "test_value"}) + assert panel_ref._data._data["nested"]["key"]["path"] == "test_value" + mock_ctx.ops.patch_panel_data.assert_called_with( + {"nested.key.path": "test_value"} + )