diff --git a/ui/components/CopyButton.js b/ui/components/CopyButton.js index f8bcc03..28ad79a 100644 --- a/ui/components/CopyButton.js +++ b/ui/components/CopyButton.js @@ -1,11 +1,19 @@ import React from "react"; import { Button, Icon, Tooltip, useClipboard } from "@chakra-ui/core"; -export default ({ bg = "secondary", copyValue }) => { +export default ({ bg = "secondary", copyValue, ...props }) => { const { onCopy, hasCopied } = useClipboard(copyValue); return ( - diff --git a/ui/components/Footer.js b/ui/components/Footer.js index 2e64b79..3167a57 100644 --- a/ui/components/Footer.js +++ b/ui/components/Footer.js @@ -71,7 +71,7 @@ export default ({ general, help, extLink, credit, terms, content }) => { /> )} { - const theme = useTheme(); const { colorMode } = useColorMode(); - const labelColor = - colorMode === "dark" ? theme.colors.whiteAlpha[600] : theme.colors.blackAlpha[600]; + const labelColor = { dark: "whiteAlpha.600", light: "blackAlpha.600" }; return ( - + {label} {helpIcon?.enable && } diff --git a/ui/components/Header.js b/ui/components/Header.js index 353bf1f..93a5fec 100644 --- a/ui/components/Header.js +++ b/ui/components/Header.js @@ -1,44 +1,123 @@ import React from "react"; -import { Flex, IconButton, useColorMode, useTheme } from "@chakra-ui/core"; -import { motion } from "framer-motion"; +import { Flex, IconButton, useColorMode } from "@chakra-ui/core"; +import { motion, AnimatePresence } from "framer-motion"; +import ResetButton from "~/components/ResetButton"; +import useMedia from "~/components/MediaProvider"; +import Title from "~/components/Title"; const AnimatedFlex = motion.custom(Flex); +const AnimatedResetButton = motion.custom(ResetButton); -export default () => { - const theme = useTheme(); +const titleVariants = { + sm: { + fullSize: { scale: 1, marginLeft: 0 }, + small: { marginLeft: "auto" } + }, + md: { + fullSize: { scale: 1 }, + small: { scale: 1 } + }, + lg: { + fullSize: { scale: 1 }, + small: { scale: 1 } + }, + xl: { + fullSize: { scale: 1 }, + small: { scale: 1 } + } +}; + +const icon = { light: "moon", dark: "sun" }; +const bg = { light: "white", dark: "black" }; +const colorSwitch = { dark: "Switch to light mode", light: "Switch to dark mode" }; +const headerTransition = { type: "spring", ease: "anticipate", damping: 15, stiffness: 100 }; + +export default ({ height, isSubmitting, handleFormReset, ...props }) => { const { colorMode, toggleColorMode } = useColorMode(); - const bg = { light: theme.colors.white, dark: theme.colors.black }; - const icon = { light: "moon", dark: "sun" }; + const { mediaSize } = useMedia(); + const resetButton = ( + + + + + + ); + const title = ( + + + </AnimatedFlex> + ); + const colorModeToggle = ( + <AnimatedFlex + layoutTransition={headerTransition} + key="colorModeToggle" + alignItems="center" + initial={{ opacity: 0 }} + animate={{ opacity: 1 }} + mb={[null, "auto"]} + > + <IconButton + aria-label={colorSwitch[colorMode]} + variant="ghost" + color="current" + ml={2} + pl={0} + fontSize="20px" + onClick={toggleColorMode} + icon={icon[colorMode]} + /> + </AnimatedFlex> + ); + const layout = { + false: { + sm: [title, resetButton, colorModeToggle], + md: [resetButton, title, colorModeToggle], + lg: [resetButton, title, colorModeToggle], + xl: [resetButton, title, colorModeToggle] + }, + true: { + sm: [resetButton, colorModeToggle, title], + md: [resetButton, title, colorModeToggle], + lg: [resetButton, title, colorModeToggle], + xl: [resetButton, title, colorModeToggle] + } + }; return ( <Flex - position="fixed" - as="header" + px={2} top="0" - zIndex="4" - bg={bg[colorMode]} - color={theme.colors.gray[500]} left="0" right="0" + zIndex="4" + as="header" width="full" - height="4rem" + flex="1 0 auto" + position="fixed" + bg={bg[colorMode]} + color="gray.500" + height={height} + {...props} > - <Flex w="100%" mx="auto" px={6} justifyContent="flex-end"> - <AnimatedFlex - align="center" - initial={{ opacity: 0 }} - animate={{ opacity: 1 }} - transition={{ duration: 0.6 }} - > - <IconButton - aria-label={`Switch to ${colorMode === "light" ? "dark" : "light"} mode`} - variant="ghost" - color="current" - ml="2" - fontSize="20px" - onClick={toggleColorMode} - icon={icon[colorMode]} - /> - </AnimatedFlex> + <Flex w="100%" mx="auto" py={6} justify="space-between" alignItems="center"> + {layout[isSubmitting][mediaSize]} </Flex> </Flex> ); diff --git a/ui/components/HyperglassForm.js b/ui/components/HyperglassForm.js index 68bd94c..60496d2 100644 --- a/ui/components/HyperglassForm.js +++ b/ui/components/HyperglassForm.js @@ -10,11 +10,10 @@ import QueryType from "~/components/QueryType"; import QueryTarget from "~/components/QueryTarget"; import QueryVrf from "~/components/QueryVrf"; import SubmitButton from "~/components/SubmitButton"; +import useConfig from "~/components/HyperglassProvider"; format.extend(String.prototype, {}); -const all = (...items) => [...items].every(i => (i ? true : false)); - const formSchema = config => yup.object().shape({ query_location: yup @@ -33,13 +32,20 @@ const formSchema = config => }); const FormRow = ({ children, ...props }) => ( - <Flex flexDirection="row" flexWrap="wrap" w="100%" my={4} {...props}> + <Flex + flexDirection="row" + flexWrap="wrap" + w="100%" + justifyContent={["center", "center", "space-between", "space-between"]} + {...props} + > {children} </Flex> ); -export default React.forwardRef( - ({ config, isSubmitting, setSubmitting, setFormData, ...props }, ref) => { +const HyperglassForm = React.forwardRef( + ({ isSubmitting, setSubmitting, setFormData, ...props }, ref) => { + const config = useConfig(); const { handleSubmit, register, setValue, errors } = useForm({ validationSchema: formSchema(config) }); @@ -47,8 +53,6 @@ export default React.forwardRef( const [queryType, setQueryType] = useState(""); const [queryVrf, setQueryVrf] = useState(""); const [availVrfs, setAvailVrfs] = useState([]); - // const [showHelpIcon, setShowHelpIcon] = useState(false); - const onSubmit = values => { setFormData(values); setSubmitting(true); @@ -138,17 +142,25 @@ export default React.forwardRef( register={register} /> </FormField> - <FormField - flexGrow={0} - label="Submit" - error={errors.query_target} - hiddenLabels + </FormRow> + <FormRow mt={0} justifyContent="flex-end"> + <Flex + w="100%" + maxW="100%" + ml="auto" + my={2} + mr={[0, 0, 2, 2]} + flexDirection="column" + flex="0 0 0" > <SubmitButton isLoading={isSubmitting} /> - </FormField> + </Flex> </FormRow> </form> </Box> ); } ); + +HyperglassForm.displayName = "HyperglassForm"; +export default HyperglassForm; diff --git a/ui/components/Label.js b/ui/components/Label.js index 745a1fc..1cab54d 100644 --- a/ui/components/Label.js +++ b/ui/components/Label.js @@ -2,20 +2,27 @@ import React from "react"; import { Flex, useColorMode, useTheme } from "@chakra-ui/core"; export default React.forwardRef( - ({ value, label, labelBg, labelColor, valueBg, valueColor }, ref) => { + ({ value, label, labelColor, valueBg, valueColor, ...props }, ref) => { const theme = useTheme(); const { colorMode } = useColorMode(); - const _labelBg = { light: theme.colors.black, dark: theme.colors.gray[200] }; - const _labelColor = { light: theme.colors.white, dark: theme.colors.white }; + const _labelColor = { dark: "whiteAlpha.700", light: "blackAlpha.700" }; const _valueBg = { light: theme.colors.primary[600], dark: theme.colors.primary[600] }; - const _valueColor = { light: theme.colors.white, dark: theme.colors.white }; + const _valueColor = { light: "white", dark: "white" }; return ( - <Flex ref={ref} flexWrap="wrap" alignItems="center" justifyContent="flex-start" mx={2}> + <Flex + ref={ref} + flexWrap="nowrap" + alignItems="center" + justifyContent="flex-start" + mx={[1, 2, 2, 2]} + my={2} + {...props} + > <Flex display="inline-flex" justifyContent="center" lineHeight="1.5" - px={3} + px={[1, 3, 3, 3]} whiteSpace="nowrap" mb={2} mr={0} @@ -25,8 +32,8 @@ export default React.forwardRef( borderTopLeftRadius={4} borderBottomRightRadius={0} borderTopRightRadius={0} - fontSize="sm" fontWeight="bold" + fontSize={["xs", "sm", "sm", "sm"]} > {value} </Flex> @@ -39,13 +46,13 @@ export default React.forwardRef( mb={2} ml={0} mr={0} - bg={labelBg || _labelBg[colorMode]} + boxShadow={`inset 0px 0px 0px 1px ${valueBg || _valueBg[colorMode]}`} color={labelColor || _labelColor[colorMode]} borderBottomRightRadius={4} borderTopRightRadius={4} borderBottomLeftRadius={0} borderTopLeftRadius={0} - fontSize="sm" + fontSize={["xs", "sm", "sm", "sm"]} > {label} </Flex> diff --git a/ui/components/Layout.js b/ui/components/Layout.js index ac346c7..08726c7 100644 --- a/ui/components/Layout.js +++ b/ui/components/Layout.js @@ -1,67 +1,67 @@ -import React, { useState } from "react"; -import { Flex, useColorMode, useTheme } from "@chakra-ui/core"; +import React, { useRef, useState } from "react"; +import { Flex, useColorMode } from "@chakra-ui/core"; import { motion, AnimatePresence } from "framer-motion"; -import ResetButton from "~/components/ResetButton"; import HyperglassForm from "~/components/HyperglassForm"; import Results from "~/components/Results"; import Header from "~/components/Header"; import Footer from "~/components/Footer"; -import Title from "~/components/Title"; import Meta from "~/components/Meta"; +import useConfig from "~/components/HyperglassProvider"; +import Debugger from "~/components/Debugger"; const AnimatedForm = motion.custom(HyperglassForm); -const AnimatedTitle = motion.custom(Title); -const AnimatedResetButton = motion.custom(ResetButton); -export default ({ config }) => { - const theme = useTheme(); +const bg = { light: "white", dark: "black" }; +const color = { light: "black", dark: "white" }; +const headerHeightDefault = { true: [16, 16, 16, 16], false: [24, 64, 64, 64] }; +const headerHeightAll = { true: [32, 32, 32, 32], false: [48, "20rem", "20rem", "20rem"] }; + +const Layout = () => { + const config = useConfig(); const { colorMode } = useColorMode(); - const bg = { light: theme.colors.white, dark: theme.colors.black }; - const color = { light: theme.colors.black, dark: theme.colors.white }; const [isSubmitting, setSubmitting] = useState(false); const [formData, setFormData] = useState({}); + const containerRef = useRef(null); const handleFormReset = () => { + containerRef.current.scrollIntoView({ behavior: "smooth", block: "start" }); setSubmitting(false); }; + const headerHeight = + config.branding.text.title_mode === "all" + ? headerHeightAll[isSubmitting] + : headerHeightDefault[isSubmitting]; return ( <> - <Meta config={config} /> + <Meta /> <Flex - flexDirection="column" - minHeight="100vh" w="100%" + ref={containerRef} + minHeight="100vh" bg={bg[colorMode]} + flexDirection="column" color={color[colorMode]} > - <Header /> + <Flex px={2} flex="1 1 auto" flexGrow={0} flexDirection="column"> + <Header + isSubmitting={isSubmitting} + handleFormReset={handleFormReset} + height={headerHeight} + /> + </Flex> <Flex - as="main" - w="100%" - flexGrow={1} - flexShrink={1} - flexBasis="auto" - alignItems="center" - justifyContent="start" - textAlign="center" - flexDirection="column" px={2} py={0} - mt={["5%", "5%", "5%", "10%"]} + w="100%" + as="main" + mt={headerHeight} + flex="1 1 auto" + textAlign="center" + alignItems="center" + justifyContent="start" + flexDirection="column" > - <AnimatePresence> - <AnimatedTitle - initial={{ opacity: 0, y: -300 }} - animate={{ opacity: 1, y: 0 }} - transition={{ duration: 0.3 }} - exit={{ opacity: 0, y: -300 }} - text={config.branding.text} - logo={config.branding.logo} - resetForm={handleFormReset} - /> - </AnimatePresence> {isSubmitting && formData && ( <Results - config={config} queryLocation={formData.query_location} queryType={formData.query_type} queryVrf={formData.query_vrf} @@ -76,7 +76,6 @@ export default ({ config }) => { animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }} exit={{ opacity: 0, x: -300 }} - config={config} isSubmitting={isSubmitting} setSubmitting={setSubmitting} setFormData={setFormData} @@ -84,18 +83,7 @@ export default ({ config }) => { )} </AnimatePresence> </Flex> - <AnimatePresence> - {isSubmitting && ( - <AnimatedResetButton - initial={{ opacity: 0, x: -50 }} - animate={{ opacity: 1, x: 0 }} - transition={{ duration: 0.3 }} - exit={{ opacity: 0, x: -50 }} - isSubmitting={isSubmitting} - onClick={handleFormReset} - /> - )} - </AnimatePresence> + {config.general.debug && <Debugger />} <Footer general={config.general} content={config.content} @@ -108,3 +96,6 @@ export default ({ config }) => { </> ); }; + +Layout.displayName = "HyperglassLayout"; +export default Layout; diff --git a/ui/components/Meta.js b/ui/components/Meta.js index 2913cac..b287ad2 100644 --- a/ui/components/Meta.js +++ b/ui/components/Meta.js @@ -1,9 +1,11 @@ import React, { useEffect, useState } from "react"; import Head from "next/head"; import { useTheme } from "@chakra-ui/core"; +import useConfig from "~/components/HyperglassProvider"; import { googleFontUrl } from "~/util"; -export default ({ config }) => { +export default () => { + const config = useConfig(); const theme = useTheme(); const [location, setLocation] = useState({}); const title = config?.general.org_name || "hyperglass"; diff --git a/ui/components/PreConfig.js b/ui/components/PreConfig.js index 8f6c0c5..c63af2b 100644 --- a/ui/components/PreConfig.js +++ b/ui/components/PreConfig.js @@ -1,21 +1,18 @@ import React from "react"; -import { Button, Flex, Heading, Spinner, useTheme, useColorMode } from "@chakra-ui/core"; +import { + Button, + ColorModeProvider, + CSSReset, + Flex, + Heading, + Spinner, + ThemeProvider, + useTheme, + useColorMode +} from "@chakra-ui/core"; +import { defaultTheme } from "~/theme"; -const ErrorMsg = ({ title }) => ( - <> - <Heading mb={4} color="danger.500" as="h1" fontSize="2xl"> - {title} - </Heading> - </> -); - -const ErrorBtn = ({ text, onClick }) => ( - <Button variant="outline" variantColor="danger" onClick={onClick}> - {text} - </Button> -); - -export default ({ loading, error, refresh }) => { +const PreConfig = ({ loading, error, refresh }) => { const theme = useTheme(); const { colorMode } = useColorMode(); const bg = { light: theme.colors.white, dark: theme.colors.dark }; @@ -45,15 +42,26 @@ export default ({ loading, error, refresh }) => { {loading && <Spinner color="primary.500" w="6rem" h="6rem" />} {!loading && error && ( <> - <ErrorMsg - title={ - error.response?.data?.output || error.message || "An Error Occurred" - } - /> - <ErrorBtn text="Retry" onClick={refresh} /> + <Heading mb={4} color="danger.500" as="h1" fontSize="2xl"> + {error.response?.data?.output || error.message || "An Error Occurred"} + </Heading> + <Button variant="outline" variantColor="danger" onClick={refresh}> + Retry + </Button> </> )} </Flex> </Flex> ); }; + +export default ({ loading, error, refresh }) => { + return ( + <ThemeProvider theme={defaultTheme}> + <ColorModeProvider> + <CSSReset /> + <PreConfig loading={loading} error={error} refresh={refresh} /> + </ColorModeProvider> + </ThemeProvider> + ); +}; diff --git a/ui/components/RequeryButton.js b/ui/components/RequeryButton.js index d2d6a93..8ea0722 100644 --- a/ui/components/RequeryButton.js +++ b/ui/components/RequeryButton.js @@ -1,11 +1,19 @@ import React from "react"; -import { Button, Icon, Spinner, Tooltip } from "@chakra-ui/core"; +import { Button, Icon, Tooltip } from "@chakra-ui/core"; -export default ({ isLoading, requery, bg = "secondary" }) => { +export default ({ requery, bg = "secondary", ...props }) => { return ( <Tooltip hasArrow label="Reload Query" placement="top"> - <Button size="sm" variantColor={bg} zIndex="1" onClick={requery} mx={1}> - {isLoading ? <Spinner size="sm" /> : <Icon size="16px" name="repeat" />} + <Button + as="a" + size="sm" + variantColor={bg} + zIndex="1" + onClick={requery} + mx={1} + {...props} + > + <Icon size="16px" name="repeat" /> </Button> </Tooltip> ); diff --git a/ui/components/ResetButton.js b/ui/components/ResetButton.js index eef90cf..708f3f8 100644 --- a/ui/components/ResetButton.js +++ b/ui/components/ResetButton.js @@ -1,11 +1,17 @@ import React from "react"; -import { Box, Button } from "@chakra-ui/core"; +import { Button } from "@chakra-ui/core"; import { FiChevronLeft } from "react-icons/fi"; export default React.forwardRef(({ isSubmitting, onClick }, ref) => ( - <Box ref={ref} position="fixed" bottom={16} left={8} opacity={isSubmitting ? 1 : 0}> - <Button variantColor="primary" variant="outline" p={2} onClick={onClick}> - <FiChevronLeft size={24} /> - </Button> - </Box> + <Button + ref={ref} + aria-label="Reset Form" + opacity={isSubmitting ? 1 : 0} + variant="ghost" + color="current" + onClick={onClick} + pl={0} + > + <FiChevronLeft size={24} /> + </Button> )); diff --git a/ui/components/Result.js b/ui/components/Result.js index 1439f38..e09e22d 100644 --- a/ui/components/Result.js +++ b/ui/components/Result.js @@ -3,10 +3,10 @@ import { AccordionItem, AccordionHeader, AccordionPanel, - AccordionIcon, Alert, Box, ButtonGroup, + css, Flex, Text, useTheme, @@ -15,26 +15,35 @@ import { import styled from "@emotion/styled"; import useAxios from "axios-hooks"; import strReplace from "react-string-replace"; +import useConfig from "~/components/HyperglassProvider"; import CopyButton from "~/components/CopyButton"; import RequeryButton from "~/components/RequeryButton"; import ResultHeader from "~/components/ResultHeader"; -const PreBox = styled(Box)` - &::selection { - background-color: ${props => props.selectionBg}; - color: ${props => props.selectionColor}; - } -`; - const FormattedError = ({ keywords, message }) => { const patternStr = `(${keywords.join("|")})`; const pattern = new RegExp(patternStr, "gi"); - const errorFmt = strReplace(message, pattern, match => <Text as="strong">{match}</Text>); + const errorFmt = strReplace(message, pattern, match => ( + <Text key={match} as="strong"> + {match} + </Text> + )); return <Text>{errorFmt}</Text>; }; -export default React.forwardRef( - ({ config, device, timeout, queryLocation, queryType, queryVrf, queryTarget }, ref) => { +const AccordionHeaderWrapper = styled(Flex)` + justify-content: space-between; + &:hover { + background-color: ${props => props.hoverBg}; + } + &:focus { + box-shadow: "outline"; + } +`; + +const Result = React.forwardRef( + ({ device, timeout, queryLocation, queryType, queryVrf, queryTarget }, ref) => { + const config = useConfig(); const theme = useTheme(); const { colorMode } = useColorMode(); const bg = { dark: theme.colors.gray[800], light: theme.colors.blackAlpha[100] }; @@ -68,60 +77,68 @@ export default React.forwardRef( <AccordionItem isDisabled={loading} ref={ref} - css={{ + css={css({ "&:last-of-type": { borderBottom: "none" }, "&:first-of-type": { borderTop: "none" } - }} + })} > - <AccordionHeader justifyContent="space-between"> - <ResultHeader - config={config} - title={device.display_name} - loading={loading} - error={error} - /> - <Flex> - <AccordionIcon /> + <AccordionHeaderWrapper hoverBg={theme.colors.blackAlpha[50]}> + <AccordionHeader flex="1 0 auto" py={2} _hover={{}} _focus={{}} w="unset"> + <ResultHeader title={device.display_name} loading={loading} error={error} /> + </AccordionHeader> + <ButtonGroup px={3} py={2}> + <CopyButton copyValue={cleanOutput} variant="ghost" /> + <RequeryButton requery={refetch} variant="ghost" /> + </ButtonGroup> + </AccordionHeaderWrapper> + <AccordionPanel + pb={4} + overflowX="auto" + css={css({ WebkitOverflowScrolling: "touch" })} + > + <Flex direction="row" flexWrap="wrap"> + <Flex direction="column" flex="1 0 auto"> + {data && ( + <Box + fontFamily="mono" + mt={5} + mx={2} + p={3} + border="1px" + borderColor="inherit" + rounded="md" + bg={bg[colorMode]} + color={color[colorMode]} + fontSize="sm" + whiteSpace="pre-wrap" + as="pre" + css={css({ + "&::selection": { + backgroundColor: selectionBg[colorMode], + color: selectionColor[colorMode] + } + })} + > + {cleanOutput} + </Box> + )} + {error && ( + <Alert + rounded="lg" + my={2} + py={4} + status={error.response?.data?.alert || "error"} + > + <FormattedError keywords={errorKw} message={errorMsg} /> + </Alert> + )} + </Flex> </Flex> - </AccordionHeader> - <AccordionPanel pb={4}> - <Box position="relative"> - {data && ( - <PreBox - fontFamily="mono" - mt={5} - p={3} - border="1px" - borderColor="inherit" - rounded="md" - bg={bg[colorMode]} - color={color[colorMode]} - fontSize="sm" - whiteSpace="pre-wrap" - as="pre" - selectionBg={selectionBg[colorMode]} - selectionColor={selectionColor[colorMode]} - > - {cleanOutput} - </PreBox> - )} - {error && ( - <Alert - rounded="lg" - my={2} - py={4} - status={error.response?.data?.alert || "error"} - > - <FormattedError keywords={errorKw} message={errorMsg} /> - </Alert> - )} - <ButtonGroup position="absolute" top={0} right={5} py={3} spacing={4}> - <CopyButton copyValue={cleanOutput} /> - <RequeryButton isLoading={loading} requery={refetch} /> - </ButtonGroup> - </Box> </AccordionPanel> </AccordionItem> ); } ); + +Result.displayName = "HyperglassQueryResult"; +export default Result; diff --git a/ui/components/ResultHeader.js b/ui/components/ResultHeader.js index 3b69c8c..1aebd80 100644 --- a/ui/components/ResultHeader.js +++ b/ui/components/ResultHeader.js @@ -1,18 +1,29 @@ import React from "react"; -import { Icon, Spinner, Stack, Text, Tooltip, useColorMode, useTheme } from "@chakra-ui/core"; +import { + AccordionIcon, + Icon, + Spinner, + Stack, + Text, + Tooltip, + useColorMode, + useTheme +} from "@chakra-ui/core"; +import useConfig from "~/components/HyperglassProvider"; -export default React.forwardRef(({ config, title, loading, error }, ref) => { +export default React.forwardRef(({ title, loading, error }, ref) => { + const config = useConfig(); const theme = useTheme(); const { colorMode } = useColorMode(); - const statusColor = { dark: theme.colors.primary[300], light: theme.colors.primary[500] }; - const defaultWarningColor = { dark: theme.colors.danger[300], light: theme.colors.danger[500] }; + const statusColor = { dark: "primary.300", light: "primary.500" }; + const defaultWarningColor = { dark: "danger.300", light: "danger.500" }; const warningColor = { dark: 300, light: 500 }; const defaultStatusColor = { - dark: theme.colors.success[300], - light: theme.colors.success[500] + dark: "success.300", + light: "success.500" }; return ( - <Stack ref={ref} isInline alignItems="center"> + <Stack ref={ref} isInline alignItems="center" w="100%"> {loading ? ( <Spinner size="sm" mr={4} color={statusColor[colorMode]} /> ) : error ? ( @@ -36,6 +47,7 @@ export default React.forwardRef(({ config, title, loading, error }, ref) => { <Icon name="check" color={defaultStatusColor[colorMode]} mr={4} size={6} /> )} <Text fontSize="lg">{title}</Text> + <AccordionIcon ml="auto" /> </Stack> ); }); diff --git a/ui/components/Results.js b/ui/components/Results.js index f869ab0..52a0994 100644 --- a/ui/components/Results.js +++ b/ui/components/Results.js @@ -1,25 +1,60 @@ import React from "react"; -import { Accordion, Box, Stack, useColorMode, useTheme } from "@chakra-ui/core"; +import { Accordion, Box, Stack, useTheme } from "@chakra-ui/core"; import { motion, AnimatePresence } from "framer-motion"; import Label from "~/components/Label"; import Result from "~/components/Result"; +import useConfig from "~/components/HyperglassProvider"; +import useMedia from "~/components/MediaProvider"; const AnimatedResult = motion.custom(Result); const AnimatedLabel = motion.custom(Label); -export default ({ - config, - queryLocation, - queryType, - queryVrf, - queryTarget, - setSubmitting, - ...props -}) => { +const labelInitial = { + left: { + sm: { opacity: 0, x: -100 }, + md: { opacity: 0, x: -100 }, + lg: { opacity: 0, x: -100 }, + xl: { opacity: 0, x: -100 } + }, + center: { + sm: { opacity: 0 }, + md: { opacity: 0 }, + lg: { opacity: 0 }, + xl: { opacity: 0 } + }, + right: { + sm: { opacity: 0, x: 100 }, + md: { opacity: 0, x: 100 }, + lg: { opacity: 0, x: 100 }, + xl: { opacity: 0, x: 100 } + } +}; +const labelAnimate = { + left: { + sm: { opacity: 1, x: 0 }, + md: { opacity: 1, x: 0 }, + lg: { opacity: 1, x: 0 }, + xl: { opacity: 1, x: 0 } + }, + center: { + sm: { opacity: 1 }, + md: { opacity: 1 }, + lg: { opacity: 1 }, + xl: { opacity: 1 } + }, + right: { + sm: { opacity: 1, x: 0 }, + md: { opacity: 1, x: 0 }, + lg: { opacity: 1, x: 0 }, + xl: { opacity: 1, x: 0 } + } +}; + +const Results = ({ queryLocation, queryType, queryVrf, queryTarget, setSubmitting, ...props }) => { + const config = useConfig(); const theme = useTheme(); - const { colorMode } = useColorMode(); + const { mediaSize } = useMedia(); const matchedVrf = config.vrfs.filter(v => v.id === queryVrf)[0]; - const labelColor = { light: theme.colors.white, dark: theme.colors.black }; return ( <> <Box @@ -31,39 +66,39 @@ export default ({ textAlign="left" {...props} > - <Stack isInline align="center" justify="center" mt={4}> + <Stack isInline align="center" justify="center" mt={4} flexWrap="wrap"> <AnimatePresence> {queryLocation && ( <> <AnimatedLabel - initial={{ opacity: 0, x: -100 }} - animate={{ opacity: 1, x: 0 }} + initial={labelInitial.left[mediaSize]} + animate={labelAnimate.left[mediaSize]} transition={{ duration: 0.3, delay: 0.3 }} exit={{ opacity: 0, x: -100 }} label={config.branding.text.query_type} value={config.branding.text[queryType]} valueBg={theme.colors.cyan[500]} - labelColor={labelColor[colorMode]} + fontSize={["xs", "sm", "sm", "sm"]} /> <AnimatedLabel - initial={{ opacity: 0, scale: 0.5 }} - animate={{ opacity: 1, scale: 1 }} + initial={labelInitial.center[mediaSize]} + animate={labelAnimate.center[mediaSize]} transition={{ duration: 0.3, delay: 0.3 }} exit={{ opacity: 0, scale: 0.5 }} label={config.branding.text.query_target} value={queryTarget} valueBg={theme.colors.teal[600]} - labelColor={labelColor[colorMode]} + fontSize={["xs", "sm", "sm", "sm"]} /> <AnimatedLabel - initial={{ opacity: 0, x: 100 }} - animate={{ opacity: 1, x: 0 }} + initial={labelInitial.right[mediaSize]} + animate={labelAnimate.right[mediaSize]} transition={{ duration: 0.3, delay: 0.3 }} exit={{ opacity: 0, x: 100 }} label={config.branding.text.query_vrf} value={matchedVrf.display_name} valueBg={theme.colors.blue[500]} - labelColor={labelColor[colorMode]} + fontSize={["xs", "sm", "sm", "sm"]} /> </> )} @@ -71,7 +106,7 @@ export default ({ </Stack> </Box> <Box - maxW={["100%", "100%", "75%", "50%"]} + maxW={["100%", "100%", "75%", "75%"]} w="100%" p={0} mx="auto" @@ -91,7 +126,6 @@ export default ({ {queryLocation && queryLocation.map((loc, i) => ( <AnimatedResult - config={config} initial={{ opacity: 0, y: 300 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3, delay: i * 0.3 }} @@ -112,3 +146,6 @@ export default ({ </> ); }; + +Results.displayName = "HyperglassResults"; +export default Results; diff --git a/ui/components/Title.js b/ui/components/Title.js index a4d6c1c..e945a56 100644 --- a/ui/components/Title.js +++ b/ui/components/Title.js @@ -1,62 +1,98 @@ import React from "react"; -import { Button, Flex, Heading, Image, Stack, useColorMode } from "@chakra-ui/core"; +import { Button, Heading, Image, Stack, useColorMode } from "@chakra-ui/core"; +import { motion, AnimatePresence } from "framer-motion"; +import useConfig from "~/components/HyperglassProvider"; +import useMedia from "~/components/MediaProvider"; -const TitleOnly = ({ text }) => ( - <Heading as="h1" size="2xl"> +const subtitleAnimation = { + transition: { duration: 0.2, type: "tween" }, + initial: { opacity: 1, scale: 1 }, + animate: { opacity: 1, scale: 1 }, + exit: { opacity: 0, scale: 0.3 } +}; + +const titleSize = { true: "2xl", false: "lg" }; +const titleMargin = { true: 2, false: 0 }; + +const TitleOnly = ({ text, showSubtitle }) => ( + <Heading as="h1" mb={titleMargin[showSubtitle]} size={titleSize[showSubtitle]}> {text} </Heading> ); -const SubtitleOnly = ({ text }) => ( - <Heading as="h3" size="md"> +const SubtitleOnly = React.forwardRef(({ text, size = "md", ...props }, ref) => ( + <Heading ref={ref} as="h3" size={size} {...props}> {text} </Heading> -); +)); -const TextOnly = ({ text }) => ( - <Stack spacing={2}> - <TitleOnly text={text.title} /> - <SubtitleOnly text={text.subtitle} /> +const AnimatedSubtitle = motion.custom(SubtitleOnly); + +const textAlignment = { false: ["right", "center"], true: ["left", "center"] }; + +const TextOnly = ({ text, mediaSize, showSubtitle, ...props }) => ( + <Stack spacing={2} textAlign={textAlignment[showSubtitle]} {...props}> + <TitleOnly text={text.title} showSubtitle={showSubtitle} /> + <AnimatePresence> + {showSubtitle && <AnimatedSubtitle text={text.subtitle} {...subtitleAnimation} />} + </AnimatePresence> </Stack> ); -const LogoOnly = ({ text, logo }) => { +const Logo = ({ text, logo }) => { const { colorMode } = useColorMode(); const logoColor = { light: logo.dark, dark: logo.light }; const logoPath = logoColor[colorMode]; - return ( - <Image - src={`http://localhost:8001${logoPath}`} - alt={text.title} - w={logo.width} - h={logo.height || null} - /> - ); + return <Image src={logoPath} alt={text.title} />; }; -const LogoTitle = ({ text, logo }) => ( +const LogoTitle = ({ text, logo, showSubtitle }) => ( <> - <LogoOnly text={text} logo={logo} /> - <SubtitleOnly text={text.title} /> + <Logo text={text} logo={logo} /> + <AnimatePresence> + {showSubtitle && ( + <AnimatedSubtitle mt={2} text={text.subtitle} {...subtitleAnimation} /> + )} + </AnimatePresence> </> ); -const All = ({ text, logo }) => ( +const All = ({ text, logo, mediaSize, showSubtitle }) => ( <> - <LogoOnly text={text} logo={logo} /> - <TextOnly text={text} /> + <Logo text={text} logo={logo} /> + <TextOnly mediaSize={mediaSize} showSubtitle={showSubtitle} mt={2} text={text} /> </> ); -const modeMap = { text_only: TextOnly, logo_only: LogoOnly, logo_title: LogoTitle, all: All }; +const modeMap = { text_only: TextOnly, logo_only: Logo, logo_title: LogoTitle, all: All }; -export default React.forwardRef(({ text, logo, resetForm }, ref) => { - const MatchedMode = modeMap[text.title_mode]; +const btnJustify = { + true: ["flex-end", "center"], + false: ["flex-start", "center"] +}; +export default React.forwardRef(({ onClick, isSubmitting, ...props }, ref) => { + const { branding } = useConfig(); + const { mediaSize } = useMedia(); + const titleMode = branding.text.title_mode; + const MatchedMode = modeMap[titleMode]; return ( - <Button variant="link" onClick={resetForm} _focus={{ boxShadow: "non" }}> - <Flex ref={ref}> - <MatchedMode text={text} logo={logo} /> - </Flex> + <Button + ref={ref} + variant="link" + onClick={onClick} + flexWrap="wrap" + _focus={{ boxShadow: "none" }} + _hover={{ textDecoration: "none" }} + justifyContent={btnJustify[isSubmitting]} + px={0} + {...props} + > + <MatchedMode + mediaSize={mediaSize} + showSubtitle={!isSubmitting} + text={branding.text} + logo={branding.logo} + /> </Button> ); });