1
0
mirror of https://github.com/checktheroads/hyperglass synced 2024-05-11 05:55:08 +00:00

fix cache UI issue

This commit is contained in:
checktheroads
2020-04-18 12:09:16 -07:00
parent e9de3f4295
commit a00be434cf
6 changed files with 346 additions and 260 deletions

View File

@@ -139,7 +139,7 @@ class Text(HyperglassModel):
query_vrf: StrictStr = "Routing Table" query_vrf: StrictStr = "Routing Table"
fqdn_tooltip: StrictStr = "Use {protocol}" # Formatted by Javascript fqdn_tooltip: StrictStr = "Use {protocol}" # Formatted by Javascript
cache_prefix: StrictStr = "Results cached for " cache_prefix: StrictStr = "Results cached for "
cache_icon: StrictStr = "Cached Response from {time}" # Formatted by Javascript cache_icon: StrictStr = "Cached Response from {time} UTC" # Formatted by Javascript
complete_time: StrictStr = "Completed in {seconds}" # Formatted by Javascript complete_time: StrictStr = "Completed in {seconds}" # Formatted by Javascript
@validator("title_mode") @validator("title_mode")

View File

@@ -4,7 +4,7 @@
from datetime import datetime from datetime import datetime
__name__ = "hyperglass" __name__ = "hyperglass"
__version__ = "1.0.0-beta.26" __version__ = "1.0.0-beta.27"
__author__ = "Matt Love" __author__ = "Matt Love"
__copyright__ = f"Copyright {datetime.now().year} Matthew Love" __copyright__ = f"Copyright {datetime.now().year} Matthew Love"
__license__ = "BSD 3-Clause Clear License" __license__ = "BSD 3-Clause Clear License"

View File

