Skip to content

Commit

Permalink
Merge pull request #4816 from thornbill/dashboard-app
Browse files Browse the repository at this point in the history
Migrate dashboard to separate app
  • Loading branch information
thornbill authored Oct 4, 2023
2 parents 817f4ad + 62f9e75 commit 8f32341
Show file tree
Hide file tree
Showing 68 changed files with 484 additions and 769 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,9 @@ Jellyfin Web is the frontend used for most of the clients available for end user
.
└── src
├── apps
│   ├── experimental # New experimental app layout
│   └── stable # Classic (stable) app layout
│   ├── dashboard # Admin dashboard app layout and routes
│   ├── experimental # New experimental app layout and routes
│   └── stable # Classic (stable) app layout and routes
├── assets # Static assets
├── components # Higher order visual components and React components
├── controllers # Legacy page views and controllers 🧹
Expand All @@ -87,7 +88,6 @@ Jellyfin Web is the frontend used for most of the clients available for end user
├── legacy # Polyfills for legacy browsers
├── libraries # Third party libraries 🧹
├── plugins # Client plugins
├── routes # React routes/pages
├── scripts # Random assortment of visual components and utilities 🐉
├── strings # Translation files
├── styles # Common app Sass stylesheets
Expand Down
13 changes: 11 additions & 2 deletions src/RootApp.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import loadable from '@loadable/component';
import { ThemeProvider } from '@mui/material/styles';
import { History } from '@remix-run/router';
import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import React from 'react';
import { useLocation } from 'react-router-dom';

import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App';
import AppHeader from 'components/AppHeader';
import Backdrop from 'components/Backdrop';
import { HistoryRouter } from 'components/router/HistoryRouter';
import { ApiProvider } from 'hooks/useApi';
import { WebConfigProvider } from 'hooks/useWebConfig';
import theme from 'themes/theme';

const DashboardApp = loadable(() => import('./apps/dashboard/App'));
const ExperimentalApp = loadable(() => import('./apps/experimental/App'));
const StableApp = loadable(() => import('./apps/stable/App'));

Expand All @@ -21,16 +24,22 @@ const RootAppLayout = () => {
const layoutMode = localStorage.getItem('layout');
const isExperimentalLayout = layoutMode === 'experimental';

const location = useLocation();
const isNewLayoutPath = Object.values(DASHBOARD_APP_PATHS)
.some(path => location.pathname.startsWith(`/${path}`));

return (
<>
<Backdrop />
<AppHeader isHidden={isExperimentalLayout} />
<AppHeader isHidden={isExperimentalLayout || isNewLayoutPath} />

{
isExperimentalLayout ?
<ExperimentalApp /> :
<StableApp />
}

<DashboardApp />
</>
);
};
Expand Down
66 changes: 66 additions & 0 deletions src/apps/dashboard/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import loadable from '@loadable/component';
import React from 'react';
import { Route, Routes } from 'react-router-dom';

import ConnectionRequired from 'components/ConnectionRequired';
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
import { AsyncPageProps, AsyncRoute, toAsyncPageRoute } from 'components/router/AsyncRoute';
import { toRedirectRoute } from 'components/router/Redirect';
import ServerContentPage from 'components/ServerContentPage';

import AppLayout from './AppLayout';
import { REDIRECTS } from './routes/_redirects';
import { ASYNC_ADMIN_ROUTES } from './routes/_asyncRoutes';
import { LEGACY_ADMIN_ROUTES } from './routes/_legacyRoutes';

const DashboardAsyncPage = loadable(
(props: { page: string }) => import(/* webpackChunkName: "[request]" */ `./routes/${props.page}`),
{ cacheKey: (props: AsyncPageProps) => props.page }
);

const toDashboardAsyncPageRoute = (route: AsyncRoute) => (
toAsyncPageRoute({
...route,
element: DashboardAsyncPage
})
);

export const DASHBOARD_APP_PATHS = {
Dashboard: 'dashboard',
MetadataManager: 'metadata',
PluginConfig: 'configurationpage'
};

const DashboardApp = () => (
<Routes>
<Route element={<ConnectionRequired isAdminRequired />}>
<Route element={<AppLayout drawerlessPaths={[ DASHBOARD_APP_PATHS.MetadataManager ]} />}>
<Route path={DASHBOARD_APP_PATHS.Dashboard}>
{ASYNC_ADMIN_ROUTES.map(toDashboardAsyncPageRoute)}
{LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)}
</Route>

