[release] v0.12.0-unstable42

This commit is contained in:
Yann Stepienik 2023-11-05 20:34:17 +00:00
parent aa963bb89f
commit 34bc76dbf8
11 changed files with 228 additions and 136 deletions

View file

@ -8,6 +8,7 @@
- Added Button to force reset HTTPS cert in settings - Added Button to force reset HTTPS cert in settings
- New color slider with reset buttons - New color slider with reset buttons
- Added a notification when updating a container - Added a notification when updating a container
- Improved icon loading speed, and added proper placeholder
- Added lazyloading to URL and Servapp pages images - Added lazyloading to URL and Servapp pages images
- Added a dangerous IP detector that stops sending HTTP response to IPs that are abusing various shields features - Added a dangerous IP detector that stops sending HTTP response to IPs that are abusing various shields features
- Added a button in the servapp page to easily download the docker backup - Added a button in the servapp page to easily download the docker backup

View file

@ -0,0 +1,48 @@
import React, { useState } from 'react';
import LazyLoad from 'react-lazyload';
import cosmosGray from '../assets/images/icons/cosmos_gray.png';
function ImageWithPlaceholder({ src, alt, placeholder, ...props }) {
const [imageSrc, setImageSrc] = useState(placeholder || cosmosGray);
const [imageRef, setImageRef] = useState();
const onLoad = event => {
event.target.classList.add('loaded');
};
const onError = () => {
setImageSrc(cosmosGray);
};
// This function will be called when the actual image is loaded
const handleImageLoad = () => {
if (imageRef) {
imageRef.src = src;
}
};
return (
<>
<img
ref={setImageRef}
{...props}
src={imageSrc}
alt={alt}
onLoad={onLoad}
onError={onError}
// style={{ opacity: imageSrc === src ? 1 : 0, transition: 'opacity 0.5s ease-in-out' }}
/>
{/* This image will load the actual image and then handleImageLoad will be triggered */}
<img
{...props}
src={src}
alt={alt}
style={{ display: 'none' }} // Hide this image
onLoad={handleImageLoad}
onError={onError}
/>
</>
);
}
export default ImageWithPlaceholder;

View file

@ -64,18 +64,22 @@ const Notification = () => {
const setAsRead = () => { const setAsRead = () => {
let unread = []; let unread = [];
let newN = notifications.map((notif) => { notifications.forEach((notif) => {
if (!notif.Read) { if (!notif.Read) {
unread.push(notif.ID); unread.push(notif.ID);
} }
notif.Read = true;
return notif;
}) })
if (unread.length > 0) { if (unread.length > 0) {
API.users.readNotifs(unread); API.users.readNotifs(unread);
} }
}
const setLocalAsRead = (id) => {
let newN = notifications.map((notif) => {
notif.Read = true;
return notif;
})
setNotifications(newN); setNotifications(newN);
} }
@ -104,6 +108,7 @@ const Notification = () => {
if (anchorRef.current && anchorRef.current.contains(event.target)) { if (anchorRef.current && anchorRef.current.contains(event.target)) {
return; return;
} }
setLocalAsRead();
setOpen(false); setOpen(false);
}; };

View file

