1
0
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:
checktheroads
2020-04-16 00:26:23 -07:00
parent 2cc153d078
commit dffdffdab4
7 changed files with 262 additions and 89 deletions

View File

@@ -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
```

View File

@@ -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,
},
}
)

View File

@@ -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()

View 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;

View File

@@ -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)

View File

@@ -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}
/>
)}
</>
);
};

View 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;