{/* NOTE: The metadata editor might deserve a dedicated app in the future */}
{toViewManagerPageRoute({
path: DASHBOARD_APP_PATHS.MetadataManager,
pageProps: {
controller: 'edititemmetadata',
view: 'edititemmetadata.html'
}
})}

<Route path={DASHBOARD_APP_PATHS.PluginConfig} element={
<ServerContentPage view='/web/configurationpage' />
} />
</Route>

{/* Suppress warnings for unhandled routes */}
<Route path='*' element={null} />
</Route>

{/* Redirects for old paths */}
{REDIRECTS.map(toRedirectRoute)}
</Routes>
);

export default DashboardApp;
108 changes: 108 additions & 0 deletions src/apps/dashboard/AppLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import { useTheme } from '@mui/material/styles';
import React, { FC, useCallback, useEffect, useState } from 'react';
import { Outlet, useLocation } from 'react-router-dom';

import AppBody from 'components/AppBody';
import AppToolbar from 'components/toolbar/AppToolbar';
import ElevationScroll from 'components/ElevationScroll';
import { DRAWER_WIDTH } from 'components/ResponsiveDrawer';
import { useApi } from 'hooks/useApi';
import { useLocalStorage } from 'hooks/useLocalStorage';

import AppDrawer from './components/drawer/AppDrawer';

import './AppOverrides.scss';

interface AppLayoutProps {
drawerlessPaths: string[]
}

interface DashboardAppSettings {
isDrawerPinned: boolean
}

const DEFAULT_APP_SETTINGS: DashboardAppSettings = {
isDrawerPinned: false
};

const AppLayout: FC<AppLayoutProps> = ({
drawerlessPaths
}) => {
const [ appSettings, setAppSettings ] = useLocalStorage<DashboardAppSettings>('DashboardAppSettings', DEFAULT_APP_SETTINGS);
const [ isDrawerActive, setIsDrawerActive ] = useState(appSettings.isDrawerPinned);
const location = useLocation();
const theme = useTheme();
const { user } = useApi();

const isDrawerAvailable = !drawerlessPaths.some(path => location.pathname.startsWith(`/${path}`));
const isDrawerOpen = isDrawerActive && isDrawerAvailable && Boolean(user);

useEffect(() => {
if (isDrawerActive !== appSettings.isDrawerPinned) {
setAppSettings({
...appSettings,
isDrawerPinned: isDrawerActive
});
}
}, [ appSettings, isDrawerActive, setAppSettings ]);

const onToggleDrawer = useCallback(() => {
setIsDrawerActive(!isDrawerActive);
}, [ isDrawerActive, setIsDrawerActive ]);

return (
<Box sx={{ display: 'flex' }}>
<ElevationScroll elevate={isDrawerOpen}>
<AppBar
position='fixed'
sx={{ zIndex: (muiTheme) => muiTheme.zIndex.drawer + 1 }}
>
<AppToolbar
isDrawerAvailable={isDrawerAvailable}
isDrawerOpen={isDrawerOpen}
onDrawerButtonClick={onToggleDrawer}
/>
</AppBar>
</ElevationScroll>

<AppDrawer
open={isDrawerOpen}
onClose={onToggleDrawer}
onOpen={onToggleDrawer}
/>

<Box
component='main'
sx={{
width: '100%',
flexGrow: 1,
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
}),
marginLeft: 0,
...(isDrawerAvailable && {
marginLeft: {
sm: `-${DRAWER_WIDTH}px`
}
}),
...(isDrawerActive && {
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen
}),
marginLeft: 0
})
}}
>
<AppBody>
<Outlet />
</AppBody>
</Box>
</Box>
);
};

export default AppLayout;
22 changes: 22 additions & 0 deletions src/apps/dashboard/AppOverrides.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Default MUI breakpoints
// https://mui.com/material-ui/customization/breakpoints/#default-breakpoints
$mui-bp-sm: 600px;
$mui-bp-md: 900px;
$mui-bp-lg: 1200px;
$mui-bp-xl: 1536px;

// Fix dashboard pages layout to work with drawer
.dashboardDocument {
.mainAnimatedPage {
position: relative;
}

.skinBody {
position: unset !important;
}

// Fix the padding of dashboard pages
.content-primary.content-primary {
padding-top: 3.25rem !important;
}
}
29 changes: 29 additions & 0 deletions src/apps/dashboard/components/drawer/AppDrawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React, { FC } from 'react';

