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

improve cache handling

This commit is contained in:
checktheroads
2020-04-16 23:43:02 -07:00
parent fa38d49a8e
commit 400685304f
15 changed files with 210 additions and 355 deletions

View File

@@ -92,17 +92,19 @@ By default, no Opengraph image is set. If you define one with `image`, hyperglas
## `text`
| Parameter | Type | Default | Description |
| :--------------- | :----: | :------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `cache` | String | `'Results will be cached for {timeout} {period}.'` | User-facing text regarding the cache timeout. `{timeout}` and `{period}` are replaced with the a generated value derived from the length of the <Link to="/docs/configuration#global-settings"><Code>request_timeout</Code></Link> setting. |
| `fqdn_tooltip` | String | `'Use {protocol}'` | Text displayed when a user hovers over the IPv4 or IPv6 button on an FQDN target resolved by DNS. `{protocol}` is replaced with the relevant IP protocol. |
| `query_location` | String | `'Location'` | Query Location (router) form label. |
| `query_target` | String | `'Target'` | Query Target (IP/hostname/community/AS Path) form label. |
| `query_type` | String | `'Query Type'` | Query Type (BGP Route, Ping, Traceroute, etc.) form label. |
| `query_vrf` | String | `'Routing Table'` | Query VRF form label. |
| `subtitle` | String | `'Network Looking Glass'` | Subtitle text. value. |
| `title` | String | `'hyperglass'` | Title text. |
| `title_mode` | String | `'text_only'` | Set the title mode. <MiniNote>Must be <Code>text_only</Code>, <Code>logo_only</Code>, <Code>logo_subtitle</Code>, or <Code>all</Code></MiniNote> |
| Parameter | Type | Default | Description |
| :--------------- | :----: | :------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cache_prefix` | String | `'Results cached for '` | Text displayed with the cache timeout countdown. |
| `cache_icon` | String | `'Cached Response'` | Text displayed when a user hovers over the lightning bolt icon, which is displayed when a response from the server was a cached response. |
| `completed_time` | String | `'Completed in {seconds}'` | Text displayed when a user hovers over the success icon for a query result. `{seconds}` will be replaced with 'n seconds' where n is the time a query took to complete. |
| `fqdn_tooltip` | String | `'Use {protocol}'` | Text displayed when a user hovers over the IPv4 or IPv6 button on an FQDN target resolved by DNS. `{protocol}` is replaced with the relevant IP protocol. |
| `query_location` | String | `'Location'` | Query Location (router) form label. |
| `query_target` | String | `'Target'` | Query Target (IP/hostname/community/AS Path) form label. |
| `query_type` | String | `'Query Type'` | Query Type (BGP Route, Ping, Traceroute, etc.) form label. |
| `query_vrf` | String | `'Routing Table'` | Query VRF form label. |
| `subtitle` | String | `'Network Looking Glass'` | Subtitle text. value. |
| `title` | String | `'hyperglass'` | Title text. |
| `title_mode` | String | `'text_only'` | Set the title mode. <MiniNote>Must be <Code>text_only</Code>, <Code>logo_only</Code>, <Code>logo_subtitle</Code>, or <Code>all</Code></MiniNote> |
:::note Title Mode
The `title_mode` parameter behaves in the following manner:

View File

@@ -1,9 +1,9 @@
"""Response model."""
# Standard Library
from typing import List
from typing import List, Optional
# Third Party
from pydantic import BaseModel, StrictStr, StrictBool, constr
from pydantic import BaseModel, StrictInt, StrictStr, StrictBool, constr
# Project
from hyperglass.configuration import params
@@ -14,6 +14,7 @@ class QueryError(BaseModel):
output: StrictStr = params.messages.general
level: constr(regex=r"(success|warning|error|danger)") = "danger"
id: Optional[StrictStr]
keywords: List[StrictStr] = []
class Config:
@@ -56,6 +57,9 @@ class QueryResponse(BaseModel):
output: StrictStr
level: constr(regex=r"success") = "success"
id: StrictStr
cached: StrictBool
runtime: StrictInt
keywords: List[StrictStr] = []
class Config:

View File

@@ -71,8 +71,17 @@ async def query(query_data: Query, request: Request):
log.info(f"Starting query execution for query {query_data.summary}")
# Check if cached entry exists
if not await cache.get(cache_key):
cache_response = await cache.get(cache_key)
cached = False
if cache_response:
# If a cached response exists, reset the expiration time.
await cache.expire(cache_key, seconds=cache_timeout)
cached = True
runtime = 0
elif not cache_response:
log.debug(f"No existing cache entry for query {cache_key}")
log.debug(
f"Created new cache key {cache_key} entry for query {query_data.summary}"
@@ -94,13 +103,22 @@ async def query(query_data: Query, request: Request):
log.debug(f"Added cache entry for query: {cache_key}")
runtime = int(round(elapsedtime, 0))
# If it does, return the cached entry
cache_response = await cache.get(cache_key)
log.debug(f"Cache match for {cache_key}:\n {cache_response}")
log.success(f"Completed query execution for {query_data.summary}")
return {"output": cache_response, "level": "success", "keywords": []}
return {
"output": cache_response,
"id": cache_key,
"cached": cached,
"runtime": runtime,
"level": "success",
"keywords": [],
}
async def import_certificate(encoded_request: EncodedRequest):

View File

@@ -4,7 +4,6 @@
import os
import copy
import json
import math
from pathlib import Path
# Third Party
@@ -169,20 +168,6 @@ try:
**params.dict(exclude={"web", "queries", "messages"})
)
# Automatically derive the cache timeout period term
# (minutes, seconds) based on the cache timeout value.
if params.cache.timeout >= 60:
_cache_timeout = math.ceil(params.cache.timeout / 60)
_cache_period = "minutes"
elif params.cache.timeout < 60:
_cache_timeout = params.cache.timeout
_cache_period = "seconds"
# Format the cache display text to match the real values.
params.web.text.cache = params.web.text.cache.format(
timeout=_cache_timeout, period=_cache_period
)
# If keywords are unmodified (default), add the org name &
# site_title.
if _params.Params().site_keywords == params.site_keywords:
@@ -420,7 +405,7 @@ networks = _build_networks()
frontend_networks = _build_frontend_networks()
frontend_devices = _build_frontend_devices()
_frontend_fields = {
"cache": {"show_text"},
"cache": {"show_text", "timeout"},
"debug": ...,
"developer_mode": ...,
"primary_asn": ...,

View File

@@ -28,7 +28,7 @@ class Cache(HyperglassModel):
show_text: StrictBool = Field(
True,
title="Show Text",
description="Show the [cache](/fixme) text in the hyperglass UI.",
description="Show the cache text in the hyperglass UI.",
)
class Config:

View File

@@ -138,7 +138,9 @@ class Text(HyperglassModel):
query_target: StrictStr = "Target"
query_vrf: StrictStr = "Routing Table"
fqdn_tooltip: StrictStr = "Use {protocol}" # Formatted by Javascript
cache: StrictStr = "Results will be cached for {timeout} {period}."
cache_prefix: StrictStr = "Results cached for "
cache_icon: StrictStr = "Cached Response"
complete_time: StrictStr = "Completed in {seconds}" # Formatted by Javascript
@validator("title_mode")
def validate_title_mode(cls, value):
@@ -147,6 +149,11 @@ class Text(HyperglassModel):
value = "logo_subtitle"
return value
@validator("cache_prefix")
def validate_cache_prefix(cls, value):
"""Ensure trailing whitespace."""
return " ".join(value.split()) + " "
class ThemeColors(HyperglassModel):
"""Validation model for theme colors."""

View File

@@ -1,125 +0,0 @@
# arista:
# ipv4_default:
# bgp_aspath: show ip bgp regexp {target}
# bgp_community: show ip bgp community {target}
# bgp_route: show ip bgp {target}
# ping: ping ip {target} source {source}
# traceroute: traceroute ip {target} source {source}
# ipv4_vpn:
# bgp_aspath: show ip bgp regexp {target} vrf {vrf}
# bgp_community: show ip bgp community {target} vrf {vrf}
# bgp_route: show ip bgp {target} vrf {vrf}
# ping: ping vrf {vrf} ip {target} source {source}
# traceroute: traceroute vrf {vrf} ip {target} source {source}
# ipv6_default:
# bgp_aspath: show ipv6 bgp regexp {target}
# bgp_community: show ipv6 bgp community {target}
# bgp_route: show ipv6 bgp {target}
# ping: ping ipv6 {target} source {source}
# traceroute: traceroute ipv6 {target} source {source}
# ipv6_vpn:
# bgp_aspath: show ipv6 bgp regexp {target} vrf {vrf}
# bgp_community: show ipv6 bgp community {target} vrf {vrf}
# bgp_route: show ipv6 bgp {target} vrf {vrf}
# ping: ping vrf {vrf} ipv6 {target} source {source}
# traceroute: traceroute vrf {vrf} ipv6 {target} source {source}
# cisco_ios:
# ipv4_default:
# bgp_aspath: show bgp ipv4 unicast quote-regexp "{target}"
# bgp_community: show bgp ipv4 unicast community {target}
# bgp_route: show bgp ipv4 unicast {target} | exclude pathid:|Epoch
# ping: ping {target} repeat 5 source {source}
# traceroute: traceroute {target} timeout 1 probe 2 source {source}
# ipv4_vpn:
# bgp_aspath: show bgp vpnv4 unicast vrf {vrf} quote-regexp "{target}"
# bgp_community: show bgp vpnv4 unicast vrf {vrf} community {target}
# bgp_route: show bgp vpnv4 unicast vrf {vrf} {target}
# ping: ping vrf {vrf} {target} repeat 5 source {source}
# traceroute: traceroute vrf {vrf} {target} timeout 1 probe 2 source {source}
# ipv6_default:
# bgp_aspath: show bgp ipv6 unicast quote-regexp "{target}"
# bgp_community: show bgp ipv6 unicast community {target}
# bgp_route: show bgp ipv6 unicast {target} | exclude pathid:|Epoch
# ping: ping ipv6 {target} repeat 5 source {source}
# traceroute: traceroute ipv6 {target} timeout 1 probe 2 source {source}
# ipv6_vpn:
# bgp_aspath: show bgp vpnv6 unicast vrf {vrf} quote-regexp "{target}"
# bgp_community: show bgp vpnv6 unicast vrf {vrf} community {target}
# bgp_route: show bgp vpnv6 unicast vrf {vrf} {target}
# ping: ping vrf {vrf} {target} repeat 5 source {source}
# traceroute: traceroute vrf {vrf} {target} timeout 1 probe 2 source {source}
# cisco_xr:
# ipv4_default:
# bgp_aspath: show bgp ipv4 unicast regexp {target}
# bgp_community: show bgp ipv4 unicast community {target}
# bgp_route: show bgp ipv4 unicast {target}
# ping: ping ipv4 {target} count 5 source {source}
# traceroute: traceroute ipv4 {target} timeout 1 probe 2 source {source}
# ipv4_vpn:
# bgp_aspath: show bgp vpnv4 unicast vrf {vrf} regexp {target}
# bgp_community: show bgp vpnv4 unicast vrf {vrf} community {target}
# bgp_route: show bgp vpnv4 unicast vrf {vrf} {target}
# ping: ping vrf {vrf} {target} count 5 source {source}
# traceroute: traceroute vrf {vrf} {target} timeout 1 probe 2 source {source}
# ipv6_default:
# bgp_aspath: show bgp ipv6 unicast regexp {target}
# bgp_community: show bgp ipv6 unicast community {target}
# bgp_route: show bgp ipv6 unicast {target}
# ping: ping ipv6 {target} count 5 source {source}
# traceroute: traceroute ipv6 {target} timeout 1 probe 2 source {source}
# ipv6_vpn:
# bgp_aspath: show bgp vpnv6 unicast vrf {vrf} regexp {target}
# bgp_community: show bgp vpnv6 unicast vrf {vrf} community {target}
# bgp_route: show bgp vpnv6 unicast vrf {vrf} {target}
# ping: ping vrf {vrf} {target} count 5 source {source}
# traceroute: traceroute vrf {vrf} {target} timeout 1 probe 2 source {source}
# huawei:
# ipv4_default:
# bgp_aspath: display bgp routing-table regular-expression {target}
# bgp_community: display bgp routing-table regular-expression {target}
# bgp_route: display bgp routing-table {target}
# ping: ping -c 5 -a {source} {target}
# traceroute: tracert -q 2 -f 1 -a {source} {target}
# ipv4_vpn:
# bgp_aspath: display bgp vpnv4 vpn-instance {vrf} routing-table regular-expression {target}
# bgp_community: display bgp vpnv4 vpn-instance {vrf} routing-table regular-expression {target}
# bgp_route: display bgp vpnv4 vpn-instance {vrf} routing-table {target}
# ping: ping -vpn-instance {vrf} -c 5 -a {source} {target}
# traceroute: tracert -q 2 -f 1 -vpn-instance {vrf} -a {source} {target}
# ipv6_default:
# bgp_aspath: display bgp ipv6 routing-table regular-expression {target}
# bgp_community: display bgp ipv6 routing-table community {target}
# bgp_route: display bgp ipv6 routing-table {target}
# ping: ping ipv6 -c 5 -a {source} {target}
# traceroute: tracert ipv6 -q 2 -f 1 -a {source} {target}
# ipv6_vpn:
# bgp_aspath: display bgp vpnv6 vpn-instance {vrf} routing-table regular-expression {target}
# bgp_community: display bgp vpnv6 vpn-instance {vrf} routing-table regular-expression {target}
# bgp_route: display bgp vpnv6 vpn-instance {vrf} routing-table {target}
# ping: ping vpnv6 vpn-instance {vrf} -c 5 -a {source} {target}
# traceroute: tracert -q 2 -f 1 vpn-instance {vrf} -a {source} {target}
# juniper:
# ipv4_default:
# bgp_aspath: show route protocol bgp table inet.0 aspath-regex "{target}"
# bgp_community: show route protocol bgp table inet.0 community {target}
# bgp_route: show route protocol bgp table inet.0 {target} detail | except Label | except Label | except "Next hop type" | except Task | except Address | except "Session Id" | except State | except "Next-hop reference" | except destinations | except "Announcement bits"
# ping: ping inet {target} count 5 source {source}
# traceroute: traceroute inet {target} wait 1 source {source}
# ipv4_vpn:
# bgp_aspath: show route protocol bgp table {vrf}.inet.0 aspath-regex "{target}"
# bgp_community: show route protocol bgp table {vrf}.inet.0 community {target}
# bgp_route: show route protocol bgp table {vrf}.inet.0 {target} detail | except Label | except Label | except "Next hop type" | except Task | except Address | except "Session Id" | except State | except "Next-hop reference" | except destinations | except "Announcement bits"
# ping: ping inet routing-instance {vrf} {target} count 5 source {source}
# traceroute: traceroute inet routing-instance {vrf} {target} wait 1 source {source}
# ipv6_default:
# bgp_aspath: show route protocol bgp table inet6.0 aspath-regex "{target}"
# bgp_community: show route protocol bgp table inet6.0 community {target}
# bgp_route: show route protocol bgp table inet6.0 {target} detail | except Label | except Label | except "Next hop type" | except Task | except Address | except "Session Id" | except State | except "Next-hop reference" | except destinations | except "Announcement bits"
# ping: ping inet6 {target} count 5 source {source}
# traceroute: traceroute inet6 {target} wait 2 source {source}
# ipv6_vpn:
# bgp_aspath: show route protocol bgp table {vrf}.inet6.0 aspath-regex "{target}"
# bgp_community: show route protocol bgp table {vrf}.inet6.0 community {target}
# bgp_route: show route protocol bgp table {vrf}.inet6.0 {target} detail | except Label | except Label | except "Next hop type" | except Task | except Address | except "Session Id" | except State | except "Next-hop reference" | except destinations | except "Announcement bits"
# ping: ping inet6 routing-instance {vrf} {target} count 5 source {source}
# traceroute: traceroute inet6 routing-instance {vrf} {target} wait 2 source {source}

View File

@@ -1,155 +0,0 @@
# cache:
# database: 1
# host: localhost
# port: 6379
# show_text: true
# timeout: 120
# cors_origins: []
# debug: false
# developer_mode: false
# docs:
# base_url: https://lg.example.net
# description: ""
# devices:
# description: List of all devices/locations with associated identifiers, display names, networks, & VRFs.
# summary: Devices List
# title: Devices
# enable: true
# mode: redoc
# openapi_uri: /openapi.json
# queries:
# description: List of supported query types.
# summary: Query Types
# title: Supported Queries
# query:
# description: Request a query response per-location.
# summary: Query the Looking Glass
# title: Submit Query
# title: "{site_title} API Documentation"
# uri: /api/docs
# listen_address: localhost
# listen_port: 8001
# messages:
# acl_denied: "{target} is a member of {denied_network}, which is not allowed."
# acl_not_allowed: "{target} is not allowed."
# authentication_error: Authentication error occurred.
# connection_error: "Error connecting to {device_name}: {error}"
# feature_not_enabled: "{feature} is not enabled."
# general: Something went wrong.
# invalid_field: "{input} is an invalid {field}."
# invalid_input: "{target} is not a valid {query_type} target."
# no_input: "{field} must be specified."
# no_output: No output.
# no_response: No response.
# request_timeout: Request timed out.
# vrf_not_associated: VRF {vrf_name} is not associated with {device_name}.
# vrf_not_found: VRF {vrf_name} is not defined.
# netmiko_delay_factor: 0.1
# org_name: Beloved Hyperglass User
# primary_asn: "65001"
# queries:
# bgp_aspath:
# display_name: BGP AS Path
# enable: true
# pattern:
# asdot: ^(\^|^\_)((\d+\.\d+)\_|(\d+\.\d+)\$|(\d+\.\d+)\(\_\.\+\_\))+$
# asplain: ^(\^|^\_)(\d+\_|\d+\$|\d+\(\_\.\+\_\))+$
# mode: asplain
# bgp_community:
# display_name: BGP Community
# enable: true
# pattern:
# decimal: ^[0-9]{1,10}$
# extended_as: ^([0-9]{0,5})\:([0-9]{1,5})$
# large: ^([0-9]{1,10})\:([0-9]{1,10})\:[0-9]{1,10}$
# bgp_route:
# display_name: BGP Route
# enable: true
# ping:
# display_name: Ping
# enable: true
# traceroute:
# display_name: Traceroute
# enable: true
# request_timeout: 90
# site_description: Beloved Hyperglass User Network Looking Glass
# site_keywords:
# - hyperglass
# - looking glass
# - lg
# - peer
# - peering
# - ip
# - ipv4
# - ipv6
# - transit
# - community
# - communities
# - bgp
# - routing
# - network
# - isp
# - internet service provider
# site_title: hyperglass
# web:
# credit:
# enable: true
# dns_provider:
# name: cloudflare
# url: https://cloudflare-dns.com/dns-query
# external_link:
# enable: true
# title: PeeringDB
# url: https://www.peeringdb.com/asn/{primary_asn}
# help_menu:
# enable: true
# file: null
# title: Help
# logo:
# dark: images/hyperglass-dark.png
# favicons: ui/images/favicons/
# height: null
# light: images/hyperglass-light.png
# width: 80%
# opengraph:
# height: 1132
# image: images/hyperglass-opengraph.png
# width: 7355
# terms:
# enable: true
# file: null
# title: Terms
# text:
# cache: Results will be cached for {timeout} {period}.
# fqdn_tooltip: Use {protocol}
# query_location: Location
# query_target: Target
# query_type: Query Type
# query_vrf: Routing Table
# subtitle: AS{primary_asn}
# title: hyperglass
# title_mode: logo_only
# theme:
# colors:
# black: "#262626"
# blue: "#314cb6"
# cyan: "#118ab2"
# danger: "#d84b4b"
# error: "#ff6b35"
# gray: "#c1c7cc"
# green: "#35b246"
# orange: "#ff6b35"
# pink: "#f2607d"
# primary: "#118ab2"
# purple: "#8d30b5"
# red: "#d84b4b"
# secondary: "#314cb6"
# success: "#35b246"
# teal: "#35b299"
# warning: "#edae49"
# white: "#f7f7f7"
# yellow: "#edae49"
# default_color_mode: null
# fonts:
# body: Nunito
# mono: Fira Code

View File

@@ -0,0 +1,41 @@
import * as React from "react";
import Countdown, { zeroPad } from "react-countdown";
import { Text, useColorMode } from "@chakra-ui/core";
const bg = { dark: "white", light: "black" };
const Renderer = ({ hours, minutes, seconds, completed, props }) => {
if (completed) {
return <Text fontSize="xs" />;
} else {
let time = [zeroPad(seconds)];
minutes !== 0 && time.unshift(zeroPad(minutes));
hours !== 0 && time.unshift(zeroPad(hours));
return (
<Text fontSize="xs" color="gray.500">
{props.text}
<Text as="span" fontSize="xs" color={bg[props.colorMode]}>
{time.join(":")}
</Text>
</Text>
);
}
};
const CacheTimeout = ({ timeout, text }) => {
const then = timeout * 1000;
const { colorMode } = useColorMode();
return (
<Countdown
date={Date.now() + then}
renderer={Renderer}
daysInHours
text={text}
colorMode={colorMode}
/>
);
};
CacheTimeout.displayName = "CacheTimeout";
export default CacheTimeout;

View File

@@ -36,6 +36,7 @@ const Greeting = ({ greetingConfig, content, onClickThrough }) => {
size="full"
isCentered
closeOnOverlayClick={!greetingConfig.required}
closeOnEsc={!greetingConfig.required}
>
<AnimatedModalOverlay
initial={{ opacity: 0 }}

View File

@@ -8,18 +8,21 @@ import {
ButtonGroup,
css,
Flex,
Tooltip,
Text,
useTheme,
useColorMode,
} from "@chakra-ui/core";
import styled from "@emotion/styled";
import LightningBolt from "~/components/icons/LightningBolt";
import useAxios from "axios-hooks";
import strReplace from "react-string-replace";
import { startCase } from "lodash";
import useConfig from "~/components/HyperglassProvider";
import useMedia from "~/components/MediaProvider";
import CopyButton from "~/components/CopyButton";
import RequeryButton from "~/components/RequeryButton";
import ResultHeader from "~/components/ResultHeader";
import { startCase } from "lodash";
import CacheTimeout from "~/components/CacheTimeout";
const FormattedError = ({ keywords, message }) => {
const patternStr = keywords.map((kw) => `(${kw})`).join("|");
@@ -48,6 +51,10 @@ const AccordionHeaderWrapper = styled(Flex)`
`;
const statusMap = { success: "success", warning: "warning", error: "warning", danger: "error" };
const bg = { dark: "gray.800", light: "blackAlpha.100" };
const color = { dark: "white", light: "black" };
const selectionBg = { dark: "white", light: "black" };
const selectionColor = { dark: "black", light: "white" };
const Result = React.forwardRef(
(
@@ -65,12 +72,8 @@ const Result = React.forwardRef(
ref
) => {
const config = useConfig();
const theme = useTheme();
const { isSm } = useMedia();
const { colorMode } = useColorMode();
const bg = { dark: theme.colors.gray[800], light: theme.colors.blackAlpha[100] };
const color = { dark: theme.colors.white, light: theme.colors.black };
const selectionBg = { dark: theme.colors.white, light: theme.colors.black };
const selectionColor = { dark: theme.colors.black, light: theme.colors.white };
const [{ data, loading, error }, refetch] = useAxios({
url: "/api/query/",
method: "post",
@@ -91,7 +94,12 @@ const Result = React.forwardRef(
setOpen(!isOpen);
setOverride(true);
};
const cleanOutput = data && data.output.split("\\n").join("\n").replace(/\n\n/g, "\n");
const cleanOutput =
data &&
data.output
.split("\\n")
.join("\n")
.replace(/\n\n/g, "\n");
const errorKw = (error && error.response?.data?.keywords) || [];
@@ -113,6 +121,33 @@ const Result = React.forwardRef(
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} placement="top">
<Box ml={1}>
<LightningBolt color={color[colorMode]} />
</Box>
</Tooltip>
)}
</>
);
const cacheSm = (
<>
{data?.cached && (
<Tooltip hasArrow label={config.web.text.cache_icon} 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]);
@@ -130,7 +165,7 @@ const Result = React.forwardRef(
"&:first-of-type": { borderTop: "none" },
})}
>
<AccordionHeaderWrapper hoverBg={theme.colors.blackAlpha[50]}>
<AccordionHeaderWrapper hoverBg="blackAlpha.50">
<AccordionHeader
flex="1 0 auto"
py={2}
@@ -145,6 +180,7 @@ const Result = React.forwardRef(
error={error}
errorMsg={errorMsg}
errorLevel={errorLevel}
runtime={data?.runtime}
/>
</AccordionHeader>
<ButtonGroup px={3} py={2}>
@@ -186,31 +222,21 @@ const Result = React.forwardRef(
{error && (
<Alert rounded="lg" my={2} py={4} status={errorLevel}>
<FormattedError keywords={errorKw} message={errorMsg} />
{/* {errorMsg} */}
</Alert>
)}
</Flex>
</Flex>
{config.cache.show_text && (
<Flex direction="row" flexWrap="wrap">
<Flex
px={3}
mt={2}
justifyContent={[
"flex-start",
"flex-start",
"flex-end",
"flex-end",
]}
flex="1 0 auto"
>
<Text fontSize="xs" color="gray.500">
{config.web.text.cache}
</Text>
</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>
);

View File

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

View File

@@ -0,0 +1,25 @@
import * as React from "react";
import { useTheme } from "@chakra-ui/core";
const LightningBolt = ({ size = 4, color = "currentColor" }) => {
const theme = useTheme();
return (
<svg
width={theme.space[size]}
height={theme.space[size]}
viewBox="0 0 16 16"
fill={theme.colors[color]}
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M11.251.068a.5.5 0 0 1 .227.58L9.677 6.5H13a.5.5 0 0 1 .364.843l-8 8.5a.5.5 0 0 1-.842-.49L6.323 9.5H3a.5.5 0 0 1-.364-.843l8-8.5a.5.5 0 0 1 .615-.09z"
clipRule="evenodd"
/>
</svg>
);
};
LightningBolt.displayName = "LightningBolt";
export default LightningBolt;

View File

@@ -23,6 +23,7 @@
"lodash": "^4.17.15",
"next": "^9.3.1",
"react": "^16.13.1",
"react-countdown": "^2.2.1",
"react-dom": "^16.13.1",
"react-hook-form": "^5.1.1",
"react-icons": "^3.9.0",
@@ -54,11 +55,6 @@
"http-proxy-middleware": "0.20.0",
"prettier": "^1.19.1"
},
"babel": {
"presets": [
"next/babel"
]
},
"eslintConfig": {
"parser": "babel-eslint",
"rules": {

View File

@@ -6455,6 +6455,13 @@ react-clientside-effect@^1.2.2:
dependencies:
"@babel/runtime" "^7.0.0"
react-countdown@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/react-countdown/-/react-countdown-2.2.1.tgz#28d56fc1874b5b0d5238b7efe3ae914ed8033783"
integrity sha512-e8dUUhlysDqgci32VOOe0uDfeDMaiyyFNrWHdmMky5fithYDt4iOJa22EF96VbkU64R4D+Bww4AbLpqA/J4dww==
dependencies:
prop-types "^15.7.2"
react-dom@^16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f"