diff --git a/.prettierrc.json b/.prettierrc.json index ed5e4fd1e..7b778ac8b 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,23 +1,26 @@ { - "printWidth": 120, - "tabWidth": 4, - "useTabs": false, - "semi": true, - "singleQuote": true, - "jsxSingleQuote": true, - "importOrder": [ - "^@/routers/(.*)$", - "^@/components/(.*)$", - "^@/hoc/(.*)$", - "^@/lib/(.*)$", - "^@/api/(.*)$", - "^@/state(.*)$", - "^@/state/(.*)$", - "^@/plugins/(.*)$", - "^@feature/(.*)$", - "^[./]" - ], - "importOrderSeparation": true, - "importOrderSortSpecifiers": true, - "plugins": ["prettier-plugin-tailwindcss", "@trivago/prettier-plugin-sort-imports"] + "printWidth": 120, + "tabWidth": 4, + "useTabs": false, + "semi": true, + "singleQuote": true, + "jsxSingleQuote": true, + "importOrder": [ + "^@/routers/(.*)$", + "^@/components/(.*)$", + "^@/hoc/(.*)$", + "^@/lib/(.*)$", + "^@/api/(.*)$", + "^@/state(.*)$", + "^@/state/(.*)$", + "^@/plugins/(.*)$", + "^@feature/(.*)$", + "^[./]" + ], + "importOrderSeparation": true, + "importOrderSortSpecifiers": true, + "plugins": [ + "prettier-plugin-tailwindcss", + "@trivago/prettier-plugin-sort-imports" + ] } diff --git a/resources/scripts/components/dashboard/AccountApiContainer.tsx b/resources/scripts/components/dashboard/AccountApiContainer.tsx index 927dc9589..c3b0d68fb 100644 --- a/resources/scripts/components/dashboard/AccountApiContainer.tsx +++ b/resources/scripts/components/dashboard/AccountApiContainer.tsx @@ -1,272 +1,261 @@ -import { Eye, EyeSlash, Key, Plus, TrashBin } from '@gravity-ui/icons'; -import { format } from 'date-fns'; -import { Actions, useStoreActions } from 'easy-peasy'; -import { Field, Form, Formik, FormikHelpers } from 'formik'; -import { useEffect, useState } from 'react'; -import { object, string } from 'yup'; +import { Eye, EyeSlash, Key, Plus, TrashBin } from "@gravity-ui/icons"; +import { format } from "date-fns"; +import { Actions, useStoreActions } from "easy-peasy"; +import { Field, Form, Formik, FormikHelpers } from "formik"; +import { lazy, useEffect, useState } from "react"; +import { object, string } from "yup"; -import FlashMessageRender from '@/components/FlashMessageRender'; -import ApiKeyModal from '@/components/dashboard/ApiKeyModal'; -import ActionButton from '@/components/elements/ActionButton'; -import Code from '@/components/elements/Code'; -import FormikFieldWrapper from '@/components/elements/FormikFieldWrapper'; -import Input from '@/components/elements/Input'; -import { MainPageHeader } from '@/components/elements/MainPageHeader'; -import PageContentBlock from '@/components/elements/PageContentBlock'; -import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; -import { Dialog } from '@/components/elements/dialog'; +import FlashMessageRender from "@/components/FlashMessageRender"; +import ApiKeyModal from "@/components/dashboard/ApiKeyModal"; +import ActionButton from "@/components/elements/ActionButton"; +import Code from "@/components/elements/Code"; +import FormikFieldWrapper from "@/components/elements/FormikFieldWrapper"; +import Input from "@/components/elements/Input"; +import { MainPageHeader } from "@/components/elements/MainPageHeader"; +import PageContentBlock from "@/components/elements/PageContentBlock"; +import SpinnerOverlay from "@/components/elements/SpinnerOverlay"; +import { Dialog } from "@/components/elements/dialog"; -import createApiKey from '@/api/account/createApiKey'; -import deleteApiKey from '@/api/account/deleteApiKey'; -import getApiKeys, { ApiKey } from '@/api/account/getApiKeys'; -import { httpErrorToHuman } from '@/api/http'; +import createApiKey from "@/api/account/createApiKey"; +import deleteApiKey from "@/api/account/deleteApiKey"; +import getApiKeys, { ApiKey } from "@/api/account/getApiKeys"; +import { httpErrorToHuman } from "@/api/http"; -import { ApplicationStore } from '@/state'; +import { ApplicationStore } from "@/state"; -import { useFlashKey } from '@/plugins/useFlash'; +import { useFlashKey } from "@/plugins/useFlash"; + +const CreateApiKeyModal = lazy(() => import("./CreateApiKeyModal")); interface CreateValues { - description: string; - allowedIps: string; + description: string; + allowedIps: string; } const AccountApiContainer = () => { - const [deleteIdentifier, setDeleteIdentifier] = useState(''); - const [keys, setKeys] = useState([]); - const [loading, setLoading] = useState(true); - const [showCreateModal, setShowCreateModal] = useState(false); - const [apiKey, setApiKey] = useState(''); - const [showKeys, setShowKeys] = useState>({}); + const [deleteIdentifier, setDeleteIdentifier] = useState(""); + const [keys, setKeys] = useState([]); + const [loading, setLoading] = useState(true); + const [showCreateModal, setShowCreateModal] = useState(false); + const [apiKey, setApiKey] = useState(""); + const [showKeys, setShowKeys] = useState>({}); - const { clearAndAddHttpError } = useFlashKey('api-keys'); - const { addError, clearFlashes } = useStoreActions((actions: Actions) => actions.flashes); + const { clearAndAddHttpError } = useFlashKey("api-keys"); + const { addError, clearFlashes } = useStoreActions( + (actions: Actions) => actions.flashes, + ); - useEffect(() => { - getApiKeys() - .then((keys) => setKeys(keys)) - .then(() => setLoading(false)) - .catch((error) => clearAndAddHttpError(error)); - }, []); + useEffect(() => { + getApiKeys() + .then((keys) => setKeys(keys)) + .then(() => setLoading(false)) + .catch((error) => clearAndAddHttpError(error)); + }, []); - const doDeletion = (identifier: string) => { - setLoading(true); - clearAndAddHttpError(); - deleteApiKey(identifier) - .then(() => setKeys((s) => [...(s || []).filter((key) => key.identifier !== identifier)])) - .catch((error) => clearAndAddHttpError(error)) - .then(() => { - setLoading(false); - setDeleteIdentifier(''); - }); - }; + const doDeletion = (identifier: string) => { + setLoading(true); + clearAndAddHttpError(); + deleteApiKey(identifier) + .then(() => + setKeys((s) => [ + ...(s || []).filter((key) => key.identifier !== identifier), + ]), + ) + .catch((error) => clearAndAddHttpError(error)) + .then(() => { + setLoading(false); + setDeleteIdentifier(""); + }); + }; - const submitCreate = (values: CreateValues, { setSubmitting, resetForm }: FormikHelpers) => { - clearFlashes('account:api-keys'); - createApiKey(values.description, values.allowedIps) - .then(({ secretToken, ...key }) => { - resetForm(); - setSubmitting(false); - setApiKey(`${key.identifier}${secretToken}`); - setKeys((s) => [...s!, key]); - setShowCreateModal(false); - }) - .catch((error) => { - console.error(error); - addError({ key: 'account:api-keys', message: httpErrorToHuman(error) }); - setSubmitting(false); - }); - }; + const submitCreate = ( + values: CreateValues, + { setSubmitting, resetForm }: FormikHelpers, + ) => { + clearFlashes("account:api-keys"); + createApiKey(values.description, values.allowedIps) + .then(({ secretToken, ...key }) => { + resetForm(); + setSubmitting(false); + setApiKey(`${key.identifier}${secretToken}`); + setKeys((s) => [...s!, key]); + setShowCreateModal(false); + }) + .catch((error) => { + console.error(error); + addError({ key: "account:api-keys", message: httpErrorToHuman(error) }); + setSubmitting(false); + }); + }; - const toggleKeyVisibility = (identifier: string) => { - setShowKeys((prev) => ({ - ...prev, - [identifier]: !prev[identifier], - })); - }; + const toggleKeyVisibility = (identifier: string) => { + setShowKeys((prev) => ({ + ...prev, + [identifier]: !prev[identifier], + })); + }; - return ( - - - 0} onModalDismissed={() => setApiKey('')} apiKey={apiKey} /> + return ( + + + 0} + onModalDismissed={() => setApiKey("")} + apiKey={apiKey} + /> - {/* Create API Key Modal */} - {showCreateModal && ( - setShowCreateModal(false)} - title='Create API Key' - confirm='Create Key' - onConfirmed={() => { - const form = document.getElementById('create-api-form') as HTMLFormElement; - if (form) { - const submitButton = form.querySelector('button[type="submit"]') as HTMLButtonElement; - if (submitButton) submitButton.click(); - } - }} - > - - {({ isSubmitting }) => ( -
- + setShowCreateModal(false)} + onSubmit={submitCreate} + /> - - - +
+
+ setShowCreateModal(true)} + className="flex items-center gap-2" + > + + Create API Key + + } + /> +
- - - +
+
+ + setDeleteIdentifier("")} + onConfirmed={() => doDeletion(deleteIdentifier)} + > + All requests using the {deleteIdentifier} key will be + invalidated. + -
+
+
+
+ ); }; export default AccountApiContainer; diff --git a/resources/scripts/components/dashboard/AccountOverviewContainer.tsx b/resources/scripts/components/dashboard/AccountOverviewContainer.tsx index 1eab0d2b5..a3e16b0f2 100644 --- a/resources/scripts/components/dashboard/AccountOverviewContainer.tsx +++ b/resources/scripts/components/dashboard/AccountOverviewContainer.tsx @@ -1,92 +1,99 @@ -import { useLocation } from 'react-router-dom'; +import { useLocation } from "react-router-dom"; -import MessageBox from '@/components/MessageBox'; -import ConfigureTwoFactorForm from '@/components/dashboard/forms/ConfigureTwoFactorForm'; -import UpdateEmailAddressForm from '@/components/dashboard/forms/UpdateEmailAddressForm'; -import UpdatePasswordForm from '@/components/dashboard/forms/UpdatePasswordForm'; -import ContentBox from '@/components/elements/ContentBox'; -import PageContentBlock from '@/components/elements/PageContentBlock'; +import MessageBox from "@/components/MessageBox"; +import ConfigureTwoFactorForm from "@/components/dashboard/forms/ConfigureTwoFactorForm"; +import UpdateEmailAddressForm from "@/components/dashboard/forms/UpdateEmailAddressForm"; +import UpdatePasswordForm from "@/components/dashboard/forms/UpdatePasswordForm"; +import ContentBox from "@/components/elements/ContentBox"; +import PageContentBlock from "@/components/elements/PageContentBlock"; -import Code from '../elements/Code'; +import Code from "../elements/Code"; const AccountOverviewContainer = () => { - const { state } = useLocation(); + const { state } = useLocation(); - return ( - -
- {state?.twoFactorRedirect && ( -
- - Your account must have two-factor authentication enabled in order to continue. - -
- )} + return ( + +
+ {state?.twoFactorRedirect && ( +
+ + Your account must have two-factor authentication enabled in order + to continue. + +
+ )} -
-
- - - -
+
+
+ + + +
-
-
- - - - - - -
-
+
+
+ + + + + + +
+
-
- -

- This is useful to provide Pyro staff if you run into an unexpected issue. -

-
- - Version: {import.meta.env.VITE_PYRODACTYL_VERSION} -{' '} - {import.meta.env.VITE_BRANCH_NAME} - - Commit : {import.meta.env.VITE_COMMIT_HASH.slice(0, 7)} -
-
-
-
-
- - ); +
+ +

+ This is useful to provide Pyro staff if you run into an + unexpected issue. +

+
+ + Version: {import.meta.env.VITE_PYRODACTYL_VERSION} -{" "} + {import.meta.env.VITE_BRANCH_NAME} + + + Commit : {import.meta.env.VITE_COMMIT_HASH.slice(0, 7)} + +
+
+
+
+
+
+ ); }; export default AccountOverviewContainer; diff --git a/resources/scripts/components/dashboard/ApiKeyModal.tsx b/resources/scripts/components/dashboard/ApiKeyModal.tsx index 52752b3f4..8d402cdf7 100644 --- a/resources/scripts/components/dashboard/ApiKeyModal.tsx +++ b/resources/scripts/components/dashboard/ApiKeyModal.tsx @@ -1,57 +1,63 @@ -import ModalContext from '@/context/ModalContext'; -import { useContext } from 'react'; +import ModalContext from "@/context/ModalContext"; +import { Activity02Icon } from "@hugeicons/core-free-icons"; +import { useContext } from "react"; -import FlashMessageRender from '@/components/FlashMessageRender'; -import CopyOnClick from '@/components/elements/CopyOnClick'; +import FlashMessageRender from "@/components/FlashMessageRender"; +import CopyOnClick from "@/components/elements/CopyOnClick"; -import asModal from '@/hoc/asModal'; +import asModal from "@/hoc/asModal"; -import ActionButton from '../elements/ActionButton'; +import ActionButton from "../elements/ActionButton"; interface Props { - apiKey: string; + apiKey: string; } const ApiKeyModal = ({ apiKey }: Props) => { - const { dismiss } = useContext(ModalContext); + const { dismiss } = useContext(ModalContext); - return ( -
- {/* Flash message section */} - + return ( +
+ {/* Flash message section */} + - {/* Modal Header */} -

- The API key you have requested is shown below. Please store it in a safe place, as it will not be shown - again. -

+ {/* Modal Header */} +

+ The API key you have requested is shown below. Please store it in a safe + place, as it will not be shown again. +

- {/* API Key Display Section */} -
-
-                    
-                        {apiKey}
-                    
+			{/* API Key Display Section */}
+			
+
+					
+						{apiKey}
+					
 
-                    {/* Copy button with icon */}
-                    
-
-
+ {/* Copy button with icon */} +
+
+
- {/* Action Buttons */} -
- dismiss()} variant='danger' className='flex items-center'> - Close - -
-
- ); + {/* Action Buttons */} +
+ dismiss()} + variant="danger" + className="flex items-center" + > + Close + +
+
+ ); }; -ApiKeyModal.displayName = 'ApiKeyModal'; +ApiKeyModal.displayName = "ApiKeyModal"; export default asModal({ - title: 'Your API Key', - closeOnEscape: true, // Allows closing the modal by pressing Escape - closeOnBackground: true, // Allows closing by clicking outside the modal + title: "Your API Key", + closeOnEscape: true, // Allows closing the modal by pressing Escape + closeOnBackground: true, // Allows closing by clicking outside the modal })(ApiKeyModal); diff --git a/resources/scripts/components/dashboard/CreateApiKeyModal.tsx b/resources/scripts/components/dashboard/CreateApiKeyModal.tsx new file mode 100644 index 000000000..39f94361e --- /dev/null +++ b/resources/scripts/components/dashboard/CreateApiKeyModal.tsx @@ -0,0 +1,104 @@ +import { Form, Formik, FormikHelpers } from "formik"; +import { Field } from "formik"; +import { object, string } from "yup"; + +import FormikFieldWrapper from "@/components/elements/FormikFieldWrapper"; +import Input from "@/components/elements/Input"; +import SpinnerOverlay from "@/components/elements/SpinnerOverlay"; +import { Dialog } from "@/components/elements/dialog"; + +interface CreateValues { + description: string; + allowedIps: string; +} + +interface CreateApiKeyModalProps { + open: boolean; + onClose: () => void; + onSubmit: ( + values: CreateValues, + helpers: FormikHelpers, + ) => void; + isSubmitting?: boolean; +} + +const validationSchema = object().shape({ + description: string() + .required("Description is required") + .min(4, "Must be at least 4 characters"), + allowedIps: string(), +}); + +export default function CreateApiKeyModal({ + open, + onClose, + onSubmit, + isSubmitting = false, +}: CreateApiKeyModalProps) { + return ( + { + // Trigger form submission programmatically + const form = document.getElementById( + "create-api-form", + ) as HTMLFormElement; + if (form) { + const submitButton = form.querySelector( + 'button[type="submit"]', + ) as HTMLButtonElement; + if (submitButton) submitButton.click(); + } + }} + // Optional: disable confirm button while submitting + confirmDisabled={isSubmitting} + > + + {({ isSubmitting: formikIsSubmitting }) => ( +
+ + + + + + + + + + + {/* Hidden submit button — triggered by Dialog confirm */} + - - - setServerViewMode('owner')} - className={serverViewMode === 'owner' ? 'bg-accent/20' : ''} - > - Your Servers Only - + const filterDropdown = useMemo( + () => ( + + + + + + setServerViewMode("owner")} + className={serverViewMode === "owner" ? "bg-accent/20" : ""} + > + Your Servers Only + - {rootAdmin && ( - <> - setServerViewMode('admin-all')} - className={serverViewMode === 'admin-all' ? 'bg-accent/20' : ''} - > - All Servers (Admin) - - - )} - setServerViewMode('all')} - className={serverViewMode === 'all' ? 'bg-accent/20' : ''} - > - All Servers - - - - ), - [rootAdmin, showOnlyAdmin], - ); + {rootAdmin && ( + <> + setServerViewMode("admin-all")} + className={serverViewMode === "admin-all" ? "bg-accent/20" : ""} + > + All Servers (Admin) + + + )} + setServerViewMode("all")} + className={serverViewMode === "all" ? "bg-accent/20" : ""} + > + All Servers + + + + ), + [rootAdmin, showOnlyAdmin], + ); - useEffect(() => { - setHeaderActions([searchSection, viewTabs, filterDropdown]); - return () => clearHeaderActions(); - }, [setHeaderActions, clearHeaderActions, searchSection, viewTabs, filterDropdown]); + useEffect(() => { + setHeaderActions([searchSection, viewTabs, filterDropdown]); + return () => clearHeaderActions(); + }, [ + setHeaderActions, + clearHeaderActions, + searchSection, + viewTabs, + filterDropdown, + ]); - useEffect(() => { - if (!servers) return; - if (servers.pagination.currentPage > 1 && !servers.items.length) { - setPage(1); - } - }, [servers]); + useEffect(() => { + if (!servers) return; + if (servers.pagination.currentPage > 1 && !servers.items.length) { + setPage(1); + } + }, [servers]); - useEffect(() => { - // Don't use react-router to handle changing this part of the URL, otherwise it - // triggers a needless re-render. We just want to track this in the URL incase the - // user refreshes the page. - window.history.replaceState(null, document.title, `/${page <= 1 ? '' : `?page=${page}`}`); - }, [page]); + useEffect(() => { + // Don't use react-router to handle changing this part of the URL, otherwise it + // triggers a needless re-render. We just want to track this in the URL incase the + // user refreshes the page. + window.history.replaceState( + null, + document.title, + `/${page <= 1 ? "" : `?page=${page}`}`, + ); + }, [page]); - useEffect(() => { - if (error) clearAndAddHttpError({ key: 'dashboard', error }); - if (!error) clearFlashes('dashboard'); - }, [error, clearAndAddHttpError, clearFlashes]); + useEffect(() => { + if (error) clearAndAddHttpError({ key: "dashboard", error }); + if (!error) clearFlashes("dashboard"); + }, [error, clearAndAddHttpError, clearFlashes]); - return ( - - {!servers ? ( - <> - ) : ( - - {({ items }) => - items.length > 0 ? ( -
- {items.map((server, index) => ( -
- div~div]:w-full max-lg:flex-row max-lg:items-center max-lg:gap-0 max-lg:[&>div~div]:w-auto' - } - key={server.uuid} - server={server} - /> -
- ))} -
- ) : ( -
-

- {serverViewMode === 'admin-all' - ? 'There are no other servers to display.' - : serverViewMode === 'all' - ? 'No Server Shared With your Account' - : 'There are no servers associated with your account.'} -

-

- {serverViewMode === 'admin-all' ? 'No other servers found' : 'No servers found'} -

-
- ) - } -
- )} -
- ); + return ( + + {!servers ? ( + <> + ) : ( + + {({ items }) => + items.length > 0 ? ( +
+ {items.map((server, index) => ( +
+ div~div]:w-full max-lg:flex-row max-lg:items-center max-lg:gap-0 max-lg:[&>div~div]:w-auto" + } + key={server.uuid} + server={server} + /> +
+ ))} +
+ ) : ( +
+

+ {serverViewMode === "admin-all" + ? "There are no other servers to display." + : serverViewMode === "all" + ? "No Server Shared With your Account" + : "There are no servers associated with your account."} +

+

+ {serverViewMode === "admin-all" + ? "No other servers found" + : "No servers found"} +

+
+ ) + } +
+ )} +
+ ); }; export default DashboardContainer; diff --git a/resources/scripts/components/dashboard/ServerRow.tsx b/resources/scripts/components/dashboard/ServerRow.tsx index c2f437893..eb3932e6b 100644 --- a/resources/scripts/components/dashboard/ServerRow.tsx +++ b/resources/scripts/components/dashboard/ServerRow.tsx @@ -1,15 +1,19 @@ -import { Fragment, useEffect, useRef, useState } from 'react'; -import { Link } from 'react-router-dom'; -import styled from 'styled-components'; +import { Fragment, useEffect, useRef, useState } from "react"; +import { Link } from "react-router-dom"; +import styled from "styled-components"; -import { bytesToString, ip } from '@/lib/formatters'; +import { bytesToString, ip } from "@/lib/formatters"; -import { Server, getGlobalDaemonType } from '@/api/server/getServer'; -import getServerResourceUsage, { ServerPowerState, ServerStats } from '@/api/server/getServerResourceUsage'; +import { Server, getGlobalDaemonType } from "@/api/server/getServer"; +import getServerResourceUsage, { + ServerPowerState, + ServerStats, +} from "@/api/server/getServerResourceUsage"; // Determines if the current value is in an alarm threshold so we can show it in red rather // than the more faded default style. -const isAlarmState = (current: number, limit: number): boolean => limit > 0 && current / (limit * 1024 * 1024) >= 0.9; +const isAlarmState = (current: number, limit: number): boolean => + limit > 0 && current / (limit * 1024 * 1024) >= 0.9; const StatusIndicatorBox = styled.div<{ $status: ServerPowerState }>` background: #ffffff11; @@ -38,159 +42,188 @@ position: relative; transition: all 250ms ease-in-out; box-shadow: ${({ $status }) => { - if (!$status || $status === 'offline') { - return '0 0 12px 1px #C74343'; - } else if ($status === 'running') { - return '0 0 12px 1px #43C760'; - } else if ($status === 'installing') { - return '0 0 12px 1px #4381c7'; - } else { - return '0 0 12px 1px #c7aa43'; - } - }}; + if (!$status || $status === "offline") { + return "0 0 12px 1px #C74343"; + } else if ($status === "running") { + return "0 0 12px 1px #43C760"; + } else if ($status === "installing") { + return "0 0 12px 1px #4381c7"; + } else { + return "0 0 12px 1px #c7aa43"; + } + }}; background: ${({ $status }) => { - if (!$status || $status === 'offline') { - return 'linear-gradient(180deg, #C74343 0%, #C74343 100%)'; - } else if ($status === 'running') { - return 'linear-gradient(180deg, #91FFA9 0%, #43C760 100%)'; - } else if ($status === 'installing') { - return 'linear-gradient(180deg, #91c7ff 0%, #4381c7 100%)'; - } else { - return 'linear-gradient(180deg, #c7aa43 0%, #c7aa43 100%)'; - } - }} + if (!$status || $status === "offline") { + return "linear-gradient(180deg, #C74343 0%, #C74343 100%)"; + } else if ($status === "running") { + return "linear-gradient(180deg, #91FFA9 0%, #43C760 100%)"; + } else if ($status === "installing") { + return "linear-gradient(180deg, #91c7ff 0%, #4381c7 100%)"; + } else { + return "linear-gradient(180deg, #c7aa43 0%, #c7aa43 100%)"; + } + }} `; type Timer = ReturnType; -const ServerRow = ({ server, className }: { server: Server; className?: string }) => { - const interval = useRef(null) as React.MutableRefObject; - const [isSuspended, setIsSuspended] = useState(server.status === 'suspended'); - const [isInstalling, setIsInstalling] = useState(server.status === 'installing'); - const [stats, setStats] = useState(null); +const ServerRow = ({ + server, + className, +}: { + server: Server; + className?: string; +}) => { + const interval = useRef(null) as React.MutableRefObject; + const [isSuspended, setIsSuspended] = useState(server.status === "suspended"); + const [isInstalling, setIsInstalling] = useState( + server.status === "installing", + ); + const [stats, setStats] = useState(null); - const getStats = () => - getServerResourceUsage(server.uuid) - .then((data) => setStats(data)) - .catch((error) => console.error(error)); + const getStats = () => + getServerResourceUsage(server.uuid) + .then((data) => setStats(data)) + .catch((error) => console.error(error)); - useEffect(() => { - setIsSuspended(stats?.isSuspended || server.status === 'suspended'); - }, [stats?.isSuspended, server.status]); + useEffect(() => { + setIsSuspended(stats?.isSuspended || server.status === "suspended"); + }, [stats?.isSuspended, server.status]); - useEffect(() => { - setIsInstalling(stats?.isInstalling || server.status === 'installing'); - }, [stats?.isInstalling, server.status]); + useEffect(() => { + setIsInstalling(stats?.isInstalling || server.status === "installing"); + }, [stats?.isInstalling, server.status]); - useEffect(() => { - // Don't waste a HTTP request if there is nothing important to show to the user because - // the server is suspended. - if (isSuspended) return; + useEffect(() => { + // Don't waste a HTTP request if there is nothing important to show to the user because + // the server is suspended. + if (isSuspended) return; - getStats().then(() => { - interval.current = setInterval(() => getStats(), 30000); - }); + getStats().then(() => { + interval.current = setInterval(() => getStats(), 30000); + }); - return () => { - if (interval.current) clearInterval(interval.current); - }; - }, [isSuspended]); + return () => { + if (interval.current) clearInterval(interval.current); + }; + }, [isSuspended]); - const alarms = { cpu: false, memory: false, disk: false }; - if (stats) { - alarms.cpu = server.limits.cpu === 0 ? false : stats.cpuUsagePercent >= server.limits.cpu * 0.9; - alarms.memory = isAlarmState(stats.memoryUsageInBytes, server.limits.memory); - alarms.disk = server.limits.disk === 0 ? false : isAlarmState(stats.diskUsageInBytes, server.limits.disk); - } + const alarms = { cpu: false, memory: false, disk: false }; + if (stats) { + alarms.cpu = + server.limits.cpu === 0 + ? false + : stats.cpuUsagePercent >= server.limits.cpu * 0.9; + alarms.memory = isAlarmState( + stats.memoryUsageInBytes, + server.limits.memory, + ); + alarms.disk = + server.limits.disk === 0 + ? false + : isAlarmState(stats.diskUsageInBytes, server.limits.disk); + } - // const diskLimit = server.limits.disk !== 0 ? bytesToString(mbToBytes(server.limits.disk)) : 'Unlimited'; - // const memoryLimit = server.limits.memory !== 0 ? bytesToString(mbToBytes(server.limits.memory)) : 'Unlimited'; - // const cpuLimit = server.limits.cpu !== 0 ? server.limits.cpu + ' %' : 'Unlimited'; + // const diskLimit = server.limits.disk !== 0 ? bytesToString(mbToBytes(server.limits.disk)) : 'Unlimited'; + // const memoryLimit = server.limits.memory !== 0 ? bytesToString(mbToBytes(server.limits.memory)) : 'Unlimited'; + // const cpuLimit = server.limits.cpu !== 0 ? server.limits.cpu + ' %' : 'Unlimited'; - return ( - -
-
-
-

