Skip to content

Commit

Permalink
Export and import graph (#781)
Browse files Browse the repository at this point in the history
* Add @types/wicg-file-system-access

* Add file saving helper to use native save dialog

* Implement FileButton locally, fixing bug choosing same file

* Fix layout when clearing and loading the same graph

* Import and export graph data

* Update changelog

* Change “import” to “load”

* Prefer not found message when all not found

* Improved default file name

* Fix lint error
  • Loading branch information
kmcginnes authored Feb 10, 2025
1 parent 9ba2a68 commit 13f4f09
Show file tree
Hide file tree
Showing 16 changed files with 1,124 additions and 31 deletions.
22 changes: 12 additions & 10 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

## Upcoming

- **Added** ability to save the rendered graph to a file, allowing for reloading
the graph later or sharing the graph with other users who have the same
connection ([#756](https://github.com/aws/graph-explorer/pull/756),
[#758](https://github.com/aws/graph-explorer/pull/758),
[#761](https://github.com/aws/graph-explorer/pull/761),
[#762](https://github.com/aws/graph-explorer/pull/762),
[#767](https://github.com/aws/graph-explorer/pull/767),
[#768](https://github.com/aws/graph-explorer/pull/768),
[#769](https://github.com/aws/graph-explorer/pull/769),
[#770](https://github.com/aws/graph-explorer/pull/770),
[#775](https://github.com/aws/graph-explorer/pull/775),
[#781](https://github.com/aws/graph-explorer/pull/781))
- **Updated** UI labels to refer to node & edge "labels" instead of "types"
([#766](https://github.com/aws/graph-explorer/pull/766))
- **Improved** neighbor count retrieval to be more efficient
Expand All @@ -15,16 +27,6 @@
([#743](https://github.com/aws/graph-explorer/pull/743))
- **Improved** pagination controls by using a single shared component
([#742](https://github.com/aws/graph-explorer/pull/742))
- **Updated** graph foundations to accommodate loading a graph from a set of IDs
([#756](https://github.com/aws/graph-explorer/pull/756),
[#758](https://github.com/aws/graph-explorer/pull/758),
[#761](https://github.com/aws/graph-explorer/pull/761),
[#762](https://github.com/aws/graph-explorer/pull/762),
[#767](https://github.com/aws/graph-explorer/pull/767),
[#768](https://github.com/aws/graph-explorer/pull/768),
[#769](https://github.com/aws/graph-explorer/pull/769),
[#770](https://github.com/aws/graph-explorer/pull/770),
[#775](https://github.com/aws/graph-explorer/pull/775))
- **Updated** styling across the app
([#777](https://github.com/aws/graph-explorer/pull/777),
[#743](https://github.com/aws/graph-explorer/pull/743),
Expand Down
1 change: 1 addition & 0 deletions packages/graph-explorer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@react-stately/list": "^3.11.2",
"@tanstack/react-query": "^5.64.2",
"@tanstack/react-query-devtools": "^5.64.2",
"@types/wicg-file-system-access": "^2023.10.5",
"clsx": "^2.1.1",
"color": "^4.2.3",
"crypto-js": "^4.2.0",
Expand Down
62 changes: 62 additions & 0 deletions packages/graph-explorer/src/components/FileButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import Button from "./Button";

export interface FileButtonProps
extends React.ComponentPropsWithoutRef<typeof Button> {
asChild?: boolean;
onChange?: (files: FileList | null) => void;
accept?: string;
multiple?: boolean;
}

/**
* A wrapper around whatever button you want to open a file dialog. It will
* automatically open the file dialog when clicked.
*
* @example
* <FileButton onChange={files => console.log(files)} asChild>
* <Button>Open File</Button>
* </FileButton>
*/
export const FileButton = React.forwardRef<HTMLButtonElement, FileButtonProps>(
(
{ asChild, onChange, accept, multiple, isDisabled, children, ...props },
ref
) => {
const inputRef = React.useRef<HTMLInputElement | null>(null);

const handleClick = () => {
!isDisabled && inputRef.current?.click();
};

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
onChange?.(files);
// Reset the input value to allow selecting the same file again
event.target.value = "";
};

const Component = asChild ? (Slot as any) : Button;

return (
<>
<Component ref={ref} type="button" onClick={handleClick} {...props}>
{children}
</Component>
<input
ref={inputRef}
type="file"
accept={accept}
multiple={multiple}
onChange={handleChange}
disabled={isDisabled}
className="hidden"
aria-hidden
/>
</>
);
}
);

FileButton.displayName = "FileButton";
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,11 @@ function useUpdateLayout({
node.unlock();
});
}

previousNodesRef.current = new Set(nodesInGraph.map(node => node.id()));
previousLayoutRef.current = layout;
}

// Ensure the previousNodesRef is updated on every run
previousNodesRef.current = new Set(nodesInGraph.map(node => node.id()));
}, [
cy,
layout,
Expand Down
2 changes: 2 additions & 0 deletions packages/graph-explorer/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export { default as PanelError } from "./PanelError";

export { default as Divider } from "./Divider";

export * from "./FileButton";

export { default as Graph } from "./Graph";
export * from "./Graph";

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useCallback } from "react";
import { useRecoilValue } from "recoil";
import { SaveIcon } from "lucide-react";
import { nodesAtom, edgesAtom, useExplorer, useConfiguration } from "@/core";
import { saveFile, toJsonFileData } from "@/utils/fileData";
import { PanelHeaderActionButton } from "@/components";
import { createDefaultFileName, createExportedGraph } from "./exportedGraph";

export function ExportGraphButton() {
const exportGraph = useExportGraph();

return (
<PanelHeaderActionButton
icon={<SaveIcon />}
label="Save graph to file"
onActionClick={() => exportGraph()}
/>
);
}

export function useExportGraph() {
const vertexIds = useRecoilValue(nodesAtom).keys().toArray();
const edgeIds = useRecoilValue(edgesAtom).keys().toArray();
const connection = useExplorer().connection;
const config = useConfiguration();

const exportGraph = useCallback(async () => {
const fileName = createDefaultFileName(
config?.displayLabel ?? "Connection"
);
const exportData = createExportedGraph(vertexIds, edgeIds, connection);
const fileToSave = toJsonFileData(exportData);
await saveFile(fileToSave, fileName);
}, [config?.displayLabel, connection, vertexIds, edgeIds]);

return exportGraph;
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ import useGraphStyles from "./useGraphStyles";
import useNodeBadges from "./useNodeBadges";
import { SelectedElements } from "@/components/Graph/Graph.model";
import { useAutoOpenDetailsSidebar } from "./useAutoOpenDetailsSidebar";
import { ImportGraphButton } from "./ImportGraphButton";
import { ExportGraphButton } from "./ExportGraphButton";
import {
BadgeInfoIcon,
CircleSlash2,
Expand Down Expand Up @@ -204,6 +206,8 @@ export default function GraphViewer({
icon={<ImageDownIcon />}
onActionClick={onSaveScreenshot}
/>
<ExportGraphButton />
<ImportGraphButton />
<PanelHeaderDivider />
<PanelHeaderActionButton
label="Zoom in"
Expand Down
Loading

0 comments on commit 13f4f09

Please sign in to comment.