diff --git a/changelog.md b/changelog.md index 49352b5..722e468 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,7 @@ +## Version 0.8.0 + - Stop showing Docker not connected when first loading status + - Add privacy settings to external links + ## Version 0.7.1 -> 0.7.10 - Fix issue where multiple DBs get created at the setup - Add more special characters to be used for password validation diff --git a/client/src/App.jsx b/client/src/App.jsx index 6d22200..b83485a 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -4,23 +4,68 @@ import * as React from 'react'; import ThemeCustomization from './themes'; import ScrollTop from './components/ScrollTop'; import Snackbar from '@mui/material/Snackbar'; -import {Alert} from '@mui/material'; +import {Alert, Box} from '@mui/material'; +import logo from './assets/images/icons/cosmos.png'; + +import * as API from './api'; import { setSnackit } from './api/wrap'; // ==============================|| APP - THEME, ROUTER, LOCAL ||============================== // +const LoadingAnimation = () => ( +
+
+
+
+
+ ); + +export let SetPrimaryColor = () => {}; +export let SetSecondaryColor = () => {}; +export let GlobalPrimaryColor = ''; +export let GlobalSecondaryColor = ''; + const App = () => { const [open, setOpen] = React.useState(false); const [message, setMessage] = React.useState(''); const [severity, setSeverity] = React.useState('error'); + const [statusLoaded, setStatusLoaded] = React.useState(false); + const [PrimaryColor, setPrimaryColor] = React.useState(API.PRIMARY_COLOR); + const [SecondaryColor, setSecondaryColor] = React.useState(API.SECONDARY_COLOR); + + SetPrimaryColor = (color) => { + setPrimaryColor(color); + GlobalPrimaryColor = color; + } + + SetSecondaryColor = (color) => { + setSecondaryColor(color); + GlobalSecondaryColor = color; + } + + React.useEffect(() => { + API.getStatus(true).then((r) => { + if(r) { + setStatusLoaded(true); + } + setPrimaryColor(API.PRIMARY_COLOR); + setSecondaryColor(API.SECONDARY_COLOR); + }).catch(() => { + setStatusLoaded(true); + setPrimaryColor(API.PRIMARY_COLOR); + setSecondaryColor(API.SECONDARY_COLOR); + }); + }, []); + setSnackit((message, severity='error') => { setMessage(message); setOpen(true); setSeverity(severity); }) - return ( - + + return statusLoaded ? + { - ) + + :
+ + {/* */} + + +
+ } export default App; diff --git a/client/src/api/index.demo.jsx b/client/src/api/index.demo.jsx index 098580c..5f52259 100644 --- a/client/src/api/index.demo.jsx +++ b/client/src/api/index.demo.jsx @@ -60,4 +60,14 @@ export const checkHost = (host) => { 100 ); }); -} \ No newline at end of file +} + +export const uploadBackground = (file) => { + return new Promise((resolve, reject) => { + setTimeout(() => { + resolve({ + "status": "ok", + "data": "" + })}, 100 ); + }); + } \ No newline at end of file diff --git a/client/src/api/index.jsx b/client/src/api/index.jsx index 2ba38fa..9b9997a 100644 --- a/client/src/api/index.jsx +++ b/client/src/api/index.jsx @@ -16,17 +16,52 @@ import wrap from './wrap'; export let CPU_ARCH = 'amd64'; export let CPU_AVX = true; -let getStatus = () => { +export let HOME_BACKGROUND; +export let PRIMARY_COLOR; +export let SECONDARY_COLOR; + +export let FIRST_LOAD = false; + +let getStatus = (initial) => { return wrap(fetch('/cosmos/api/status', { method: 'GET', headers: { 'Content-Type': 'application/json' } - })) + }), initial) .then(async (response) => { CPU_ARCH = response.data.CPU; CPU_AVX = response.data.AVX; + HOME_BACKGROUND = response.data.homepage.Background; + PRIMARY_COLOR = response.data.theme.PrimaryColor; + SECONDARY_COLOR = response.data.theme.SecondaryColor; + FIRST_LOAD = true; return response + }).catch((response) => { + const urlSearch = encodeURIComponent(window.location.search); + const redirectTo = (window.location.pathname + urlSearch); + + if(response.status != 'OK') { + if( + window.location.href.indexOf('/cosmos-ui/newInstall') == -1 && + window.location.href.indexOf('/cosmos-ui/login') == -1 && + window.location.href.indexOf('/cosmos-ui/loginmfa') == -1 && + window.location.href.indexOf('/cosmos-ui/newmfa') == -1 && + window.location.href.indexOf('/cosmos-ui/register') == -1 && + window.location.href.indexOf('/cosmos-ui/forgot-password') == -1) { + if(response.status == 'NEW_INSTALL') { + window.location.href = '/cosmos-ui/newInstall'; + } else if (response.status == 'error' && response.code == "HTTP004") { + window.location.href = '/cosmos-ui/login?redirect=' + redirectTo; + } else if (response.status == 'error' && response.code == "HTTP006") { + window.location.href = '/cosmos-ui/loginmfa?redirect=' + redirectTo; + } else if (response.status == 'error' && response.code == "HTTP007") { + window.location.href = '/cosmos-ui/newmfa?redirect=' + redirectTo; + } + } else { + return "nothing"; + } + } }); } @@ -157,6 +192,15 @@ let getDNS = (host) => { }); } +let uploadBackground = (file) => { + const formData = new FormData(); + formData.append('background', file); + return wrap(fetch('/cosmos/api/background', { + method: 'POST', + body: formData + })); +}; + const isDemo = import.meta.env.MODE === 'demo'; let auth = _auth; @@ -176,6 +220,7 @@ if(isDemo) { isOnline = indexDemo.isOnline; checkHost = indexDemo.checkHost; getDNS = indexDemo.getDNS; + uploadBackground = indexDemo.uploadBackground; } export { @@ -188,5 +233,6 @@ export { newInstall, isOnline, checkHost, - getDNS + getDNS, + uploadBackground }; \ No newline at end of file diff --git a/client/src/api/wrap.js b/client/src/api/wrap.js index c6b174e..e6e45ba 100644 --- a/client/src/api/wrap.js +++ b/client/src/api/wrap.js @@ -1,20 +1,32 @@ let snackit; -export default function wrap(apicall) { +export default function wrap(apicall, noError = false) { return apicall.then(async (response) => { let rep; try { rep = await response.json(); - } catch { - snackit('Server error'); - throw new Error('Server error'); + } catch (err) { + if (!noError) { + snackit('Server error'); + throw new Error('Server error'); + } else { + const e = new Error(rep.message); + e.status = rep.status; + e.code = rep.code; + throw e; + } } + if (response.status == 200) { return rep; } - snackit(rep.message); + + if (!noError) { + snackit(rep.message); + } + const e = new Error(rep.message); - e.status = response.status; + e.status = rep.status; e.code = rep.code; throw e; }); diff --git a/client/src/assets/images/icons/cosmos_simple_black.png b/client/src/assets/images/icons/cosmos_simple_black.png new file mode 100644 index 0000000..2df8670 Binary files /dev/null and b/client/src/assets/images/icons/cosmos_simple_black.png differ diff --git a/client/src/assets/images/icons/cosmos_simple_white.png b/client/src/assets/images/icons/cosmos_simple_white.png new file mode 100644 index 0000000..3c6cbd9 Binary files /dev/null and b/client/src/assets/images/icons/cosmos_simple_white.png differ diff --git a/client/src/components/Logo/Logo.jsx b/client/src/components/Logo/Logo.jsx index 72c401f..0d8cecd 100644 --- a/client/src/components/Logo/Logo.jsx +++ b/client/src/components/Logo/Logo.jsx @@ -2,12 +2,14 @@ import { useTheme } from '@mui/material/styles'; import { fontWeight } from '@mui/system'; -import logo from '../../assets/images/icons/cosmos.png'; +import logo from '../../assets/images/icons/cosmos_simple_black.png'; +import logoDark from '../../assets/images/icons/cosmos_simple_white.png'; // ==============================|| LOGO SVG ||============================== // const Logo = () => { const theme = useTheme(); + const isLight = theme.palette.mode === 'light'; return ( /** @@ -17,7 +19,7 @@ const Logo = () => { * */ <> - Cosmos + Cosmos Cosmos ); diff --git a/client/src/components/fileUpload.jsx b/client/src/components/fileUpload.jsx index eca1932..a7ba9fc 100644 --- a/client/src/components/fileUpload.jsx +++ b/client/src/components/fileUpload.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { Button } from '@mui/material'; import { UploadOutlined } from '@ant-design/icons'; -export default function UploadButtons({OnChange, accept}) { +export default function UploadButtons({OnChange, accept, label}) { return (
diff --git a/client/src/index.css b/client/src/index.css index 5c5b5bc..3c6d9ab 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -21,6 +21,58 @@ } +@keyframes pulsing { + 0% { -webkit-transform: scale(1); } + 50% { -webkit-transform: scale(1.1); } + 100% { -webkit-transform: scale(1); } +} + +@keyframes bounce { + 0%, 80%, 100% { + transform: scale(0); + } + 40% { + transform: scale(1.0); + } +} + +.dot { + background-color: #222; + border-radius: 100%; + width: 1.5rem; + height: 1.5rem; + margin: 0 0.25rem; + + /* Animation */ + animation: bounce 1.4s infinite; + animation-fill-mode: both; +} + +@media (prefers-color-scheme: dark) { + .dot { + background-color: #eee; + } +} + +.dot:nth-child(1) { + animation-delay: -0.32s; +} + +.dot:nth-child(2) { + animation-delay: -0.16s; +} + +.dot:nth-child(3) { + animation-delay: 0s; +} + +.loader { + display: flex; + justify-content: center; + align-items: center; + height: 100%; +} + .shinyButton { overflow: hidden; } @@ -87,4 +139,8 @@ .raw-table table th, .raw-table table td { padding: 5px; border: 1px solid #ccc; +} + +.pulsing { + animation: pulsing 2s ease-in-out infinite; } \ No newline at end of file diff --git a/client/src/layout/MainLayout/Drawer/DrawerContent/Navigation/NavItem.jsx b/client/src/layout/MainLayout/Drawer/DrawerContent/Navigation/NavItem.jsx index 2be4088..c0750e8 100644 --- a/client/src/layout/MainLayout/Drawer/DrawerContent/Navigation/NavItem.jsx +++ b/client/src/layout/MainLayout/Drawer/DrawerContent/Navigation/NavItem.jsx @@ -23,7 +23,9 @@ const NavItem = ({ item, level }) => { itemTarget = '_blank'; } - let listItemProps = { component: forwardRef((props, ref) => ) }; + let listItemProps = { component: forwardRef((props, ref) => ) }; if (item?.external) { listItemProps = { component: 'a', href: item.url, target: itemTarget }; } diff --git a/client/src/pages/config/users/configman.jsx b/client/src/pages/config/users/configman.jsx index 6c5b386..e40e378 100644 --- a/client/src/pages/config/users/configman.jsx +++ b/client/src/pages/config/users/configman.jsx @@ -22,6 +22,7 @@ import { Collapse, TextField, MenuItem, + Skeleton, } from '@mui/material'; import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons'; @@ -31,11 +32,17 @@ import { WarningOutlined, PlusCircleOutlined, CopyOutlined, ExclamationCircleOut import { CosmosCheckbox, CosmosFormDivider, CosmosInputPassword, CosmosInputText, CosmosSelect } from './formShortcuts'; import CountrySelect, { countries } from '../../../components/countrySelect'; import { DnsChallengeComp } from '../../../utils/dns-challenge-comp'; +import { values } from 'lodash'; +import UploadButtons from '../../../components/fileUpload'; +import { TwitterPicker + } from 'react-color'; +import {SetPrimaryColor, SetSecondaryColor} from '../../../App'; const ConfigManagement = () => { const [config, setConfig] = React.useState(null); const [openModal, setOpenModal] = React.useState(false); + const [uploadingBackground, setUploadingBackground] = React.useState(false); function refresh() { API.config.get().then((res) => { @@ -82,7 +89,11 @@ const ConfigManagement = () => { Email_UseTLS : config.EmailConfig.UseTLS, SkipPruneNetwork: config.DockerConfig.SkipPruneNetwork, - DefaultDataPath: config.DockerConfig.DefaultDataPath || "/usr" + DefaultDataPath: config.DockerConfig.DefaultDataPath || "/usr", + + Background: config && config.HomepageConfig && config.HomepageConfig.Background, + PrimaryColor: config && config.ThemeConfig && config.ThemeConfig.PrimaryColor, + SecondaryColor: config && config.ThemeConfig && config.ThemeConfig.SecondaryColor, }} validationSchema={Yup.object().shape({ Hostname: Yup.string().max(255).required('Hostname is required'), @@ -124,7 +135,16 @@ const ConfigManagement = () => { ...config.DockerConfig, SkipPruneNetwork: values.SkipPruneNetwork, DefaultDataPath: values.DefaultDataPath - } + }, + HomepageConfig: { + ...config.HomepageConfig, + Background: values.Background + }, + ThemeConfig: { + ...config.ThemeConfig, + PrimaryColor: values.PrimaryColor, + SecondaryColor: values.SecondaryColor + }, } API.config.set(toSave).then((data) => { @@ -153,6 +173,29 @@ const ConfigManagement = () => { {(formik) => (
+ + {formik.errors.submit && ( + + {formik.errors.submit} + + )} + + + + + + + @@ -231,6 +274,64 @@ const ConfigManagement = () => { + + + + + {!uploadingBackground && formik.values.Background && preview seems broken. Please re-upload.} + {uploadingBackground && } + + { + const file = e.target.files[0]; + API.uploadBackground(file).then((data) => { + formik.setFieldValue('Background', "/cosmos/api/background/" + data.data.extension.replace(".", "")); + }); + }} + /> + + + + + + + + Primary Color + { + let colorRGB = `rgb(${color.rgb.r}, ${color.rgb.g}, ${color.rgb.b}, ${color.rgb.a})` + formik.setFieldValue('PrimaryColor', colorRGB); + SetPrimaryColor(colorRGB); + }} + /> + + + + + + Secondary Color + formik.setFieldValue('SecondaryColor', color.rgb)} + /> + + + + diff --git a/client/src/pages/home/index.jsx b/client/src/pages/home/index.jsx index 927accd..cceee99 100644 --- a/client/src/pages/home/index.jsx +++ b/client/src/pages/home/index.jsx @@ -1,6 +1,6 @@ import { useParams } from "react-router"; import Back from "../../components/back"; -import { Alert, Box, CircularProgress, Grid, Stack, useTheme } from "@mui/material"; +import { Alert, Box, CircularProgress, Grid, Stack, Typography, useMediaQuery, useTheme } from "@mui/material"; import { useEffect, useState } from "react"; import * as API from "../../api"; import wallpaper from '../../assets/images/wallpaper2.jpg'; @@ -10,19 +10,23 @@ import { getFaviconURL } from "../../utils/routes"; import { Link } from "react-router-dom"; import { getFullOrigin } from "../../utils/routes"; import IsLoggedIn from "../../isLoggedIn"; +import { ServAppIcon } from "../../utils/servapp-icon"; +import Chart from 'react-apexcharts'; export const HomeBackground = () => { const theme = useTheme(); const isDark = theme.palette.mode === 'dark'; + const customPaper = API.HOME_BACKGROUND; return ( - - + ); }; @@ -31,15 +35,16 @@ export const TransparentHeader = () => { const theme = useTheme(); const isDark = theme.palette.mode === 'dark'; - const backColor = isDark ? '0,0,0' : '255,255,255'; - const textColor = isDark ? 'white' : 'dark'; + const backColor = isDark ? '0,0,0' : '255,255,255'; + const textColor = isDark ? 'white' : 'dark'; return