diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..23c4cb3b50 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +--- +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/build-db.yml b/.github/workflows/build-db.yml index d252c4fd7f..e85b5dfac8 100644 --- a/.github/workflows/build-db.yml +++ b/.github/workflows/build-db.yml @@ -26,7 +26,7 @@ jobs: - name: Clone fiftyone uses: actions/checkout@v4 - name: Set up Python 3.9 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.9 - name: Install dependencies @@ -65,14 +65,14 @@ jobs: cd package/db python -Im build --sdist - name: Upload - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: ${{ matrix.platform == 'sdist' }} with: name: dist-${{ matrix.platform }} path: package/db/dist/*.tar.gz - name: Upload wheel if: ${{ matrix.platform != 'sdist' }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: dist-${{ matrix.platform }} path: package/db/dist/*.whl @@ -86,7 +86,7 @@ jobs: - name: Clone fiftyone uses: actions/checkout@v4 - name: Download fiftyone-db - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: dist-sdist path: downloads @@ -107,7 +107,7 @@ jobs: if: startsWith(github.ref, 'refs/tags/db-v') steps: - name: Download - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: path: downloads - name: Install dependencies diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 2a85292e4d..25cf3c178c 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -28,7 +28,7 @@ jobs: with: submodules: true - name: Set up Python 3.8 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 - name: Install dependencies @@ -36,7 +36,7 @@ jobs: pip install --upgrade pip setuptools wheel build - name: Cache Node Modules id: node-cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | app/node_modules @@ -72,7 +72,7 @@ jobs: working-directory: package/desktop run: RELEASE_DIR=${PWD}/../../app/packages/desktop/release python -Im build -C="--build-option=--plat-name=${{ matrix.platform }}" - name: Upload wheel - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: wheel-${{ matrix.platform }} path: package/desktop/dist/*.whl @@ -83,7 +83,7 @@ jobs: if: startsWith(github.ref, 'refs/tags/desktop-v') steps: - name: Download wheels - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: path: downloads - name: Install dependencies diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index f6e1e60462..5b51249a9e 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -54,7 +54,7 @@ jobs: token: ${{ secrets.TEAMS_GITHUB_PAT }} ref: main - name: Set up Python 3.8 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 - name: Install pip dependencies @@ -77,7 +77,7 @@ jobs: pip install . - name: Cache Node Modules id: node-cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | app/node_modules @@ -90,7 +90,7 @@ jobs: run: | ./docs/generate_docs.bash -t fiftyone-teams - name: Upload docs - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: docs path: docs/build/html/ @@ -101,15 +101,15 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Download docs - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: docs path: docs-download/ - name: Authorize gcloud - uses: google-github-actions/auth@v1 + uses: google-github-actions/auth@v2 with: credentials_json: "${{ secrets.DOCS_GCP_CREDENTIALS }}" - name: Set up gcloud - uses: google-github-actions/setup-gcloud@v1 + uses: google-github-actions/setup-gcloud@v2 - name: publish run: gsutil -m rsync -dR docs-download gs://docs.voxel51.com diff --git a/.github/workflows/build-graphql.yml b/.github/workflows/build-graphql.yml index 6e0584da46..fbb8e0f537 100644 --- a/.github/workflows/build-graphql.yml +++ b/.github/workflows/build-graphql.yml @@ -16,7 +16,7 @@ jobs: - name: Clone fiftyone uses: actions/checkout@v4 - name: Set up Python 3.9 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.9 - name: Install dependencies @@ -34,7 +34,7 @@ jobs: cd package/graphql python -Im build - name: Upload wheel(s) - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: dist path: package/graphql/dist/* @@ -45,7 +45,7 @@ jobs: if: startsWith(github.ref, 'refs/tags/db-v') steps: - name: Download wheels - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: path: downloads - name: Install dependencies diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c8550ec15d..be0656cb7d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,7 +9,7 @@ jobs: changes: ${{ steps.filter.outputs.changes }} steps: - uses: actions/checkout@v4 - - uses: dorny/paths-filter@v2 + - uses: dorny/paths-filter@v3 id: filter with: filters: | @@ -27,7 +27,7 @@ jobs: with: submodules: true - name: Set up Python 3.8 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 - name: Install dependencies @@ -35,7 +35,7 @@ jobs: pip install --upgrade pip setuptools wheel build - name: Cache Node Modules id: node-cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | app/node_modules @@ -56,7 +56,7 @@ jobs: - name: Build python run: make python -o app - name: Upload dist - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: dist path: dist/ diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 722386ddc4..3b4ce08885 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -23,12 +23,12 @@ jobs: submodules: true - name: Setup node 18 - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 - name: Start MongoDB - uses: supercharge/mongodb-github-action@1.10.0 + uses: supercharge/mongodb-github-action@1.11.0 with: mongodb-version: latest @@ -42,7 +42,7 @@ jobs: - name: Cache Node Modules id: app-node-cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | app/node_modules @@ -60,7 +60,6 @@ jobs: - name: Install fiftyone run: | pip install . - pip install fiftyone-db-ubuntu2204 - name: Configure id: test_config @@ -69,11 +68,11 @@ jobs: python tests/utils/github_actions_flags.py - name: FFmpeg - uses: FedericoCarboni/setup-ffmpeg@v2 + uses: FedericoCarboni/setup-ffmpeg@v3 - name: Cache E2E Node Modules id: e2e-node-cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | e2e-pw/node_modules @@ -91,7 +90,7 @@ jobs: working-directory: e2e-pw - name: Cache playwright browser - uses: actions/cache@v3 + uses: actions/cache@v4 id: playwright-browser-cache with: path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }} @@ -113,7 +112,7 @@ jobs: - name: Upload test report if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: playwright-report path: e2e-pw/playwright-report/ diff --git a/.github/workflows/lint-app.yml b/.github/workflows/lint-app.yml new file mode 100644 index 0000000000..0bd1e07346 --- /dev/null +++ b/.github/workflows/lint-app.yml @@ -0,0 +1,47 @@ +name: Lint App + +on: workflow_call + +jobs: + eslint: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - uses: actions/checkout@v4 + - uses: dorny/paths-filter@v3 + id: changes + with: + filters: | + changes: + - 'app/**' + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "16" + + - name: Cache Node Modules + id: node-cache + uses: actions/cache@v4 + with: + path: | + app/node_modules + app/.yarn/cache + key: node-modules-${{ hashFiles('app/yarn.lock') }} + + - name: Install Dependencies + if: steps.node-cache.outputs.cache-hit != 'true' + run: cd app && yarn install + + - name: Lint ESLint packages + run: | + cd app + ESLINT_PACKAGES=$(grep -v '^#' ./eslint-packages.txt | xargs) + yarn eslint $ESLINT_PACKAGES + + - name: Lint Biome Packages + run: | + cd app + yarn check diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index f6e915fbdd..bee364d1c9 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -16,11 +16,14 @@ jobs: e2e: uses: ./.github/workflows/e2e.yml + lint: + uses: ./.github/workflows/lint-app.yml + teams: runs-on: ubuntu-latest - if: github.base_ref == 'develop' + if: false && github.base_ref == 'develop' # temporarily disabled steps: - - uses: convictional/trigger-workflow-and-wait@v1.6.1 + - uses: convictional/trigger-workflow-and-wait@v1.6.5 with: owner: voxel51 repo: fiftyone-teams @@ -29,7 +32,12 @@ jobs: workflow_file_name: merge-oss.yml ref: develop wait_interval: 20 - client_payload: '{ "branch": "${{ github.head_ref || github.ref_name }}" }' + client_payload: | + { + "author": "${{ github.event.pull_request.user.login }}", + "branch": "${{ github.head_ref || github.ref_name }}", + "pr": ${{ github.event.pull_request.number }} + } propagate_failure: true trigger_workflow: true wait_workflow: true @@ -39,7 +47,10 @@ jobs: all-tests: runs-on: ubuntu-latest - needs: [build, test] + needs: [build, lint, test] if: always() steps: - - run: sh -c ${{ needs.build.result == 'success' && needs.test.result == 'success' }} + - run: sh -c ${{ + needs.build.result == 'success' && + needs.lint.result == 'success' && + needs.test.result == 'success' }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6ebe4121e9..9007bd6697 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -21,7 +21,7 @@ jobs: needs: [build, test] steps: - name: Download dist - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: dist path: dist/ @@ -48,14 +48,14 @@ jobs: - name: Clone fiftyone uses: actions/checkout@v4 - name: Download dist - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: dist path: dist - name: docker run: make docker-export -o python - name: Upload image - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: docker-image path: fiftyone.tar.gz diff --git a/.github/workflows/push-release.yml b/.github/workflows/push-release.yml index 50882445b6..0856f0782c 100644 --- a/.github/workflows/push-release.yml +++ b/.github/workflows/push-release.yml @@ -3,7 +3,6 @@ name: Push Release on: push: branches: - - main - release/v[0-9]+.[0-9]+.[0-9]+ workflow_dispatch: inputs: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6d6944363e..fd2bd37942 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ jobs: uses: actions/checkout@v4 - name: Cache id: node-cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | app/node_modules @@ -20,13 +20,7 @@ jobs: if: steps.node-cache.outputs.cache-hit != 'true' run: cd app && yarn install - name: Run - run: cd app && yarn test --coverage && mv ./coverage/coverage-final.json ../ - - name: Upload - uses: codecov/codecov-action@v3 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./coverage-final.json - flags: app + run: cd app && yarn test test-python: runs-on: ${{ matrix.os }} @@ -40,11 +34,14 @@ jobs: - windows-latest python: - "3.8" + - "3.9" - "3.10" - "3.11" exclude: - os: windows-latest python: "3.8" + - os: windows-latest + python: "3.9" - os: windows-latest python: "3.10" - os: windows-latest @@ -56,7 +53,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Setup - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 id: pip-cache with: python-version: ${{ matrix.python }} @@ -72,14 +69,13 @@ jobs: - name: Install fiftyone run: | pip install . - pip install fiftyone-db-ubuntu2204 - name: Configure id: test_config run: | python tests/utils/setup_config.py python tests/utils/github_actions_flags.py - name: FFmpeg - uses: FedericoCarboni/setup-ffmpeg@v2 + uses: FedericoCarboni/setup-ffmpeg@v3 # Important: use pytest_wrapper.py instead of pytest directly to ensure # that services shut down cleanly and do not conflict with steps later in # this workflow @@ -94,7 +90,7 @@ jobs: --ignore tests/no_wrapper - name: Upload if: ${{ !startsWith(matrix.os, 'windows') && matrix.python == '3.11' }} - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.xml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b1d3ae6fbf..8e747bb9fb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,3 +37,11 @@ repos: - id: prettier exclude: ^docs/theme/ language_version: system + - repo: local + hooks: + - id: frontend-check + name: biome check + entry: npx biome check --apply --files-ignore-unknown=true --no-errors-on-unmatched + language: system + types: [text] + files: "^app\\/packages\\/(app|spotlight)\\/.*\\.(css|html|json|md|ts)$" diff --git a/.prettierignore b/.prettierignore index 2febb8f257..66bfef4c78 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,6 @@ +app/packages/app +app/packages/spotlight *.html voxel51-website.js **/__generated__ -**/dist \ No newline at end of file +**/dist diff --git a/MANIFEST.in b/MANIFEST.in index 0747620071..06fe67a977 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -10,6 +10,7 @@ prune package prune requirements prune tools prune tests +prune dist global-exclude .* diff --git a/Makefile b/Makefile index 95dbc80b41..cff0660167 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,10 @@ app: @cd app && yarn && yarn build && cd .. -python: app +clean: + @rm -rf ./dist/* + +python: app clean @python -Im build docker: python diff --git a/README.md b/README.md index e0e7fdf70f..2416c47d51 100644 --- a/README.md +++ b/README.md @@ -132,11 +132,12 @@ to make adjustments. If you are working in Google Colab, You will need: -- [Python](https://www.python.org) (3.7 or newer) +- [Python](https://www.python.org) (3.8 - 3.11) - [Node.js](https://nodejs.org) - on Linux, we recommend using [nvm](https://github.com/nvm-sh/nvm) to install an up-to-date version. -- [Yarn](https://yarnpkg.com) - once Node.js is installed, you can install - Yarn via `npm install -g yarn` +- [Yarn](https://yarnpkg.com) - once Node.js is installed, you can + [enable Yarn](https://yarnpkg.com/getting-started/install) via + `corepack enable` - On Linux, you will need at least the `openssl` and `libcurl` packages. On Debian-based distributions, you will need to install `libcurl4` or `libcurl3` instead of `libcurl`, depending on the age of your distribution. @@ -154,8 +155,7 @@ sudo dnf install libcurl openssl We strongly recommend that you install FiftyOne in a [virtual environment](https://voxel51.com/docs/fiftyone/getting_started/virtualenv.html) -to maintain a clean workspace. The install script is only supported in -POSIX-based systems (e.g. Mac and Linux). +to maintain a clean workspace. First, clone the repository: @@ -167,7 +167,11 @@ cd fiftyone Then run the install script: ```shell +# Mac or Linux bash install.bash + +# Windows +.\install.bat ``` **NOTE:** If you run into issues importing FiftyOne, you may need to add the @@ -203,7 +207,11 @@ you should perform a developer installation using the `-d` flag of the install script: ```shell +# Mac or Linux bash install.bash -d + +# Windows +.\install.bat -d ``` Although not required, developers typically prefer to configure their FiftyOne @@ -222,7 +230,12 @@ cell and then **restarting the runtime**: git clone --depth 1 https://github.com/voxel51/fiftyone.git cd fiftyone + +# Mac or Linux bash install.bash + +# Windows +.\install.bat ``` ### Docker installs diff --git a/app/eslint-packages.txt b/app/eslint-packages.txt new file mode 100644 index 0000000000..9b6dd8978e --- /dev/null +++ b/app/eslint-packages.txt @@ -0,0 +1,3 @@ +# require these paths to pass linting +# for PRs to be merge-able +packages/operators diff --git a/app/package.json b/app/package.json index 7b15f87d6d..1ecda4b59e 100644 --- a/app/package.json +++ b/app/package.json @@ -7,13 +7,14 @@ "main": "index.js", "scripts": { "build": "yarn workspace @fiftyone/app build", + "build:win32": "yarn workspace @fiftyone/app build:win32", + "check": "yarn workspace @fiftyone/app check && yarn workspace @fiftyone/spotlight check", "compile": "yarn relay-compiler", "dev": "yarn workspace @fiftyone/app dev", "dev:py": "python ../fiftyone/server/main.py", "dev:wpy": "concurrently -k yarn:dev yarn:dev:py", "doc": "./gen-docs.sh", "lint:prettify": "prettier --config ../.prettierrc.js --ignore-path ../.prettierignore --write \"packages/**/*.(ts|js|jsx|tsx|json|css|scss)\"", - "postinstall": "patch-package", "start": "yarn workspace @fiftyone/app start", "start-desktop": "yarn workspace FiftyOne start-desktop", "test": "yarn vitest run", @@ -23,10 +24,11 @@ "devDependencies": { "@testing-library/react": "latest", "@testing-library/react-hooks": "latest", + "@types/react-plotly.js": "^2.6.3", "@typescript-eslint/eslint-plugin": "^5.44.0", "@typescript-eslint/parser": "^5.44.0", - "@vitest/coverage-v8": "^0.34.6", - "@vitest/ui": "^0.34.7", + "@vitest/coverage-v8": "^2.0.5", + "@vitest/ui": "^2.0.5", "concurrently": "^7.2.1", "eslint": "^8.28.0", "eslint-config-prettier": "^8.5.0", @@ -35,7 +37,6 @@ "eslint-plugin-react": "^7.31.11", "eslint-plugin-react-hooks": "^4.6.0", "jsdom": "^20.0.2", - "patch-package": "^6.4.7", "prettier": "^2.8.0", "relay-compiler": "^14.1.0", "relay-runtime": "^14.1.0", @@ -45,13 +46,15 @@ "vite": "^5.2.12", "vite-plugin-eslint": "^1.8.1", "vite-plugin-relay": "^2.0.0", - "vitest": "^1.6.0" + "vitest": "^2.0.5" }, "workspaces": [ "packages/*" ], "packageManager": "yarn@3.2.1", "dependencies": { - "jpeg-js": "^0.4.4" + "jpeg-js": "^0.4.4", + "react-player": "^2.16.0", + "react-plotly.js": "^2.6.0" } } diff --git a/app/packages/aggregations/tsconfig.json b/app/packages/aggregations/tsconfig.json index a5750c8086..85858b0a8e 100644 --- a/app/packages/aggregations/tsconfig.json +++ b/app/packages/aggregations/tsconfig.json @@ -1,20 +1,20 @@ { - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "lib": ["ESNext", "DOM", "DOM.Iterable"], - "moduleResolution": "Node", - "sourceMap": true, - "resolveJsonModule": true, - "esModuleInterop": true, - "noEmit": false, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, - "noImplicitAny": false, - "plugins": [], - "composite": true - }, - "include": ["./src"], - "exclude": ["node_modules"] + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "moduleResolution": "Node", + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "noEmit": false, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noImplicitAny": false, + "plugins": [], + "composite": true + }, + "include": ["./src"], + "exclude": ["node_modules"] } diff --git a/app/packages/analytics/.eslintrc b/app/packages/analytics/.eslintrc new file mode 100644 index 0000000000..b3c5d866e5 --- /dev/null +++ b/app/packages/analytics/.eslintrc @@ -0,0 +1,5 @@ +{ + "rules": { + "react-hooks/rules-of-hooks": "off" + } +} diff --git a/app/packages/analytics/.gitignore b/app/packages/analytics/.gitignore new file mode 100644 index 0000000000..04e39855e7 --- /dev/null +++ b/app/packages/analytics/.gitignore @@ -0,0 +1,11 @@ +.DS_Store +build/** +node_modules/** + +.yarn/* +!.yarn/releases +!.yarn/plugins +!.yarn/sdks +!.yarn/versions + +yarn-error.log diff --git a/app/packages/analytics/.prettierignore b/app/packages/analytics/.prettierignore new file mode 100644 index 0000000000..378eac25d3 --- /dev/null +++ b/app/packages/analytics/.prettierignore @@ -0,0 +1 @@ +build diff --git a/app/packages/analytics/README.md b/app/packages/analytics/README.md new file mode 100644 index 0000000000..0d4742619f --- /dev/null +++ b/app/packages/analytics/README.md @@ -0,0 +1,28 @@ +# FiftyOne Analytics + +FiftyOne Analytics package. + +## Configuring Analytics + +```typescript +const [info, setInfo] = useAnalyticsInfo(); + +setInfo({ + writeKey: "", + userId: "123", + userGroup: "group1", + doNotTrack: false, +}); +``` + +## Tracking Events from React + +```typescript +function MyComponent() { + const trackEvent = useTrackEvent(); + + useEffect(() => { + trackEvent("my_component_loaded", { customProp: 42 }); + }, []); +} +``` diff --git a/app/packages/analytics/babel.config.js b/app/packages/analytics/babel.config.js new file mode 100644 index 0000000000..5cb73d1e3a --- /dev/null +++ b/app/packages/analytics/babel.config.js @@ -0,0 +1,7 @@ +/* global module */ +module.exports = { + presets: [ + ["@babel/preset-env", { targets: { node: "current" } }], + "@babel/preset-typescript", + ], +}; diff --git a/app/packages/analytics/package.json b/app/packages/analytics/package.json new file mode 100644 index 0000000000..96c3b316aa --- /dev/null +++ b/app/packages/analytics/package.json @@ -0,0 +1,16 @@ +{ + "name": "@fiftyone/analytics", + "author": "Voxel51, Inc.", + "version": "0.0.0", + "description": "FiftyOne analytics", + "homepage": "https://github.com/voxel51/fiftyone/app/packages/analytics", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/voxel51/fiftyone/" + }, + "main": "./src/index.ts", + "private": true, + "dependencies": { + "@segment/analytics-next": "^1.70.0" + } +} diff --git a/app/packages/analytics/src/index.ts b/app/packages/analytics/src/index.ts new file mode 100644 index 0000000000..73cf4804e5 --- /dev/null +++ b/app/packages/analytics/src/index.ts @@ -0,0 +1,6 @@ +export { DEFAULT_WRITE_KEYS } from "./keys"; +export { analyticsInfo } from "./state"; +export { default as useAnalyticsInfo } from "./useAnalyticsInfo"; +export { default as useTrackEvent } from "./useTrackEvent"; +export * from "./usingAnalytics"; +export { default as usingAnalytics } from "./usingAnalytics"; diff --git a/app/packages/analytics/src/keys.ts b/app/packages/analytics/src/keys.ts new file mode 100644 index 0000000000..f69223b71b --- /dev/null +++ b/app/packages/analytics/src/keys.ts @@ -0,0 +1,4 @@ +export const DEFAULT_WRITE_KEYS = { + dev: "MrAGfUuvQq2FOJIgAgbwgjMQgRNgruRa", // oss-dev + prod: "SjCRPH72QTHlVhFZIT5067V9rhuq80Dl", // oss-prod +}; diff --git a/app/packages/analytics/src/state.ts b/app/packages/analytics/src/state.ts new file mode 100644 index 0000000000..7f7548c017 --- /dev/null +++ b/app/packages/analytics/src/state.ts @@ -0,0 +1,7 @@ +import { atom } from "recoil"; +import type { AnalyticsInfo } from "./usingAnalytics"; + +export const analyticsInfo = atom({ + key: "analyticsInfo", + default: null, +}); diff --git a/app/packages/analytics/src/useAnalyticsInfo.ts b/app/packages/analytics/src/useAnalyticsInfo.ts new file mode 100644 index 0000000000..c6c597ae69 --- /dev/null +++ b/app/packages/analytics/src/useAnalyticsInfo.ts @@ -0,0 +1,10 @@ +import { useRecoilState } from "recoil"; +import { analyticsInfo } from "./state"; +import type { AnalyticsInfo } from "./usingAnalytics"; + +export default function useAnalyticsInfo(): [ + AnalyticsInfo, + (info: AnalyticsInfo) => void +] { + return useRecoilState(analyticsInfo); +} diff --git a/app/packages/analytics/src/useTrackEvent.ts b/app/packages/analytics/src/useTrackEvent.ts new file mode 100644 index 0000000000..c687162d7c --- /dev/null +++ b/app/packages/analytics/src/useTrackEvent.ts @@ -0,0 +1,21 @@ +import { useCallback } from "react"; +import { useRecoilValue } from "recoil"; +import { analyticsInfo } from "./state"; +import type { AnalyticsInfo } from "./usingAnalytics"; +import usingAnalytics from "./usingAnalytics"; + +/** + * Track an event. This can be called from any component to track an event, however + * only when a user is opted in to analytics, will an event be sent to the analytics + * service. + */ +export default function useTrackEvent() { + const info = useRecoilValue(analyticsInfo); + return useCallback( + (eventName: string, properties?: Record) => { + const analytics = usingAnalytics(info); + analytics.trackEvent(eventName, properties); + }, + [info] + ); +} diff --git a/app/packages/analytics/src/usingAnalytics.ts b/app/packages/analytics/src/usingAnalytics.ts new file mode 100644 index 0000000000..534b236d6c --- /dev/null +++ b/app/packages/analytics/src/usingAnalytics.ts @@ -0,0 +1,86 @@ +import { AnalyticsBrowser } from "@segment/analytics-next"; + +export type AnalyticsInfo = { + writeKey: string; + userId: string; + userGroup: string; + doNotTrack?: boolean; + debug: boolean; +}; + +let _analytics: Analytics = null; + +export default function usingAnalytics(info: AnalyticsInfo): Analytics { + if (!_analytics) { + _analytics = new Analytics(); + } + if (info) { + _analytics.load(info); + } + return _analytics; +} + +export class Analytics { + private _segment?: AnalyticsBrowser; + private _debug = false; + load(info: AnalyticsInfo) { + if (this._segment) return; + this._debug = info?.debug; + if (!info || info.doNotTrack) { + console.warn("Analytics disabled"); + console.log(info); + this.disable(); + return; + } + if (!info.writeKey) { + console.warn("Analytics disabled (no write key)"); + this.disable(); + return; + } + this.enable(info); + } + + enable(info: AnalyticsInfo) { + this._segment = AnalyticsBrowser.load({ + writeKey: info.writeKey, + }); + if (info.userId) { + this.identify(info.userId); + } + if (info.userGroup) { + this.group(info.userGroup); + } + } + + disable() { + this._segment = null; + } + + page(name?: string, properties?: {}) { + if (!this._segment) return; + this._segment.page(name, properties); + } + + track(name: string, properties?: {}) { + if (this._debug) { + console.log("track", name, properties); + } + if (!this._segment) return; + this._segment.track(name, properties); + } + + trackEvent(name: string, properties?: {}) { + if (!this._segment) return; + this.track(name, properties); + } + + identify(userId: string, traits?: {}) { + if (!this._segment) return; + this._segment.identify(userId, traits); + } + + group(groupId: string, traits?: {}) { + if (!this._segment) return; + this._segment.group(groupId, traits); + } +} diff --git a/app/packages/analytics/tsconfig.json b/app/packages/analytics/tsconfig.json new file mode 100644 index 0000000000..ab0959a2de --- /dev/null +++ b/app/packages/analytics/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "moduleResolution": "Node", + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noImplicitAny": false, + "plugins": [], + "types": ["vite/client"], + "jsx": "react-jsx" + }, + "include": ["./src"], + "references": [] +} diff --git a/app/packages/analytics/vite.config.ts b/app/packages/analytics/vite.config.ts new file mode 100644 index 0000000000..3ce6a2094b --- /dev/null +++ b/app/packages/analytics/vite.config.ts @@ -0,0 +1,18 @@ +import { UserConfig } from "vite"; + +export default { + esbuild: true, + build: { + lib: { + entry: "src/index.ts", + formats: ["es"], + }, + target: "es2015", + minify: false, + }, + resolve: { + alias: { + "@fiftyone/looker": "@fiftyone/looker/src/index.ts", + }, + }, +}; diff --git a/app/packages/app/biome.json b/app/packages/app/biome.json new file mode 100644 index 0000000000..e285305b3f --- /dev/null +++ b/app/packages/app/biome.json @@ -0,0 +1,16 @@ +{ + "files": { + "ignore": ["./src/**/*.graphql.ts"] + }, + "formatter": { + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 80 + }, + "javascript": { + "formatter": { + "trailingCommas": "es5" + }, + "jsxRuntime": "reactClassic" + } +} diff --git a/app/packages/app/package.json b/app/packages/app/package.json index 6f1676f060..d9c8568a72 100644 --- a/app/packages/app/package.json +++ b/app/packages/app/package.json @@ -5,14 +5,18 @@ "private": true, "main": "./src/index.tsx", "scripts": { + "check": "biome check --write ./src README.md tsconfig.json vite.config.ts", "dev": "vite", "build": "yarn workspace @fiftyone/fiftyone compile && yarn build-bare && yarn copy-to-python", + "build:win32": "yarn workspace @fiftyone/fiftyone compile && yarn build-bare && yarn copy-to-python:win32", "build-bare": "NODE_OPTIONS=--max-old-space-size=4096 && tsc && vite build", "build-desktop": "NODE_OPTIONS=--max-old-space-size=4096 && tsc && vite build --mode desktop", "copy-to-python": "rm -rf ../../../fiftyone/server/static && cp -r ./dist ../../../fiftyone/server/static", + "copy-to-python:win32": "robocopy './dist' '../../../fiftyone/server/static' /MIR", "copy-to-desktop": "rm -rf ../desktop/dist/ && cp -r ./dist ../desktop/dist" }, "dependencies": { + "@fiftyone/analytics": "*", "@fiftyone/components": "*", "@fiftyone/core": "*", "@fiftyone/relay": "*", @@ -41,6 +45,7 @@ "uuid": "^8.3.2" }, "devDependencies": { + "@biomejs/biome": "^1.8.3", "@types/lodash": "^4.14.182", "@types/mime": "^2.0.3", "@types/react": "^18.2.48", @@ -48,10 +53,9 @@ "@types/react-router": "^5.1.20", "@types/styled-components": "^5.1.23", "@vitejs/plugin-react-refresh": "^1.3.3", - "prettier": "2.2.1", "relay-compiler": "^14.1.0", "rollup-plugin-polyfill-node": "^0.6.2", - "typescript": "^4.7.4", + "typescript": "^5.3.2", "typescript-plugin-css-modules": "^5.0.2", "vite": "^5.2.12", "vite-plugin-relay": "^1.0.7", diff --git a/app/packages/app/src/Network.tsx b/app/packages/app/src/Network.tsx index 4c17eb66e7..ba719710d9 100644 --- a/app/packages/app/src/Network.tsx +++ b/app/packages/app/src/Network.tsx @@ -3,10 +3,10 @@ import { RelayEnvironmentKey } from "@fiftyone/state"; import React from "react"; import { RelayEnvironmentProvider } from "react-relay"; import { RecoilRelayEnvironment } from "recoil-relay"; -import { IEnvironment } from "relay-runtime"; +import type { IEnvironment } from "relay-runtime"; import Sync from "./Sync"; -import { Queries } from "./makeRoutes"; -import { Renderer, RouterContext, RoutingContext } from "./routing"; +import type { Queries } from "./makeRoutes"; +import { Renderer, RouterContext, type RoutingContext } from "./routing"; const Network: React.FC<{ environment: IEnvironment; diff --git a/app/packages/app/src/Renderer.tsx b/app/packages/app/src/Renderer.tsx index d18d77ce88..e9ee86327e 100644 --- a/app/packages/app/src/Renderer.tsx +++ b/app/packages/app/src/Renderer.tsx @@ -1,16 +1,31 @@ +import type { Queries } from "./makeRoutes"; +import type { Entry } from "./routing"; + import { Loading, Pending } from "@fiftyone/components"; import { subscribe } from "@fiftyone/relay"; -import { theme, themeConfig } from "@fiftyone/state"; +import { + isModalActive, + theme, + themeConfig, + useSetExpandedSample, + useSetModalState, +} from "@fiftyone/state"; import { useColorScheme } from "@mui/material"; -import React, { Suspense, useEffect, useLayoutEffect } from "react"; +import React, { + Suspense, + useCallback, + useEffect, + useLayoutEffect, + useState, +} from "react"; import { atom, useRecoilState, + useRecoilTransaction_UNSTABLE, useRecoilValue, useSetRecoilState, } from "recoil"; -import { Queries } from "./makeRoutes"; -import { Entry, useRouterContext } from "./routing"; +import { useRouterContext } from "./routing"; export const pendingEntry = atom({ key: "pendingEntry", @@ -38,17 +53,39 @@ const ColorScheme = () => { }; const Renderer = () => { - const [routeEntry, setRouteEntry] = useRecoilState(entry); + const routeEntry = useRecoilValue(entry); + const [pending, setPending] = useRecoilState(pendingEntry); const router = useRouterContext(); + const [ready, setReady] = useState(false); + const setModalState = useSetModalState(); + const setExpansion = useSetExpandedSample(); + + const apply = useRecoilTransaction_UNSTABLE( + ({ set }) => + (result: Entry) => { + set(entry, result); + setReady(true); + }, + [router] + ); + + const init = useCallback( + async (result: Entry) => { + await setModalState(); + await setExpansion(); + apply(result); + }, + [apply, setExpansion, setModalState] + ); useEffect(() => { - router.load().then(setRouteEntry); + router.load().then(init); subscribe((_, { set }) => { - set(entry, router.get()); + set(entry, router.get(true)); set(pendingEntry, false); }); - }, [router, setRouteEntry]); + }, [init, router]); useEffect(() => { return router.subscribe( @@ -59,22 +96,32 @@ const Renderer = () => { const loading = Pixelating...; - if (!routeEntry) return loading; + if (!routeEntry || !ready) return loading; return ( - - - {pending && } + + + + {pending && } ); }; +const Modal = () => { + const active = Boolean(useRecoilValue(isModalActive)); + useEffect(() => { + document.getElementById("modal")?.classList.toggle("modalon", active); + }, [active]); + + return null; +}; const Route = ({ route }: { route: Entry }) => { const Component = route.component; useEffect(() => { - document.dispatchEvent(new CustomEvent("page-change", { bubbles: true })); + route && + document.dispatchEvent(new CustomEvent("page-change", { bubbles: true })); }, [route]); return ; diff --git a/app/packages/app/src/Sync.tsx b/app/packages/app/src/Sync.tsx index ebdab63659..5c7f9abdb9 100644 --- a/app/packages/app/src/Sync.tsx +++ b/app/packages/app/src/Sync.tsx @@ -2,28 +2,39 @@ import { Loading } from "@fiftyone/components"; import { usePlugins } from "@fiftyone/plugins"; import { setDataset, - setDatasetMutation, + setGroupSlice, + setSample, setSpaces, - setSpacesMutation, setView, - setViewMutation, - subscribe, Writer, + type setDatasetMutation, + type setGroupSliceMutation, + type setSampleMutation, + type setSpacesMutation, + type setViewMutation, } from "@fiftyone/relay"; import * as fos from "@fiftyone/state"; -import { Session, SESSION_DEFAULT, stateSubscription } from "@fiftyone/state"; -import { Action } from "history"; -import React, { useEffect, useRef } from "react"; +import { + SESSION_DEFAULT, + stateSubscription, + type Session, +} from "@fiftyone/state"; +import type { Action } from "history"; +import React, { useRef } from "react"; import { useRelayEnvironment } from "react-relay"; import { useRecoilValue } from "recoil"; -import { commitMutation, Environment, OperationType } from "relay-runtime"; -import Setup from "./components/Setup"; -import { IndexPageQuery } from "./pages/__generated__/IndexPageQuery.graphql"; import { + commitMutation, + type Environment, + type OperationType, +} from "relay-runtime"; +import Setup from "./components/Setup"; +import type { DatasetPageQuery, DatasetPageQuery$data, } from "./pages/datasets/__generated__/DatasetPageQuery.graphql"; -import { Entry, useRouterContext } from "./routing"; +import type { IndexPageQuery } from "./pages/__generated__/IndexPageQuery.graphql"; +import { useRouterContext, type Entry } from "./routing"; import { AppReadyState } from "./useEvents/registerEvent"; import useEventSource from "./useEventSource"; import useSetters from "./useSetters"; @@ -48,13 +59,6 @@ const Sync = ({ children }: { children?: React.ReactNode }) => { useWriters(subscription, environment, router, sessionRef); const readyState = useEventSource(router, sessionRef); - useEffect( - () => - subscribe((_, { reset }) => { - reset(fos.currentModalSample); - }), - [] - ); return ( @@ -74,16 +78,16 @@ const Sync = ({ children }: { children?: React.ReactNode }) => { }} setters={setters} subscribe={(fn) => { - return router.subscribe((entry, action) => { + return router.subscribe(({ state, ...entry }, action) => { dispatchSideEffect({ action, currentEntry: router.get(), environment, - nextEntry: entry, + nextEntry: { state, ...entry }, subscription, session: sessionRef.current, }); - fn(entry); + fn({ ...entry, event: state.event }); }); }} > @@ -113,6 +117,26 @@ const dispatchSideEffect = ({ return; } + session.modalSelector = nextEntry.state.modalSelector; + + if ( + currentEntry.state.event === "modal" || + nextEntry.state.event === "modal" + ) { + if (nextEntry.state.event !== "modal") { + session.selectedLabels = []; + } + commitMutation(environment, { + mutation: setSample, + variables: { + groupId: nextEntry.state.modalSelector?.groupId, + id: nextEntry.state.modalSelector?.id, + subscription, + }, + }); + return; + } + session.selectedLabels = []; session.selectedSamples = new Set(); @@ -136,36 +160,58 @@ const dispatchSideEffect = ({ // @ts-ignore const data: DatasetPageQuery$data = nextEntry.data; + + session.modalSelector = nextEntry.state?.modalSelector; + const updateSlice = + currentEntry.state.groupSlice !== nextEntry.state.groupSlice; + if (updateSlice) { + session.sessionGroupSlice = nextEntry.state.groupSlice || undefined; + } + + let update = !fos.viewsAreEqual( + currentEntry.state.view, + nextEntry.state.view + ); if (currentDataset !== nextDataset) { + update = true; session.colorScheme = fos.ensureColorScheme( data.dataset?.appConfig?.colorScheme, data.config ); session.fieldVisibilityStage = nextEntry.state.fieldVisibility; - session.sessionGroupSlice = data.dataset?.defaultGroupSlice || undefined; session.sessionSpaces = nextEntry.state?.workspace ?? fos.SPACES_DEFAULT; } - commitMutation(environment, { - mutation: setView, - variables: { - view: nextEntry.state.view, - savedViewSlug: nextEntry.state.savedViewSlug, - form: {}, - datasetName: nextDataset, - subscription, - }, - onCompleted: () => { - nextEntry.state?.workspace && - commitMutation(environment, { - mutation: setSpaces, - variables: { - spaces: nextEntry.state?.workspace, - subscription, - }, - }); - }, - }); + update && + commitMutation(environment, { + mutation: setView, + variables: { + view: nextEntry.state.view, + savedViewSlug: nextEntry.state.savedViewSlug, + form: {}, + datasetName: nextDataset, + subscription, + }, + onCompleted: () => { + nextEntry.state?.workspace && + commitMutation(environment, { + mutation: setSpaces, + variables: { + spaces: nextEntry.state?.workspace, + subscription, + }, + }); + + updateSlice && + commitMutation(environment, { + mutation: setGroupSlice, + variables: { + slice: session.sessionGroupSlice, + subscription, + }, + }); + }, + }); }; export default Sync; diff --git a/app/packages/app/src/components/Analytics.tsx b/app/packages/app/src/components/Analytics.tsx new file mode 100644 index 0000000000..991d70062d --- /dev/null +++ b/app/packages/app/src/components/Analytics.tsx @@ -0,0 +1,49 @@ +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 { + Analytics$data, + Analytics$key, +} from "./__generated__/Analytics.graphql"; + +const useCallGA = (info: Analytics$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: Analytics$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..bfdfb4345d --- /dev/null +++ b/app/packages/app/src/components/AnalyticsConsent.tsx @@ -0,0 +1,114 @@ +import { DEFAULT_WRITE_KEYS, useAnalyticsInfo } from "@fiftyone/analytics"; +import { Box, Grid, Link, Typography, Button } 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/DatasetSelector.tsx b/app/packages/app/src/components/DatasetSelector.tsx index 57eb85b95c..23f0e974f9 100644 --- a/app/packages/app/src/components/DatasetSelector.tsx +++ b/app/packages/app/src/components/DatasetSelector.tsx @@ -1,4 +1,4 @@ -import { Selector, UseSearch } from "@fiftyone/components"; +import { Selector, type UseSearch } from "@fiftyone/components"; import { datasetName, useSetDataset } from "@fiftyone/state"; import React from "react"; import { useRecoilValue } from "recoil"; @@ -8,9 +8,9 @@ const DatasetLink: React.FC<{ value: string; className?: string }> = ({ value, }) => { return ( - + {value} - + ); }; @@ -22,6 +22,7 @@ const DatasetSelector: React.FC<{ return ( + cy={"dataset"} component={DatasetLink} placeholder={"Select dataset"} inputStyle={{ height: 40, maxWidth: 300 }} diff --git a/app/packages/app/src/components/Nav.tsx b/app/packages/app/src/components/Nav.tsx index 7d392fe619..de367f3269 100644 --- a/app/packages/app/src/components/Nav.tsx +++ b/app/packages/app/src/components/Nav.tsx @@ -9,21 +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 } 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 { NavGA$key } from "./__generated__/NavGA.graphql"; +import type { NavDatasets$key } from "./__generated__/NavDatasets.graphql"; +import type { NavFragment$key } from "./__generated__/NavFragment.graphql"; const getUseSearch = (fragment: NavDatasets$key) => { return (search: string) => { @@ -64,95 +61,64 @@ const getUseSearch = (fragment: NavDatasets$key) => { }; }; -export const useGA = (fragment: NavGA$key) => { - const info = useFragment( - graphql` - fragment NavGA on Query { - context - dev - doNotTrack - uid - version - } - `, - fragment - ); - - useEffect(() => { - if (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 - }, - }); - }, []); -}; - -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 ); - useGA(data); + 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/Setup.tsx b/app/packages/app/src/components/Setup.tsx index 814175ec1a..745fabf95d 100644 --- a/app/packages/app/src/components/Setup.tsx +++ b/app/packages/app/src/components/Setup.tsx @@ -7,7 +7,7 @@ import { useTheme, } from "@fiftyone/components"; import { isNotebook } from "@fiftyone/state"; -import { isElectron, scrollbarStyles } from "@fiftyone/utilities"; +import { isElectron, styles } from "@fiftyone/utilities"; import { animated, useSpring } from "@react-spring/web"; import React, { useState } from "react"; import { useRecoilValue } from "recoil"; @@ -40,16 +40,16 @@ const Code = styled.pre` border-radius: 3px; overflow: auto; - ${scrollbarStyles} + ${styles.scrollbarStyles} `; const port = (() => { if (isElectron()) { - return parseInt(process.env.FIFTYONE_SERVER_PORT) || 5151; + return Number.parseInt(process.env.FIFTYONE_SERVER_PORT) || 5151; } if (typeof window !== "undefined" && window.location.port !== undefined) { - return parseInt(window.location.port); + return Number.parseInt(window.location.port); } return ""; @@ -129,7 +129,7 @@ const SetupWrapper = styled.div` background: ${({ theme }) => theme.background.level2}; border-top: 1px solid ${({ theme }) => theme.primary.plainBorder}; - ${scrollbarStyles}; + ${styles.scrollbarStyles} `; const SetupContainer = styled.div` 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/index.tsx b/app/packages/app/src/index.tsx index 528b532d6b..5215a80e3f 100644 --- a/app/packages/app/src/index.tsx +++ b/app/packages/app/src/index.tsx @@ -1,7 +1,7 @@ import { ErrorBoundary, ThemeProvider } from "@fiftyone/components"; import { BeforeScreenshotContext, screenshotCallbacks } from "@fiftyone/state"; import { SnackbarProvider } from "notistack"; -import React from "react"; +import type React from "react"; import { createRoot } from "react-dom/client"; import { RecoilRoot } from "recoil"; import Network from "./Network"; diff --git a/app/packages/app/src/makeRoutes.ts b/app/packages/app/src/makeRoutes.ts index eb57f06094..ae5b2aa865 100644 --- a/app/packages/app/src/makeRoutes.ts +++ b/app/packages/app/src/makeRoutes.ts @@ -1,8 +1,8 @@ import { createResourceGroup } from "@fiftyone/utilities"; -import { ConcreteRequest } from "relay-runtime"; -import { IndexPageQuery } from "./pages/__generated__/IndexPageQuery.graphql"; -import { DatasetPageQuery } from "./pages/datasets/__generated__/DatasetPageQuery.graphql"; -import { +import type { ConcreteRequest } from "relay-runtime"; +import type { IndexPageQuery } from "./pages/__generated__/IndexPageQuery.graphql"; +import type { DatasetPageQuery } from "./pages/datasets/__generated__/DatasetPageQuery.graphql"; +import type { Route, RouteDefinition, RouteOptions, diff --git a/app/packages/app/src/pages/IndexPage.tsx b/app/packages/app/src/pages/IndexPage.tsx index b5a596080d..ed13063647 100644 --- a/app/packages/app/src/pages/IndexPage.tsx +++ b/app/packages/app/src/pages/IndexPage.tsx @@ -3,8 +3,9 @@ import React from "react"; import { usePreloadedQuery } from "react-relay"; import { graphql } from "relay-runtime"; import Nav from "../components/Nav"; -import { Route } from "../routing"; -import { IndexPageQuery } from "./__generated__/IndexPageQuery.graphql"; +import type { Route } from "../routing"; +import type { IndexPageQuery } from "./__generated__/IndexPageQuery.graphql"; +import style from "./index.module.css"; const IndexPageQueryNode = graphql` query IndexPageQuery($search: String = "", $count: Int, $cursor: String) { @@ -26,11 +27,14 @@ const IndexPage: Route = ({ prepared }) => { const totalDatasets = queryRef.allDatasets; return ( - <> -