From 070a07d0659ef3fbf90834c8106cd5b9ce9e59a9 Mon Sep 17 00:00:00 2001 From: thatmattlove Date: Thu, 9 Sep 2021 17:59:26 -0700 Subject: [PATCH] Implement client-side UI config fetching --- hyperglass/api/__init__.py | 10 ++- hyperglass/api/routes.py | 9 ++- hyperglass/cli/util.py | 4 +- hyperglass/ui/components/index.ts | 1 + hyperglass/ui/components/layout/frame.tsx | 20 ++++-- hyperglass/ui/components/loadError.tsx | 81 +++++++++++++++++++++ hyperglass/ui/components/loading.tsx | 33 +++------ hyperglass/ui/components/noconfig.tsx | 28 ++++++++ hyperglass/ui/hooks/index.ts | 1 + hyperglass/ui/hooks/useHyperglassConfig.ts | 61 ++++++++++++++++ hyperglass/ui/hooks/useLGState.ts | 8 ++- hyperglass/ui/next.config.js | 18 +++-- hyperglass/ui/nextdev.js | 20 +++--- hyperglass/ui/pages/_app.tsx | 36 ++-------- hyperglass/ui/pages/_document.tsx | 1 + hyperglass/ui/pages/index.tsx | 37 ++++++---- hyperglass/ui/tsconfig.json | 2 +- hyperglass/ui/types/globals.d.ts | 9 +++ hyperglass/ui/util/config.ts | 69 ++++++++++++++++++ hyperglass/ui/util/index.ts | 1 + hyperglass/util/frontend.py | 83 ++++++++++------------ 21 files changed, 387 insertions(+), 145 deletions(-) create mode 100644 hyperglass/ui/components/loadError.tsx create mode 100644 hyperglass/ui/components/noconfig.tsx create mode 100644 hyperglass/ui/hooks/useHyperglassConfig.ts create mode 100644 hyperglass/ui/util/config.ts diff --git a/hyperglass/api/__init__.py b/hyperglass/api/__init__.py index 1b00832..a2539ca 100644 --- a/hyperglass/api/__init__.py +++ b/hyperglass/api/__init__.py @@ -26,6 +26,7 @@ from hyperglass.api.routes import ( queries, routers, communities, + ui_props, import_certificate, ) from hyperglass.exceptions import HyperglassError @@ -172,7 +173,7 @@ def _custom_openapi(): CORS_ORIGINS = params.cors_origins.copy() if params.developer_mode: - CORS_ORIGINS.append(URL_DEV) + CORS_ORIGINS = [*CORS_ORIGINS, URL_DEV, "http://localhost:3000"] # CORS Configuration app.add_middleware( @@ -240,6 +241,13 @@ app.add_api_route( response_class=JSONResponse, ) +app.add_api_route( + path="/ui/props/", + endpoint=ui_props, + methods=["GET", "OPTIONS"], + response_class=JSONResponse, +) + # Enable certificate import route only if a device using # hyperglass-agent is defined. if [n for n in devices.all_nos if n in TRANSPORT_REST]: diff --git a/hyperglass/api/routes.py b/hyperglass/api/routes.py index d070ffb..6065efd 100644 --- a/hyperglass/api/routes.py +++ b/hyperglass/api/routes.py @@ -20,7 +20,7 @@ from hyperglass.api.tasks import process_headers, import_public_key from hyperglass.constants import __version__ from hyperglass.exceptions import HyperglassError from hyperglass.models.api import Query, EncodedRequest -from hyperglass.configuration import REDIS_CONFIG, params, devices +from hyperglass.configuration import REDIS_CONFIG, params, devices, frontend_params from hyperglass.execution.main import execute # Local @@ -264,4 +264,9 @@ async def info(): } -endpoints = [query, docs, routers, info] +async def ui_props(): + """Serve UI configration.""" + return frontend_params + + +endpoints = [query, docs, routers, info, ui_props] diff --git a/hyperglass/cli/util.py b/hyperglass/cli/util.py index 1a6759a..776b72b 100644 --- a/hyperglass/cli/util.py +++ b/hyperglass/cli/util.py @@ -65,7 +65,7 @@ def build_ui(timeout: int) -> None: """Create a new UI build.""" try: # Project - from hyperglass.configuration import CONFIG_PATH, params, frontend_params + from hyperglass.configuration import CONFIG_PATH, params from hyperglass.util.frontend import build_frontend from hyperglass.compat._asyncio import aiorun except ImportError as e: @@ -84,7 +84,7 @@ def build_ui(timeout: int) -> None: dev_mode=params.developer_mode, dev_url=f"http://localhost:{str(params.listen_port)}/", prod_url="/api/", - params=frontend_params, + params=params.export_dict(), force=True, app_path=CONFIG_PATH, ) diff --git a/hyperglass/ui/components/index.ts b/hyperglass/ui/components/index.ts index 17216b1..12cd0be 100644 --- a/hyperglass/ui/components/index.ts +++ b/hyperglass/ui/components/index.ts @@ -9,6 +9,7 @@ export * from './header'; export * from './help'; export * from './label'; export * from './layout'; +export * from './loadError'; export * from './loading'; export * from './lookingGlass'; export * from './markdown'; diff --git a/hyperglass/ui/components/layout/frame.tsx b/hyperglass/ui/components/layout/frame.tsx index d84f7f9..799612a 100644 --- a/hyperglass/ui/components/layout/frame.tsx +++ b/hyperglass/ui/components/layout/frame.tsx @@ -1,17 +1,20 @@ -import { useRef } from 'react'; +import { useEffect, useRef } from 'react'; +import { useRouter } from 'next/router'; import { Flex } from '@chakra-ui/react'; import { isSafari } from 'react-device-detect'; import { If, Debugger, Greeting, Footer, Header } from '~/components'; import { useConfig } from '~/context'; -import { useLGState, useLGMethods } from '~/hooks'; +import { useLGState, useLGMethods, useGoogleAnalytics } from '~/hooks'; import { ResetButton } from './resetButton'; import type { TFrame } from './types'; -export const Frame: React.FC = (props: TFrame) => { - const { developer_mode } = useConfig(); +export const Frame = (props: TFrame): JSX.Element => { + const router = useRouter(); + const { developer_mode, google_analytics } = useConfig(); const { isSubmitting } = useLGState(); const { resetForm } = useLGMethods(); + const { initialize, trackPage } = useGoogleAnalytics(); const containerRef = useRef({} as HTMLDivElement); @@ -21,6 +24,15 @@ export const Frame: React.FC = (props: TFrame) => { resetForm(); } + useEffect(() => { + initialize(google_analytics, developer_mode); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + trackPage(router.pathname); + }, [router.pathname, trackPage]); + return ( <> void; + + /** + * If `true`, the UI is currently retrying the config fetch. + */ + inProgress: boolean; +} + +/** + * Error component to be displayed when the hyperglass UI is unable to communicate with the + * hyperglass API to retrieve its configuration. + */ +export const LoadError = (props: LoadErrorProps): JSX.Element => { + const { error, retry, inProgress, ...rest } = props; + + return ( + + + + + + + + + Error Loading Configuration + + + {error.baseMessage} + + {` ${error.url}`} + + + {typeof error.detail !== 'undefined' && ( + {error.detail} + )} + + + + ); +}; diff --git a/hyperglass/ui/components/loading.tsx b/hyperglass/ui/components/loading.tsx index 44b3202..e3f117b 100644 --- a/hyperglass/ui/components/loading.tsx +++ b/hyperglass/ui/components/loading.tsx @@ -1,25 +1,10 @@ -import { Flex, Spinner } from '@chakra-ui/react'; +import { Spinner } from '@chakra-ui/react'; +import { NoConfig } from './noconfig'; -import type { LoadableBaseOptions } from 'next/dynamic'; - -export const Loading: LoadableBaseOptions['loading'] = () => ( - - - - - -); +export const Loading = (): JSX.Element => { + return ( + + + + ); +}; diff --git a/hyperglass/ui/components/noconfig.tsx b/hyperglass/ui/components/noconfig.tsx new file mode 100644 index 0000000..f7e729e --- /dev/null +++ b/hyperglass/ui/components/noconfig.tsx @@ -0,0 +1,28 @@ +import { Center, Flex, ChakraProvider, extendTheme } from '@chakra-ui/react'; +import { mode } from '@chakra-ui/theme-tools'; + +import type { CenterProps } from '@chakra-ui/react'; + +const theme = extendTheme({ + useSystemColorMode: true, + styles: { + global: props => ({ + html: { scrollBehavior: 'smooth', height: '-webkit-fill-available' }, + body: { + background: mode('white', 'black')(props), + color: mode('black', 'white')(props), + overflowX: 'hidden', + }, + }), + }, +}); + +export const NoConfig = (props: CenterProps): JSX.Element => { + return ( + + +
+ + + ); +}; diff --git a/hyperglass/ui/hooks/index.ts b/hyperglass/ui/hooks/index.ts index 2e4ab79..1cafd0d 100644 --- a/hyperglass/ui/hooks/index.ts +++ b/hyperglass/ui/hooks/index.ts @@ -5,6 +5,7 @@ export * from './useDirective'; export * from './useDNSQuery'; export * from './useGoogleAnalytics'; export * from './useGreeting'; +export * from './useHyperglassConfig'; export * from './useLGQuery'; export * from './useLGState'; export * from './useOpposingColor'; diff --git a/hyperglass/ui/hooks/useHyperglassConfig.ts b/hyperglass/ui/hooks/useHyperglassConfig.ts new file mode 100644 index 0000000..4515eeb --- /dev/null +++ b/hyperglass/ui/hooks/useHyperglassConfig.ts @@ -0,0 +1,61 @@ +import { useState } from 'react'; +import { useQuery } from 'react-query'; +import { getHyperglassConfig } from '~/util'; + +import type { UseQueryResult } from 'react-query'; +import type { ConfigLoadError } from '~/util'; +import type { IConfig } from '~/types'; + +type UseHyperglassConfig = UseQueryResult & { + /** + * Initial configuration load has failed. + */ + initFailed: boolean; + + /** + * If `true`, the initial loading component should be rendered. + */ + isLoadingInitial: boolean; + + /** + * If `true`, an error component should be rendered. + */ + showError: boolean; + + /** + * Data has been loaded and there are no errors. + */ + ready: boolean; +}; + +/** + * Retrieve and cache hyperglass's UI configuration from the hyperglass API. + */ +export function useHyperglassConfig(): UseHyperglassConfig { + // Track whether or not the initial configuration load has failed. If it has not (default), the + // UI will display the `` component. If it has failed, the `` component + // will be displayed, which will also show the loading state. + const [initFailed, setInitFailed] = useState(false); + + const query = useQuery({ + queryKey: 'hyperglass-ui-config', + queryFn: getHyperglassConfig, + refetchOnWindowFocus: false, + refetchInterval: 10000, + cacheTime: Infinity, + onError: () => { + if (!initFailed) { + setInitFailed(true); + } + }, + onSuccess: () => { + if (initFailed) { + setInitFailed(false); + } + }, + }); + const isLoadingInitial = !initFailed && query.isLoading && !query.isError; + const showError = query.isError || (initFailed && query.isLoading); + const ready = query.isSuccess && !query.isLoading; + return { initFailed, isLoadingInitial, showError, ready, ...query }; +} diff --git a/hyperglass/ui/hooks/useLGState.ts b/hyperglass/ui/hooks/useLGState.ts index 7da5a4d..d6aac5b 100644 --- a/hyperglass/ui/hooks/useLGState.ts +++ b/hyperglass/ui/hooks/useLGState.ts @@ -96,7 +96,13 @@ class MethodsInstance { try { result = JSON.parse(JSON.stringify(obj)); } catch (err) { - console.error(err.message); + let error; + if (err instanceof Error) { + error = err.message; + } else { + error = String(err); + } + console.error(error); } return result; } diff --git a/hyperglass/ui/next.config.js b/hyperglass/ui/next.config.js index 3d2e103..68148fd 100644 --- a/hyperglass/ui/next.config.js +++ b/hyperglass/ui/next.config.js @@ -1,17 +1,15 @@ -const envVars = require('/tmp/hyperglass.env.json'); -const { configFile } = envVars; -const config = require(String(configFile)); +const envData = require('/tmp/hyperglass.env.json'); -module.exports = { +/** + * @type {import('next').NextConfig} + */ +const nextConfig = { reactStrictMode: true, poweredByHeader: false, - env: { - _NODE_ENV_: config.NODE_ENV, - _HYPERGLASS_URL_: config._HYPERGLASS_URL_, - _HYPERGLASS_CONFIG_: config._HYPERGLASS_CONFIG_, - _HYPERGLASS_FAVICONS_: config._HYPERGLASS_FAVICONS_, - }, + env: { ...envData }, typescript: { ignoreBuildErrors: true, }, }; + +module.exports = nextConfig; diff --git a/hyperglass/ui/nextdev.js b/hyperglass/ui/nextdev.js index 0facba9..4acc93f 100644 --- a/hyperglass/ui/nextdev.js +++ b/hyperglass/ui/nextdev.js @@ -2,20 +2,22 @@ const express = require('express'); const proxyMiddleware = require('http-proxy-middleware'); const next = require('next'); -const envVars = require('/tmp/hyperglass.env.json'); -const { configFile } = envVars; -const config = require(String(configFile)); +const config = require('/tmp/hyperglass.env.json'); -const { NODE_ENV: env, _HYPERGLASS_URL_: envUrl } = config; +const { + env: { NODE_ENV }, + hyperglass: { url }, +} = config; const devProxy = { - '/api/query/': { target: envUrl + 'api/query/', pathRewrite: { '^/api/query/': '' } }, - '/images': { target: envUrl + 'images', pathRewrite: { '^/images': '' } }, - '/custom': { target: envUrl + 'custom', pathRewrite: { '^/custom': '' } }, + '/api/query/': { target: url + 'api/query/', pathRewrite: { '^/api/query/': '' } }, + '/ui/props/': { target: url + 'ui/props/', pathRewrite: { '^/ui/props/': '' } }, + '/images': { target: url + 'images', pathRewrite: { '^/images': '' } }, + '/custom': { target: url + 'custom', pathRewrite: { '^/custom': '' } }, }; const port = parseInt(process.env.PORT, 10) || 3000; -const dev = env !== 'production'; +const dev = NODE_ENV !== 'production'; const app = next({ dir: '.', // base directory where everything is, could move to src later dev, @@ -43,7 +45,7 @@ app if (err) { throw err; } - console.log(`> Ready on port ${port} [${env}]`); + console.log(`> Ready on port ${port} [${NODE_ENV}]`); }); }) .catch(err => { diff --git a/hyperglass/ui/pages/_app.tsx b/hyperglass/ui/pages/_app.tsx index 7c6eff9..00fc54d 100644 --- a/hyperglass/ui/pages/_app.tsx +++ b/hyperglass/ui/pages/_app.tsx @@ -1,33 +1,16 @@ -import { useEffect } from 'react'; import Head from 'next/head'; -import { HyperglassProvider } from '~/context'; -import { useGoogleAnalytics } from '~/hooks'; -import { IConfig } from '~/types'; +import { QueryClient, QueryClientProvider } from 'react-query'; -import type { AppProps, AppInitialProps, AppContext } from 'next/app'; +import type { AppProps } from 'next/app'; if (process.env.NODE_ENV === 'development') { require('@hookstate/devtools'); } -type TApp = { config: IConfig }; +const queryClient = new QueryClient(); -type GetInitialPropsReturn = AppProps & AppInitialProps & { appProps: IP }; - -type NextApp = React.FC> & { - getInitialProps(c?: AppContext): Promise<{ appProps: IP }>; -}; - -const App: NextApp = (props: GetInitialPropsReturn) => { - const { Component, pageProps, appProps, router } = props; - const { config } = appProps; - const { initialize, trackPage } = useGoogleAnalytics(); - - initialize(config.google_analytics, config.developer_mode); - - useEffect(() => { - router.events.on('routeChangeComplete', trackPage); - }, [router.events, trackPage]); +const App = (props: AppProps): JSX.Element => { + const { Component, pageProps } = props; return ( <> @@ -44,16 +27,11 @@ const App: NextApp = (props: GetInitialPropsReturn) => { content="width=device-width, initial-scale=1, user-scalable=no, maximum-scale=1.0, minimum-scale=1.0" /> - + - + ); }; -App.getInitialProps = async function getInitialProps() { - const config = process.env._HYPERGLASS_CONFIG_ as unknown as IConfig; - return { appProps: { config } }; -}; - export default App; diff --git a/hyperglass/ui/pages/_document.tsx b/hyperglass/ui/pages/_document.tsx index f54a823..3f517c9 100644 --- a/hyperglass/ui/pages/_document.tsx +++ b/hyperglass/ui/pages/_document.tsx @@ -1,4 +1,5 @@ import Document, { Html, Head, Main, NextScript } from 'next/document'; + import type { DocumentContext, DocumentInitialProps } from 'next/document'; class MyDocument extends Document { diff --git a/hyperglass/ui/pages/index.tsx b/hyperglass/ui/pages/index.tsx index de247e7..b16e7e2 100644 --- a/hyperglass/ui/pages/index.tsx +++ b/hyperglass/ui/pages/index.tsx @@ -1,9 +1,12 @@ import Head from 'next/head'; import dynamic from 'next/dynamic'; -import { Meta, Loading } from '~/components'; +import { Meta, Loading, If, LoadError } from '~/components'; +import { HyperglassProvider } from '~/context'; +import { useHyperglassConfig } from '~/hooks'; +import { getFavicons } from '~/util'; import type { GetStaticProps } from 'next'; -import type { Favicon, FaviconComponent } from '~/types'; +import type { FaviconComponent } from '~/types'; const Layout = dynamic(() => import('~/components').then(i => i.Layout), { loading: Loading, @@ -13,8 +16,11 @@ interface TIndex { favicons: FaviconComponent[]; } -const Index: React.FC = (props: TIndex) => { +const Index = (props: TIndex): JSX.Element => { const { favicons } = props; + const { data, error, isLoading, ready, refetch, showError, isLoadingInitial } = + useHyperglassConfig(); + return ( <> @@ -23,23 +29,24 @@ const Index: React.FC = (props: TIndex) => { return ; })} - - + + + + + + + + + + + + ); }; export const getStaticProps: GetStaticProps = async () => { - const faviconConfig = process.env._HYPERGLASS_FAVICONS_ as unknown as Favicon[]; - const favicons = faviconConfig.map(icon => { - const { image_format, dimensions, prefix } = icon; - let { rel } = icon; - if (rel === null) { - rel = ''; - } - const src = `/images/favicons/${prefix}-${dimensions[0]}x${dimensions[1]}.${image_format}`; - return { rel, href: src, type: `image/${image_format}` }; - }); + const favicons = await getFavicons(); return { props: { favicons }, }; diff --git a/hyperglass/ui/tsconfig.json b/hyperglass/ui/tsconfig.json index 8d3a05c..a04d0f5 100644 --- a/hyperglass/ui/tsconfig.json +++ b/hyperglass/ui/tsconfig.json @@ -35,7 +35,7 @@ "next-env.d.ts", "**/*.ts", "**/*.tsx", - "types/globals.d.ts", + "types/*.d.ts", "next.config.js", "nextdev.js" ] diff --git a/hyperglass/ui/types/globals.d.ts b/hyperglass/ui/types/globals.d.ts index e6dc9cd..c8d1eb7 100644 --- a/hyperglass/ui/types/globals.d.ts +++ b/hyperglass/ui/types/globals.d.ts @@ -65,4 +65,13 @@ declare global { Omit & { transition?: MotionProps['transition'] }; type MeronexIcon = import('@meronex/icons').IconBaseProps; + + type RequiredProps = { [P in keyof T]-?: Exclude }; + + declare namespace NodeJS { + export interface ProcessEnv { + hyperglass: { favicons: import('./config').Favicon[]; version: string }; + buildId: string; + } + } } diff --git a/hyperglass/ui/util/config.ts b/hyperglass/ui/util/config.ts new file mode 100644 index 0000000..9b11b39 --- /dev/null +++ b/hyperglass/ui/util/config.ts @@ -0,0 +1,69 @@ +import { isObject } from '~/types'; + +import type { IConfig, FaviconComponent } from '~/types'; + +export class ConfigLoadError extends Error { + public url: string = '/ui/props/'; + public detail?: string; + public baseMessage: string; + + constructor(detail?: string) { + super(); + this.detail = detail; + this.baseMessage = `Unable to connect to hyperglass at`; + this.message = `${this.baseMessage} '${this.url}'`; + console.error(this); + } + + public toString(): string { + if (typeof this.detail !== 'undefined') { + return `${this.message} (${this.detail})`; + } + return this.message; + } +} + +export async function getHyperglassConfig(): Promise { + let mode: RequestInit['mode']; + + if (process.env.NODE_ENV === 'production') { + mode = 'same-origin'; + } else if (process.env.NODE_ENV === 'development') { + mode = 'cors'; + } + const options: RequestInit = { method: 'GET', mode, headers: { 'user-agent': 'hyperglass-ui' } }; + + try { + const response = await fetch('/ui/props/', options); + const data = await response.json(); + if (!response.ok) { + throw response; + } + if (isObject(data)) { + return data as IConfig; + } + } catch (error) { + if (error instanceof TypeError) { + throw new ConfigLoadError('Network Connection Error'); + } + if (error instanceof Response) { + throw new ConfigLoadError(`${error.status}: ${error.statusText}`); + } + if (error instanceof Error) { + throw new ConfigLoadError(); + } + throw new ConfigLoadError(String(error)); + } + throw new ConfigLoadError('Unknown Error'); +} + +export async function getFavicons(): Promise { + 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}` }; + }); +} diff --git a/hyperglass/ui/util/index.ts b/hyperglass/ui/util/index.ts index 6d81607..bcb6134 100644 --- a/hyperglass/ui/util/index.ts +++ b/hyperglass/ui/util/index.ts @@ -1,2 +1,3 @@ export * from './common'; +export * from './config'; export * from './theme'; diff --git a/hyperglass/util/frontend.py b/hyperglass/util/frontend.py index 1d2480a..e5cf382 100644 --- a/hyperglass/util/frontend.py +++ b/hyperglass/util/frontend.py @@ -245,6 +245,7 @@ async def build_frontend( # noqa: C901 app_path: Path, force: bool = False, timeout: int = 180, + full: bool = False, ) -> bool: """Perform full frontend UI build process. @@ -273,32 +274,35 @@ async def build_frontend( # noqa: C901 """ # Standard Library import hashlib - import tempfile # Third Party - from favicons import Favicons + from favicons import Favicons # type:ignore # Project from hyperglass.constants import __version__ + log.info("Starting UI build") + + # Create temporary file. json file extension is added for easy + # webpack JSON parsing. env_file = Path("/tmp/hyperglass.env.json") # noqa: S108 package_json = await read_package_json() env_vars = { - "_HYPERGLASS_CONFIG_": params, - "_HYPERGLASS_VERSION_": __version__, - "_HYPERGLASS_PACKAGE_JSON_": package_json, - "_HYPERGLASS_APP_PATH_": str(app_path), + "hyperglass": {"version": __version__}, + "env": {}, } # Set NextJS production/development mode and base URL based on # developer_mode setting. if dev_mode: - env_vars.update({"NODE_ENV": "development", "_HYPERGLASS_URL_": dev_url}) + env_vars["env"].update({"NODE_ENV": "development"}) + env_vars["hyperglass"].update({"url": dev_url}) else: - env_vars.update({"NODE_ENV": "production", "_HYPERGLASS_URL_": prod_url}) + env_vars["env"].update({"NODE_ENV": "production"}) + env_vars["hyperglass"].update({"url": prod_url}) # Check if hyperglass/ui/node_modules has been initialized. If not, # initialize it. @@ -328,64 +332,49 @@ async def build_frontend( # noqa: C901 ) as favicons: await favicons.generate() log.debug("Generated {} favicons", favicons.completed) - env_vars.update({"_HYPERGLASS_FAVICONS_": favicons.formats()}) - - env_json = json.dumps(env_vars, default=str) - + env_vars["hyperglass"].update({"favicons": favicons.formats()}) + build_data = { + "params": params, + "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(env_json.encode()).hexdigest() + build_id = hashlib.sha256(build_json.encode()).hexdigest() # Read hard-coded environment file from last build. If build ID # matches this build's ID, don't run a new build. if env_file.exists() and not force: - with env_file.open("r") as ef: ef_id = json.load(ef).get("buildId", "empty") - - log.debug("Previous Build ID: {id}", id=ef_id) + log.debug("Previous Build ID: {}", ef_id) if ef_id == build_id: - log.debug( "UI parameters unchanged since last build, skipping UI build..." ) - return True - # Create temporary file. json file extension is added for easy - # webpack JSON parsing. - temp_file = tempfile.NamedTemporaryFile( - mode="w+", prefix="hyperglass_", suffix=".json", delete=not dev_mode - ) + env_vars["buildId"] = build_id - log.info("Starting UI build...") - log.debug( - f"Created temporary UI config file: '{temp_file.name}' for build {build_id}" - ) + env_file.write_text(json.dumps(env_vars, default=str)) + log.debug("Wrote UI environment file '{}'", str(env_file)) - with Path(temp_file.name).open("w+") as temp: - temp.write(env_json) + # Initiate Next.JS export process. + if any((not dev_mode, force, full)): + initialize_result = await node_initial(timeout, dev_mode) + build_result = await build_ui(app_path=app_path) - # Write "permanent" file (hard-coded named) for Node to read. - env_file.write_text( - json.dumps({"configFile": temp_file.name, "buildId": build_id}) - ) + if initialize_result: + log.debug(initialize_result) + elif initialize_result == "": + log.debug("Re-initialized node_modules") - # While temporary file is still open, initiate UI build process. - if not dev_mode or force: - initialize_result = await node_initial(timeout, dev_mode) - build_result = await build_ui(app_path=app_path) - - if initialize_result: - log.debug(initialize_result) - 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") + 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)