Skip to content

Commit

Permalink
started working on adapters tab for wizard (#2400)
Browse files Browse the repository at this point in the history
  • Loading branch information
foxriver76 authored Mar 7, 2024
1 parent 1fe4b2f commit 1771848
Show file tree
Hide file tree
Showing 17 changed files with 2,150 additions and 1,630 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ The icons may not be reused in other projects without the proper flaticon licens
### **WORK IN PROGRESS**
-->
## Changelog
### **WORK IN PROGRESS**
* (foxriver76) added a new tab to the installation wizard which recommends common adapters

### 6.15.2 (2024-03-07)
* (foxriver76) fixed cron dialog

Expand Down
27 changes: 14 additions & 13 deletions packages/admin/src/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2012,10 +2012,11 @@ class App extends Router {
renderWizardDialog() {
if (this.state.wizard) {
return <WizardDialog
executeCommand={(cmd, host, cb) => this.executeCommand(cmd, host, cb)}
host={this.state.currentHost}
socket={this.socket}
themeName={this.state.themeName}
toggleTheme={this.toggleTheme}
t={I18n.t}
lang={I18n.getLanguage()}
onClose={redirect => {
this.setState({ wizard: false, showRedirect: redirect, redirectCountDown: 10 }, () => {
Expand All @@ -2026,7 +2027,7 @@ class App extends Router {
} else {
window.location = this.state.showRedirect;
}
}, 1000);
}, 1_000);
}
});
}}
Expand Down Expand Up @@ -2386,6 +2387,17 @@ class App extends Router {
)}
>
<Toolbar>
<IconButton
size="large"
edge="start"
className={Utils.clsx(
classes.menuButton,
!small && this.state.drawerState !== DrawerStates.closed && classes.hide,
)}
onClick={() => this.handleDrawerState(DrawerStates.opened)}
>
<MenuIcon />
</IconButton>
<IconButton
size="large"
onClick={() => this.setState({ notificationsDialog: true })}
Expand All @@ -2399,17 +2411,6 @@ class App extends Router {
</Badge>
</Tooltip>
</IconButton>
<IconButton
size="large"
edge="start"
className={Utils.clsx(
classes.menuButton,
!small && this.state.drawerState !== DrawerStates.closed && classes.hide,
)}
onClick={() => this.handleDrawerState(DrawerStates.opened)}
>
<MenuIcon />
</IconButton>
<div className={classes.wrapperButtons}>
<IsVisible name="admin.appBar.discovery" config={this.adminGuiConfig}>
{this.state.discoveryAlive && <Tooltip title={I18n.t('Discovery devices')}>
Expand Down
234 changes: 234 additions & 0 deletions packages/admin/src/src/components/Wizard/WizardAdaptersTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import React from 'react';
import {
Paper, Toolbar, Button, Accordion, Box, AccordionSummary, AccordionDetails, Checkbox, Typography, LinearProgress,
} from '@mui/material';
import { Check as IconCheck, ExpandMore as ExpandMoreIcon } from '@mui/icons-material';
import { type AdminConnection, I18n } from '@iobroker/adapter-react-v5';
import type { Repository } from '@/types';

interface WizardAdaptersTabProps {
/** Function to call if wizard step finishes */
onDone: () => void;
/** The socket connection */
socket: AdminConnection;
/** The host name */
host: string;
/** Execute command on given host */
executeCommand: (cmd: string, host: string, cb: () => void) => void;
}

interface WizardAdaptersTabState {
/** The repository */
repository: Repository;
/** Adapters which should be installed */
selectedAdapters: string[];
}

interface AdapterOptions {
/** Adapter name */
name: string;
/** Adapter description */
description: string;
}

export default class WizardAdaptersTab extends React.Component<WizardAdaptersTabProps, WizardAdaptersTabState> {
/** Height of the toolbar */
private readonly TOOLBAR_HEIGHT = 64;

constructor(props: WizardAdaptersTabProps) {
super(props);

this.state = {
repository: {},
selectedAdapters: [],
};
}

/**
* Lifecycle hook called if component is mounted
*/
async componentDidMount(): Promise<void> {
try {
const repository = await this.props.socket
.getRepository(
this.props.host,
{ repo: this.props.socket.systemConfig.common.activeRepo },
);

this.setState({ repository });
} catch (e) {
console.error(`Could not read repository: ${e.message}`);
}
}

/**
* Install adapters if next button is called
*/
async onDone(): Promise<void> {
const { selectedAdapters } = this.state;

this.props.onDone();

// after calling onDone we install in background
for (const adapter of selectedAdapters) {
await new Promise<void>(resolve => {
this.props.executeCommand(`add ${adapter}`, this.props.host, resolve);
});
}
}

/**
* Render Accordion for given adapter
*
* @param options Adapter specific information
*/
renderAdapterAccordion(options: AdapterOptions): React.ReactNode {
const { name, description } = options;

const adapter = this.state.repository[name];

if (!adapter) {
return null;
}

const lang = this.props.socket.systemLang;

const title = adapter.titleLang[lang] || adapter.titleLang.en;

return <Box sx={{ display: 'flex', alignItems: 'flex-start' }}>
<Accordion sx={{
borderColor: 'rgba(0, 0, 0, 0.2)', borderWidth: '1px', borderStyle: 'solid', width: '100%',
}}
>
<AccordionSummary
sx={{
backgroundColor: 'primary.main',
fontWeight: 'bold',
height: 52,
}}
expandIcon={<ExpandMoreIcon />}
aria-controls="panel1-content"
id="panel1-header"
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Checkbox
sx={{ color: 'text.primary', '&.Mui-checked': { color: 'text.primary' } }}
onClick={e => e.stopPropagation()}
onChange={() => {
const { selectedAdapters } = this.state;

if (selectedAdapters.includes(name)) {
const idx = selectedAdapters.indexOf(name);
selectedAdapters.splice(idx, 1);
} else {
selectedAdapters.push(name);
}

this.setState({ selectedAdapters });
}}
/>
<img alt={title} src={adapter.extIcon} style={{ width: 45, height: 45 }} />
{title}
</Box>
</AccordionSummary>
<AccordionDetails sx={{
backgroundColor: 'background.appbar', whiteSpace: 'pre-wrap', fontSize: 16, textAlign: 'left',
}}
>
<Typography>{description}</Typography>
</AccordionDetails>
</Accordion>
</Box>;
}

/**
* Render the actual content
*/
renderContent(): React.ReactNode {
return <Box sx={{ display: 'flex', gap: 1, flexDirection: 'column' }}>
<Typography sx={{ paddingTop: 2 }}>{I18n.t('wizard adapter general description')}</Typography>
<h2>{I18n.t('Cloud')}</h2>
<Typography>{I18n.t('cloud wizard category')}</Typography>
{this.renderAdapterAccordion({
name: 'iot',
description: I18n.t('iot wizard description'),
})}
{this.renderAdapterAccordion({ name: 'cloud', description: I18n.t('cloud wizard description') })}

<h2>{I18n.t('Logic')}</h2>
<Typography>{I18n.t('logic wizard category')}</Typography>
{this.renderAdapterAccordion({ name: 'javascript', description: I18n.t('javascript wizard description') })}
{this.renderAdapterAccordion({ name: 'scenes', description: I18n.t('scenes wizard description') })}

<h2>{I18n.t('Notifications')}</h2>
<Typography>{I18n.t('notifications wizard category')}</Typography>
{this.renderAdapterAccordion({ name: 'notification-manager', description: I18n.t('notification-manager wizard description') })}
{this.renderAdapterAccordion({ name: 'telegram', description: I18n.t('telegram wizard description') })}
{this.renderAdapterAccordion({ name: 'email', description: I18n.t('email wizard description') })}
{this.renderAdapterAccordion({ name: 'pushover', description: I18n.t('pushover wizard description') })}
{this.renderAdapterAccordion({ name: 'signal-cmb', description: I18n.t('signal-cmb wizard description') })}

<h2>{I18n.t('History data')}</h2>
<Typography>{I18n.t('history wizard category')}</Typography>
{this.renderAdapterAccordion({ name: 'history', description: I18n.t('history wizard description') })}
{this.renderAdapterAccordion({ name: 'sql', description: I18n.t('sql wizard description') })}

<h2>{I18n.t('Weather')}</h2>
<Typography>{I18n.t('weather wizard category')}</Typography>
{this.renderAdapterAccordion({ name: 'weatherunderground', description: I18n.t('weatherunderground wizard description') })}

<h2>{I18n.t('Visualization')}</h2>
<Typography>{I18n.t('visualization wizard category')}</Typography>
{this.renderAdapterAccordion({ name: 'vis-2', description: I18n.t('vis-2 wizard description') })}
</Box>;
}

/**
* Render the component
*/
render(): React.ReactNode {
const repositoryReceived = Object.keys(this.state.repository).length > 0;

return <Paper sx={{
height: '100%',
maxHeight: '100%',
maxWidth: '100%',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Box sx={{
height: `calc(100% - ${this.TOOLBAR_HEIGHT}px)`,
width: '90%',
maxWidth: '800px',
overflow: 'auto',
'-ms-overflow-style': 'none',
'scrollbar-width': 'none',
'&::-webkit-scrollbar': {
display: 'none',
},
}}
>
{repositoryReceived ? this.renderContent() : <LinearProgress />}
</Box>
<Toolbar sx={{
height: this.TOOLBAR_HEIGHT,
lineHeight: `${this.TOOLBAR_HEIGHT}px`,
width: '100%',
}}
>
<div style={{ flexGrow: 1 }} />
<Button
color="primary"
variant="contained"
onClick={() => this.onDone()}
startIcon={<IconCheck />}
>
{I18n.t('Apply')}
</Button>
</Toolbar>
</Paper>;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ class WizardPasswordTab extends Component {
color="primary"
variant="contained"
onClick={() => this.props.onDone(this.state.password)}
disabled={!!this.state.errorPasswordRepeat || this.state.errorPassword}
disabled={!this.state.passwordRepeat || !!this.state.errorPasswordRepeat || this.state.errorPassword}
startIcon={<IconCheck />}
>
{this.props.t('Set administrator password')}
Expand Down
Loading

0 comments on commit 1771848

Please sign in to comment.