diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 131b01c808..2073ef0a61 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -7,6 +7,7 @@ on: - develop - feat/* - main + - py-panels-develop - release/v[0-9]+.[0-9]+.[0-9]+ jobs: diff --git a/app/packages/app/package.json b/app/packages/app/package.json index 6f1676f060..125188f23a 100644 --- a/app/packages/app/package.json +++ b/app/packages/app/package.json @@ -13,6 +13,7 @@ "copy-to-desktop": "rm -rf ../desktop/dist/ && cp -r ./dist ../desktop/dist" }, "dependencies": { + "@fiftyone/analytics": "*", "@fiftyone/components": "*", "@fiftyone/core": "*", "@fiftyone/relay": "*", diff --git a/app/packages/app/src/components/Analytics.tsx b/app/packages/app/src/components/Analytics.tsx new file mode 100644 index 0000000000..8e65b143b2 --- /dev/null +++ b/app/packages/app/src/components/Analytics.tsx @@ -0,0 +1,46 @@ +import { isElectron } from "@fiftyone/utilities"; +import React, { useCallback } from "react"; +import ReactGA from "react-ga4"; +import { graphql, useFragment } from "react-relay"; +import gaConfig from "../ga"; +import AnalyticsConsent from "./AnalyticsConsent"; +import type { NavGA$data, NavGA$key } from "./__generated__/NavGA.graphql"; + +const useCallGA = (info: NavGA$data) => { + return useCallback(() => { + const dev = info.dev; + const buildType = dev ? "dev" : "prod"; + ReactGA.initialize(gaConfig.app_ids[buildType], { + testMode: false, + gaOptions: { + storage: "none", + cookieDomain: "none", + clientId: info.uid, + page_location: "omitted", + page_path: "omitted", + kind: isElectron() ? "Desktop" : "Web", + version: info.version, + context: info.context, + checkProtocolTask: null, // disable check, allow file:// URLs + }, + }); + }, [info]); +}; + +export default function Analytics({ fragment }: { fragment: NavGA$key }) { + const info = useFragment( + graphql` + fragment Analytics on Query { + context + dev + doNotTrack + uid + version + } + `, + fragment + ); + const callGA = useCallGA(info); + + return ; +} diff --git a/app/packages/app/src/components/AnalyticsConsent.tsx b/app/packages/app/src/components/AnalyticsConsent.tsx new file mode 100644 index 0000000000..9d5b04d6d3 --- /dev/null +++ b/app/packages/app/src/components/AnalyticsConsent.tsx @@ -0,0 +1,117 @@ +import { DEFAULT_WRITE_KEYS, useAnalyticsInfo } from "@fiftyone/analytics"; +import { Button } from "@fiftyone/components"; +import { Box, Grid, Link, Typography } from "@mui/material"; +import React, { useCallback, useEffect, useState } from "react"; +import type { NavGA$data } from "./__generated__/NavGA.graphql"; + +const FIFTYONE_DO_NOT_TRACK_LS = "fiftyone-do-not-track"; + +function useAnalyticsConsent(disabled?: boolean) { + const [ready, setReady] = useState(false); + const [show, setShow] = useState(false); + const doNotTrack = window.localStorage.getItem(FIFTYONE_DO_NOT_TRACK_LS); + useEffect(() => { + if (disabled || doNotTrack === "true" || doNotTrack === "false") { + setShow(false); + setReady(true); + } else { + setShow(true); + } + }, [disabled, doNotTrack]); + + const handleDisable = useCallback(() => { + window.localStorage.setItem(FIFTYONE_DO_NOT_TRACK_LS, "true"); + setShow(false); + setReady(true); + }, []); + + const handleEnable = useCallback(() => { + window.localStorage.setItem(FIFTYONE_DO_NOT_TRACK_LS, "false"); + setReady(true); + setShow(false); + }, []); + + return { + doNotTrack: doNotTrack === "true" || disabled, + handleDisable, + handleEnable, + ready, + show, + }; +} + +export default function AnalyticsConsent({ + callGA, + info, +}: { + callGA: () => void; + info: NavGA$data; +}) { + const [_, setAnalyticsInfo] = useAnalyticsInfo(); + + const { doNotTrack, handleDisable, handleEnable, ready, show } = + useAnalyticsConsent(info.doNotTrack); + + useEffect(() => { + if (!ready) { + return; + } + const buildType = info.dev ? "dev" : "prod"; + const writeKey = DEFAULT_WRITE_KEYS[buildType]; + setAnalyticsInfo({ + userId: info.uid, + userGroup: "fiftyone-oss", + writeKey, + doNotTrack: doNotTrack, + debug: info.dev, + }); + !doNotTrack && callGA(); + }, [callGA, doNotTrack, info, ready, setAnalyticsInfo]); + + if (!show) { + return null; + } + + return ( + + `1px solid ${theme.palette.divider}`} + backgroundColor="background.paper" + > + + + Help us improve FiftyOne + + + We use cookies to understand how FiftyOne is used and to improve the + product. You can help us by enabling analytics. + + + + + Disable + + + + + + + + + + ); +} + +// a component that pins the content to the bottom of the screen, floating +function PinBottom({ children }: React.PropsWithChildren) { + return ( + + {children} + + ); +} diff --git a/app/packages/app/src/components/Nav.tsx b/app/packages/app/src/components/Nav.tsx index d78002bc01..f187ce3399 100644 --- a/app/packages/app/src/components/Nav.tsx +++ b/app/packages/app/src/components/Nav.tsx @@ -9,26 +9,18 @@ import { import { ViewBar } from "@fiftyone/core"; import * as fos from "@fiftyone/state"; import { useRefresh } from "@fiftyone/state"; -import { isElectron } from "@fiftyone/utilities"; import { DarkMode, LightMode } from "@mui/icons-material"; import { useColorScheme } from "@mui/material"; -import React, { Suspense, useEffect, useMemo } from "react"; -import ReactGA from "react-ga4"; +import React, { Suspense, useMemo } from "react"; import { useFragment, usePaginationFragment } from "react-relay"; import { useDebounce } from "react-use"; -import { - useRecoilState, - useRecoilValue, - useResetRecoilState, - useSetRecoilState, -} from "recoil"; +import { useRecoilValue, useSetRecoilState } from "recoil"; import { graphql } from "relay-runtime"; -import gaConfig from "../ga"; +import Analytics from "./Analytics"; import DatasetSelector from "./DatasetSelector"; import Teams from "./Teams"; import { NavDatasets$key } from "./__generated__/NavDatasets.graphql"; import { NavFragment$key } from "./__generated__/NavFragment.graphql"; -import { DEFAULT_WRITE_KEYS, useAnalyticsInfo } from "@fiftyone/analytics"; const getUseSearch = (fragment: NavDatasets$key) => { return (search: string) => { @@ -69,107 +61,64 @@ const getUseSearch = (fragment: NavDatasets$key) => { }; }; -export const useGA = (info) => { - useEffect(() => { - if (!info || info.doNotTrack) { - return; - } - const dev = info.dev; - const buildType = dev ? "dev" : "prod"; - ReactGA.initialize(gaConfig.app_ids[buildType], { - testMode: false, - gaOptions: { - storage: "none", - cookieDomain: "none", - clientId: info.uid, - page_location: "omitted", - page_path: "omitted", - kind: isElectron() ? "Desktop" : "Web", - version: info.version, - context: info.context, - checkProtocolTask: null, // disable check, allow file:// URLs - }, - }); - }, [info]); -}; - -const Nav: React.FC<{ - fragment: NavFragment$key; - hasDataset: boolean; -}> = ({ fragment, hasDataset }) => { +const Nav: React.FC< + React.PropsWithChildren<{ + fragment: NavFragment$key; + hasDataset: boolean; + }> +> = ({ children, fragment, hasDataset }) => { const data = useFragment( graphql` fragment NavFragment on Query { + ...Analytics ...NavDatasets - ...NavGA } `, fragment ); - const info = useFragment( - graphql` - fragment NavGA on Query { - context - dev - doNotTrack - uid - version - } - `, - data - ); - const [analyticsInfo, setAnalyticsInfo] = useAnalyticsInfo(); - useEffect(() => { - const buildType = info.dev ? "dev" : "prod"; - const writeKey = DEFAULT_WRITE_KEYS[buildType]; - setAnalyticsInfo({ - userId: info.uid, - userGroup: "fiftyone-oss", - writeKey, - doNotTrack: info.doNotTrack, - debug: info.dev, - }); - }, [info, setAnalyticsInfo]); - useGA(info); const useSearch = getUseSearch(data); const refresh = useRefresh(); const { mode, setMode } = useColorScheme(); - const [_, setTheme] = useRecoilState(fos.theme); + const setTheme = useSetRecoilState(fos.theme); return ( -
} - > - {hasDataset && ( - }> - - - )} - {!hasDataset &&
} -
- - { - const nextMode = mode === "dark" ? "light" : "dark"; - setMode(nextMode); - setTheme(nextMode); - }} - sx={{ - color: (theme) => theme.palette.text.secondary, - pr: 0, - }} - > - {mode === "dark" ? : } - - - - -
-
+ <> +
} + > + {hasDataset && ( + }> + + + )} + {!hasDataset &&
} +
+ + { + const nextMode = mode === "dark" ? "light" : "dark"; + setMode(nextMode); + setTheme(nextMode); + }} + sx={{ + color: (theme) => theme.palette.text.secondary, + pr: 0, + }} + > + {mode === "dark" ? : } + + + + +
+
+ {children} + + ); }; diff --git a/app/packages/app/src/components/__generated__/NavGA.graphql.ts b/app/packages/app/src/components/__generated__/Analytics.graphql.ts similarity index 77% rename from app/packages/app/src/components/__generated__/NavGA.graphql.ts rename to app/packages/app/src/components/__generated__/Analytics.graphql.ts index 43b77f3e47..c103f01e1d 100644 --- a/app/packages/app/src/components/__generated__/NavGA.graphql.ts +++ b/app/packages/app/src/components/__generated__/Analytics.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<94f4e2440a2b7380f82dfe4c09c00930>> + * @generated SignedSource<<814914ffd53575969ca480cdc6f3d1f0>> * @lightSyntaxTransform * @nogrep */ @@ -10,24 +10,24 @@ import { Fragment, ReaderFragment } from 'relay-runtime'; import { FragmentRefs } from "relay-runtime"; -export type NavGA$data = { +export type Analytics$data = { readonly context: string; readonly dev: boolean; readonly doNotTrack: boolean; readonly uid: string; readonly version: string; - readonly " $fragmentType": "NavGA"; + readonly " $fragmentType": "Analytics"; }; -export type NavGA$key = { - readonly " $data"?: NavGA$data; - readonly " $fragmentSpreads": FragmentRefs<"NavGA">; +export type Analytics$key = { + readonly " $data"?: Analytics$data; + readonly " $fragmentSpreads": FragmentRefs<"Analytics">; }; const node: ReaderFragment = { "argumentDefinitions": [], "kind": "Fragment", "metadata": null, - "name": "NavGA", + "name": "Analytics", "selections": [ { "alias": null, @@ -69,6 +69,6 @@ const node: ReaderFragment = { "abstractKey": null }; -(node as any).hash = "a2d13e827ff06e46baffc9244d708b0a"; +(node as any).hash = "042d0c5e3b5c588fc852e8a26d260126"; export default node; diff --git a/app/packages/app/src/components/__generated__/NavFragment.graphql.ts b/app/packages/app/src/components/__generated__/NavFragment.graphql.ts index fa06cdfbf8..cf49310146 100644 --- a/app/packages/app/src/components/__generated__/NavFragment.graphql.ts +++ b/app/packages/app/src/components/__generated__/NavFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<85bc3d6372c6f08bcdf0a2533aae4d98>> + * @generated SignedSource<<46385c140146f2317005e105dd92f070>> * @lightSyntaxTransform * @nogrep */ @@ -11,7 +11,7 @@ import { Fragment, ReaderFragment } from 'relay-runtime'; import { FragmentRefs } from "relay-runtime"; export type NavFragment$data = { - readonly " $fragmentSpreads": FragmentRefs<"NavDatasets" | "NavGA">; + readonly " $fragmentSpreads": FragmentRefs<"Analytics" | "NavDatasets">; readonly " $fragmentType": "NavFragment"; }; export type NavFragment$key = { @@ -28,18 +28,18 @@ const node: ReaderFragment = { { "args": null, "kind": "FragmentSpread", - "name": "NavDatasets" + "name": "Analytics" }, { "args": null, "kind": "FragmentSpread", - "name": "NavGA" + "name": "NavDatasets" } ], "type": "Query", "abstractKey": null }; -(node as any).hash = "f8b963593ae22123acdf5393b9a8a274"; +(node as any).hash = "b4c1e5cfb810c869d7f48d036fc48cad"; export default node; diff --git a/app/packages/app/src/pages/IndexPage.tsx b/app/packages/app/src/pages/IndexPage.tsx index 1bf331dc13..79da2bbd64 100644 --- a/app/packages/app/src/pages/IndexPage.tsx +++ b/app/packages/app/src/pages/IndexPage.tsx @@ -27,15 +27,14 @@ const IndexPage: Route = ({ prepared }) => { const totalDatasets = queryRef.allDatasets; return ( - <> -