Skip to content

Commit

Permalink
Local storage (#75)
Browse files Browse the repository at this point in the history
* (feat) Allow network switching based on localStorage

* (feat) Switch networks menu now appears on error pages.
(fix) Nicer spacing for the switch networks menu
(fix) The network menu legend now tells you which network you are on

* (fix) Formatting and type fixes

* (fix) Prettier + fix remaining build issues

* (feat) You can now type new network locations directly

* (fix) Ability to set the name of a network by parameter
(fix) Remove parameters when selecting a network

* (feat) Very simple docs

* (fix) Fix formatting
  • Loading branch information
rrw-zilliqa authored Dec 18, 2024
1 parent a6f9e0c commit cd51621
Show file tree
Hide file tree
Showing 16 changed files with 442 additions and 106 deletions.
5 changes: 5 additions & 0 deletions README.zilliqa.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ ignore changes to them, they are generated by `npm start` as well as
Be warned! If you use `vite` directly, you may end up with analysis
errors due to their absence.

## Parameters

You can now pass `network=` and `name=` parameters to preload a network into `localStorage`.
For large sets of prewritten parameters, there is a list of connection objects in `config.json`.

## Starting for development

.. because I keep forgetting!
Expand Down
10 changes: 8 additions & 2 deletions public/config.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
{
"erigonURL": "http://localhost:8545",
"assetsURLPrefix": "http://localhost:5175"
}
"assetsURLPrefix": "http://localhost:5175",
"connections": [
{ "menuName": "zq1-mainnet", "url": "https://mainnet-v934-fireblocks.mainnet-20240103-ase1.zq1.network" },
{ "menuName": "zq1-testnet", "url": "https://testnet-v932-fireblocks.testnet-ase1.zq1.dev" },
{ "menuName": "zq2-prototestnet", "url": "https://api.zq2-prototestnet.zilliqa.com" },
{ "menuName": "zq2-protomainnet", "url": "https://api.zq2-protomainnet.zilliqa.com" }
]
}
6 changes: 5 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -255,11 +255,15 @@ const Layout: FC = () => {
<ConnectionErrorPanel
connStatus={ConnectionStatus.CONNECTING}
nodeURL={config.erigonURL!}
config={config}
/>
)
}
>
<Await resolve={data.rt} errorElement={<ProbeErrorHandler />}>
<Await
resolve={data.rt}
errorElement={<ProbeErrorHandler config={config} />}
>
{(runtime) => (
// App is healthy from here
<QueryClientProvider client={queryClient}>
Expand Down
18 changes: 17 additions & 1 deletion src/ConnectionErrorPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import {
faBarsProgress,
faCheckCircle,
faClock,
faTimesCircle,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { FC, PropsWithChildren, memo } from "react";
import NetworkMenuWithConfig from "./NetworkMenuWithConfig";
import { ConnectionStatus } from "./types";
import { OtterscanConfig } from "./useConfig";

type ConnectionErrorPanelProps = {
connStatus: ConnectionStatus;
nodeURL: string;
config: OtterscanConfig;
};

const ConnectionErrorPanel: FC<ConnectionErrorPanelProps> = ({
connStatus,
nodeURL,
config,
}) => {
return (
<div className="flex h-screen flex-col font-sans">
Expand Down Expand Up @@ -51,13 +56,19 @@ const ConnectionErrorPanel: FC<ConnectionErrorPanelProps> = ({
</Step>
</>
)}
<div className="flex space-x-2 mt-2">
<span className="text-blue-600">
<FontAwesomeIcon icon={faBarsProgress} size="1x" />
</span>
<NetworkMenuWithConfig config={config} />
</div>
</div>
</div>
);
};

type StepProps = {
type: "wait" | "ok" | "error";
type: "wait" | "ok" | "error" | "change";
msg: string;
};

Expand All @@ -80,6 +91,11 @@ const Step: FC<PropsWithChildren<StepProps>> = memo(
<FontAwesomeIcon icon={faTimesCircle} size="1x" />
</span>
)}
{type === "change" && (
<span className="text-blue-600">
<FontAwesomeIcon icon={faBarsProgress} size="1x" />
</span>
)}
<span>{msg}</span>
</div>
{children && <div className="ml-7 mt-4 text-sm">{children}</div>}
Expand Down
7 changes: 4 additions & 3 deletions src/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { faQrcode, faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { FC, lazy, memo, useContext, useState } from "react";
import { Link } from "react-router-dom";
import NetworkMenu from "./NetworkMenu";
import PriceBox from "./PriceBox";
import SourcifyMenu from "./SourcifyMenu";
import InlineCode from "./components/InlineCode";
Expand Down Expand Up @@ -41,9 +42,9 @@ const Header: FC<HeaderProps> = ({ sourcifyPresent }) => {
</span>
</div>
</Link>
<div className="inline sm:hidden">
{sourcifyPresent && <SourcifyMenu />}
</div>
</div>
<div className="pt-2 flex items-center justify-center">
<NetworkMenu />
</div>
<div className="flex items-baseline gap-x-3">
{(provider._network.chainId === 1n ||
Expand Down
2 changes: 1 addition & 1 deletion src/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const Home: FC = () => {

return (
<>
<Header sourcifyPresent={false} />
<Header sourcifyPresent={true} />
<div className="mx-1 my-1">
<ChainInfo />
</div>
Expand Down
10 changes: 10 additions & 0 deletions src/NetworkMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React, { useContext } from "react";
import NetworkMenuWithConfig from "./NetworkMenuWithConfig.tsx";
import { RuntimeContext } from "./useRuntime";

const NetworkMenu: React.FC = () => {
let { config } = useContext(RuntimeContext);
return <NetworkMenuWithConfig config={config} />;
};

export default React.memo(NetworkMenu);
212 changes: 212 additions & 0 deletions src/NetworkMenuWithConfig.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/react";
import React, { FC, PropsWithChildren, useState } from "react";
import InlineCode from "./components/InlineCode";
import {
OtterscanConfig,
chooseConnection,
deleteParametersFromLocation,
newConnection,
} from "./useConfig";

type NetworkMenuWithConfigProps = {
config: OtterscanConfig;
};

// className="flex h-full w-full items-center justify-center space-x-2 rounded border px-2 py-1 text-sm">
const NetworkMenuWithConfig: FC<NetworkMenuWithConfigProps> = ({ config }) => {
const [goToOpen, setGoToOpen] = useState<boolean>(false);
const [connectUrl, setConnectUrl] = useState<string>("");
const [connectName, setConnectName] = useState<string>("");
let connections = config.connections;
if (connections === undefined) {
return (
<Menu>
<div className="relative self-stretch h-full">
<MenuButton className="flex items-center justify-center space-x-2 rounded border px-2 py-1 text-sm">
No Networks
</MenuButton>
<MenuItems> </MenuItems>
</div>
</Menu>
);
}

async function newNetwork(name: string, url: string) {
let result = await newConnection(config, connectName, connectUrl);
goToNetwork(name);
}

async function goToNetwork(name: string) {
console.log("Switch to network " + name);
const result = await chooseConnection(config, name);
if (result) {
console.log("Connection changed. Reloading .. ");
await deleteParametersFromLocation();
//window.location.reload();
}
}
var legend =
connections.find((elem) => elem?.url == config?.erigonURL)?.menuName ??
"Networks";

const connectionItems = connections.map((conn) => (
<div key={conn?.menuName}>
<NetworkMenuItem
onClick={() => goToNetwork(conn?.menuName)}
checked={conn?.url === config.erigonURL}
name={conn?.menuName}
url={conn?.url ?? "(none)"}
></NetworkMenuItem>
</div>
));
return (
<>
<Menu>
<div className="relative self-stretch h-full">
<MenuButton className="flex items-center justify-center space-x-2 rounded border px-2 py-1 text-sm">
{legend}
</MenuButton>
<MenuItems className="absolute left-0 mt-1 flex min-w-max flex-col rounded-b border bg-white p-1 text-sm">
{connectionItems}
<NetworkSetItem onClick={() => setGoToOpen(true)} />
</MenuItems>
</div>
</Menu>
{goToOpen && (
<div
className="fixed inset-0 z-10 overflow-y-auto"
aria-labelledby="modal-title"
role="dialog"
aria-modal="true"
>
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div
className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"
aria-hidden="true"
></div>

<span
className="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true"
>
&#8203;
</span>

<div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:w-full sm:align-middle sm:max-w-lg">
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:items-start">
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3
className="text-lg leading-6 font-medium text-gray-900"
id="modal-title"
>
Connect to a network
</h3>
<div className="w-full mt-2 w-full">
<div className="text-sm">
{" "}
You can also configure these by giving the{" "}
<InlineCode>network</InlineCode> URL parameter to
indicate a URL and (optionally){" "}
<InlineCode>name</InlineCode> for the name
</div>
<div className="text-sm text-gray-500 w-full m-4">
Name:{" "}
<input
className="border ml-4 border-gray-400"
id="name"
onChange={(e) => setConnectName(e.target.value)}
></input>
</div>
<div className="text-sm w-full m-4 min-w-[600px]">
URL:{" "}
<input
id="url"
className="min-w-[40ch] border ml-4 border-gray-400"
onChange={(e) => setConnectUrl(e.target.value)}
></input>
</div>
</div>
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button
type="button"
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
onClick={() => {
setGoToOpen(false);
}}
>
Close
</button>
<button
type="button"
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
onClick={() => {
if (!newNetwork(connectName, connectUrl)) {
alert("Connection not found?!");
}
setGoToOpen(false);
}}
>
Connect
</button>
</div>
</div>
</div>
</div>
)}
</>
);
};

type NetworkSetItemProps = {
onClick: (event?: any) => void;
};

export const NetworkSetItem: React.FC<NetworkSetItemProps> = ({ onClick }) => {
return (
<MenuItem>
{({ focus }) => (
<div
className={`px-2 py-1 text-left text-sm ${
focus ? "border-zq-lightblue text-gray-500" : "text-gray-400"
} transition-colors transition-transform duration-75`}
>
<button name="Connect" onClick={onClick}>
Connect
</button>
</div>
)}
</MenuItem>
);
};

type NetworkMenuItemProps = {
checked?: boolean;
name: string;
url: string;
onClick: (event?: any) => void;
};

export const NetworkMenuItem: React.FC<
PropsWithChildren<NetworkMenuItemProps>
> = ({ checked, name, url, onClick }) => (
<MenuItem>
{({ focus }) => (
<button
className={`px-2 py-1 text-left text-sm ${
focus ? "border-zq-lightblue text-gray-500" : "text-gray-400"
} transition-colors transition-transform duration-75 ${
checked ? "text-gray-900" : ""
}`}
onClick={onClick}
>
{name} / {url}
</button>
)}
</MenuItem>
);

export default React.memo(NetworkMenuWithConfig);
18 changes: 15 additions & 3 deletions src/ProbeErrorHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,26 @@ import { FC } from "react";
import { useAsyncError } from "react-router-dom";
import ConnectionErrorPanel from "./ConnectionErrorPanel";
import { ProbeError } from "./ProbeError";
import { OtterscanConfig } from "./useConfig";

const ProbeErrorHandler: FC = () => {
type ProbeErrorHandlerProps = {
config: OtterscanConfig;
};

const ProbeErrorHandler: FC<ProbeErrorHandlerProps> = ({
config,
}: ProbeErrorHandlerProps) => {
const err = useAsyncError();
if (!(err instanceof ProbeError)) {
throw err;
}

return <ConnectionErrorPanel connStatus={err.status} nodeURL={err.nodeURL} />;
return (
<ConnectionErrorPanel
connStatus={err.status}
nodeURL={err.nodeURL}
config={config}
/>
);
};

export default ProbeErrorHandler;
Loading

0 comments on commit cd51621

Please sign in to comment.