diff --git a/ui/v2.5/src/components/Setup/Setup.tsx b/ui/v2.5/src/components/Setup/Setup.tsx index 916890794..1f8ec4614 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 SetupStateContext = React.createContext(null); + +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 [mutateDownloadFFMpeg] = GQL.useDownloadFfMpegMutation(); - 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 homeDirPath = pathJoin(status?.homeDir ?? homeDir, ".stash"); + + const state: ISetupContextState = { + systemStatus, + configuration, + windows, + macApp, + pathJoin, + pathDir, + homeDir, + homeDirPath, + pwd, + workingDir, + setupState, + setupError, + }; + + return ( + + {children} + + ); +}; + +interface IWizardStep { + next: (input?: Partial) => void; + goBack: () => void; +} + +const WelcomeSpecificConfig: React.FC = ({ next }) => { + const { systemStatus } = useSetupContext(); + const status = systemStatus?.systemStatus; + const overrideConfig = status?.configPath; + + function onNext() { + next({ configLocation: overrideConfig! }); + } + + return ( + <> +
+

+ +

+

+ +

+

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

+

+ +

+
+ +
+
+ +
+
+ + ); +}; + +const DefaultWelcomeStep: React.FC = ({ next }) => { + const { pathJoin, homeDir, macApp, homeDirPath, pwd, workingDir } = + useSetupContext(); + const fallbackStashDir = pathJoin(homeDir, ".stash"); const fallbackConfigPath = pathJoin(fallbackStashDir, "config.yml"); + function onConfigLocationChosen(inWorkingDir: boolean) { + const configLocation = inWorkingDir ? "config.yml" : ""; + next({ configLocation }); + } + + return ( + <> +
+

+ +

+

+ +

+

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

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

+ +

+
+ +
+

+ +

+ +
+ + +
+
+ + ); +}; + +const WelcomeStep: React.FC = (props) => { + const { systemStatus } = useSetupContext(); + const status = systemStatus?.systemStatus; const overrideConfig = status?.configPath; + + return overrideConfig ? ( + + ) : ( + + ); +}; + +const StashAlert: React.FC<{ close: (confirm: boolean) => void }> = ({ + close, +}) => { + const intl = useIntl(); + + return ( + close(true), + }} + cancel={{ onClick: () => close(false) }} + > +

+ +

+
+ ); +}; + +const DatabaseSection: React.FC<{ + databaseFile: string; + setDatabaseFile: React.Dispatch>; +}> = ({ databaseFile, setDatabaseFile }) => { + const intl = useIntl(); + + return ( + +

+ +

+

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

+ setDatabaseFile(e.currentTarget.value)} + /> +
+ ); +}; + +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); + } + setShowSelectDialog(false); + } + + 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)} + /> +
+ +
+ +
+
+ ); +}; + +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; - const overrideDatabase = configuration?.general.databasePath; - useEffect(() => { - if (configuration) { - const configStashes = configuration.general.stashes; - if (configStashes.length > 0) { - setStashes( - configStashes.map((s) => { - const { __typename, ...withoutTypename } = s; - return withoutTypename; - }) - ); - } + function preNext() { + if (stashes.length === 0) { + setShowStashAlert(true); + } else { + onNext(); } - }, [configuration]); - - const discordLink = ( - Discord - ); - const githubLink = ( - - - - ); - - function onConfigLocationChosen(inWorkDir: boolean) { - setSetupInWorkDir(inWorkDir); - next(); } - function goBack(n?: number) { - let dec = n; - if (!dec) { - dec = 1; - } - setStep(Math.max(0, step - dec)); + function onNext() { + const input: Partial = { + stashes, + databaseFile, + generatedLocation, + cacheLocation, + blobsLocation: storeBlobsInDatabase ? "" : blobsLocation, + storeBlobsInDatabase, + }; + next(input); } - function next() { - setStep(step + 1); - } - - function confirmPaths() { - if (stashes.length > 0) { - next(); - return; - } - - setShowStashAlert(true); - } - - function maybeRenderStashAlert() { - if (!showStashAlert) { - return; - } - - return ( - { + return ( + <> + {showStashAlert ? ( + { setShowStashAlert(false); - next(); - }, - }} - cancel={{ onClick: () => setShowStashAlert(false) }} - > + if (confirm) { + onNext(); + } + }} + /> + ) : null} +
+

+ +

- +

- - ); +
+
+ +

+ +

+

+ +

+ + setStashes(s)} + /> + +
+ {overrideDatabase ? null : ( + + )} + {overrideGenerated ? null : ( + + )} + {overrideCache ? null : ( + + )} + {overrideBlobs ? null : ( + + )} +
+
+
+ + +
+
+ + ); +}; + +const StashExclusions: React.FC<{ stash: GQL.StashConfig }> = ({ stash }) => { + if (!stash.excludeImage && !stash.excludeVideo) { + return null; } - const WelcomeSpecificConfig = () => { - return ( - <> -
-

- -

-

- -

-

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

-

- -

-
+ const excludes = []; + if (stash.excludeVideo) { + excludes.push("videos"); + } + if (stash.excludeImage) { + excludes.push("images"); + } -
-
- -
-
- - ); - }; + return {`(excludes ${excludes.join(" and ")})`}; +}; - function DefaultWelcomeStep() { - const homeDirPath = pathJoin(status?.homeDir ?? homeDir, ".stash"); +const ConfirmStep: React.FC = ({ goBack, next }) => { + const { configuration, pathDir, pathJoin, pwd, setupState } = + useSetupContext(); - return ( - <> -
-

- -

-

- -

-

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

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

- -

-
+ 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; -
-

- -

+ const overrideDatabase = configuration?.general.databasePath; + const overrideGenerated = configuration?.general.generatedPath; + const overrideCache = configuration?.general.cachePath; + const overrideBlobs = configuration?.general.blobsPath; -
- + +
+
+ + ); +}; + +const DiscordLink = ( + Discord +); +const GithubLink = ( + + + +); + +const ErrorStep: React.FC<{ error: string; goBack: () => void }> = ({ + error, + goBack, +}) => { + return ( + <> +
+

+ +

+

+ {error} }} + /> +

+

+ +

+
+
+
+ +
+
+ + ); +}; + +const SuccessStep: React.FC<{}> = () => { + const intl = useIntl(); + const history = useHistory(); + + 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}, - path: fallbackStashDir, }} /> -
- {homeDirPath} - - - -
- - ); - } - - function onGeneratedSelectClosed(d?: string) { - if (d) { - setGeneratedLocation(d); - } - - setShowGeneratedSelectDialog(false); - } - - function maybeRenderGeneratedSelectDialog() { - if (!showGeneratedSelectDialog) { - return; - } - - return ; - } - - function onBlobsClosed(d?: string) { - if (d) { - setBlobsLocation(d); - } - - setShowBlobsDialog(false); - } - - function maybeRenderBlobsSelectDialog() { - if (!showBlobsDialog) { - return; - } - - return ; - } - - function maybeRenderDatabase() { - if (overrideDatabase) return; - - return ( - -

- -

-

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

- setDatabaseFile(e.currentTarget.value)} - /> -
- ); - } - - function maybeRenderGenerated() { - if (overrideGenerated) return; - - return ( - -

- -

-

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

- - setGeneratedLocation(e.currentTarget.value)} - /> - - - - -
- ); - } - - function onCacheSelectClosed(d?: string) { - if (d) { - setCacheLocation(d); - } - - setShowCacheSelectDialog(false); - } - - function maybeRenderCacheSelectDialog() { - if (!showCacheSelectDialog) { - return; - } - - return ; - } - - function maybeRenderCache() { - if (overrideCache) return; - - return ( - -

- -

-

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

- - setCacheLocation(e.currentTarget.value)} - /> - - - - -
- ); - } - - function maybeRenderBlobs() { - if (overrideBlobs) return; - - return ( - -

- -

-

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

-

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

- -

- setStoreBlobsInDatabase(!storeBlobsInDatabase)} - /> -

- - {!storeBlobsInDatabase && ( - - setBlobsLocation(e.currentTarget.value)} - disabled={storeBlobsInDatabase} - /> - - - - - )} -
- ); - } - - function SetPathsStep() { - return ( - <> - {maybeRenderStashAlert()} -
-

- -

-

- -

-
-
- -

- -

+

- -

- - setStashes(s)} + setDownloadFFmpeg(!downloadFFmpeg)} /> - -
- {maybeRenderDatabase()} - {maybeRenderGenerated()} - {maybeRenderCache()} - {maybeRenderBlobs()} -
-
-
- - -
-
- - ); +

+ + ) : null} + +
+

+ +

+

+ }} + /> +

+

+ +

+
+
+

+ +

+

+ + Open Collective + + ), + }} + /> +

+

+ +

+
+
+

+ +

+
+
+
+ +
+
+ + ); +}; + +const FinishStep: React.FC = ({ goBack }) => { + const { setupError } = useSetupContext(); + + 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(); + setCreating(false); + setStep(step + 1); } } - function ConfirmStep() { - let cfgDir: string; - let config: string; - if (overrideConfig) { - cfgDir = pathDir(overrideConfig); - config = overrideConfig; + function next(input?: Partial) { + setSetupInput({ ...setupInput, ...input }); + + if (Step === ConfirmStep) { + // create the system + createSystem(); } else { - cfgDir = setupInWorkDir ? pwd : fallbackStashDir; - config = pathJoin(cfgDir, "config.yml"); + 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); + function goBack() { + if (Step === FinishStep) { + // go back to the step before ConfirmStep + setStep(step - 2); + } else { + setStep(step - 1); } - - return ( - <> -
-

- -

-

- {setupError} }} - /> -

-

- -

-
-
-
- -
-
- - ); } - function onFinishClick() { - if ((!status?.ffmpegPath || !status?.ffprobePath) && downloadFFmpeg) { - mutateDownloadFFMpeg(); - } - - 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 ; - } - - 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]; - - function renderCreating() { + if (statusError) { return ( - - - + + + + + + ); + } + + 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 36db2798a..0eceeb8e5 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 edad1c8e7..784579c95 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",