@@ -1,16 +1,16 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { import {
AccordionItem, AccordionItem,
AccordionHeader, AccordionHeader,
AccordionPanel, AccordionPanel,
Alert, Alert,
Box, Box,
ButtonGroup, ButtonGroup,
css, css,
Flex, Flex,
Tooltip, Tooltip,
Text, Text,
useColorMode, useColorMode
} from "@chakra-ui/core"; } from "@chakra-ui/core";
import styled from "@emotion/styled"; import styled from "@emotion/styled";
import LightningBolt from "~/components/icons/LightningBolt"; import LightningBolt from "~/components/icons/LightningBolt";
@@ -28,230 +28,301 @@ import CacheTimeout from "~/components/CacheTimeout";
format.extend(String.prototype, {}); format.extend(String.prototype, {});
const FormattedError = ({ keywords, message }) => { const FormattedError = ({ keywords, message }) => {
const patternStr = keywords.map((kw) => `(${kw})`).join("|"); const patternStr = keywords.map(kw => `(${kw})`).join("|");
const pattern = new RegExp(patternStr, "gi"); const pattern = new RegExp(patternStr, "gi");
let errorFmt; let errorFmt;
try { try {
errorFmt = strReplace(message, pattern, (match) => ( errorFmt = strReplace(message, pattern, match => (
<Text key={match} as="strong"> <Text key={match} as="strong">
{match} {match}
</Text> </Text>
)); ));
} catch (err) { } catch (err) {
errorFmt = <Text as="span">{message}</Text>; errorFmt = <Text as="span">{message}</Text>;
} }
return <Text as="span">{keywords.length !== 0 ? errorFmt : message}</Text>; return <Text as="span">{keywords.length !== 0 ? errorFmt : message}</Text>;
}; };
const AccordionHeaderWrapper = styled(Flex)` const AccordionHeaderWrapper = styled(Flex)`
justify-content: space-between; justify-content: space-between;
&:hover { &:hover {
background-color: ${(props) => props.hoverBg}; background-color: ${props => props.hoverBg};
} }
&:focus { &:focus {
box-shadow: "outline"; box-shadow: "outline";
} }
`; `;
const statusMap = { success: "success", warning: "warning", error: "warning", danger: "error" }; const statusMap = {
success: "success",
warning: "warning",
error: "warning",
danger: "error"
};
const bg = { dark: "gray.800", light: "blackAlpha.100" }; const bg = { dark: "gray.800", light: "blackAlpha.100" };
const color = { dark: "white", light: "black" }; const color = { dark: "white", light: "black" };
const selectionBg = { dark: "white", light: "black" }; const selectionBg = { dark: "white", light: "black" };
const selectionColor = { dark: "black", light: "white" }; const selectionColor = { dark: "black", light: "white" };
const Result = React.forwardRef( const Result = React.forwardRef(
( (
{ {
device, device,
timeout, timeout,
queryLocation, queryLocation,
queryType, queryType,
queryVrf, queryVrf,
queryTarget, queryTarget,
index, index,
resultsComplete, resultsComplete,
setComplete, setComplete
}, },
ref ref
) => { ) => {
const config = useConfig(); const config = useConfig();
const { isSm } = useMedia(); const { isSm } = useMedia();
const { colorMode } = useColorMode(); const { colorMode } = useColorMode();
const [{ data, loading, error }, refetch] = useAxios({ const [{ data, loading, error }, refetch] = useAxios({
url: "/api/query/", url: "/api/query/",
method: "post", method: "post",
data: { data: {
query_location: queryLocation, query_location: queryLocation,
query_type: queryType, query_type: queryType,
query_vrf: queryVrf, query_vrf: queryVrf,
query_target: queryTarget, query_target: queryTarget
}, },
timeout: timeout, timeout: timeout,
useCache: false, useCache: false
}); });
const [isOpen, setOpen] = useState(false); const [isOpen, setOpen] = useState(false);
const [hasOverride, setOverride] = useState(false); const [hasOverride, setOverride] = useState(false);
const handleToggle = () => { const handleToggle = () => {
setOpen(!isOpen); setOpen(!isOpen);
setOverride(true); setOverride(true);
}; };
const cleanOutput = const cleanOutput =
data && data &&
data.output data.output
.split("\\n") .split("\\n")
.join("\n") .join("\n")
.replace(/\n\n/g, "\n"); .replace(/\n\n/g, "\n");
const errorKw = (error && error.response?.data?.keywords) || []; const errorKw = (error && error.response?.data?.keywords) || [];
let errorMsg; let errorMsg;
if (error && error.response?.data?.output) { if (error && error.response?.data?.output) {
errorMsg = error.response.data.output; errorMsg = error.response.data.output;
} else if (error && error.message.startsWith("timeout")) { } else if (error && error.message.startsWith("timeout")) {
errorMsg = config.messages.request_timeout; errorMsg = config.messages.request_timeout;
} else if (error?.response?.statusText) { } else if (error?.response?.statusText) {
errorMsg = startCase(error.response.statusText); errorMsg = startCase(error.response.statusText);
} else if (error && error.message) { } else if (error && error.message) {
errorMsg = startCase(error.message); errorMsg = startCase(error.message);
} else { } else {
errorMsg = config.messages.general; errorMsg = config.messages.general;
}
error && console.dir(error);
const errorLevel =
(error?.response?.data?.level && statusMap[error.response?.data?.level]) ?? "error";
const cacheLg = (
<>
<CacheTimeout timeout={config.cache.timeout} text={config.web.text.cache_prefix} />
{data?.cached && (
<Tooltip
hasArrow
label={config.web.text.cache_icon.format({ time: data?.timestamp })}
placement="top"
>
<Box ml={1}>
<LightningBolt color={color[colorMode]} />
</Box>
</Tooltip>
)}
</>
);
const cacheSm = (
<>
{data?.cached && (
<Tooltip
hasArrow
label={config.web.text.cache_icon.format({ time: data?.timestamp })}
placement="top"
>
<Box mr={1}>
<LightningBolt color={color[colorMode]} />
</Box>
</Tooltip>
)}
<CacheTimeout timeout={config.cache.timeout} text={config.web.text.cache_prefix} />
</>
);
const cacheData = isSm ? cacheSm : cacheLg;
useEffect(() => {
!loading && resultsComplete === null && setComplete(index);
}, [loading, resultsComplete]);
useEffect(() => {
resultsComplete === index && !hasOverride && setOpen(true);
}, [resultsComplete, index]);
return (
<AccordionItem
isOpen={isOpen}
isDisabled={loading}
ref={ref}
css={css({
"&:last-of-type": { borderBottom: "none" },
"&:first-of-type": { borderTop: "none" },
})}
>
<AccordionHeaderWrapper hoverBg="blackAlpha.50">
<AccordionHeader
flex="1 0 auto"
py={2}
_hover={{}}
_focus={{}}
w="unset"
onClick={handleToggle}
>
<ResultHeader
title={device.display_name}
loading={loading}
error={error}
errorMsg={errorMsg}
errorLevel={errorLevel}
runtime={data?.runtime}
/>
</AccordionHeader>
<ButtonGroup px={3} py={2}>
<CopyButton copyValue={cleanOutput} variant="ghost" isDisabled={loading} />
<RequeryButton requery={refetch} variant="ghost" isDisabled={loading} />
</ButtonGroup>
</AccordionHeaderWrapper>
<AccordionPanel
pb={4}
overflowX="auto"
css={css({ WebkitOverflowScrolling: "touch" })}
>
<Flex direction="row" flexWrap="wrap">
<Flex direction="column" flex="1 0 auto" maxW={error ? "100%" : null}>
{data && !error && (
<Box
fontFamily="mono"
mt={5}
mx={2}
p={3}
border="1px"
borderColor="inherit"
rounded="md"
bg={bg[colorMode]}
color={color[colorMode]}
fontSize="sm"
whiteSpace="pre-wrap"
as="pre"
css={css({
"&::selection": {
backgroundColor: selectionBg[colorMode],
color: selectionColor[colorMode],
},
})}
>
{cleanOutput}
</Box>
)}
{error && (
<Alert rounded="lg" my={2} py={4} status={errorLevel}>
<FormattedError keywords={errorKw} message={errorMsg} />
</Alert>
)}
</Flex>
</Flex>
<Flex direction="row" flexWrap="wrap">
<Flex
px={3}
mt={2}
justifyContent={["flex-start", "flex-start", "flex-end", "flex-end"]}
flex="1 0 auto"
>
{data && !error && config.cache.show_text && cacheData}
</Flex>
</Flex>
</AccordionPanel>
</AccordionItem>
);
} }
error && console.dir(error);
const errorLevel =
(error?.response?.data?.level &&
statusMap[error.response?.data?.level]) ??
"error";
const cacheLg = (
<>
<CacheTimeout
timeout={config.cache.timeout}
text={config.web.text.cache_prefix}
/>
{data?.cached && (
<Tooltip
hasArrow
label={config.web.text.cache_icon.format({ time: data?.timestamp })}
placement="top"
>
<Box ml={1}>
<LightningBolt color={color[colorMode]} />
</Box>
</Tooltip>
)}
</>
);
const cacheSm = (
<>
{data?.cached && (
<Tooltip
hasArrow
label={config.web.text.cache_icon.format({ time: data?.timestamp })}
placement="top"
>
<Box mr={1}>
<LightningBolt color={color[colorMode]} />
</Box>
</Tooltip>
)}
<CacheTimeout
timeout={config.cache.timeout}
text={config.web.text.cache_prefix}
/>
</>
);
const cacheData = isSm ? cacheSm : cacheLg;
useEffect(() => {
!loading && resultsComplete === null && setComplete(index);
}, [loading, resultsComplete]);
useEffect(() => {
resultsComplete === index && !hasOverride && setOpen(true);
}, [resultsComplete, index]);
useEffect(() => {
data && console.log(data);
});
return (
<AccordionItem
isOpen={isOpen}
isDisabled={loading}
ref={ref}
css={css({
"&:last-of-type": { borderBottom: "none" },
"&:first-of-type": { borderTop: "none" }
})}
>
<AccordionHeaderWrapper hoverBg="blackAlpha.50">
<AccordionHeader
flex="1 0 auto"
py={2}
_hover={{}}
_focus={{}}
w="unset"
onClick={handleToggle}
>
<ResultHeader
title={device.display_name}
loading={loading}
error={error}
errorMsg={errorMsg}
errorLevel={errorLevel}
runtime={data?.runtime}
/>
</AccordionHeader>
<ButtonGroup px={3} py={2}>
<CopyButton
copyValue={cleanOutput}
variant="ghost"
isDisabled={loading}
/>
<RequeryButton
requery={refetch}
variant="ghost"
isDisabled={loading}
/>
</ButtonGroup>
</AccordionHeaderWrapper>
<AccordionPanel
pb={4}
overflowX="auto"
css={css({ WebkitOverflowScrolling: "touch" })}
>
<Flex direction="row" flexWrap="wrap">
<Flex
direction="column"
flex="1 0 auto"
maxW={error ? "100%" : null}
>
{data && !error && (
<Box
fontFamily="mono"
mt={5}
mx={2}
p={3}
border="1px"
borderColor="inherit"
rounded="md"
bg={bg[colorMode]}
color={color[colorMode]}
fontSize="sm"
whiteSpace="pre-wrap"
as="pre"
css={css({
"&::selection": {
backgroundColor: selectionBg[colorMode],
color: selectionColor[colorMode]
}
})}
>
{cleanOutput}
</Box>
)}
{error && (
<Alert rounded="lg" my={2} py={4} status={errorLevel}>
<FormattedError keywords={errorKw} message={errorMsg} />
</Alert>
)}
</Flex>
</Flex>
<Flex direction="row" flexWrap="wrap">
<Flex
px={3}
mt={2}
justifyContent={[
"flex-start",
"flex-start",
"flex-end",
"flex-end"
]}
flex="1 0 auto"
>
{data && !error && config.cache.show_text && isSm ? (
<>
<Tooltip
display={data?.cached ? null : "none"}
hasArrow
label={config.web.text.cache_icon.format({
time: data?.timestamp
})}
placement="top"
>
<Box mr={1} display={data?.cached ? null : "none"}>
<LightningBolt color={color[colorMode]} />
</Box>
</Tooltip>
<CacheTimeout
timeout={config.cache.timeout}
text={config.web.text.cache_prefix}
/>
</>
) : data && !error && config.cache.show_text ? (
<>
<CacheTimeout
timeout={config.cache.timeout}
text={config.web.text.cache_prefix}
/>
<Tooltip
display={data?.cached ? null : "none"}
hasArrow
label={config.web.text.cache_icon.format({
time: data?.timestamp
})}
placement="top"
>
<Box ml={1} display={data?.cached ? null : "none"}>
<LightningBolt color={color[colorMode]} />
</Box>
</Tooltip>
</>
) : null}
</Flex>
</Flex>
</AccordionPanel>
</AccordionItem>
);
}
); );
Result.displayName = "HyperglassQueryResult"; Result.displayName = "HyperglassQueryResult";

