forked from mirrors/thatmattlove-hyperglass
Generate opengraph images automatically, update configuration defaults with new logos
This commit is contained in:
parent
270ffecebb
commit
d0eaffb74e
7 changed files with 297 additions and 61 deletions
|
|
@ -6,8 +6,7 @@ from typing import Optional
|
|||
from pathlib import Path
|
||||
|
||||
# Third Party
|
||||
import PIL.Image as PilImage
|
||||
from pydantic import FilePath, StrictInt, root_validator
|
||||
from pydantic import FilePath, StrictInt, validator
|
||||
|
||||
# Project
|
||||
from hyperglass.models import HyperglassModel
|
||||
|
|
@ -21,11 +20,11 @@ class OpenGraph(HyperglassModel):
|
|||
|
||||
width: Optional[StrictInt]
|
||||
height: Optional[StrictInt]
|
||||
image: FilePath = DEFAULT_IMAGES / "hyperglass-opengraph.png"
|
||||
image: FilePath = DEFAULT_IMAGES / "hyperglass-opengraph.jpg"
|
||||
|
||||
@root_validator
|
||||
def validate_opengraph(cls, values):
|
||||
"""Set default opengraph image location.
|
||||
@validator("image")
|
||||
def validate_opengraph(cls, value):
|
||||
"""Ensure the opengraph image is a supported format.
|
||||
|
||||
Arguments:
|
||||
value {FilePath} -- Path to opengraph image file.
|
||||
|
|
@ -34,41 +33,11 @@ class OpenGraph(HyperglassModel):
|
|||
{Path} -- Opengraph image file path object
|
||||
"""
|
||||
supported_extensions = (".jpg", ".jpeg", ".png")
|
||||
if (
|
||||
values["image"] is not None
|
||||
and values["image"].suffix not in supported_extensions
|
||||
):
|
||||
if value is not None and value.suffix not in supported_extensions:
|
||||
raise ValueError(
|
||||
"OpenGraph image must be one of {e}".format(
|
||||
e=", ".join(supported_extensions)
|
||||
)
|
||||
)
|
||||
|
||||
with PilImage.open(values["image"]) as img:
|
||||
width, height = img.size
|
||||
if values["width"] is None:
|
||||
values["width"] = width
|
||||
if values["height"] is None:
|
||||
values["height"] = height
|
||||
|
||||
return values
|
||||
|
||||
class Config:
|
||||
"""Pydantic model configuration."""
|
||||
|
||||
title = "OpenGraph"
|
||||
description = "OpenGraph configuration parameters"
|
||||
fields = {
|
||||
"width": {
|
||||
"title": "Width",
|
||||
"description": "Width of OpenGraph image. If unset, the width will be automatically derived by reading the image file.",
|
||||
},
|
||||
"height": {
|
||||
"title": "Height",
|
||||
"description": "Height of OpenGraph image. If unset, the height will be automatically derived by reading the image file.",
|
||||
},
|
||||
"image": {
|
||||
"title": "Image File",
|
||||
"description": "Valid path to a JPG or PNG file to use as the OpenGraph image.",
|
||||
},
|
||||
}
|
||||
return value
|
||||
|
|
|
|||
|
|
@ -92,10 +92,10 @@ class Greeting(HyperglassModel):
|
|||
class Logo(HyperglassModel):
|
||||
"""Validation model for logo configuration."""
|
||||
|
||||
light: FilePath = DEFAULT_IMAGES / "hyperglass-light.png"
|
||||
dark: FilePath = DEFAULT_IMAGES / "hyperglass-dark.png"
|
||||
favicon: FilePath = DEFAULT_IMAGES / "icon.svg"
|
||||
width: Optional[Union[StrictInt, constr(regex=r"^([1-9][0-9]?|100)\%$")]] = "80%"
|
||||
light: FilePath = DEFAULT_IMAGES / "hyperglass-light.svg"
|
||||
dark: FilePath = DEFAULT_IMAGES / "hyperglass-dark.svg"
|
||||
favicon: FilePath = DEFAULT_IMAGES / "hyperglass-icon.svg"
|
||||
width: Optional[Union[StrictInt, constr(regex=r"^([1-9][0-9]?|100)\%$")]] = "75%"
|
||||
height: Optional[Union[StrictInt, constr(regex=r"^([1-9][0-9]?|100)\%$")]]
|
||||
|
||||
|
||||
|
|
@ -144,8 +144,8 @@ class Text(HyperglassModel):
|
|||
class ThemeColors(HyperglassModel):
|
||||
"""Validation model for theme colors."""
|
||||
|
||||
black: Color = "#262626"
|
||||
white: Color = "#f7f7f7"
|
||||
black: Color = "#121212"
|
||||
white: Color = "#f5f6f7"
|
||||
gray: Color = "#c1c7cc"
|
||||
red: Color = "#d84b4b"
|
||||
orange: Color = "#ff6b35"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,194 @@
|
|||
# cache:
|
||||
# database: 1
|
||||
# host: localhost
|
||||
# port: 6379
|
||||
# show_text: true
|
||||
# timeout: 120
|
||||
# cors_origins: []
|
||||
# debug: true
|
||||
# developer_mode: true
|
||||
# docs:
|
||||
# base_url: https://lg.example.net
|
||||
# communities:
|
||||
# description: List of BGP communities.
|
||||
# summary: BGP Communities List
|
||||
# title: BGP Communities
|
||||
# description: ''
|
||||
# devices:
|
||||
# description:
|
||||
# List of all devices/locations with associated identifiers, display
|
||||
# names, networks, & VRFs.
|
||||
# summary: Devices List
|
||||
# title: Devices
|
||||
# enable: true
|
||||
# mode: redoc
|
||||
# openapi_uri: /openapi.json
|
||||
# queries:
|
||||
# description: List of supported query types.
|
||||
# summary: Query Types
|
||||
# title: Supported Queries
|
||||
# query:
|
||||
# description: Request a query response per-location.
|
||||
# summary: Query the Looking Glass
|
||||
# title: Submit Query
|
||||
# title: '{site_title} API Documentation'
|
||||
# uri: /api/docs
|
||||
# listen_address: 0.0.0.0
|
||||
# listen_port: 8001
|
||||
# logging:
|
||||
# directory: /tmp
|
||||
# format: text
|
||||
# http: null
|
||||
# max_size: 50000000
|
||||
# syslog: null
|
||||
# messages:
|
||||
# acl_denied: '{target} is a member of {denied_network}, which is not allowed.'
|
||||
# acl_not_allowed: '{target} is not allowed.'
|
||||
# authentication_error: Authentication error occurred.
|
||||
# connection_error: 'Error connecting to {device_name}: {error}'
|
||||
# feature_not_enabled: '{feature} is not enabled.'
|
||||
# general: Something went wrong.
|
||||
# invalid_field: '{input} is an invalid {field}.'
|
||||
# invalid_input: '{target} is not a valid {query_type} target.'
|
||||
# no_input: '{field} must be specified.'
|
||||
# no_output: The query completed, but no matching results were found.
|
||||
# no_response: No response.
|
||||
# parsing_error: An error occurred while parsing the query output.
|
||||
# request_timeout: Request timed out.
|
||||
# vrf_not_associated: VRF {vrf_name} is not associated with {device_name}.
|
||||
# vrf_not_found: VRF {vrf_name} is not defined.
|
||||
# netmiko_delay_factor: 0.1
|
||||
# org_name: Beloved Hyperglass User
|
||||
# primary_asn: '65001'
|
||||
# queries:
|
||||
# bgp_aspath:
|
||||
# display_name: BGP AS Path
|
||||
# enable: true
|
||||
# pattern:
|
||||
# asdot: ^(\^|^\_)((\d+\.\d+)\_|(\d+\.\d+)\$|(\d+\.\d+)\(\_\.\+\_\))+$
|
||||
# asplain: .*
|
||||
# mode: asplain
|
||||
# bgp_community:
|
||||
# communities:
|
||||
# - community: 14525:5001
|
||||
# description: Phoenix, AZ Metro Aggregate Routes
|
||||
# display_name: 14525:5001
|
||||
# display_name: BGP Community
|
||||
# enable: true
|
||||
# mode: select
|
||||
# pattern:
|
||||
# decimal: ^[0-9]{1,10}$
|
||||
# extended_as: ^([0-9]{0,5})\:([0-9]{1,5})$
|
||||
# large: ^([0-9]{1,10})\:([0-9]{1,10})\:[0-9]{1,10}$
|
||||
# bgp_route:
|
||||
# display_name: BGP Route
|
||||
# enable: true
|
||||
# ping:
|
||||
# display_name: Ping
|
||||
# enable: true
|
||||
# traceroute:
|
||||
# display_name: Traceroute
|
||||
# enable: true
|
||||
# request_timeout: 90
|
||||
# site_description: Beloved Hyperglass User Network Looking Glass
|
||||
# site_keywords:
|
||||
# - Beloved Hyperglass User
|
||||
# - bgp
|
||||
# - communities
|
||||
# - community
|
||||
# - dev hyperglass
|
||||
# - hyperglass
|
||||
# - internet service provider
|
||||
# - ip
|
||||
# - ipv4
|
||||
# - ipv6
|
||||
# - isp
|
||||
# - lg
|
||||
# - looking glass
|
||||
# - network
|
||||
# - peer
|
||||
# - peering
|
||||
# - routing
|
||||
# - transit
|
||||
# site_title: dev hyperglass
|
||||
# structured:
|
||||
# communities:
|
||||
# items:
|
||||
# - 14525:\d{4}
|
||||
# mode: permit
|
||||
# rpki:
|
||||
# mode: external
|
||||
# web:
|
||||
# credit:
|
||||
# enable: true
|
||||
# dns_provider:
|
||||
# name: cloudflare
|
||||
# url: https://cloudflare-dns.com/dns-query
|
||||
# external_link:
|
||||
# enable: true
|
||||
# title: PeeringDB
|
||||
# url: https://www.peeringdb.com/asn/{primary_asn}
|
||||
# greeting:
|
||||
# button: Continue
|
||||
# enable: false
|
||||
# file: null
|
||||
# required: false
|
||||
# title: Welcome
|
||||
# help_menu:
|
||||
# enable: true
|
||||
# file: null
|
||||
# title: Help
|
||||
# logo:
|
||||
# dark: /Users/ml/dev/hyperglass/hyperglass/images/hyperglass-dark.svg
|
||||
# favicon: /Users/ml/dev/hyperglass/hyperglass/images/hyperglass-icon.svg
|
||||
# height: null
|
||||
# light: /Users/ml/dev/hyperglass/hyperglass/images/hyperglass-light.svg
|
||||
# width: 75%
|
||||
# opengraph:
|
||||
# height: null
|
||||
# image: /Users/ml/dev/hyperglass/hyperglass/images/hyperglass-opengraph.jpg
|
||||
# width: null
|
||||
# terms:
|
||||
# enable: true
|
||||
# file: null
|
||||
# title: Terms
|
||||
# text:
|
||||
# cache_icon: Cached from {time} UTC
|
||||
# cache_prefix: 'Results cached for '
|
||||
# complete_time: Completed in {seconds}
|
||||
# fqdn_tooltip: Use {protocol}
|
||||
# query_location: Location
|
||||
# query_target: Target
|
||||
# query_type: Query Type
|
||||
# query_vrf: Routing Table
|
||||
# rpki_invalid: Invalid
|
||||
# rpki_unknown: No ROAs Exist
|
||||
# rpki_unverified: Not Verified
|
||||
# rpki_valid: Valid
|
||||
# subtitle: really really really really really long subtitle
|
||||
# title: hyperglass
|
||||
# title_mode: logo_subtitle
|
||||
# theme:
|
||||
# colors:
|
||||
# black: '#121212'
|
||||
# blue: '#314cb6'
|
||||
# cyan: '#118ab2'
|
||||
# danger: '#d84b4b'
|
||||
# error: '#ff6b35'
|
||||
# gray: '#c1c7cc'
|
||||
# green: '#35b246'
|
||||
# orange: '#ff6b35'
|
||||
# pink: '#f2607d'
|
||||
# primary: '#118ab2'
|
||||
# purple: '#8d30b5'
|
||||
# red: '#d84b4b'
|
||||
# secondary: '#314cb6'
|
||||
# success: '#35b246'
|
||||
# teal: '#35b299'
|
||||
# warning: '#edae49'
|
||||
# white: '#f5f6f7'
|
||||
# yellow: '#edae49'
|
||||
# default_color_mode: null
|
||||
# fonts:
|
||||
# body: Nunito
|
||||
# mono: Fira Code
|
||||
|
|
@ -28,13 +28,10 @@ const Meta = () => {
|
|||
"isp"
|
||||
];
|
||||
const language = config?.language ?? "en";
|
||||
const ogImage = config?.web.opengraph.image ?? null;
|
||||
const ogImageHeight = config?.web.opengraph.height ?? null;
|
||||
const ogImageWidth = config?.web.opengraph.width ?? null;
|
||||
const primaryFont = googleFontUrl(theme.fonts.body);
|
||||
const monoFont = googleFontUrl(theme.fonts.mono);
|
||||
useEffect(() => {
|
||||
if (typeof window !== undefined && location === {}) {
|
||||
if (typeof window !== "undefined" && location === {}) {
|
||||
setLocation(window.location);
|
||||
}
|
||||
}, [location]);
|
||||
|
|
@ -48,11 +45,8 @@ const Meta = () => {
|
|||
<meta name="url" content={location.href} />
|
||||
<meta name="og:title" content={title} />
|
||||
<meta name="og:url" content={location.href} />
|
||||
<meta name="og:image" content={ogImage} />
|
||||
<meta name="og:description" content={description} />
|
||||
<meta property="og:image:alt" content={siteName} />
|
||||
<meta property="og:image:width" content={ogImageWidth} />
|
||||
<meta property="og:image:height" content={ogImageHeight} />
|
||||
<link href={primaryFont} rel="stylesheet" />
|
||||
<link href={monoFont} rel="stylesheet" />
|
||||
</Head>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React from "react";
|
||||
import * as React from "react";
|
||||
import Head from "next/head";
|
||||
// import { useRouter } from "next/router";
|
||||
import { HyperglassProvider } from "~/components/HyperglassProvider";
|
||||
// import Error from "./_error";
|
||||
|
|
@ -11,9 +12,20 @@ const Hyperglass = ({ Component, pageProps }) => {
|
|||
// return <Error msg="/structured" statusCode={404} />;
|
||||
// }
|
||||
return (
|
||||
<HyperglassProvider config={config}>
|
||||
<Component {...pageProps} />
|
||||
</HyperglassProvider>
|
||||
<>
|
||||
<Head>
|
||||
<title>hyperglass</title>
|
||||
<meta httpEquiv="Content-Type" content="text/html" />
|
||||
<meta charSet="UTF-8" />
|
||||
<meta name="og:type" content="website" />
|
||||
<meta name="og:image" content="/images/opengraph.jpg" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
</Head>
|
||||
<HyperglassProvider config={config}>
|
||||
<Component {...pageProps} />
|
||||
</HyperglassProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -13,11 +13,12 @@ class MyDocument extends Document {
|
|||
<Head>
|
||||
<link rel="dns-prefetch" href="//fonts.googleapis.com" />
|
||||
<link rel="dns-prefetch" href="//fonts.gstatic.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin />
|
||||
<link
|
||||
rel="preconnect"
|
||||
href="https://fonts.gstatic.com"
|
||||
crossOrigin="true"
|
||||
/>
|
||||
<link rel="preconnect" href="https://www.google-analytics.com" />
|
||||
<meta charSet="UTF-8" />
|
||||
<meta httpEquiv="Content-Type" content="text/html" />
|
||||
<meta name="og:type" content="website" />
|
||||
</Head>
|
||||
<body>
|
||||
<script src="noflash.js" />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"""Utility functions."""
|
||||
|
||||
# Standard Library
|
||||
import math
|
||||
import shutil
|
||||
from queue import Queue
|
||||
from typing import Iterable
|
||||
|
|
@ -413,6 +414,58 @@ async def read_package_json():
|
|||
return package_json
|
||||
|
||||
|
||||
def generate_opengraph(
|
||||
image_path: Path,
|
||||
max_width: int,
|
||||
max_height: int,
|
||||
target_path: Path,
|
||||
background_color: str,
|
||||
):
|
||||
"""Generate an OpenGraph compliant image."""
|
||||
from PIL import Image
|
||||
|
||||
def center_point(background: Image, foreground: Image):
|
||||
"""Generate a tuple of center points for PIL."""
|
||||
bg_x, bg_y = background.size[0:2]
|
||||
fg_x, fg_y = foreground.size[0:2]
|
||||
x1 = math.floor((bg_x / 2) - (fg_x / 2))
|
||||
y1 = math.floor((bg_y / 2) - (fg_y / 2))
|
||||
x2 = math.floor((bg_x / 2) + (fg_x / 2))
|
||||
y2 = math.floor((bg_y / 2) + (fg_y / 2))
|
||||
return (x1, y1, x2, y2)
|
||||
|
||||
# Convert image to JPEG format with static name "opengraph.jpg"
|
||||
dst_path = target_path / "opengraph.jpg"
|
||||
|
||||
# Copy the original image to the target path
|
||||
copied = shutil.copy2(image_path, target_path)
|
||||
|
||||
with Image.open(copied) as src:
|
||||
|
||||
# Only resize the image if it needs to be resized
|
||||
if src.size[0] != max_width or src.size[1] != max_height:
|
||||
|
||||
# Resize image while maintaining aspect ratio
|
||||
src.thumbnail((max_width, max_height))
|
||||
|
||||
# Only impose a background image if the original image has
|
||||
# alpha/transparency channels
|
||||
if src.mode in ("RGBA", "LA"):
|
||||
background = Image.new("RGB", (max_width, max_height), background_color)
|
||||
background.paste(src, box=center_point(background, src))
|
||||
dst = background
|
||||
else:
|
||||
dst = src
|
||||
|
||||
# Save new image to derived target path
|
||||
dst.save(dst_path)
|
||||
|
||||
if not dst_path.exists():
|
||||
raise RuntimeError(f"Unable to save resized image to {str(dst_path)}")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class FileCopy(Thread):
|
||||
"""Custom thread for copyfiles() function."""
|
||||
|
||||
|
|
@ -476,7 +529,7 @@ def copyfiles(src_files: Iterable[Path], dst_files: Iterable[Path]):
|
|||
return True
|
||||
|
||||
|
||||
async def migrate_images(app_path, params):
|
||||
async def migrate_images(app_path: Path, params: dict):
|
||||
"""Migrate images from source code to install directory."""
|
||||
images_dir = app_path / "static" / "images"
|
||||
favicon_dir = images_dir / "favicons"
|
||||
|
|
@ -493,7 +546,12 @@ async def migrate_images(app_path, params):
|
|||
|
||||
|
||||
async def build_frontend( # noqa: C901
|
||||
dev_mode, dev_url, prod_url, params, app_path, force=False
|
||||
dev_mode: bool,
|
||||
dev_url: str,
|
||||
prod_url: str,
|
||||
params: dict,
|
||||
app_path: Path,
|
||||
force: bool = False,
|
||||
):
|
||||
"""Perform full frontend UI build process.
|
||||
|
||||
|
|
@ -616,6 +674,14 @@ async def build_frontend( # noqa: C901
|
|||
|
||||
await migrate_images(app_path, params)
|
||||
|
||||
generate_opengraph(
|
||||
Path(params["web"]["opengraph"]["image"]),
|
||||
1200,
|
||||
630,
|
||||
app_path / "static" / "images",
|
||||
params["web"]["theme"]["colors"]["black"],
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise RuntimeError(str(e)) from None
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue