Skip to content

Commit

Permalink
Replaces useOpenGexf by proper atom and actions
Browse files Browse the repository at this point in the history
Details:
- Adds typings and helpers for AsyncAction
- Adds graph/files.ts, with files lifecycle logic
- Replaces all useOpenGexf usages by the new atom and actions
- Deletes useOpenGexf
  • Loading branch information
jacomyal committed Jan 30, 2024
1 parent 68d6a04 commit 96f0809
Show file tree
Hide file tree
Showing 15 changed files with 142 additions and 108 deletions.
11 changes: 7 additions & 4 deletions src/core/Initialize.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { AuthInit } from "./user/AuthInit";
import { Loader } from "../components/Loader";
import { useNotifications } from "./notifications";
import { graphDatasetAtom } from "./graph";
import { useOpenGexf } from "./graph/useOpenGexf";
import { parseDataset, getEmptyGraphDataset } from "./graph/utils";
import { filtersAtom } from "./filters";
import { parseFiltersState } from "./filters/utils";
Expand All @@ -21,12 +20,15 @@ import { useModal } from "./modals";
import { WelcomeModal } from "../views/graphPage/modals/WelcomeModal";
import { resetCamera } from "./sigma";
import { I18n } from "../locales/provider";
import { useFileActions, useFileState } from "./context/dataContexts";
import { fileStateAtom } from "./graph/files";

export const Initialize: FC<PropsWithChildren<unknown>> = ({ children }) => {
const { t } = useTranslation();
const { notify } = useNotifications();
const { openModal } = useModal();
const { loading, openRemoteFile } = useOpenGexf();
const { type: fileStateType } = useFileState();
const { openRemoteFile } = useFileActions();

useKonami(
() => {
Expand Down Expand Up @@ -86,7 +88,8 @@ export const Initialize: FC<PropsWithChildren<unknown>> = ({ children }) => {

// If query params has gexf
// => try to load the file
if (!graphFound && url.searchParams.has("gexf")) {
const isIdle = fileStateAtom.get().type === "idle";
if (!graphFound && url.searchParams.has("gexf") && isIdle) {
const gexfUrl = url.searchParams.get("gexf") || "";
try {
await openRemoteFile({
Expand Down Expand Up @@ -152,7 +155,7 @@ export const Initialize: FC<PropsWithChildren<unknown>> = ({ children }) => {
return (
<I18n>
<AuthInit />
{loading ? <Loader /> : children}
{children}
</I18n>
);
};
21 changes: 14 additions & 7 deletions src/core/context/dataContexts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { preferencesActions, preferencesAtom } from "../preferences";
import { selectionActions, selectionAtom } from "../selection";
import { sigmaActions, sigmaAtom, sigmaStateAtom } from "../sigma";
import { searchActions, searchAtom } from "../search";
import { Action } from "../utils/producers";
import { fileActions, fileStateAtom } from "../graph/files";

/**
* Helpers:
Expand Down Expand Up @@ -38,6 +40,7 @@ const ATOMS = {
sigma: sigmaAtom,
filters: filtersAtom,
selection: selectionAtom,
fileState: fileStateAtom,
appearance: appearanceAtom,
sigmaState: sigmaStateAtom,
sigmaGraph: sigmaGraphAtom,
Expand All @@ -53,6 +56,7 @@ const CONTEXTS = {
sigma: createContext(ATOMS.sigma),
filters: createContext(ATOMS.filters),
selection: createContext(ATOMS.selection),
fileState: createContext(ATOMS.fileState),
appearance: createContext(ATOMS.appearance),
sigmaState: createContext(ATOMS.sigmaState),
sigmaGraph: createContext(ATOMS.sigmaGraph),
Expand All @@ -79,11 +83,19 @@ export const AtomsContextsRoot: FC<{ children?: ReactNode }> = ({ children }) =>
</>
);
};
export const resetStates: Action = () => {
filtersActions.resetFilters();
selectionActions.reset();
appearanceActions.resetState();
sigmaActions.resetState();
searchActions.reset();
};

// Read data:
export const useFilters = makeUseAtom(CONTEXTS.filters);
export const useSigmaAtom = makeUseAtom(CONTEXTS.sigma);
export const useSelection = makeUseAtom(CONTEXTS.selection);
export const useFileState = makeUseAtom(CONTEXTS.fileState);
export const useAppearance = makeUseAtom(CONTEXTS.appearance);
export const useSigmaState = makeUseAtom(CONTEXTS.sigmaState);
export const useSigmaGraph = makeUseAtom(CONTEXTS.sigmaGraph);
Expand All @@ -100,13 +112,8 @@ export const useAppearanceActions = makeUseActions(appearanceActions);
export const useGraphDatasetActions = makeUseActions(graphDatasetActions);
export const usePreferencesActions = makeUseActions(preferencesActions);
export const useSearchActions = makeUseActions(searchActions);
export const useFileActions = makeUseActions(fileActions);

export const useResetStates = () => {
return () => {
filtersActions.resetFilters();
selectionActions.reset();
appearanceActions.resetState();
sigmaActions.resetState();
searchActions.reset();
};
return resetStates;
};
70 changes: 70 additions & 0 deletions src/core/graph/files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { asyncAction } from "../utils/producers";
import { FileState, LocalFile, RemoteFile } from "./types";
import { parse } from "graphology-gexf";
import Graph from "graphology";
import { resetStates } from "../context/dataContexts";
import { getEmptyFileState, initializeGraphDataset } from "./utils";
import { preferencesActions } from "../preferences";
import { resetCamera } from "../sigma";
import { atom } from "../utils/atoms";
import { graphDatasetActions } from "./index";

/**
* Public API:
* ***********
*/
export const fileStateAtom = atom<FileState>(getEmptyFileState());

/**
* Actions:
* ********
*/
export const openRemoteFile = asyncAction(async (remote: RemoteFile) => {
const { setGraphDataset } = graphDatasetActions;
const { addRemoteFile } = preferencesActions;

if (fileStateAtom.get().type === "loading") throw new Error("A file is already being loaded");

fileStateAtom.set({ type: "loading" });
try {
const response = await fetch(remote.url);
const gexf = await response.text();
const graph = parse(Graph, gexf, { allowUndeclaredAttributes: true, addMissingNodes: true });
graph.setAttribute("title", remote.filename);
resetStates();
setGraphDataset({ ...initializeGraphDataset(graph), origin: remote });
addRemoteFile(remote);
resetCamera({ forceRefresh: true });
} catch (e) {
fileStateAtom.set({ type: "error", message: (e as Error).message });
throw e;
} finally {
fileStateAtom.set({ type: "idle" });
}
});

export const openLocalFile = asyncAction(async (file: LocalFile) => {
const { setGraphDataset } = graphDatasetActions;

if (fileStateAtom.get().type === "loading") throw new Error("A file is already being loaded");

fileStateAtom.set({ type: "loading" });
try {
const content = await file.source.text();
const graph = parse(Graph, content, { allowUndeclaredAttributes: true });
graph.setAttribute("title", file.filename);
resetStates();
setGraphDataset({ ...initializeGraphDataset(graph), origin: file });
resetCamera({ forceRefresh: true });
} catch (e) {
fileStateAtom.set({ type: "error", message: (e as Error).message });
throw e;
} finally {
fileStateAtom.set({ type: "idle" });
}
});

export const fileActions = {
openRemoteFile,
openLocalFile,
};
2 changes: 1 addition & 1 deletion src/core/graph/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ graphDatasetAtom.bind((graphDataset, previousGraphDataset) => {
filteredGraphsAtom.set(newCache);
}

// When graph data or fields changed changed, we reindex it for the search
// When graph data or fields changed, we reindex it for the search
if (updatedKeys.has("fullGraph") || updatedKeys.has("edgeFields") || updatedKeys.has("nodeFields")) {
searchActions.indexAll();
}
Expand Down
1 change: 1 addition & 0 deletions src/core/graph/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,4 @@ export interface GraphDataset {
// Ex: is it a local or a remote file
origin: GraphOrigin;
}
export type FileState = { type: "idle" } | { type: "loading" } | { type: "error"; message?: string };
62 changes: 0 additions & 62 deletions src/core/graph/useOpenGexf.ts

This file was deleted.

11 changes: 1 addition & 10 deletions src/core/graph/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,7 @@ describe("Graph utilities", () => {

it("should properly detect separators", () => {
expect(
inferFieldType(
[
"TypeScript",
"Neo4J,TypeScript",
"Python,TypeScript",
"TypeScript,Python",
"TypeScript",
],
5,
),
inferFieldType(["TypeScript", "Neo4J,TypeScript", "Python,TypeScript", "TypeScript,Python", "TypeScript"], 5),
).toEqual({
quantitative: null,
qualitative: { separator: "," },
Expand Down
5 changes: 5 additions & 0 deletions src/core/graph/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
DatalessGraph,
EdgeRenderingData,
FieldModel,
FileState,
FullGraph,
GraphDataset,
ItemData,
Expand All @@ -35,6 +36,10 @@ export function getEmptyGraphDataset(): GraphDataset {
};
}

export function getEmptyFileState(): FileState {
return { type: "idle" };
}

/**
* Appearance lifecycle helpers (state serialization / deserialization):
*/
Expand Down
7 changes: 3 additions & 4 deletions src/core/metrics/collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ import { toNumber } from "../utils/casting";

// Definition of a custom metric function for nodes
// eslint-disable-next-line no-new-func
const nodeMetricCustomFn = new Function(`return (
const nodeMetricCustomFn = new Function(`return (
function nodeMetric(id, attributes, index, graph) {
// Your code goes here
return Math.random();
}
}
)`)();

// Definition of a custom metric function for edges
// eslint-disable-next-line no-new-func
const edgeMetricCustomFn = new Function(`return (
const edgeMetricCustomFn = new Function(`return (
function edgeMetric(id, attributes, index, graph) {
// Your code goes here
return Math.random();
Expand Down Expand Up @@ -256,7 +256,6 @@ export const NODE_METRICS: Metric<"nodes", any, any>[] = [
const id = fullGraph.nodes()[0];
const attributs = fullGraph.getNodeAttributes(id);
const result = fn(id, attributs, 0, fullGraph);
console.log(isNumber(result), isString(result));
if (!isNumber(result) && !isString(result))
throw new Error("Function must return either a number or a string");
},
Expand Down
2 changes: 1 addition & 1 deletion src/core/utils/atoms.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe("Atoms library", () => {
});

describe("#derivedAtom", () => {
it("should be updated when depencies are updated", () => {
it("should be updated when dependencies are updated", () => {
const a1 = atom(0);
const a2 = atom(0);
const sum = derivedAtom([a1, a2], (v1, v2, previousValue) => v1 + v2);
Expand Down
17 changes: 15 additions & 2 deletions src/core/utils/producers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@ import { WritableAtom } from "./atoms";

export type Reducer<T> = (v: T) => T;

export type Action<Args extends unknown[] = []> = (...args: Args) => void;
export type AsyncAction<Args extends unknown[]> = (...args: Args) => Promise<void>;

/**
* A short function to help automatically type an AsyncAction Args generic.
*/
export function asyncAction<Args extends unknown[]>(fn: (...args: Args) => Promise<void>) {
return fn as AsyncAction<Args>;
}

/**
* A Producer is a function that returns a reducer. These are easy to test, and
* it is easy to create mutating actions from them as well.
Expand All @@ -17,7 +27,10 @@ export type MultiProducer<Ts extends Array<unknown>, Args extends unknown[] = []
* This allows writing the logic as unit-testable producers, but spread through
* the code simple actions.
*/
export function producerToAction<T, Args extends unknown[] = []>(producer: Producer<T, Args>, atom: WritableAtom<T>) {
export function producerToAction<T, Args extends unknown[] = []>(
producer: Producer<T, Args>,
atom: WritableAtom<T>,
): Action<Args> {
return (...args: Args) => {
atom.set(producer(...args));
};
Expand All @@ -27,7 +40,7 @@ export function multiproducerToAction<Ts extends Array<unknown>, Args extends un
atoms: {
[K in keyof Ts]: WritableAtom<Ts[K]>;
},
) {
): Action<Args> {
return (...args: Args) => {
const reducers = producer(...args);
atoms.forEach((atom, i) => atom.set(reducers[i]));
Expand Down
1 change: 0 additions & 1 deletion src/views/graphPage/StatisticsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,6 @@ export const MetricForm: FC<{ metric: Metric<any, any, any>; onClose: () => void
checkFunction: param.functionCheck,
},
beforeSubmit: ({ run, script }) => {
console.log(script);
onChange("parameters", param.id, script);
if (run) setTimeout(submit, 0);
},
Expand Down
Loading

0 comments on commit 96f0809

Please sign in to comment.