forked from mirrors/thatmattlove-hyperglass
Implement client-side UI config fetching
This commit is contained in:
parent
d1dae9d6ba
commit
070a07d065
21 changed files with 387 additions and 145 deletions
|
|
@ -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]:
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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<TFrame> = (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<HTMLDivElement>({} as HTMLDivElement);
|
||||
|
||||
|
|
@ -21,6 +24,15 @@ export const Frame: React.FC<TFrame> = (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 (
|
||||
<>
|
||||
<Flex
|
||||
|
|
|
|||
81
hyperglass/ui/components/loadError.tsx
Normal file
81
hyperglass/ui/components/loadError.tsx
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import {
|
||||
Box,
|
||||
Alert,
|
||||
Button,
|
||||
VStack,
|
||||
AlertIcon,
|
||||
AlertTitle,
|
||||
AlertDescription,
|
||||
} from '@chakra-ui/react';
|
||||
import { NoConfig } from './noconfig';
|
||||
|
||||
import type { CenterProps } from '@chakra-ui/react';
|
||||
import type { ConfigLoadError } from '~/util';
|
||||
|
||||
interface LoadErrorProps extends CenterProps {
|
||||
/**
|
||||
* Error thrown by `getHyperglassConfig` when any errors occur.
|
||||
*/
|
||||
error: ConfigLoadError;
|
||||
|
||||
/**
|
||||
* Callback to retry the config fetch.
|
||||
*/
|
||||
retry: () => 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 (
|
||||
<NoConfig {...rest}>
|
||||
<Alert
|
||||
status="error"
|
||||
height="200px"
|
||||
variant="subtle"
|
||||
borderRadius="lg"
|
||||
textAlign="center"
|
||||
alignItems="center"
|
||||
flexDirection="column"
|
||||
justifyContent="center"
|
||||
maxW={{ base: '95%', md: '80%', lg: '60%', xl: '40%' }}
|
||||
>
|
||||
<Box pos="absolute" right={0} top={0} m={4}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
colorScheme="red"
|
||||
isLoading={inProgress}
|
||||
onClick={() => retry()}
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
</Box>
|
||||
<AlertIcon boxSize={8} mr={0} />
|
||||
<VStack spacing={2}>
|
||||
<AlertTitle mt={4} mb={1} fontSize="xl">
|
||||
Error Loading Configuration
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
<span>{error.baseMessage}</span>
|
||||
<Box as="span" fontWeight="bold">
|
||||
{` ${error.url}`}
|
||||
</Box>
|
||||
</AlertDescription>
|
||||
{typeof error.detail !== 'undefined' && (
|
||||
<AlertDescription fontWeight="light">{error.detail}</AlertDescription>
|
||||
)}
|
||||
</VStack>
|
||||
</Alert>
|
||||
</NoConfig>
|
||||
);
|
||||
};
|
||||
|
|
@ -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'] = () => (
|
||||
<Flex flexDirection="column" minHeight="100vh" w="100%">
|
||||
<Flex
|
||||
px={2}
|
||||
py={0}
|
||||
w="100%"
|
||||
bg="white"
|
||||
color="black"
|
||||
flex="1 1 auto"
|
||||
textAlign="center"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
flexDirection="column"
|
||||
css={{
|
||||
'@media (prefers-color-scheme: dark)': { backgroundColor: 'black', color: 'white' },
|
||||
}}
|
||||
>
|
||||
<Spinner color="primary.500" w="6rem" h="6rem" />
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
export const Loading = (): JSX.Element => {
|
||||
return (
|
||||
<NoConfig color="#118ab2">
|
||||
<Spinner boxSize="8rem" />
|
||||
</NoConfig>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
28
hyperglass/ui/components/noconfig.tsx
Normal file
28
hyperglass/ui/components/noconfig.tsx
Normal file
|
|
@ -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 (
|
||||
<ChakraProvider theme={theme}>
|
||||
<Flex flexDirection="column" minHeight="100vh" w="100%">
|
||||
<Center flex="1 1 auto" {...props} />
|
||||
</Flex>
|
||||
</ChakraProvider>
|
||||
);
|
||||
};
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
61
hyperglass/ui/hooks/useHyperglassConfig.ts
Normal file
61
hyperglass/ui/hooks/useHyperglassConfig.ts
Normal file
|
|
@ -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<IConfig, ConfigLoadError> & {
|
||||
/**
|
||||
* 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 `<Loading/>` component. If it has failed, the `<LoadError/>` component
|
||||
// will be displayed, which will also show the loading state.
|
||||
const [initFailed, setInitFailed] = useState<boolean>(false);
|
||||
|
||||
const query = useQuery<IConfig, ConfigLoadError>({
|
||||
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 };
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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<IP> = AppProps & AppInitialProps & { appProps: IP };
|
||||
|
||||
type NextApp<IP> = React.FC<GetInitialPropsReturn<IP>> & {
|
||||
getInitialProps(c?: AppContext): Promise<{ appProps: IP }>;
|
||||
};
|
||||
|
||||
const App: NextApp<TApp> = (props: GetInitialPropsReturn<TApp>) => {
|
||||
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<TApp> = (props: GetInitialPropsReturn<TApp>) => {
|
|||
content="width=device-width, initial-scale=1, user-scalable=no, maximum-scale=1.0, minimum-scale=1.0"
|
||||
/>
|
||||
</Head>
|
||||
<HyperglassProvider config={config}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Component {...pageProps} />
|
||||
</HyperglassProvider>
|
||||
</QueryClientProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
App.getInitialProps = async function getInitialProps() {
|
||||
const config = process.env._HYPERGLASS_CONFIG_ as unknown as IConfig;
|
||||
return { appProps: { config } };
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import Document, { Html, Head, Main, NextScript } from 'next/document';
|
||||
|
||||
import type { DocumentContext, DocumentInitialProps } from 'next/document';
|
||||
|
||||
class MyDocument extends Document {
|
||||
|
|
|
|||
|
|
@ -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<Dict>(() => import('~/components').then(i => i.Layout), {
|
||||
loading: Loading,
|
||||
|
|
@ -13,8 +16,11 @@ interface TIndex {
|
|||
favicons: FaviconComponent[];
|
||||
}
|
||||
|
||||
const Index: React.FC<TIndex> = (props: TIndex) => {
|
||||
const Index = (props: TIndex): JSX.Element => {
|
||||
const { favicons } = props;
|
||||
const { data, error, isLoading, ready, refetch, showError, isLoadingInitial } =
|
||||
useHyperglassConfig();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
|
|
@ -23,23 +29,24 @@ const Index: React.FC<TIndex> = (props: TIndex) => {
|
|||
return <link rel={rel} href={href} type={type} key={idx} />;
|
||||
})}
|
||||
</Head>
|
||||
<Meta />
|
||||
<Layout />
|
||||
<If c={isLoadingInitial}>
|
||||
<Loading />
|
||||
</If>
|
||||
<If c={showError}>
|
||||
<LoadError error={error!} retry={refetch} inProgress={isLoading} />
|
||||
</If>
|
||||
<If c={ready}>
|
||||
<HyperglassProvider config={data!}>
|
||||
<Meta />
|
||||
<Layout />
|
||||
</HyperglassProvider>
|
||||
</If>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const getStaticProps: GetStaticProps<TIndex> = 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 },
|
||||
};
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"types/globals.d.ts",
|
||||
"types/*.d.ts",
|
||||
"next.config.js",
|
||||
"nextdev.js"
|
||||
]
|
||||
|
|
|
|||
9
hyperglass/ui/types/globals.d.ts
vendored
9
hyperglass/ui/types/globals.d.ts
vendored
|
|
@ -65,4 +65,13 @@ declare global {
|
|||
Omit<MotionProps, keyof T> & { transition?: MotionProps['transition'] };
|
||||
|
||||
type MeronexIcon = import('@meronex/icons').IconBaseProps;
|
||||
|
||||
type RequiredProps<T> = { [P in keyof T]-?: Exclude<T[P], undefined> };
|
||||
|
||||
declare namespace NodeJS {
|
||||
export interface ProcessEnv {
|
||||
hyperglass: { favicons: import('./config').Favicon[]; version: string };
|
||||
buildId: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
69
hyperglass/ui/util/config.ts
Normal file
69
hyperglass/ui/util/config.ts
Normal file
|
|
@ -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<IConfig> {
|
||||
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<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}` };
|
||||
});
|
||||
}
|
||||
|
|
@ -1,2 +1,3 @@
|
|||
export * from './common';
|
||||
export * from './config';
|
||||
export * from './theme';
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue