Skip to content

Commit

Permalink
Single Query Routing and Recoil Syncing (voxel51#2742)
Browse files Browse the repository at this point in the history
🚀 🚀 🚀 🚀 🚀

---------

Co-authored-by: Lanny W <lanzhenwang9@gmail.com>
  • Loading branch information
benjaminpkane and lanzhenw authored Sep 29, 2023
1 parent b2c889a commit 491c237
Show file tree
Hide file tree
Showing 329 changed files with 10,543 additions and 7,249 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ on: workflow_call
jobs:
test-e2e:
timeout-minutes: 60
runs-on: ubuntu-latest
runs-on: ubuntu-latest-m
env:
FIFTYONE_DO_NOT_TRACK: true
ELECTRON_EXTRA_LAUNCH_ARGS: "--disable-gpu"
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
fail-fast: false
matrix:
os:
- ubuntu-20.04
- ubuntu-latest-m
- windows-latest
python:
- 3.8
Expand Down Expand Up @@ -69,6 +69,7 @@ jobs:
- name: Install fiftyone
run: |
pip install .
pip install fiftyone-db-ubuntu2204
- name: Configure
id: test_config
run: |
Expand Down
46 changes: 46 additions & 0 deletions app/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,52 @@ FiftyOne App codebase.

See the App's [README.md](README.md) for installation instructions.

## Extending the Session

An App `session` is represented by a single
[server](../fiftyone/server/main.py) that holds a
[StateDescription](../fiftyone/core/state.py). This `state` object is defined
as a global in [`fiftyone.server.events`](../fiftyone.server.events.py) and can
be retrieved with
[`fiftyone.server.events.get_state`](../fiftyone.server.events.py).

### Python Session client state

Adding new state involves the following additions to the
[`fiftyone.server.session`](../fiftyone/core/session/) and
[`fiftyone.core.state`](../fiftyone/core/state)

- Property and/or method definitions, e.g. `Session.selected` getter and
setter methods
- Adding new attributes for serialization and deserialization in the
`StateDescription`
- Initialization logic in `Session.__init__`
- Contributing
[API documentation](https://docs.voxel51.com/api/fiftyone.core.session.html)
- Defining any new events associated with the new state
- Adding an event listener in
[\_attach_event_listeners](../fiftyone/core/session/session.py)
- Declaring the
[event subscription on the client](../fiftyone/core/session/events.py)
- Defining the [event dataclass](../fiftyone/core/session/events.py) in
`fiftyone.core.session.events`

Note that some events are App specific, and some are Python Session specific.
See the [`@fiftyone/app`](./packages/app) for information on App event details

### Session server state

Implementing session state on the server requires processing the state and
receiving App mutations

- [State processing](../fiftyone/server/events.py)
- [GraphQL Mutations](../fiftyone/server/mutation.py)

Note that dispatching an event in
[fiftyone.server.events](../fiftyone/server/events.py), including a
`subscription` id prevents dispatching the event to the client that triggered
the event.

## Copyright

Copyright 2017-2023, Voxel51, Inc.<br> voxel51.com
1 change: 1 addition & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"main": "index.js",
"scripts": {
"build": "yarn workspace @fiftyone/app build",
"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",
Expand Down
40 changes: 40 additions & 0 deletions app/packages/app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# FiftyOne App

The FiftyOne App is a lightweight single page app around the `@fiftyone/core`
components. It is the client controller for syncing session state with the
server.

## Contracts

### Receiving session updates

Session updates are received by the App via
[`useEventSource`](./src/useEventSource.ts). Hooks that handle events are
registered in the [`./src/useEvents`](./src/useEvents/) directory. To add a new
event, add a new `EventHandlerHook` in the directory and register it in
[`./useEvents/index.ts`](./src/useEvents/index.ts) with a corresponding server
event name (in came case).

### Writing session updates

The core session values are defined in `@fiftyone/state` as `sessionAtom`s.
When a `sessionAtom` is written to via a `recoil` set call, the value
immediately takes effect in the `sessionAtom`. Side effects of writing to the
atom can be registered in [`./src/useWriter`](./src/useWriters/index.ts). These
side effects are enumerated by the `RegisteredWriter` type and derive from
`@fiftyone/state`'s `Session` definition.

### Updating via Setters

The complex case of handling state updates that affect other recoil state is
encapsulated in [`./src/useSetters`](./src/useSetters/). One example of this is
updating the `view` in the App. The `view` atom in `@fiftyone/state` is
implemented as a
[`graphQLSyncFragmentAtom`](../relay/src/graphQLSyncFragmentAtom.ts) because
its value is tied to the current page query. Setting the atom is handled with
indirection by including the `selectorEffect` when creating the
`graphQLSyncFragmentAtom`. This requires an associated setter registered in
[`./src/useSetters/index.ts`](./src/useSetters/index.ts) that will control how
to transition to the next page state. See
[`./src/useSetters/onSetView.ts`](./src/useSetters/onSetView.ts) for a concrete
example.
11 changes: 5 additions & 6 deletions app/packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
"private": true,
"main": "./src/index.tsx",
"scripts": {
"compile": "yarn workspace @fiftyone/core compile && yarn workspace @fiftyone/relay compile",
"dev": "vite",
"build": "yarn compile && yarn build-bare && yarn copy-to-python",
"build": "yarn workspace @fiftyone/fiftyone compile && yarn build-bare && yarn copy-to-python",
"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",
Expand All @@ -19,12 +18,14 @@
"@fiftyone/flashlight": "*",
"@fiftyone/looker": "*",
"@fiftyone/map": "*",
"@fiftyone/relay": "*",
"@fiftyone/spaces": "*",
"@fiftyone/state": "*",
"@fiftyone/utilities": "*",
"@material-ui/core": "4.11.4",
"@material-ui/icons": "^4.9.1",
"@react-spring/web": "^9.4.3",
"@recoiljs/refine": "^0.1.1",
"@use-gesture/react": "10.1.1",
"@xstate/react": "1.3.3",
"classnames": "^2.2.6",
Expand All @@ -45,12 +46,13 @@
"react-input-autosize": "^3.0.0",
"react-is": "^17.0.1",
"react-laag": "^2.0.3",
"react-relay": "^14.0.0",
"react-relay": "14.1.0",
"react-use-measure": "^2.0.1",
"react-uuid": "^1.0.2",
"recharts": "2.0.9",
"recoil": "0.7.6",
"recoil-relay": "^0.1.0",
"recoil-sync": "^0.2.0",
"resize-observer-polyfill": "^1.5.1",
"searchable-react-json-view": "^0.0.8",
"styled-components": "^5.3.3",
Expand All @@ -62,10 +64,7 @@
"@types/mime": "^2.0.3",
"@types/react": "^18.0.9",
"@types/react-dom": "^18.0.3",
"@types/react-relay": "^14.1.0",
"@types/react-router": "^5.1.18",
"@types/relay-compiler": "^8.0.2",
"@types/relay-runtime": "^14.1.0",
"@types/styled-components": "^5.1.23",
"@vitejs/plugin-react-refresh": "^1.3.3",
"prettier": "2.2.1",
Expand Down
63 changes: 23 additions & 40 deletions app/packages/app/src/Network.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,32 @@
import { Loading, RouteRenderer } from "@fiftyone/components";
import * as fos from "@fiftyone/state";
import { RelayEnvironmentContext } from "@fiftyone/relay";
import { RelayEnvironmentKey } from "@fiftyone/state";

import React, { Suspense, useContext, useEffect } from "react";
import { Environment } from "react-relay";
import { useRecoilValue } from "recoil";
import { RecoilRelayEnvironmentProvider } from "recoil-relay";

const Renderer: React.FC = () => {
const context = useContext(fos.RouterContext);

return (
<Suspense fallback={<Loading>Pixelating...</Loading>}>
<RouteRenderer router={context} />
</Suspense>
);
};
import React from "react";
import { RelayEnvironmentProvider } from "react-relay";
import { RecoilRelayEnvironment } from "recoil-relay";
import { IEnvironment } from "relay-runtime";
import Sync from "./Sync";
import { Queries, Renderer, RouterContext, RoutingContext } from "./routing";

const Network: React.FC<{
environment: Environment;
context: fos.RoutingContext<any>;
environment: IEnvironment;
context: RoutingContext<Queries>;
}> = ({ environment, context }) => {
return (
<RecoilRelayEnvironmentProvider
environment={environment}
environmentKey={RelayEnvironmentKey}
>
<fos.RouterContext.Provider value={context}>
<Renderer />
</fos.RouterContext.Provider>
</RecoilRelayEnvironmentProvider>
<RelayEnvironmentProvider environment={environment}>
<RecoilRelayEnvironment
environment={environment}
environmentKey={RelayEnvironmentKey}
>
<RouterContext.Provider value={context}>
<RelayEnvironmentContext.Provider value={environment}>
<Sync>
<Renderer />
</Sync>
</RelayEnvironmentContext.Provider>
</RouterContext.Provider>
</RecoilRelayEnvironment>
</RelayEnvironmentProvider>
);
};

export const NetworkRenderer = ({ makeRoutes }) => {
const { context, environment } = fos.useRouter(makeRoutes, []);

const isModalOpen = useRecoilValue(fos.isModalActive);

useEffect(() => {
document.body.classList.toggle("noscroll", isModalOpen);
document.getElementById("modal")?.classList.toggle("modalon", isModalOpen);
}, [isModalOpen]);

return <Network environment={environment} context={context} />;
};

export default Network;
83 changes: 83 additions & 0 deletions app/packages/app/src/Renderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Loading, Pending } from "@fiftyone/components";
import { subscribe } from "@fiftyone/relay";
import { theme, themeConfig } from "@fiftyone/state";
import { useColorScheme } from "@mui/material";
import React, { Suspense, useEffect, useLayoutEffect } from "react";
import {
atom,
useRecoilState,
useRecoilValue,
useSetRecoilState,
} from "recoil";
import { Queries } from "./makeRoutes";
import { Entry, useRouterContext } from "./routing";

export const pendingEntry = atom<boolean>({
key: "pendingEntry",
default: false,
});

export const entry = atom<Entry<Queries> | null>({
key: "Entry",
default: null,
dangerouslyAllowMutability: true,
});

const ColorScheme = () => {
const { setMode } = useColorScheme();
const current = useRecoilValue(themeConfig);
const setTheme = useSetRecoilState(theme);
useLayoutEffect(() => {
if (current !== "browser") {
setTheme(current);
setMode(current);
}
}, [current, setMode, setTheme]);

return null;
};

const Renderer = () => {
const [routeEntry, setRouteEntry] = useRecoilState(entry);
const [pending, setPending] = useRecoilState(pendingEntry);
const router = useRouterContext();

useEffect(() => {
router.load().then(setRouteEntry);
subscribe((_, { set }) => {
set(entry, router.get());
set(pendingEntry, false);
});
}, [router, setRouteEntry]);

useEffect(() => {
return router.subscribe(
() => undefined,
() => setPending(true)
);
}, [router, setPending]);

const loading = <Loading>Pixelating...</Loading>;

if (!routeEntry) return loading;

return (
<Suspense fallback={loading}>
<ColorScheme />
<Route route={routeEntry} />
{pending && <Pending />}
</Suspense>
);
};

const Route = ({ route }: { route: Entry<Queries> }) => {
const Component = route.component;

useEffect(() => {
document.dispatchEvent(new CustomEvent("page-change", { bubbles: true }));
}, [route]);

return <Component prepared={route.preloadedQuery} />;
};

export default Renderer;
Loading

0 comments on commit 491c237

Please sign in to comment.