diff --git a/docs/docs/ui.mdx b/docs/docs/ui.mdx
index 2a1f2b6..4b4bc4b 100644
--- a/docs/docs/ui.mdx
+++ b/docs/docs/ui.mdx
@@ -21,78 +21,12 @@ The `web` subsection contains multiple subsections of its own, should you wish t
| `credit` | Developer credit & GitHub Link | ➡️ |
| `dns_provider` | DNS over HTTPS Provider | ➡️ |
| `external_link` | Link to external site | ➡️ |
+| `greeting` | Greeting Modal | ➡️ |
| `logo` | Logo & Favicons | ➡️ |
| `opengraph` | [OpenGraph](https://ogp.me/) | ➡️ |
| `terms` | Terms & Conditions | ➡️ |
| `theme` | Colors & Fonts | ➡️ |
-## Example
-
-```yaml
-web:
- credit:
- enable: true
- dns_provider:
- name: google
- url: https://dns.google/resolve
- 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-light.png
- favicons: ui/images/favicons/
- height: null
- light: /images/hyperglass-dark.png
- width: 384
- opengraph:
- height: 1132
- image: /images/hyperglass-opengraph.png
- width: 7355
- terms:
- enable: true
- file: null
- title: Terms
- text:
- cache: Results will be cached for 2 minutes.
- fqdn_tooltip: "Use {protocol}"
- query_location: Location
- query_target: Target
- query_type: Query Type
- query_vrf: Routing Table
- subtitle: AS65001
- title: hyperglass
- title_mode: text_only
- theme:
- default_color_mode: light
- 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"
- fonts:
- body: Nunito
- mono: Fira Code
-```
-
## `credit`
| Parameter | Type | Default | Description |
@@ -117,14 +51,24 @@ If your organization's policy allows, and you don't mind, I request that you kee
| `title` | String | `'PeeringDB'` | Link title/label |
| `url` | String | `'https://www.peeringdb.com/asn/{primary_asn}'` | Target URL. `{primary_asn}` will be replaced with the `primary_asn` value from Global Settings |
+## `greeting`
+
+| Parameter | Type | Default | Description |
+| :--------- | :-----: | :----------- | :------------------------------------------------------------------------------------------- |
+| `enable` | Boolean | `false` | Enable or disable the greeting modal. |
+| `file` | String | | Path to a [markdown](https://www.markdownguide.org/) file containing the modal body content. |
+| `title` | String | `'Welcome'` | Modal title. |
+| `button` | String | `'Continue'` | Button text. |
+| `required` | Boolean | `false` | If `true` the user must click the button in order to submit a query. |
+
## `logo`
-| Parameter | Type | Default | Description |
-| :-------- | :-----: | :----------------------------------------------------------- | :------------------------------------------- |
-| `light` | String | `'hyperglass/hyperglass/static/images/hyperglass-dark.png'` | Path to logo that will be used in light mode |
-| `dark` | String | `'hyperglass/hyperglass/static/images/hyperglass-light.png'` | Path to logo that will be used in dark mode |
-| `width` | Integer | `384` | Maximum logo width in pixels |
-| `height` | Integer | | Maximum logo height in pixels |
+| Parameter | Type | Default | Description |
+| :-------- | :-----: | :------------------------------ | :------------------------------------------- |
+| `light` | String | `'images/hyperglass-dark.png'` | Path to logo that will be used in light mode |
+| `dark` | String | `'images/hyperglass-light.png'` | Path to logo that will be used in dark mode |
+| `width` | Integer | `384` | Maximum logo width in pixels |
+| `height` | Integer | | Maximum logo height in pixels |
## `opengraph`
@@ -218,3 +162,70 @@ Currently, only [Google Fonts](https://fonts.google.com/) are supported.
| :-------- | :----: | :----------------------- | :-------------------------------------- |
| `body` | String | | Main body font |
| `mono` | String | | Monospace font, used for command output |
+
+## Example
+
+```yaml
+web:
+ credit:
+ enable: true
+ dns_provider:
+ name: google
+ url: https://dns.google/resolve
+ 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-light.png
+ favicons: ui/images/favicons/
+ height: null
+ light: /images/hyperglass-dark.png
+ width: 384
+ opengraph:
+ height: 1132
+ image: /images/hyperglass-opengraph.png
+ width: 7355
+ terms:
+ enable: true
+ file: null
+ title: Terms
+ text:
+ cache: Results will be cached for 2 minutes.
+ fqdn_tooltip: "Use {protocol}"
+ query_location: Location
+ query_target: Target
+ query_type: Query Type
+ query_vrf: Routing Table
+ subtitle: AS65001
+ title: hyperglass
+ title_mode: text_only
+ theme:
+ default_color_mode: light
+ 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"
+ fonts:
+ body: Nunito
+ mono: Fira Code
+```
diff --git a/hyperglass/configuration/__init__.py b/hyperglass/configuration/__init__.py
index 28a2827..c265196 100644
--- a/hyperglass/configuration/__init__.py
+++ b/hyperglass/configuration/__init__.py
@@ -391,6 +391,12 @@ def _build_vrf_help():
return all_help
+content_greeting = get_markdown(
+ config_path=params.web.greeting,
+ default="",
+ params={"title": params.web.greeting.title},
+)
+
content_vrf = _build_vrf_help()
content_help_params = copy.copy(content_params)
@@ -436,6 +442,7 @@ _frontend_params.update(
"terms": content_terms,
"credit": content_credit,
"vrf": content_vrf,
+ "greeting": content_greeting,
},
}
)
diff --git a/hyperglass/configuration/models/web.py b/hyperglass/configuration/models/web.py
index 644f2b7..d81ae79 100644
--- a/hyperglass/configuration/models/web.py
+++ b/hyperglass/configuration/models/web.py
@@ -69,6 +69,23 @@ class HelpMenu(HyperglassModel):
title: StrictStr = "Help"
+class Greeting(HyperglassModel):
+ """Validation model for greeting modal."""
+
+ enable: StrictBool = False
+ file: Optional[FilePath]
+ title: StrictStr = "Welcome"
+ button: StrictStr = "Continue"
+ required: StrictBool = False
+
+ @validator("file")
+ def validate_file(cls, value, values):
+ """Ensure file is specified if greeting is enabled."""
+ if values["enable"] and value is None:
+ raise ValueError("Greeting is enabled, but no file is specified.")
+ return value
+
+
class Logo(HyperglassModel):
"""Validation model for logo configuration."""
@@ -214,6 +231,7 @@ class Web(HyperglassModel):
credit: Credit = Credit()
dns_provider: DnsOverHttps = DnsOverHttps()
external_link: ExternalLink = ExternalLink()
+ greeting: Greeting = Greeting()
help_menu: HelpMenu = HelpMenu()
logo: Logo = Logo()
opengraph: OpenGraph = OpenGraph()
diff --git a/hyperglass/ui/components/Greeting.js b/hyperglass/ui/components/Greeting.js
new file mode 100644
index 0000000..52184f9
--- /dev/null
+++ b/hyperglass/ui/components/Greeting.js
@@ -0,0 +1,72 @@
+import * as React from "react";
+import {
+ Button,
+ Modal,
+ ModalOverlay,
+ ModalContent,
+ ModalHeader,
+ ModalFooter,
+ ModalBody,
+ ModalCloseButton,
+ useColorMode,
+ useDisclosure,
+} from "@chakra-ui/core";
+import MarkDown from "~/components/MarkDown";
+import { motion } from "framer-motion";
+
+const bg = { light: "white", dark: "black" };
+const color = { light: "black", dark: "white" };
+
+const AnimatedModalContent = motion.custom(ModalContent);
+const AnimatedModalOverlay = motion.custom(ModalOverlay);
+
+const Greeting = ({ greetingConfig, content, onClickThrough }) => {
+ const { isOpen, onOpen, onClose } = useDisclosure(true);
+ const { colorMode } = useColorMode();
+
+ const handleClick = () => {
+ onClickThrough(true);
+ onClose();
+ };
+
+ return (
+
+
+
+ {greetingConfig.title}
+ {!greetingConfig.required && }
+
+
+
+
+
+
+
+
+ );
+};
+
+Greeting.displayName = "Greeting";
+
+export default Greeting;
diff --git a/hyperglass/ui/components/HyperglassForm.js b/hyperglass/ui/components/HyperglassForm.js
index 928173b..5d0f09f 100644
--- a/hyperglass/ui/components/HyperglassForm.js
+++ b/hyperglass/ui/components/HyperglassForm.js
@@ -16,7 +16,7 @@ import useConfig from "~/components/HyperglassProvider";
format.extend(String.prototype, {});
-const formSchema = config =>
+const formSchema = (config) =>
yup.object().shape({
query_location: yup
.array()
@@ -28,7 +28,7 @@ const formSchema = config =>
query_vrf: yup.string(),
query_target: yup
.string()
- .required(config.messages.no_input.format({ field: config.web.text.query_target }))
+ .required(config.messages.no_input.format({ field: config.web.text.query_target })),
});
const FormRow = ({ children, ...props }) => (
@@ -44,11 +44,11 @@ const FormRow = ({ children, ...props }) => (
);
const HyperglassForm = React.forwardRef(
- ({ isSubmitting, setSubmitting, setFormData, ...props }, ref) => {
+ ({ isSubmitting, setSubmitting, setFormData, greetingAck, setGreetingAck, ...props }, ref) => {
const config = useConfig();
const { handleSubmit, register, setValue, errors } = useForm({
validationSchema: formSchema(config),
- defaultValues: { query_vrf: "default" }
+ defaultValues: { query_vrf: "default" },
});
const [queryLocation, setQueryLocation] = useState([]);
@@ -59,21 +59,26 @@ const HyperglassForm = React.forwardRef(
const [fqdnTarget, setFqdnTarget] = useState("");
const [displayTarget, setDisplayTarget] = useState("");
const [families, setFamilies] = useState([]);
- const onSubmit = values => {
- setFormData(values);
- setSubmitting(true);
+ const onSubmit = (values) => {
+ if (!greetingAck && config.web.greeting.required) {
+ window.location.reload(false);
+ setGreetingAck(false);
+ } else {
+ setFormData(values);
+ setSubmitting(true);
+ }
};
- const handleLocChange = locObj => {
+ const handleLocChange = (locObj) => {
setQueryLocation(locObj.value);
const allVrfs = [];
const deviceVrfs = [];
- locObj.value.map(loc => {
+ locObj.value.map((loc) => {
const locVrfs = [];
- config.devices[loc].vrfs.map(vrf => {
+ config.devices[loc].vrfs.map((vrf) => {
locVrfs.push({
label: vrf.display_name,
- value: vrf.id
+ value: vrf.id,
});
deviceVrfs.push([{ id: vrf.id, ipv4: vrf.ipv4, ipv6: vrf.ipv6 }]);
});
@@ -89,10 +94,10 @@ const HyperglassForm = React.forwardRef(
deviceVrfs.length !== 0 &&
intersecting.length !== 0 &&
deviceVrfs
- .filter(v => intersecting.every(i => i.id === v.id))
+ .filter((v) => intersecting.every((i) => i.id === v.id))
.reduce((a, b) => a.concat(b))
- .filter(v => v.id === "default")
- .map(v => {
+ .filter((v) => v.id === "default")
+ .map((v) => {
v.ipv4 === true && ipv4++;
v.ipv6 === true && ipv6++;
});
@@ -107,7 +112,7 @@ const HyperglassForm = React.forwardRef(
}
};
- const handleChange = e => {
+ const handleChange = (e) => {
setValue(e.field, e.value);
e.field === "query_location"
? handleLocChange(e)
diff --git a/hyperglass/ui/components/Layout.js b/hyperglass/ui/components/Layout.js
index 5300605..4ce89b7 100644
--- a/hyperglass/ui/components/Layout.js
+++ b/hyperglass/ui/components/Layout.js
@@ -1,13 +1,15 @@
import React, { useRef, useState } from "react";
-import { Flex, useColorMode } from "@chakra-ui/core";
+import { Flex, useColorMode, useDisclosure } from "@chakra-ui/core";
import { motion, AnimatePresence } from "framer-motion";
import HyperglassForm from "~/components/HyperglassForm";
import Results from "~/components/Results";
import Header from "~/components/Header";
import Footer from "~/components/Footer";
+import Greeting from "~/components/Greeting";
import Meta from "~/components/Meta";
import useConfig from "~/components/HyperglassProvider";
import Debugger from "~/components/Debugger";
+import useSessionStorage from "~/hooks/useSessionStorage";
const AnimatedForm = motion.custom(HyperglassForm);
@@ -19,7 +21,9 @@ const Layout = () => {
const { colorMode } = useColorMode();
const [isSubmitting, setSubmitting] = useState(false);
const [formData, setFormData] = useState({});
+ const [greetingAck, setGreetingAck] = useSessionStorage("hyperglass-greeting-ack", false);
const containerRef = useRef(null);
+
const handleFormReset = () => {
containerRef.current.scrollIntoView({ behavior: "smooth", block: "start" });
setSubmitting(false);
@@ -68,6 +72,8 @@ const Layout = () => {
isSubmitting={isSubmitting}
setSubmitting={setSubmitting}
setFormData={setFormData}
+ greetingAck={greetingAck}
+ setGreetingAck={setGreetingAck}
/>
)}
@@ -75,6 +81,13 @@ const Layout = () => {
{config.developer_mode && }
+ {config.web.greeting.enable && !greetingAck && (
+
+ )}
>
);
};
diff --git a/hyperglass/ui/hooks/useSessionStorage.js b/hyperglass/ui/hooks/useSessionStorage.js
new file mode 100644
index 0000000..3d90ab0
--- /dev/null
+++ b/hyperglass/ui/hooks/useSessionStorage.js
@@ -0,0 +1,47 @@
+/*
+react-use: useSessionStorage
+https://github.com/streamich/react-use/blob/master/src/useSessionStorage.ts
+*/
+
+import { useEffect, useState } from "react";
+
+const useSessionStorage = (key, initialValue, raw) => {
+ const isClient = typeof window === "object";
+ if (!isClient) {
+ return [initialValue, () => {}];
+ }
+
+ const [state, setState] = useState(() => {
+ try {
+ const sessionStorageValue = sessionStorage.getItem(key);
+ if (typeof sessionStorageValue !== "string") {
+ sessionStorage.setItem(
+ key,
+ raw ? String(initialValue) : JSON.stringify(initialValue)
+ );
+ return initialValue;
+ } else {
+ return raw ? sessionStorageValue : JSON.parse(sessionStorageValue || "null");
+ }
+ } catch {
+ // If user is in private mode or has storage restriction
+ // sessionStorage can throw. JSON.parse and JSON.stringify
+ // cat throw, too.
+ return initialValue;
+ }
+ });
+
+ useEffect(() => {
+ try {
+ const serializedState = raw ? String(state) : JSON.stringify(state);
+ sessionStorage.setItem(key, serializedState);
+ } catch {
+ // If user is in private mode or has storage restriction
+ // sessionStorage can throw. Also JSON.stringify can throw.
+ }
+ });
+
+ return [state, setState];
+};
+
+export default useSessionStorage;