diff --git a/hyperglass/models/config/web.py b/hyperglass/models/config/web.py
index 4704f00..d534e15 100644
--- a/hyperglass/models/config/web.py
+++ b/hyperglass/models/config/web.py
@@ -61,7 +61,7 @@ class Menu(HyperglassModel):
order: StrictInt = 0
@validator("content")
- def validate_content(cls, value):
+ def validate_content(cls: "Menu", value: str) -> str:
"""Read content from file if a path is provided."""
if len(value) < 260:
@@ -135,14 +135,14 @@ class Text(HyperglassModel):
ip_button: StrictStr = "My IP"
@validator("title_mode")
- def validate_title_mode(cls, value):
+ def validate_title_mode(cls: "Text", value: str) -> str:
"""Set legacy logo_title to logo_subtitle."""
if value == "logo_title":
value = "logo_subtitle"
return value
@validator("cache_prefix")
- def validate_cache_prefix(cls, value):
+ def validate_cache_prefix(cls: "Text", value: str) -> str:
"""Ensure trailing whitespace."""
return " ".join(value.split()) + " "
@@ -172,21 +172,16 @@ class ThemeColors(HyperglassModel):
danger: t.Optional[Color]
@validator(*FUNC_COLOR_MAP.keys(), pre=True, always=True)
- def validate_colors(cls, value, values, field):
- """Set default functional color mapping.
-
- Arguments:
- value {str|None} -- Functional color
- values {str} -- Already-validated colors
- Returns:
- {str} -- Mapped color.
- """
+ def validate_colors(
+ cls: "ThemeColors", value: str, values: t.Dict[str, t.Optional[str]], field
+ ) -> str:
+ """Set default functional color mapping."""
if value is None:
default_color = FUNC_COLOR_MAP[field.name]
value = str(values[default_color])
return value
- def dict(self, *args, **kwargs):
+ def dict(self, *args: t.Any, **kwargs: t.Any) -> t.Dict[str, str]:
"""Return dict for colors only."""
return {k: v.as_hex() for k, v in self.__dict__.items()}
@@ -213,20 +208,32 @@ class DnsOverHttps(HyperglassModel):
url: StrictStr = ""
@root_validator
- def validate_dns(cls, values):
- """Assign url field to model based on selected provider.
-
- Arguments:
- values {dict} -- Dict of selected provider
-
- Returns:
- {dict} -- Dict with url attribute
- """
+ def validate_dns(cls: "DnsOverHttps", values: t.Dict[str, str]) -> t.Dict[str, str]:
+ """Assign url field to model based on selected provider."""
provider = values["name"]
values["url"] = DNS_OVER_HTTPS[provider]
return values
+class HighlightPattern(HyperglassModel):
+ """Validation model for highlight pattern configuration."""
+
+ pattern: StrictStr
+ label: t.Optional[StrictStr] = None
+ color: StrictStr = "primary"
+
+ @validator("color")
+ def validate_color(cls: "HighlightPattern", value: str) -> str:
+ """Ensure highlight color is a valid theme color."""
+ colors = list(ThemeColors.__fields__.keys())
+ color_list = "\n - ".join(("", *colors))
+ if value not in colors:
+ raise ValueError(
+ "{!r} is not a supported color. Must be one of:{!s}".format(value, color_list)
+ )
+ return value
+
+
class Web(HyperglassModel):
"""Validation model for all web/browser-related configuration."""
@@ -247,6 +254,7 @@ class Web(HyperglassModel):
location_display_mode: LocationDisplayMode = "auto"
custom_javascript: t.Optional[FilePath]
custom_html: t.Optional[FilePath]
+ highlight: t.List[HighlightPattern] = []
class WebPublic(Web):
diff --git a/hyperglass/ui/components/output/highlighted.tsx b/hyperglass/ui/components/output/highlighted.tsx
new file mode 100644
index 0000000..359e496
--- /dev/null
+++ b/hyperglass/ui/components/output/highlighted.tsx
@@ -0,0 +1,57 @@
+import React, { memo } from 'react';
+import { Badge, Tooltip, useStyleConfig } from '@chakra-ui/react';
+import isEqual from 'react-fast-compare';
+import replace from 'react-string-replace';
+
+import type { TooltipProps } from '@chakra-ui/react';
+import type { Highlight as HighlightConfig } from '~/types';
+
+interface HighlightedProps {
+ patterns: HighlightConfig[];
+ children: string;
+}
+
+interface HighlightProps {
+ label: string | null;
+ colorScheme: string;
+ children: React.ReactNode;
+}
+
+const Highlight = (props: HighlightProps): JSX.Element => {
+ const { colorScheme, label, children } = props;
+ const { bg, color } = useStyleConfig('Button', { colorScheme }) as TooltipProps;
+ return (
+
+ {children}
+
+ );
+};
+
+const _Highlighted = (props: HighlightedProps): JSX.Element => {
+ const { patterns, children } = props;
+ let result: React.ReactNodeArray = [];
+ let times: number = 0;
+
+ for (const config of patterns) {
+ let toReplace: string | React.ReactNodeArray = children;
+ if (times !== 0) {
+ toReplace = result;
+ }
+ result = replace(toReplace, new RegExp(`(${config.pattern})`, 'gm'), (m, i) => (
+
+ {m}
+
+ ));
+ times++;
+ }
+
+ return (
+ <>
+ {result.map(r => (
+ <>{r}>
+ ))}
+ >
+ );
+};
+
+export const Highlighted = memo(_Highlighted, isEqual);
diff --git a/hyperglass/ui/components/output/text.tsx b/hyperglass/ui/components/output/text.tsx
index 8fa794c..976bd17 100644
--- a/hyperglass/ui/components/output/text.tsx
+++ b/hyperglass/ui/components/output/text.tsx
@@ -1,5 +1,6 @@
import { Box } from '@chakra-ui/react';
-import { useColorValue } from '~/context';
+import { useColorValue, useConfig } from '~/context';
+import { Highlighted } from './highlighted';
import type { TTextOutput } from './types';
@@ -11,6 +12,10 @@ export const TextOutput: React.FC = (props: TTextOutput) => {
const selectionBg = useColorValue('black', 'white');
const selectionColor = useColorValue('white', 'black');
+ const {
+ web: { highlight },
+ } = useConfig();
+
return (
= (props: TTextOutput) => {
}}
{...rest}
>
- {children.split('\\n').join('\n').replace(/\n\n/g, '\n')}
+
+ {children.split('\\n').join('\n').replace(/\n\n/g, '\n')}
+
);
};
diff --git a/hyperglass/ui/package.json b/hyperglass/ui/package.json
index ab154b5..b32efb5 100644
--- a/hyperglass/ui/package.json
+++ b/hyperglass/ui/package.json
@@ -42,6 +42,7 @@
"react-markdown": "^5.0.3",
"react-query": "^3.16.0",
"react-select": "^5.2.1",
+ "react-string-replace": "^v0.5.0",
"react-table": "^7.7.0",
"remark-gfm": "^1.0.0",
"string-format": "^2.0.0",
diff --git a/hyperglass/ui/types/config.ts b/hyperglass/ui/types/config.ts
index 651af93..63ba68a 100644
--- a/hyperglass/ui/types/config.ts
+++ b/hyperglass/ui/types/config.ts
@@ -85,6 +85,12 @@ interface _Credit {
enable: boolean;
}
+interface _Highlight {
+ pattern: string;
+ label: string | null;
+ color: string;
+}
+
interface _Web {
credit: _Credit;
dns_provider: { name: string; url: string };
@@ -97,6 +103,7 @@ interface _Web {
text: _Text;
theme: _ThemeConfig;
location_display_mode: 'auto' | 'gallery' | 'dropdown';
+ highlight: _Highlight[];
}
type _DirectiveBase = {
@@ -201,3 +208,4 @@ export type Greeting = CamelCasedProperties<_Greeting>;
export type Logo = CamelCasedProperties<_Logo>;
export type Link = CamelCasedProperties<_Link>;
export type Menu = CamelCasedProperties<_Menu>;
+export type Highlight = CamelCasedProperties<_Highlight>;
diff --git a/hyperglass/ui/yarn.lock b/hyperglass/ui/yarn.lock
index 98657ac..4cf1033 100644
--- a/hyperglass/ui/yarn.lock
+++ b/hyperglass/ui/yarn.lock
@@ -7058,6 +7058,11 @@ react-simple-animate@^3.3.12:
resolved "https://registry.yarnpkg.com/react-simple-animate/-/react-simple-animate-3.3.12.tgz#ddea0f230feb3c1f069fbdb0a26e735e0b233265"
integrity sha512-lFXjxD6ficcpOMsHfcDs1jqdkCve6jNlJnubOCzVOLswFDRANsaLN4KwpezDuliEFz8Q1zyj4J7Tmj3KMRnPcg==
+react-string-replace@^v0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/react-string-replace/-/react-string-replace-0.5.0.tgz#d563ea608f98c449eea5eb3c7511fe806dba7025"
+ integrity sha512-xtfotmm+Gby5LjfYg3s5+eT6bnwgOIdZRAdHTouLY/7SicDtX4JXjG7CAGaGDcS0ax4nsaaEVNRxArSa8BgQKw==
+
react-style-singleton@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.1.1.tgz#ce7f90b67618be2b6b94902a30aaea152ce52e66"