@ -12,6 +12,7 @@ import { redirectToLocal } from '../../../utils/indexs';
import { CosmosCheckbox } from '../users/formShortcuts'; import { CosmosCheckbox } from '../users/formShortcuts';
import { Field } from 'formik'; import { Field } from 'formik';
import MiniPlotComponent from '../../dashboard/components/mini-plot'; import MiniPlotComponent from '../../dashboard/components/mini-plot';
import ImageWithPlaceholder from '../../../components/imageWithPlaceholder';
const info = { const info = {
backgroundColor: 'rgba(0, 0, 0, 0.1)', backgroundColor: 'rgba(0, 0, 0, 0.1)',
@ -39,7 +40,7 @@ const RouteOverview = ({ routeConfig }) => {
</div>}> </div>}>
<Stack spacing={2} direction={isMobile ? 'column' : 'row'} alignItems={isMobile ? 'center' : 'flex-start'}> <Stack spacing={2} direction={isMobile ? 'column' : 'row'} alignItems={isMobile ? 'center' : 'flex-start'}>
<div> <div>
<img className="loading-image" alt="" src={getFaviconURL(routeConfig)} width="128px" /> <ImageWithPlaceholder className="loading-image" alt="" src={getFaviconURL(routeConfig)} width="128px" />
</div> </div>
<Stack spacing={2} style={{ width: '100%' }}> <Stack spacing={2} style={{ width: '100%' }}>
<strong><ContainerOutlined />Description</strong> <strong><ContainerOutlined />Description</strong>

View file

@ -41,6 +41,7 @@ import { useNavigate } from 'react-router';
import NewRouteCreate from '../routes/newRoute'; import NewRouteCreate from '../routes/newRoute';
import LazyLoad from 'react-lazyload'; import LazyLoad from 'react-lazyload';
import MiniPlotComponent from '../../dashboard/components/mini-plot'; import MiniPlotComponent from '../../dashboard/components/mini-plot';
import ImageWithPlaceholder from '../../../components/imageWithPlaceholder';
const stickyButton = { const stickyButton = {
position: 'fixed', position: 'fixed',
@ -166,7 +167,7 @@ const ProxyManagement = () => {
{ {
title: '', title: '',
field: (r) => <LazyLoad width={"64px"} height={"64px"}> field: (r) => <LazyLoad width={"64px"} height={"64px"}>
<img className="loading-image" alt="" src={getFaviconURL(r)} width="64px" height="64px"/> <ImageWithPlaceholder className="loading-image" alt="" src={getFaviconURL(r)} width="64px" height="64px"/>
</LazyLoad>, </LazyLoad>,
style: { style: {
textAlign: 'center', textAlign: 'center',

View file

@ -39,7 +39,7 @@ const _MiniPlotComponent = ({metrics, labels, noLabels, noBackground, agglo, tit
const [ref, inView] = useInView(); const [ref, inView] = useInView();
useEffect(() => { useEffect(() => {
if(!inView) return; if(!inView || series.length) return;
let xAxis = []; let xAxis = [];
@ -202,7 +202,7 @@ const _MiniPlotComponent = ({metrics, labels, noLabels, noBackground, agglo, tit
alignItems='center' sx={{padding: '0px 20px', width: '100%', backgroundColor: noBackground ? '' : 'rgba(0,0,0,0.075)'}} alignItems='center' sx={{padding: '0px 20px', width: '100%', backgroundColor: noBackground ? '' : 'rgba(0,0,0,0.075)'}}
justifyContent={'space-around'}> justifyContent={'space-around'}>
<Stack direction='column' justifyContent={'center'} alignItems={'flex-start'} spacing={0} style={{ <Stack direction='column' justifyContent={'center'} alignItems={'flex-start'} spacing={0} style={{
width: '160px', width: '160px',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
}}> }}>

View file

@ -1,12 +1,13 @@
import { getFaviconURL } from "./routes"; import { getFaviconURL } from "./routes";
import logogray from '../assets/images/icons/cosmos_gray.png'; import logogray from '../assets/images/icons/cosmos_gray.png';
import LazyLoad from 'react-lazyload'; import LazyLoad from 'react-lazyload';
import ImageWithPlaceholder from "../components/imageWithPlaceholder";
export const ServAppIcon = ({route, container, width, ...pprops}) => { export const ServAppIcon = ({route, container, width, ...pprops}) => {
return <LazyLoad width={width} height={width}> return <LazyLoad width={width} height={width}>
{(container && container.Labels["cosmos-icon"]) ? {(container && container.Labels["cosmos-icon"]) ?
<img src={container.Labels["cosmos-icon"]} {...pprops} width={width} height={width}></img> :( <ImageWithPlaceholder src={container.Labels["cosmos-icon"]} {...pprops} width={width} height={width}></ImageWithPlaceholder> :(
route ? <img src={getFaviconURL(route)} {...pprops} width={width} height={width}></img> route ? <ImageWithPlaceholder src={getFaviconURL(route)} {...pprops} width={width} height={width}></ImageWithPlaceholder>
: <img src={logogray} {...pprops} width={width} height={width}></img>)} : <ImageWithPlaceholder src={logogray} {...pprops} width={width} height={width}></ImageWithPlaceholder>)}
</LazyLoad>; </LazyLoad>;
}; };

View file

@ -1,6 +1,6 @@
{ {
"name": "cosmos-server", "name": "cosmos-server",
"version": "0.12.0-unstable41", "version": "0.12.0-unstable42",
"description": "", "description": "",
"main": "test-server.js", "main": "test-server.js",
"bugs": { "bugs": {

View file

@ -284,7 +284,10 @@ func InitServer() *mux.Router {
router.Use(utils.BlockByCountryMiddleware(config.BlockedCountries, config.CountryBlacklistIsWhitelist)) router.Use(utils.BlockByCountryMiddleware(config.BlockedCountries, config.CountryBlacklistIsWhitelist))
} }
router.HandleFunc("/logo", SendLogo) logoAPI := router.PathPrefix("/logo").Subrouter()
SecureAPI(logoAPI, true)
logoAPI.HandleFunc("/", SendLogo)
srapi := router.PathPrefix("/cosmos").Subrouter() srapi := router.PathPrefix("/cosmos").Subrouter()

View file

@ -11,6 +11,7 @@ import (
"path" "path"
"time" "time"
"context" "context"
"sync"
"go.deanishe.net/favicon" "go.deanishe.net/favicon"
@ -69,7 +70,11 @@ func sendFallback(w http.ResponseWriter) {
} }
var IconCacheLock = make(chan bool, 1) var IconCacheLock = make(chan bool, 1)
type result struct {
IconURL string
CachedImage CachedImage
Error error
}
func GetFavicon(w http.ResponseWriter, req *http.Request) { func GetFavicon(w http.ResponseWriter, req *http.Request) {
if utils.LoggedInOnly(w, req) != nil { if utils.LoggedInOnly(w, req) != nil {
return return
@ -89,29 +94,26 @@ func GetFavicon(w http.ResponseWriter, req *http.Request) {
return return
} }
if(req.Method == "GET") { if req.Method == "GET" {
utils.Log("Fetch favicon for " + siteurl) utils.Log("Fetch favicon for " + siteurl)
// Check if we have the favicon in cache // Check if we have the favicon in cache
if _, ok := IconCache[siteurl]; ok { if resp, ok := IconCache[siteurl]; ok {
utils.Debug("Favicon in cache") utils.Debug("Favicon in cache")
resp := IconCache[siteurl]
sendImage(w, resp) sendImage(w, resp)
return return
} }
var icons []*favicon.Icon var icons []*favicon.Icon
var defaultIcons = []*favicon.Icon{ var defaultIcons = []*favicon.Icon{
&favicon.Icon{URL: "favicon.png", Width: 0}, {URL: "/favicon.ico", Width: 0},
&favicon.Icon{URL: "/favicon.png", Width: 0}, {URL: "/favicon.png", Width: 0},
&favicon.Icon{URL: "favicon.ico", Width: 0}, {URL: "favicon.ico", Width: 0},
&favicon.Icon{URL: "/favicon.ico", Width: 0}, {URL: "favicon.png", Width: 0},
} }
// follow siteurl and check if any redirect. // follow siteurl and check if any redirect.
respNew, err := httpGetWithTimeout(siteurl) respNew, err := httpGetWithTimeout(siteurl)
if err != nil { if err != nil {
utils.Error("FaviconFetch", err) utils.Error("FaviconFetch", err)
icons = append(icons, defaultIcons...) icons = append(icons, defaultIcons...)
@ -138,33 +140,58 @@ func GetFavicon(w http.ResponseWriter, req *http.Request) {
} }
} }
// Create a channel to collect favicon fetch results
resultsChan := make(chan result)
// Create a wait group to wait for all goroutines to finish
var wg sync.WaitGroup
// Loop through each icon and start a goroutine to fetch it
for _, icon := range icons { for _, icon := range icons {
if icon.Width <= 256 { if icon.Width <= 256 {
wg.Add(1)
go func(icon *favicon.Icon) {
defer wg.Done()
fetchAndCacheIcon(icon, siteurl, resultsChan)
}(icon)
}
}
// Close the results channel when all fetches are done
go func() {
wg.Wait()
close(resultsChan)
}()
// Collect the results
for result := range resultsChan {
IconCache[siteurl] = result.CachedImage
sendImage(w, IconCache[siteurl])
return
}
utils.Log("Favicon final fallback")
sendFallback(w)
return
} else {
utils.Error("Favicon: Method not allowed "+req.Method, nil)
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
return
}
}
// fetchAndCacheIcon is a helper function to fetch and cache the icon
func fetchAndCacheIcon(icon *favicon.Icon, baseSiteURL string, resultsChan chan<- result) {
iconURL := icon.URL iconURL := icon.URL
u, err := url.Parse(siteurl) u, err := url.Parse(baseSiteURL)
if err != nil { if err != nil {
utils.Debug("FaviconFetch failed to parse " + err.Error()) utils.Debug("FaviconFetch failed to parse " + err.Error())
continue return
} }
if !strings.HasPrefix(iconURL, "http") { if !strings.HasPrefix(iconURL, "http") {
if strings.HasPrefix(iconURL, ".") { // Process the iconURL to make it absolute
// Relative URL starting with "." iconURL = resolveIconURL(iconURL, u)
// Resolve the relative URL based on the base URL
baseURL := u.Scheme + "://" + u.Host
iconURL = baseURL + iconURL[1:]
} else if strings.HasPrefix(iconURL, "/") {
// Relative URL starting with "/"
// Append the relative URL to the base URL
iconURL = u.Scheme + "://" + u.Host + iconURL
} else {
// Relative URL without starting dot or slash
// Construct the absolute URL based on the current page's URL path
baseURL := u.Scheme + "://" + u.Host
baseURLPath := path.Dir(u.Path)
iconURL = baseURL + baseURLPath + "/" + iconURL
}
} }
utils.Debug("Favicon Trying to fetch " + iconURL) utils.Debug("Favicon Trying to fetch " + iconURL)
@ -173,50 +200,55 @@ func GetFavicon(w http.ResponseWriter, req *http.Request) {
resp, err := httpGetWithTimeout(iconURL) resp, err := httpGetWithTimeout(iconURL)
if err != nil { if err != nil {
utils.Debug("FaviconFetch - " + err.Error()) utils.Debug("FaviconFetch - " + err.Error())
continue return
}
defer resp.Body.Close()
// Check if response is successful and content type is image
if resp.StatusCode != 200 || (!strings.Contains(resp.Header.Get("Content-Type"), "image") && !strings.Contains(resp.Header.Get("Content-Type"), "octet-stream")) {
utils.Debug("FaviconFetch - " + iconURL + " - not 200 or not image ")
return
} }
// check if 200 and if image
if resp.StatusCode != 200 {
utils.Debug("FaviconFetch - " + iconURL + " - not 200 ")
continue
} else if !strings.Contains(resp.Header.Get("Content-Type"), "image") && !strings.Contains(resp.Header.Get("Content-Type"), "octet-stream") {
utils.Debug("FaviconFetch - " + iconURL + " - not image ")
continue
} else {
utils.Log("Favicon found " + iconURL)
// Cache the response
body, err := ioutil.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
utils.Debug("FaviconFetch - cant read " + err.Error()) utils.Debug("FaviconFetch - can't read " + err.Error())
continue return
} }
finalImage := CachedImage{ // Prepare the cached image
cachedImage := CachedImage{
ContentType: resp.Header.Get("Content-Type"), ContentType: resp.Header.Get("Content-Type"),
ETag: resp.Header.Get("ETag"), ETag: resp.Header.Get("ETag"),
Body: body, Body: body,
} }
IconCache[siteurl] = finalImage // Send the result back via the channel
resultsChan <- result{IconURL: iconURL, CachedImage: cachedImage}
sendImage(w, IconCache[siteurl]) }
return
}
}
}
utils.Log("Favicon final fallback")
sendFallback(w)
return
// resolveIconURL processes the iconURL to make it an absolute URL if it is relative
func resolveIconURL(iconURL string, baseURL *url.URL) string {
if strings.HasPrefix(iconURL, ".") {
// Relative URL starting with "."
// Resolve the relative URL based on the base URL
return baseURL.Scheme + "://" + baseURL.Host + iconURL[1:]
} else if strings.HasPrefix(iconURL, "/") {
// Relative URL starting with "/"
// Append the relative URL to the base URL
return baseURL.Scheme + "://" + baseURL.Host + iconURL
} else { } else {
utils.Error("Favicon: Method not allowed" + req.Method, nil) // Relative URL without starting dot or slash
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") // Construct the absolute URL based on the current page's URL path
return baseURLPath := path.Dir(baseURL.Path)
if baseURLPath == "." {
baseURLPath = ""
}
return baseURL.Scheme + "://" + baseURL.Host + baseURLPath + "/" + iconURL
} }
} }
func PingURL(w http.ResponseWriter, req *http.Request) { func PingURL(w http.ResponseWriter, req *http.Request) {
if utils.LoggedInOnly(w, req) != nil { if utils.LoggedInOnly(w, req) != nil {
return return

View file

@ -115,6 +115,12 @@ func SaveMetrics() {
}, },
} }
CheckAlerts(dp.Key, "latest", utils.AlertMetricTrack{
Key: dp.Key,
Object: dp.Object,
Max: dp.Max,
}, dp.Value)
// This ensures that if the document doesn't exist, it'll be created // This ensures that if the document doesn't exist, it'll be created
options := options.Update().SetUpsert(true) options := options.Update().SetUpsert(true)
@ -188,12 +194,6 @@ func PushSetMetric(key string, value int, def DataDef) {
} }
} }
CheckAlerts(key, "latest", utils.AlertMetricTrack{
Key: key,
Object: def.Object,
Max: def.Max,
}, value)
lastInserted[key] = originalValue lastInserted[key] = originalValue
}() }()
} }