mirror of
https://github.com/checktheroads/hyperglass
synced 2024-05-11 05:55:08 +00:00
add Greeting modal component
This commit is contained in:
157
docs/docs/ui.mdx
157
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 | <PageLink to="#credit">➡️</PageLink> |
|
||||
| `dns_provider` | DNS over HTTPS Provider | <PageLink to="#dns_provider">➡️</PageLink> |
|
||||
| `external_link` | Link to external site | <PageLink to="#external_link">➡️</PageLink> |
|
||||
| `greeting` | Greeting Modal | <PageLink to="#greeting">➡️</PageLink> |
|
||||
| `logo` | Logo & Favicons | <PageLink to="#logo">➡️</PageLink> |
|
||||
| `opengraph` | [OpenGraph](https://ogp.me/) | <PageLink to="#opengraph">➡️</PageLink> |
|
||||
| `terms` | Terms & Conditions | <PageLink to="#terms">➡️</PageLink> |
|
||||
| `theme` | Colors & Fonts | <PageLink to="#theme">➡️</PageLink> |
|
||||
|
||||
## 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 <Link to="/docs/configuration#global-settings">Global Settings</Link> |
|
||||
|
||||
## `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 | <Font name='Nunito'/> | Main body font |
|
||||
| `mono` | String | <Font name='Fira Code'/> | 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
|
||||
```
|
||||
|
@@ -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,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
@@ -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()
|
||||
|
72
hyperglass/ui/components/Greeting.js
Normal file
72
hyperglass/ui/components/Greeting.js
Normal file
@@ -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 (
|
||||
<Modal
|
||||
onClose={handleClick}
|
||||
isOpen={isOpen}
|
||||
size="full"
|
||||
isCentered
|
||||
closeOnOverlayClick={!greetingConfig.required}
|
||||
>
|
||||
<AnimatedModalOverlay
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3, delay: 0.7 }}
|
||||
/>
|
||||
<AnimatedModalContent
|
||||
initial={{ scale: 0.5, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ duration: 0.3, delay: 0.7 }}
|
||||
bg={bg[colorMode]}
|
||||
color={color[colorMode]}
|
||||
py={4}
|
||||
borderRadius="md"
|
||||
maxW={["95%", "75%", "75%", "75%"]}
|
||||
>
|
||||
<ModalHeader>{greetingConfig.title}</ModalHeader>
|
||||
{!greetingConfig.required && <ModalCloseButton />}
|
||||
<ModalBody>
|
||||
<MarkDown content={content} />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variantColor="primary" onClick={handleClick}>
|
||||
{greetingConfig.button}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</AnimatedModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
Greeting.displayName = "Greeting";
|
||||
|
||||
export default Greeting;
|
@@ -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)
|
||||
|
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
@@ -75,6 +81,13 @@ const Layout = () => {
|
||||
<Footer />
|
||||
{config.developer_mode && <Debugger />}
|
||||
</Flex>
|
||||
{config.web.greeting.enable && !greetingAck && (
|
||||
<Greeting
|
||||
greetingConfig={config.web.greeting}
|
||||
content={config.content.greeting}
|
||||
onClickThrough={setGreetingAck}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
47
hyperglass/ui/hooks/useSessionStorage.js
Normal file
47
hyperglass/ui/hooks/useSessionStorage.js
Normal file
@@ -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;
|
Reference in New Issue
Block a user