diff --git a/ui/v2.5/src/components/Setup/Setup.tsx b/ui/v2.5/src/components/Setup/Setup.tsx index 9168907949e..1f8ec4614a4 100644 --- a/ui/v2.5/src/components/Setup/Setup.tsx +++ b/ui/v2.5/src/components/Setup/Setup.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useContext } from "react"; +import React, { useState, useContext, useCallback } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { Alert, @@ -29,45 +29,55 @@ import { import { releaseNotes } from "src/docs/en/ReleaseNotes"; import { ExternalLink } from "../Shared/ExternalLink"; -export const Setup: React.FC = () => { - const { configuration, loading: configLoading } = - useContext(ConfigurationContext); - const [saveUI] = useConfigureUI(); +interface ISetupContextState { + configuration: GQL.ConfigDataFragment; + systemStatus: GQL.SystemStatusQuery; - const [step, setStep] = useState(0); - const [setupInWorkDir, setSetupInWorkDir] = useState(false); - const [stashes, setStashes] = useState([]); - const [showStashAlert, setShowStashAlert] = useState(false); - const [databaseFile, setDatabaseFile] = useState(""); - const [generatedLocation, setGeneratedLocation] = useState(""); - const [cacheLocation, setCacheLocation] = useState(""); - const [storeBlobsInDatabase, setStoreBlobsInDatabase] = useState(false); - const [blobsLocation, setBlobsLocation] = useState(""); - const [loading, setLoading] = useState(false); - const [setupError, setSetupError] = useState(); - const [downloadFFmpeg, setDownloadFFmpeg] = useState(true); + setupState: Partial; + setupError: string | undefined; - const intl = useIntl(); - const history = useHistory(); + pathJoin: (...paths: string[]) => string; + pathDir(path: string): string; - const [showGeneratedSelectDialog, setShowGeneratedSelectDialog] = - useState(false); - const [showCacheSelectDialog, setShowCacheSelectDialog] = useState(false); - const [showBlobsDialog, setShowBlobsDialog] = useState(false); + homeDir: string; + windows: boolean; + macApp: boolean; + homeDirPath: string; + pwd: string; + workingDir: string; +} - const { data: systemStatus, loading: statusLoading } = useSystemStatus(); - const status = systemStatus?.systemStatus; +const SetupStateContext = React.createContext(null); - const [mutateDownloadFFMpeg] = GQL.useDownloadFfMpegMutation(); +const useSetupContext = () => { + const context = React.useContext(SetupStateContext); + + if (context === null) { + throw new Error("useSettings must be used within a SettingsContext"); + } + + return context; +}; + +const SetupContext: React.FC<{ + setupState: Partial; + setupError: string | undefined; + systemStatus: GQL.SystemStatusQuery; + configuration: GQL.ConfigDataFragment; +}> = ({ setupState, setupError, systemStatus, configuration, children }) => { + const status = systemStatus?.systemStatus; const windows = status?.os === "windows"; const pathSep = windows ? "\\" : "/"; const homeDir = windows ? "%USERPROFILE%" : "$HOME"; const pwd = windows ? "%CD%" : "$PWD"; - function pathJoin(...paths: string[]) { - return paths.join(pathSep); - } + const pathJoin = useCallback( + (...paths: string[]) => { + return paths.join(pathSep); + }, + [pathSep] + ); // simply returns everything preceding the last path separator function pathDir(path: string) { @@ -83,528 +93,877 @@ export const Setup: React.FC = () => { // so in this situation disallow setting up in the working directory. const macApp = status?.os === "darwin" && workingDir === "/"; - const fallbackStashDir = pathJoin(homeDir, ".stash"); - const fallbackConfigPath = pathJoin(fallbackStashDir, "config.yml"); + const homeDirPath = pathJoin(status?.homeDir ?? homeDir, ".stash"); + + const state: ISetupContextState = { + systemStatus, + configuration, + windows, + macApp, + pathJoin, + pathDir, + homeDir, + homeDirPath, + pwd, + workingDir, + setupState, + setupError, + }; - const overrideConfig = status?.configPath; - const overrideGenerated = configuration?.general.generatedPath; - const overrideCache = configuration?.general.cachePath; - const overrideBlobs = configuration?.general.blobsPath; - const overrideDatabase = configuration?.general.databasePath; + return ( + + {children} + + ); +}; - useEffect(() => { - if (configuration) { - const configStashes = configuration.general.stashes; - if (configStashes.length > 0) { - setStashes( - configStashes.map((s) => { - const { __typename, ...withoutTypename } = s; - return withoutTypename; - }) - ); - } - } - }, [configuration]); +interface IWizardStep { + next: (input?: Partial) => void; + goBack: () => void; +} - const discordLink = ( - Discord - ); - const githubLink = ( - - - - ); +const WelcomeSpecificConfig: React.FC = ({ next }) => { + const { systemStatus } = useSetupContext(); + const status = systemStatus?.systemStatus; + const overrideConfig = status?.configPath; - function onConfigLocationChosen(inWorkDir: boolean) { - setSetupInWorkDir(inWorkDir); - next(); + function onNext() { + next({ configLocation: overrideConfig! }); } - function goBack(n?: number) { - let dec = n; - if (!dec) { - dec = 1; - } - setStep(Math.max(0, step - dec)); - } + return ( + <> +
+

+ +

+

+ +

+

+ {chunks}, + }} + /> +

+

+ +

+
+ +
+
+ +
+
+ + ); +}; - function next() { - setStep(step + 1); - } +const DefaultWelcomeStep: React.FC = ({ next }) => { + const { pathJoin, homeDir, macApp, homeDirPath, pwd, workingDir } = + useSetupContext(); - function confirmPaths() { - if (stashes.length > 0) { - next(); - return; - } + const fallbackStashDir = pathJoin(homeDir, ".stash"); + const fallbackConfigPath = pathJoin(fallbackStashDir, "config.yml"); - setShowStashAlert(true); + function onConfigLocationChosen(inWorkingDir: boolean) { + const configLocation = inWorkingDir ? "config.yml" : ""; + next({ configLocation }); } - function maybeRenderStashAlert() { - if (!showStashAlert) { - return; - } - - return ( - { - setShowStashAlert(false); - next(); - }, - }} - cancel={{ onClick: () => setShowStashAlert(false) }} - > + return ( + <> +
+

+ +

+

+ +

- + {chunks}, + fallback_path: fallbackConfigPath, + }} + />

- - ); - } - - const WelcomeSpecificConfig = () => { - return ( - <> -
-

- -

-

- -

-

- {chunks}, - }} - /> -

-

- -

-
- -
-
- -
-
- - ); - }; + + {chunks}, + }} + /> + +

+ +

+
- function DefaultWelcomeStep() { - const homeDirPath = pathJoin(status?.homeDir ?? homeDir, ".stash"); +
+

+ +

- return ( - <> -
-

- -

-

- -

-

- {chunks}, - fallback_path: fallbackConfigPath, - }} - /> -

- +
+
- -
-

- -

- -
- - + -
-
- - ); - } + + + ) : ( + <> + {chunks}, + path: pwd, + }} + /> +
+ {workingDir} + + )} + + +
+ + ); +}; - function onGeneratedSelectClosed(d?: string) { - if (d) { - setGeneratedLocation(d); - } +const WelcomeStep: React.FC = (props) => { + const { systemStatus } = useSetupContext(); + const status = systemStatus?.systemStatus; + const overrideConfig = status?.configPath; - setShowGeneratedSelectDialog(false); - } + return overrideConfig ? ( + + ) : ( + + ); +}; - function maybeRenderGeneratedSelectDialog() { - if (!showGeneratedSelectDialog) { - return; - } +const StashAlert: React.FC<{ close: (confirm: boolean) => void }> = ({ + close, +}) => { + const intl = useIntl(); - return ; - } + return ( + close(true), + }} + cancel={{ onClick: () => close(false) }} + > +

+ +

+
+ ); +}; - function onBlobsClosed(d?: string) { - if (d) { - setBlobsLocation(d); - } +const DatabaseSection: React.FC<{ + databaseFile: string; + setDatabaseFile: React.Dispatch>; +}> = ({ databaseFile, setDatabaseFile }) => { + const intl = useIntl(); - setShowBlobsDialog(false); - } + return ( + +

+ +

+

+ {chunks}, + }} + /> +
+ {chunks}, + }} + /> +

+ setDatabaseFile(e.currentTarget.value)} + /> +
+ ); +}; - function maybeRenderBlobsSelectDialog() { - if (!showBlobsDialog) { - return; +const DirectorySelector: React.FC<{ + value: string; + setValue: React.Dispatch>; + placeholder: string; + disabled?: boolean; +}> = ({ value, setValue, placeholder, disabled = false }) => { + const [showSelectDialog, setShowSelectDialog] = useState(false); + + function onSelectClosed(dir?: string) { + if (dir) { + setValue(dir); } - - return ; + setShowSelectDialog(false); } - function maybeRenderDatabase() { - if (overrideDatabase) return; - - return ( - -

- -

-

- {chunks}, - }} - /> -
- {chunks}, - }} - /> -

+ return ( + <> + {showSelectDialog ? ( + + ) : null} + setValue(e.currentTarget.value)} + disabled={disabled} + /> + + + + + + ); +}; + +const GeneratedSection: React.FC<{ + generatedLocation: string; + setGeneratedLocation: React.Dispatch>; +}> = ({ generatedLocation, setGeneratedLocation }) => { + const intl = useIntl(); + + return ( + +

+ +

+

+ {chunks}, + }} + /> +

+ +
+ ); +}; + +const CacheSection: React.FC<{ + cacheLocation: string; + setCacheLocation: React.Dispatch>; +}> = ({ cacheLocation, setCacheLocation }) => { + const intl = useIntl(); + + return ( + +

+ +

+

+ {chunks}, + }} + /> +

+ +
+ ); +}; + +const BlobsSection: React.FC<{ + blobsLocation: string; + setBlobsLocation: React.Dispatch>; + storeBlobsInDatabase: boolean; + setStoreBlobsInDatabase: React.Dispatch>; +}> = ({ + blobsLocation, + setBlobsLocation, + storeBlobsInDatabase, + setStoreBlobsInDatabase, +}) => { + const intl = useIntl(); + + return ( + +

+ +

+

+ {chunks}, + }} + /> +

+

+ {chunks}, + strong: (chunks: string) => {chunks}, + }} + /> +

+ +
+ setStoreBlobsInDatabase(!storeBlobsInDatabase)} + /> +
+ +
+ setDatabaseFile(e.currentTarget.value)} + disabled={storeBlobsInDatabase} /> - - ); +
+
+ ); +}; + +const SetPathsStep: React.FC = ({ goBack, next }) => { + const { configuration } = useSetupContext(); + + const [showStashAlert, setShowStashAlert] = useState(false); + + const [stashes, setStashes] = useState([]); + const [databaseFile, setDatabaseFile] = useState(""); + const [generatedLocation, setGeneratedLocation] = useState(""); + const [cacheLocation, setCacheLocation] = useState(""); + const [storeBlobsInDatabase, setStoreBlobsInDatabase] = useState(false); + const [blobsLocation, setBlobsLocation] = useState(""); + + const overrideDatabase = configuration?.general.databasePath; + const overrideGenerated = configuration?.general.generatedPath; + const overrideCache = configuration?.general.cachePath; + const overrideBlobs = configuration?.general.blobsPath; + + function preNext() { + if (stashes.length === 0) { + setShowStashAlert(true); + } else { + onNext(); + } } - function maybeRenderGenerated() { - if (overrideGenerated) return; + function onNext() { + const input: Partial = { + stashes, + databaseFile, + generatedLocation, + cacheLocation, + blobsLocation: storeBlobsInDatabase ? "" : blobsLocation, + storeBlobsInDatabase, + }; + next(input); + } - return ( - -

- -

+ return ( + <> + {showStashAlert ? ( + { + setShowStashAlert(false); + if (confirm) { + onNext(); + } + }} + /> + ) : null} +
+

+ +

- {chunks}, - }} - /> +

- - setGeneratedLocation(e.currentTarget.value)} +
+
+ +

+ +

+

+ +

