[release] v0.12.0-unstable42
This commit is contained in:
parent
aa963bb89f
commit
34bc76dbf8
11 changed files with 228 additions and 136 deletions
|
@ -8,6 +8,7 @@
|
|||
- Added Button to force reset HTTPS cert in settings
|
||||
- New color slider with reset buttons
|
||||
- Added a notification when updating a container
|
||||
- Improved icon loading speed, and added proper placeholder
|
||||
- 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 button in the servapp page to easily download the docker backup
|
||||
|
|
48
client/src/components/imageWithPlaceholder.jsx
Normal file
48
client/src/components/imageWithPlaceholder.jsx
Normal 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;
|
|
@ -64,18 +64,22 @@ const Notification = () => {
|
|||
const setAsRead = () => {
|
||||
let unread = [];
|
||||
|
||||
let newN = notifications.map((notif) => {
|
||||
notifications.forEach((notif) => {
|
||||
if (!notif.Read) {
|
||||
unread.push(notif.ID);
|
||||
}
|
||||
notif.Read = true;
|
||||
return notif;
|
||||
})
|
||||
|
||||
if (unread.length > 0) {
|
||||
API.users.readNotifs(unread);
|
||||
}
|
||||
}
|
||||
|
||||
const setLocalAsRead = (id) => {
|
||||
let newN = notifications.map((notif) => {
|
||||
notif.Read = true;
|
||||
return notif;
|
||||
})
|
||||
setNotifications(newN);
|
||||
}
|
||||
|
||||
|
@ -104,6 +108,7 @@ const Notification = () => {
|
|||
if (anchorRef.current && anchorRef.current.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
setLocalAsRead();
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import { redirectToLocal } from '../../../utils/indexs';
|
|||
import { CosmosCheckbox } from '../users/formShortcuts';
|
||||
import { Field } from 'formik';
|
||||
import MiniPlotComponent from '../../dashboard/components/mini-plot';
|
||||
import ImageWithPlaceholder from '../../../components/imageWithPlaceholder';
|
||||
|
||||
const info = {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.1)',
|
||||
|
@ -39,7 +40,7 @@ const RouteOverview = ({ routeConfig }) => {
|
|||
</div>}>
|
||||
<Stack spacing={2} direction={isMobile ? 'column' : 'row'} alignItems={isMobile ? 'center' : 'flex-start'}>
|
||||
<div>
|
||||
<img className="loading-image" alt="" src={getFaviconURL(routeConfig)} width="128px" />
|
||||
<ImageWithPlaceholder className="loading-image" alt="" src={getFaviconURL(routeConfig)} width="128px" />
|
||||
</div>
|
||||
<Stack spacing={2} style={{ width: '100%' }}>
|
||||
<strong><ContainerOutlined />Description</strong>
|
||||
|
|
|
@ -41,6 +41,7 @@ import { useNavigate } from 'react-router';
|
|||
import NewRouteCreate from '../routes/newRoute';
|
||||
import LazyLoad from 'react-lazyload';
|
||||
import MiniPlotComponent from '../../dashboard/components/mini-plot';
|
||||
import ImageWithPlaceholder from '../../../components/imageWithPlaceholder';
|
||||
|
||||
const stickyButton = {
|
||||
position: 'fixed',
|
||||
|
@ -166,7 +167,7 @@ const ProxyManagement = () => {
|
|||
{
|
||||
title: '',
|
||||
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>,
|
||||
style: {
|
||||
textAlign: 'center',
|
||||
|
|
|
@ -39,7 +39,7 @@ const _MiniPlotComponent = ({metrics, labels, noLabels, noBackground, agglo, tit
|
|||
const [ref, inView] = useInView();
|
||||
|
||||
useEffect(() => {
|
||||
if(!inView) return;
|
||||
if(!inView || series.length) return;
|
||||
|
||||
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)'}}
|
||||
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',
|
||||
whiteSpace: 'nowrap',
|
||||
}}>
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { getFaviconURL } from "./routes";
|
||||
import logogray from '../assets/images/icons/cosmos_gray.png';
|
||||
import LazyLoad from 'react-lazyload';
|
||||
import ImageWithPlaceholder from "../components/imageWithPlaceholder";
|
||||
|
||||
export const ServAppIcon = ({route, container, width, ...pprops}) => {
|
||||
return <LazyLoad width={width} height={width}>
|
||||
{(container && container.Labels["cosmos-icon"]) ?
|
||||
<img src={container.Labels["cosmos-icon"]} {...pprops} width={width} height={width}></img> :(
|
||||
route ? <img src={getFaviconURL(route)} {...pprops} width={width} height={width}></img>
|
||||
: <img src={logogray} {...pprops} width={width} height={width}></img>)}
|
||||
<ImageWithPlaceholder src={container.Labels["cosmos-icon"]} {...pprops} width={width} height={width}></ImageWithPlaceholder> :(
|
||||
route ? <ImageWithPlaceholder src={getFaviconURL(route)} {...pprops} width={width} height={width}></ImageWithPlaceholder>
|
||||
: <ImageWithPlaceholder src={logogray} {...pprops} width={width} height={width}></ImageWithPlaceholder>)}
|
||||
</LazyLoad>;
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "cosmos-server",
|
||||
"version": "0.12.0-unstable41",
|
||||
"version": "0.12.0-unstable42",
|
||||
"description": "",
|
||||
"main": "test-server.js",
|
||||
"bugs": {
|
||||
|
|
|
@ -284,8 +284,11 @@ func InitServer() *mux.Router {
|
|||
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.HandleFunc("/api/dns", GetDNSRoute)
|
||||
|
|
266
src/icons.go
266
src/icons.go
|
@ -11,6 +11,7 @@ import (
|
|||
"path"
|
||||
"time"
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"go.deanishe.net/favicon"
|
||||
|
||||
|
@ -69,154 +70,185 @@ func sendFallback(w http.ResponseWriter) {
|
|||
}
|
||||
|
||||
var IconCacheLock = make(chan bool, 1)
|
||||
|
||||
type result struct {
|
||||
IconURL string
|
||||
CachedImage CachedImage
|
||||
Error error
|
||||
}
|
||||
func GetFavicon(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.LoggedInOnly(w, req) != nil {
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
// get url from query string
|
||||
escsiteurl := req.URL.Query().Get("q")
|
||||
|
||||
|
||||
IconCacheLock <- true
|
||||
defer func() { <-IconCacheLock }()
|
||||
|
||||
|
||||
// URL decode
|
||||
siteurl, err := url.QueryUnescape(escsiteurl)
|
||||
if err != nil {
|
||||
utils.Error("Favicon: URL decode", err)
|
||||
utils.HTTPError(w, "URL decode", http.StatusInternalServerError, "FA002")
|
||||
return
|
||||
utils.Error("Favicon: URL decode", err)
|
||||
utils.HTTPError(w, "URL decode", http.StatusInternalServerError, "FA002")
|
||||
return
|
||||
}
|
||||
|
||||
if(req.Method == "GET") {
|
||||
utils.Log("Fetch favicon for " + siteurl)
|
||||
if req.Method == "GET" {
|
||||
utils.Log("Fetch favicon for " + siteurl)
|
||||
|
||||
// Check if we have the favicon in cache
|
||||
if _, ok := IconCache[siteurl]; ok {
|
||||
utils.Debug("Favicon in cache")
|
||||
resp := IconCache[siteurl]
|
||||
sendImage(w, resp)
|
||||
return
|
||||
}
|
||||
|
||||
var icons []*favicon.Icon
|
||||
var defaultIcons = []*favicon.Icon{
|
||||
&favicon.Icon{URL: "favicon.png", Width: 0},
|
||||
&favicon.Icon{URL: "/favicon.png", Width: 0},
|
||||
&favicon.Icon{URL: "favicon.ico", Width: 0},
|
||||
&favicon.Icon{URL: "/favicon.ico", Width: 0},
|
||||
}
|
||||
|
||||
// follow siteurl and check if any redirect.
|
||||
|
||||
respNew, err := httpGetWithTimeout(siteurl)
|
||||
|
||||
if err != nil {
|
||||
utils.Error("FaviconFetch", err)
|
||||
icons = append(icons, defaultIcons...)
|
||||
} else {
|
||||
siteurl = respNew.Request.URL.String()
|
||||
icons, err = favicon.Find(siteurl)
|
||||
|
||||
if err != nil || len(icons) == 0 {
|
||||
icons = append(icons, defaultIcons...)
|
||||
} else {
|
||||
// Check if icons list is missing any default values
|
||||
for _, defaultIcon := range defaultIcons {
|
||||
found := false
|
||||
for _, icon := range icons {
|
||||
if icon.URL == defaultIcon.URL {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
icons = append(icons, defaultIcon)
|
||||
}
|
||||
}
|
||||
// Check if we have the favicon in cache
|
||||
if resp, ok := IconCache[siteurl]; ok {
|
||||
utils.Debug("Favicon in cache")
|
||||
sendImage(w, resp)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, icon := range icons {
|
||||
if icon.Width <= 256 {
|
||||
var icons []*favicon.Icon
|
||||
var defaultIcons = []*favicon.Icon{
|
||||
{URL: "/favicon.ico", Width: 0},
|
||||
{URL: "/favicon.png", Width: 0},
|
||||
{URL: "favicon.ico", Width: 0},
|
||||
{URL: "favicon.png", Width: 0},
|
||||
}
|
||||
|
||||
iconURL := icon.URL
|
||||
u, err := url.Parse(siteurl)
|
||||
if err != nil {
|
||||
utils.Debug("FaviconFetch failed to parse " + err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(iconURL, "http") {
|
||||
if strings.HasPrefix(iconURL, ".") {
|
||||
// Relative URL starting with "."
|
||||
// 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
|
||||
// follow siteurl and check if any redirect.
|
||||
respNew, err := httpGetWithTimeout(siteurl)
|
||||
if err != nil {
|
||||
utils.Error("FaviconFetch", err)
|
||||
icons = append(icons, defaultIcons...)
|
||||
} else {
|
||||
siteurl = respNew.Request.URL.String()
|
||||
icons, err = favicon.Find(siteurl)
|
||||
|
||||
if err != nil || len(icons) == 0 {
|
||||
icons = append(icons, defaultIcons...)
|
||||
} 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
|
||||
// Check if icons list is missing any default values
|
||||
for _, defaultIcon := range defaultIcons {
|
||||
found := false
|
||||
for _, icon := range icons {
|
||||
if icon.URL == defaultIcon.URL {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
icons = append(icons, defaultIcon)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
utils.Debug("Favicon Trying to fetch " + iconURL)
|
||||
}
|
||||
|
||||
// Fetch the favicon
|
||||
resp, err := httpGetWithTimeout(iconURL)
|
||||
if err != nil {
|
||||
utils.Debug("FaviconFetch - " + err.Error())
|
||||
continue
|
||||
}
|
||||
// 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
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
utils.Debug("FaviconFetch - cant read " + err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
finalImage := CachedImage{
|
||||
ContentType: resp.Header.Get("Content-Type"),
|
||||
ETag: resp.Header.Get("ETag"),
|
||||
Body: body,
|
||||
// Loop through each icon and start a goroutine to fetch it
|
||||
for _, icon := range icons {
|
||||
if icon.Width <= 256 {
|
||||
wg.Add(1)
|
||||
go func(icon *favicon.Icon) {
|
||||
defer wg.Done()
|
||||
fetchAndCacheIcon(icon, siteurl, resultsChan)
|
||||
}(icon)
|
||||
}
|
||||
}
|
||||
|
||||
IconCache[siteurl] = finalImage
|
||||
// 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
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
u, err := url.Parse(baseSiteURL)
|
||||
if err != nil {
|
||||
utils.Debug("FaviconFetch failed to parse " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(iconURL, "http") {
|
||||
// Process the iconURL to make it absolute
|
||||
iconURL = resolveIconURL(iconURL, u)
|
||||
}
|
||||
|
||||
utils.Debug("Favicon Trying to fetch " + iconURL)
|
||||
|
||||
// Fetch the favicon
|
||||
resp, err := httpGetWithTimeout(iconURL)
|
||||
if err != nil {
|
||||
utils.Debug("FaviconFetch - " + err.Error())
|
||||
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
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
utils.Debug("FaviconFetch - can't read " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Prepare the cached image
|
||||
cachedImage := CachedImage{
|
||||
ContentType: resp.Header.Get("Content-Type"),
|
||||
ETag: resp.Header.Get("ETag"),
|
||||
Body: body,
|
||||
}
|
||||
|
||||
// Send the result back via the channel
|
||||
resultsChan <- result{IconURL: iconURL, CachedImage: cachedImage}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// Relative URL without starting dot or slash
|
||||
// Construct the absolute URL based on the current page's URL path
|
||||
baseURLPath := path.Dir(baseURL.Path)
|
||||
if baseURLPath == "." {
|
||||
baseURLPath = ""
|
||||
}
|
||||
return baseURL.Scheme + "://" + baseURL.Host + baseURLPath + "/" + iconURL
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func PingURL(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.LoggedInOnly(w, req) != nil {
|
||||
return
|
||||
|
|
|
@ -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
|
||||
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
|
||||
}()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue