diff --git a/app/packages/core/src/components/Actions/ActionsRow.tsx b/app/packages/core/src/components/Actions/ActionsRow.tsx index 2ae70aa77d..3fe844eb48 100644 --- a/app/packages/core/src/components/Actions/ActionsRow.tsx +++ b/app/packages/core/src/components/Actions/ActionsRow.tsx @@ -168,9 +168,7 @@ const Tag = ({ const tagging = useRecoilValue(fos.anyTagging); const ref = useRef(null); useOutsideClick(ref, () => open && setOpen(false)); - const lightning = useRecoilValue(fos.lightning); - const unlocked = fos.useLightingUnlocked(); - const disabled = tagging || (lightning && !modal && !unlocked); + const disabled = tagging; lookerRef && useEventHandler(lookerRef.current, "play", () => { diff --git a/app/packages/core/src/components/Actions/Options.tsx b/app/packages/core/src/components/Actions/Options.tsx index 199c00d457..a0715992b0 100644 --- a/app/packages/core/src/components/Actions/Options.tsx +++ b/app/packages/core/src/components/Actions/Options.tsx @@ -193,6 +193,10 @@ const DynamicGroupsViewMode = ({ modal }: { modal: boolean }) => { return options; }, [isOrderedDynamicGroup, hasGroupSlices]); + if (!modal && !isOrderedDynamicGroup) { + return null; + } + return ( <> Dynamic Groups Navigation diff --git a/app/packages/core/src/components/Common/Checkbox.tsx b/app/packages/core/src/components/Common/Checkbox.tsx index 65519f3933..33a656b68e 100644 --- a/app/packages/core/src/components/Common/Checkbox.tsx +++ b/app/packages/core/src/components/Common/Checkbox.tsx @@ -2,7 +2,7 @@ import { LoadingDots, useTheme } from "@fiftyone/components"; import { Checkbox as MaterialCheckbox } from "@mui/material"; import { animated } from "@react-spring/web"; import React, { useMemo } from "react"; -import { RecoilValueReadOnly, constSelector } from "recoil"; +import { constSelector, RecoilValueReadOnly } from "recoil"; import styled from "styled-components"; import { prettify } from "../../utils/generic"; import { ItemAction } from "../Actions/ItemAction"; @@ -17,7 +17,7 @@ interface CheckboxProps { loading?: boolean; value: boolean; setValue?: (value: boolean) => void; - count?: number; + count?: number | RecoilValueReadOnly; subcountAtom?: RecoilValueReadOnly; disabled?: boolean; muted?: boolean; @@ -56,7 +56,7 @@ function Checkbox({ const [text, coloring] = getValueString(formatter ? formatter(name) : name); const countAtom = useMemo( - () => (typeof count === "number" ? constSelector(count) : null), + () => (typeof count === "number" ? constSelector(count) : count ?? null), [count] ); diff --git a/app/packages/core/src/components/Filters/StringFilter/Checkboxes.tsx b/app/packages/core/src/components/Filters/StringFilter/Checkboxes.tsx index 6e9fe9a049..a53ee3df1f 100644 --- a/app/packages/core/src/components/Filters/StringFilter/Checkboxes.tsx +++ b/app/packages/core/src/components/Filters/StringFilter/Checkboxes.tsx @@ -1,6 +1,6 @@ import { LoadingDots } from "@fiftyone/components"; import * as fos from "@fiftyone/state"; -import React, { MutableRefObject } from "react"; +import React from "react"; import { RecoilState, selectorFamily, @@ -14,6 +14,7 @@ import { isBooleanField, isInKeypointsField } from "../state"; import { CHECKBOX_LIMIT, nullSort } from "../utils"; import Reset from "./Reset"; import { Result } from "./Result"; +import { pathSearchCount } from "./state"; import { showSearchSelector } from "./useSelected"; interface CheckboxesProps { @@ -23,7 +24,6 @@ interface CheckboxesProps { isMatchingAtom: RecoilState; modal: boolean; path: string; - selectedMap: MutableRefObject>; } const isSkeleton = selectorFamily({ @@ -86,13 +86,11 @@ const useValues = ({ path, results, selected, - selectedMap, }: { modal: boolean; path: string; results: Result[] | null; selected: (string | null)[]; - selectedMap: Map; }) => { const name = path.split(".").slice(-1)[0]; const unlocked = fos.useLightingUnlocked(); @@ -106,7 +104,7 @@ const useValues = ({ let allValues = selected.map((value) => ({ value, - count: hasCount ? counts.get(value) || selectedMap.get(value) || 0 : null, + count: hasCount ? counts.get(value) ?? null : null, loading: unlocked && loading, })); const objectId = useRecoilValue(fos.isObjectIdField(path)); @@ -149,7 +147,6 @@ const Checkboxes = ({ isMatchingAtom, modal, path, - selectedMap, }: CheckboxesProps) => { const [selected, setSelected] = useRecoilState(selectedAtom); const color = useRecoilValue(fos.pathColor(path)); @@ -160,7 +157,6 @@ const Checkboxes = ({ path, results, selected, - selectedMap: selectedMap.current, }); const show = useRecoilValue(showSearchSelector({ modal, path })); @@ -191,8 +187,9 @@ const Checkboxes = ({ loading={loading} count={ typeof count !== "number" || !isFilterMode || keypoints - ? undefined - : selectedMap.current.get(value) ?? count + ? // only string and id fields use pathSearchCount + pathSearchCount({ modal, path, value: value as string }) + : count } setValue={(checked: boolean) => { if (checked) { diff --git a/app/packages/core/src/components/Filters/StringFilter/StringFilter.tsx b/app/packages/core/src/components/Filters/StringFilter/StringFilter.tsx index b387e517e1..8e6e431fdd 100644 --- a/app/packages/core/src/components/Filters/StringFilter/StringFilter.tsx +++ b/app/packages/core/src/components/Filters/StringFilter/StringFilter.tsx @@ -69,7 +69,7 @@ const StringFilter = ({ path, resultsAtom ); - const { onSelect, selectedMap } = useOnSelect(modal, path, selectedAtom); + const onSelect = useOnSelect(modal, path, selectedAtom); const skeleton = useRecoilValue(isInKeypointsField(path)) && name === "keypoints"; const theme = useTheme(); @@ -127,7 +127,6 @@ const StringFilter = ({ excludeAtom={excludeAtom} isMatchingAtom={isMatchingAtom} modal={modal} - selectedMap={selectedMap} /> diff --git a/app/packages/core/src/components/Filters/StringFilter/state.ts b/app/packages/core/src/components/Filters/StringFilter/state.ts index d5a3073ef3..bcf307ef76 100644 --- a/app/packages/core/src/components/Filters/StringFilter/state.ts +++ b/app/packages/core/src/components/Filters/StringFilter/state.ts @@ -1,4 +1,5 @@ import * as fos from "@fiftyone/state"; +import { isMatchingAtom, stringExcludeAtom } from "@fiftyone/state"; import { getFetchFunction } from "@fiftyone/utilities"; import { atomFamily, selectorFamily } from "recoil"; import { labelTagsCount } from "../../Sidebar/Entries/EntryCounts"; @@ -13,18 +14,49 @@ export const stringSearch = atomFamily< default: "", }); +const pathSearchFilters = selectorFamily({ + key: "pathSearchFilters", + get: + ({ modal, path }: { modal: boolean; path: string }) => + ({ get }) => { + const filters = { ...get(modal ? fos.modalFilters : fos.filters) }; + + // omit the path being searched, but include coinciding filters + delete filters[path]; + + return filters; + }, +}); + +export const pathSearchCount = selectorFamily({ + key: "pathSearchCount", + get: + ({ modal, path, value }: { modal: boolean; path: string; value: string }) => + ({ get }) => { + return ( + get( + stringSearchResults({ + modal, + path, + filter: { path, value }, + }) + )?.values?.[0].count || 0 + ); + }, +}); + export const stringSearchResults = selectorFamily< { values?: Result[]; count?: number; }, - { path: string; modal: boolean } + { path: string; modal: boolean; filter?: { path: string; value: string } } >({ key: "stringSearchResults", get: - ({ path, modal }) => + ({ path, modal, filter }) => async ({ get }) => { - const search = get(stringSearch({ modal, path })); + const search = filter ? "" : get(stringSearch({ modal, path })); const sorting = get(fos.sortFilterResults(modal)); const mixed = get(fos.groupStatistics(modal)) === "group"; const selected = get(fos.stringSelectedValuesAtom({ path, modal })); @@ -61,8 +93,16 @@ export const stringSearchResults = selectorFamily< view: get(fos.view), path, search, - selected, - filters: !modal ? get(fos.lightningFilters) : null, + selected: filter ? [] : selected, + filters: filter + ? { + [filter.path]: { + exclude: get(stringExcludeAtom({ path, modal })), + isMatching: get(isMatchingAtom({ path, modal })), + values: [filter.value], + }, + } + : get(pathSearchFilters({ modal, path })), group_id: modal ? get(fos.groupId) || null : null, mixed, slice: get(fos.groupSlice), diff --git a/app/packages/core/src/components/Filters/StringFilter/useOnSelect.ts b/app/packages/core/src/components/Filters/StringFilter/useOnSelect.ts index 96703805af..c8e532432b 100644 --- a/app/packages/core/src/components/Filters/StringFilter/useOnSelect.ts +++ b/app/packages/core/src/components/Filters/StringFilter/useOnSelect.ts @@ -1,7 +1,6 @@ import { SelectorValidationError } from "@fiftyone/components"; import { isObjectIdField, snackbarErrors } from "@fiftyone/state"; import { isObjectIdString } from "@fiftyone/utilities"; -import { useRef } from "react"; import { RecoilState, useRecoilCallback } from "recoil"; import { Result } from "./Result"; @@ -10,31 +9,25 @@ export default function ( path: string, selectedAtom: RecoilState<(string | null)[]> ) { - const selectedMap = useRef>(new Map()); - return { - onSelect: useRecoilCallback( - ({ snapshot, set }) => - async (value: string | null, d?: Result) => { - const isObjectId = await snapshot.getPromise(isObjectIdField(path)); - if (isObjectId && (value === null || !isObjectIdString(value))) { - set(snackbarErrors, [ - `${value} is not a 24 character hexadecimal string`, - ]); - throw new SelectorValidationError(); - } + return useRecoilCallback( + ({ snapshot, set }) => + async (value: string | null, d?: Result) => { + const isObjectId = await snapshot.getPromise(isObjectIdField(path)); + if (isObjectId && (value === null || !isObjectIdString(value))) { + set(snackbarErrors, [ + `${value} is not a 24 character hexadecimal string`, + ]); + throw new SelectorValidationError(); + } - const selected = new Set(await snapshot.getPromise(selectedAtom)); - if (d?.value === null) { - value = null; - } - selectedMap.current.set(value, d?.count || null); - selected.add(value); - set(selectedAtom, [...selected].sort()); - - return ""; - }, - [modal, path, selectedAtom, selectedMap] - ), - selectedMap, - }; + const selected = new Set(await snapshot.getPromise(selectedAtom)); + if (d?.value === null) { + value = null; + } + selected.add(value); + set(selectedAtom, [...selected].sort()); + return ""; + }, + [modal, path, selectedAtom] + ); } diff --git a/app/packages/core/src/components/Modal/Group/DynamicGroup/index.tsx b/app/packages/core/src/components/Modal/Group/DynamicGroup/index.tsx index 9af19891f0..fd6f769539 100644 --- a/app/packages/core/src/components/Modal/Group/DynamicGroup/index.tsx +++ b/app/packages/core/src/components/Modal/Group/DynamicGroup/index.tsx @@ -1,6 +1,6 @@ import * as fos from "@fiftyone/state"; import React, { useEffect } from "react"; -import { useRecoilValue, useSetRecoilState } from "recoil"; +import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil"; import { NestedGroup } from "./NestedGroup"; import { NonNestedDynamicGroup } from "./NonNestedGroup"; import { useDynamicGroupSamples } from "./useDynamicGroupSamples"; @@ -11,6 +11,11 @@ export const DynamicGroup = () => { const { queryRef } = useDynamicGroupSamples(); const shouldRenderImaVid = useRecoilValue(fos.shouldRenderImaVidLooker); + const [dynamicGroupsViewMode, setDynamicGroupsViewMode] = useRecoilState( + fos.dynamicGroupsViewMode + ); + const isOrderedDynamicGroup = useRecoilValue(fos.isOrderedDynamicGroup); + const setDynamicGroupCurrentElementIndex = useSetRecoilState( fos.dynamicGroupCurrentElementIndex ); @@ -26,6 +31,14 @@ export const DynamicGroup = () => { } }, [shouldRenderImaVid, imaVidIndex, setDynamicGroupCurrentElementIndex]); + useEffect(() => { + // if dynamic group view mode is video but dynamic group is not ordered, + // we want to set view mode back to pagination (default) + if (dynamicGroupsViewMode === "video" && !isOrderedDynamicGroup) { + setDynamicGroupsViewMode("pagination"); + } + }, [dynamicGroupsViewMode, isOrderedDynamicGroup]); + return ( <> {hasGroupSlices ? ( diff --git a/app/packages/core/src/components/Sidebar/Entries/PathValueEntry.tsx b/app/packages/core/src/components/Sidebar/Entries/PathValueEntry.tsx index e47a3bbdc0..545e1cc7da 100644 --- a/app/packages/core/src/components/Sidebar/Entries/PathValueEntry.tsx +++ b/app/packages/core/src/components/Sidebar/Entries/PathValueEntry.tsx @@ -296,7 +296,7 @@ const SlicesLoadable = ({ path }: { path: string }) => { >
{slice}
(path: string) => { const data = { ...loadable.contents }; - let field = useRecoilValue(fos.field(keys[0])); + const target = useRecoilValue(fos.field(keys[0])); slices.forEach((slice) => { let sliceData = data[slice].sample; + let field = target; for (let index = 0; index < keys.length; index++) { if (!sliceData) { break; } const key = keys[index]; - sliceData = sliceData[field?.dbField || key]; if (keys[index + 1]) { diff --git a/app/packages/core/src/components/Sidebar/Sidebar.tsx b/app/packages/core/src/components/Sidebar/Sidebar.tsx index f2f228ba6d..ed6170d9f3 100644 --- a/app/packages/core/src/components/Sidebar/Sidebar.tsx +++ b/app/packages/core/src/components/Sidebar/Sidebar.tsx @@ -382,7 +382,7 @@ const SidebarColumn = styled.div` const Container = animated(styled.div` position: relative; min-height: 100%; - margin: 0 0.25rem 0 1rem; + margin: 0 1rem; & > div { position: absolute; diff --git a/app/packages/core/src/components/Sidebar/ViewSelection/ViewDialog.tsx b/app/packages/core/src/components/Sidebar/ViewSelection/ViewDialog.tsx index 24103622b9..4426abc329 100644 --- a/app/packages/core/src/components/Sidebar/ViewSelection/ViewDialog.tsx +++ b/app/packages/core/src/components/Sidebar/ViewSelection/ViewDialog.tsx @@ -194,6 +194,8 @@ export default function ViewDialog(props: Props) { return ( { setIsOpen(false); diff --git a/app/packages/looker-3d/src/action-bar/SliceSelector.tsx b/app/packages/looker-3d/src/action-bar/SliceSelector.tsx index cf89220d08..abdebf3056 100644 --- a/app/packages/looker-3d/src/action-bar/SliceSelector.tsx +++ b/app/packages/looker-3d/src/action-bar/SliceSelector.tsx @@ -43,7 +43,10 @@ export const SliceSelector = () => { return ( <> - +
{activeSlicesLabel}
@@ -70,7 +73,7 @@ const PcdsSelector = () => { return ( Select point clouds -
+
{allPcdSlices.map((slice) => { return ( ( const { groupBy } = snapshot .getLoadable(groupAtoms.dynamicGroupParameters) .valueMaybe(); - const groupByFieldValue = String( - get(sample, getSanitizedGroupByExpression(groupBy)) + const groupByFieldValue = get( + sample, + getSanitizedGroupByExpression(groupBy) ); + const groupByFieldValueTransformed = groupByFieldValue + ? String(groupByFieldValue) + : null; const totalFrameCountPromise = getPromise( - dynamicGroupsElementCount(groupByFieldValue) + dynamicGroupsElementCount(groupByFieldValueTransformed) ); const page = snapshot - .getLoadable(groupAtoms.dynamicGroupPageSelector(groupByFieldValue)) + .getLoadable( + groupAtoms.dynamicGroupPageSelector(groupByFieldValueTransformed) + ) .valueMaybe(); const firstFrameNumber = isModal @@ -183,7 +189,10 @@ export default ( const imavidKey = snapshot .getLoadable( - groupAtoms.imaVidStoreKey({ groupByFieldValue, modal: isModal }) + groupAtoms.imaVidStoreKey({ + groupByFieldValue: groupByFieldValueTransformed, + modal: isModal, + }) ) .valueOrThrow(); diff --git a/app/packages/state/src/recoil/aggregations.ts b/app/packages/state/src/recoil/aggregations.ts index f46d103203..8affb7d4da 100644 --- a/app/packages/state/src/recoil/aggregations.ts +++ b/app/packages/state/src/recoil/aggregations.ts @@ -56,6 +56,13 @@ export const aggregationQuery = graphQLSelectorFamily< const dataset = get(selectors.datasetName); if (!dataset) return null; + const useSidebarSampleId = !root && modal && !get(groupId) && !mixed; + const sampleIds = useSidebarSampleId ? [get(sidebarSampleId)] : []; + + if (useSidebarSampleId && sampleIds[0] === null) { + return null; + } + const lightningFilters = lightning || (!modal && @@ -80,10 +87,7 @@ export const aggregationQuery = graphQLSelectorFamily< hiddenLabels: !root ? get(selectors.hiddenLabelsArray) : [], paths, mixed, - sampleIds: - !root && modal && !get(groupId) && !mixed - ? [get(sidebarSampleId)] - : [], + sampleIds, slices: mixed ? null : get(currentSlices(modal)), // when mixed, slice is not needed slice: get(groupSlice), view: customView ? customView : !root ? get(viewAtoms.view) : [], diff --git a/app/packages/state/src/recoil/groups.ts b/app/packages/state/src/recoil/groups.ts index 640f6d537c..df62e3f12f 100644 --- a/app/packages/state/src/recoil/groups.ts +++ b/app/packages/state/src/recoil/groups.ts @@ -544,7 +544,9 @@ export const activeModalSample = selector({ key: "activeModalSample", get: ({ get }) => { if (get(pinned3d)) { - return get(activePcdSlicesToSampleMap)[get(pinned3DSampleSlice)]?.sample; + const slices = get(activePcdSlices); + const key = slices.length === 1 ? slices[0] : get(pinned3DSampleSlice); + return get(activePcdSlicesToSampleMap)[key]?.sample; } return get(modalSample).sample; diff --git a/app/packages/state/src/recoil/modal.ts b/app/packages/state/src/recoil/modal.ts index 643a4a4b4b..0a173b4a33 100644 --- a/app/packages/state/src/recoil/modal.ts +++ b/app/packages/state/src/recoil/modal.ts @@ -46,8 +46,9 @@ export const sidebarSampleId = selector({ return id; } } - // suspend - return new Promise(() => null); + + // if we are playing or seeking, we don't want to change the sidebar sample and fire agg query + return null; } const override = get(pinned3DSampleSlice); diff --git a/app/packages/state/src/recoil/sidebar.test.ts b/app/packages/state/src/recoil/sidebar.test.ts index 57bd23a487..7410777cd6 100644 --- a/app/packages/state/src/recoil/sidebar.test.ts +++ b/app/packages/state/src/recoil/sidebar.test.ts @@ -185,9 +185,9 @@ const mockFields = { ], }; -describe("ResolveGroups works", () => { - it("dataset groups should resolve when curent is undefined", () => { - const test = sidebar.resolveGroups(mockFields.sampleFields, []); +describe("test sidebar groups resolution", () => { + it("resolves with sample fields", () => { + const test = sidebar.resolveGroups(mockFields.sampleFields, [], [], []); expect(test.length).toBe(5); expect(test[0].name).toBe("tags"); @@ -202,7 +202,7 @@ describe("ResolveGroups works", () => { expect(test[4].paths.length).toBe(2); }); - it("when dataset appconfig does not have sidebarGroups settings, use default settings", () => { + it("resolves merges of current client setting", () => { const mockSidebarGroups = [ { name: "tags", paths: [], expanded: true }, { @@ -234,7 +234,8 @@ describe("ResolveGroups works", () => { const test = sidebar.resolveGroups( mockFields.sampleFields, [], - mockSidebarGroups + mockSidebarGroups, + [] ); expect(test.length).toBe(7); @@ -245,4 +246,113 @@ describe("ResolveGroups works", () => { expect(test[6].name).toBe("test group b"); expect(test[6].expanded).toBeFalsy(); }); + + it("resolves merges with dataset app config", () => { + const mockSidebarGroups = [ + { + name: "labels", + paths: ["predictions", "ground_truth"], + expanded: true, + }, + { + name: "primitives", + paths: ["id", "filepath", "uniqueness"], + expanded: true, + }, + { name: "other", paths: ["dict_field", "list_field"] }, + { name: "test group a", paths: [] }, + { name: "test group b", paths: [] }, + { + name: "metadata", + paths: [], + expanded: true, + }, + { name: "tags", paths: [], expanded: true }, + ]; + + const test = sidebar.resolveGroups( + mockFields.sampleFields, + [], + [], + mockSidebarGroups + ); + + expect(test.length).toBe(7); + + const tags = test[0]; + expect(tags.name).toBe("tags"); + expect(tags.paths).toEqual([]); + + const labels = test[1]; + expect(labels.name).toBe("labels"); + expect(labels.paths).toEqual(["predictions", "ground_truth"]); + + const primitives = test[2]; + expect(primitives.name).toEqual("primitives"); + expect(primitives.paths).toEqual(["id", "filepath", "uniqueness"]); + expect(primitives.expanded).toBe(true); + + const other = test[3]; + expect(other.name).toEqual("other"); + expect(other.paths).toEqual(["dict_field", "list_field"]); + + const testA = test[4]; + expect(testA.name).toBe("test group a"); + + const testB = test[5]; + expect(testB.name).toBe("test group b"); + + const metadata = test[6]; + expect(metadata.name).toBe("metadata"); + expect(metadata.expanded).toBe(true); + expect(metadata.paths).toEqual([ + "metadata.size_types", + "metadata.mime_type", + "metadata.width", + "metadata.height", + "metadata.num_channels", + ]); + }); + + it("resolves merges with improper dataset app config", () => { + const mockSidebarGroups = [ + { + name: "improper", + paths: ["ground_truth"], + expanded: true, + }, + ]; + + const test = sidebar.resolveGroups( + mockFields.sampleFields, + [], + [], + mockSidebarGroups + ); + + const tags = test[0]; + expect(tags.name).toBe("tags"); + expect(tags.paths).toEqual([]); + + const metadata = test[1]; + expect(metadata.name).toBe("metadata"); + expect(metadata.expanded).toBe(true); + expect(metadata.paths).toEqual([ + "metadata.size_types", + "metadata.mime_type", + "metadata.width", + "metadata.height", + "metadata.num_channels", + ]); + + const improper = test[2]; + expect(improper.name).toBe("improper"); + expect(improper.paths).toEqual(["ground_truth"]); + expect(improper.expanded).toBe(true); + + const labels = test[3]; + expect(labels.name).toBe("labels"); + expect(labels.paths).toEqual(["predictions"]); + expect(labels.expanded).toBe(true); + }); }); diff --git a/app/packages/state/src/recoil/sidebar.ts b/app/packages/state/src/recoil/sidebar.ts index 64e250c238..7782627f29 100644 --- a/app/packages/state/src/recoil/sidebar.ts +++ b/app/packages/state/src/recoil/sidebar.ts @@ -9,7 +9,6 @@ import { setSidebarGroups, setSidebarGroupsMutation, sidebarGroupsFragment, - sidebarGroupsFragment$data, sidebarGroupsFragment$key, } from "@fiftyone/relay"; import { @@ -164,14 +163,14 @@ export const RESERVED_GROUPS = new Set([ const LABELS = withPath(LABELS_PATH, VALID_LABEL_TYPES); -const DEFAULT_IMAGE_GROUPS = [ +const DEFAULT_IMAGE_GROUPS: State.SidebarGroup[] = [ { name: "tags", paths: [] }, { name: "metadata", paths: [] }, { name: "labels", paths: [] }, { name: "primitives", paths: [] }, ]; -const DEFAULT_VIDEO_GROUPS = [ +const DEFAULT_VIDEO_GROUPS: State.SidebarGroup[] = [ { name: "tags", paths: [] }, { name: "metadata", paths: [] }, { name: "labels", paths: [] }, @@ -184,17 +183,18 @@ const NONE = [null, undefined]; export const resolveGroups = ( sampleFields: StrictField[], frameFields: StrictField[], - sidebarGroups?: State.SidebarGroup[], - config: NonNullable< - sidebarGroupsFragment$data["appConfig"] - >["sidebarGroups"] = [] + currentGroups: State.SidebarGroup[], + configGroups: State.SidebarGroup[] ): State.SidebarGroup[] => { - let groups = sidebarGroups?.length - ? sidebarGroups + let groups = currentGroups.length + ? currentGroups + : configGroups.length + ? configGroups : frameFields.length ? DEFAULT_VIDEO_GROUPS : DEFAULT_IMAGE_GROUPS; - const expanded = config.reduce((map, { name, expanded }) => { + + const expanded = configGroups.reduce((map, { name, expanded }) => { map[name] = expanded; return map; }, {}); @@ -205,6 +205,23 @@ export const resolveGroups = ( : { ...group }; }); + const metadata = groups.find(({ name }) => name === "metadata"); + if (!metadata) { + groups.unshift({ + name: "metadata", + expanded: true, + paths: [], + }); + } + + const tags = groups.find(({ name }) => name === "tags"); + groups = groups.filter(({ name }) => name !== "tags"); + groups.unshift({ + name: "tags", + expanded: tags?.expanded, + paths: [], + }); + const present = new Set(groups.map(({ paths }) => paths).flat()); const updater = groupUpdater( groups, @@ -212,17 +229,23 @@ export const resolveGroups = ( present ); - updater("labels", fieldsMatcher(sampleFields, labelsMatcher(), present)); + updater( + "labels", + fieldsMatcher(sampleFields, labelsMatcher(), present), + true + ); frameFields.length && updater( "frame labels", - fieldsMatcher(frameFields, labelsMatcher(), present, "frames.") + fieldsMatcher(frameFields, labelsMatcher(), present, "frames."), + true ); updater( "primitives", - fieldsMatcher(sampleFields, primitivesMatcher, present) + fieldsMatcher(sampleFields, primitivesMatcher, present), + true ); sampleFields.filter(groupFilter).forEach(({ fields, name }) => { @@ -237,7 +260,8 @@ export const resolveGroups = ( present.add(`frames.${name}`); updater( `frames.${name}`, - fieldsMatcher(fields || [], () => true, present, `frames.${name}.`) + fieldsMatcher(fields || [], () => true, present, `frames.${name}.`), + true ); }); @@ -262,13 +286,13 @@ const groupUpdater = ( groups[i].paths = filterPaths(groups[i].paths, schema); } - return (name: string, paths: string[]) => { + return (name: string, paths: string[], expanded = false) => { if (paths.length === 0) return; paths.forEach((path) => present.add(path)); const index = groupNames.indexOf(name); if (index < 0) { - groups.push({ name, paths, expanded: false }); + groups.push({ name, paths, expanded }); groupNames.push(name); return; } @@ -279,13 +303,11 @@ const groupUpdater = ( }; export const [resolveSidebarGroups, sidebarGroupsDefinition] = (() => { - let config: NonNullable< - sidebarGroupsFragment$data["appConfig"] - >["sidebarGroups"] = []; + let configGroups: State.SidebarGroup[] = []; let current: State.SidebarGroup[] = []; return [ (sampleFields: StrictField[], frameFields: StrictField[]) => { - return resolveGroups(sampleFields, frameFields, current, config); + return resolveGroups(sampleFields, frameFields, current, configGroups); }, graphQLSyncFragmentAtomFamily< sidebarGroupsFragment$key, @@ -297,7 +319,10 @@ export const [resolveSidebarGroups, sidebarGroupsDefinition] = (() => { keys: ["dataset"], sync: (modal) => !modal, read: (data, prev) => { - config = data.appConfig?.sidebarGroups || []; + configGroups = (data.appConfig?.sidebarGroups || []).map((group) => ({ + ...group, + paths: [...group.paths], + })); current = resolveGroups( collapseFields( readFragment( @@ -310,7 +335,7 @@ export const [resolveSidebarGroups, sidebarGroupsDefinition] = (() => { .frameFields ), data.name === prev?.name ? current : [], - config + configGroups ); return current; diff --git a/app/packages/state/src/recoil/view.ts b/app/packages/state/src/recoil/view.ts index b96be672fa..52239de6c6 100644 --- a/app/packages/state/src/recoil/view.ts +++ b/app/packages/state/src/recoil/view.ts @@ -238,9 +238,17 @@ export const dynamicGroupViewQuery = selectorFamily< // todo: sanitize expressions const groupBySanitized = getSanitizedGroupByExpression(groupBy); - const groupByValue = groupByFieldValueExplicit - ? String(groupByFieldValueExplicit) - : String(get(groupByFieldValue)); + let groupByValue; + + if (groupByFieldValueExplicit) { + groupByValue = String(groupByFieldValueExplicit); + } else { + groupByValue = get(groupByFieldValue); + + if (groupByValue) { + groupByValue = String(groupByValue); + } + } const viewStages: State.Stage[] = [ { diff --git a/app/packages/utilities/src/styles.ts b/app/packages/utilities/src/styles.ts index b5c1cd55ef..261aecbd6e 100644 --- a/app/packages/utilities/src/styles.ts +++ b/app/packages/utilities/src/styles.ts @@ -14,9 +14,7 @@ scrollbar-color: ${({ theme }) => theme.text.tertiary} ${({ theme }) => border: solid 4px transparent ${theme.text.tertiary}; } -@-moz-document url-prefix() { - padding-right: 16px; -} + ::-webkit-scrollbar-thumb { box-shadow: inset 0 0 10px 10px transparent; diff --git a/docs/source/integrations/huggingface.rst b/docs/source/integrations/huggingface.rst index 89b248a4d8..f8d89a571e 100644 --- a/docs/source/integrations/huggingface.rst +++ b/docs/source/integrations/huggingface.rst @@ -30,8 +30,8 @@ _________ All `Transformers models `_ -that support image classification, object detection, and semantic segmentation -can be passed directly to your FiftyOne dataset's +that support image classification, object detection, semantic segmentation, or +monocular depth estimation tasks can be passed directly to your FiftyOne dataset's :meth:`apply_model() ` method. @@ -372,6 +372,57 @@ model's name or path as a keyword argument: session = fo.launch_app(dataset) + +.. _huggingface-monocular-depth-estimation: + +Monocular depth estimation +-------------------------- + +You can pass a `transformers` monocular depth estimation model directly to your +FiftyOne dataset's :meth:`apply_model() ` +method: + +.. code-block:: python + :linenos: + + # DPT + from transformers import DPTForDepthEstimation + model = DPTForDepthEstimation.from_pretrained("Intel/dpt-large") + + # GLPN + from transformers import GLPNForDepthEstimation + model = GLPNForDepthEstimation.from_pretrained("vinvino02/glpn-kitti") + + +.. code-block:: python + :linenos: + + dataset.apply_model(model, label_field="depth_predictions") + + session = fo.launch_app(dataset) + +Alternatively, you can load `transformers` depth estimation models directly from +the :ref:`FiftyOne Model Zoo `! + +To load a `transformers` depth estimation model from the zoo, specify +`"depth-estimation-transformer-torch"` as the first argument, and pass in the +model's name or path as a keyword argument: + +.. code-block:: python + :linenos: + + import fiftyone.zoo as foz + + model = foz.load_zoo_model( + "depth-estimation-transformer-torch", + name_or_path="Intel/dpt-hybrid-midas", + ) + + dataset.apply_model(model, label_field="dpt_hybrid_midas") + + session = fo.launch_app(dataset) + + .. _huggingface-zero-shot-classification: Zero-shot classification diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index b8ddde1e21..d92f709faf 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -3,6 +3,53 @@ FiftyOne Release Notes .. default-role:: code +FiftyOne Teams 1.5.6 +-------------------- +*Released February 14, 2024* + +Includes all updates from :ref:`FiftyOne 0.23.5 `, plus: + +- Improved dataset search user experience +- Post login redirects will now send the user to the correct page + +.. _release-notes-v0.23.5: + +FiftyOne 0.23.5 +--------------- +*Released February 14, 2024* + + +What's New + +- Added subcounts to search results in the sidebar + `#3973 `_ +- Added :class:`fiftyone.operators.types.ViewTargetProperty` to make it simpler to add view selection to a :class:`fiftyone.operators.Operator` + `#4076 `_ +- Added support for apply monocular depth estimation transformers from the + Hugging Face `transformers` library directly to FiftyOne datasets + `#4082 `_ + + +Bugs + +- Fixed an issue where increments were padded improperly + `#4035 `_ +- Fixed an issue when setting `session.color_scheme` + `#4060 `_ +- Fixed sidebar groups resolution when the dataset app config setting is configured + `#4064 `_ +- Fixed issue when `SelectGroupSlices` view stage is applied with only one slice within video grouped datasets + `#4066 `_ +- Fixed non-default pcd slice rendering in the App + `#4044 `_ +- Dynamic groups configuration options are now only shown when relevant + `#4068 `_ +- Fixed issue with dynamic groups mode pagination + `#4068 `_ +- Enabled tagging in sidebar lightning mode + `#4048 `_ + + FiftyOne Teams 1.5.5 -------------------- *Released January 25, 2024* @@ -318,7 +365,7 @@ Features - Added support for manually marking delegated operations :ref:`as failed ` - Added support for - :ref:`monioring the progress ` + :ref:`monitoring the progress ` of delegated operations - Improved handling of plugin secrets - Added the ability to attach authorization tokens to media/asset requests diff --git a/docs/source/teams/installation.rst b/docs/source/teams/installation.rst index 0d08ba6fdf..c42ae9e7af 100644 --- a/docs/source/teams/installation.rst +++ b/docs/source/teams/installation.rst @@ -496,6 +496,10 @@ to a specific list of bucket(s): do not wish to provide a single set of credentials to cover all buckets that your team plans to use within a given cloud storage provider. +Alternatively, credentials can be updated programmatically with the +:meth:`add_cloud_credentials() ` +method in the Management SDK. + Any cloud credentials uploaded via this method will automatically be used by the Teams UI when any user attempts to load media associated with the appropriate provider or specific bucket. diff --git a/docs/source/teams/management_sdk.rst b/docs/source/teams/management_sdk.rst index 4a6556fd72..5cd9c621c6 100644 --- a/docs/source/teams/management_sdk.rst +++ b/docs/source/teams/management_sdk.rst @@ -39,6 +39,15 @@ API keys :members: :undoc-members: +.. _teams-sdk-cloud-credentials: + +Cloud Credentials +----------------- + +.. automodule:: fiftyone.management.cloud_credentials + :members: + :undoc-members: + .. _teams-sdk-dataset-permissions: Dataset permissions diff --git a/e2e-pw/package.json b/e2e-pw/package.json index 9651a360ec..6d080f013b 100644 --- a/e2e-pw/package.json +++ b/e2e-pw/package.json @@ -4,24 +4,24 @@ "main": "index.js", "license": "MIT", "devDependencies": { - "@playwright/test": "^1.40.1", + "@playwright/test": "^1.41.2", "@types/fluent-ffmpeg": "^2.1.24", "@types/wait-on": "^5.3.4", - "@typescript-eslint/eslint-plugin": "^6.14.0", - "@typescript-eslint/parser": "^6.14.0", - "dotenv": "^16.3.1", - "eslint": "^8.55.0", - "eslint-plugin-playwright": "^0.20.0", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "dotenv": "^16.4.1", + "eslint": "^8.56.0", + "eslint-plugin-playwright": "^0.22.2", "fluent-ffmpeg": "^2.1.2", "jimp": "^0.22.10", "tree-kill": "^1.2.2", "ts-dedent": "^2.2.0", "typescript": "^5.3.3", - "vitest": "^1.0.4", + "vitest": "^1.2.2", "wait-on": "^7.2.0" }, "scripts": { - "lint": "bash -c 'set +e; eslint --quiet --ignore-pattern *.js .; set -e; tsc --skipLibCheck --sourceMap false'", + "lint": "bash -c 'set +e; eslint --quiet --ignore-pattern *.js .; set -e; tsc --skipLibCheck --noImplicitAny --sourceMap false'", "unittests": "vitest", "check-flaky": "./scripts/check-flaky.sh", "kill-port": "./scripts/kill-port.sh", diff --git a/e2e-pw/src/oss/poms/color-modal/index.ts b/e2e-pw/src/oss/poms/color-modal/index.ts index 6ef1915211..ae94f2ece8 100644 --- a/e2e-pw/src/oss/poms/color-modal/index.ts +++ b/e2e-pw/src/oss/poms/color-modal/index.ts @@ -81,7 +81,7 @@ export class ColorModalPom { .click({ force: true }); } - async addNewPairs(pairs) { + async addNewPairs(pairs: { value: string; color: string }[]) { for (let i = 0; i < pairs.length; i++) { await this.addANewPair(pairs[i].value, pairs[i].color, i); } diff --git a/e2e-pw/src/oss/poms/modal/index.ts b/e2e-pw/src/oss/poms/modal/index.ts index f2073450d4..528121fe6b 100644 --- a/e2e-pw/src/oss/poms/modal/index.ts +++ b/e2e-pw/src/oss/poms/modal/index.ts @@ -45,6 +45,10 @@ export class ModalPom { return this.locator.getByTestId("looker3d"); } + get looker3dActionBar() { + return this.locator.getByTestId("looker3d-action-bar"); + } + get carousel() { return this.locator.getByTestId("group-carousel"); } @@ -153,9 +157,7 @@ export class ModalPom { slice: string, allowErrorInfo = false ) { - const currentSlice = await this.sidebar.getSidebarEntryText( - `sidebar-entry-${groupField}` - ); + const currentSlice = await this.sidebar.getSidebarEntryText(groupField); const lookers = this.groupCarousel.getByTestId("looker"); const looker = lookers.filter({ hasText: slice }).first(); await looker.click({ position: { x: 10, y: 60 } }); @@ -191,6 +193,17 @@ export class ModalPom { return this.looker3d.click(); } + async toggleLooker3dSlice(slice: string) { + await this.looker3dActionBar.getByTestId("looker3d-select-slices").click(); + + await this.looker3dActionBar + .getByTestId("looker3d-slice-checkboxes") + .getByTestId(`checkbox-${slice}`) + .click(); + + await this.clickOnLooker3d(); + } + async clickOnLooker() { return this.looker.click(); } diff --git a/e2e-pw/src/oss/poms/modal/modal-sidebar.ts b/e2e-pw/src/oss/poms/modal/modal-sidebar.ts index 2a4b98bba7..db9385e911 100644 --- a/e2e-pw/src/oss/poms/modal/modal-sidebar.ts +++ b/e2e-pw/src/oss/poms/modal/modal-sidebar.ts @@ -22,7 +22,7 @@ export class ModalSidebarPom { } async getSampleTagCount() { - return Number(await this.getSidebarEntryText("sidebar-entry-tags")); + return Number(await this.getSidebarEntryText("tags")); } async getLabelTagCount() { @@ -35,11 +35,11 @@ export class ModalSidebarPom { } async getSampleId() { - return this.getSidebarEntryText("sidebar-entry-id"); + return this.getSidebarEntryText("id"); } async getSampleFilepath(abs = true) { - const absPath = await this.getSidebarEntryText("sidebar-entry-filepath"); + const absPath = await this.getSidebarEntryText("filepath"); if (!abs) { return absPath.split("/").at(-1); diff --git a/e2e-pw/src/oss/poms/saved-views.ts b/e2e-pw/src/oss/poms/saved-views.ts index 12a4219657..e31948d8c9 100644 --- a/e2e-pw/src/oss/poms/saved-views.ts +++ b/e2e-pw/src/oss/poms/saved-views.ts @@ -11,6 +11,15 @@ export type Color = | "Orange" | "Purple"; +export type SaveViewParams = { + name: string; + description: string; + color: Color; + id?: number; + newColor?: Color; + slug?: string; +}; + const defaultColor = "Gray"; export class SavedViewsPom { @@ -47,31 +56,33 @@ export class SavedViewsPom { return this.savedViewOption(slug).getByTestId("btn-edit-selection"); } - async saveViewInputs({ name, description, color, newColor }) { + async saveViewInputs({ name, description, color, newColor }: SaveViewParams) { await this.nameInput().fill(name, { timeout: 2000 }); await this.descriptionInput().fill(description, { timeout: 2000 }); await this.colorInput(color).click({ timeout: 2000 }); await this.colorOption(newColor).click(); } - async saveView(view) { - await this.openCreateModal(); - await this.saveViewInputs(view); - await this.saveButton().click(); + async waitUntilModalHidden() { + await this.dialogLocator.waitFor({ state: "hidden" }); } - async saveView2() { + async saveView(view: SaveViewParams) { + await this.openCreateModal(); + await this.saveViewInputs(view); await this.saveButton().click(); + await this.waitUntilModalHidden(); } async deleteView(name: string) { await this.savedViewOption(name).hover(); await this.optionEdit(name).click(); - await this.clickDeleteBtn(); + await this.deleteViewClick(); } async deleteViewClick() { await this.clickDeleteBtn(); + await this.waitUntilModalHidden(); } async editView( @@ -81,13 +92,16 @@ export class SavedViewsPom { newColor: Color ) { await this.nameInput().clear(); - await this.nameInput().type(name); + await this.nameInput().pressSequentially(name); await this.descriptionInput().clear(); - await this.descriptionInput().type(description); - await this.colorInput(color).click(); + await this.descriptionInput().pressSequentially(description); + // need to force click otherwise intercepted by material-ui + // eslint-disable-next-line playwright/no-force-option + await this.colorInputContainer().click({ force: true }); await this.colorOption(newColor).click(); await this.saveButton().click(); + await this.waitUntilModalHidden(); } async clickColor(color: Color = defaultColor) { @@ -95,7 +109,13 @@ export class SavedViewsPom { } async clearView() { - await this.clearViewBtn().click(); + if (await this.canClearView()) { + const urlBeforeClear = this.page.url(); + await this.clearViewBtn().click(); + await this.page.waitForFunction((urlBeforeClear) => { + return window.location.href !== urlBeforeClear; + }, urlBeforeClear); + } } async clickCloseModal() { @@ -107,9 +127,7 @@ export class SavedViewsPom { } clearViewBtn() { - return this.selector() - .getByTestId("saved-views-btn-selection-clear") - .first(); + return this.locator.getByTestId("saved-views-btn-selection-clear").first(); } closeModalBtn() { @@ -155,8 +173,14 @@ export class SavedViewsPom { return this.dialogLocator.getByTestId("saved-views-input-description"); } + colorInputContainer() { + return this.dialogLocator.getByTestId( + "saved-views-input-color-selection-selection" + ); + } + colorInput(c: Color = defaultColor) { - return this.dialogLocator.getByRole("combobox").getByText(c); + return this.colorInputContainer().getByText(c); } colorOption(c: Color = "Purple") { @@ -198,7 +222,8 @@ export class SavedViewsPom { } async clickDeleteBtn() { - return this.deleteBtn().click(); + await this.deleteBtn().click(); + await this.waitUntilModalHidden(); } } @@ -313,7 +338,7 @@ class SavedViewAsserter { excluded: string[] ) { await this.svp.searchInput().clear(); - await this.svp.searchInput().type(term); + await this.svp.searchInput().pressSequentially(term); if (expectedResult.length) { await this.svp diff --git a/e2e-pw/src/oss/specs/groups/ima-vid.spec.ts b/e2e-pw/src/oss/specs/groups/ima-vid.spec.ts index 638fbee791..d0227d88e5 100644 --- a/e2e-pw/src/oss/specs/groups/ima-vid.spec.ts +++ b/e2e-pw/src/oss/specs/groups/ima-vid.spec.ts @@ -8,10 +8,7 @@ import { getUniqueDatasetNameWithPrefix } from "src/oss/utils"; import { createBlankImage } from "src/shared/media-factory/image"; const NUM_VIDEOS = 2; -const FRAME_COLORS = { - 0: "#ff0000", - 1: "#00ff00", -}; +const FRAME_COLORS = ["#ff0000", "#00ff00"]; const NUM_FRAMES_PER_VIDEO = 150; const datasetName = getUniqueDatasetNameWithPrefix(`group-ima-vid`); diff --git a/e2e-pw/src/oss/specs/groups/modal-multi-pcd.spec.ts b/e2e-pw/src/oss/specs/groups/modal-multi-pcd.spec.ts new file mode 100644 index 0000000000..8d22d71662 --- /dev/null +++ b/e2e-pw/src/oss/specs/groups/modal-multi-pcd.spec.ts @@ -0,0 +1,54 @@ +import { test as base } from "src/oss/fixtures"; +import { GridPom } from "src/oss/poms/grid"; +import { ModalPom } from "src/oss/poms/modal"; +import { getUniqueDatasetNameWithPrefix } from "src/oss/utils"; + +const test = base.extend<{ grid: GridPom; modal: ModalPom }>({ + grid: async ({ page, eventUtils }, use) => { + await use(new GridPom(page, eventUtils)); + }, + modal: async ({ page, eventUtils }, use) => { + await use(new ModalPom(page, eventUtils)); + }, +}); + +const datasetName = getUniqueDatasetNameWithPrefix(`modal-multi-pcd`); + +test.beforeAll(async ({ fiftyoneLoader }) => { + await fiftyoneLoader.executePythonCode(` + import fiftyone.zoo as foz + + dataset = foz.load_zoo_dataset( + "quickstart-groups", dataset_name="${datasetName}", max_samples=3 + ) + dataset.persistent = True + dataset.group_slice = "pcd" + extra = dataset.first().copy() + extra.group.name = "extra" + dataset.add_sample(extra)`); +}); + +test.beforeEach(async ({ page, fiftyoneLoader }) => { + await fiftyoneLoader.waitUntilGridVisible(page, datasetName); +}); + +test.describe("multi-pcd", () => { + test("multi-pcd slice in modal", async ({ grid, modal }) => { + await grid.openFirstSample(); + await modal.group.toggleMedia("carousel"); + await modal.group.toggleMedia("viewer"); + await modal.clickOnLooker3d(); + + await modal.toggleLooker3dSlice("extra"); + + await modal.sidebar.assert.verifySidebarEntryText("pcd-group.name", "pcd"); + await modal.sidebar.assert.verifySidebarEntryText( + "extra-group.name", + "extra" + ); + + await modal.toggleLooker3dSlice("pcd"); + + await modal.sidebar.assert.verifySidebarEntryText("group.name", "extra"); + }); +}); 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 6503f61680..219f956107 100644 --- a/e2e-pw/src/oss/specs/smoke-tests/quickstart.spec.ts +++ b/e2e-pw/src/oss/specs/smoke-tests/quickstart.spec.ts @@ -34,10 +34,6 @@ test.describe("quickstart", () => { // test navigation await grid.openFirstSample(); await modal.waitForSampleLoadDomAttribute(); - - // test copy text - await modal.sidebar.getSidebarEntry("id").press(`Ctrl+KeyC`); - modal.assert.verifySampleNavigation("forward"); }); test("entry counts text when toPatches then groupedBy", async ({ grid }) => { diff --git a/e2e-pw/src/oss/specs/smoke-tests/saved-views.spec.ts b/e2e-pw/src/oss/specs/smoke-tests/saved-views.spec.ts index 5a1a3fa3e2..450fee5e4c 100644 --- a/e2e-pw/src/oss/specs/smoke-tests/saved-views.spec.ts +++ b/e2e-pw/src/oss/specs/smoke-tests/saved-views.spec.ts @@ -1,5 +1,5 @@ import { test as base, expect } from "src/oss/fixtures"; -import { Color, SavedViewsPom } from "src/oss/poms/saved-views"; +import { Color, SaveViewParams, SavedViewsPom } from "src/oss/poms/saved-views"; import { getUniqueDatasetNameWithPrefix } from "src/oss/utils"; const ColorList = [ @@ -14,20 +14,20 @@ const ColorList = [ "Purple", ]; -export const updatedView = { +export const updatedView: SaveViewParams = { name: "test updated", description: "test updated", color: "Yellow" as Color, }; -export const updatedView2 = { +export const updatedView2: SaveViewParams = { name: "test updated 2", description: "test updated 2", color: "Orange" as Color, slug: "test-updated-2", }; -const testView = { +const testView: SaveViewParams = { id: 0, name: "test", description: "description", @@ -36,7 +36,7 @@ const testView = { slug: "test", }; -const testView1 = { +const testView1: SaveViewParams = { id: 1, name: "test 1", description: "description ", @@ -45,7 +45,7 @@ const testView1 = { slug: "test-1", }; -const testView2 = { +const testView2: SaveViewParams = { id: 2, name: "test 2", description: "description 2", @@ -54,7 +54,7 @@ const testView2 = { slug: "test-2", }; -const datasetName = getUniqueDatasetNameWithPrefix("smoke-quickstart"); +const datasetName = getUniqueDatasetNameWithPrefix("quickstart-saved-views"); const test = base.extend<{ savedViews: SavedViewsPom }>({ savedViews: async ({ page }, use) => { @@ -64,9 +64,15 @@ const test = base.extend<{ savedViews: SavedViewsPom }>({ test.describe("saved views", () => { test.beforeAll(async ({ fiftyoneLoader }) => { - await fiftyoneLoader.loadZooDataset("quickstart", datasetName, { - max_samples: 5, - }); + await fiftyoneLoader.executePythonCode(` + import fiftyone as fo + + dataset_name = "${datasetName}" + dataset = fo.Dataset(name=dataset_name) + dataset.persistent = True + + dataset.add_sample(fo.Sample(filepath="image1.jpg")) + `); }); test.beforeEach(async ({ page, fiftyoneLoader, savedViews }) => { @@ -75,7 +81,7 @@ test.describe("saved views", () => { await deleteSavedView(savedViews, updatedView2.slug); }); - async function deleteSavedView(savedViews, slug: string) { + async function deleteSavedView(savedViews: SavedViewsPom, slug: string) { const hasUnsaved = savedViews.canClearView(); if (!hasUnsaved) { await savedViews.clearView(); @@ -90,10 +96,6 @@ test.describe("saved views", () => { } } - test("page has the correct title", async ({ page }) => { - await expect(page).toHaveTitle(/FiftyOne/); - }); - test("saved views selector exists", async ({ savedViews }) => { await expect(savedViews.selector()).toBeVisible(); }); @@ -198,7 +200,7 @@ test.describe("saved views", () => { await savedViews.clickCloseModal(); }); - test("searching through saved views works", async ({ savedViews }) => { + test.fixme("searching through saved views works", async ({ savedViews }) => { await savedViews.saveView(testView1); await savedViews.clearViewBtn().waitFor({ state: "visible" }); await savedViews.clearViewBtn().click(); @@ -214,6 +216,7 @@ test.describe("saved views", () => { await savedViews.assert.verifySearch("test 3", [], ["test-1", "test-2"]); await savedViews.assert.verifySearch("test", ["test-1", "test-2"], []); + await savedViews.openSelect(); await savedViews.deleteView("test-1"); await savedViews.selector().click(); await savedViews.deleteView("test-2"); diff --git a/e2e-pw/src/shared/event-utils/index.ts b/e2e-pw/src/shared/event-utils/index.ts index 40300dc801..7a7ce78d7d 100644 --- a/e2e-pw/src/shared/event-utils/index.ts +++ b/e2e-pw/src/shared/event-utils/index.ts @@ -17,6 +17,7 @@ export class EventUtils { ({ eventName_, exposedFunctionName_ }) => new Promise((resolve) => { const cb = (e: CustomEvent) => { + // @ts-expect-error - TS doesn't know that the function is exposed if (window[exposedFunctionName_](e)) { resolve(); document.removeEventListener(eventName_, cb); diff --git a/e2e-pw/tsconfig.json b/e2e-pw/tsconfig.json index 098a41be1e..524fe8a3ab 100644 --- a/e2e-pw/tsconfig.json +++ b/e2e-pw/tsconfig.json @@ -4,6 +4,7 @@ "moduleResolution": "node", "esModuleInterop": true, "module": "CommonJS", + "noImplicitAny": true, "lib": ["ES6", "dom", "dom.iterable"], "types": ["node"], "sourceMap": true, diff --git a/e2e-pw/yarn.lock b/e2e-pw/yarn.lock index 2a9e56a6c8..23393a2c6f 100644 --- a/e2e-pw/yarn.lock +++ b/e2e-pw/yarn.lock @@ -144,10 +144,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.55.0": - version "8.55.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.55.0.tgz#b721d52060f369aa259cf97392403cb9ce892ec6" - integrity sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA== +"@eslint/js@8.56.0": + version "8.56.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.56.0.tgz#ef20350fec605a7f7035a01764731b2de0f3782b" + integrity sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A== "@hapi/hoek@^9.0.0": version "9.3.0" @@ -470,12 +470,12 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@playwright/test@^1.40.1": - version "1.40.1" - resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.40.1.tgz#9e66322d97b1d74b9f8718bacab15080f24cde65" - integrity sha512-EaaawMTOeEItCRvfmkI9v6rBkF1svM8wjl/YPRrg2N2Wmp+4qJYkWtJsbew1szfKKDm6fPLy4YAanBhIlf9dWw== +"@playwright/test@^1.41.2": + version "1.41.2" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.41.2.tgz#bd9db40177f8fd442e16e14e0389d23751cdfc54" + integrity sha512-qQB9h7KbibJzrDpkXkYvsmiDJK14FULCCZgEcoe2AvFAS64oCirWTwzTlAYEbKaRxWs5TFesE1Na6izMv3HfGg== dependencies: - playwright "1.40.1" + playwright "1.41.2" "@rollup/rollup-android-arm-eabi@4.6.1": version "4.6.1" @@ -564,6 +564,11 @@ resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276" integrity sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A== +"@types/estree@^1.0.0": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== + "@types/fluent-ffmpeg@^2.1.24": version "2.1.24" resolved "https://registry.yarnpkg.com/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.24.tgz#f53c57700bc4360ac638554545c8da2c465434c1" @@ -600,16 +605,16 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@^6.14.0": - version "6.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.14.0.tgz#fc1ab5f23618ba590c87e8226ff07a760be3dd7b" - integrity sha512-1ZJBykBCXaSHG94vMMKmiHoL0MhNHKSVlcHVYZNw+BKxufhqQVTOawNpwwI1P5nIFZ/4jLVop0mcY6mJJDFNaw== +"@typescript-eslint/eslint-plugin@^6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz#30830c1ca81fd5f3c2714e524c4303e0194f9cd3" + integrity sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA== dependencies: "@eslint-community/regexpp" "^4.5.1" - "@typescript-eslint/scope-manager" "6.14.0" - "@typescript-eslint/type-utils" "6.14.0" - "@typescript-eslint/utils" "6.14.0" - "@typescript-eslint/visitor-keys" "6.14.0" + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/type-utils" "6.21.0" + "@typescript-eslint/utils" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" debug "^4.3.4" graphemer "^1.4.0" ignore "^5.2.4" @@ -617,72 +622,73 @@ semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/parser@^6.14.0": - version "6.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.14.0.tgz#a2d6a732e0d2b95c73f6a26ae7362877cc1b4212" - integrity sha512-QjToC14CKacd4Pa7JK4GeB/vHmWFJckec49FR4hmIRf97+KXole0T97xxu9IFiPxVQ1DBWrQ5wreLwAGwWAVQA== +"@typescript-eslint/parser@^6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b" + integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ== dependencies: - "@typescript-eslint/scope-manager" "6.14.0" - "@typescript-eslint/types" "6.14.0" - "@typescript-eslint/typescript-estree" "6.14.0" - "@typescript-eslint/visitor-keys" "6.14.0" + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/typescript-estree" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@6.14.0": - version "6.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.14.0.tgz#53d24363fdb5ee0d1d8cda4ed5e5321272ab3d48" - integrity sha512-VT7CFWHbZipPncAZtuALr9y3EuzY1b1t1AEkIq2bTXUPKw+pHoXflGNG5L+Gv6nKul1cz1VH8fz16IThIU0tdg== +"@typescript-eslint/scope-manager@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1" + integrity sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg== dependencies: - "@typescript-eslint/types" "6.14.0" - "@typescript-eslint/visitor-keys" "6.14.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" -"@typescript-eslint/type-utils@6.14.0": - version "6.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.14.0.tgz#ac9cb5ba0615c837f1a6b172feeb273d36e4f8af" - integrity sha512-x6OC9Q7HfYKqjnuNu5a7kffIYs3No30isapRBJl1iCHLitD8O0lFbRcVGiOcuyN837fqXzPZ1NS10maQzZMKqw== +"@typescript-eslint/type-utils@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz#6473281cfed4dacabe8004e8521cee0bd9d4c01e" + integrity sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag== dependencies: - "@typescript-eslint/typescript-estree" "6.14.0" - "@typescript-eslint/utils" "6.14.0" + "@typescript-eslint/typescript-estree" "6.21.0" + "@typescript-eslint/utils" "6.21.0" debug "^4.3.4" ts-api-utils "^1.0.1" -"@typescript-eslint/types@6.14.0": - version "6.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.14.0.tgz#935307f7a931016b7a5eb25d494ea3e1f613e929" - integrity sha512-uty9H2K4Xs8E47z3SnXEPRNDfsis8JO27amp2GNCnzGETEW3yTqEIVg5+AI7U276oGF/tw6ZA+UesxeQ104ceA== +"@typescript-eslint/types@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" + integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== -"@typescript-eslint/typescript-estree@6.14.0": - version "6.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.14.0.tgz#90c7ddd45cd22139adf3d4577580d04c9189ac13" - integrity sha512-yPkaLwK0yH2mZKFE/bXkPAkkFgOv15GJAUzgUVonAbv0Hr4PK/N2yaA/4XQbTZQdygiDkpt5DkxPELqHguNvyw== +"@typescript-eslint/typescript-estree@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46" + integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ== dependencies: - "@typescript-eslint/types" "6.14.0" - "@typescript-eslint/visitor-keys" "6.14.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" + minimatch "9.0.3" semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/utils@6.14.0": - version "6.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.14.0.tgz#856a9e274367d99ffbd39c48128b93a86c4261e3" - integrity sha512-XwRTnbvRr7Ey9a1NT6jqdKX8y/atWG+8fAIu3z73HSP8h06i3r/ClMhmaF/RGWGW1tHJEwij1uEg2GbEmPYvYg== +"@typescript-eslint/utils@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.21.0.tgz#4714e7a6b39e773c1c8e97ec587f520840cd8134" + integrity sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ== dependencies: "@eslint-community/eslint-utils" "^4.4.0" "@types/json-schema" "^7.0.12" "@types/semver" "^7.5.0" - "@typescript-eslint/scope-manager" "6.14.0" - "@typescript-eslint/types" "6.14.0" - "@typescript-eslint/typescript-estree" "6.14.0" + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/typescript-estree" "6.21.0" semver "^7.5.4" -"@typescript-eslint/visitor-keys@6.14.0": - version "6.14.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.14.0.tgz#1d1d486581819287de824a56c22f32543561138e" - integrity sha512-fB5cw6GRhJUz03MrROVuj5Zm/Q+XWlVdIsFj+Zb1Hvqouc8t+XP2H5y53QYU/MGtd2dPg6/vJJlhoX3xc2ehfw== +"@typescript-eslint/visitor-keys@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47" + integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A== dependencies: - "@typescript-eslint/types" "6.14.0" + "@typescript-eslint/types" "6.21.0" eslint-visitor-keys "^3.4.1" "@ungap/structured-clone@^1.2.0": @@ -690,46 +696,47 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@vitest/expect@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-1.0.4.tgz#2751018b6e527841043e046ff424304453a0a024" - integrity sha512-/NRN9N88qjg3dkhmFcCBwhn/Ie4h064pY3iv7WLRsDJW7dXnEgeoa8W9zy7gIPluhz6CkgqiB3HmpIXgmEY5dQ== +"@vitest/expect@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-1.2.2.tgz#39ea22e849bbf404b7e5272786551aa99e2663d0" + integrity sha512-3jpcdPAD7LwHUUiT2pZTj2U82I2Tcgg2oVPvKxhn6mDI2On6tfvPQTjAI4628GUGDZrCm4Zna9iQHm5cEexOAg== dependencies: - "@vitest/spy" "1.0.4" - "@vitest/utils" "1.0.4" + "@vitest/spy" "1.2.2" + "@vitest/utils" "1.2.2" chai "^4.3.10" -"@vitest/runner@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-1.0.4.tgz#c4dcb88c07f40b91293ff1331747ee58fad6d5e4" - integrity sha512-rhOQ9FZTEkV41JWXozFM8YgOqaG9zA7QXbhg5gy6mFOVqh4PcupirIJ+wN7QjeJt8S8nJRYuZH1OjJjsbxAXTQ== +"@vitest/runner@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-1.2.2.tgz#8b060a56ecf8b3d607b044d79f5f50d3cd9fee2f" + integrity sha512-JctG7QZ4LSDXr5CsUweFgcpEvrcxOV1Gft7uHrvkQ+fsAVylmWQvnaAr/HDp3LAH1fztGMQZugIheTWjaGzYIg== dependencies: - "@vitest/utils" "1.0.4" + "@vitest/utils" "1.2.2" p-limit "^5.0.0" pathe "^1.1.1" -"@vitest/snapshot@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-1.0.4.tgz#7020983b3963b473237fea08d347ea83b266b9bb" - integrity sha512-vkfXUrNyNRA/Gzsp2lpyJxh94vU2OHT1amoD6WuvUAA12n32xeVZQ0KjjQIf8F6u7bcq2A2k969fMVxEsxeKYA== +"@vitest/snapshot@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-1.2.2.tgz#f56fd575569774968f3eeba9382a166c26201042" + integrity sha512-SmGY4saEw1+bwE1th6S/cZmPxz/Q4JWsl7LvbQIky2tKE35US4gd0Mjzqfr84/4OD0tikGWaWdMja/nWL5NIPA== dependencies: magic-string "^0.30.5" pathe "^1.1.1" pretty-format "^29.7.0" -"@vitest/spy@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-1.0.4.tgz#e182c78fb9b1178ff789ad7eb4560ba6750e6e9b" - integrity sha512-9ojTFRL1AJVh0hvfzAQpm0QS6xIS+1HFIw94kl/1ucTfGCaj1LV/iuJU4Y6cdR03EzPDygxTHwE1JOm+5RCcvA== +"@vitest/spy@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-1.2.2.tgz#8fc2aeccb96cecbbdd192c643729bd5f97a01c86" + integrity sha512-k9Gcahssw8d7X3pSLq3e3XEu/0L78mUkCjivUqCQeXJm9clfXR/Td8+AP+VC1O6fKPIDLcHDTAmBOINVuv6+7g== dependencies: tinyspy "^2.2.0" -"@vitest/utils@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-1.0.4.tgz#6e673eaf87a2ff28a12688d17bdbb62cc22bf773" - integrity sha512-gsswWDXxtt0QvtK/y/LWukN7sGMYmnCcv1qv05CsY6cU/Y1zpGX1QuvLs+GO1inczpE6Owixeel3ShkjhYtGfA== +"@vitest/utils@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-1.2.2.tgz#94b5a1bd8745ac28cf220a99a8719efea1bcfc83" + integrity sha512-WKITBHLsBHlpjnDQahr+XK6RE7MiAsgrIkr0pGhQ9ygoxBfUeG0lUG5iLlzqjmKSlBv3+j5EGsriBzh+C3Tq9g== dependencies: diff-sequences "^29.6.3" + estree-walker "^3.0.3" loupe "^2.3.7" pretty-format "^29.7.0" @@ -738,10 +745,10 @@ acorn-jsx@^5.3.2: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn-walk@^8.3.0: - version "8.3.1" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.1.tgz#2f10f5b69329d90ae18c58bf1fa8fccd8b959a43" - integrity sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw== +acorn-walk@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.2.tgz#7703af9415f1b6db9315d6895503862e231d34aa" + integrity sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A== acorn@^8.10.0, acorn@^8.9.0: version "8.10.0" @@ -837,6 +844,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -976,10 +990,10 @@ dom-walk@^0.1.0: resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84" integrity sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w== -dotenv@^16.3.1: - version "16.3.1" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" - integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== +dotenv@^16.4.1: + version "16.4.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.1.tgz#1d9931f1d3e5d2959350d1250efab299561f7f11" + integrity sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ== esbuild@^0.19.3: version "0.19.8" @@ -1014,10 +1028,10 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -eslint-plugin-playwright@^0.20.0: - version "0.20.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-playwright/-/eslint-plugin-playwright-0.20.0.tgz#8b92cf77ab01807f8aa3d4429fdbd9dba7bb9311" - integrity sha512-JWTSwUyPPipSOm6AK8z78bQXtKRCykvhSGUewcmZuxstSZ5oGsykW2JaRXJQ2IIfzKJToCBeKD2ISc8Li8qVEQ== +eslint-plugin-playwright@^0.22.2: + version "0.22.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-playwright/-/eslint-plugin-playwright-0.22.2.tgz#c3a42672fc659ac671f4f097d2e498085e201dba" + integrity sha512-LtOB9myIX1O7HHqg9vtvBLjvXq1MXKuXIcD1nS+qZiMUJV6s9HBdilURAr9pIFc9kEelbVF54hOJ8pMxHvJP7g== dependencies: globals "^13.23.0" @@ -1034,15 +1048,15 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4 resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -eslint@^8.55.0: - version "8.55.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.55.0.tgz#078cb7b847d66f2c254ea1794fa395bf8e7e03f8" - integrity sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA== +eslint@^8.56.0: + version "8.56.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.56.0.tgz#4957ce8da409dc0809f99ab07a1b94832ab74b15" + integrity sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.6.1" "@eslint/eslintrc" "^2.1.4" - "@eslint/js" "8.55.0" + "@eslint/js" "8.56.0" "@humanwhocodes/config-array" "^0.11.13" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" @@ -1106,6 +1120,13 @@ estraverse@^5.1.0, estraverse@^5.2.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== +estree-walker@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" + integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== + dependencies: + "@types/estree" "^1.0.0" + esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" @@ -1595,6 +1616,13 @@ min-document@^2.19.0: dependencies: dom-walk "^0.1.0" +minimatch@9.0.3: + version "9.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== + dependencies: + brace-expansion "^2.0.1" + minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -1804,17 +1832,17 @@ pkg-types@^1.0.3: mlly "^1.2.0" pathe "^1.1.0" -playwright-core@1.40.1: - version "1.40.1" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.40.1.tgz#442d15e86866a87d90d07af528e0afabe4c75c05" - integrity sha512-+hkOycxPiV534c4HhpfX6yrlawqVUzITRKwHAmYfmsVreltEl6fAZJ3DPfLMOODw0H3s1Itd6MDCWmP1fl/QvQ== +playwright-core@1.41.2: + version "1.41.2" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.41.2.tgz#db22372c708926c697acc261f0ef8406606802d9" + integrity sha512-VaTvwCA4Y8kxEe+kfm2+uUUw5Lubf38RxF7FpBxLPmGe5sdNkSg5e3ChEigaGrX7qdqT3pt2m/98LiyvU2x6CA== -playwright@1.40.1: - version "1.40.1" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.40.1.tgz#a11bf8dca15be5a194851dbbf3df235b9f53d7ae" - integrity sha512-2eHI7IioIpQ0bS1Ovg/HszsN/XKNwEG1kbzSDDmADpclKc7CyqkHw7Mg2JCz/bbCxg25QUPcjksoMW7JcIFQmw== +playwright@1.41.2: + version "1.41.2" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.41.2.tgz#4e760b1c79f33d9129a8c65cc27953be6dd35042" + integrity sha512-v0bOa6H2GJChDL8pAeLa/LZC4feoAMbSQm1/jF/ySsWWoaNItvrMP7GEkvEEFyCTUYKMxjQKaTSg5up7nR6/8A== dependencies: - playwright-core "1.40.1" + playwright-core "1.41.2" optionalDependencies: fsevents "2.3.2" @@ -2072,10 +2100,10 @@ tinycolor2@^1.6.0: resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.6.0.tgz#f98007460169b0263b97072c5ae92484ce02d09e" integrity sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw== -tinypool@^0.8.1: - version "0.8.1" - resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.8.1.tgz#b6c4e4972ede3e3e5cda74a3da1679303d386b03" - integrity sha512-zBTCK0cCgRROxvs9c0CGK838sPkeokNGdQVUUwHAbynHFlmyJYj825f/oRs528HaIJ97lo0pLIlDUzwN+IorWg== +tinypool@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.8.2.tgz#84013b03dc69dacb322563a475d4c0a9be00f82a" + integrity sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ== tinyspy@^2.2.0: version "2.2.0" @@ -2173,10 +2201,10 @@ util-deprecate@^1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -vite-node@1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-1.0.4.tgz#36d6c49e3b5015967d883845561ed67abe6553cc" - integrity sha512-9xQQtHdsz5Qn8hqbV7UKqkm8YkJhzT/zr41Dmt5N7AlD8hJXw/Z7y0QiD5I8lnTthV9Rvcvi0QW7PI0Fq83ZPg== +vite-node@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-1.2.2.tgz#f6d329b06f9032130ae6eac1dc773f3663903c25" + integrity sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg== dependencies: cac "^6.7.14" debug "^4.3.4" @@ -2195,17 +2223,17 @@ vite@^5.0.0: optionalDependencies: fsevents "~2.3.3" -vitest@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/vitest/-/vitest-1.0.4.tgz#c4b39ba4fcba674499c90e28f4d8dd16fa1d4eb3" - integrity sha512-s1GQHp/UOeWEo4+aXDOeFBJwFzL6mjycbQwwKWX2QcYfh/7tIerS59hWQ20mxzupTJluA2SdwiBuWwQHH67ckg== - dependencies: - "@vitest/expect" "1.0.4" - "@vitest/runner" "1.0.4" - "@vitest/snapshot" "1.0.4" - "@vitest/spy" "1.0.4" - "@vitest/utils" "1.0.4" - acorn-walk "^8.3.0" +vitest@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-1.2.2.tgz#9e29ad2a74a5df553c30c5798c57a062d58ce299" + integrity sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw== + dependencies: + "@vitest/expect" "1.2.2" + "@vitest/runner" "1.2.2" + "@vitest/snapshot" "1.2.2" + "@vitest/spy" "1.2.2" + "@vitest/utils" "1.2.2" + acorn-walk "^8.3.2" cac "^6.7.14" chai "^4.3.10" debug "^4.3.4" @@ -2217,9 +2245,9 @@ vitest@^1.0.4: std-env "^3.5.0" strip-literal "^1.3.0" tinybench "^2.5.1" - tinypool "^0.8.1" + tinypool "^0.8.2" vite "^5.0.0" - vite-node "1.0.4" + vite-node "1.2.2" why-is-node-running "^2.2.2" wait-on@^7.2.0: diff --git a/fiftyone/core/odm/dataset.py b/fiftyone/core/odm/dataset.py index 88939ac5cd..d3764e5f1c 100644 --- a/fiftyone/core/odm/dataset.py +++ b/fiftyone/core/odm/dataset.py @@ -313,7 +313,7 @@ def to_dict(self, extended=False): @classmethod def from_dict(cls, d): d = dict(**d) - d["_id"] = ObjectId(d["id"]) + d["_id"] = ObjectId(d.get("id", None)) return super().from_dict(d) diff --git a/fiftyone/operators/executor.py b/fiftyone/operators/executor.py index 2794b0dad2..03041aafd6 100644 --- a/fiftyone/operators/executor.py +++ b/fiftyone/operators/executor.py @@ -491,6 +491,9 @@ def view(self): filters = self.request_params.get("filters", None) extended = self.request_params.get("extended", None) + if dataset is None: + return None + self._view = fosv.get_view( dataset, stages=stages, @@ -501,6 +504,30 @@ def view(self): return self._view + def target_view(self, param_name="view_target"): + """The target :class:`fiftyone.core.view.DatasetView` for the operator + being executed. + """ + target = self.params.get(param_name, None) + if target == "SELECTED_SAMPLES": + return self.view.select(self.selected) + if target == "DATASET": + return self.dataset + return self.view + + @property + def has_custom_view(self): + """Whether the operator has a custom view.""" + stages = self.request_params.get("view", None) + filters = self.request_params.get("filters", None) + extended = self.request_params.get("extended", None) + has_stages = stages is not None and stages != [] and stages != {} + has_filters = filters is not None and filters != [] and filters != {} + has_extended = ( + extended is not None and extended != [] and extended != {} + ) + return has_stages or has_filters or has_extended + @property def selected(self): """The list of selected sample IDs (if any).""" diff --git a/fiftyone/operators/types.py b/fiftyone/operators/types.py index 1bd800d180..c1d6c3c0fd 100644 --- a/fiftyone/operators/types.py +++ b/fiftyone/operators/types.py @@ -270,6 +270,47 @@ def clone(self): clone.properties = self.properties.copy() return clone + def view_target(self, ctx, name="view_target", view_type=None, **kwargs): + """Defines a view target property. + + Examples:: + + import fiftyone.operators.types as types + + # + # in resolve_input() + # + + inputs = types.Object() + + vt = inputs.view_target(ctx) + + # or add the property directly + # vt = types.ViewTargetProperty(ctx) + # inputs.add_property("view_target", vt) + + return types.Property(inputs) + + # + # in execute() + # + + target_view = ctx.target_view() + + Args: + ctx: the :class:`fiftyone.operators.ExecutionContext` + name: the name of the property + view_type (RadioGroup): the view type to use (RadioGroup, Dropdown, + etc.) + + Returns: + a :class:`ViewTargetProperty` + """ + view_type = view_type or RadioGroup + property = ViewTargetProperty(ctx, view_type) + self.add_property(name, property) + return property + def to_json(self): """Converts the object definition to JSON. @@ -683,9 +724,10 @@ class Choice(View): caption (None): a caption for the :class:`Choice` """ - def __init__(self, value, **kwargs): + def __init__(self, value, include=True, **kwargs): super().__init__(**kwargs) self.value = value + self.include = include def clone(self): """Clones the :class:`Choice`. @@ -723,7 +765,11 @@ class Choices(View): def __init__(self, **kwargs): super().__init__(**kwargs) - self.choices = kwargs.get("choices", []) + self._choices = kwargs.get("choices", []) + + @property + def choices(self): + return [choice for choice in self._choices if choice.include] def values(self): """Returns the choice values for this instance. @@ -743,12 +789,20 @@ def add_choice(self, value, **kwargs): the :class:`Choice` that was added """ choice = Choice(value, **kwargs) - self.choices.append(choice) + self.append(choice) return choice + def append(self, choice): + """Appends a :class:`Choice` to the list of choices. + + Args: + choice: a :class:`Choice` instance + """ + self._choices.append(choice) + def clone(self): clone = super().clone() - clone.choices = [choice.clone() for choice in self.choices] + clone._choices = [choice.clone() for choice in self.choices] return clone def to_json(self): @@ -1596,3 +1650,101 @@ class PromptView(View): def __init__(self, **kwargs): super().__init__(**kwargs) + + +class ViewTargetOptions(object): + """Represents the options for a :class:`ViewTargetProperty`. + + Attributes: + entire_dataset: a :class:`Choice` for the entire dataset + current_view: a :class:`Choice` for the current view + selected_samples: a :class:`Choice` for the selected samples + """ + + def __init__(self, choices_view, **kwargs): + super().__init__(**kwargs) + self.choices_view = choices_view + self.entire_dataset = Choice( + "DATASET", + label="Entire dataset", + description="Run on the entire dataset", + include=False, + ) + self.current_view = Choice( + "CURRENT_VIEW", + label="Current view", + description="Run on the current view", + include=False, + ) + self.selected_samples = Choice( + "SELECTED_SAMPLES", + label="Selected samples", + description="Run on the selected samples", + include=False, + ) + [ + choices_view.append(choice) + for choice in [ + self.entire_dataset, + self.current_view, + self.selected_samples, + ] + ] + + def values(self): + return self.choices_view.values() + + +class ViewTargetProperty(Property): + """Displays a view target selector. + + Examples:: + + import fiftyone.operators.types as types + + # in resolve_input + inputs = types.Object() + vt = inputs.view_target(ctx) + # or add the property directly + # vt = types.ViewTargetProperty(ctx) + # inputs.add_property("view_target", vt) + return types.Property(inputs) + + # in execute() + target_view = ctx.target_view() + + Attributes: + options: a :class:`ViewTargetOptions` instance + + Args: + ctx: the :class:`fiftyone.operators.ExecutionContext` + view_type (RadioGroup): the type of view to use (RadioGroup or Dropdown) + """ + + def __init__(self, ctx, view_type=RadioGroup, **kwargs): + has_custom_view = ctx.has_custom_view + has_selected = bool(ctx.selected) + default_target = "DATASET" + choice_view = view_type() + options = ViewTargetOptions(choice_view) + self._options = options + + if has_custom_view or has_selected: + options.entire_dataset.include = True + + if has_custom_view: + options.current_view.include = True + default_target = "CURRENT_VIEW" + + if has_selected: + options.selected_samples.include = True + default_target = "SELECTED_SAMPLES" + + type = Enum(options.values()) + super().__init__( + type, default=default_target, view=choice_view, **kwargs + ) + + @property + def options(self): + return self._options diff --git a/fiftyone/server/routes/frames.py b/fiftyone/server/routes/frames.py index 3a35f169ac..f3c008998b 100644 --- a/fiftyone/server/routes/frames.py +++ b/fiftyone/server/routes/frames.py @@ -15,6 +15,7 @@ import fiftyone.core.view as fov from fiftyone.server.decorators import route +import fiftyone.core.media as fom import fiftyone.server.view as fosv @@ -33,7 +34,7 @@ async def post(self, request: Request, data: dict): view = fosv.get_view(dataset, stages=stages, extended_stages=extended) view = fov.make_optimized_select_view(view, sample_id) - if group_slice is not None: + if view.media_type == fom.GROUP and group_slice is not None: view.group_slice = group_slice end_frame = min(num_frames + start_frame, frame_count) diff --git a/fiftyone/utils/labelbox.py b/fiftyone/utils/labelbox.py index fe0d527998..8cb2ac9dcf 100644 --- a/fiftyone/utils/labelbox.py +++ b/fiftyone/utils/labelbox.py @@ -151,7 +151,7 @@ def supports_keyframes(self): @property def supports_video_sample_fields(self): - return False # @todo resolve FiftyOne bug to allow this to be True + return True @property def requires_label_schema(self): @@ -585,7 +585,10 @@ def upload_samples(self, samples, anno_key, backend): project_name = config.project_name members = config.members classes_as_attrs = config.classes_as_attrs - is_video = samples.media_type == fomm.VIDEO + is_video = (samples.media_type == fomm.VIDEO) or ( + samples.media_type == fomm.GROUP + and samples.group_media_types[samples.group_slice] == fomm.VIDEO + ) for label_field, label_info in label_schema.items(): if label_info["existing_field"]: @@ -641,8 +644,13 @@ def download_annotations(self, results): project = self._client.get_project(project_id) labels_json = self._download_project_labels(project=project) - is_video = results._samples.media_type == fomm.VIDEO - + is_video = (results._samples.media_type == fomm.VIDEO) or ( + results._samples.media_type == fomm.GROUP + and results._samples.group_media_types[ + results._samples.group_slice + ] + == fomm.VIDEO + ) annotations = {} if classes_as_attrs: @@ -675,7 +683,7 @@ def download_annotations(self, results): video_d_list = self._get_video_labels(d["Label"]) frames = {} for label_d in video_d_list: - frame_number = label_d["frameNumber"] + frame_number = int(label_d["frameNumber"]) frame_id = frame_id_map[sample_id][frame_number] labels_dict = _parse_image_labels( label_d, frame_size, class_attr=class_attr @@ -693,20 +701,19 @@ def download_annotations(self, results): label_schema, ) - else: - labels_dict = _parse_image_labels( - d["Label"], frame_size, class_attr=class_attr - ) - if not classes_as_attrs: - labels_dict = self._process_label_fields( - label_schema, labels_dict - ) - annotations = self._add_labels_to_results( - annotations, - labels_dict, - sample_id, - label_schema, + labels_dict = _parse_image_labels( + d["Label"], frame_size, class_attr=class_attr + ) + if not classes_as_attrs: + labels_dict = self._process_label_fields( + label_schema, labels_dict ) + annotations = self._add_labels_to_results( + annotations, + labels_dict, + sample_id, + label_schema, + ) return annotations @@ -1013,6 +1020,9 @@ def _get_sample_metadata(self, project, sample_id): return metadata def _get_video_labels(self, label_dict): + if "frames" not in label_dict: + return {} + url = label_dict["frames"] headers = {"Authorization": "Bearer %s" % self._api_key} response = requests.get(url, headers=headers) @@ -2297,28 +2307,34 @@ def _parse_attributes(cd_list): attributes = {} for cd in cd_list: - name = cd["value"] - if "answer" in cd: - answer = cd["answer"] - if isinstance(answer, list): - # Dropdown - answers = [_parse_attribute(a["value"]) for a in answer] - if len(answers) == 1: - answers = answers[0] - - attributes[name] = answers - - elif isinstance(answer, dict): - # Radio question - attributes[name] = _parse_attribute(answer["value"]) - else: - # Free text - attributes[name] = _parse_attribute(answer) + if isinstance(cd, list): + attributes.update(_parse_attributes(cd)) - if "answers" in cd: - # Checklist - answer = cd["answers"] - attributes[name] = [_parse_attribute(a["value"]) for a in answer] + else: + name = cd["value"] + if "answer" in cd: + answer = cd["answer"] + if isinstance(answer, list): + # Dropdown + answers = [_parse_attribute(a["value"]) for a in answer] + if len(answers) == 1: + answers = answers[0] + + attributes[name] = answers + + elif isinstance(answer, dict): + # Radio question + attributes[name] = _parse_attribute(answer["value"]) + else: + # Free text + attributes[name] = _parse_attribute(answer) + + if "answers" in cd: + # Checklist + answer = cd["answers"] + attributes[name] = [ + _parse_attribute(a["value"]) for a in answer + ] return attributes diff --git a/fiftyone/utils/transformers.py b/fiftyone/utils/transformers.py index a2a6a70991..b292cbe07e 100644 --- a/fiftyone/utils/transformers.py +++ b/fiftyone/utils/transformers.py @@ -27,6 +27,7 @@ DEFAULT_CLASSIFICATION_PATH = "google/vit-base-patch16-224" DEFAULT_DETECTION_PATH = "hustvl/yolos-tiny" DEFAULT_SEGMENTATION_PATH = "nvidia/segformer-b0-finetuned-ade-512-512" +DEFAULT_DEPTH_ESTIMATION_PATH = "Intel/dpt-hybrid-midas" DEFAULT_ZERO_SHOT_CLASSIFICATION_PATH = "openai/clip-vit-large-patch14" DEFAULT_ZERO_SHOT_DETECTION_PATH = "google/owlvit-base-patch32" @@ -38,9 +39,9 @@ def convert_transformers_model(model, task=None): Args: model: a ``transformers`` model task (None): the task of the model. Supported values are - ``"image-classification"``, ``"object-detection"``, and - ``"semantic-segmentation"``. If not specified, the task is - automatically inferred from the model + ``"image-classification"``, ``"object-detection"``, + ``"semantic-segmentation"``, and ``"depth-estimation"``. + If not specified, the task is automatically inferred from the model Returns: a :class:`fiftyone.core.models.Model` @@ -60,6 +61,8 @@ def convert_transformers_model(model, task=None): return _convert_transformer_for_object_detection(model) elif model_type == "semantic-segmentation": return _convert_transformer_for_semantic_segmentation(model) + elif model_type == "depth-estimation": + return _convert_transformer_for_depth_estimation(model) elif model_type == "base-model": return _convert_transformer_base_model(model) else: @@ -89,6 +92,11 @@ def _convert_transformer_for_semantic_segmentation(model): return FiftyOneTransformerForSemanticSegmentation(config) +def _convert_transformer_for_depth_estimation(model): + config = FiftyOneTransformerConfig({"model": model}) + return FiftyOneTransformerForDepthEstimation(config) + + def _convert_zero_shot_transformer_for_image_classification(model): config = FiftyOneZeroShotTransformerConfig({"model": model}) return FiftyOneZeroShotTransformerForImageClassification(config) @@ -122,6 +130,7 @@ def get_model_type(model, task=None): "image-classification", "object-detection", "semantic-segmentation", + "depth-estimation", ) if task is not None and task not in supported_tasks: raise ValueError( @@ -142,6 +151,8 @@ def get_model_type(model, task=None): task = "object-detection" elif _is_transformer_for_semantic_segmentation(model): task = "semantic-segmentation" + elif _is_transformer_for_depth_estimation(model): + task = "depth-estimation" elif _is_transformer_base_model(model): task = "base-model" else: @@ -209,6 +220,31 @@ def _create_segmentation(mask): return fol.Segmentation(mask=mask) +def to_heatmap(results): + """Converts the Transformers depth estimation results to FiftyOne format. + + Args: + results: Transformers depth estimation results + + Returns: + a single or list of :class:`fiftyone.core.labels.Heatmap` + """ + + if len(results.shape) == 2: + return _create_heatmap(results) + + if len(results) == 1: + return _create_heatmap(results[0]) + + return [_create_heatmap(results[i]) for i in range(len(results))] + + +def _create_heatmap(heatmap): + ## normalize the heatmap + heatmap /= np.max(heatmap) + return fol.Heatmap(map=heatmap) + + def to_detections(results, id2label, image_sizes): """Converts the Transformers detection results to FiftyOne format. @@ -853,6 +889,61 @@ def predict_all(self, args): return self._predict(inputs, target_sizes) +class FiftyOneTransformerForDepthEstimationConfig(FiftyOneTransformerConfig): + """Configuration for a :class:`FiftyOneTransformerForDepthEstimation`. + + Args: + model (None): a ``transformers`` model + name_or_path (None): the name or path to a checkpoint file to load + """ + + def __init__(self, d): + super().__init__(d) + if self.model is None and self.name_or_path is None: + self.name_or_path = DEFAULT_DEPTH_ESTIMATION_PATH + + +class FiftyOneTransformerForDepthEstimation(FiftyOneTransformer): + """FiftyOne wrapper around a ``transformers`` model for depth estimation. + + Args: + config: a `FiftyOneTransformerConfig` + """ + + def _load_model(self, config): + if config.model is not None: + return config.model + + return transformers.AutoModelForDepthEstimation.from_pretrained( + config.name_or_path + ) + + def _predict(self, inputs, target_sizes): + with torch.no_grad(): + outputs = self.model(**inputs) + + predicted_depth = outputs.predicted_depth + prediction = torch.nn.functional.interpolate( + predicted_depth.unsqueeze(1), + size=target_sizes[0], + mode="bicubic", + align_corners=False, + ) + prediction = prediction.squeeze(1).cpu().numpy() + + return to_heatmap(prediction) + + def predict(self, arg): + target_sizes = [arg.shape[:-1][::-1]] + inputs = self.image_processor(arg, return_tensors="pt") + return self._predict(inputs, target_sizes) + + def predict_all(self, args): + target_sizes = [i.shape[:-1][::-1] for i in args] + inputs = self.image_processor(args, return_tensors="pt") + return self._predict(inputs, target_sizes) + + def _has_text_and_image_features(model): return hasattr(model.base_model, "get_image_features") and hasattr( model.base_model, "get_text_features" @@ -906,6 +997,10 @@ def _is_transformer_for_semantic_segmentation(model): return "For" in ms and "Segmentation" in ms +def _is_transformer_for_depth_estimation(model): + return "ForDepthEstimation" in _get_model_type_string(model) + + def _is_transformer_base_model(model): model_type = _get_model_type_string(model) return "Model" in model_type and "For" not in model_type diff --git a/fiftyone/zoo/models/manifest-torch.json b/fiftyone/zoo/models/manifest-torch.json index dfa099ea17..3b5beafdd8 100644 --- a/fiftyone/zoo/models/manifest-torch.json +++ b/fiftyone/zoo/models/manifest-torch.json @@ -1984,6 +1984,29 @@ "tags": ["segmentation", "torch", "transformers"], "date_added": "2024-01-17 14:25:51" }, + { + "base_name": "depth-estimation-transformer-torch", + "base_filename": null, + "version": null, + "description": "Hugging Face Transformers model for monocular depth estimation", + "source": "https://huggingface.co/docs/transformers/tasks/monocular_depth_estimation", + "size_bytes": null, + "default_deployment_config_dict": { + "type": "fiftyone.utils.transformers.FiftyOneTransformerForDepthEstimation", + "config": {} + }, + "requirements": { + "packages": ["torch", "torchvision", "transformers"], + "cpu": { + "support": true + }, + "gpu": { + "support": true + } + }, + "tags": ["depth", "torch", "transformers"], + "date_added": "2024-02-06 14:25:51" + }, { "base_name": "zero-shot-classification-transformer-torch", "base_filename": null, diff --git a/package/desktop/setup.py b/package/desktop/setup.py index b96d5d8003..c78327e285 100644 --- a/package/desktop/setup.py +++ b/package/desktop/setup.py @@ -16,7 +16,7 @@ import shutil -VERSION = "0.33.3" +VERSION = "0.33.4" def get_version(): diff --git a/setup.py b/setup.py index 0e3ab89ed3..bb59bf00d3 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ from setuptools import setup, find_packages -VERSION = "0.23.4" +VERSION = "0.23.5" def get_version(): diff --git a/tests/unittests/odm_tests.py b/tests/unittests/odm_tests.py new file mode 100644 index 0000000000..a85f241331 --- /dev/null +++ b/tests/unittests/odm_tests.py @@ -0,0 +1,21 @@ +""" +FiftyOne odm unit tests. + +| Copyright 2017-2024, Voxel51, Inc. +| `voxel51.com `_ +| +""" +import unittest + +from bson import ObjectId + +import fiftyone as fo + + +class ColorSchemeTests(unittest.TestCase): + def test_color_scheme_serialization(self): + color_scheme = fo.ColorScheme.from_dict({}) + self.assertIsInstance(color_scheme.id, str) + + d = fo.ColorScheme().to_dict() + self.assertIsInstance(d["id"], str)