+ + setStashes(s)} + /> + +
+ {overrideDatabase ? null : ( + - - - - - - ); - } + )} + {overrideGenerated ? null : ( + + )} + {overrideCache ? null : ( + + )} + {overrideBlobs ? null : ( + + )} +
+
+
+ + +
+
+ + ); +}; - function onCacheSelectClosed(d?: string) { - if (d) { - setCacheLocation(d); - } +const StashExclusions: React.FC<{ stash: GQL.StashConfig }> = ({ stash }) => { + if (!stash.excludeImage && !stash.excludeVideo) { + return null; + } - setShowCacheSelectDialog(false); + const excludes = []; + if (stash.excludeVideo) { + excludes.push("videos"); + } + if (stash.excludeImage) { + excludes.push("images"); } - function maybeRenderCacheSelectDialog() { - if (!showCacheSelectDialog) { - return; - } + return {`(excludes ${excludes.join(" and ")})`}; +}; + +const ConfirmStep: React.FC = ({ goBack, next }) => { + const { configuration, pathDir, pathJoin, pwd, setupState } = + useSetupContext(); + + const cfgFile = setupState.configLocation + ? setupState.configLocation + : pathJoin(pwd, "config.yml"); + const cfgDir = pathDir(cfgFile); + const stashes = setupState.stashes ?? []; + const { + databaseFile, + generatedLocation, + cacheLocation, + blobsLocation, + storeBlobsInDatabase, + } = setupState; - return ; + const overrideDatabase = configuration?.general.databasePath; + const overrideGenerated = configuration?.general.generatedPath; + const overrideCache = configuration?.general.cachePath; + const overrideBlobs = configuration?.general.blobsPath; + + function joinCfgDir(path: string) { + if (cfgDir) { + return pathJoin(cfgDir, path); + } else { + return path; + } } - function maybeRenderCache() { - if (overrideCache) return; + return ( + <> +
+

+ +

+

+ +

+
+
+ +
+
+ {cfgFile} +
+
+
+
+ +
+
+
    + {stashes.map((s) => ( +
  • + {s.path} + +
  • + ))} +
+
+
+ {!overrideDatabase && ( +
+
+ +
+
+ {databaseFile || joinCfgDir("stash-go.sqlite")} +
+
+ )} + {!overrideGenerated && ( +
+
+ +
+
+ {generatedLocation || joinCfgDir("generated")} +
+
+ )} + {!overrideCache && ( +
+
+ +
+
+ {cacheLocation || joinCfgDir("cache")} +
+
+ )} + {!overrideBlobs && ( +
+
+ +
+
+ + {storeBlobsInDatabase ? ( + + ) : ( + blobsLocation || joinCfgDir("blobs") + )} + +
+
+ )} +
+
+
+ + +
+
+ + ); +}; - return ( - -

- -

+const DiscordLink = ( + Discord +); +const GithubLink = ( + + + +); + +const ErrorStep: React.FC<{ error: string; goBack: () => void }> = ({ + error, + goBack, +}) => { + return ( + <> +
+

+ +

{chunks}, - }} + id="setup.errors.something_went_wrong_while_setting_up_your_system" + values={{ error:

{error}
}} />

- - setCacheLocation(e.currentTarget.value)} +

+ - - - - - - ); - } +

+
+
+
+ +
+
+ + ); +}; - function maybeRenderBlobs() { - if (overrideBlobs) return; +const SuccessStep: React.FC<{}> = () => { + const intl = useIntl(); + const history = useHistory(); - return ( - -

- -

+ const [mutateDownloadFFMpeg] = GQL.useDownloadFfMpegMutation(); + + const [downloadFFmpeg, setDownloadFFmpeg] = useState(true); + + const { systemStatus } = useSetupContext(); + const status = systemStatus?.systemStatus; + + function onFinishClick() { + if ((!status?.ffmpegPath || !status?.ffprobePath) && downloadFFmpeg) { + mutateDownloadFFMpeg(); + } + + history.push("/settings?tab=library"); + } + + return ( + <> +
+

+ +

+

+ +

{chunks}, + localized_task: intl.formatMessage({ + id: "config.categories.tasks", + }), + localized_scan: intl.formatMessage({ id: "actions.scan" }), }} />

+ {!status?.ffmpegPath || !status?.ffprobePath ? ( + <> + + {chunks}, + }} + /> + +

+ setDownloadFFmpeg(!downloadFFmpeg)} + /> +

+ + ) : null} +
+
+

+ +

+

+ }} + /> +

+

+
+
+

+ +

+

+ {chunks}, - strong: (chunks: string) => {chunks}, + open_collective_link: ( + + Open Collective + + ), }} />

-

- setStoreBlobsInDatabase(!storeBlobsInDatabase)} - /> +

+
+
+

+ +

+
+
+
+ +
+
+ + ); +}; - {!storeBlobsInDatabase && ( - - setBlobsLocation(e.currentTarget.value)} - disabled={storeBlobsInDatabase} - /> - - - - - )} -
- ); - } +const FinishStep: React.FC = ({ goBack }) => { + const { setupError } = useSetupContext(); - function SetPathsStep() { - return ( - <> - {maybeRenderStashAlert()} -
-

- -

-

- -

-
-
- -

- -

-

- -

- - setStashes(s)} - /> - -
- {maybeRenderDatabase()} - {maybeRenderGenerated()} - {maybeRenderCache()} - {maybeRenderBlobs()} -
-
-
- - -
-
- - ); + if (setupError !== undefined) { + return ; } - function maybeRenderExclusions(s: GQL.StashConfig) { - if (!s.excludeImage && !s.excludeVideo) { - return; - } + return ; +}; - const excludes = []; - if (s.excludeVideo) { - excludes.push("videos"); - } - if (s.excludeImage) { - excludes.push("images"); - } +export const Setup: React.FC = () => { + const intl = useIntl(); + const { configuration, loading: configLoading } = + useContext(ConfigurationContext); - return `(excludes ${excludes.join(" and ")})`; - } + const [saveUI] = useConfigureUI(); - async function onSave() { - let configLocation = overrideConfig; - if (!configLocation) { - configLocation = setupInWorkDir ? "config.yml" : ""; - } + const { + data: systemStatus, + loading: statusLoading, + error: statusError, + } = useSystemStatus(); + + const [step, setStep] = useState(0); + const [setupInput, setSetupInput] = useState>({}); + const [creating, setCreating] = useState(false); + const [setupError, setSetupError] = useState(undefined); + + const history = useHistory(); + const steps: React.FC[] = [ + WelcomeStep, + SetPathsStep, + ConfirmStep, + FinishStep, + ]; + const Step = steps[step]; + + async function createSystem() { try { - setLoading(true); - await mutateSetup({ - configLocation, - databaseFile, - generatedLocation, - cacheLocation, - storeBlobsInDatabase, - blobsLocation, - stashes, - }); + setCreating(true); + setSetupError(undefined); + await mutateSetup(setupInput as GQL.SetupInput); // Set lastNoteSeen to hide release notes dialog await saveUI({ variables: { @@ -621,318 +980,95 @@ export const Setup: React.FC = () => { setSetupError(String(e)); } } finally { - setLoading(false); - next(); - } - } - - function ConfirmStep() { - let cfgDir: string; - let config: string; - if (overrideConfig) { - cfgDir = pathDir(overrideConfig); - config = overrideConfig; - } else { - cfgDir = setupInWorkDir ? pwd : fallbackStashDir; - config = pathJoin(cfgDir, "config.yml"); + setCreating(false); + setStep(step + 1); } - - function joinCfgDir(path: string) { - if (cfgDir) { - return pathJoin(cfgDir, path); - } else { - return path; - } - } - - return ( - <> -
-

- -

-

- -

-
-
- -
-
- {config} -
-
-
-
- -
-
-
    - {stashes.map((s) => ( -
  • - {s.path} - {maybeRenderExclusions(s)} -
  • - ))} -
-
-
- {!overrideDatabase && ( -
-
- -
-
- {databaseFile || joinCfgDir("stash-go.sqlite")} -
-
- )} - {!overrideGenerated && ( -
-
- -
-
- {generatedLocation || joinCfgDir("generated")} -
-
- )} - {!overrideCache && ( -
-
- -
-
- {cacheLocation || joinCfgDir("cache")} -
-
- )} - {!overrideBlobs && ( -
-
- -
-
- - {storeBlobsInDatabase ? ( - - ) : ( - blobsLocation || joinCfgDir("blobs") - )} - -
-
- )} -
-
-
- - -
-
- - ); } - function ErrorStep() { - function onBackClick() { - setSetupError(undefined); - goBack(2); - } - - return ( - <> -
-

- -

-

- {setupError} }} - /> -

-

- -

-
-
-
- -
-
- - ); - } + function next(input?: Partial) { + setSetupInput({ ...setupInput, ...input }); - function onFinishClick() { - if ((!status?.ffmpegPath || !status?.ffprobePath) && downloadFFmpeg) { - mutateDownloadFFMpeg(); + if (Step === ConfirmStep) { + // create the system + createSystem(); + } else { + setStep(step + 1); } - - history.push("/settings?tab=library"); } - function SuccessStep() { - return ( - <> -
-

- -

-

- -

-

- {chunks}, - localized_task: intl.formatMessage({ - id: "config.categories.tasks", - }), - localized_scan: intl.formatMessage({ id: "actions.scan" }), - }} - /> -

- {!status?.ffmpegPath || !status?.ffprobePath ? ( - <> - - {chunks}, - }} - /> - -

- setDownloadFFmpeg(!downloadFFmpeg)} - /> -

- - ) : null} -
-
-

- -

-

- }} - /> -

-

- -

-
-
-

- -

-

- - Open Collective - - ), - }} - /> -

-

- -

-
-
-

- -

-
-
-
- -
-
- - ); - } - - function FinishStep() { - if (setupError !== undefined) { - return ; + function goBack() { + if (Step === FinishStep) { + // go back to the step before ConfirmStep + setStep(step - 2); + } else { + setStep(step - 1); } - - return ; } - // only display setup wizard if system is not setup - if (statusLoading || configLoading) { + if (configLoading || statusLoading) { return ; } - if (step === 0 && status && status.status !== GQL.SystemStatusEnum.Setup) { + if ( + step === 0 && + systemStatus && + systemStatus.systemStatus.status !== GQL.SystemStatusEnum.Setup + ) { // redirect to main page history.push("/"); return ; } - const WelcomeStep = overrideConfig - ? WelcomeSpecificConfig - : DefaultWelcomeStep; - const steps = [WelcomeStep, SetPathsStep, ConfirmStep, FinishStep]; - const Step = steps[step]; + if (statusError) { + return ( + + + + + + ); + } - function renderCreating() { + if (!configuration || !systemStatus) { return ( - - - + + + + + ); } return ( - - {maybeRenderGeneratedSelectDialog()} - {maybeRenderCacheSelectDialog()} - {maybeRenderBlobsSelectDialog()} -

- -

- {loading ? ( - renderCreating() - ) : ( + + +

+ +

- + {creating ? ( + + ) : ( + + )} - )} -
+
+ ); }; diff --git a/ui/v2.5/src/components/Setup/styles.scss b/ui/v2.5/src/components/Setup/styles.scss index 36db2798a57..0eceeb8e521 100644 --- a/ui/v2.5/src/components/Setup/styles.scss +++ b/ui/v2.5/src/components/Setup/styles.scss @@ -24,3 +24,10 @@ margin-left: 0.5rem; } } + +.setup-wizard { + #blobs > div { + margin-bottom: 1rem; + margin-top: 0; + } +} diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index edad1c8e7cf..784579c95b8 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -1298,7 +1298,9 @@ "errors": { "something_went_wrong": "Oh no! Something went wrong!", "something_went_wrong_description": "If this looks like a problem with your inputs, go ahead and click back to fix them up. Otherwise, raise a bug on the {githubLink} or seek help in the {discordLink}.", - "something_went_wrong_while_setting_up_your_system": "Something went wrong while setting up your system. Here is the error we received: {error}" + "something_went_wrong_while_setting_up_your_system": "Something went wrong while setting up your system. Here is the error we received: {error}", + "unable_to_retrieve_system_status": "Unable to retrieve system status: {error}", + "unexpected_error": "An unexpected error occurred: {error}" }, "folder": { "file_path": "File path",