import ResponsiveDrawer, { ResponsiveDrawerProps } from 'components/ResponsiveDrawer';

import ServerDrawerSection from './sections/ServerDrawerSection';
import DevicesDrawerSection from './sections/DevicesDrawerSection';
import LiveTvDrawerSection from './sections/LiveTvDrawerSection';
import AdvancedDrawerSection from './sections/AdvancedDrawerSection';
import PluginDrawerSection from './sections/PluginDrawerSection';

const AppDrawer: FC<ResponsiveDrawerProps> = ({
open = false,
onClose,
onOpen
}) => (
<ResponsiveDrawer
open={open}
onClose={onClose}
onOpen={onOpen}
>
<ServerDrawerSection />
<DevicesDrawerSection />
<LiveTvDrawerSection />
<AdvancedDrawerSection />
<PluginDrawerSection />
</ResponsiveDrawer>
);

export default AppDrawer;
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ import ListItemLink from 'components/ListItemLink';
import globalize from 'scripts/globalize';

const PLUGIN_PATHS = [
'/installedplugins.html',
'/availableplugins.html',
'/repositories.html',
'/addplugin.html',
'/dashboard/plugins',
'/dashboard/plugins/catalog',
'/dashboard/plugins/repositories',
'/dashboard/plugins/add',
'/configurationpage'
];

Expand All @@ -41,39 +41,39 @@ const AdvancedDrawerSection = () => {
}
>
<ListItem disablePadding>
<ListItemLink to='/networking.html'>
<ListItemLink to='/dashboard/networking'>
<ListItemIcon>
<Lan />
</ListItemIcon>
<ListItemText primary={globalize.translate('TabNetworking')} />
</ListItemLink>
</ListItem>
<ListItem disablePadding>
<ListItemLink to='/apikeys.html'>
<ListItemLink to='/dashboard/keys'>
<ListItemIcon>
<VpnKey />
</ListItemIcon>
<ListItemText primary={globalize.translate('HeaderApiKeys')} />
</ListItemLink>
</ListItem>
<ListItem disablePadding>
<ListItemLink to='/log.html'>
<ListItemLink to='/dashboard/logs'>
<ListItemIcon>
<Article />
</ListItemIcon>
<ListItemText primary={globalize.translate('TabLogs')} />
</ListItemLink>
</ListItem>
<ListItem disablePadding>
<ListItemLink to='/notificationsettings.html'>
<ListItemLink to='/dashboard/notifications'>
<ListItemIcon>
<EditNotifications />
</ListItemIcon>
<ListItemText primary={globalize.translate('Notifications')} />
</ListItemLink>
</ListItem>
<ListItem disablePadding>
<ListItemLink to='/installedplugins.html' selected={false}>
<ListItemLink to='/dashboard/plugins' selected={false}>
<ListItemIcon>
<Extension />
</ListItemIcon>
Expand All @@ -83,19 +83,19 @@ const AdvancedDrawerSection = () => {
</ListItem>
<Collapse in={isPluginSectionOpen} timeout='auto' unmountOnExit>
<List component='div' disablePadding>
<ListItemLink to='/installedplugins.html' sx={{ pl: 4 }}>
<ListItemLink to='/dashboard/plugins' sx={{ pl: 4 }}>
<ListItemText inset primary={globalize.translate('TabMyPlugins')} />
</ListItemLink>
<ListItemLink to='/availableplugins.html' sx={{ pl: 4 }}>
<ListItemLink to='/dashboard/plugins/catalog' sx={{ pl: 4 }}>
<ListItemText inset primary={globalize.translate('TabCatalog')} />
</ListItemLink>
<ListItemLink to='/repositories.html' sx={{ pl: 4 }}>
<ListItemLink to='/dashboard/plugins/repositories' sx={{ pl: 4 }}>
<ListItemText inset primary={globalize.translate('TabRepositories')} />
</ListItemLink>
</List>
</Collapse>
<ListItem disablePadding>
<ListItemLink to='/scheduledtasks.html'>
<ListItemLink to='/dashboard/tasks'>
<ListItemIcon>
<Schedule />
</ListItemIcon>
Expand Down
Loading

0 comments on commit 8f32341

Please sign in to comment.