diff --git a/blueprint.sh b/blueprint.sh index 13989e1..93bc3ee 100644 --- a/blueprint.sh +++ b/blueprint.sh @@ -655,6 +655,21 @@ if [[ ( $2 == "-i" ) || ( $2 == "-install" ) ]]; then VCMD="y" PLACE_REACT "$Components_Server_Schedules_Edit_BeforeEdit" "Server/Schedules/Edit/BeforeEdit.tsx" "$OldComponents_Server_Schedules_Edit_BeforeEdit" PLACE_REACT "$Components_Server_Schedules_Edit_AfterEdit" "Server/Schedules/Edit/AfterEdit.tsx" "$OldComponents_Server_Schedules_Edit_AfterEdit" + PLACE_REACT "$Components_Server_Users_BeforeContent" "Server/Users/BeforeContent.tsx" "$OldComponents_Server_Users_BeforeContent" + PLACE_REACT "$Components_Server_Users_AfterContent" "Server/Users/AfterContent.tsx" "$OldComponents_Server_Users_AfterContent" + + PLACE_REACT "$Components_Server_Backups_BeforeContent" "Server/Backups/BeforeContent.tsx" "$OldComponents_Server_Backups_BeforeContent" + PLACE_REACT "$Components_Server_Backups_AfterContent" "Server/Backups/AfterContent.tsx" "$OldComponents_Server_Backups_AfterContent" + + PLACE_REACT "$Components_Server_Network_BeforeContent" "Server/Network/BeforeContent.tsx" "$OldComponents_Server_Network_BeforeContent" + PLACE_REACT "$Components_Server_Network_AfterContent" "Server/Network/AfterContent.tsx" "$OldComponents_Server_Network_AfterContent" + + PLACE_REACT "$Components_Server_Startup_BeforeContent" "Server/Startup/BeforeContent.tsx" "$OldComponents_Server_Startup_BeforeContent" + PLACE_REACT "$Components_Server_Startup_AfterContent" "Server/Startup/AfterContent.tsx" "$OldComponents_Server_Startup_AfterContent" + + PLACE_REACT "$Components_Server_Settings_BeforeContent" "Server/Settings/BeforeContent.tsx" "$OldComponents_Server_Settings_BeforeContent" + PLACE_REACT "$Components_Server_Settings_AfterContent" "Server/Settings/AfterContent.tsx" "$OldComponents_Server_Settings_AfterContent" + else # warn about missing components.yml file log_yellow "[WARNING] Could not find '$dashboard_components/Components.yml', component extendability might be limited." @@ -1085,6 +1100,21 @@ if [[ ( $2 == "-r" ) || ( $2 == "-remove" ) ]]; then VCMD="y" REMOVE_REACT "$Components_Server_Schedules_Edit_BeforeEdit" "Server/Schedules/Edit/BeforeEdit.tsx" REMOVE_REACT "$Components_Server_Schedules_Edit_AfterEdit" "Server/Schedules/Edit/AfterEdit.tsx" + REMOVE_REACT "$Components_Server_Users_BeforeContent" "Server/Users/BeforeContent.tsx" + REMOVE_REACT "$Components_Server_Users_AfterContent" "Server/Users/AfterContent.tsx" + + REMOVE_REACT "$Components_Server_Backups_BeforeContent" "Server/Backups/BeforeContent.tsx" + REMOVE_REACT "$Components_Server_Backups_AfterContent" "Server/Backups/AfterContent.tsx" + + REMOVE_REACT "$Components_Server_Network_BeforeContent" "Server/Network/BeforeContent.tsx" + REMOVE_REACT "$Components_Server_Network_AfterContent" "Server/Network/AfterContent.tsx" + + REMOVE_REACT "$Components_Server_Startup_BeforeContent" "Server/Startup/BeforeContent.tsx" + REMOVE_REACT "$Components_Server_Startup_AfterContent" "Server/Startup/AfterContent.tsx" + + REMOVE_REACT "$Components_Server_Settings_BeforeContent" "Server/Settings/BeforeContent.tsx" + REMOVE_REACT "$Components_Server_Settings_AfterContent" "Server/Settings/AfterContent.tsx" + YARN="y" fi diff --git a/resources/scripts/blueprint/components/Server/Backups/AfterContent.tsx b/resources/scripts/blueprint/components/Server/Backups/AfterContent.tsx new file mode 100644 index 0000000..5c85b6c --- /dev/null +++ b/resources/scripts/blueprint/components/Server/Backups/AfterContent.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +/* blueprint/import */ + +export default () => { + return ( + <> + {/* blueprint/react */} + + ); +}; diff --git a/resources/scripts/blueprint/components/Server/Backups/BeforeContent.tsx b/resources/scripts/blueprint/components/Server/Backups/BeforeContent.tsx new file mode 100644 index 0000000..5c85b6c --- /dev/null +++ b/resources/scripts/blueprint/components/Server/Backups/BeforeContent.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +/* blueprint/import */ + +export default () => { + return ( + <> + {/* blueprint/react */} + + ); +}; diff --git a/resources/scripts/blueprint/components/Server/Network/AfterContent.tsx b/resources/scripts/blueprint/components/Server/Network/AfterContent.tsx new file mode 100644 index 0000000..5c85b6c --- /dev/null +++ b/resources/scripts/blueprint/components/Server/Network/AfterContent.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +/* blueprint/import */ + +export default () => { + return ( + <> + {/* blueprint/react */} + + ); +}; diff --git a/resources/scripts/blueprint/components/Server/Network/BeforeContent.tsx b/resources/scripts/blueprint/components/Server/Network/BeforeContent.tsx new file mode 100644 index 0000000..5c85b6c --- /dev/null +++ b/resources/scripts/blueprint/components/Server/Network/BeforeContent.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +/* blueprint/import */ + +export default () => { + return ( + <> + {/* blueprint/react */} + + ); +}; diff --git a/resources/scripts/blueprint/components/Server/Settings/AfterContent.tsx b/resources/scripts/blueprint/components/Server/Settings/AfterContent.tsx new file mode 100644 index 0000000..5c85b6c --- /dev/null +++ b/resources/scripts/blueprint/components/Server/Settings/AfterContent.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +/* blueprint/import */ + +export default () => { + return ( + <> + {/* blueprint/react */} + + ); +}; diff --git a/resources/scripts/blueprint/components/Server/Settings/BeforeContent.tsx b/resources/scripts/blueprint/components/Server/Settings/BeforeContent.tsx new file mode 100644 index 0000000..5c85b6c --- /dev/null +++ b/resources/scripts/blueprint/components/Server/Settings/BeforeContent.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +/* blueprint/import */ + +export default () => { + return ( + <> + {/* blueprint/react */} + + ); +}; diff --git a/resources/scripts/blueprint/components/Server/Startup/AfterContent.tsx b/resources/scripts/blueprint/components/Server/Startup/AfterContent.tsx new file mode 100644 index 0000000..5c85b6c --- /dev/null +++ b/resources/scripts/blueprint/components/Server/Startup/AfterContent.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +/* blueprint/import */ + +export default () => { + return ( + <> + {/* blueprint/react */} + + ); +}; diff --git a/resources/scripts/blueprint/components/Server/Startup/BeforeContent.tsx b/resources/scripts/blueprint/components/Server/Startup/BeforeContent.tsx new file mode 100644 index 0000000..5c85b6c --- /dev/null +++ b/resources/scripts/blueprint/components/Server/Startup/BeforeContent.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +/* blueprint/import */ + +export default () => { + return ( + <> + {/* blueprint/react */} + + ); +}; diff --git a/resources/scripts/blueprint/components/Server/Users/AfterContent.tsx b/resources/scripts/blueprint/components/Server/Users/AfterContent.tsx new file mode 100644 index 0000000..5c85b6c --- /dev/null +++ b/resources/scripts/blueprint/components/Server/Users/AfterContent.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +/* blueprint/import */ + +export default () => { + return ( + <> + {/* blueprint/react */} + + ); +}; diff --git a/resources/scripts/blueprint/components/Server/Users/BeforeContent.tsx b/resources/scripts/blueprint/components/Server/Users/BeforeContent.tsx new file mode 100644 index 0000000..5c85b6c --- /dev/null +++ b/resources/scripts/blueprint/components/Server/Users/BeforeContent.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +/* blueprint/import */ + +export default () => { + return ( + <> + {/* blueprint/react */} + + ); +}; diff --git a/resources/scripts/components/server/backups/BackupContainer.tsx b/resources/scripts/components/server/backups/BackupContainer.tsx new file mode 100644 index 0000000..b5a1fc7 --- /dev/null +++ b/resources/scripts/components/server/backups/BackupContainer.tsx @@ -0,0 +1,90 @@ +import React, { useContext, useEffect, useState } from 'react'; +import Spinner from '@/components/elements/Spinner'; +import useFlash from '@/plugins/useFlash'; +import Can from '@/components/elements/Can'; +import CreateBackupButton from '@/components/server/backups/CreateBackupButton'; +import FlashMessageRender from '@/components/FlashMessageRender'; +import BackupRow from '@/components/server/backups/BackupRow'; +import tw from 'twin.macro'; +import getServerBackups, { Context as ServerBackupContext } from '@/api/swr/getServerBackups'; +import { ServerContext } from '@/state/server'; +import ServerContentBlock from '@/components/elements/ServerContentBlock'; +import Pagination from '@/components/elements/Pagination'; + +import BeforeContent from '@/blueprint/components/Server/Backups/BeforeContent'; +import AfterContent from '@/blueprint/components/Server/Backups/AfterContent'; + +const BackupContainer = () => { + const { page, setPage } = useContext(ServerBackupContext); + const { clearFlashes, clearAndAddHttpError } = useFlash(); + const { data: backups, error, isValidating } = getServerBackups(); + + const backupLimit = ServerContext.useStoreState((state) => state.server.data!.featureLimits.backups); + + useEffect(() => { + if (!error) { + clearFlashes('backups'); + + return; + } + + clearAndAddHttpError({ error, key: 'backups' }); + }, [error]); + + if (!backups || (error && isValidating)) { + return ; + } + + return ( + + + + + {({ items }) => + !items.length ? ( + // Don't show any error messages if the server has no backups and the user cannot + // create additional ones for the server. + !backupLimit ? null : ( +

