Skip to content

Commit

Permalink
Merge pull request #1 from mrv777/develop
Browse files Browse the repository at this point in the history
fix: Add toasts, more table info, readme update
  • Loading branch information
mrv777 authored Oct 11, 2024
2 parents c43c02f + 867e797 commit bf74345
Show file tree
Hide file tree
Showing 10 changed files with 147 additions and 234 deletions.
215 changes: 18 additions & 197 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
# Tauri + Next.js Template
# BitHive

![Tauri window screenshot](public/tauri-nextjs-template_screenshot.png)
BitHive is a cross-platform desktop application built with Tauri, Next.js, and React. It combines the power of a Rust backend with a modern React frontend to deliver a fast and efficient developer tool.

This is a [Tauri](https://tauri.app/) project template using [Next.js](https://nextjs.org/),
bootstrapped by combining [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app)
and [`create tauri-app`](https://tauri.app/v1/guides/getting-started/setup).
## Features

- Cross-platform support (macOS, Windows, Linux)
- Built with Tauri for a lightweight and secure desktop application
- Modern web technologies (Next.js, React) for the frontend
- Tailwind CSS and DaisyUI for styling
- React Query for efficient data fetching and state management
- Recharts for data visualization

## Screenshots

![BitHive Screenshot](./images/Bithive_screenshot.png)

This template uses [`pnpm`](https://pnpm.io/) as the Node.js dependency
manager.
### Development Info

## Template Features
This is a [Tauri](https://tauri.app/) project template using [Next.js](https://nextjs.org/),
bootstrapped by combining [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) and [`create tauri-app`](https://tauri.app/v1/guides/getting-started/setup).

- TypeScript frontend using Next.js React framework
- [TailwindCSS](https://tailwindcss.com/) as a utility-first atomic CSS framework
Expand All @@ -25,192 +34,4 @@ manager.
- [clippy](https://github.com/rust-lang/rust-clippy) and
[rustfmt](https://github.com/rust-lang/rustfmt) for Rust code
- GitHub Actions to check code formatting and linting for both TypeScript and Rust

## Getting Started

### Running development server and use Tauri window

After cloning for the first time, set up git pre-commit hooks:

```shell
pnpm prepare
```

To develop and run the frontend in a Tauri window:

```shell
pnpm dev
```

This will load the Next.js frontend directly in a Tauri webview window, in addition to
starting a development server on `localhost:3000`.

### Building for release

To export the Next.js frontend via SSG and build the Tauri application for release:

```shell
pnpm build
```

Please remember to change the bundle identifier in
`tauri.conf.json > tauri > bundle > identifier`, as the default value will yield an
error that prevents you from building the application for release.

### Source structure

Next.js frontend source files are located in `src/` and Tauri Rust application source
files are located in `src-tauri/`. Please consult the Next.js and Tauri documentation
respectively for questions pertaining to either technology.

## Caveats

### Static Site Generation / Pre-rendering

Next.js is a great React frontend framework which supports server-side rendering (SSR)
as well as static site generation (SSG or pre-rendering). For the purposes of creating a
Tauri frontend, only SSG can be used since SSR requires an active Node.js server.

Using Next.js and SSG helps to provide a quick and performant single-page-application
(SPA) frontend experience. More information regarding this can be found here:
https://nextjs.org/docs/basic-features/pages#pre-rendering

### `next/image`

The [`next/image` component](https://nextjs.org/docs/basic-features/image-optimization)
is an enhancement over the regular `<img>` HTML element with additional optimizations
built in. However, because we are not deploying the frontend onto Vercel directly, some
optimizations must be disabled to properly build and export the frontend via SSG.
As such, the
[`unoptimized` property](https://nextjs.org/docs/api-reference/next/image#unoptimized)
is set to true for the `next/image` component in the `next.config.js` configuration.
This will allow the image to be served as-is from source, without
changes to its quality, size, or format.

### error[E0554]: `#![feature]` may not be used on the stable release channel

If you are getting this issue when trying to run `pnpm tauri dev`, it may be that you
have a newer version of a Rust dependency that uses an unstable feature.
`pnpm tauri build` should still work for production builds, but to get the dev command
working, either downgrade the dependency or use Rust nightly via
`rustup override set nightly`.

### ReferenceError: navigator is not defined

If you are using Tauri's `invoke` function or any OS related Tauri function from within
JavaScript, you may encounter this error when importing the function in a global,
non-browser context. This is due to the nature of Next.js' dev server effectively
running a Node.js server for SSR and hot module replacement (HMR), and Node.js does not
have a notion of `window` or `navigator`.

#### Solution 1 - Dependency Injection (may not always work)

Make sure that you are calling these functions within the browser context, e.g. within a
React component inside a `useEffect` hook when the DOM actually exists by then. If you
are trying to use a Tauri function in a generalized utility source file, a workaround is
to use dependency injection for the function itself to delay the actual importing of the
real function (see example below for more info).

Example using Tauri's `invoke` function:

`src/lib/some_tauri_functions.ts` (problematic)

```typescript
// Generalized file containing all the invoke functions we need to fetch data from Rust
import { invoke } from "@tauri-apps/api/tauri"

const loadFoo = (): Promise<string> => {
return invoke<string>("invoke_handler_foo")
}

const loadBar = (): Promise<string> => {
return invoke<string>("invoke_handler_bar")
}

const loadBaz = (): Promise<string> => {
return invoke<string>("invoke_handler_baz")
}

// and so on ...
```

`src/lib/some_tauri_functions.ts` (fixed)

```typescript
// Generalized file containing all the invoke functions we need to fetch data from Rust
//
// We apply the idea of dependency injection to use a supplied invoke function as a
// function argument, rather than directly referencing the Tauri invoke function.
// Hence, don't import invoke globally in this file.
//
// import { invoke } from "@tauri-apps/api/tauri" <-- remove this!
//

import { InvokeArgs } from "@tauri-apps/api/tauri"
type InvokeFunction = <T>(cmd: string, args?: InvokeArgs | undefined) => Promise<T>

const loadFoo = (invoke: InvokeFunction): Promise<string> => {
return invoke<string>("invoke_handler_foo")
}

const loadBar = (invoke: InvokeFunction): Promise<string> => {
return invoke<string>("invoke_handler_bar")
}

const loadBaz = (invoke: InvokeFunction): Promise<string> => {
return invoke<string>("invoke_handler_baz")
}

// and so on ...
```

Then, when using `loadFoo`/`loadBar`/`loadBaz` within your React components, import the
invoke function from `@tauri-apps/api` and pass `invoke` into the loadXXX function as
the `InvokeFunction` argument. This should allow the actual Tauri API to be bundled
only within the context of a React component, so it should not be loaded by Next.js upon
initial startup until the browser has finished loading the page.

#### Solution 2: Wrap Tauri API behind dynamic `import()`

Since the Tauri API needs to read from the browser's `window` and `navigator` object,
this data does not exist in a Node.js and hence SSR environment. One can create an
exported function that wraps the Tauri API behind a dynamic runtime `import()` call.

Example: create a `src/lib/tauri.ts` to re-export `invoke`

```typescript
import type { InvokeArgs } from "@tauri-apps/api/tauri"

const isNode = (): boolean =>
Object.prototype.toString.call(typeof process !== "undefined" ? process : 0) ===
"[object process]"

export async function invoke<T>(
cmd: string,
args?: InvokeArgs | undefined,
): Promise<T> {
if (isNode()) {
// This shouldn't ever happen when React fully loads
return Promise.resolve(undefined as unknown as T)
}
const tauriAppsApi = await import("@tauri-apps/api")
const tauriInvoke = tauriAppsApi.invoke
return tauriInvoke(cmd, args)
}
```

Then, instead of importing `import { invoke } from "@tauri-apps/api/tauri"`, use invoke
from `import { invoke } from "@/lib/tauri"`.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and
API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

And to learn more about Tauri, take a look at the following resources:

- [Tauri Documentation - Guides](https://tauri.app/v1/guides/) - learn about the Tauri
toolkit.
- pnpm as the package manager
Binary file added images/Bithive_screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bithive",
"version": "0.1.0",
"version": "0.1.1",
"private": true,
"author": {
"name": "MrV",
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
},
"package": {
"productName": "BitHive",
"version": "0.1.0"
"version": "0.1.1"
},
"tauri": {
"allowlist": {
Expand Down
2 changes: 1 addition & 1 deletion src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ const Header: React.FC<HeaderProps> = () => {
<path strokeLinecap="round" strokeLinejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>
</button>
<button onClick={resetLocalStorage} className="btn btn-ghost btn-circle tooltip tooltip-left flex items-center justify-center" data-tip="Reset Local Storage">
<button onClick={resetLocalStorage} className="btn btn-ghost btn-circle tooltip tooltip-left flex items-center justify-center" data-tip="Reset Everything">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="red" className="w-5 h-5">
<path strokeLinecap="round" strokeLinejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
</svg>
Expand Down
32 changes: 26 additions & 6 deletions src/components/HiveDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ const HiveDashboard: React.FC = () => {

combined.hashRate = combined.hashRate > 0 ? combined.hashRate : null;

const devicesWithData = statusQueries.filter((query: any) => query.data).length;
const devicesWithData = statusQueries.filter(
(query: any) => query.data
).length;
const devicesWithoutData = hiveData.length - devicesWithData;

return { combinedData: combined, devicesWithData, devicesWithoutData };
Expand Down Expand Up @@ -121,8 +123,9 @@ const HiveDashboard: React.FC = () => {
const updatedHive = [...hiveData, newIp];
updateHiveData(updatedHive);
await queryClient.invalidateQueries();
showToast(`Miner ${newIp} added successfully`, "success");
} catch (error) {
console.error("Failed to add to hive:", error);
showToast(`Failed to add miner ${newIp}`, "error");
}
}
},
Expand All @@ -135,11 +138,21 @@ const HiveDashboard: React.FC = () => {
updateHiveData(updatedHive);
// Refetch hive data
await queryClient.invalidateQueries();
showToast(`Miner ${ip} removed successfully`, "success");
} catch (error) {
console.error("Failed to remove from hive:", error);
showToast(`Failed to remove miner ${ip}`, "error");
}
};

const [toasts, setToasts] = useState<Array<{ message: string; type: "success" | "error" }>>([]);

const showToast = (message: string, type: "success" | "error") => {
setToasts(prevToasts => [...prevToasts, { message, type }]);
setTimeout(() => {
setToasts(prevToasts => prevToasts.slice(1));
}, 5000);
};

return (
<div>
{/* Display errors for any statusQueries with isError */}
Expand Down Expand Up @@ -200,9 +213,9 @@ const HiveDashboard: React.FC = () => {
<input
type="checkbox"
checked={isVisible}
onChange={() =>
{ toggleColumn(column as keyof typeof visibleColumns); }
}
onChange={() => {
toggleColumn(column as keyof typeof visibleColumns);
}}
className="checkbox"
/>
</label>
Expand All @@ -224,6 +237,13 @@ const HiveDashboard: React.FC = () => {
<div className="mt-4">
<AddMinerForm onSubmit={addMiner} />
</div>
<div className="toast toast-top toast-end">
{toasts.map((toast, index) => (
<div key={index} className={`alert alert-${toast.type}`}>
<span>{toast.message}</span>
</div>
))}
</div>
</div>
);
};
Expand Down
12 changes: 7 additions & 5 deletions src/components/MinerStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Tooltip,
ResponsiveContainer,
} from "recharts";
import { calculateExpectedHashRate } from "@/utils/helpers";

// Add this utility function at the top of the file
const formatDifficulty = (value: number): string => {
Expand Down Expand Up @@ -54,7 +55,6 @@ const MinerStatus: React.FC<MinerStatusProps> = ({ data, ready }) => {
const dataIndexRef = useRef<number>(0); // Change this line

useEffect(() => {
if (!ready) return;
// Load data from local storage on component mount
const storedData = localStorage.getItem(
`minerStatusData-${data.hostname}-${data.stratumUser}`
Expand All @@ -79,8 +79,10 @@ const MinerStatus: React.FC<MinerStatusProps> = ({ data, ready }) => {
};

// Calculate expected hash rate
const calculatedExpectedHashRate = Math.floor(
data.frequency * ((data.smallCoreCount * data.asicCount) / 1000)
const calculatedExpectedHashRate = calculateExpectedHashRate(
data.frequency,
data.smallCoreCount,
data.asicCount
);
setExpectedHashRate(calculatedExpectedHashRate);

Expand Down Expand Up @@ -208,7 +210,7 @@ const MinerStatus: React.FC<MinerStatusProps> = ({ data, ready }) => {
className={`stat-value ${
data.temp >= 68
? "text-red-500"
: data.temp >= 60
: data.temp >= 62
? "text-yellow-500"
: "text-green-500"
}`}
Expand All @@ -218,7 +220,7 @@ const MinerStatus: React.FC<MinerStatusProps> = ({ data, ready }) => {
<div className="stat-desc">
{data.temp >= 68
? "Warning: Overheating"
: data.temp >= 60
: data.temp >= 62
? "Caution"
: "Healthy"}
</div>
Expand Down
Loading

0 comments on commit bf74345

Please sign in to comment.