ioBroker / adapter-react-v5

MIT License
6 stars 6 forks source link

Help ReactJS classes for adapter config

You can find demo on https://github.com/ioBroker/adapter-react-demo

Getting started

If you want to create the configuration page with ReactJS:

  1. Create github repo for adapter.
  2. execute npx create-react-app src . It will take a while.
  3. cd src
  4. Modify package.json file in src directory:
    • Change name from src to ADAPTERNAME-admin (Of course replace ADAPTERNAME with yours)
    • Add to devDependencies:
      "@iobroker/adapter-react": "^4.0.10",

      Versions can be higher. So your src/package.json should look like:

      {
      "name": "ADAPTERNAME-admin",
      "version": "0.1.0",
      "private": true,
      "dependencies": {
      "react": "^18.2.0",
      "react-dom": "^18.2.0",
      "react-icons": "^4.6.0",
      "react-scripts": "^5.0.1",
      "@iobroker/adapter-react-v5": "^3.2.7",
      "del": "^6.1.1",
      "gulp": "^4.0.2"
      },
      "scripts": {
      "start": "react-scripts start",
      "build": "react-scripts build",
      "test": "react-scripts test",
      "eject": "react-scripts eject"
      },
      "eslintConfig": {
      "extends": "react-app"
      },
      "homepage": ".",
      "browserslist": [
      ">0.2%",
      "not dead",
      "not ie <= 11",
      "not op_mini all"
      ]
      }
  5. Call in src: npm install
  6. Copy gulpfile.js into src: cp node_modules/@iobroker/adapter-react/gulpfile.js gulpfile.js
  7. Start your dummy application npm run start for developing or build with npm run build and copy files in build directory to www or to admin. In the admin you must rename index.html to index_m.html.
  8. You can do that with gulp tasks: gulp build, gulp copy, gulp renameIndex or gulp renameTab

Development

  1. Add socket.io to public/index.html. After
<link rel="manifest" href="https://github.com/ioBroker/adapter-react-v5/blob/main/%PUBLIC_URL%/manifest.json" />

insert

<script>
    var script = document.createElement('script');
    window.registerSocketOnLoad = function (cb) {
        window.socketLoadedHandler = cb;
    };
    const parts = (window.location.search || '').replace(/^\?/, '').split('&');
    const query = {};
    parts.forEach(item => {
        const [name, val] = item.split('=');
        query[decodeURIComponent(name)] = val !== undefined ? decodeURIComponent(val) : true;
    });
    script.onload = function () { typeof window.socketLoadedHandler === 'function' && window.socketLoadedHandler(); };
    script.src = window.location.port === '3000' ? window.location.protocol + '//' + (query.host || window.location.hostname) + ':' + (query.port || 8081) + '/lib/js/socket.io.js' : '%PUBLIC_URL%/../../lib/js/socket.io.js';

    document.head.appendChild(script);
</script>
  1. Add to App.js constructor initialization for I18n:

    class App extends GenericApp {
    constructor(props) {
        const extendedProps = {...props};
        extendedProps.encryptedFields = ['pass']; // this parameter will be encrypted and decrypted automatically
        extendedProps.translations = {
            'en': require('./i18n/en'),
            'de': require('./i18n/de'),
            'ru': require('./i18n/ru'),
            'pt': require('./i18n/pt'),
            'nl': require('./i18n/nl'),
            'fr': require('./i18n/fr'),
            'it': require('./i18n/it'),
            'es': require('./i18n/es'),
            'pl': require('./i18n/pl'),
            'uk': require('./i18n/uk'),
            'zh-cn': require('./i18n/zh-cn'),
        };
        // get actual admin port
        extendedProps.socket = {port: parseInt(window.location.port, 10)};
    
        // Only if close, save buttons are not required at the bottom (e.g. if admin tab)
        // extendedProps.bottomButtons = false; 
    
        // only for debug purposes
        if (extendedProps.socket.port === 3000) {
            extendedProps.socket.port = 8081;
        }
    
        // allow to manage GenericApp the sentry initialisation or do not set the sentryDSN if no sentry available
        extendedProps.sentryDSN = 'https://yyy@sentry.iobroker.net/xx';
    
        super(extendedProps);
    }
    ...
    }
  2. Replace index.js with the following code to support themes:

    
    import React from 'react';
    import ReactDOM from 'react-dom';
    import { MuiThemeProvider} from '@material-ui/core/styles';
    import * as serviceWorker from './serviceWorker';

import './index.css'; import App from './App'; import { version } from '../package.json';

import theme from '@iobroker/adapter-react/Theme';

console.log('iobroker.scenes@' + version); let themeName = window.localStorage ? window.localStorage.getItem('App.theme') || 'light' : 'light';

function build() { return ReactDOM.render(<MuiThemeProvider theme={ theme(themeName) }> <App onThemeChange={_theme => { themeName = _theme; build(); }}/> , document.getElementById('root')); }

build();

// If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: http://bit.ly/CRA-PWA serviceWorker.unregister();


5. Add to App.js encoding and decoding of values:

class App extend GenericApp { ... onPrepareLoad(settings) { settings.pass = this.decode(settings.pass); } onPrepareSave(settings) { settings.pass = this.encode(settings.pass); } }


6. The optional step is to validate the data to be saved:

