From 400685304f03b3d00ec20daa553da7c665a5ad35 Mon Sep 17 00:00:00 2001 From: checktheroads Date: Thu, 16 Apr 2020 23:43:02 -0700 Subject: [PATCH] improve cache handling --- docs/docs/ui.mdx | 24 +-- hyperglass/api/models/response.py | 8 +- hyperglass/api/routes.py | 24 ++- hyperglass/configuration/__init__.py | 17 +- hyperglass/configuration/models/cache.py | 2 +- hyperglass/configuration/models/web.py | 9 +- hyperglass/examples/commands.yaml | 125 -------------- hyperglass/examples/hyperglass.yaml | 155 ------------------ hyperglass/ui/components/CacheTimeout.js | 41 +++++ hyperglass/ui/components/Greeting.js | 1 + hyperglass/ui/components/Result.js | 82 +++++---- hyperglass/ui/components/ResultHeader.js | 39 ++++- .../ui/components/icons/LightningBolt.js | 25 +++ hyperglass/ui/package.json | 6 +- hyperglass/ui/yarn.lock | 7 + 15 files changed, 210 insertions(+), 355 deletions(-) create mode 100644 hyperglass/ui/components/CacheTimeout.js create mode 100644 hyperglass/ui/components/icons/LightningBolt.js diff --git a/docs/docs/ui.mdx b/docs/docs/ui.mdx index 4b4bc4b..2906057 100644 --- a/docs/docs/ui.mdx +++ b/docs/docs/ui.mdx @@ -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 request_timeout 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. Must be text_only, logo_only, logo_subtitle, or all | +| 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. Must be text_only, logo_only, logo_subtitle, or all | :::note Title Mode The `title_mode` parameter behaves in the following manner: diff --git a/hyperglass/api/models/response.py b/hyperglass/api/models/response.py index f06565f..463b500 100644 --- a/hyperglass/api/models/response.py +++ b/hyperglass/api/models/response.py @@ -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: diff --git a/hyperglass/api/routes.py b/hyperglass/api/routes.py index a731639..23b3880 100644 --- a/hyperglass/api/routes.py +++ b/hyperglass/api/routes.py @@ -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): diff --git a/hyperglass/configuration/__init__.py b/hyperglass/configuration/__init__.py index e7e92fb..03f3afc 100644 --- a/hyperglass/configuration/__init__.py +++ b/hyperglass/configuration/__init__.py @@ -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": ..., diff --git a/hyperglass/configuration/models/cache.py b/hyperglass/configuration/models/cache.py index 7f372a8..efc2ded 100644 --- a/hyperglass/configuration/models/cache.py +++ b/hyperglass/configuration/models/cache.py @@ -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: diff --git a/hyperglass/configuration/models/web.py b/hyperglass/configuration/models/web.py index 1d1f08b..f1572ec 100644 --- a/hyperglass/configuration/models/web.py +++ b/hyperglass/configuration/models/web.py @@ -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.""" diff --git a/hyperglass/examples/commands.yaml b/hyperglass/examples/commands.yaml index 3145145..e69de29 100644 --- a/hyperglass/examples/commands.yaml +++ b/hyperglass/examples/commands.yaml @@ -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} diff --git a/hyperglass/examples/hyperglass.yaml b/hyperglass/examples/hyperglass.yaml index 6703fce..e69de29 100644 --- a/hyperglass/examples/hyperglass.yaml +++ b/hyperglass/examples/hyperglass.yaml @@ -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 diff --git a/hyperglass/ui/components/CacheTimeout.js b/hyperglass/ui/components/CacheTimeout.js new file mode 100644 index 0000000..4c37beb --- /dev/null +++ b/hyperglass/ui/components/CacheTimeout.js @@ -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 ; + } else { + let time = [zeroPad(seconds)]; + minutes !== 0 && time.unshift(zeroPad(minutes)); + hours !== 0 && time.unshift(zeroPad(hours)); + return ( + + {props.text} + + {time.join(":")} + + + ); + } +}; + +const CacheTimeout = ({ timeout, text }) => { + const then = timeout * 1000; + const { colorMode } = useColorMode(); + return ( + + ); +}; + +CacheTimeout.displayName = "CacheTimeout"; + +export default CacheTimeout; diff --git a/hyperglass/ui/components/Greeting.js b/hyperglass/ui/components/Greeting.js index 52184f9..d4405c7 100644 --- a/hyperglass/ui/components/Greeting.js +++ b/hyperglass/ui/components/Greeting.js @@ -36,6 +36,7 @@ const Greeting = ({ greetingConfig, content, onClickThrough }) => { size="full" isCentered closeOnOverlayClick={!greetingConfig.required} + closeOnEsc={!greetingConfig.required} > { 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 = ( + <> + + {data?.cached && ( + + + + + + )} + + ); + const cacheSm = ( + <> + {data?.cached && ( + + + + + + )} + + + ); + + 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" }, })} > - + @@ -186,31 +222,21 @@ const Result = React.forwardRef( {error && ( - {/* {errorMsg} */} )} - {config.cache.show_text && ( - - - - {config.web.text.cache} - - + + + {data && !error && config.cache.show_text && cacheData} - )} + ); diff --git a/hyperglass/ui/components/ResultHeader.js b/hyperglass/ui/components/ResultHeader.js index b3df955..aab8c30 100644 --- a/hyperglass/ui/components/ResultHeader.js +++ b/hyperglass/ui/components/ResultHeader.js @@ -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 ( {loading ? ( @@ -23,7 +40,13 @@ export default React.forwardRef(({ title, loading, error, errorMsg, errorLevel } /> ) : ( - + + + )} {title} diff --git a/hyperglass/ui/components/icons/LightningBolt.js b/hyperglass/ui/components/icons/LightningBolt.js new file mode 100644 index 0000000..29d40d6 --- /dev/null +++ b/hyperglass/ui/components/icons/LightningBolt.js @@ -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 ( + + + + ); +}; + +LightningBolt.displayName = "LightningBolt"; + +export default LightningBolt; diff --git a/hyperglass/ui/package.json b/hyperglass/ui/package.json index cb4078c..eac339b 100644 --- a/hyperglass/ui/package.json +++ b/hyperglass/ui/package.json @@ -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": { diff --git a/hyperglass/ui/yarn.lock b/hyperglass/ui/yarn.lock index bee9df1..d153468 100644 --- a/hyperglass/ui/yarn.lock +++ b/hyperglass/ui/yarn.lock @@ -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"