forked from mirrors/thatmattlove-hyperglass
Closes #175: Remove usage of hyperglass.env.json
This commit is contained in:
parent
b49b6ea58e
commit
8aeb8036ff
17 changed files with 224 additions and 195 deletions
4
hyperglass/ui/.eslintignore
Normal file
4
hyperglass/ui/.eslintignore
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.next/
|
||||||
|
favicon-formats.ts
|
||||||
1
hyperglass/ui/.gitignore
vendored
1
hyperglass/ui/.gitignore
vendored
|
|
@ -1,4 +1,5 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.env*
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
# dev/test files
|
# dev/test files
|
||||||
TODO.txt
|
TODO.txt
|
||||||
|
|
|
||||||
|
|
@ -5,4 +5,5 @@ yarn.lock
|
||||||
package-lock.json
|
package-lock.json
|
||||||
.eslintrc
|
.eslintrc
|
||||||
tsconfig.json
|
tsconfig.json
|
||||||
.next/
|
.next/
|
||||||
|
favicon-formats.ts
|
||||||
|
|
|
||||||
11
hyperglass/ui/components/favicon.tsx
Normal file
11
hyperglass/ui/components/favicon.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import type { Favicon as FaviconProps } from '~/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a `<link/>` element to reference a server-side favicon.
|
||||||
|
*/
|
||||||
|
export const Favicon = (props: FaviconProps): JSX.Element => {
|
||||||
|
const { image_format, dimensions, prefix, rel } = props;
|
||||||
|
const [w, h] = dimensions;
|
||||||
|
const src = `/images/favicons/${prefix}-${w}x${h}.${image_format}`;
|
||||||
|
return <link rel={rel ?? ''} href={src} type={`image/${image_format}`} />;
|
||||||
|
};
|
||||||
|
|
@ -2,6 +2,7 @@ export * from './card';
|
||||||
export * from './codeBlock';
|
export * from './codeBlock';
|
||||||
export * from './countdown';
|
export * from './countdown';
|
||||||
export * from './debugger';
|
export * from './debugger';
|
||||||
|
export * from './favicon';
|
||||||
export * from './footer';
|
export * from './footer';
|
||||||
export * from './form';
|
export * from './form';
|
||||||
export * from './greeting';
|
export * from './greeting';
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { useTheme } from '@chakra-ui/react';
|
||||||
import { useConfig } from '~/context';
|
import { useConfig } from '~/context';
|
||||||
import { googleFontUrl } from '~/util';
|
import { googleFontUrl } from '~/util';
|
||||||
|
|
||||||
export const Meta: React.FC = () => {
|
export const Meta = (): JSX.Element => {
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
const { fonts } = useTheme();
|
const { fonts } = useTheme();
|
||||||
const [location, setLocation] = useState('/');
|
const [location, setLocation] = useState('/');
|
||||||
|
|
@ -12,22 +12,6 @@ export const Meta: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
siteTitle: title = 'hyperglass',
|
siteTitle: title = 'hyperglass',
|
||||||
siteDescription: description = 'Network Looking Glass',
|
siteDescription: description = 'Network Looking Glass',
|
||||||
siteKeywords: keywords = [
|
|
||||||
'hyperglass',
|
|
||||||
'looking glass',
|
|
||||||
'lg',
|
|
||||||
'peer',
|
|
||||||
'peering',
|
|
||||||
'ipv4',
|
|
||||||
'ipv6',
|
|
||||||
'transit',
|
|
||||||
'community',
|
|
||||||
'communities',
|
|
||||||
'bgp',
|
|
||||||
'routing',
|
|
||||||
'network',
|
|
||||||
'isp',
|
|
||||||
],
|
|
||||||
} = useConfig();
|
} = useConfig();
|
||||||
|
|
||||||
const siteName = `${title} - ${description}`;
|
const siteName = `${title} - ${description}`;
|
||||||
|
|
@ -42,8 +26,7 @@ export const Meta: React.FC = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Head>
|
<Head>
|
||||||
<title>{title}</title>
|
<title key="title">{title}</title>
|
||||||
<meta name="language" content="en" />
|
|
||||||
<meta name="url" content={location} />
|
<meta name="url" content={location} />
|
||||||
<meta name="og:title" content={title} />
|
<meta name="og:title" content={title} />
|
||||||
<meta name="og:url" content={location} />
|
<meta name="og:url" content={location} />
|
||||||
|
|
@ -52,8 +35,7 @@ export const Meta: React.FC = () => {
|
||||||
<meta name="description" content={description} />
|
<meta name="description" content={description} />
|
||||||
<meta property="og:image:alt" content={siteName} />
|
<meta property="og:image:alt" content={siteName} />
|
||||||
<meta name="og:description" content={description} />
|
<meta name="og:description" content={description} />
|
||||||
<meta name="keywords" content={keywords.join(', ')} />
|
<meta name="hyperglass-version" content={config.version} />
|
||||||
<meta name="hg-version" content={config.version} />
|
|
||||||
</Head>
|
</Head>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
1
hyperglass/ui/favicon-formats.ts
Normal file
1
hyperglass/ui/favicon-formats.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
import type { Favicon } from '~/types';export default [{"dimensions": [64, 64], "image_format": "ico", "prefix": "favicon", "rel": null}, {"dimensions": [16, 16], "image_format": "png", "prefix": "favicon", "rel": "icon"}, {"dimensions": [32, 32], "image_format": "png", "prefix": "favicon", "rel": "icon"}, {"dimensions": [64, 64], "image_format": "png", "prefix": "favicon", "rel": "icon"}, {"dimensions": [96, 96], "image_format": "png", "prefix": "favicon", "rel": "icon"}, {"dimensions": [180, 180], "image_format": "png", "prefix": "favicon", "rel": "icon"}, {"dimensions": [57, 57], "image_format": "png", "prefix": "apple-touch-icon", "rel": "apple-touch-icon"}, {"dimensions": [60, 60], "image_format": "png", "prefix": "apple-touch-icon", "rel": "apple-touch-icon"}, {"dimensions": [72, 72], "image_format": "png", "prefix": "apple-touch-icon", "rel": "apple-touch-icon"}, {"dimensions": [76, 76], "image_format": "png", "prefix": "apple-touch-icon", "rel": "apple-touch-icon"}, {"dimensions": [114, 114], "image_format": "png", "prefix": "apple-touch-icon", "rel": "apple-touch-icon"}, {"dimensions": [120, 120], "image_format": "png", "prefix": "apple-touch-icon", "rel": "apple-touch-icon"}, {"dimensions": [144, 144], "image_format": "png", "prefix": "apple-touch-icon", "rel": "apple-touch-icon"}, {"dimensions": [152, 152], "image_format": "png", "prefix": "apple-touch-icon", "rel": "apple-touch-icon"}, {"dimensions": [167, 167], "image_format": "png", "prefix": "apple-touch-icon", "rel": "apple-touch-icon"}, {"dimensions": [180, 180], "image_format": "png", "prefix": "apple-touch-icon", "rel": "apple-touch-icon"}, {"dimensions": [70, 70], "image_format": "png", "prefix": "mstile", "rel": null}, {"dimensions": [270, 270], "image_format": "png", "prefix": "mstile", "rel": null}, {"dimensions": [310, 310], "image_format": "png", "prefix": "mstile", "rel": null}, {"dimensions": [310, 150], "image_format": "png", "prefix": "mstile", "rel": null}, {"dimensions": [196, 196], "image_format": "png", "prefix": "favicon", "rel": "shortcut icon"}] as Favicon[];
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
const envData = require('/tmp/hyperglass.env.json');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {import('next').NextConfig}
|
* @type {import('next').NextConfig}
|
||||||
*/
|
*/
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
poweredByHeader: false,
|
poweredByHeader: false,
|
||||||
env: { ...envData },
|
|
||||||
typescript: {
|
typescript: {
|
||||||
ignoreBuildErrors: true,
|
ignoreBuildErrors: true,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2,21 +2,9 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const proxyMiddleware = require('http-proxy-middleware');
|
const proxyMiddleware = require('http-proxy-middleware');
|
||||||
const next = require('next');
|
const next = require('next');
|
||||||
const config = require('/tmp/hyperglass.env.json');
|
|
||||||
|
|
||||||
const {
|
|
||||||
env: { NODE_ENV },
|
|
||||||
hyperglass: { url },
|
|
||||||
} = config;
|
|
||||||
|
|
||||||
const devProxy = {
|
|
||||||
'/api/query/': { target: url + 'api/query/', pathRewrite: { '^/api/query/': '' } },
|
|
||||||
'/ui/props/': { target: url + 'ui/props/', pathRewrite: { '^/ui/props/': '' } },
|
|
||||||
'/images': { target: url + 'images', pathRewrite: { '^/images': '' } },
|
|
||||||
};
|
|
||||||
|
|
||||||
const port = parseInt(process.env.PORT, 10) || 3000;
|
const port = parseInt(process.env.PORT, 10) || 3000;
|
||||||
const dev = NODE_ENV !== 'production';
|
const dev = process.env.NODE_ENV !== 'production';
|
||||||
const app = next({
|
const app = next({
|
||||||
dir: '.', // base directory where everything is, could move to src later
|
dir: '.', // base directory where everything is, could move to src later
|
||||||
dev,
|
dev,
|
||||||
|
|
@ -30,8 +18,20 @@ app
|
||||||
.then(() => {
|
.then(() => {
|
||||||
server = express();
|
server = express();
|
||||||
|
|
||||||
|
const devProxy = {
|
||||||
|
'/api/query/': {
|
||||||
|
target: process.env.HYPERGLASS_URL + 'api/query/',
|
||||||
|
pathRewrite: { '^/api/query/': '' },
|
||||||
|
},
|
||||||
|
'/ui/props/': {
|
||||||
|
target: process.env.HYPERGLASS_URL + 'ui/props/',
|
||||||
|
pathRewrite: { '^/ui/props/': '' },
|
||||||
|
},
|
||||||
|
'/images': { target: process.env.HYPERGLASS_URL + 'images', pathRewrite: { '^/images': '' } },
|
||||||
|
};
|
||||||
|
|
||||||
// Set up the proxy.
|
// Set up the proxy.
|
||||||
if (dev && devProxy) {
|
if (dev) {
|
||||||
Object.keys(devProxy).forEach(context => {
|
Object.keys(devProxy).forEach(context => {
|
||||||
server.use(proxyMiddleware(context, devProxy[context]));
|
server.use(proxyMiddleware(context, devProxy[context]));
|
||||||
});
|
});
|
||||||
|
|
@ -44,7 +44,7 @@ app
|
||||||
if (err) {
|
if (err) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
console.log(`> Ready on port ${port} [${NODE_ENV}]`);
|
console.log(`> Ready on port ${port} [${process.env.NODE_ENV}]`);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import Head from 'next/head';
|
|
||||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||||
|
|
||||||
import type { AppProps } from 'next/app';
|
import type { AppProps } from 'next/app';
|
||||||
|
|
@ -9,24 +8,9 @@ const App = (props: AppProps): JSX.Element => {
|
||||||
const { Component, pageProps } = props;
|
const { Component, pageProps } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<QueryClientProvider client={queryClient}>
|
||||||
<Head>
|
<Component {...pageProps} />
|
||||||
<title>hyperglass</title>
|
</QueryClientProvider>
|
||||||
<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" />
|
|
||||||
<meta
|
|
||||||
name="viewport"
|
|
||||||
content="width=device-width, initial-scale=1, user-scalable=no, maximum-scale=1.0, minimum-scale=1.0"
|
|
||||||
/>
|
|
||||||
</Head>
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<Component {...pageProps} />
|
|
||||||
</QueryClientProvider>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import Document, { Html, Head, Main, NextScript } from 'next/document';
|
import Document, { Html, Head, Main, NextScript } from 'next/document';
|
||||||
|
import { Favicon } from '~/components';
|
||||||
|
import favicons from '../favicon-formats';
|
||||||
|
|
||||||
import type { DocumentContext, DocumentInitialProps } from 'next/document';
|
import type { DocumentContext, DocumentInitialProps } from 'next/document';
|
||||||
|
|
||||||
|
|
@ -12,10 +14,23 @@ class MyDocument extends Document {
|
||||||
return (
|
return (
|
||||||
<Html lang="en">
|
<Html lang="en">
|
||||||
<Head>
|
<Head>
|
||||||
|
<meta name="language" content="en" />
|
||||||
|
<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" />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1, user-scalable=no, maximum-scale=1.0, minimum-scale=1.0"
|
||||||
|
/>
|
||||||
<link rel="dns-prefetch" href="//fonts.gstatic.com" />
|
<link rel="dns-prefetch" href="//fonts.gstatic.com" />
|
||||||
<link rel="dns-prefetch" href="//fonts.googleapis.com" />
|
<link rel="dns-prefetch" href="//fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://www.google-analytics.com" />
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="true" />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="true" />
|
||||||
|
{favicons.map((favicon, idx) => (
|
||||||
|
<Favicon key={idx} {...favicon} />
|
||||||
|
))}
|
||||||
</Head>
|
</Head>
|
||||||
<body>
|
<body>
|
||||||
<Main />
|
<Main />
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,20 @@
|
||||||
import Head from 'next/head';
|
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import { Meta, Loading, If, LoadError } from '~/components';
|
import { Meta, Loading, If, LoadError } from '~/components';
|
||||||
import { HyperglassProvider } from '~/context';
|
import { HyperglassProvider } from '~/context';
|
||||||
import { useHyperglassConfig } from '~/hooks';
|
import { useHyperglassConfig } from '~/hooks';
|
||||||
import { getFavicons } from '~/util';
|
|
||||||
|
|
||||||
import type { GetStaticProps } from 'next';
|
import type { NextPage } from 'next';
|
||||||
import type { FaviconComponent } from '~/types';
|
|
||||||
|
|
||||||
const Layout = dynamic<Dict>(() => import('~/components').then(i => i.Layout), {
|
const Layout = dynamic<Dict>(() => import('~/components').then(i => i.Layout), {
|
||||||
loading: Loading,
|
loading: Loading,
|
||||||
});
|
});
|
||||||
|
|
||||||
interface TIndex {
|
const Index: NextPage = () => {
|
||||||
favicons: FaviconComponent[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const Index = (props: TIndex): JSX.Element => {
|
|
||||||
const { favicons } = props;
|
|
||||||
const { data, error, isLoading, ready, refetch, showError, isLoadingInitial } =
|
const { data, error, isLoading, ready, refetch, showError, isLoadingInitial } =
|
||||||
useHyperglassConfig();
|
useHyperglassConfig();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
|
||||||
{favicons.map((icon, idx) => {
|
|
||||||
const { rel, href, type } = icon;
|
|
||||||
return <link rel={rel} href={href} type={type} key={idx} />;
|
|
||||||
})}
|
|
||||||
</Head>
|
|
||||||
<If c={isLoadingInitial}>
|
<If c={isLoadingInitial}>
|
||||||
<Loading />
|
<Loading />
|
||||||
</If>
|
</If>
|
||||||
|
|
@ -45,11 +31,4 @@ const Index = (props: TIndex): JSX.Element => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps<TIndex> = async () => {
|
|
||||||
const favicons = await getFavicons();
|
|
||||||
return {
|
|
||||||
props: { favicons },
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Index;
|
export default Index;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import type { Theme } from './theme';
|
import type { Theme } from './theme';
|
||||||
import type { CamelCasedPropertiesDeep, CamelCasedProperties } from 'type-fest';
|
import type { CamelCasedPropertiesDeep, CamelCasedProperties } from 'type-fest';
|
||||||
|
|
||||||
// export type QueryFields = 'query_type' | 'query_target' | 'query_location' | 'query_vrf';
|
|
||||||
|
|
||||||
type Side = 'left' | 'right';
|
type Side = 'left' | 'right';
|
||||||
|
|
||||||
export type ParsedDataField = [string, keyof Route, 'left' | 'right' | 'center' | null];
|
export type ParsedDataField = [string, keyof Route, 'left' | 'right' | 'center' | null];
|
||||||
|
|
@ -189,12 +187,6 @@ export interface Favicon {
|
||||||
prefix: string;
|
prefix: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FaviconComponent {
|
|
||||||
rel: string;
|
|
||||||
href: string;
|
|
||||||
type: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Config = CamelCasedPropertiesDeep<_ConfigDeep> & CamelCasedProperties<_ConfigShallow>;
|
export type Config = CamelCasedPropertiesDeep<_ConfigDeep> & CamelCasedProperties<_ConfigShallow>;
|
||||||
export type ThemeConfig = CamelCasedProperties<_ThemeConfig>;
|
export type ThemeConfig = CamelCasedProperties<_ThemeConfig>;
|
||||||
export type Content = CamelCasedProperties<_Content>;
|
export type Content = CamelCasedProperties<_Content>;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { isObject } from '~/types';
|
import { isObject } from '~/types';
|
||||||
|
|
||||||
import type { Config, FaviconComponent } from '~/types';
|
import type { Config } from '~/types';
|
||||||
|
|
||||||
export class ConfigLoadError extends Error {
|
export class ConfigLoadError extends Error {
|
||||||
public url: string = '/ui/props/';
|
public url: string = '/ui/props/';
|
||||||
|
|
@ -56,14 +56,3 @@ export async function getHyperglassConfig(): Promise<Config> {
|
||||||
}
|
}
|
||||||
throw new ConfigLoadError('Unknown Error');
|
throw new ConfigLoadError('Unknown Error');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFavicons(): Promise<FaviconComponent[]> {
|
|
||||||
const { favicons: faviconConfig } = process.env.hyperglass;
|
|
||||||
return faviconConfig.map(icon => {
|
|
||||||
const { image_format, dimensions, prefix } = icon;
|
|
||||||
const [w, h] = dimensions;
|
|
||||||
const rel = icon.rel ?? '';
|
|
||||||
const src = `/images/favicons/${prefix}-${w}x${h}.${image_format}`;
|
|
||||||
return { rel, href: src, type: `image/${image_format}` };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
# Standard Library
|
# Standard Library
|
||||||
import shutil
|
import shutil
|
||||||
|
import typing as t
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
from typing import List, Tuple, Union, Iterable, Optional, Generator
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
|
|
@ -11,14 +11,8 @@ from threading import Thread
|
||||||
from hyperglass.log import log
|
from hyperglass.log import log
|
||||||
|
|
||||||
|
|
||||||
async def move_files(src: Path, dst: Path, files: Iterable[Path]) -> Tuple[str]: # noqa: C901
|
async def move_files(src: Path, dst: Path, files: t.Iterable[Path]) -> t.Tuple[str]: # noqa: C901
|
||||||
"""Move iterable of files from source to destination.
|
"""Move iterable of files from source to destination."""
|
||||||
|
|
||||||
Arguments:
|
|
||||||
src {Path} -- Current directory of files
|
|
||||||
dst {Path} -- Target destination directory
|
|
||||||
files {Iterable} -- Iterable of files
|
|
||||||
"""
|
|
||||||
|
|
||||||
def error(*args, **kwargs):
|
def error(*args, **kwargs):
|
||||||
msg = ", ".join(args)
|
msg = ", ".join(args)
|
||||||
|
|
@ -39,7 +33,7 @@ async def move_files(src: Path, dst: Path, files: Iterable[Path]) -> Tuple[str]:
|
||||||
except TypeError:
|
except TypeError:
|
||||||
raise error("{p} is not a valid path", p=dst)
|
raise error("{p} is not a valid path", p=dst)
|
||||||
|
|
||||||
if not isinstance(files, (List, Tuple, Generator)):
|
if not isinstance(files, (t.List, t.Tuple, t.Generator)):
|
||||||
raise error(
|
raise error(
|
||||||
"{fa} must be an iterable (list, tuple, or generator). Received {f}",
|
"{fa} must be an iterable (list, tuple, or generator). Received {f}",
|
||||||
fa="Files argument",
|
fa="Files argument",
|
||||||
|
|
@ -95,7 +89,7 @@ class FileCopy(Thread):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def copyfiles(src_files: Iterable[Path], dst_files: Iterable[Path]):
|
def copyfiles(src_files: t.Iterable[Path], dst_files: t.Iterable[Path]):
|
||||||
"""Copy iterable of files from source to destination with threading."""
|
"""Copy iterable of files from source to destination with threading."""
|
||||||
queue = Queue()
|
queue = Queue()
|
||||||
threads = ()
|
threads = ()
|
||||||
|
|
@ -131,7 +125,7 @@ def copyfiles(src_files: Iterable[Path], dst_files: Iterable[Path]):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def check_path(path: Union[Path, str], mode: str = "r", create: bool = False) -> Optional[Path]:
|
def check_path(path: t.Union[Path, str], mode: str = "r", create: bool = False) -> t.Optional[Path]:
|
||||||
"""Verify if a path exists and is accessible."""
|
"""Verify if a path exists and is accessible."""
|
||||||
|
|
||||||
result = None
|
result = None
|
||||||
|
|
@ -157,3 +151,30 @@ def check_path(path: Union[Path, str], mode: str = "r", create: bool = False) ->
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def dotenv_to_dict(dotenv: t.Union[Path, str]) -> t.Dict[str, str]:
|
||||||
|
"""Convert a .env file to a Python dict."""
|
||||||
|
if not isinstance(dotenv, (Path, str)):
|
||||||
|
raise TypeError("Argument 'file' must be a Path object or string")
|
||||||
|
result = {}
|
||||||
|
data = ""
|
||||||
|
if isinstance(dotenv, Path):
|
||||||
|
if not dotenv.exists():
|
||||||
|
raise FileNotFoundError("{!r} does not exist", str(dotenv))
|
||||||
|
with dotenv.open("r") as f:
|
||||||
|
data = f.read()
|
||||||
|
else:
|
||||||
|
data = dotenv
|
||||||
|
|
||||||
|
for line in (line for line in (line.strip() for line in data.splitlines()) if line):
|
||||||
|
parts = line.split("=")
|
||||||
|
if len(parts) != 2:
|
||||||
|
raise TypeError(
|
||||||
|
f"Line {line!r} is improperly formatted. "
|
||||||
|
"Expected a key/value pair such as 'key=value'"
|
||||||
|
)
|
||||||
|
key, value = line.split("=")
|
||||||
|
result[key.strip()] = value.strip()
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ from pathlib import Path
|
||||||
from hyperglass.log import log
|
from hyperglass.log import log
|
||||||
|
|
||||||
# Local
|
# Local
|
||||||
from .files import copyfiles, check_path
|
from .files import copyfiles, check_path, dotenv_to_dict
|
||||||
|
|
||||||
if t.TYPE_CHECKING:
|
if t.TYPE_CHECKING:
|
||||||
# Project
|
# Project
|
||||||
|
|
@ -239,6 +239,26 @@ def migrate_images(app_path: Path, params: "UIParameters"):
|
||||||
return copyfiles(src_files, dst_files)
|
return copyfiles(src_files, dst_files)
|
||||||
|
|
||||||
|
|
||||||
|
def write_favicon_formats(formats: t.Tuple[t.Dict[str, t.Any]]) -> None:
|
||||||
|
"""Create a TypeScript file in the `ui` directory containing favicon formats.
|
||||||
|
|
||||||
|
This file should stay the same, unless the favicons library updates
|
||||||
|
supported formats.
|
||||||
|
"""
|
||||||
|
# Standard Library
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
file = Path(__file__).parent.parent / "ui" / "favicon-formats.ts"
|
||||||
|
|
||||||
|
# Sort each favicon definition to ensure the result stays the same
|
||||||
|
# time the UI build runs.
|
||||||
|
ordered = json.dumps([OrderedDict(sorted(fmt.items())) for fmt in formats])
|
||||||
|
data = "import type {{ Favicon }} from '~/types';export default {} as Favicon[];".format(
|
||||||
|
ordered
|
||||||
|
)
|
||||||
|
file.write_text(data)
|
||||||
|
|
||||||
|
|
||||||
async def build_frontend( # noqa: C901
|
async def build_frontend( # noqa: C901
|
||||||
dev_mode: bool,
|
dev_mode: bool,
|
||||||
dev_url: str,
|
dev_url: str,
|
||||||
|
|
@ -249,19 +269,7 @@ async def build_frontend( # noqa: C901
|
||||||
timeout: int = 180,
|
timeout: int = 180,
|
||||||
full: bool = False,
|
full: bool = False,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Perform full frontend UI build process.
|
"""Perform full frontend UI build process."""
|
||||||
|
|
||||||
Securely creates temporary file, writes frontend configuration
|
|
||||||
parameters to file as JSON. Then writes the name of the temporary
|
|
||||||
file to /tmp/hyperglass.env.json as {"configFile": <file_name> }.
|
|
||||||
|
|
||||||
Webpack reads /tmp/hyperglass.env.json, loads the temporary file,
|
|
||||||
and sets its contents to Node environment variables during the build
|
|
||||||
process.
|
|
||||||
|
|
||||||
After the build is successful, the temporary file is automatically
|
|
||||||
closed during garbage collection.
|
|
||||||
"""
|
|
||||||
# Standard Library
|
# Standard Library
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
|
|
@ -273,24 +281,18 @@ async def build_frontend( # noqa: C901
|
||||||
|
|
||||||
# Create temporary file. json file extension is added for easy
|
# Create temporary file. json file extension is added for easy
|
||||||
# webpack JSON parsing.
|
# webpack JSON parsing.
|
||||||
env_file = Path("/tmp/hyperglass.env.json") # noqa: S108
|
dot_env_file = Path(__file__).parent.parent / "ui" / ".env"
|
||||||
|
env_config = {}
|
||||||
|
|
||||||
package_json = await read_package_json()
|
package_json = await read_package_json()
|
||||||
|
|
||||||
env_vars = {
|
|
||||||
"hyperglass": {"version": __version__},
|
|
||||||
"env": {},
|
|
||||||
}
|
|
||||||
|
|
||||||
# Set NextJS production/development mode and base URL based on
|
# Set NextJS production/development mode and base URL based on
|
||||||
# developer_mode setting.
|
# developer_mode setting.
|
||||||
if dev_mode:
|
if dev_mode:
|
||||||
env_vars["env"].update({"NODE_ENV": "development"})
|
env_config.update({"HYPERGLASS_URL": dev_url, "NODE_ENV": "development"})
|
||||||
env_vars["hyperglass"].update({"url": dev_url})
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
env_vars["env"].update({"NODE_ENV": "production"})
|
env_config.update({"HYPERGLASS_URL": prod_url, "NODE_ENV": "production"})
|
||||||
env_vars["hyperglass"].update({"url": prod_url})
|
|
||||||
|
|
||||||
# Check if hyperglass/ui/node_modules has been initialized. If not,
|
# Check if hyperglass/ui/node_modules has been initialized. If not,
|
||||||
# initialize it.
|
# initialize it.
|
||||||
|
|
@ -310,71 +312,70 @@ async def build_frontend( # noqa: C901
|
||||||
images_dir = app_path / "static" / "images"
|
images_dir = app_path / "static" / "images"
|
||||||
favicon_dir = images_dir / "favicons"
|
favicon_dir = images_dir / "favicons"
|
||||||
|
|
||||||
try:
|
if not favicon_dir.exists():
|
||||||
if not favicon_dir.exists():
|
favicon_dir.mkdir()
|
||||||
favicon_dir.mkdir()
|
|
||||||
async with Favicons(
|
|
||||||
source=params.web.logo.favicon,
|
|
||||||
output_directory=favicon_dir,
|
|
||||||
base_url="/images/favicons/",
|
|
||||||
) as favicons:
|
|
||||||
await favicons.generate()
|
|
||||||
log.debug("Generated {} favicons", favicons.completed)
|
|
||||||
env_vars["hyperglass"].update({"favicons": favicons.formats()})
|
|
||||||
build_data = {
|
|
||||||
"params": params.export_dict(),
|
|
||||||
"version": __version__,
|
|
||||||
"package_json": package_json,
|
|
||||||
}
|
|
||||||
build_json = json.dumps(build_data, default=str)
|
|
||||||
# Create SHA256 hash from all parameters passed to UI, use as
|
|
||||||
# build identifier.
|
|
||||||
build_id = hashlib.sha256(build_json.encode()).hexdigest()
|
|
||||||
|
|
||||||
# Read hard-coded environment file from last build. If build ID
|
async with Favicons(
|
||||||
# matches this build's ID, don't run a new build.
|
source=params.web.logo.favicon,
|
||||||
if env_file.exists() and not force:
|
output_directory=favicon_dir,
|
||||||
with env_file.open("r") as ef:
|
base_url="/images/favicons/",
|
||||||
ef_id = json.load(ef).get("buildId", "empty")
|
) as favicons:
|
||||||
log.debug("Previous Build ID: {}", ef_id)
|
await favicons.generate()
|
||||||
|
log.debug("Generated {} favicons", favicons.completed)
|
||||||
|
write_favicon_formats(favicons.formats())
|
||||||
|
|
||||||
if ef_id == build_id:
|
build_data = {
|
||||||
log.debug("UI parameters unchanged since last build, skipping UI build...")
|
"params": params.export_dict(),
|
||||||
return True
|
"version": __version__,
|
||||||
|
"package_json": package_json,
|
||||||
|
}
|
||||||
|
|
||||||
env_vars["buildId"] = build_id
|
build_json = json.dumps(build_data, default=str)
|
||||||
|
|
||||||
env_file.write_text(json.dumps(env_vars, default=str))
|
# Create SHA256 hash from all parameters passed to UI, use as
|
||||||
log.debug("Wrote UI environment file '{}'", str(env_file))
|
# build identifier.
|
||||||
|
build_id = hashlib.sha256(build_json.encode()).hexdigest()
|
||||||
|
|
||||||
# Initiate Next.JS export process.
|
# Read hard-coded environment file from last build. If build ID
|
||||||
if any((not dev_mode, force, full)):
|
# matches this build's ID, don't run a new build.
|
||||||
log.info("Starting UI build")
|
if dot_env_file.exists() and not force:
|
||||||
initialize_result = await node_initial(timeout, dev_mode)
|
env_data = dotenv_to_dict(dot_env_file)
|
||||||
build_result = await build_ui(app_path=app_path)
|
env_build_id = env_data.get("HYPERGLASS_BUILD_ID", "None")
|
||||||
|
log.debug("Previous Build ID: {!r}", env_build_id)
|
||||||
|
|
||||||
if initialize_result:
|
if env_build_id == build_id:
|
||||||
log.debug(initialize_result)
|
log.debug("UI parameters unchanged since last build, skipping UI build...")
|
||||||
elif initialize_result == "":
|
return True
|
||||||
log.debug("Re-initialized node_modules")
|
|
||||||
|
|
||||||
if build_result:
|
env_config.update({"HYPERGLASS_BUILD_ID": build_id})
|
||||||
log.success("Completed UI build")
|
|
||||||
elif dev_mode and not force:
|
|
||||||
log.debug("Running in developer mode, did not build new UI files")
|
|
||||||
|
|
||||||
migrate_images(app_path, params)
|
dot_env_file.write_text("\n".join(f"{k}={v}" for k, v in env_config.items()))
|
||||||
|
log.debug("Wrote UI environment file {!r}", str(dot_env_file))
|
||||||
|
|
||||||
generate_opengraph(
|
# Initiate Next.JS export process.
|
||||||
params.web.opengraph.image,
|
if any((not dev_mode, force, full)):
|
||||||
1200,
|
log.info("Starting UI build")
|
||||||
630,
|
initialize_result = await node_initial(timeout, dev_mode)
|
||||||
images_dir,
|
build_result = await build_ui(app_path=app_path)
|
||||||
params.web.theme.colors.black,
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as err:
|
if initialize_result:
|
||||||
log.error(err)
|
log.debug(initialize_result)
|
||||||
raise RuntimeError(str(err)) from None
|
elif initialize_result == "":
|
||||||
|
log.debug("Re-initialized node_modules")
|
||||||
|
|
||||||
|
if build_result:
|
||||||
|
log.success("Completed UI build")
|
||||||
|
elif dev_mode and not force:
|
||||||
|
log.debug("Running in developer mode, did not build new UI files")
|
||||||
|
|
||||||
|
migrate_images(app_path, params)
|
||||||
|
|
||||||
|
generate_opengraph(
|
||||||
|
params.web.opengraph.image,
|
||||||
|
1200,
|
||||||
|
630,
|
||||||
|
images_dir,
|
||||||
|
params.web.theme.colors.black,
|
||||||
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
|
||||||
50
hyperglass/util/tests/test_files.py
Normal file
50
hyperglass/util/tests/test_files.py
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
"""Test file-related utilities."""
|
||||||
|
|
||||||
|
# Standard Library
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Third Party
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
# Local
|
||||||
|
from ..files import dotenv_to_dict
|
||||||
|
|
||||||
|
ENV_TEST = """KEY1=VALUE1
|
||||||
|
KEY2=VALUE2
|
||||||
|
KEY3=VALUE3
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def test_dotenv_to_dict_string():
|
||||||
|
result = dotenv_to_dict(ENV_TEST)
|
||||||
|
assert result.get("KEY1") == "VALUE1"
|
||||||
|
assert result.get("KEY2") == "VALUE2"
|
||||||
|
assert result.get("KEY3") == "VALUE3"
|
||||||
|
|
||||||
|
|
||||||
|
def test_dotenv_to_dict_file():
|
||||||
|
_, filename = tempfile.mkstemp()
|
||||||
|
file = Path(filename)
|
||||||
|
with file.open("w+") as f:
|
||||||
|
f.write(ENV_TEST)
|
||||||
|
result = dotenv_to_dict(file)
|
||||||
|
assert result.get("KEY1") == "VALUE1"
|
||||||
|
assert result.get("KEY2") == "VALUE2"
|
||||||
|
assert result.get("KEY3") == "VALUE3"
|
||||||
|
file.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
def test_dotenv_to_dict_raises_type_error():
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
dotenv_to_dict(True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_dotenv_to_dict_raises_filenotfounderror():
|
||||||
|
with pytest.raises(FileNotFoundError):
|
||||||
|
dotenv_to_dict(Path("/tmp/not-a-thing")) # noqa: S108
|
||||||
|
|
||||||
|
|
||||||
|
def test_dotenv_invalid_format():
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
dotenv_to_dict("this should raise an error")
|
||||||
Loading…
Add table
Reference in a new issue