- {server.name} -

{' '} -
-
-

- {server.allocations - .filter((alloc) => alloc.isDefault) - .map((allocation) => ( - - {allocation.alias || ip(allocation.ip)}:{allocation.port} - - ))} -

-
-
- - - ); + return ( + +
+
+
+

+ {server.name} +

{" "} +
+
+

+ {server.allocations + .filter((alloc) => alloc.isDefault) + .map((allocation) => ( + + {allocation.alias || ip(allocation.ip)}:{allocation.port} + + ))} +

+
+
+ + + ); }; export default ServerRow; diff --git a/resources/scripts/components/dashboard/activity/ActivityLogContainer.tsx b/resources/scripts/components/dashboard/activity/ActivityLogContainer.tsx index a61f79f60..9b715657d 100644 --- a/resources/scripts/components/dashboard/activity/ActivityLogContainer.tsx +++ b/resources/scripts/components/dashboard/activity/ActivityLogContainer.tsx @@ -1,386 +1,456 @@ -import { ArrowDownToLine, ArrowRotateLeft, Funnel, Magnifier, Xmark } from '@gravity-ui/icons'; -import { useEffect, useMemo, useState } from 'react'; +import { + ArrowDownToLine, + ArrowRotateLeft, + Funnel, + Magnifier, + Xmark, +} from "@gravity-ui/icons"; +import { useEffect, useMemo, useState } from "react"; -import FlashMessageRender from '@/components/FlashMessageRender'; -import ActionButton from '@/components/elements/ActionButton'; -import { MainPageHeader } from '@/components/elements/MainPageHeader'; -import PageContentBlock from '@/components/elements/PageContentBlock'; -import Select from '@/components/elements/Select'; -import Spinner from '@/components/elements/Spinner'; -import ActivityLogEntry from '@/components/elements/activity/ActivityLogEntry'; -import { Input } from '@/components/elements/inputs'; -import PaginationFooter from '@/components/elements/table/PaginationFooter'; +import FlashMessageRender from "@/components/FlashMessageRender"; +import ActionButton from "@/components/elements/ActionButton"; +import { MainPageHeader } from "@/components/elements/MainPageHeader"; +import PageContentBlock from "@/components/elements/PageContentBlock"; +import Select from "@/components/elements/Select"; +import Spinner from "@/components/elements/Spinner"; +import ActivityLogEntry from "@/components/elements/activity/ActivityLogEntry"; +import { Input } from "@/components/elements/inputs"; +import PaginationFooter from "@/components/elements/table/PaginationFooter"; -import { ActivityLogFilters, useActivityLogs } from '@/api/account/activity'; +import { ActivityLogFilters, useActivityLogs } from "@/api/account/activity"; -import { useFlashKey } from '@/plugins/useFlash'; -import useLocationHash from '@/plugins/useLocationHash'; +import { useFlashKey } from "@/plugins/useFlash"; +import useLocationHash from "@/plugins/useLocationHash"; const ActivityLogContainer = () => { - const { hash } = useLocationHash(); - const { clearAndAddHttpError } = useFlashKey('account'); - const [filters, setFilters] = useState({ page: 1, sorts: { timestamp: -1 } }); - const [searchTerm, setSearchTerm] = useState(''); - const [selectedEventType, setSelectedEventType] = useState(''); - const [showFilters, setShowFilters] = useState(false); - const [autoRefresh, setAutoRefresh] = useState(false); - const [dateRange, setDateRange] = useState('all'); + const { hash } = useLocationHash(); + const { clearAndAddHttpError } = useFlashKey("account"); + const [filters, setFilters] = useState({ + page: 1, + sorts: { timestamp: -1 }, + }); + const [searchTerm, setSearchTerm] = useState(""); + const [selectedEventType, setSelectedEventType] = useState(""); + const [showFilters, setShowFilters] = useState(false); + const [autoRefresh, setAutoRefresh] = useState(false); + const [dateRange, setDateRange] = useState("all"); - const { data, isValidating, error } = useActivityLogs(filters, { - revalidateOnMount: true, - revalidateOnFocus: false, - refreshInterval: autoRefresh ? 30000 : 0, // Auto-refresh every 30 seconds - }); + const { data, isValidating, error } = useActivityLogs(filters, { + revalidateOnMount: true, + revalidateOnFocus: false, + refreshInterval: autoRefresh ? 30000 : 0, // Auto-refresh every 30 seconds + }); - // Extract unique event types for filter dropdown - const eventTypes = useMemo(() => { - if (!data?.items) return []; - const types = [...new Set(data.items.map((item) => item.event))]; - return types.sort(); - }, [data?.items]); + // Extract unique event types for filter dropdown + const eventTypes = useMemo(() => { + if (!data?.items) return []; + const types = [...new Set(data.items.map((item) => item.event))]; + return types.sort(); + }, [data?.items]); - // Filter data based on search term and event type - const filteredData = useMemo(() => { - if (!data?.items) return data; + // Filter data based on search term and event type + const filteredData = useMemo(() => { + if (!data?.items) return data; - let filtered = data.items; + let filtered = data.items; - if (searchTerm) { - filtered = filtered.filter( - (item) => - item.event.toLowerCase().includes(searchTerm.toLowerCase()) || - item.ip?.toLowerCase().includes(searchTerm.toLowerCase()) || - item.relationships.actor?.username?.toLowerCase().includes(searchTerm.toLowerCase()) || - JSON.stringify(item.properties).toLowerCase().includes(searchTerm.toLowerCase()), - ); - } + if (searchTerm) { + filtered = filtered.filter( + (item) => + item.event.toLowerCase().includes(searchTerm.toLowerCase()) || + item.ip?.toLowerCase().includes(searchTerm.toLowerCase()) || + item.relationships.actor?.username + ?.toLowerCase() + .includes(searchTerm.toLowerCase()) || + JSON.stringify(item.properties) + .toLowerCase() + .includes(searchTerm.toLowerCase()), + ); + } - if (selectedEventType) { - filtered = filtered.filter((item) => item.event === selectedEventType); - } + if (selectedEventType) { + filtered = filtered.filter((item) => item.event === selectedEventType); + } - // Apply date range filtering - if (dateRange !== 'all') { - const now = new Date(); - const cutoff = new Date(); + // Apply date range filtering + if (dateRange !== "all") { + const now = new Date(); + const cutoff = new Date(); - switch (dateRange) { - case '1h': - cutoff.setHours(now.getHours() - 1); - break; - case '24h': - cutoff.setDate(now.getDate() - 1); - break; - case '7d': - cutoff.setDate(now.getDate() - 7); - break; - case '30d': - cutoff.setDate(now.getDate() - 30); - break; - } + switch (dateRange) { + case "1h": + cutoff.setHours(now.getHours() - 1); + break; + case "24h": + cutoff.setDate(now.getDate() - 1); + break; + case "7d": + cutoff.setDate(now.getDate() - 7); + break; + case "30d": + cutoff.setDate(now.getDate() - 30); + break; + } - filtered = filtered.filter((item) => new Date(item.timestamp) >= cutoff); - } + filtered = filtered.filter((item) => new Date(item.timestamp) >= cutoff); + } - return { ...data, items: filtered }; - }, [data, searchTerm, selectedEventType, dateRange]); + return { ...data, items: filtered }; + }, [data, searchTerm, selectedEventType, dateRange]); - const exportLogs = () => { - if (!filteredData?.items) return; + const exportLogs = () => { + if (!filteredData?.items) return; - const csvContent = [ - ['Timestamp', 'Event', 'Actor', 'IP Address', 'Properties'].join(','), - ...filteredData.items.map((item) => - [ - new Date(item.timestamp).toISOString(), - item.event, - item.relationships.actor?.username || 'System', - item.ip || '', - JSON.stringify(item.properties).replace(/"/g, '""'), - ] - .map((field) => `"${field}"`) - .join(','), - ), - ].join('\n'); + const csvContent = [ + ["Timestamp", "Event", "Actor", "IP Address", "Properties"].join(","), + ...filteredData.items.map((item) => + [ + new Date(item.timestamp).toISOString(), + item.event, + item.relationships.actor?.username || "System", + item.ip || "", + JSON.stringify(item.properties).replace(/"/g, '""'), + ] + .map((field) => `"${field}"`) + .join(","), + ), + ].join("\n"); - const blob = new Blob([csvContent], { type: 'text/csv' }); - const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `activity-log-${new Date().toISOString().split('T')[0]}.csv`; - a.click(); - window.URL.revokeObjectURL(url); - }; + const blob = new Blob([csvContent], { type: "text/csv" }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `activity-log-${new Date().toISOString().split("T")[0]}.csv`; + a.click(); + window.URL.revokeObjectURL(url); + }; - const clearAllFilters = () => { - setFilters((value) => ({ ...value, filters: {} })); - setSearchTerm(''); - setSelectedEventType(''); - setDateRange('all'); - }; + const clearAllFilters = () => { + setFilters((value) => ({ ...value, filters: {} })); + setSearchTerm(""); + setSelectedEventType(""); + setDateRange("all"); + }; - const hasActiveFilters = - filters.filters?.event || filters.filters?.ip || searchTerm || selectedEventType || dateRange !== 'all'; + const hasActiveFilters = + filters.filters?.event || + filters.filters?.ip || + searchTerm || + selectedEventType || + dateRange !== "all"; - // Keyboard shortcuts - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (e.ctrlKey || e.metaKey) { - switch (e.key) { - case 'f': - e.preventDefault(); - setShowFilters(!showFilters); - break; - case 'r': - e.preventDefault(); - setAutoRefresh(!autoRefresh); - break; - case 'e': - e.preventDefault(); - exportLogs(); - break; - } - } - }; + // Keyboard shortcuts + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.ctrlKey || e.metaKey) { + switch (e.key) { + case "f": + e.preventDefault(); + setShowFilters(!showFilters); + break; + case "r": + e.preventDefault(); + setAutoRefresh(!autoRefresh); + break; + case "e": + e.preventDefault(); + exportLogs(); + break; + } + } + }; - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, [showFilters, autoRefresh]); + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [showFilters, autoRefresh]); - useEffect(() => { - setFilters((value) => ({ ...value, filters: { ip: hash.ip, event: hash.event } })); - }, [hash]); + useEffect(() => { + setFilters((value) => ({ + ...value, + filters: { ip: hash.ip, event: hash.event }, + })); + }, [hash]); - useEffect(() => { - clearAndAddHttpError(error); - }, [error]); + useEffect(() => { + clearAndAddHttpError(error); + }, [error]); - return ( - -
- + return ( + +
+ -
- -
- setShowFilters(!showFilters)} - className='flex items-center gap-2' - title='Toggle Filters (Ctrl+F)' - > - - Filters - {hasActiveFilters && } - - setAutoRefresh(!autoRefresh)} - className='flex items-center gap-2' - title='Auto Refresh (Ctrl+R)' - > - {autoRefresh ? ( - - ) : ( - - )} - {autoRefresh ? 'Live' : 'Refresh'} - - - - Export - -
-
-
+
+ +
+ setShowFilters(!showFilters)} + className="flex items-center gap-2" + title="Toggle Filters (Ctrl+F)" + > + + Filters + {hasActiveFilters && ( + + )} + + setAutoRefresh(!autoRefresh)} + className="flex items-center gap-2" + title="Auto Refresh (Ctrl+R)" + > + {autoRefresh ? ( + + ) : ( + + )} + {autoRefresh ? "Live" : "Refresh"} + + + + Export + +
+
+
- {showFilters && ( -
-
-
-
- -
-

Filters

-
+ {showFilters && ( +
+
+
+
+ +
+

+ Filters +

+
-
-
- -
- - setSearchTerm(e.target.value)} - style={{ paddingLeft: '2.5rem' }} - /> -
-
+
+
+ +
+ + setSearchTerm(e.target.value)} + style={{ paddingLeft: "2.5rem" }} + /> +
+
-
- - -
+
+ + +
-
- - -
+
+ + +
-
- {hasActiveFilters && ( - - - Clear All Filters - - )} -
-
-
-
- )} +
+ {hasActiveFilters && ( + + + Clear All Filters + + )} +
+
+
+
+ )} -
-
-
-
- -
-

Activity Events

- {filteredData?.items && ( - - ({filteredData.items.length} {filteredData.items.length === 1 ? 'event' : 'events'}) - - )} -
+
+
+
+
+ +
+

+ Activity Events +

+ {filteredData?.items && ( + + ({filteredData.items.length}{" "} + {filteredData.items.length === 1 ? "event" : "events"}) + + )} +
- {!data && isValidating ? ( - - ) : !filteredData?.items?.length ? ( -
- -

- {hasActiveFilters ? 'No Matching Activity' : 'No Activity Yet'} -

-

- {hasActiveFilters - ? "Try adjusting your filters or search terms to find the activity you're looking for." - : 'Activity logs will appear here as you use your account. Check back later or perform some actions to see them here.'} -

- {hasActiveFilters && ( -
- - Clear All Filters - - setShowFilters(true)}> - Adjust Filters - -
- )} -
- ) : ( -
- {filteredData.items.map((activity) => ( - - {typeof activity.properties.useragent === 'string' && } - - ))} -
- )} + {!data && isValidating ? ( + + ) : !filteredData?.items?.length ? ( +
+ +

+ {hasActiveFilters + ? "No Matching Activity" + : "No Activity Yet"} +

+

+ {hasActiveFilters + ? "Try adjusting your filters or search terms to find the activity you're looking for." + : "Activity logs will appear here as you use your account. Check back later or perform some actions to see them here."} +

+ {hasActiveFilters && ( +
+ + Clear All Filters + + setShowFilters(true)} + > + Adjust Filters + +
+ )} +
+ ) : ( +
+ {filteredData.items.map((activity) => ( + + {typeof activity.properties.useragent === "string" && ( + + )} + + ))} +
+ )} - {data && ( -
- setFilters((value) => ({ ...value, page }))} - /> -
- )} -
-
-
- - ); + {data && ( +
+ + setFilters((value) => ({ ...value, page })) + } + /> +
+ )} +
+
+
+
+ ); }; export default ActivityLogContainer; diff --git a/resources/scripts/components/dashboard/forms/ConfigureTwoFactorForm.tsx b/resources/scripts/components/dashboard/forms/ConfigureTwoFactorForm.tsx index dc354416c..627cfa614 100644 --- a/resources/scripts/components/dashboard/forms/ConfigureTwoFactorForm.tsx +++ b/resources/scripts/components/dashboard/forms/ConfigureTwoFactorForm.tsx @@ -1,55 +1,68 @@ -import { useStoreState } from 'easy-peasy'; -import { useEffect, useState } from 'react'; +import { useStoreState } from "easy-peasy"; +import { useEffect, useState } from "react"; -import DisableTOTPDialog from '@/components/dashboard/forms/DisableTOTPDialog'; -import RecoveryTokensDialog from '@/components/dashboard/forms/RecoveryTokensDialog'; -import SetupTOTPDialog from '@/components/dashboard/forms/SetupTOTPDialog'; -import ActionButton from '@/components/elements/ActionButton'; +import DisableTOTPDialog from "@/components/dashboard/forms/DisableTOTPDialog"; +import RecoveryTokensDialog from "@/components/dashboard/forms/RecoveryTokensDialog"; +import SetupTOTPDialog from "@/components/dashboard/forms/SetupTOTPDialog"; +import ActionButton from "@/components/elements/ActionButton"; -import { ApplicationStore } from '@/state'; +import { ApplicationStore } from "@/state"; -import useFlash from '@/plugins/useFlash'; +import useFlash from "@/plugins/useFlash"; const ConfigureTwoFactorForm = () => { - const [tokens, setTokens] = useState([]); - const [visible, setVisible] = useState<'enable' | 'disable' | null>(null); - const isEnabled = useStoreState((state: ApplicationStore) => state.user.data!.useTotp); - const { clearFlashes } = useFlash(); + const [tokens, setTokens] = useState([]); + const [visible, setVisible] = useState<"enable" | "disable" | null>(null); + const isEnabled = useStoreState( + (state: ApplicationStore) => state.user.data!.useTotp, + ); + const { clearFlashes } = useFlash(); - useEffect(() => { - return () => { - clearFlashes('account:two-step'); - }; - }, [visible]); + useEffect(() => { + return () => { + clearFlashes("account:two-step"); + }; + }, [visible]); - const onTokens = (tokens: string[]) => { - setTokens(tokens); - setVisible(null); - }; + const onTokens = (tokens: string[]) => { + setTokens(tokens); + setVisible(null); + }; - return ( -
- setVisible(null)} onTokens={onTokens} /> - 0} onClose={() => setTokens([])} /> - setVisible(null)} /> -

- {isEnabled - ? 'Your account is protected by an authenticator app.' - : 'You have not configured an authenticator app.'} -

-
- {isEnabled ? ( - setVisible('disable')}> - Remove Authenticator App - - ) : ( - setVisible('enable')}> - Enable Authenticator App - - )} -
-
- ); + return ( +
+ setVisible(null)} + onTokens={onTokens} + /> + 0} + onClose={() => setTokens([])} + /> + setVisible(null)} + /> +

+ {isEnabled + ? "Your account is protected by an authenticator app." + : "You have not configured an authenticator app."} +

+
+ {isEnabled ? ( + setVisible("disable")}> + Remove Authenticator App + + ) : ( + setVisible("enable")}> + Enable Authenticator App + + )} +
+
+ ); }; export default ConfigureTwoFactorForm; diff --git a/resources/scripts/components/dashboard/forms/CreateApiKeyForm.tsx b/resources/scripts/components/dashboard/forms/CreateApiKeyForm.tsx index 8c8b31046..9e109c7c6 100644 --- a/resources/scripts/components/dashboard/forms/CreateApiKeyForm.tsx +++ b/resources/scripts/components/dashboard/forms/CreateApiKeyForm.tsx @@ -1,104 +1,118 @@ -import { Actions, useStoreActions } from 'easy-peasy'; -import { Field, Form, Formik, FormikHelpers } from 'formik'; -import { useState } from 'react'; -import { Fragment } from 'react'; -import { object, string } from 'yup'; +import { Activity02Icon } from "@hugeicons/core-free-icons"; +import { Actions, useStoreActions } from "easy-peasy"; +import { Field, Form, Formik, FormikHelpers } from "formik"; +import { useState } from "react"; +import { Fragment } from "react"; +import { object, string } from "yup"; -import FlashMessageRender from '@/components/FlashMessageRender'; -import ApiKeyModal from '@/components/dashboard/ApiKeyModal'; -import ActionButton from '@/components/elements/ActionButton'; -import ContentBox from '@/components/elements/ContentBox'; -import FormikFieldWrapper from '@/components/elements/FormikFieldWrapper'; -import Input from '@/components/elements/Input'; -import PageContentBlock from '@/components/elements/PageContentBlock'; -import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; +import FlashMessageRender from "@/components/FlashMessageRender"; +import ApiKeyModal from "@/components/dashboard/ApiKeyModal"; +import ActionButton from "@/components/elements/ActionButton"; +import ContentBox from "@/components/elements/ContentBox"; +import FormikFieldWrapper from "@/components/elements/FormikFieldWrapper"; +import Input from "@/components/elements/Input"; +import PageContentBlock from "@/components/elements/PageContentBlock"; +import SpinnerOverlay from "@/components/elements/SpinnerOverlay"; -import createApiKey from '@/api/account/createApiKey'; -import { ApiKey } from '@/api/account/getApiKeys'; -import { httpErrorToHuman } from '@/api/http'; +import createApiKey from "@/api/account/createApiKey"; +import { ApiKey } from "@/api/account/getApiKeys"; +import { httpErrorToHuman } from "@/api/http"; -import { ApplicationStore } from '@/state'; +import { ApplicationStore } from "@/state"; interface Values { - description: string; - allowedIps: string; + description: string; + allowedIps: string; } -const CreateApiKeyForm = ({ onKeyCreated }: { onKeyCreated: (key: ApiKey) => void }) => { - const [apiKey, setApiKey] = useState(''); - const { addError, clearFlashes } = useStoreActions((actions: Actions) => actions.flashes); +const CreateApiKeyForm = ({ + onKeyCreated, +}: { + onKeyCreated: (key: ApiKey) => void; +}) => { + const [apiKey, setApiKey] = useState(""); + const { addError, clearFlashes } = useStoreActions( + (actions: Actions) => actions.flashes, + ); - const submit = (values: Values, { setSubmitting, resetForm }: FormikHelpers) => { - clearFlashes('account'); - createApiKey(values.description, values.allowedIps) - .then(({ secretToken, ...key }) => { - resetForm(); - setSubmitting(false); - setApiKey(`${key.identifier}${secretToken}`); - onKeyCreated(key); - }) - .catch((error) => { - console.error(error); + const submit = ( + values: Values, + { setSubmitting, resetForm }: FormikHelpers, + ) => { + clearFlashes("account"); + createApiKey(values.description, values.allowedIps) + .then(({ secretToken, ...key }) => { + resetForm(); + setSubmitting(false); + setApiKey(`${key.identifier}${secretToken}`); + onKeyCreated(key); + }) + .catch((error) => { + console.error(error); - addError({ key: 'account', message: httpErrorToHuman(error) }); - setSubmitting(false); - }); - }; + addError({ key: "account", message: httpErrorToHuman(error) }); + setSubmitting(false); + }); + }; - return ( - <> - {/* Flash Messages */} - + return ( + <> + {/* Flash Messages */} + - {/* Modal for API Key */} - 0} onModalDismissed={() => setApiKey('')} apiKey={apiKey} /> + {/* Modal for API Key */} + 0} + onModalDismissed={() => setApiKey("")} + apiKey={apiKey} + /> - {/* Form for creating API key */} - - - {({ isSubmitting }) => ( -
- {/* Show spinner overlay when submitting */} - + {/* Form for creating API key */} + + + {({ isSubmitting }) => ( + + {/* Show spinner overlay when submitting */} + - {/* Description Field */} - - - + {/* Description Field */} + + + - {/* Allowed IPs Field */} - - - + {/* Allowed IPs Field */} + + + - {/* Submit Button below form fields */} -
- - {isSubmitting ? 'Creating...' : 'Create API Key'} - -
- - )} -
-
- - ); + {/* Submit Button below form fields */} +
+ + {isSubmitting ? "Creating..." : "Create API Key"} + +
+ + )} +
+
+ + ); }; -CreateApiKeyForm.displayName = 'CreateApiKeyForm'; +CreateApiKeyForm.displayName = "CreateApiKeyForm"; export default CreateApiKeyForm; diff --git a/resources/scripts/components/dashboard/forms/DisableTOTPDialog.tsx b/resources/scripts/components/dashboard/forms/DisableTOTPDialog.tsx index 4ec799957..12d09c4aa 100644 --- a/resources/scripts/components/dashboard/forms/DisableTOTPDialog.tsx +++ b/resources/scripts/components/dashboard/forms/DisableTOTPDialog.tsx @@ -1,85 +1,88 @@ // FIXME: replace with radix tooltip // import Tooltip from '@/components/elements/tooltip/Tooltip'; -import { useContext, useEffect, useState } from 'react'; +import { useContext, useEffect, useState } from "react"; -import FlashMessageRender from '@/components/FlashMessageRender'; -import ActionButton from '@/components/elements/ActionButton'; -import { Dialog, DialogWrapperContext } from '@/components/elements/dialog'; -import { Input } from '@/components/elements/inputs'; +import FlashMessageRender from "@/components/FlashMessageRender"; +import ActionButton from "@/components/elements/ActionButton"; +import { Dialog, DialogWrapperContext } from "@/components/elements/dialog"; +import { Input } from "@/components/elements/inputs"; -import asDialog from '@/hoc/asDialog'; +import asDialog from "@/hoc/asDialog"; -import disableAccountTwoFactor from '@/api/account/disableAccountTwoFactor'; +import disableAccountTwoFactor from "@/api/account/disableAccountTwoFactor"; -import { useStoreActions } from '@/state/hooks'; +import { useStoreActions } from "@/state/hooks"; -import { useFlashKey } from '@/plugins/useFlash'; +import { useFlashKey } from "@/plugins/useFlash"; const DisableTOTPDialog = () => { - const [submitting, setSubmitting] = useState(false); - const [password, setPassword] = useState(''); - const { clearAndAddHttpError } = useFlashKey('account:two-step'); - const { close, setProps } = useContext(DialogWrapperContext); - const updateUserData = useStoreActions((actions) => actions.user.updateUserData); + const [submitting, setSubmitting] = useState(false); + const [password, setPassword] = useState(""); + const { clearAndAddHttpError } = useFlashKey("account:two-step"); + const { close, setProps } = useContext(DialogWrapperContext); + const updateUserData = useStoreActions( + (actions) => actions.user.updateUserData, + ); - useEffect(() => { - setProps((state) => ({ ...state, preventExternalClose: submitting })); - }, [submitting]); + useEffect(() => { + setProps((state) => ({ ...state, preventExternalClose: submitting })); + }, [submitting]); - const submit = (e: React.FormEvent) => { - e.preventDefault(); - e.stopPropagation(); + const submit = (e: React.FormEvent) => { + e.preventDefault(); + e.stopPropagation(); - if (submitting) return; + if (submitting) return; - setSubmitting(true); - clearAndAddHttpError(); - disableAccountTwoFactor(password) - .then(() => { - updateUserData({ useTotp: false }); - close(); - }) - .catch(clearAndAddHttpError) - .then(() => setSubmitting(false)); - }; + setSubmitting(true); + clearAndAddHttpError(); + disableAccountTwoFactor(password) + .then(() => { + updateUserData({ useTotp: false }); + close(); + }) + .catch(clearAndAddHttpError) + .then(() => setSubmitting(false)); + }; - return ( -
- - - setPassword(e.currentTarget.value)} - /> - - - Cancel - - {/* + + + setPassword(e.currentTarget.value)} + /> + + + Cancel + + {/* 0} content={'You must enter your account password to continue.'} > */} - - Disable - - {/* */} - - - ); + + Disable + + {/* */} + + + ); }; export default asDialog({ - title: 'Remove Authenticator App', - description: 'Removing your authenticator app will make your account less secure.', + title: "Remove Authenticator App", + description: + "Removing your authenticator app will make your account less secure.", })(DisableTOTPDialog); diff --git a/resources/scripts/components/dashboard/forms/RecoveryTokensDialog.tsx b/resources/scripts/components/dashboard/forms/RecoveryTokensDialog.tsx index 0b29d1219..603a1080a 100644 --- a/resources/scripts/components/dashboard/forms/RecoveryTokensDialog.tsx +++ b/resources/scripts/components/dashboard/forms/RecoveryTokensDialog.tsx @@ -1,54 +1,58 @@ -import ActionButton from '@/components/elements/ActionButton'; -import CopyOnClick from '@/components/elements/CopyOnClick'; -import { Alert } from '@/components/elements/alert'; -import { Dialog, DialogProps } from '@/components/elements/dialog'; +import ActionButton from "@/components/elements/ActionButton"; +import CopyOnClick from "@/components/elements/CopyOnClick"; +import { Alert } from "@/components/elements/alert"; +import { Dialog, DialogProps } from "@/components/elements/dialog"; interface RecoveryTokenDialogProps extends DialogProps { - tokens: string[]; + tokens: string[]; } -const RecoveryTokensDialog = ({ tokens, open, onClose }: RecoveryTokenDialogProps) => { - const grouped = [] as [string, string][]; - tokens.forEach((token, index) => { - if (index % 2 === 0) { - grouped.push([token, tokens[index + 1] || '']); - } - }); +const RecoveryTokensDialog = ({ + tokens, + open, + onClose, +}: RecoveryTokenDialogProps) => { + const grouped = [] as [string, string][]; + tokens.forEach((token, index) => { + if (index % 2 === 0) { + grouped.push([token, tokens[index + 1] || ""]); + } + }); - return ( - - - -
-                    {grouped.map((value) => (
-                        
-                            {value[0]}
-                             
-                            {value[1]}
-                             
-                        
-                    ))}
-                
-
- - These codes will not be shown again. - - - - Done - - -
- ); + return ( + + + +
+					{grouped.map((value) => (
+						
+							{value[0]}
+							 
+							{value[1]}
+							 
+						
+					))}
+				
+
+ + These codes will not be shown again. + + + + Done + + +
+ ); }; export default RecoveryTokensDialog; diff --git a/resources/scripts/components/dashboard/forms/SetupTOTPDialog.tsx b/resources/scripts/components/dashboard/forms/SetupTOTPDialog.tsx index 91c4c59a4..a3f8b5bbd 100644 --- a/resources/scripts/components/dashboard/forms/SetupTOTPDialog.tsx +++ b/resources/scripts/components/dashboard/forms/SetupTOTPDialog.tsx @@ -1,114 +1,125 @@ // FIXME: replace with radix tooltip // import Tooltip from '@/components/elements/tooltip/Tooltip'; -import { Actions, useStoreActions } from 'easy-peasy'; -import { QRCodeSVG } from 'qrcode.react'; -import { useContext, useEffect, useState } from 'react'; +import { Actions, useStoreActions } from "easy-peasy"; +import { QRCodeSVG } from "qrcode.react"; +import { useContext, useEffect, useState } from "react"; -import FlashMessageRender from '@/components/FlashMessageRender'; -import ActionButton from '@/components/elements/ActionButton'; -import CopyOnClick from '@/components/elements/CopyOnClick'; -import Spinner from '@/components/elements/Spinner'; -import { Dialog, DialogWrapperContext } from '@/components/elements/dialog'; -import { Input } from '@/components/elements/inputs'; +import FlashMessageRender from "@/components/FlashMessageRender"; +import ActionButton from "@/components/elements/ActionButton"; +import CopyOnClick from "@/components/elements/CopyOnClick"; +import Spinner from "@/components/elements/Spinner"; +import { Dialog, DialogWrapperContext } from "@/components/elements/dialog"; +import { Input } from "@/components/elements/inputs"; -import asDialog from '@/hoc/asDialog'; +import asDialog from "@/hoc/asDialog"; -import enableAccountTwoFactor from '@/api/account/enableAccountTwoFactor'; -import getTwoFactorTokenData, { TwoFactorTokenData } from '@/api/account/getTwoFactorTokenData'; +import enableAccountTwoFactor from "@/api/account/enableAccountTwoFactor"; +import getTwoFactorTokenData, { + TwoFactorTokenData, +} from "@/api/account/getTwoFactorTokenData"; -import { ApplicationStore } from '@/state'; +import { ApplicationStore } from "@/state"; -import { useFlashKey } from '@/plugins/useFlash'; +import { useFlashKey } from "@/plugins/useFlash"; interface Props { - onTokens: (tokens: string[]) => void; + onTokens: (tokens: string[]) => void; } const ConfigureTwoFactorForm = ({ onTokens }: Props) => { - const [submitting, setSubmitting] = useState(false); - const [value, setValue] = useState(''); - const [password, setPassword] = useState(''); - const [token, setToken] = useState(null); - const { clearAndAddHttpError } = useFlashKey('account:two-step'); - const updateUserData = useStoreActions((actions: Actions) => actions.user.updateUserData); + const [submitting, setSubmitting] = useState(false); + const [value, setValue] = useState(""); + const [password, setPassword] = useState(""); + const [token, setToken] = useState(null); + const { clearAndAddHttpError } = useFlashKey("account:two-step"); + const updateUserData = useStoreActions( + (actions: Actions) => actions.user.updateUserData, + ); - const { close, setProps } = useContext(DialogWrapperContext); + const { close, setProps } = useContext(DialogWrapperContext); - useEffect(() => { - getTwoFactorTokenData() - .then(setToken) - .catch((error) => clearAndAddHttpError(error)); - }, []); + useEffect(() => { + getTwoFactorTokenData() + .then(setToken) + .catch((error) => clearAndAddHttpError(error)); + }, []); - useEffect(() => { - setProps((state) => ({ ...state, preventExternalClose: submitting })); - }, [submitting]); + useEffect(() => { + setProps((state) => ({ ...state, preventExternalClose: submitting })); + }, [submitting]); - const submit = (e: React.FormEvent) => { - e.preventDefault(); - e.stopPropagation(); + const submit = (e: React.FormEvent) => { + e.preventDefault(); + e.stopPropagation(); - if (submitting) return; + if (submitting) return; - setSubmitting(true); - clearAndAddHttpError(); - enableAccountTwoFactor(value, password) - .then((tokens) => { - updateUserData({ useTotp: true }); - onTokens(tokens); - }) - .catch((error) => { - clearAndAddHttpError(error); - setSubmitting(false); - }); - }; + setSubmitting(true); + clearAndAddHttpError(); + enableAccountTwoFactor(value, password) + .then((tokens) => { + updateUserData({ useTotp: true }); + onTokens(tokens); + }) + .catch((error) => { + clearAndAddHttpError(error); + setSubmitting(false); + }); + }; - return ( -
- -
- {!token ? ( - - ) : ( - - )} -
- -

- {token?.secret.match(/.{1,4}/g)!.join(' ') || 'Loading...'} -

-
-

- Scan the QR code above using an authenticator app, or enter the secret code above. Then, enter the - 6-digit code it generates below. -

- setValue(e.currentTarget.value)} - className={'mt-3'} - placeholder={'000000'} - type={'text'} - inputMode={'numeric'} - autoComplete={'one-time-code'} - pattern={'\\d{6}'} - /> - - setPassword(e.currentTarget.value)} - /> - - - Cancel - - {/* + +
+ {!token ? ( + + ) : ( + + )} +
+ +

+ {token?.secret.match(/.{1,4}/g)!.join(" ") || "Loading..."} +

+
+

+ Scan the QR code above using an authenticator app, or enter the secret + code above. Then, enter the 6-digit code it generates below. +

+ setValue(e.currentTarget.value)} + className={"mt-3"} + placeholder={"000000"} + type={"text"} + inputMode={"numeric"} + autoComplete={"one-time-code"} + pattern={"\\d{6}"} + /> + + setPassword(e.currentTarget.value)} + /> + + + Cancel + + {/* 0 && value.length === 6} content={ !token @@ -117,21 +128,22 @@ const ConfigureTwoFactorForm = ({ onTokens }: Props) => { } delay={100} > */} - - Enable - - {/* */} - - - ); + + Enable + + {/*
*/} +
+ + ); }; export default asDialog({ - title: 'Enable Authenticator App', - description: "You'll be required to enter a verification code each time you sign in.", + title: "Enable Authenticator App", + description: + "You'll be required to enter a verification code each time you sign in.", })(ConfigureTwoFactorForm); diff --git a/resources/scripts/components/dashboard/forms/UpdateEmailAddressForm.tsx b/resources/scripts/components/dashboard/forms/UpdateEmailAddressForm.tsx index 9dbbb82f8..53a7fc86f 100644 --- a/resources/scripts/components/dashboard/forms/UpdateEmailAddressForm.tsx +++ b/resources/scripts/components/dashboard/forms/UpdateEmailAddressForm.tsx @@ -1,77 +1,105 @@ -import { Actions, State, useStoreActions, useStoreState } from 'easy-peasy'; -import { Form, Formik, FormikHelpers } from 'formik'; -import { Fragment } from 'react'; -import * as Yup from 'yup'; +import { Actions, State, useStoreActions, useStoreState } from "easy-peasy"; +import { Form, Formik, FormikHelpers } from "formik"; +import { Fragment } from "react"; +import * as Yup from "yup"; -import ActionButton from '@/components/elements/ActionButton'; -import Field from '@/components/elements/Field'; -import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; +import ActionButton from "@/components/elements/ActionButton"; +import Field from "@/components/elements/Field"; +import SpinnerOverlay from "@/components/elements/SpinnerOverlay"; -import { httpErrorToHuman } from '@/api/http'; +import { httpErrorToHuman } from "@/api/http"; -import { ApplicationStore } from '@/state'; +import { ApplicationStore } from "@/state"; interface Values { - email: string; - password: string; + email: string; + password: string; } const schema = Yup.object().shape({ - email: Yup.string().email().required(), - password: Yup.string().required('You must provide your current account password.'), + email: Yup.string().email().required(), + password: Yup.string().required( + "You must provide your current account password.", + ), }); const UpdateEmailAddressForm = () => { - const user = useStoreState((state: State) => state.user.data); - const updateEmail = useStoreActions((state: Actions) => state.user.updateUserEmail); + const user = useStoreState( + (state: State) => state.user.data, + ); + const updateEmail = useStoreActions( + (state: Actions) => state.user.updateUserEmail, + ); - const { clearFlashes, addFlash } = useStoreActions((actions: Actions) => actions.flashes); + const { clearFlashes, addFlash } = useStoreActions( + (actions: Actions) => actions.flashes, + ); - const submit = (values: Values, { resetForm, setSubmitting }: FormikHelpers) => { - clearFlashes('account:email'); + const submit = ( + values: Values, + { resetForm, setSubmitting }: FormikHelpers, + ) => { + clearFlashes("account:email"); - updateEmail({ ...values }) - .then(() => - addFlash({ - type: 'success', - key: 'account:email', - message: 'Your primary email has been updated.', - }), - ) - .catch((error) => - addFlash({ - type: 'error', - key: 'account:email', - title: 'Error', - message: httpErrorToHuman(error), - }), - ) - .then(() => { - resetForm(); - setSubmitting(false); - }); - }; + updateEmail({ ...values }) + .then(() => + addFlash({ + type: "success", + key: "account:email", + message: "Your primary email has been updated.", + }), + ) + .catch((error) => + addFlash({ + type: "error", + key: "account:email", + title: "Error", + message: httpErrorToHuman(error), + }), + ) + .then(() => { + resetForm(); + setSubmitting(false); + }); + }; - return ( - - {({ isSubmitting, isValid }) => ( - - -
- -
- -
-
- - Update Email - -
- -
- )} -
- ); + return ( + + {({ isSubmitting, isValid }) => ( + + +
+ +
+ +
+
+ + Update Email + +
+ +
+ )} +
+ ); }; export default UpdateEmailAddressForm; diff --git a/resources/scripts/components/dashboard/forms/UpdatePasswordForm.tsx b/resources/scripts/components/dashboard/forms/UpdatePasswordForm.tsx index 8765df80c..33eaa7077 100644 --- a/resources/scripts/components/dashboard/forms/UpdatePasswordForm.tsx +++ b/resources/scripts/components/dashboard/forms/UpdatePasswordForm.tsx @@ -1,110 +1,119 @@ -import { Actions, State, useStoreActions, useStoreState } from 'easy-peasy'; -import { Form, Formik, FormikHelpers } from 'formik'; -import { Fragment } from 'react'; -import * as Yup from 'yup'; +import { Actions, State, useStoreActions, useStoreState } from "easy-peasy"; +import { Form, Formik, FormikHelpers } from "formik"; +import { Fragment } from "react"; +import * as Yup from "yup"; -import ActionButton from '@/components/elements/ActionButton'; -import Field from '@/components/elements/Field'; -import Spinner from '@/components/elements/Spinner'; -import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; +import ActionButton from "@/components/elements/ActionButton"; +import Field from "@/components/elements/Field"; +import Spinner from "@/components/elements/Spinner"; +import SpinnerOverlay from "@/components/elements/SpinnerOverlay"; -import updateAccountPassword from '@/api/account/updateAccountPassword'; -import { httpErrorToHuman } from '@/api/http'; +import updateAccountPassword from "@/api/account/updateAccountPassword"; +import { httpErrorToHuman } from "@/api/http"; -import { ApplicationStore } from '@/state'; +import { ApplicationStore } from "@/state"; interface Values { - current: string; - password: string; - confirmPassword: string; + current: string; + password: string; + confirmPassword: string; } const schema = Yup.object().shape({ - current: Yup.string().min(1).required('You must provide your current account password.'), - password: Yup.string().min(8).required(), - confirmPassword: Yup.string().test( - 'password', - 'Password confirmation does not match the password you entered.', - function (value) { - return value === this.parent.password; - }, - ), + current: Yup.string() + .min(1) + .required("You must provide your current account password."), + password: Yup.string().min(8).required(), + confirmPassword: Yup.string().test( + "password", + "Password confirmation does not match the password you entered.", + function (value) { + return value === this.parent.password; + }, + ), }); const UpdatePasswordForm = () => { - const user = useStoreState((state: State) => state.user.data); - const { clearFlashes, addFlash } = useStoreActions((actions: Actions) => actions.flashes); + const user = useStoreState( + (state: State) => state.user.data, + ); + const { clearFlashes, addFlash } = useStoreActions( + (actions: Actions) => actions.flashes, + ); - if (!user) { - return null; - } + if (!user) { + return null; + } - const submit = (values: Values, { setSubmitting }: FormikHelpers) => { - clearFlashes('account:password'); - updateAccountPassword({ ...values }) - .then(() => { - // @ts-expect-error this is valid - window.location = '/auth/login'; - }) - .catch((error) => - addFlash({ - key: 'account:password', - type: 'error', - title: 'Error', - message: httpErrorToHuman(error), - }), - ) - .then(() => setSubmitting(false)); - }; + const submit = (values: Values, { setSubmitting }: FormikHelpers) => { + clearFlashes("account:password"); + updateAccountPassword({ ...values }) + .then(() => { + // @ts-expect-error this is valid + window.location = "/auth/login"; + }) + .catch((error) => + addFlash({ + key: "account:password", + type: "error", + title: "Error", + message: httpErrorToHuman(error), + }), + ) + .then(() => setSubmitting(false)); + }; - return ( - - - {({ isSubmitting, isValid }) => ( - - -
- -
- -
-
- -
-
- - {isSubmitting && } - {isSubmitting ? 'Updating...' : 'Update Password'} - -
- -
- )} -
-
- ); + return ( + + + {({ isSubmitting, isValid }) => ( + + +
+ +
+ +
+
+ +
+
+ + {isSubmitting && } + {isSubmitting ? "Updating..." : "Update Password"} + +
+ +
+ )} +
+
+ ); }; export default UpdatePasswordForm; diff --git a/resources/scripts/components/dashboard/header/HeaderCentered.tsx b/resources/scripts/components/dashboard/header/HeaderCentered.tsx index 0e4cbb08c..ea1ced1bb 100644 --- a/resources/scripts/components/dashboard/header/HeaderCentered.tsx +++ b/resources/scripts/components/dashboard/header/HeaderCentered.tsx @@ -1,18 +1,18 @@ -import { cn } from '@/lib/utils'; +import { cn } from "@/lib/utils"; -const HeaderCentered = ({ children, className = '' }) => { - return ( -
-
- {children} -
-
- ); +const HeaderCentered = ({ children, className = "" }) => { + return ( +
+
+ {children} +
+
+ ); }; export default HeaderCentered; diff --git a/resources/scripts/components/dashboard/header/SearchSection.tsx b/resources/scripts/components/dashboard/header/SearchSection.tsx index 4a62cc2da..9109878b5 100644 --- a/resources/scripts/components/dashboard/header/SearchSection.tsx +++ b/resources/scripts/components/dashboard/header/SearchSection.tsx @@ -1,51 +1,51 @@ -import { Search01Icon } from '@hugeicons/core-free-icons'; -import { HugeiconsIcon } from '@hugeicons/react'; -import { memo, useState } from 'react'; +import { Search01Icon } from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { memo, useState } from "react"; -import { Input } from '@/components/ui/input'; -import { KeyboardShortcut } from '@/components/ui/keyboard-shortcut'; +import { Input } from "@/components/ui/input"; +import { KeyboardShortcut } from "@/components/ui/keyboard-shortcut"; const SearchIcon = memo(() => ( - + )); -SearchIcon.displayName = 'SearchIcon'; +SearchIcon.displayName = "SearchIcon"; interface SearchSectionProps { - className?: string; + className?: string; } const SearchSection = memo(({ className }: SearchSectionProps) => { - const [searchValue, setSearchValue] = useState(''); + const [searchValue, setSearchValue] = useState(""); - return ( -
-
- { - setSearchValue(e.target.value); - }} - className='pl-10 pr-16' - /> - - {!searchValue && ( -
- -
- )} -
-
- ); + return ( +
+
+ { + setSearchValue(e.target.value); + }} + className="pl-10 pr-16" + /> + + {!searchValue && ( +
+ +
+ )} +
+
+ ); }); -SearchSection.displayName = 'SearchSection'; +SearchSection.displayName = "SearchSection"; export default SearchSection; diff --git a/resources/scripts/components/dashboard/ssh/AccountSSHContainer.tsx b/resources/scripts/components/dashboard/ssh/AccountSSHContainer.tsx index 9fe0c99ca..523b0e60b 100644 --- a/resources/scripts/components/dashboard/ssh/AccountSSHContainer.tsx +++ b/resources/scripts/components/dashboard/ssh/AccountSSHContainer.tsx @@ -1,268 +1,307 @@ -import { Eye, EyeSlash, Key, Plus, TrashBin } from '@gravity-ui/icons'; -import { format } from 'date-fns'; -import { Actions, useStoreActions } from 'easy-peasy'; -import { Field, Form, Formik, FormikHelpers } from 'formik'; -import { useEffect, useState } from 'react'; -import { object, string } from 'yup'; +import { Eye, EyeSlash, Key, Plus, TrashBin } from "@gravity-ui/icons"; +import { format } from "date-fns"; +import { Actions, useStoreActions } from "easy-peasy"; +import { Field, Form, Formik, FormikHelpers } from "formik"; +import { useEffect, useState } from "react"; +import { object, string } from "yup"; -import FlashMessageRender from '@/components/FlashMessageRender'; -import ActionButton from '@/components/elements/ActionButton'; -import Code from '@/components/elements/Code'; -import FormikFieldWrapper from '@/components/elements/FormikFieldWrapper'; -import Input from '@/components/elements/Input'; -import { MainPageHeader } from '@/components/elements/MainPageHeader'; -import PageContentBlock from '@/components/elements/PageContentBlock'; -import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; -import { Dialog } from '@/components/elements/dialog'; +import FlashMessageRender from "@/components/FlashMessageRender"; +import ActionButton from "@/components/elements/ActionButton"; +import Code from "@/components/elements/Code"; +import FormikFieldWrapper from "@/components/elements/FormikFieldWrapper"; +import Input from "@/components/elements/Input"; +import { MainPageHeader } from "@/components/elements/MainPageHeader"; +import PageContentBlock from "@/components/elements/PageContentBlock"; +import SpinnerOverlay from "@/components/elements/SpinnerOverlay"; +import { Dialog } from "@/components/elements/dialog"; -import { createSSHKey, deleteSSHKey, useSSHKeys } from '@/api/account/ssh-keys'; -import { httpErrorToHuman } from '@/api/http'; +import { createSSHKey, deleteSSHKey, useSSHKeys } from "@/api/account/ssh-keys"; +import { httpErrorToHuman } from "@/api/http"; -import { ApplicationStore } from '@/state'; +import { ApplicationStore } from "@/state"; -import { useFlashKey } from '@/plugins/useFlash'; +import { useFlashKey } from "@/plugins/useFlash"; interface CreateValues { - name: string; - publicKey: string; + name: string; + publicKey: string; } const AccountSSHContainer = () => { - const [deleteKey, setDeleteKey] = useState<{ name: string; fingerprint: string } | null>(null); - const [showCreateModal, setShowCreateModal] = useState(false); - const [showKeys, setShowKeys] = useState>({}); + const [deleteKey, setDeleteKey] = useState<{ + name: string; + fingerprint: string; + } | null>(null); + const [showCreateModal, setShowCreateModal] = useState(false); + const [showKeys, setShowKeys] = useState>({}); - const { clearAndAddHttpError } = useFlashKey('account:ssh-keys'); - const { addError, clearFlashes } = useStoreActions((actions: Actions) => actions.flashes); - const { data, isValidating, error, mutate } = useSSHKeys({ - revalidateOnMount: true, - revalidateOnFocus: false, - }); + const { clearAndAddHttpError } = useFlashKey("account:ssh-keys"); + const { addError, clearFlashes } = useStoreActions( + (actions: Actions) => actions.flashes, + ); + const { data, isValidating, error, mutate } = useSSHKeys({ + revalidateOnMount: true, + revalidateOnFocus: false, + }); - useEffect(() => { - clearAndAddHttpError(error); - }, [error]); + useEffect(() => { + clearAndAddHttpError(error); + }, [error]); - const doDeletion = () => { - if (!deleteKey) return; + const doDeletion = () => { + if (!deleteKey) return; - clearAndAddHttpError(); - Promise.all([ - mutate((data) => data?.filter((value) => value.fingerprint !== deleteKey.fingerprint), false), - deleteSSHKey(deleteKey.fingerprint), - ]) - .catch((error) => { - mutate(undefined, true).catch(console.error); - clearAndAddHttpError(error); - }) - .finally(() => { - setDeleteKey(null); - }); - }; + clearAndAddHttpError(); + Promise.all([ + mutate( + (data) => + data?.filter((value) => value.fingerprint !== deleteKey.fingerprint), + false, + ), + deleteSSHKey(deleteKey.fingerprint), + ]) + .catch((error) => { + mutate(undefined, true).catch(console.error); + clearAndAddHttpError(error); + }) + .finally(() => { + setDeleteKey(null); + }); + }; - const submitCreate = (values: CreateValues, { setSubmitting, resetForm }: FormikHelpers) => { - clearFlashes('account:ssh-keys'); - createSSHKey(values.name, values.publicKey) - .then((key) => { - resetForm(); - setSubmitting(false); - mutate((data) => (data || []).concat(key)); - setShowCreateModal(false); - }) - .catch((error) => { - console.error(error); - addError({ key: 'account:ssh-keys', message: httpErrorToHuman(error) }); - setSubmitting(false); - }); - }; + const submitCreate = ( + values: CreateValues, + { setSubmitting, resetForm }: FormikHelpers, + ) => { + clearFlashes("account:ssh-keys"); + createSSHKey(values.name, values.publicKey) + .then((key) => { + resetForm(); + setSubmitting(false); + mutate((data) => (data || []).concat(key)); + setShowCreateModal(false); + }) + .catch((error) => { + console.error(error); + addError({ key: "account:ssh-keys", message: httpErrorToHuman(error) }); + setSubmitting(false); + }); + }; - const toggleKeyVisibility = (fingerprint: string) => { - setShowKeys((prev) => ({ - ...prev, - [fingerprint]: !prev[fingerprint], - })); - }; + const toggleKeyVisibility = (fingerprint: string) => { + setShowKeys((prev) => ({ + ...prev, + [fingerprint]: !prev[fingerprint], + })); + }; - return ( - - + return ( + + - {/* Create SSH Key Modal */} - {showCreateModal && ( - setShowCreateModal(false)} - title='Add SSH Key' - confirm='Add Key' - onConfirmed={() => { - const form = document.getElementById('create-ssh-form') as HTMLFormElement; - if (form) { - const submitButton = form.querySelector('button[type="submit"]') as HTMLButtonElement; - if (submitButton) submitButton.click(); - } - }} - > - - {({ isSubmitting }) => ( -
- + {/* Create SSH Key Modal */} + {showCreateModal && ( + setShowCreateModal(false)} + title="Add SSH Key" + confirm="Add Key" + onConfirmed={() => { + const form = document.getElementById( + "create-ssh-form", + ) as HTMLFormElement; + if (form) { + const submitButton = form.querySelector( + 'button[type="submit"]', + ) as HTMLButtonElement; + if (submitButton) submitButton.click(); + } + }} + > + + {({ isSubmitting }) => ( + + - - - + + + - - - + + + - - - ); + return ( + <> + setVisible(false)} + > + Removing the {name} SSH key will invalidate its usage + across the Panel. + + + + ); }; export default DeleteSSHKeyButton;