+ {page > 1 + ? "Looks like we've run out of backups to show you, try going back a page." + : 'It looks like there are no backups currently stored for this server.'} +

+ ) + ) : ( + items.map((backup, index) => ( + 0 ? tw`mt-2` : undefined} /> + )) + ) + } +
+ {backupLimit === 0 && ( +

+ Backups cannot be created for this server because the backup limit is set to 0. +

+ )} + +
+ {backupLimit > 0 && backups.backupCount > 0 && ( +

+ {backups.backupCount} of {backupLimit} backups have been created for this server. +

+ )} + {backupLimit > 0 && backupLimit > backups.backupCount && ( + + )} +
+
+ +
+ ); +}; + +export default () => { + const [page, setPage] = useState(1); + return ( + + + + ); +}; diff --git a/resources/scripts/components/server/network/NetworkContainer.tsx b/resources/scripts/components/server/network/NetworkContainer.tsx new file mode 100644 index 0000000..dadfb7c --- /dev/null +++ b/resources/scripts/components/server/network/NetworkContainer.tsx @@ -0,0 +1,89 @@ +import React, { useEffect, useState } from 'react'; +import Spinner from '@/components/elements/Spinner'; +import { useFlashKey } from '@/plugins/useFlash'; +import ServerContentBlock from '@/components/elements/ServerContentBlock'; +import { ServerContext } from '@/state/server'; +import AllocationRow from '@/components/server/network/AllocationRow'; +import Button from '@/components/elements/Button'; +import createServerAllocation from '@/api/server/network/createServerAllocation'; +import tw from 'twin.macro'; +import Can from '@/components/elements/Can'; +import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; +import getServerAllocations from '@/api/swr/getServerAllocations'; +import isEqual from 'react-fast-compare'; +import { useDeepCompareEffect } from '@/plugins/useDeepCompareEffect'; + +import BeforeContent from '@/blueprint/components/Server/Network/BeforeContent'; +import AfterContent from '@/blueprint/components/Server/Network/AfterContent'; + +const NetworkContainer = () => { + const [loading, setLoading] = useState(false); + const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid); + const allocationLimit = ServerContext.useStoreState((state) => state.server.data!.featureLimits.allocations); + const allocations = ServerContext.useStoreState((state) => state.server.data!.allocations, isEqual); + const setServerFromState = ServerContext.useStoreActions((actions) => actions.server.setServerFromState); + + const { clearFlashes, clearAndAddHttpError } = useFlashKey('server:network'); + const { data, error, mutate } = getServerAllocations(); + + useEffect(() => { + mutate(allocations); + }, []); + + useEffect(() => { + clearAndAddHttpError(error); + }, [error]); + + useDeepCompareEffect(() => { + if (!data) return; + + setServerFromState((state) => ({ ...state, allocations: data })); + }, [data]); + + const onCreateAllocation = () => { + clearFlashes(); + + setLoading(true); + createServerAllocation(uuid) + .then((allocation) => { + setServerFromState((s) => ({ ...s, allocations: s.allocations.concat(allocation) })); + return mutate(data?.concat(allocation), false); + }) + .catch((error) => clearAndAddHttpError(error)) + .then(() => setLoading(false)); + }; + + return ( + + {!data ? ( + + ) : ( + <> + + {data.map((allocation) => ( + + ))} + {allocationLimit > 0 && ( + + +
+

+ You are currently using {data.length} of {allocationLimit} allowed allocations for + this server. +

+ {allocationLimit > data.length && ( + + )} +
+
+ )} + + + )} +
+ ); +}; + +export default NetworkContainer; diff --git a/resources/scripts/components/server/settings/SettingsContainer.tsx b/resources/scripts/components/server/settings/SettingsContainer.tsx new file mode 100644 index 0000000..37e3d66 --- /dev/null +++ b/resources/scripts/components/server/settings/SettingsContainer.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import TitledGreyBox from '@/components/elements/TitledGreyBox'; +import { ServerContext } from '@/state/server'; +import { useStoreState } from 'easy-peasy'; +import RenameServerBox from '@/components/server/settings/RenameServerBox'; +import FlashMessageRender from '@/components/FlashMessageRender'; +import Can from '@/components/elements/Can'; +import ReinstallServerBox from '@/components/server/settings/ReinstallServerBox'; +import tw from 'twin.macro'; +import Input from '@/components/elements/Input'; +import Label from '@/components/elements/Label'; +import ServerContentBlock from '@/components/elements/ServerContentBlock'; +import isEqual from 'react-fast-compare'; +import CopyOnClick from '@/components/elements/CopyOnClick'; +import { ip } from '@/lib/formatters'; +import { Button } from '@/components/elements/button/index'; + +import BeforeContent from '@/blueprint/components/Server/Backups/BeforeContent'; +import AfterContent from '@/blueprint/components/Server/Backups/AfterContent'; + +export default () => { + const username = useStoreState((state) => state.user.data!.username); + const id = ServerContext.useStoreState((state) => state.server.data!.id); + const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid); + const node = ServerContext.useStoreState((state) => state.server.data!.node); + const sftp = ServerContext.useStoreState((state) => state.server.data!.sftpDetails, isEqual); + + return ( + + + +
+
+ + +
+ + + + +
+
+ + + + +
+
+
+
+

+ Your SFTP password is the same as the password you use to access this panel. +

+
+
+ +
+
+
+ +
+

Node

+ {node} +
+ +
+

Server ID

+ {uuid} +
+
+
+
+
+ +
+ +
+
+ + + +
+
+ +
+ ); +}; diff --git a/resources/scripts/components/server/startup/StartupContainer.tsx b/resources/scripts/components/server/startup/StartupContainer.tsx new file mode 100644 index 0000000..8dbdc42 --- /dev/null +++ b/resources/scripts/components/server/startup/StartupContainer.tsx @@ -0,0 +1,142 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import TitledGreyBox from '@/components/elements/TitledGreyBox'; +import tw from 'twin.macro'; +import VariableBox from '@/components/server/startup/VariableBox'; +import ServerContentBlock from '@/components/elements/ServerContentBlock'; +import getServerStartup from '@/api/swr/getServerStartup'; +import Spinner from '@/components/elements/Spinner'; +import { ServerError } from '@/components/elements/ScreenBlock'; +import { httpErrorToHuman } from '@/api/http'; +import { ServerContext } from '@/state/server'; +import { useDeepCompareEffect } from '@/plugins/useDeepCompareEffect'; +import Select from '@/components/elements/Select'; +import isEqual from 'react-fast-compare'; +import Input from '@/components/elements/Input'; +import setSelectedDockerImage from '@/api/server/setSelectedDockerImage'; +import InputSpinner from '@/components/elements/InputSpinner'; +import useFlash from '@/plugins/useFlash'; + +import BeforeContent from '@/blueprint/components/Server/Startup/BeforeContent'; +import AfterContent from '@/blueprint/components/Server/Startup/AfterContent'; + +const StartupContainer = () => { + const [loading, setLoading] = useState(false); + const { clearFlashes, clearAndAddHttpError } = useFlash(); + + const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid); + const variables = ServerContext.useStoreState( + ({ server }) => ({ + variables: server.data!.variables, + invocation: server.data!.invocation, + dockerImage: server.data!.dockerImage, + }), + isEqual + ); + + const { data, error, isValidating, mutate } = getServerStartup(uuid, { + ...variables, + dockerImages: { [variables.dockerImage]: variables.dockerImage }, + }); + + const setServerFromState = ServerContext.useStoreActions((actions) => actions.server.setServerFromState); + const isCustomImage = + data && + !Object.values(data.dockerImages) + .map((v) => v.toLowerCase()) + .includes(variables.dockerImage.toLowerCase()); + + useEffect(() => { + // Since we're passing in initial data this will not trigger on mount automatically. We + // want to always fetch fresh information from the API however when we're loading the startup + // information. + mutate(); + }, []); + + useDeepCompareEffect(() => { + if (!data) return; + + setServerFromState((s) => ({ + ...s, + invocation: data.invocation, + variables: data.variables, + })); + }, [data]); + + const updateSelectedDockerImage = useCallback( + (v: React.ChangeEvent) => { + setLoading(true); + clearFlashes('startup:image'); + + const image = v.currentTarget.value; + setSelectedDockerImage(uuid, image) + .then(() => setServerFromState((s) => ({ ...s, dockerImage: image }))) + .catch((error) => { + console.error(error); + clearAndAddHttpError({ key: 'startup:image', error }); + }) + .then(() => setLoading(false)); + }, + [uuid] + ); + + return !data ? ( + !error || (error && isValidating) ? ( + + ) : ( + mutate()} /> + ) + ) : ( + + +
+ +
+

{data.invocation}

+
+
+ + {Object.keys(data.dockerImages).length > 1 && !isCustomImage ? ( + <> + + + +

+ This is an advanced feature allowing you to select a Docker image to use when running + this server instance. +

+ + ) : ( + <> + + {isCustomImage && ( +

+ This {"server's"} Docker image has been manually set by an administrator and cannot + be changed through this UI. +

+ )} + + )} +
+
+

Variables

+
+ {data.variables.map((variable) => ( + + ))} +
+ +
+ ); +}; + +export default StartupContainer; diff --git a/resources/scripts/components/server/users/UsersContainer.tsx b/resources/scripts/components/server/users/UsersContainer.tsx new file mode 100644 index 0000000..f1349cf --- /dev/null +++ b/resources/scripts/components/server/users/UsersContainer.tsx @@ -0,0 +1,70 @@ +import React, { useEffect, useState } from 'react'; +import { ServerContext } from '@/state/server'; +import { Actions, useStoreActions, useStoreState } from 'easy-peasy'; +import { ApplicationStore } from '@/state'; +import Spinner from '@/components/elements/Spinner'; +import AddSubuserButton from '@/components/server/users/AddSubuserButton'; +import UserRow from '@/components/server/users/UserRow'; +import FlashMessageRender from '@/components/FlashMessageRender'; +import getServerSubusers from '@/api/server/users/getServerSubusers'; +import { httpErrorToHuman } from '@/api/http'; +import Can from '@/components/elements/Can'; +import ServerContentBlock from '@/components/elements/ServerContentBlock'; +import tw from 'twin.macro'; + +import BeforeContent from '@/blueprint/components/Server/Users/BeforeContent'; +import AfterContent from '@/blueprint/components/Server/Users/AfterContent'; + +export default () => { + const [loading, setLoading] = useState(true); + + const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid); + const subusers = ServerContext.useStoreState((state) => state.subusers.data); + const setSubusers = ServerContext.useStoreActions((actions) => actions.subusers.setSubusers); + + const permissions = useStoreState((state: ApplicationStore) => state.permissions.data); + const getPermissions = useStoreActions((actions: Actions) => actions.permissions.getPermissions); + const { addError, clearFlashes } = useStoreActions((actions: Actions) => actions.flashes); + + useEffect(() => { + clearFlashes('users'); + getServerSubusers(uuid) + .then((subusers) => { + setSubusers(subusers); + setLoading(false); + }) + .catch((error) => { + console.error(error); + addError({ key: 'users', message: httpErrorToHuman(error) }); + }); + }, []); + + useEffect(() => { + getPermissions().catch((error) => { + addError({ key: 'users', message: httpErrorToHuman(error) }); + console.error(error); + }); + }, []); + + if (!subusers.length && (loading || !Object.keys(permissions).length)) { + return ; + } + + return ( + + + + {!subusers.length ? ( +

It looks like you don't have any subusers.

+ ) : ( + subusers.map((subuser) => ) + )} + +
+ +
+
+ +
+ ); +};