Skip to content

Feature: added custom app example using React 19 #33

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion ui/app/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,22 @@
Custom UI apps can be added here use the manager UI app as a template.
# Custom UI apps

In this folder, you can add custom (web) applications that will be shipped along with OpenRemote.
For example, special mobile apps for end users, or apps for less-technical consumers are widespread.

Developing these custom apps is pretty straightforward, thanks to the built-in packages we provide.
These make communicating with OpenRemote easier, and allows developers to quickly set up an user interface.

## Example apps

We provided several example apps to get familiar with the architecture.
All apps can be ran using `npm run serve`, and visited at http://localhost:9000/custom/.
Here's a list of the apps, and what they do;

### /custom
This is an example web application built with [Lit Web Components](https://lit.dev) and [Webpack](https://webpack.js.org).
Apps in our main OpenRemote [repository](https://github.com/openremote/openremote) are built with these technologies as well.
It can be used as a template to add your own pages on top of it.

### /custom-app-react
This is an example web application built with [React 19](https://react.dev) and [RSPack](https://rspack.rs).
*(more information soon)*
13 changes: 13 additions & 0 deletions ui/app/custom-app-react/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Local
.DS_Store
*.local
*.log*

# Dist
node_modules
dist/

# IDE
.vscode/*
!.vscode/extensions.json
.idea
12 changes: 12 additions & 0 deletions ui/app/custom-app-react/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/react.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Custom app example using React</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
30 changes: 30 additions & 0 deletions ui/app/custom-app-react/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "@openremote/custom-app-react",
"version": "1.0.0",
"description": "OpenRemote Custom App using React",
"author": "OpenRemote",
"license": "AGPL-3.0-or-later",
"private": true,
"scripts": {
"build": "cross-env NODE_ENV=production rspack build",
"serve": "cross-env NODE_ENV=development rspack serve"
},
"dependencies": {
"@openremote/core": "^1.6.5",
"@openremote/model": "^1.6.5",
"@openremote/or-mwc-components": "^1.6.5",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@rspack/cli": "^1.1.8",
"@rspack/core": "^1.1.8",
"@rspack/plugin-react-refresh": "1.0.0",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"cross-env": "^7.0.3",
"react-refresh": "^0.14.0",
"ts-node": "^10.9.2",
"typescript": "^5.6.3"
}
}
80 changes: 80 additions & 0 deletions ui/app/custom-app-react/rspack.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { defineConfig } from "@rspack/cli";
import { rspack } from "@rspack/core";
import * as RefreshPlugin from "@rspack/plugin-react-refresh";

const isDev = process.env.NODE_ENV === "development";

// Target browsers, see: https://github.com/browserslist/browserslist
const targets = ["chrome >= 87", "edge >= 88", "firefox >= 78", "safari >= 14"];


export default defineConfig({
context: __dirname,
devServer: {
host: "0.0.0.0",
port: 9000,
open: false
},
entry: {
main: "./src/main.tsx"
},
resolve: {
extensions: ["...", ".ts", ".tsx", ".jsx"]
},
module: {
rules: [
{
test: /\.svg$/,
type: "asset"
},
{
test: /(maplibre|mapbox|@material|gridstack|@mdi).*\.css$/, //output css as strings
type: "asset/source"
},
{
test: /\.(jsx?|tsx?)$/,
use: [
{
loader: "builtin:swc-loader",
options: {
jsc: {
parser: {
syntax: "typescript",
tsx: true
},
transform: {
react: {
runtime: "automatic",
development: isDev,
refresh: isDev
}
}
},
env: { targets }
}
}
]
}
]
},
plugins: [
new rspack.HtmlRspackPlugin({
template: "./index.html"
}),
isDev ? new RefreshPlugin() : null
].filter(Boolean),
optimization: {
minimizer: [
new rspack.SwcJsMinimizerRspackPlugin(),
new rspack.LightningCssMinimizerRspackPlugin({
minimizerOptions: { targets }
})
]
},
output: {
publicPath: isDev ? "/custom/" : "/",
},
experiments: {
css: true
}
});
41 changes: 41 additions & 0 deletions ui/app/custom-app-react/src/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}

.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}

@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

@media (prefers-reduced-motion: no-preference) {
a > .logo {
animation: logo-spin infinite 20s linear;
}
}

.card {
padding: 2em;
}

.read-the-docs {
color: #888;
}
54 changes: 54 additions & 0 deletions ui/app/custom-app-react/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useState, useRef, useEffect } from "react";
import reactLogo from "./assets/react.svg";
import {InputType, OrMwcInput} from "@openremote/or-mwc-components/or-mwc-input";
import "./App.css";

/**
* In React, for web components to be recognized, it's common to add each HTML tag in the JSX IntrinsicElements interface.
* Be aware; your IDE might still not understand these web components correctly.
*/
declare global {
namespace JSX {
interface IntrinsicElements {
"or-mwc-input": OrMwcInput
}
}
}

function App() {
const [count, setCount] = useState(0);
const buttonRef = useRef(null);
const handleButtonClick = () => setCount(count => count + 1);

useEffect(() => {
(buttonRef.current as any)?.addEventListener("or-mwc-input-changed", handleButtonClick);
return () => {
(buttonRef.current as any)?.removeEventListener("or-mwc-input-changed", handleButtonClick);
};
}, []);

return (
<div className="App">
<div>
<a href="https://reactjs.org" target="_blank" rel="noreferrer">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Rspack + React + TypeScript</h1>
<div className="card">
<button type="button" onClick={() => setCount(count => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
<or-mwc-input ref={buttonRef} type={InputType.BUTTON} outlined label="An OpenRemote button"></or-mwc-input>
</div>
<p className="read-the-docs">
Click on the React logo to learn more
</p>
</div>
);
}

export default App;
1 change: 1 addition & 0 deletions ui/app/custom-app-react/src/assets/react.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
70 changes: 70 additions & 0 deletions ui/app/custom-app-react/src/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;

color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;

font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}

a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}

body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}

h1 {
font-size: 3.2em;
line-height: 1.1;
}

button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}

@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
30 changes: 30 additions & 0 deletions ui/app/custom-app-react/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import {manager} from "@openremote/core";
import {ManagerConfig} from "@openremote/model";
import "./index.css";

/**
* Define the Manager configuration to talk with OpenRemote.
* For example, defining the realm and URL to communicate with. (these will be consumed with HTTP API calls for example)
* We also enable autoLogin to prompt a Keycloak login before the app appears.
*/
const managerConfig: ManagerConfig = {
realm: "custom",
managerUrl: "http://192.168.0.101:8080",
autoLogin: true
};

/**
* Initialize the Manager connection.
* Afterward, we can start rendering the React DOM UI.
*/
manager.init(managerConfig).then(() => {

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
});
Loading
Loading