;
-
-export const inject =
- (...stores: I[]) =>
- // eslint-disable-next-line @typescript-eslint/ban-types
- (
- node: React.ComponentType
- ): React.ComponentType>> =>
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- mobxInject(...stores)(node) as any;
-
-export const InjectProvider: React.FC<{stores: StoreMapping}> = ({children, stores}) => (
- {children}
-);
diff --git a/ui/src/layout/Header.tsx b/ui/src/layout/Header.tsx
index 74a288fbc..35efa39ea 100644
--- a/ui/src/layout/Header.tsx
+++ b/ui/src/layout/Header.tsx
@@ -1,27 +1,29 @@
-import AppBar from '@material-ui/core/AppBar';
-import Button from '@material-ui/core/Button';
-import IconButton from '@material-ui/core/IconButton';
-import {createStyles, Theme, WithStyles, withStyles} from '@material-ui/core/styles';
-import Toolbar from '@material-ui/core/Toolbar';
-import Typography from '@material-ui/core/Typography';
-import AccountCircle from '@material-ui/icons/AccountCircle';
-import Chat from '@material-ui/icons/Chat';
-import DevicesOther from '@material-ui/icons/DevicesOther';
-import ExitToApp from '@material-ui/icons/ExitToApp';
-import Highlight from '@material-ui/icons/Highlight';
-import GitHubIcon from '@material-ui/icons/GitHub';
-import MenuIcon from '@material-ui/icons/Menu';
-import Apps from '@material-ui/icons/Apps';
-import SupervisorAccount from '@material-ui/icons/SupervisorAccount';
-import { makeObservable } from 'mobx';
-import React, {Component, CSSProperties} from 'react';
+import AppBar from '@mui/material/AppBar';
+import Button from '@mui/material/Button';
+import IconButton from '@mui/material/IconButton';
+import Toolbar from '@mui/material/Toolbar';
+import Typography from '@mui/material/Typography';
+import AccountCircle from '@mui/icons-material/AccountCircle';
+import Chat from '@mui/icons-material/Chat';
+import DevicesOther from '@mui/icons-material/DevicesOther';
+import ExitToApp from '@mui/icons-material/ExitToApp';
+import Highlight from '@mui/icons-material/Highlight';
+import GitHubIcon from '@mui/icons-material/GitHub';
+import MenuIcon from '@mui/icons-material/Menu';
+import Apps from '@mui/icons-material/Apps';
+import SupervisorAccount from '@mui/icons-material/SupervisorAccount';
+import React, {CSSProperties} from 'react';
+import {useNavigate} from 'react-router';
import {Link} from 'react-router-dom';
-import {observer} from 'mobx-react';
-import {Hidden, PropTypes, withWidth} from '@material-ui/core';
-import {Breakpoint} from '@material-ui/core/styles/createBreakpoints';
+import {SxProps, useMediaQuery, useTheme} from '@mui/material';
+import {makeStyles} from 'tss-react/mui';
+import {useAppDispatch, useAppSelector} from '../store';
+import {logout} from '../store/auth-actions.ts';
+import {uiActions} from '../store/ui-slice.ts';
+import {toggleTheme} from '../store/ui-actions.ts';
-const styles = (theme: Theme) =>
- createStyles({
+const useStyles = makeStyles()((theme) => {
+ return {
appBar: {
zIndex: theme.zIndex.drawer + 1,
[theme.breakpoints.down('xs')]: {
@@ -61,162 +63,124 @@ const styles = (theme: Theme) =>
color: 'inherit',
textDecoration: 'none',
},
- });
+ };
+});
-type Styles = WithStyles<'link' | 'menuButtons' | 'toolbar' | 'titleName' | 'title' | 'appBar'>;
-
-interface IProps extends Styles {
- loggedIn: boolean;
- name: string;
- admin: boolean;
+interface IProps {
version: string;
- toggleTheme: VoidFunction;
- showSettings: VoidFunction;
- logout: VoidFunction;
style: CSSProperties;
- width: Breakpoint;
- setNavOpen: (open: boolean) => void;
}
-@observer
-class Header extends Component {
- constructor(props: any) {
- super(props);
- makeObservable(this);
- }
+const Header = ({version, style}: IProps) => {
+ const theme = useTheme();
+ const {classes} = useStyles();
+ const dispatch = useAppDispatch();
- public render() {
- const {
- classes,
- version,
- name,
- loggedIn,
- admin,
- toggleTheme,
- logout,
- style,
- setNavOpen,
- width,
- } = this.props;
+ const loggedIn = useAppSelector((state) => state.auth.loggedIn);
- const position = width === 'xs' ? 'sticky' : 'fixed';
+ return (
+
+
+
+ {loggedIn && }
+
+
dispatch(toggleTheme())} color="inherit">
+
+
- return (
-
-
-
- {loggedIn && this.renderButtons(name, admin, logout, width, setNavOpen)}
-
+
+
+ );
+};
-
-
-
-
-
-
-
-
- );
- }
+const RenderButtons = () => {
+ const dispatch = useAppDispatch();
+ const {classes} = useStyles();
+ const navigate = useNavigate();
+ const admin = useAppSelector((state) => state.auth.user.admin);
+ const name = useAppSelector((state) => state.auth.user.name);
- private renderButtons(
- name: string,
- admin: boolean,
- logout: VoidFunction,
- width: Breakpoint,
- setNavOpen: (open: boolean) => void
- ) {
- const {classes, showSettings} = this.props;
- return (
-
-
- }
- onClick={() => setNavOpen(true)}
- label="menu"
- width={width}
- color="inherit"
- />
-
- {admin && (
-
-
}
- label="users"
- width={width}
- color="inherit"
- />
-
- )}
-
-
} label="apps" width={width} color="inherit" />
-
-
-
}
- label="clients"
- width={width}
- color="inherit"
- />
-
-
-
}
- label="plugins"
- width={width}
- color="inherit"
- />
+ return (
+
+ }
+ onClick={() => dispatch(uiActions.setNavOpen(true))}
+ label="menu"
+ color="inherit"
+ />
+ {admin && (
+
+ } label="users" color="inherit" />
- }
- label={name}
- onClick={showSettings}
- id="changepw"
- width={width}
- color="inherit"
- />
- }
- label="Logout"
- onClick={logout}
- id="logout"
- width={width}
- color="inherit"
- />
-
- );
- }
-}
+ )}
+
+
} label="apps" color="inherit" />
+
+
+
} label="clients" color="inherit" />
+
+
+
} label="plugins" color="inherit" />
+
+
}
+ label={name}
+ onClick={() => dispatch(uiActions.setShowSettings(true))}
+ color="inherit"
+ />
+
}
+ label="Logout"
+ id="logout"
+ onClick={() => {
+ dispatch(logout())
+ .then(() => navigate('/login'))
+ }}
+ color="inherit"
+ />
+
+ );
+};
const ResponsiveButton: React.FC<{
- width: Breakpoint;
- color: PropTypes.Color;
+ color: any;
label: string;
id?: string;
onClick?: () => void;
icon: React.ReactNode;
-}> = ({width, icon, label, ...rest}) => {
- if (width === 'xs' || width === 'sm') {
+ sx?: SxProps;
+}> = ({icon, label, ...rest}) => {
+ const theme = useTheme();
+ const smallerMd = useMediaQuery(theme.breakpoints.down('md'));
+
+ if (smallerMd) {
return {icon};
}
return (
@@ -226,4 +190,4 @@ const ResponsiveButton: React.FC<{
);
};
-export default withWidth()(withStyles(styles, {withTheme: true})(Header));
+export default Header;
diff --git a/ui/src/layout/Layout.tsx b/ui/src/layout/Layout.tsx
deleted file mode 100644
index be0eac4ba..000000000
--- a/ui/src/layout/Layout.tsx
+++ /dev/null
@@ -1,184 +0,0 @@
-import { MuiThemeProvider, Theme, WithStyles, withStyles} from '@material-ui/core';
-import { createTheme } from '@material-ui/core/styles';
-import CssBaseline from '@material-ui/core/CssBaseline';
-import * as React from 'react';
-import { HashRouter, Redirect, Route, Switch } from 'react-router-dom';
-import Header from './Header';
-import LoadingSpinner from '../common/LoadingSpinner';
-import Navigation from './Navigation';
-import ScrollUpButton from '../common/ScrollUpButton';
-import SettingsDialog from '../common/SettingsDialog';
-import SnackBarHandler from '../snack/SnackBarHandler';
-import * as config from '../config';
-import Applications from '../application/Applications';
-import Clients from '../client/Clients';
-import Plugins from '../plugin/Plugins';
-import PluginDetailView from '../plugin/PluginDetailView';
-import Login from '../user/Login';
-import Messages from '../message/Messages';
-import Users from '../user/Users';
-import { observer } from 'mobx-react';
-import { makeObservable, observable, action } from 'mobx';
-import { inject, Stores } from '../inject';
-import { ConnectionErrorBanner } from '../common/ConnectionErrorBanner';
-
-const styles = (theme: Theme) => ({
- content: {
- margin: '0 auto',
- marginTop: 64,
- padding: theme.spacing(4),
- width: '100%',
- [theme.breakpoints.down('xs')]: {
- marginTop: 0,
- },
- },
-});
-
-const localStorageThemeKey = 'gotify-theme';
-type ThemeKey = 'dark' | 'light';
-const themeMap: Record = {
- light: createTheme({
- palette: {
- type: 'light',
- },
- }),
- dark: createTheme({
- palette: {
- type: 'dark',
- },
- }),
-};
-
-const isThemeKey = (value: string | null): value is ThemeKey =>
- value === 'light' || value === 'dark';
-
-@observer
-class Layout extends React.Component<
- WithStyles<'content'> & Stores<'currentUser' | 'snackManager'>
-> {
- @observable
- private currentTheme: ThemeKey = 'dark';
- @observable
- private showSettings = false;
- @observable
- private navOpen = false;
-
- constructor(props: any) {
- super(props);
- makeObservable(this);
- }
-
- @action
- private setNavOpen = (open: boolean) => {
- this.navOpen = open;
- };
-
- @action
- private setShowSettings = (show: boolean) => {
- this.showSettings = show;
- };
-
- @action
- public componentDidMount() {
- const localStorageTheme = window.localStorage.getItem(localStorageThemeKey);
- if (isThemeKey(localStorageTheme)) {
- this.currentTheme = localStorageTheme;
- } else {
- window.localStorage.setItem(localStorageThemeKey, this.currentTheme);
- }
- }
-
- @action
- public render() {
- const {showSettings, currentTheme} = this;
- const {
- classes,
- currentUser: {
- loggedIn,
- authenticating,
- user: { name, admin },
- logout,
- tryReconnect,
- connectionErrorMessage,
- },
- } = this.props;
- const theme = themeMap[currentTheme];
- const loginRoute = () => (loggedIn ? : );
- const { version } = config.get('version');
- return (
-
-
-
- {!connectionErrorMessage ? null : (
-
tryReconnect()}
- message={connectionErrorMessage}
- />
- )}
-
-
-
-
- );
- }
-
- @action
- private toggleTheme() {
- this.currentTheme = this.currentTheme === 'dark' ? 'light' : 'dark';
- localStorage.setItem(localStorageThemeKey, this.currentTheme);
- }
-}
-
-export default withStyles(styles, {withTheme: true})(inject('currentUser', 'snackManager')(Layout));
diff --git a/ui/src/layout/Navigation.tsx b/ui/src/layout/Navigation.tsx
index 30fcd6dfb..80eed4ba6 100644
--- a/ui/src/layout/Navigation.tsx
+++ b/ui/src/layout/Navigation.tsx
@@ -1,147 +1,149 @@
-import Divider from '@material-ui/core/Divider';
-import Drawer from '@material-ui/core/Drawer';
-import {StyleRules, Theme, WithStyles, withStyles} from '@material-ui/core/styles';
-import { makeObservable } from 'mobx';
-import React, {Component} from 'react';
+import React, {useEffect, useState} from 'react';
import {Link} from 'react-router-dom';
-import {observer} from 'mobx-react';
-import {inject, Stores } from '../inject';
-import { mayAllowPermission, requestPermission } from '../snack/browserNotification';
+import Divider from '@mui/material/Divider';
+import Drawer from '@mui/material/Drawer';
+import {makeStyles} from 'tss-react/mui';
+import {mayAllowPermission, requestPermission} from '../snack/browserNotification';
import {
Button,
- Hidden,
IconButton,
Typography,
- ListItem,
+ ListItemButton,
ListItemText,
ListItemAvatar,
Avatar,
-} from '@material-ui/core';
-import { DrawerProps } from '@material-ui/core/Drawer/Drawer';
-import CloseIcon from '@material-ui/icons/Close';
+ Box,
+} from '@mui/material';
+import {DrawerProps} from '@mui/material/Drawer/Drawer';
+import CloseIcon from '@mui/icons-material/Close';
+import {useAppDispatch, useAppSelector} from '../store';
+import {fetchApps} from '../store/app-actions.ts';
+import {appActions} from '../store/app-slice.ts';
+import {uiActions} from '../store/ui-slice.ts';
+import * as config from '../config';
-const styles = (theme: Theme): StyleRules<'root' | 'drawerPaper' | 'toolbar' | 'link'> => ({
- root: {
- height: '100%',
- },
- drawerPaper: {
- position: 'relative',
- width: 250,
- minHeight: '100%',
- height: '100vh',
- },
- toolbar: theme.mixins.toolbar,
- link: {
- color: 'inherit',
- textDecoration: 'none',
- },
+const useStyles = makeStyles()(() => {
+ return {
+ root: {
+ height: '100%',
+ },
+ drawerPaper: {
+ position: 'relative',
+ width: 250,
+ minHeight: '100%',
+ height: '100vh',
+ },
+ // toolbar: theme.mixins.toolbar,
+ link: {
+ color: 'inherit',
+ textDecoration: 'none',
+ },
+ };
});
-type Styles = WithStyles<'root' | 'drawerPaper' | 'toolbar' | 'link'>;
+const Navigation = () => {
+ const [showRequestNotification, setShowRequestNotification] = useState(mayAllowPermission());
+ const apps = useAppSelector((state) => state.app.items);
+ const {classes} = useStyles();
+ const dispatch = useAppDispatch();
+ const loggedIn = useAppSelector((state) => state.auth.loggedIn);
-interface IProps {
- loggedIn: boolean;
- navOpen: boolean;
- setNavOpen: (open: boolean) => void;
-}
+ useEffect(() => {
+ if (loggedIn) {
+ dispatch(fetchApps());
+ }
+ }, [dispatch, loggedIn]);
-@observer
-class Navigation extends Component<
- IProps & Styles & Stores<'appStore'>,
- { showRequestNotification: boolean }
-> {
- public state = { showRequestNotification: mayAllowPermission() };
+ const userApps =
+ apps.length === 0
+ ? null
+ : apps.map((app) => (
+ {
+ dispatch(uiActions.setNavOpen(false));
+ // TODO: make sure we can select the app 'All Messages', does not work yet
+ dispatch(appActions.select(app));
+ }}
+ className={`${classes.link} item`}
+ to={'/messages/' + app.id}
+ key={app.id}>
+
+
+
+
+
+
+
+ ));
- constructor(props: any) {
- super(props);
- makeObservable(this);
- }
+ const placeholderItems = [
+
+
+ ,
+
+
+ ,
+ ];
+ return (
+
+ {/**/}
+
+ {
+ dispatch(uiActions.setNavOpen(false));
+ dispatch(appActions.select(null));
+ }}>
+
+
+
+
+
+ {loggedIn ? userApps : placeholderItems}
+
+
+ {showRequestNotification ? (
+
+ ) : null}
+
+
+ );
+};
- public render() {
- const {classes, loggedIn, appStore, navOpen, setNavOpen} = this.props;
- const {showRequestNotification} = this.state;
- const apps = appStore.getItems();
+const ResponsiveDrawer: React.FC = ({children, ...rest}) => {
+ const dispatch = useAppDispatch();
+ const navOpen = useAppSelector((state) => state.ui.navOpen);
- const userApps =
- apps.length === 0
- ? null
- : apps.map((app) => (
- setNavOpen(false)}
- className={`${classes.link} item`}
- to={'/messages/' + app.id}
- key={app.id}>
-
-
-
-
-
-
-
- ));
+ return (
+ <>
+
+
+ dispatch(uiActions.setNavOpen(false))}>
+
+
+ {children}
+
+
+
+
+ {children}
+
+
+ >
+ );
+};
- const placeholderItems = [
-
-
- ,
-
-
- ,
- ];
-
- return (
-
-
- setNavOpen(false)}>
-
-
-
-
-
- {loggedIn ? userApps : placeholderItems}
-
-
- {showRequestNotification ? (
-
- ) : null}
-
-
- );
- }
-}
-
-const ResponsiveDrawer: React.FC<
- DrawerProps & {navOpen: boolean; setNavOpen: (open: boolean) => void}
-> = ({navOpen, setNavOpen, children, ...rest}) => (
- <>
-
-
- setNavOpen(false)}>
-
-
- {children}
-
-
-
-
- {children}
-
-
- >
-);
-
-export default withStyles(styles, {withTheme: true})(inject('appStore')(Navigation));
+export default Navigation;
diff --git a/ui/src/message/Message.tsx b/ui/src/message/Message.tsx
index dc1a16e10..d3350c168 100644
--- a/ui/src/message/Message.tsx
+++ b/ui/src/message/Message.tsx
@@ -1,17 +1,18 @@
-import IconButton from '@material-ui/core/IconButton';
-import {createStyles, Theme, withStyles, WithStyles} from '@material-ui/core/styles';
-import Typography from '@material-ui/core/Typography';
-import Delete from '@material-ui/icons/Delete';
-import React from 'react';
+import React, {useEffect, useRef} from 'react';
+import IconButton from '@mui/material/IconButton';
+import {Theme} from '@mui/material/styles';
+import Typography from '@mui/material/Typography';
+import DeleteIcon from '@mui/icons-material/Delete';
import TimeAgo from 'react-timeago';
+import {makeStyles} from 'tss-react/mui';
import Container from '../common/Container';
import * as config from '../config';
import {Markdown} from '../common/Markdown';
import {RenderMode, contentType} from './extras';
import {IMessageExtras} from '../types';
-const styles = (theme: Theme) =>
- createStyles({
+const useStyles = makeStyles()((theme: Theme) => {
+ return {
header: {
display: 'flex',
flexWrap: 'wrap',
@@ -66,7 +67,8 @@ const styles = (theme: Theme) =>
maxWidth: '100%',
},
},
- });
+ };
+});
interface IProps {
title: string;
@@ -89,66 +91,64 @@ const priorityColor = (priority: number) => {
}
};
-class Message extends React.PureComponent> {
- private node: HTMLDivElement | null = null;
+const Message = ({fDelete, title, date, image, priority, content, extras, height}: IProps) => {
+ const {classes} = useStyles();
+ const node = useRef(null);
- public componentDidMount = () =>
- this.props.height(this.node ? this.node.getBoundingClientRect().height : 0);
+ useEffect(() => {
+ // TODO: fix this
+ // height(node ? node.getBoundingClientRect().height : 0);
+ }, []);
- private renderContent = () => {
- const content = this.props.content;
- switch (contentType(this.props.extras)) {
+ const renderContent = () => {
+ switch (contentType(extras)) {
case RenderMode.Markdown:
return {content};
case RenderMode.Plain:
default:
- return {content};
+ return {content};
}
};
- public render(): React.ReactNode {
- const {fDelete, classes, title, date, image, priority} = this.props;
-
- return (
- (this.node = ref)}>
-
-
- {image !== null ? (
-
- ) : null}
-
-
-
-
- {title}
-
-
-
-
-
-
-
-
-
- {this.renderContent()}
+ return (
+
+
+
+ {image !== null ? (
+
+ ) : null}
+
+
+
+
+ {title}
+
+
+
+
+
+
-
-
- );
- }
-}
+
+ {renderContent()}
+
+
+
+
+ );
+};
-export default withStyles(styles, {withTheme: true})(Message);
+export default Message;
diff --git a/ui/src/message/Messages.tsx b/ui/src/message/Messages.tsx
index 149eaab5d..af268c33d 100644
--- a/ui/src/message/Messages.tsx
+++ b/ui/src/message/Messages.tsx
@@ -1,46 +1,126 @@
-import Grid from '@material-ui/core/Grid';
-import Typography from '@material-ui/core/Typography';
-import React, {Component} from 'react';
-import {RouteComponentProps} from 'react-router';
+import Grid from '@mui/material/Grid2';
+import Typography from '@mui/material/Typography';
+import React, {useEffect, useState} from 'react';
+import {useParams} from 'react-router';
import DefaultPage from '../common/DefaultPage';
-import Button from '@material-ui/core/Button';
+import Button from '@mui/material/Button';
+import {useAppDispatch, useAppSelector} from '../store';
+import {getAppName} from '../store/app-actions.ts';
+import {fetchMessages, removeMessagesByApp, removeSingleMessage} from '../store/message-actions.ts';
import Message from './Message';
-import {observer} from 'mobx-react';
-import {inject, Stores } from '../inject';
-import { action, makeObservable, observable } from 'mobx';
-import ReactInfinite from 'react-infinite';
-import { IMessage } from '../types';
import ConfirmDialog from '../common/ConfirmDialog';
import LoadingSpinner from '../common/LoadingSpinner';
-type IProps = RouteComponentProps<{ id: string }>;
+const Messages = () => {
+ const dispatch = useAppDispatch();
+ const {id} = useParams();
+ const appId = id !== undefined ? parseInt(id as string) : -1;
-interface IState {
- appId: number;
-}
+ const [ toDeleteAll, setToDeleteAll ] = useState(false);
+
+ const heights: Record = {};
+
+ const selectedApp = useAppSelector((state) => state.app.items.find(app => app.id === appId));
+ const apps = useAppSelector((state) => state.app.items);
+ const messages = useAppSelector((state) => state.message.items);
+ const hasMore = useAppSelector((state) => state.message.hasMore);
+ const name = dispatch(getAppName(appId));
+ const messagesLoaded = useAppSelector((state) => state.message.loaded);
+ const hasMessages = messages.length !== 0;
+
+ useEffect(() => {
+ dispatch(fetchMessages(appId));
+ }, [ dispatch, appId ]);
+
+ const label = (text: string) => (
+
+
+ {text}
+
+
+ );
+ return (
+
+
+
+
+ }>
+ {!messagesLoaded ? (
+
+ ) : hasMessages ? (
+
+ {/* TODO: maybe replace ReactInfitite with react-window, which is also documented here: https://mui.com/material-ui/react-list/#virtualized-list */}
+ {/* heights[m.id] || 1)}>*/}
+ {/* {messages.map(renderMessage)}*/}
+ {/**/}
+ {messages.map((message) => (
+ {
+ if (!heights[message.id]) {
+ heights[message.id] = height;
+ }
+ }}
+ fDelete={() => dispatch(removeSingleMessage(message))}
+ title={message.title}
+ date={message.date}
+ content={message.message}
+ image={apps.find(app => app.id == message.appid)?.image}
+ extras={message.extras}
+ priority={message.priority}
+ />
+ ),
+ )}
+
+ {hasMore ? : label('You\'ve reached the end')}
+
+ ) : (
+ label('No messages')
+ )}
+
+ {toDeleteAll && (
+ setToDeleteAll(false)}
+ fOnSubmit={() => dispatch(removeMessagesByApp(selectedApp))}
+ />
+ )}
+
+ );
+};
+/*
@observer
-class Messages extends Component, IState> {
+class Messages_old extends Component, IState> {
@observable
private heights: Record = {};
@observable
private deleteAll = false;
- constructor(props: any) {
- super(props);
- makeObservable(this);
- }
-
- @action
- private setHeight(id: string, height: number) {
- this.heights[id] = height;
- }
-
- @action
- private setDeleteAll(deleteAll: boolean) {
- this.deleteAll = deleteAll;
- }
-
private static appId(props: IProps) {
if (props === undefined) {
return -1;
@@ -69,69 +149,6 @@ class Messages extends Component,
this.updateAll();
}
- public render() {
- const {appId} = this.state;
- const {messagesStore, appStore} = this.props;
- const messages = messagesStore.get(appId);
- const hasMore = messagesStore.canLoadMore(appId);
- const name = appStore.getName(appId);
- const hasMessages = messages.length !== 0;
-
- return (
-
-
-
-