Merge pull request #6689 from thornbill/wizard-app
Migrate wizard to dedicated app
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
import { DASHBOARD_APP_PATHS, DASHBOARD_APP_ROUTES } from 'apps/dashboard/routes/routes';
|
||||
import { EXPERIMENTAL_APP_ROUTES } from 'apps/experimental/routes/routes';
|
||||
import { STABLE_APP_ROUTES } from 'apps/stable/routes/routes';
|
||||
import { WIZARD_APP_ROUTES } from 'apps/wizard/routes/routes';
|
||||
import AppHeader from 'components/AppHeader';
|
||||
import Backdrop from 'components/Backdrop';
|
||||
import BangRedirect from 'components/router/BangRedirect';
|
||||
@@ -25,6 +26,7 @@ const router = createHashRouter([
|
||||
children: [
|
||||
...(isExperimentalLayout ? EXPERIMENTAL_APP_ROUTES : STABLE_APP_ROUTES),
|
||||
...DASHBOARD_APP_ROUTES,
|
||||
...WIZARD_APP_ROUTES,
|
||||
{
|
||||
path: '!/*',
|
||||
Component: BangRedirect
|
||||
|
||||
@@ -17,7 +17,7 @@ export const DASHBOARD_APP_PATHS = {
|
||||
|
||||
export const DASHBOARD_APP_ROUTES: RouteObject[] = [
|
||||
{
|
||||
element: <ConnectionRequired isAdminRequired />,
|
||||
element: <ConnectionRequired level='admin' />,
|
||||
children: [
|
||||
{
|
||||
lazy: () => import('../AppLayout'),
|
||||
|
||||
@@ -35,47 +35,5 @@ export const LEGACY_PUBLIC_ROUTES: LegacyRoute[] = [
|
||||
controller: 'session/resetPassword/index',
|
||||
view: 'session/resetPassword/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'wizardremoteaccess',
|
||||
pageProps: {
|
||||
controller: 'wizard/remote/index',
|
||||
view: 'wizard/remote/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'wizardfinish',
|
||||
pageProps: {
|
||||
controller: 'wizard/finish/index',
|
||||
view: 'wizard/finish/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'wizardlibrary',
|
||||
pageProps: {
|
||||
controller: 'wizard/library',
|
||||
view: 'wizard/library.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'wizardsettings',
|
||||
pageProps: {
|
||||
controller: 'wizard/settings/index',
|
||||
view: 'wizard/settings/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'wizardstart',
|
||||
pageProps: {
|
||||
controller: 'wizard/start/index',
|
||||
view: 'wizard/start/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'wizarduser',
|
||||
pageProps: {
|
||||
controller: 'wizard/user/index',
|
||||
view: 'wizard/user/index.html'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -36,7 +36,7 @@ export const EXPERIMENTAL_APP_ROUTES: RouteObject[] = [
|
||||
|
||||
{
|
||||
/* Public routes */
|
||||
element: <ConnectionRequired isUserRequired={false} />,
|
||||
element: <ConnectionRequired level='public' />,
|
||||
children: [
|
||||
...LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute),
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LegacyRoute } from '../../../../components/router/LegacyRoute';
|
||||
import type { LegacyRoute } from 'components/router/LegacyRoute';
|
||||
|
||||
export const LEGACY_PUBLIC_ROUTES: LegacyRoute[] = [
|
||||
{
|
||||
@@ -35,47 +35,5 @@ export const LEGACY_PUBLIC_ROUTES: LegacyRoute[] = [
|
||||
controller: 'session/resetPassword/index',
|
||||
view: 'session/resetPassword/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'wizardremoteaccess',
|
||||
pageProps: {
|
||||
controller: 'wizard/remote/index',
|
||||
view: 'wizard/remote/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'wizardfinish',
|
||||
pageProps: {
|
||||
controller: 'wizard/finish/index',
|
||||
view: 'wizard/finish/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'wizardlibrary',
|
||||
pageProps: {
|
||||
controller: 'wizard/library',
|
||||
view: 'wizard/library.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'wizardsettings',
|
||||
pageProps: {
|
||||
controller: 'wizard/settings/index',
|
||||
view: 'wizard/settings/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'wizardstart',
|
||||
pageProps: {
|
||||
controller: 'wizard/start/index',
|
||||
view: 'wizard/start/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'wizarduser',
|
||||
pageProps: {
|
||||
controller: 'wizard/user/index',
|
||||
view: 'wizard/user/index.html'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -31,7 +31,7 @@ export const STABLE_APP_ROUTES: RouteObject[] = [
|
||||
|
||||
{
|
||||
/* Public routes */
|
||||
element: <ConnectionRequired isUserRequired={false} />,
|
||||
element: <ConnectionRequired level='public' />,
|
||||
children: [
|
||||
...LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute),
|
||||
/* Fallback route for invalid paths */
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import loading from '../../../components/loading/loading';
|
||||
import loading from 'components/loading/loading';
|
||||
|
||||
function onFinish() {
|
||||
loading.show();
|
||||
@@ -361,7 +361,7 @@ function getVirtualFolderHtml(page, virtualFolder, index) {
|
||||
|
||||
window.WizardLibraryPage = {
|
||||
next: function () {
|
||||
Dashboard.navigate('wizardsettings');
|
||||
Dashboard.navigate('wizard/settings');
|
||||
}
|
||||
};
|
||||
pageClassOn('pageshow', 'mediaLibraryPage', function () {
|
||||
+6
-6
@@ -1,8 +1,8 @@
|
||||
import loading from '../../../components/loading/loading';
|
||||
import '../../../elements/emby-checkbox/emby-checkbox';
|
||||
import '../../../elements/emby-button/emby-button';
|
||||
import '../../../elements/emby-select/emby-select';
|
||||
import Dashboard from '../../../utils/dashboard';
|
||||
import loading from 'components/loading/loading';
|
||||
import 'elements/emby-checkbox/emby-checkbox';
|
||||
import 'elements/emby-button/emby-button';
|
||||
import 'elements/emby-select/emby-select';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
|
||||
function save(page) {
|
||||
loading.show();
|
||||
@@ -23,7 +23,7 @@ function save(page) {
|
||||
}
|
||||
|
||||
function navigateToNextPage() {
|
||||
Dashboard.navigate('wizardfinish');
|
||||
Dashboard.navigate('wizard/finish');
|
||||
}
|
||||
|
||||
function onSubmit(e) {
|
||||
+6
-6
@@ -1,8 +1,8 @@
|
||||
import loading from '../../../components/loading/loading';
|
||||
import '../../../elements/emby-checkbox/emby-checkbox';
|
||||
import '../../../elements/emby-button/emby-button';
|
||||
import '../../../elements/emby-select/emby-select';
|
||||
import Dashboard from '../../../utils/dashboard';
|
||||
import loading from 'components/loading/loading';
|
||||
import 'elements/emby-checkbox/emby-checkbox';
|
||||
import 'elements/emby-button/emby-button';
|
||||
import 'elements/emby-select/emby-select';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
|
||||
function save(page) {
|
||||
loading.show();
|
||||
@@ -66,7 +66,7 @@ function reload(page) {
|
||||
}
|
||||
|
||||
function navigateToNextPage() {
|
||||
Dashboard.navigate('wizardremoteaccess');
|
||||
Dashboard.navigate('wizard/remoteaccess');
|
||||
}
|
||||
|
||||
function onSubmit(e) {
|
||||
@@ -1,7 +1,7 @@
|
||||
import loading from '../../../components/loading/loading';
|
||||
import '../../../elements/emby-button/emby-button';
|
||||
import '../../../elements/emby-select/emby-select';
|
||||
import Dashboard from '../../../utils/dashboard';
|
||||
import loading from 'components/loading/loading';
|
||||
import 'elements/emby-button/emby-button';
|
||||
import 'elements/emby-select/emby-select';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
import dom from 'scripts/dom';
|
||||
|
||||
function loadPage(page, config, languageOptions) {
|
||||
@@ -24,7 +24,7 @@ function save(page) {
|
||||
url: apiClient.getUrl('Startup/Configuration'),
|
||||
contentType: 'application/json'
|
||||
}).then(function () {
|
||||
Dashboard.navigate('wizarduser');
|
||||
Dashboard.navigate('wizard/user');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
import loading from '../../../components/loading/loading';
|
||||
import globalize from '../../../lib/globalize';
|
||||
import '../../../styles/dashboard.scss';
|
||||
import '../../../elements/emby-input/emby-input';
|
||||
import '../../../elements/emby-button/emby-button';
|
||||
import Dashboard from '../../../utils/dashboard';
|
||||
import toast from '../../../components/toast/toast';
|
||||
import loading from 'components/loading/loading';
|
||||
import globalize from 'lib/globalize';
|
||||
import 'styles/dashboard.scss';
|
||||
import 'elements/emby-input/emby-input';
|
||||
import 'elements/emby-button/emby-button';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
import toast from 'components/toast/toast';
|
||||
|
||||
function getApiClient() {
|
||||
return ApiClient;
|
||||
}
|
||||
|
||||
function nextWizardPage() {
|
||||
Dashboard.navigate('wizardlibrary')
|
||||
Dashboard.navigate('wizard/library')
|
||||
.catch(err => {
|
||||
console.error('[Wizard > User] error navigating to library setup', err);
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import { Navigate, RouteObject } from 'react-router-dom';
|
||||
|
||||
import AppLayout from 'apps/stable/AppLayout';
|
||||
import { AppType } from 'constants/appType';
|
||||
import ConnectionRequired from 'components/ConnectionRequired';
|
||||
import ErrorBoundary from 'components/router/ErrorBoundary';
|
||||
import { type LegacyRoute, toViewManagerPageRoute } from 'components/router/LegacyRoute';
|
||||
|
||||
const ROUTES: LegacyRoute[] = [
|
||||
{
|
||||
path: 'remoteaccess',
|
||||
pageProps: {
|
||||
appType: AppType.Wizard,
|
||||
controller: 'remote/index',
|
||||
view: 'remote/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'finish',
|
||||
pageProps: {
|
||||
appType: AppType.Wizard,
|
||||
controller: 'finish/index',
|
||||
view: 'finish/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'library',
|
||||
pageProps: {
|
||||
appType: AppType.Wizard,
|
||||
controller: 'library',
|
||||
view: 'library.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
pageProps: {
|
||||
appType: AppType.Wizard,
|
||||
controller: 'settings/index',
|
||||
view: 'settings/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'start',
|
||||
pageProps: {
|
||||
appType: AppType.Wizard,
|
||||
controller: 'start/index',
|
||||
view: 'start/index.html'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'user',
|
||||
pageProps: {
|
||||
appType: AppType.Wizard,
|
||||
controller: 'user/index',
|
||||
view: 'user/index.html'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export const WIZARD_APP_ROUTES: RouteObject[] = [
|
||||
{
|
||||
element: <ConnectionRequired level='wizard' />,
|
||||
children: [
|
||||
{
|
||||
Component: AppLayout,
|
||||
path: 'wizard',
|
||||
children: [
|
||||
{ index: true, element: <Navigate replace to='start' /> },
|
||||
...ROUTES.map(toViewManagerPageRoute)
|
||||
],
|
||||
ErrorBoundary
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -1,23 +1,49 @@
|
||||
import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
import type { ConnectResponse } from 'jellyfin-apiclient';
|
||||
import type { ApiClient, ConnectResponse } from 'jellyfin-apiclient';
|
||||
|
||||
import globalize from 'lib/globalize';
|
||||
import { ConnectionState } from 'utils/jellyfin-apiclient/ConnectionState';
|
||||
|
||||
import alert from './alert';
|
||||
import Loading from './loading/LoadingComponent';
|
||||
import ServerConnections from './ServerConnections';
|
||||
import globalize from '../lib/globalize';
|
||||
import { ConnectionState } from '../utils/jellyfin-apiclient/ConnectionState';
|
||||
|
||||
enum AccessLevel {
|
||||
/** Requires a user with administrator access */
|
||||
Admin = 'admin',
|
||||
/** No access restrictions */
|
||||
Public = 'public',
|
||||
/** Requires a valid user session */
|
||||
User = 'user',
|
||||
/** Requires the startup wizard to NOT be completed */
|
||||
Wizard = 'wizard'
|
||||
};
|
||||
|
||||
type AccessLevelValue = `${AccessLevel}`;
|
||||
|
||||
enum BounceRoutes {
|
||||
Home = '/home',
|
||||
Login = '/login',
|
||||
SelectServer = '/selectserver',
|
||||
StartWizard = '/wizardstart'
|
||||
StartWizard = '/wizard/start'
|
||||
}
|
||||
|
||||
type ConnectionRequiredProps = {
|
||||
isAdminRequired?: boolean,
|
||||
isUserRequired?: boolean
|
||||
level?: AccessLevelValue
|
||||
};
|
||||
|
||||
const fetchPublicSystemInfo = async (apiClient: ApiClient) => {
|
||||
const infoResponse = await fetch(
|
||||
`${apiClient.serverAddress()}/System/Info/Public`,
|
||||
{ cache: 'no-cache' }
|
||||
);
|
||||
|
||||
if (!infoResponse.ok) {
|
||||
throw new Error('Public system info request failed');
|
||||
}
|
||||
|
||||
return infoResponse.json();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -26,8 +52,7 @@ type ConnectionRequiredProps = {
|
||||
* If a condition fails, this component will navigate to the appropriate page.
|
||||
*/
|
||||
const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
|
||||
isAdminRequired = false,
|
||||
isUserRequired = true
|
||||
level = 'user'
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
@@ -76,17 +101,27 @@ const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
|
||||
}
|
||||
|
||||
console.warn('[ConnectionRequired] unhandled connection state', connectionResponse.State);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [location.pathname, navigate]);
|
||||
|
||||
const handleWizard = useCallback(async (firstConnection: ConnectResponse | null) => {
|
||||
const apiClient = firstConnection?.ApiClient || ServerConnections.currentApiClient();
|
||||
if (!apiClient) {
|
||||
throw new Error('No ApiClient available');
|
||||
}
|
||||
|
||||
const systemInfo = await fetchPublicSystemInfo(apiClient);
|
||||
if (systemInfo?.StartupWizardCompleted) {
|
||||
console.info('[ConnectionRequired] startup wizard is complete, redirecting home');
|
||||
navigate(BounceRoutes.Home);
|
||||
}
|
||||
}, [ navigate ]);
|
||||
|
||||
const handleIncompleteWizard = useCallback(async (firstConnection: ConnectResponse) => {
|
||||
if (firstConnection.State === ConnectionState.ServerSignIn) {
|
||||
// Verify the wizard is complete
|
||||
try {
|
||||
const infoResponse = await fetch(`${firstConnection.ApiClient.serverAddress()}/System/Info/Public`, { cache: 'no-cache' });
|
||||
if (!infoResponse.ok) {
|
||||
throw new Error('Public system info request failed');
|
||||
}
|
||||
const systemInfo = await infoResponse.json();
|
||||
const systemInfo = await fetchPublicSystemInfo(firstConnection.ApiClient);
|
||||
if (!systemInfo?.StartupWizardCompleted) {
|
||||
// Update the current ApiClient
|
||||
// TODO: Is there a better place to handle this?
|
||||
@@ -113,7 +148,7 @@ const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
|
||||
const client = ServerConnections.currentApiClient();
|
||||
|
||||
// If this is a user route, ensure a user is logged in
|
||||
if ((isAdminRequired || isUserRequired) && !client?.isLoggedIn()) {
|
||||
if ((level === AccessLevel.Admin || level === AccessLevel.User) && !client?.isLoggedIn()) {
|
||||
try {
|
||||
console.warn('[ConnectionRequired] unauthenticated user attempted to access user route');
|
||||
bounce(await ServerConnections.connect())
|
||||
@@ -127,7 +162,7 @@ const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
|
||||
}
|
||||
|
||||
// If this is an admin route, ensure the user has access
|
||||
if (isAdminRequired) {
|
||||
if (level === AccessLevel.Admin) {
|
||||
try {
|
||||
const user = await client?.getCurrentUser();
|
||||
if (!user?.Policy?.IsAdministrator) {
|
||||
@@ -145,7 +180,7 @@ const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
}, [bounce, isAdminRequired, isUserRequired]);
|
||||
}, [bounce, level]);
|
||||
|
||||
useEffect(() => {
|
||||
// Check connection status on initial page load
|
||||
@@ -155,7 +190,14 @@ const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
|
||||
console.debug('[ConnectionRequired] connection state', firstConnection?.State);
|
||||
ServerConnections.firstConnection = true;
|
||||
|
||||
if (firstConnection && firstConnection.State !== ConnectionState.SignedIn && !apiClient?.isLoggedIn()) {
|
||||
if (level === AccessLevel.Wizard) {
|
||||
handleWizard(firstConnection)
|
||||
.catch(err => {
|
||||
console.error('[ConnectionRequired] could not validate wizard status', err);
|
||||
});
|
||||
} else if (
|
||||
firstConnection && firstConnection.State !== ConnectionState.SignedIn && !apiClient?.isLoggedIn()
|
||||
) {
|
||||
handleIncompleteWizard(firstConnection)
|
||||
.catch(err => {
|
||||
console.error('[ConnectionRequired] could not start wizard', err);
|
||||
@@ -169,7 +211,7 @@ const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
|
||||
}).catch(err => {
|
||||
console.error('[ConnectionRequired] failed to connect', err);
|
||||
});
|
||||
}, [handleIncompleteWizard, validateUserAccess]);
|
||||
}, [handleIncompleteWizard, handleWizard, level, validateUserAccess]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
|
||||
@@ -15,6 +15,11 @@ const FallbackRoute = () => {
|
||||
hash: location.hash
|
||||
};
|
||||
|
||||
// Redirect old wizard paths
|
||||
if (RegExp(/^\/wizard[a-z]+\.html/i).test(location.pathname)) {
|
||||
return { ..._to, pathname: `/wizard/${location.pathname.slice(7, -5)}` };
|
||||
}
|
||||
|
||||
// If a path ends in ".html", redirect to the path with it removed
|
||||
if (location.pathname.endsWith('.html')) {
|
||||
return { ..._to, pathname: location.pathname.slice(0, -5) };
|
||||
|
||||
@@ -38,19 +38,26 @@ const importController = (
|
||||
controller: string,
|
||||
view: string
|
||||
) => {
|
||||
if (appType === AppType.Dashboard) {
|
||||
return Promise.all([
|
||||
import(/* webpackChunkName: "[request]" */ `../../apps/dashboard/controllers/${controller}`),
|
||||
import(/* webpackChunkName: "[request]" */ `../../apps/dashboard/controllers/${view}`)
|
||||
.then(html => globalize.translateHtml(html))
|
||||
]);
|
||||
switch (appType) {
|
||||
case AppType.Dashboard:
|
||||
return Promise.all([
|
||||
import(/* webpackChunkName: "[request]" */ `../../apps/dashboard/controllers/${controller}`),
|
||||
import(/* webpackChunkName: "[request]" */ `../../apps/dashboard/controllers/${view}`)
|
||||
.then(html => globalize.translateHtml(html))
|
||||
]);
|
||||
case AppType.Wizard:
|
||||
return Promise.all([
|
||||
import(/* webpackChunkName: "[request]" */ `../../apps/wizard/controllers/${controller}`),
|
||||
import(/* webpackChunkName: "[request]" */ `../../apps/wizard/controllers/${view}`)
|
||||
.then(html => globalize.translateHtml(html))
|
||||
]);
|
||||
default:
|
||||
return Promise.all([
|
||||
import(/* webpackChunkName: "[request]" */ `../../controllers/${controller}`),
|
||||
import(/* webpackChunkName: "[request]" */ `../../controllers/${view}`)
|
||||
.then(html => globalize.translateHtml(html))
|
||||
]);
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
import(/* webpackChunkName: "[request]" */ `../../controllers/${controller}`),
|
||||
import(/* webpackChunkName: "[request]" */ `../../controllers/${view}`)
|
||||
.then(html => globalize.translateHtml(html))
|
||||
]);
|
||||
};
|
||||
|
||||
const loadView = async (
|
||||
|
||||
@@ -4,5 +4,6 @@
|
||||
export enum AppType {
|
||||
Dashboard = 'dashboard',
|
||||
Experimental = 'experimental',
|
||||
Stable = 'stable'
|
||||
Stable = 'stable',
|
||||
Wizard = 'wizard'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user