onPrepareSave(settings) { super.onPrepareSave(settings); if (DATA_INVALID) { return false; // configuration will not be saved } else { return true; } }


## Components

### Connection.tsx
This is a non-react class to provide the communication for socket connection with the server. 

### GenericApp.tsx

### i18n.ts

### Theme.tsx

### Dialogs
Some dialogs are predefined and could be used out of the box.

#### Confirm.tsx
<!-- TODO: Provide screenshot here -->

Usage: 

import React from 'react'; import ConfirmDialog from '@iobroker/adapter-react/Dialogs/Confirm' import I18n from '@iobroker/adapter-react/i18n';

class ExportImportDialog extends React.Component { constructor(props) { super(props);

    this.state = {
        confirmDialog: false,
    };
}   

renderConfirmDialog() {
    if (!this.state.confirmDialog) {
        return null;
    }
    return <ConfirmDialog
        title={ I18n.t('Scene will be overwritten.') }
        text={ I18n.t('All data will be lost. Confirm?') }
        ok={ I18n.t('Yes') }
        cancel={ I18n.t('Cancel') }
        suppressQuestionMinutes={5}
        dialogName="myConfirmDialogThatCouldBeSuppressed"
        suppressText={I18n.t('Suppress question for next %s minutes', 5)}
        onClose={isYes => {
            this.setState({ confirmDialog: false} );
        }}
    />;
}
render() {
    return <div>
        <Button onClick={ () => this.setState({confirmDialog: true}) }>Click</Button>
        { this.renderConfirmDialog() }
    </div>
}

}

export default ExportImportDialog;


#### Error.tsx
<!-- TODO: Provide screenshot here -->

#### Message.tsx
<!-- TODO: Provide screenshot here -->

renderMessage() { if (this.state.showMessage) { return <Message text={this.state.showMessage} onClose={() => this.setState({showMessage: false})} />; } else { return null; } }


#### SelectID.tsx
![Logo](img/selectID.png)

import DialogSelectID from '@iobroker/adapter-react/Dialogs/SelectID';

class MyComponent extends Component { constructor(props) { super(props); this.state = { showSelectId: false, }; }

renderSelectIdDialog() { if (this.state.showSelectId) { return <DialogSelectID key="tableSelect" imagePrefix="../.." dialogName={this.props.adapterName} themeType={this.props.themeType} socket={this.props.socket} statesOnly={true} selected={this.state.selectIdValue} onClose={() => this.setState({showSelectId: false})} onOk={(selected, name) => { this.setState({showSelectId: false, selectIdValue: selected});
}} />; } else { return null; } } render() { return renderSelectIdDialog(); } }


#### Cron
Include `"react-text-mask": "^5.4.3",` in package.json.

<!-- TODO: Provide screenshot here -->

function renderCron() { if (!showCron) { return null; } else {
return <DialogCron key="dialogCron1" cron={this.state.cronValue || ' *'} onClose={() => this.setState({ showCron: false })} onOk={cronValue => { this.setState({ cronValue }) }} />; } }


### Components

#### Utils.tsx
##### getObjectNameFromObj
`getObjectNameFromObj(obj, settings, options, isDesc)`

Get object name from a single object.

Usage: `Utils.getObjectNameFromObj(this.objects[id], null, {language: I18n.getLanguage()})`

##### getObjectIcon
`getObjectIcon(id, obj)`

Get icon from the object.

Usage: 

const icon = Utils.getObjectIcon(id, this.objects[id]); return ();


##### isUseBright
`isUseBright(color, defaultValue)`

Usage: `

#### Loader.tsx
![Logo](img/loader.png)

render() { if (!this.state.loaded) { return

; } // render loaded data } ``` #### Logo.tsx ![Logo](img/logo.png) ``` render() { return
this.setState({errorText: text})} onLoad={this.props.onLoad} /> ... ; } ``` #### Router.tsx #### ObjectBrowser.js It is better to use `Dialog/SelectID`, but if you want: ![Logo](img/objectBrowser.png) ``` { this.filters = filterConfig; window.localStorage.setItem(this.dialogName, JSON.stringify(filterConfig)); }} onSelect={(selected, name, isDouble) => { if (JSON.stringify(selected) !== JSON.stringify(this.state.selected)) { this.setState({selected, name}, () => isDouble && this.handleOk()); } else if (isDouble) { this.handleOk(); } }} /> ``` #### TreeTable.ts ![Logo](img/tableTree.png) ``` // STYLES const styles = theme => ({ tableDiv: { width: '100%', overflow: 'hidden', height: 'calc(100% - 48px)', }, }); class MyComponent extends Component { constructor(props) { super(props); this.state = { data: [ { id: 'UniqueID1' // required fieldIdInData: 'Name1', myType: 'number', }, { id: 'UniqueID2' // required fieldIdInData: 'Name12', myType: 'string', }, ], }; this.columns = [ { title: 'Name of field', // required, else it will be "field" field: 'fieldIdInData', // required editable: false, // or true [default - true] cellStyle: { // CSS style - // optional maxWidth: '12rem', overflow: 'hidden', wordBreak: 'break-word', }, lookup: { // optional => edit will be automatically "SELECT" 'value1': 'text1', 'value2': 'text2', } }, { title: 'Type', // required, else it will be "field" field: 'myType', // required editable: true, // or true [default - true] lookup: { // optional => edit will be automatically "SELECT" 'number': 'Number', 'string': 'String', 'boolean': 'Boolean', }, type: 'number/string/color/oid/icon/boolean', // oid=ObjectID,icon=base64-icon editComponent: props =>
Prefix{