diff --git a/hyperglass/ui/components/ChakraSelect.js b/hyperglass/ui/components/ChakraSelect.js
deleted file mode 100644
index c7e013c..0000000
--- a/hyperglass/ui/components/ChakraSelect.js
+++ /dev/null
@@ -1,190 +0,0 @@
-import * as React from 'react';
-import { Text, useColorMode, useTheme } from '@chakra-ui/core';
-import Select from 'react-select';
-import { opposingColor } from 'app/util';
-
-export const ChakraSelect = React.forwardRef(
- ({ placeholder = 'Select...', isFullWidth, size, children, ...props }, ref) => {
- const theme = useTheme();
- const { colorMode } = useColorMode();
- const sizeMap = {
- lg: { height: theme.space[12] },
- md: { height: theme.space[10] },
- sm: { height: theme.space[8] },
- };
- const colorSetPrimaryBg = {
- dark: theme.colors.primary[300],
- light: theme.colors.primary[500],
- };
- const colorSetPrimaryColor = opposingColor(theme, colorSetPrimaryBg[colorMode]);
- const bg = {
- dark: theme.colors.whiteAlpha[100],
- light: theme.colors.white,
- };
- const color = {
- dark: theme.colors.whiteAlpha[800],
- light: theme.colors.black,
- };
- const borderFocused = theme.colors.secondary[500];
- const borderDisabled = theme.colors.whiteAlpha[100];
- const border = {
- dark: theme.colors.whiteAlpha[50],
- light: theme.colors.gray[100],
- };
- const borderRadius = theme.space[1];
- const hoverColor = {
- dark: theme.colors.whiteAlpha[200],
- light: theme.colors.gray[300],
- };
- const { height } = sizeMap[size];
- const optionBgActive = {
- dark: theme.colors.primary[400],
- light: theme.colors.primary[600],
- };
- const optionBgColor = opposingColor(theme, optionBgActive[colorMode]);
- const optionSelectedBg = {
- dark: theme.colors.whiteAlpha[400],
- light: theme.colors.blackAlpha[400],
- };
- const optionSelectedColor = opposingColor(theme, optionSelectedBg[colorMode]);
- const selectedDisabled = theme.colors.whiteAlpha[400];
- const placeholderColor = {
- dark: theme.colors.whiteAlpha[700],
- light: theme.colors.gray[600],
- };
- const menuBg = {
- dark: theme.colors.blackFaded[800],
- light: theme.colors.whiteFaded[50],
- };
- const menuColor = {
- dark: theme.colors.white,
- light: theme.colors.blackAlpha[800],
- };
- const scrollbar = {
- dark: theme.colors.whiteAlpha[300],
- light: theme.colors.blackAlpha[300],
- };
- const scrollbarHover = {
- dark: theme.colors.whiteAlpha[400],
- light: theme.colors.blackAlpha[400],
- };
- const scrollbarBg = {
- dark: theme.colors.whiteAlpha[50],
- light: theme.colors.blackAlpha[50],
- };
- return (
-
- );
- },
-);
diff --git a/hyperglass/ui/components/HelpModal.js b/hyperglass/ui/components/HelpModal.js
deleted file mode 100644
index 9cc6fcf..0000000
--- a/hyperglass/ui/components/HelpModal.js
+++ /dev/null
@@ -1,66 +0,0 @@
-import * as React from 'react';
-import {
- IconButton,
- Modal,
- ModalOverlay,
- ModalContent,
- ModalHeader,
- ModalBody,
- ModalCloseButton,
- useDisclosure,
- useColorMode,
- useTheme,
-} from '@chakra-ui/core';
-import { motion, AnimatePresence } from 'framer-motion';
-import { Markdown } from 'app/components';
-
-const AnimatedIcon = motion.custom(IconButton);
-
-export const HelpModal = ({ item, name }) => {
- const { isOpen, onOpen, onClose } = useDisclosure();
- const { colors } = useTheme();
- const { colorMode } = useColorMode();
- const bg = { light: 'whiteFaded.50', dark: 'blackFaded.800' };
- const color = { light: 'black', dark: 'white' };
- const iconColor = {
- light: colors.primary[500],
- dark: colors.primary[300],
- };
- return (
- <>
-
-
-
-
-
-
- {item.params.title}
-
-
-
-
-
-
- >
- );
-};
diff --git a/hyperglass/ui/components/LookingGlass.js b/hyperglass/ui/components/LookingGlass.js
deleted file mode 100644
index 7168fe4..0000000
--- a/hyperglass/ui/components/LookingGlass.js
+++ /dev/null
@@ -1,46 +0,0 @@
-import * as React from 'react';
-import { motion, AnimatePresence } from 'framer-motion';
-import { Layout, HyperglassForm, Results } from 'app/components';
-import { useHyperglassState } from 'app/context';
-
-const AnimatedForm = motion.custom(HyperglassForm);
-
-export const LookingGlass = () => {
- const {
- isSubmitting,
- setSubmitting,
- formData,
- setFormData,
- greetingAck,
- setGreetingAck,
- } = useHyperglassState();
-
- return (
-
- {isSubmitting && formData && (
-
- )}
-
- {!isSubmitting && (
-
- )}
-
-
- );
-};
diff --git a/hyperglass/ui/components/QueryLocation.js b/hyperglass/ui/components/QueryLocation.js
deleted file mode 100644
index 00fffaf..0000000
--- a/hyperglass/ui/components/QueryLocation.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import * as React from 'react';
-import { ChakraSelect } from 'app/components';
-
-const buildLocations = networks => {
- const locations = [];
- networks.map(net => {
- const netLocations = [];
- net.locations.map(loc => {
- netLocations.push({
- label: loc.display_name,
- value: loc.name,
- group: net.display_name,
- });
- });
- locations.push({ label: net.display_name, options: netLocations });
- });
- return locations;
-};
-
-export const QueryLocation = ({ locations, onChange, label }) => {
- const options = buildLocations(locations);
- const handleChange = e => {
- const selected = [];
- e &&
- e.map(sel => {
- selected.push(sel.value);
- });
- onChange({ field: 'query_location', value: selected });
- };
- return (
-
- );
-};
diff --git a/hyperglass/ui/components/Title.js b/hyperglass/ui/components/Title.js
deleted file mode 100644
index a1f29a0..0000000
--- a/hyperglass/ui/components/Title.js
+++ /dev/null
@@ -1,115 +0,0 @@
-/** @jsx jsx */
-import { jsx } from '@emotion/core';
-import { forwardRef } from 'react';
-import { Button, Heading, Image, Stack, useColorMode } from '@chakra-ui/core';
-import { useConfig, useMedia } from 'app/context';
-
-const titleSize = { true: ['2xl', '2xl', '5xl', '5xl'], false: '2xl' };
-const titleMargin = { true: 2, false: 0 };
-const textAlignment = { false: ['right', 'center'], true: ['left', 'center'] };
-const logoName = { light: 'dark', dark: 'light' };
-const justifyMap = {
- true: ['flex-end', 'center', 'center', 'center'],
- false: ['flex-start', 'center', 'center', 'center'],
-};
-
-const logoFallback = {
- light: 'https://res.cloudinary.com/hyperglass/image/upload/v1593916013/logo-dark.svg',
- dark: 'https://res.cloudinary.com/hyperglass/image/upload/v1593916013/logo-light.svg',
-};
-
-const TitleOnly = ({ text, showSubtitle }) => (
-
- {text}
-
-);
-
-const SubtitleOnly = ({ text, mediaSize, ...props }) => (
-
- {text}
-
-);
-
-const TextOnly = ({ text, mediaSize, showSubtitle, ...props }) => (
-
-
- {showSubtitle && }
-
-);
-
-const Logo = ({ text, logo }) => {
- const { colorMode } = useColorMode();
- const { width, dark_format, light_format } = logo;
- const logoExt = { light: dark_format, dark: light_format };
- return (
-
- );
-};
-
-const LogoSubtitle = ({ text, logo, mediaSize }) => (
- <>
-
-
- >
-);
-
-const All = ({ text, logo, mediaSize, showSubtitle }) => (
- <>
-
-
- >
-);
-
-const modeMap = {
- text_only: TextOnly,
- logo_only: Logo,
- logo_subtitle: LogoSubtitle,
- all: All,
-};
-
-export const Title = forwardRef(({ onClick, isSubmitting, ...props }, ref) => {
- const { web } = useConfig();
- const { mediaSize } = useMedia();
- const titleMode = web.text.title_mode;
- const MatchedMode = modeMap[titleMode];
- return (
-
- );
-});
diff --git a/hyperglass/ui/components/form/queryLocation.tsx b/hyperglass/ui/components/form/queryLocation.tsx
new file mode 100644
index 0000000..86287c3
--- /dev/null
+++ b/hyperglass/ui/components/form/queryLocation.tsx
@@ -0,0 +1,46 @@
+import { useMemo } from 'react';
+import { Select } from '~/components';
+
+import type { TNetwork } from '~/types';
+import type { TQueryLocation, OnChangeArgs } from './types';
+
+function isOnChangeArgsArray(e: OnChangeArgs | OnChangeArgs[]): e is OnChangeArgs[] {
+ return Array.isArray(e);
+}
+
+function buildOptions(networks: TNetwork[]) {
+ return networks.map(net => {
+ const label = net.display_name;
+ const options = net.locations.map(loc => ({
+ label: loc.display_name,
+ value: loc.name,
+ group: net.display_name,
+ }));
+ return { label, options };
+ });
+}
+
+export const QueryLocation = (props: TQueryLocation) => {
+ const { locations, onChange, label } = props;
+
+ const options = useMemo(() => buildOptions(locations), [locations.length]);
+
+ function handleChange(e: OnChangeArgs | OnChangeArgs[]): void {
+ if (isOnChangeArgsArray(e)) {
+ const value = e.map(sel => sel.value as string);
+ onChange({ label: 'query_location', value });
+ }
+ }
+
+ return (
+
+ );
+};
diff --git a/hyperglass/ui/components/form/types.ts b/hyperglass/ui/components/form/types.ts
index cbb2f74..71f7517 100644
--- a/hyperglass/ui/components/form/types.ts
+++ b/hyperglass/ui/components/form/types.ts
@@ -1,5 +1,6 @@
import type { FormControlProps } from '@chakra-ui/react';
import type { FieldError } from 'react-hook-form';
+import type { TNetwork } from '~/types';
export interface TField extends FormControlProps {
name: string;
@@ -9,3 +10,11 @@ export interface TField extends FormControlProps {
labelAddOn?: React.ReactNode;
fieldAddOn?: React.ReactNode;
}
+
+export type OnChangeArgs = { label: string; value: string | string[] };
+
+export interface TQueryLocation {
+ locations: TNetwork[];
+ onChange(f: OnChangeArgs | OnChangeArgs[]): void;
+ label: string;
+}
diff --git a/hyperglass/ui/components/help/index.ts b/hyperglass/ui/components/help/index.ts
new file mode 100644
index 0000000..133aa74
--- /dev/null
+++ b/hyperglass/ui/components/help/index.ts
@@ -0,0 +1 @@
+export * from './modal';
diff --git a/hyperglass/ui/components/help/modal.tsx b/hyperglass/ui/components/help/modal.tsx
new file mode 100644
index 0000000..4e367f8
--- /dev/null
+++ b/hyperglass/ui/components/help/modal.tsx
@@ -0,0 +1,63 @@
+import dynamic from 'next/dynamic';
+import {
+ IconButton,
+ Modal,
+ ModalOverlay,
+ ModalContent,
+ ModalHeader,
+ ModalBody,
+ ModalCloseButton,
+ useDisclosure,
+} from '@chakra-ui/react';
+import { motion, AnimatePresence } from 'framer-motion';
+import { Markdown } from '~/components';
+import { useColorValue } from '~/context';
+
+import type { THelpModal } from './types';
+
+const Info = dynamic(() => import('@meronex/icons/fi').then(i => i.FiInfo));
+
+export const HelpModal = (props: THelpModal) => {
+ const { item, name, ...rest } = props;
+ const { isOpen, onOpen, onClose } = useDisclosure();
+ const bg = useColorValue('whiteFaded.50', 'blackFaded.800');
+ const color = useColorValue('black', 'white');
+ return (
+ <>
+
+
+ }
+ onClick={onOpen}
+ colorScheme="primary"
+ aria-label={`${name}_help`}
+ />
+
+
+
+
+
+ {item.params.title}
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/hyperglass/ui/components/help/types.ts b/hyperglass/ui/components/help/types.ts
new file mode 100644
index 0000000..181604a
--- /dev/null
+++ b/hyperglass/ui/components/help/types.ts
@@ -0,0 +1,7 @@
+import type { ModalContentProps } from '@chakra-ui/react';
+import type { TQueryContent, TQueryFields } from '~/types';
+
+export interface THelpModal extends ModalContentProps {
+ item: TQueryContent;
+ name: TQueryFields;
+}
diff --git a/hyperglass/ui/components/index.ts b/hyperglass/ui/components/index.ts
index f8deda0..da81584 100644
--- a/hyperglass/ui/components/index.ts
+++ b/hyperglass/ui/components/index.ts
@@ -1,6 +1,5 @@
export * from './buttons';
export * from './card';
-export * from './ChakraSelect';
export * from './codeBlock';
export * from './CommunitySelect';
export * from './countdown';
@@ -9,21 +8,20 @@ export * from './footer';
export * from './form';
export * from './greeting';
export * from './header';
-export * from './HelpModal';
+export * from './help';
export * from './HyperglassForm';
export * from './label';
export * from './layout';
export * from './loading';
-export * from './LookingGlass';
export * from './markdown';
export * from './meta';
export * from './output';
-export * from './QueryLocation';
export * from './QueryTarget';
export * from './QueryType';
export * from './QueryVrf';
export * from './ResolvedTarget';
export * from './results';
+export * from './select';
export * from './table';
-export * from './Title';
+export * from './title';
export * from './util';
diff --git a/hyperglass/ui/components/layout.tsx b/hyperglass/ui/components/layout/frame.tsx
similarity index 92%
rename from hyperglass/ui/components/layout.tsx
rename to hyperglass/ui/components/layout/frame.tsx
index 055c9c9..846ebed 100644
--- a/hyperglass/ui/components/layout.tsx
+++ b/hyperglass/ui/components/layout/frame.tsx
@@ -4,7 +4,9 @@ import { useConfig, useColorValue, useGlobalState } from '~/context';
import { If, Debugger, Greeting, Footer, Header } from '~/components';
import { useGreeting } from '~/hooks';
-export const Layout: React.FC = props => {
+import type { TFrame } from './types';
+
+export const Frame = (props: TFrame) => {
const { web, developer_mode } = useConfig();
const { isSubmitting, formData } = useGlobalState();
const [greetingAck, setGreetingAck] = useGreeting();
@@ -27,9 +29,10 @@ export const Layout: React.FC = props => {
bg={bg}
w="100%"
color={color}
+ flexDir="column"
minHeight="100vh"
ref={containerRef}
- flexDirection="column">
+ {...props}>
diff --git a/hyperglass/ui/components/layout/index.ts b/hyperglass/ui/components/layout/index.ts
new file mode 100644
index 0000000..aee1748
--- /dev/null
+++ b/hyperglass/ui/components/layout/index.ts
@@ -0,0 +1,2 @@
+export * from './frame';
+export * from './layout';
diff --git a/hyperglass/ui/components/layout/layout.tsx b/hyperglass/ui/components/layout/layout.tsx
new file mode 100644
index 0000000..bf1e200
--- /dev/null
+++ b/hyperglass/ui/components/layout/layout.tsx
@@ -0,0 +1,42 @@
+import { motion, AnimatePresence } from 'framer-motion';
+import { If, HyperglassForm, Results } from '~/components';
+import { useGlobalState } from '~/context';
+import { all } from '~/util';
+import { Frame } from './frame';
+
+export const Layout: React.FC = () => {
+ const { isSubmitting, formData } = useGlobalState();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/hyperglass/ui/components/layout/types.ts b/hyperglass/ui/components/layout/types.ts
new file mode 100644
index 0000000..275c7a9
--- /dev/null
+++ b/hyperglass/ui/components/layout/types.ts
@@ -0,0 +1,3 @@
+import type { FlexProps } from '@chakra-ui/react';
+
+export interface TFrame extends FlexProps {}
diff --git a/hyperglass/ui/components/results/group.tsx b/hyperglass/ui/components/results/group.tsx
index bbaa748..a7825fd 100644
--- a/hyperglass/ui/components/results/group.tsx
+++ b/hyperglass/ui/components/results/group.tsx
@@ -8,7 +8,7 @@ import { Result } from './individual';
import type { TResults } from './types';
export const Results = (props: TResults) => {
- const { queryLocation, queryType, queryVrf, queryTarget, setSubmitting, ...rest } = props;
+ const { queryLocation, queryType, queryVrf, queryTarget, ...rest } = props;
const { request_timeout, devices, queries, vrfs, web } = useConfig();
const targetBg = useToken('colors', 'teal.600');
const queryBg = useToken('colors', 'cyan.500');
diff --git a/hyperglass/ui/components/results/header.tsx b/hyperglass/ui/components/results/header.tsx
index 53029f8..bb40851 100644
--- a/hyperglass/ui/components/results/header.tsx
+++ b/hyperglass/ui/components/results/header.tsx
@@ -1,6 +1,6 @@
import dynamic from 'next/dynamic';
import { forwardRef, useMemo } from 'react';
-import { AccordionIcon, Icon, Spinner, Stack, Text, Tooltip, useColorMode } from '@chakra-ui/react';
+import { AccordionIcon, Icon, Spinner, Stack, Text, Tooltip } from '@chakra-ui/react';
import { useConfig, useColorValue } from '~/context';
import { useStrf } from '~/hooks';
diff --git a/hyperglass/ui/components/results/types.ts b/hyperglass/ui/components/results/types.ts
index e45dbd5..4128984 100644
--- a/hyperglass/ui/components/results/types.ts
+++ b/hyperglass/ui/components/results/types.ts
@@ -32,7 +32,6 @@ export interface TResult {
}
export interface TResults extends BoxProps {
- setSubmitting(v: boolean): boolean;
queryType: TQueryTypes;
queryLocation: string[];
queryTarget: string;
diff --git a/hyperglass/ui/components/select/index.ts b/hyperglass/ui/components/select/index.ts
new file mode 100644
index 0000000..c739673
--- /dev/null
+++ b/hyperglass/ui/components/select/index.ts
@@ -0,0 +1 @@
+export * from './select';
diff --git a/hyperglass/ui/components/select/select.tsx b/hyperglass/ui/components/select/select.tsx
new file mode 100644
index 0000000..4d733a7
--- /dev/null
+++ b/hyperglass/ui/components/select/select.tsx
@@ -0,0 +1,81 @@
+import { createContext, useContext, useMemo, useState } from 'react';
+import ReactSelect from 'react-select';
+import { Box } from '@chakra-ui/react';
+import { useColorMode } from '~/context';
+import {
+ useRSTheme,
+ useMenuStyle,
+ useMenuPortal,
+ useOptionStyle,
+ useControlStyle,
+ useMenuListStyle,
+ useMultiValueStyle,
+ usePlaceholderStyle,
+ useSingleValueStyle,
+ useMultiValueLabelStyle,
+ useMultiValueRemoveStyle,
+ useIndicatorSeparatorStyle,
+} from './styles';
+
+import type { TSelect, TSelectOption, TSelectContext, TBoxAsReactSelect } from './types';
+
+const SelectContext = createContext(Object());
+export const useSelectContext = () => useContext(SelectContext);
+
+const ReactSelectAsBox = (props: TBoxAsReactSelect) => ;
+
+export const Select = (props: TSelect) => {
+ const { ctl, options, multi, onSelect, ...rest } = props;
+ const [isOpen, setIsOpen] = useState(false);
+ const { colorMode } = useColorMode();
+
+ const selectContext = useMemo(() => ({ colorMode, isOpen }), [colorMode, isOpen]);
+
+ const handleChange = (changed: TSelectOption | TSelectOption[]) => {
+ if (!Array.isArray(changed)) {
+ changed = [changed];
+ }
+ if (typeof onSelect === 'function') {
+ onSelect(changed);
+ }
+ };
+
+ const multiValue = useMultiValueStyle({ colorMode });
+ const multiValueLabel = useMultiValueLabelStyle({ colorMode });
+ const multiValueRemove = useMultiValueRemoveStyle({ colorMode });
+ const menuPortal = useMenuPortal({ colorMode });
+ const rsTheme = useRSTheme({ colorMode });
+
+ return (
+
+ {
+ isOpen && setIsOpen(false);
+ }}
+ onMenuOpen={() => {
+ !isOpen && setIsOpen(true);
+ }}
+ theme={rsTheme}
+ styles={{
+ menuPortal,
+ multiValue,
+ multiValueLabel,
+ multiValueRemove,
+ menu: useMenuStyle,
+ option: useOptionStyle,
+ control: useControlStyle,
+ menuList: useMenuListStyle,
+ singleValue: useSingleValueStyle,
+ placeholder: usePlaceholderStyle,
+ indicatorSeparator: useIndicatorSeparatorStyle,
+ }}
+ {...rest}
+ />
+
+ );
+};
diff --git a/hyperglass/ui/components/select/styles.tsx b/hyperglass/ui/components/select/styles.tsx
new file mode 100644
index 0000000..95fc4b9
--- /dev/null
+++ b/hyperglass/ui/components/select/styles.tsx
@@ -0,0 +1,221 @@
+import { useCallback, useMemo } from 'react';
+import { useToken } from '@chakra-ui/react';
+import { mergeWith } from '@chakra-ui/utils';
+import { useOpposingColor } from '~/hooks';
+import { useColorValue, useMobile } from '~/context';
+import { useSelectContext } from './select';
+
+import type {
+ TControl,
+ TIndicator,
+ TMenu,
+ TMenuList,
+ TMultiValueState,
+ TOption,
+ TPlaceholder,
+ TStyles,
+ TRSTheme,
+ TMultiValue,
+} from './types';
+
+export const useControlStyle = (base: TStyles, state: TControl): TStyles => {
+ const { isFocused } = state;
+ const { colorMode } = useSelectContext();
+ const borderHover = useColorValue(
+ useToken('colors', 'gray.300'),
+ useToken('colors', 'whiteAlpha.200'),
+ );
+ const focusBorder = useToken('colors', 'secondary.500');
+ const invalidBorder = useColorValue(useToken('colors', 'red.500'), useToken('colors', 'red.300'));
+
+ const borderRadius = useToken('radii', 'md');
+ const minHeight = useToken('sizes', 'lg');
+ const color = useColorValue(useToken('colors', 'black'), useToken('colors', 'whiteAlpha.800'));
+ const backgroundColor = useColorValue(
+ useToken('colors', 'white'),
+ useToken('colors', 'whiteAlpha.100'),
+ );
+ const styles = {
+ backgroundColor,
+ borderRadius,
+ color,
+ minHeight,
+ '&:hover': { borderColor: isFocused ? focusBorder : borderHover },
+ '&:hover > div > span': { backgroundColor: borderHover },
+
+ '&.invalid': { borderColor: invalidBorder, boxShadow: `0 0 0 1px ${invalidBorder}` },
+ };
+ return useMemo(() => mergeWith({}, base, styles), [colorMode, isFocused]);
+};
+
+export const useMenuStyle = (base: TStyles, state: TMenu): TStyles => {
+ const { colorMode, isOpen } = useSelectContext();
+ const backgroundColor = useColorValue(
+ useToken('colors', 'white'),
+ useToken('colors', 'whiteFaded.50'),
+ );
+ const borderRadius = useToken('radii', 'md');
+ const styles = { borderRadius, backgroundColor };
+ return useMemo(() => mergeWith({}, base, styles), [colorMode, isOpen]);
+};
+
+export const useMenuListStyle = (base: TStyles, state: TMenuList): TStyles => {
+ const { colorMode, isOpen } = useSelectContext();
+
+ const scrollbarTrack = useColorValue(
+ useToken('colors', 'blackAlpha.50'),
+ useToken('colors', 'whiteAlpha.50'),
+ );
+ const scrollbarThumb = useColorValue(
+ useToken('colors', 'blackAlpha.300'),
+ useToken('colors', 'whiteAlpha.300'),
+ );
+
+ const scrollbarThumbHover = useColorValue(
+ useToken('colors', 'blackAlpha.400'),
+ useToken('colors', 'whiteAlpha.400'),
+ );
+
+ const styles = {
+ '&::-webkit-scrollbar': { width: '5px' },
+ '&::-webkit-scrollbar-track': { backgroundColor: scrollbarTrack },
+ '&::-webkit-scrollbar-thumb': { backgroundColor: scrollbarThumb },
+ '&::-webkit-scrollbar-thumb:hover': { backgroundColor: scrollbarThumbHover },
+ '-ms-overflow-style': { display: 'none' },
+ };
+ return useMemo(() => mergeWith({}, base, styles), [colorMode, isOpen]);
+};
+
+export const useOptionStyle = (base: TStyles, state: TOption): TStyles => {
+ const { isFocused } = state;
+ const { colorMode, isOpen } = useSelectContext();
+ const fontSize = useToken('fontSizes', 'lg');
+ const disabled = useToken('colors', 'whiteAlpha.400');
+ const selected = useColorValue(
+ useToken('colors', 'blackAlpha.400'),
+ useToken('colors', 'whiteAlpha.400'),
+ );
+ const focused = useColorValue(
+ useToken('colors', 'primary.500'),
+ useToken('colors', 'primary.300'),
+ );
+ const active = useColorValue(
+ useToken('colors', 'primary.600'),
+ useToken('colors', 'primary.400'),
+ );
+ const disabledColor = useOpposingColor(disabled);
+ const selectedColor = useOpposingColor(selected);
+ const focusedColor = useOpposingColor(focused);
+ const activeColor = useOpposingColor(active);
+
+ const styles = {
+ backgroundColor: state.isDisabled
+ ? disabled
+ : state.isSelected
+ ? selected
+ : state.isFocused
+ ? focused
+ : 'transparent',
+ color: state.isDisabled
+ ? disabledColor
+ : state.isSelected
+ ? selectedColor
+ : state.isFocused
+ ? focusedColor
+ : 'transparent',
+ fontSize,
+ '&:focus': { backgroundColor: active, color: activeColor },
+ '&:active': { backgroundColor: active, color: activeColor },
+ };
+ return useMemo(() => mergeWith({}, base, styles), [colorMode, isFocused, isOpen]);
+};
+
+export const useIndicatorSeparatorStyle = (base: TStyles, state: TIndicator): TStyles => {
+ const { colorMode } = useSelectContext();
+ const backgroundColor = useColorValue(
+ useToken('colors', 'whiteAlpha.700'),
+ useToken('colors', 'gray.600'),
+ );
+ const styles = { backgroundColor };
+ return useMemo(() => mergeWith({}, base, styles), [colorMode]);
+};
+
+export const usePlaceholderStyle = (base: TStyles, state: TPlaceholder): TStyles => {
+ const { colorMode } = useSelectContext();
+ const color = useColorValue(useToken('colors', 'whiteAlpha.700'), useToken('colors', 'gray.600'));
+ return useMemo(() => mergeWith({}, base, { color }), [colorMode]);
+};
+
+export const useSingleValueStyle = (props: TStyles) => {
+ const { colorMode } = useSelectContext();
+
+ const color = useColorValue('black', 'whiteAlpha.800');
+ const fontSize = useToken('fontSizes', 'lg');
+
+ const styles = { color, fontSize };
+ return useCallback((base: TStyles, state: TMultiValueState) => mergeWith({}, base, styles), [
+ color,
+ colorMode,
+ ]);
+};
+
+export const useMultiValueStyle = (props: TMultiValue) => {
+ const { colorMode } = props;
+
+ const backgroundColor = useColorValue(
+ useToken('colors', 'primary.500'),
+ useToken('colors', 'primary.300'),
+ );
+ const color = useOpposingColor(backgroundColor);
+ const styles = { backgroundColor, color };
+
+ return useCallback((base: TStyles, state: TMultiValueState) => mergeWith({}, base, styles), [
+ backgroundColor,
+ colorMode,
+ ]);
+};
+
+export const useMultiValueLabelStyle = (props: TMultiValue) => {
+ const { colorMode } = props;
+ const backgroundColor = useColorValue(
+ useToken('colors', 'primary.500'),
+ useToken('colors', 'primary.300'),
+ );
+ const color = useOpposingColor(backgroundColor);
+ const styles = { color };
+
+ return useCallback((base: TStyles, state: TMultiValueState) => mergeWith({}, base, styles), [
+ colorMode,
+ ]);
+};
+
+export const useMultiValueRemoveStyle = (props: TMultiValue) => {
+ const { colorMode } = props;
+ const backgroundColor = useColorValue(
+ useToken('colors', 'primary.500'),
+ useToken('colors', 'primary.300'),
+ );
+ const color = useOpposingColor(backgroundColor);
+ const styles = {
+ color,
+ '&:hover': { backgroundColor: 'inherit', color, opacity: 0.7 },
+ };
+ return useCallback((base: TStyles, state: TMultiValueState) => mergeWith({}, base, styles), [
+ colorMode,
+ ]);
+};
+
+export const useRSTheme = (props: TMultiValue) => {
+ const borderRadius = useToken('radii', 'md');
+ return useCallback((t: TRSTheme): TRSTheme => ({ ...t, borderRadius }), []);
+};
+
+export const useMenuPortal = (props: TMultiValue) => {
+ const isMobile = useMobile();
+ const styles = {
+ zIndex: isMobile ? 1500 : 1,
+ };
+ return useCallback((base: TStyles, state: TMultiValueState) => mergeWith({}, base, styles), [
+ isMobile,
+ ]);
+};
diff --git a/hyperglass/ui/components/select/types.ts b/hyperglass/ui/components/select/types.ts
new file mode 100644
index 0000000..a1af0ca
--- /dev/null
+++ b/hyperglass/ui/components/select/types.ts
@@ -0,0 +1,81 @@
+import type {
+ Props as IReactSelect,
+ ControlProps,
+ MenuProps,
+ MenuListComponentProps,
+ OptionProps,
+ MultiValueProps,
+ IndicatorProps,
+ Theme,
+ PlaceholderProps,
+} from 'react-select';
+import type { BoxProps } from '@chakra-ui/react';
+import type { ColorNames } from '~/types';
+
+export interface TSelectState {
+ [k: string]: string[];
+}
+
+export type TSelectOption = {
+ label: string;
+ value: string;
+};
+
+export type TSelectOptionGroup = {
+ label: string;
+ options: TSelectOption[];
+};
+
+export type TOptions = Array;
+
+export type TBoxAsReactSelect = Omit &
+ Omit;
+
+export interface TSelect extends TBoxAsReactSelect {
+ options: TOptions;
+ name: string;
+ required?: boolean;
+ multi?: boolean;
+ onSelect?: (v: TSelectOption[]) => void;
+ onChange?: (c: TSelectOption | TSelectOption[]) => void;
+ colorScheme?: ColorNames;
+}
+
+export interface TSelectContext {
+ colorMode: 'light' | 'dark';
+ isOpen: boolean;
+}
+
+export interface TMultiValueRemoveProps {
+ children: Node;
+ data: any;
+ innerProps: {
+ className: string;
+ onTouchEnd: (e: any) => void;
+ onClick: (e: any) => void;
+ onMouseDown: (e: any) => void;
+ };
+ selectProps: any;
+}
+
+export interface TRSTheme extends Omit {
+ borderRadius: string | number;
+}
+
+export type TControl = ControlProps;
+
+export type TMenu = MenuProps;
+
+export type TMenuList = MenuListComponentProps;
+
+export type TOption = OptionProps;
+
+export type TMultiValueState = MultiValueProps;
+
+export type TIndicator = IndicatorProps;
+
+export type TPlaceholder = PlaceholderProps;
+
+export type TMultiValue = Pick;
+
+export type { Styles as TStyles } from 'react-select';
diff --git a/hyperglass/ui/components/title/index.ts b/hyperglass/ui/components/title/index.ts
new file mode 100644
index 0000000..f71556e
--- /dev/null
+++ b/hyperglass/ui/components/title/index.ts
@@ -0,0 +1 @@
+export * from './title';
diff --git a/hyperglass/ui/components/title/logo.tsx b/hyperglass/ui/components/title/logo.tsx
new file mode 100644
index 0000000..a2a1b7e
--- /dev/null
+++ b/hyperglass/ui/components/title/logo.tsx
@@ -0,0 +1,33 @@
+import { Image } from '@chakra-ui/react';
+import { useColorValue, useConfig } from '~/context';
+
+import type { TLogo } from './types';
+
+export const Logo = (props: TLogo) => {
+ const { web } = useConfig();
+ const { width, dark_format, light_format } = web.logo;
+
+ const src = useColorValue(`/images/dark${dark_format}`, `/images/light${light_format}`);
+ const fallbackSrc = useColorValue(
+ 'https://res.cloudinary.com/hyperglass/image/upload/v1593916013/logo-dark.svg',
+ 'https://res.cloudinary.com/hyperglass/image/upload/v1593916013/logo-light.svg',
+ );
+
+ return (
+
+ );
+};
diff --git a/hyperglass/ui/components/title/subtitleOnly.tsx b/hyperglass/ui/components/title/subtitleOnly.tsx
new file mode 100644
index 0000000..85f84e2
--- /dev/null
+++ b/hyperglass/ui/components/title/subtitleOnly.tsx
@@ -0,0 +1,18 @@
+import { Heading } from '@chakra-ui/react';
+import { useConfig } from '~/context';
+
+import type { TSubtitleOnly } from './types';
+
+export const SubtitleOnly = (props: TSubtitleOnly) => {
+ const { web } = useConfig();
+ return (
+
+ {web.text.subtitle}
+
+ );
+};
diff --git a/hyperglass/ui/components/title/title.tsx b/hyperglass/ui/components/title/title.tsx
new file mode 100644
index 0000000..75e8fe9
--- /dev/null
+++ b/hyperglass/ui/components/title/title.tsx
@@ -0,0 +1,85 @@
+import { forwardRef } from 'react';
+import { Button, Stack } from '@chakra-ui/react';
+import { If } from '~/components';
+import { useConfig, useGlobalState } from '~/context';
+import { useBooleanValue } from '~/hooks';
+import { TitleOnly } from './titleOnly';
+import { SubtitleOnly } from './subtitleOnly';
+import { Logo } from './logo';
+
+import type { TTitle, TTextOnly } from './types';
+
+const TextOnly = (props: TTextOnly) => {
+ const { showSubtitle, ...rest } = props;
+ const textAlign = useBooleanValue(
+ showSubtitle,
+ { base: 'right', md: 'center' },
+ { base: 'left', md: 'center' },
+ );
+ return (
+
+
+
+
+
+
+ );
+};
+
+const LogoSubtitle = () => (
+ <>
+
+
+ >
+);
+
+const All = (props: TTextOnly) => {
+ const { showSubtitle, ...rest } = props;
+ return (
+ <>
+
+
+ >
+ );
+};
+
+export const Title = forwardRef((props, ref) => {
+ const { web } = useConfig();
+ const titleMode = web.text.title_mode;
+
+ const { isSubmitting } = useGlobalState();
+
+ const justify = useBooleanValue(
+ isSubmitting.value,
+ { base: 'flex-end', md: 'center' },
+ { base: 'flex-start', md: 'center' },
+ );
+
+ return (
+
+ );
+});
diff --git a/hyperglass/ui/components/title/titleOnly.tsx b/hyperglass/ui/components/title/titleOnly.tsx
new file mode 100644
index 0000000..eb842cc
--- /dev/null
+++ b/hyperglass/ui/components/title/titleOnly.tsx
@@ -0,0 +1,17 @@
+import { Heading } from '@chakra-ui/react';
+import { useConfig } from '~/context';
+import { useBooleanValue } from '~/hooks';
+
+import type { TTitleOnly } from './types';
+
+export const TitleOnly = (props: TTitleOnly) => {
+ const { showSubtitle, ...rest } = props;
+ const { web } = useConfig();
+ const fontSize = useBooleanValue(showSubtitle, { base: '2xl', lg: '5xl' }, '2xl');
+ const margin = useBooleanValue(showSubtitle, 2, 0);
+ return (
+
+ {web.text.title}
+
+ );
+};
diff --git a/hyperglass/ui/components/title/types.ts b/hyperglass/ui/components/title/types.ts
new file mode 100644
index 0000000..426a5c0
--- /dev/null
+++ b/hyperglass/ui/components/title/types.ts
@@ -0,0 +1,15 @@
+import type { ButtonProps, HeadingProps, ImageProps, StackProps } from '@chakra-ui/react';
+
+export interface TTitle extends ButtonProps {}
+
+export interface TTitleOnly extends HeadingProps {
+ showSubtitle: boolean;
+}
+
+export interface TSubtitleOnly extends HeadingProps {}
+
+export interface TLogo extends ImageProps {}
+
+export interface TTextOnly extends StackProps {
+ showSubtitle: boolean;
+}
diff --git a/hyperglass/ui/context/MediaProvider.js b/hyperglass/ui/context/MediaProvider.js
deleted file mode 100644
index d56f258..0000000
--- a/hyperglass/ui/context/MediaProvider.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import * as React from 'react';
-import { createContext, useContext, useMemo } from 'react';
-import { useMediaLayout } from 'use-media';
-
-const MediaContext = createContext(null);
-
-export const MediaProvider = ({ theme, children }) => {
- const { sm, md, lg, xl } = theme.breakpoints;
- const isSm = useMediaLayout({ maxWidth: md });
- const isMd = useMediaLayout({ minWidth: md, maxWidth: lg });
- const isLg = useMediaLayout({ minWidth: lg, maxWidth: xl });
- const isXl = useMediaLayout({ minWidth: xl });
- let mediaSize = false;
- switch (true) {
- case isSm:
- mediaSize = 'sm';
- break;
- case isMd:
- mediaSize = 'md';
- break;
- case isLg:
- mediaSize = 'lg';
- break;
- case isXl:
- mediaSize = 'xl';
- break;
- }
- const value = useMemo(
- () => ({
- isSm: isSm,
- isMd: isMd,
- isLg: isLg,
- isXl: isXl,
- mediaSize: mediaSize,
- }),
- [isSm, isMd, isLg, isXl, mediaSize],
- );
- return {children};
-};
-
-export const useMedia = () => useContext(MediaContext);
diff --git a/hyperglass/ui/pages/index.js b/hyperglass/ui/pages/index.js
index 57bbb4a..107c2d0 100644
--- a/hyperglass/ui/pages/index.js
+++ b/hyperglass/ui/pages/index.js
@@ -2,12 +2,10 @@ import * as React from 'react';
import Head from 'next/head';
import dynamic from 'next/dynamic';
import { Meta, Loading } from 'app/components';
-const LookingGlass = dynamic(
- () => import('app/components/LookingGlass').then(i => i.LookingGlass),
- {
- loading: Loading,
- },
-);
+
+const Layout = dynamic(() => import('~/components').then(i => i.Layout), {
+ loading: Loading,
+});
const Index = ({ faviconComponents }) => {
return (
@@ -18,7 +16,7 @@ const Index = ({ faviconComponents }) => {
))}
-
+
>
);
};
diff --git a/hyperglass/ui/types/config.ts b/hyperglass/ui/types/config.ts
index 576cecb..ee8c2c8 100644
--- a/hyperglass/ui/types/config.ts
+++ b/hyperglass/ui/types/config.ts
@@ -1,5 +1,7 @@
import { Colors, Fonts } from './theme';
+export type TQueryFields = 'query_type' | 'query_target' | 'query_location' | 'query_vrf';
+
export interface IConfigMessages {
no_input: string;
acl_denied: string;
@@ -49,13 +51,20 @@ export interface TConfigGreeting {
required: boolean;
}
+export interface TConfigWebLogo {
+ width: string;
+ height: string | null;
+ light_format: string;
+ dark_format: string;
+}
+
export interface IConfigWeb {
credit: { enable: boolean };
dns_provider: { name: string; url: string };
external_link: { enable: boolean; title: string; url: string };
greeting: TConfigGreeting;
help_menu: { enable: boolean; title: string };
- logo: { width: string; height: string | null; light_format: string; dark_format: string };
+ logo: TConfigWebLogo;
terms: { enable: boolean; title: string };
text: IConfigWebText;
theme: IConfigTheme;
@@ -111,18 +120,18 @@ export interface TDevice extends TDeviceBase {
vrfs: IDeviceVrf[];
}
-export interface INetworkLocation extends TDeviceBase {
+export interface TNetworkLocation extends TDeviceBase {
vrfs: IDeviceVrfBase[];
}
-export interface INetwork {
+export interface TNetwork {
display_name: string;
- locations: INetworkLocation[];
+ locations: TNetworkLocation[];
}
export type TParsedDataField = [string, keyof TRoute, 'left' | 'right' | 'center' | null];
-export interface IQueryContent {
+export interface TQueryContent {
content: string;
enable: boolean;
params: {
@@ -141,11 +150,11 @@ export interface IConfigContent {
greeting: string;
vrf: {
[k: string]: {
- bgp_route: IQueryContent;
- bgp_community: IQueryContent;
- bgp_aspath: IQueryContent;
- ping: IQueryContent;
- traceroute: IQueryContent;
+ bgp_route: TQueryContent;
+ bgp_community: TQueryContent;
+ bgp_aspath: TQueryContent;
+ ping: TQueryContent;
+ traceroute: TQueryContent;
};
};
}
@@ -166,6 +175,7 @@ export interface IConfig {
hyperglass_version: string;
queries: IConfigQueries;
devices: TDevice[];
+ networks: TNetwork[];
vrfs: IDeviceVrfBase[];
parsed_data_fields: TParsedDataField[];
content: IConfigContent;
diff --git a/hyperglass/ui/types/data.ts b/hyperglass/ui/types/data.ts
index a5db302..fb780bb 100644
--- a/hyperglass/ui/types/data.ts
+++ b/hyperglass/ui/types/data.ts
@@ -1,4 +1,4 @@
-export type TQueryTypes = 'bgp_route' | 'bgp_community' | 'bgp_aspath' | 'ping' | 'traceroute';
+export type TQueryTypes = '' | 'bgp_route' | 'bgp_community' | 'bgp_aspath' | 'ping' | 'traceroute';
export interface IFormData {
query_location: string[];
diff --git a/hyperglass/ui/types/globals.d.ts b/hyperglass/ui/types/globals.d.ts
index 40f588a..fd8d66c 100644
--- a/hyperglass/ui/types/globals.d.ts
+++ b/hyperglass/ui/types/globals.d.ts
@@ -62,5 +62,5 @@ declare global {
type Animated = Omit &
Omit & { transition?: MotionProps['transition'] };
- type MeronexIcon = import('@meronex/icons').IconType;
+ type MeronexIcon = import('@meronex/icons').IconBaseProps;
}
diff --git a/hyperglass/ui/types/theme.ts b/hyperglass/ui/types/theme.ts
index 6a4d089..0df1102 100644
--- a/hyperglass/ui/types/theme.ts
+++ b/hyperglass/ui/types/theme.ts
@@ -35,10 +35,10 @@ interface CustomColors {
}
type AllColors = CustomColors & ChakraColors;
-type ColorKey = keyof AllColors;
+export type ColorNames = keyof AllColors;
export interface Colors extends AllColors {
- original: { [key in ColorKey]: string };
+ original: { [key in ColorNames]: string };
}
export interface Fonts {
diff --git a/hyperglass/ui/util/common.ts b/hyperglass/ui/util/common.ts
new file mode 100644
index 0000000..354651c
--- /dev/null
+++ b/hyperglass/ui/util/common.ts
@@ -0,0 +1,8 @@
+export function all(...iter: any[]) {
+ for (let i of iter) {
+ if (!i) {
+ return false;
+ }
+ }
+ return true;
+}
diff --git a/hyperglass/ui/util/index.ts b/hyperglass/ui/util/index.ts
index 293e236..7df1a9d 100644
--- a/hyperglass/ui/util/index.ts
+++ b/hyperglass/ui/util/index.ts
@@ -1,2 +1,3 @@
+export * from './common';
export * from './formatters';
export * from './theme';