View File

@@ -1,55 +1,70 @@
import React from "react"; import React from "react";
import { AccordionIcon, Icon, Spinner, Stack, Text, Tooltip, useColorMode } from "@chakra-ui/core"; import {
AccordionIcon,
Icon,
Spinner,
Stack,
Text,
Tooltip,
useColorMode
} from "@chakra-ui/core";
import format from "string-format"; import format from "string-format";
import useConfig from "~/components/HyperglassProvider"; import useConfig from "~/components/HyperglassProvider";
format.extend(String.prototype, {}); format.extend(String.prototype, {});
const runtimeText = (runtime, text) => { const runtimeText = (runtime, text) => {
let unit; let unit;
if (runtime === 1) { if (runtime === 1) {
unit = "seconds"; unit = "second";
} else { } else {
unit = "second"; unit = "seconds";
} }
const fmt = text.format({ seconds: runtime }); const fmt = text.format({ seconds: runtime });
return `${fmt} ${unit}`; return `${fmt} ${unit}`;
}; };
const statusColor = { dark: "primary.300", light: "primary.500" }; const statusColor = { dark: "primary.300", light: "primary.500" };
const warningColor = { dark: 300, light: 500 }; const warningColor = { dark: 300, light: 500 };
const defaultStatusColor = { const defaultStatusColor = {
dark: "success.300", dark: "success.300",
light: "success.500", light: "success.500"
}; };
export default React.forwardRef(({ title, loading, error, errorMsg, errorLevel, runtime }, ref) => { export default React.forwardRef(
({ title, loading, error, errorMsg, errorLevel, runtime }, ref) => {
const { colorMode } = useColorMode(); const { colorMode } = useColorMode();
const config = useConfig(); const config = useConfig();
return ( return (
<Stack ref={ref} isInline alignItems="center" w="100%"> <Stack ref={ref} isInline alignItems="center" w="100%">
{loading ? ( {loading ? (
<Spinner size="sm" mr={4} color={statusColor[colorMode]} /> <Spinner size="sm" mr={4} color={statusColor[colorMode]} />
) : error ? ( ) : error ? (
<Tooltip hasArrow label={errorMsg} placement="top"> <Tooltip hasArrow label={errorMsg} placement="top">
<Icon <Icon
name="warning" name="warning"
color={`${errorLevel}.${warningColor[colorMode]}`} color={`${errorLevel}.${warningColor[colorMode]}`}
mr={4} mr={4}
size={6} size={6}
/> />
</Tooltip> </Tooltip>
) : ( ) : (
<Tooltip <Tooltip
hasArrow hasArrow
label={runtimeText(runtime, config.web.text.complete_time)} label={runtimeText(runtime, config.web.text.complete_time)}
placement="top" placement="top"
> >
<Icon name="check" color={defaultStatusColor[colorMode]} mr={4} size={6} /> <Icon
</Tooltip> name="check"
)} color={defaultStatusColor[colorMode]}
<Text fontSize="lg">{title}</Text> mr={4}
<AccordionIcon ml="auto" /> size={6}
</Stack> />
</Tooltip>
)}
<Text fontSize="lg">{title}</Text>
<AccordionIcon ml="auto" />
</Stack>
); );
}); }
);

View File

@@ -2,7 +2,7 @@
set -e set -e
HYPERGLASS_VERSION="1.0.0b26" HYPERGLASS_VERSION="1.0.0b27"
MIN_PYTHON_MAJOR="3" MIN_PYTHON_MAJOR="3"
MIN_PYTHON_MINOR="6" MIN_PYTHON_MINOR="6"

View File

@@ -4,7 +4,7 @@ build-backend = "poetry.masonry.api"
[tool.poetry] [tool.poetry]
name = "hyperglass" name = "hyperglass"
version = "1.0.0-beta.26" version = "1.0.0-beta.27"
description = "hyperglass is the modern network looking glass that tries to make the internet better." description = "hyperglass is the modern network looking glass that tries to make the internet better."
authors = ["Matt Love <matt@hyperglass.io>"] authors = ["Matt Love <matt@hyperglass.io>"]
readme = "README.md" readme = "README.md"