1
0
mirror of https://github.com/checktheroads/hyperglass synced 2024-05-11 05:55:08 +00:00

continue typescript & chakra v1 migrations [skip ci]

This commit is contained in:
checktheroads
2020-11-30 00:21:00 -07:00
parent a2a7f3407b
commit 781a2608a0
36 changed files with 774 additions and 530 deletions

View File

@@ -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 (
<Select
ref={ref}
styles={{
container: base => ({
...base,
minHeight: height,
borderRadius: borderRadius,
width: '100%',
}),
control: (base, state) => ({
...base,
minHeight: height,
backgroundColor: bg[colorMode],
color: color[colorMode],
borderColor: state.isDisabled
? borderDisabled
: state.isFocused
? borderFocused
: border[colorMode],
borderRadius: borderRadius,
'&:hover': {
borderColor: hoverColor[colorMode],
},
}),
menu: base => ({
...base,
backgroundColor: menuBg[colorMode],
borderRadius: borderRadius,
}),
menuList: base => ({
...base,
'&::-webkit-scrollbar': { width: '5px' },
'&::-webkit-scrollbar-track': {
backgroundColor: scrollbarBg[colorMode],
},
'&::-webkit-scrollbar-thumb': {
backgroundColor: scrollbar[colorMode],
},
'&::-webkit-scrollbar-thumb:hover': {
backgroundColor: scrollbarHover[colorMode],
},
'-ms-overflow-style': { display: 'none' },
}),
option: (base, state) => ({
...base,
backgroundColor: state.isDisabled
? selectedDisabled
: state.isSelected
? optionSelectedBg[colorMode]
: state.isFocused
? colorSetPrimaryBg[colorMode]
: 'transparent',
color: state.isDisabled
? selectedDisabled
: state.isFocused
? colorSetPrimaryColor
: state.isSelected
? optionSelectedColor
: menuColor[colorMode],
fontSize: theme.fontSizes[size],
'&:active': {
backgroundColor: optionBgActive[colorMode],
color: optionBgColor,
},
}),
indicatorSeparator: base => ({
...base,
backgroundColor: placeholderColor[colorMode],
}),
dropdownIndicator: base => ({
...base,
color: placeholderColor[colorMode],
'&:hover': {
color: color[colorMode],
},
}),
valueContainer: base => ({
...base,
paddingLeft: theme.space[4],
paddingRight: theme.space[4],
}),
multiValue: base => ({
...base,
backgroundColor: colorSetPrimaryBg[colorMode],
}),
multiValueLabel: base => ({
...base,
color: colorSetPrimaryColor,
}),
multiValueRemove: base => ({
...base,
color: colorSetPrimaryColor,
'&:hover': {
color: colorSetPrimaryColor,
backgroundColor: 'inherit',
},
}),
singleValue: base => ({
...base,
color: color[colorMode],
fontSize: theme.fontSizes[size],
}),
}}
placeholder={
<Text color={placeholderColor[colorMode]} fontSize={size} fontFamily={theme.fonts.body}>
{placeholder}
</Text>
}
{...props}>
{children}
</Select>
);
},
);

View File

@@ -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 (
<>
<AnimatePresence>
<AnimatedIcon
initial={{ opacity: 0, scale: 0.3, color: colors.gray[500] }}
animate={{ opacity: 1, scale: 1, color: iconColor[colorMode] }}
transition={{ duration: 0.2 }}
exit={{ opacity: 0, scale: 0.3 }}
variantColor="primary"
aria-label={`${name}_help`}
icon="info-outline"
variant="link"
size="sm"
h="unset"
w={3}
minW={3}
maxW={3}
h={3}
minH={3}
maxH={3}
ml={1}
mb={1}
onClick={onOpen}
/>
</AnimatePresence>
<Modal isOpen={isOpen} onClose={onClose} size="xl">
<ModalOverlay />
<ModalContent bg={bg[colorMode]} color={color[colorMode]} py={4} borderRadius="md">
<ModalHeader>{item.params.title}</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Markdown content={item.content} />
</ModalBody>
</ModalContent>
</Modal>
</>
);
};

View File

@@ -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 (
<Layout>
{isSubmitting && formData && (
<Results
queryLocation={formData.query_location}
queryType={formData.query_type}
queryVrf={formData.query_vrf}
queryTarget={formData.query_target}
setSubmitting={setSubmitting}
/>
)}
<AnimatePresence>
{!isSubmitting && (
<AnimatedForm
initial={{ opacity: 0, y: 300 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
exit={{ opacity: 0, x: -300 }}
isSubmitting={isSubmitting}
setSubmitting={setSubmitting}
setFormData={setFormData}
greetingAck={greetingAck}
setGreetingAck={setGreetingAck}
/>
)}
</AnimatePresence>
</Layout>
);
};

View File

@@ -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 (
<ChakraSelect
isMulti
size="lg"
options={options}
aria-label={label}
name="query_location"
onChange={handleChange}
closeMenuOnSelect={false}
/>
);
};

View File

@@ -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 }) => (
<Heading as="h1" mb={titleMargin[showSubtitle]} fontSize={titleSize[showSubtitle]}>
{text}
</Heading>
);
const SubtitleOnly = ({ text, mediaSize, ...props }) => (
<Heading
as="h3"
fontSize={['md', 'md', 'xl', 'xl']}
whiteSpace="break-spaces"
textAlign={['left', 'left', 'center', 'center']}
{...props}>
{text}
</Heading>
);
const TextOnly = ({ text, mediaSize, showSubtitle, ...props }) => (
<Stack spacing={2} maxW="100%" textAlign={textAlignment[showSubtitle]} {...props}>
<TitleOnly text={text.title} showSubtitle={showSubtitle} />
{showSubtitle && <SubtitleOnly text={text.subtitle} mediaSize={mediaSize} />}
</Stack>
);
const Logo = ({ text, logo }) => {
const { colorMode } = useColorMode();
const { width, dark_format, light_format } = logo;
const logoExt = { light: dark_format, dark: light_format };
return (
<Image
css={{
userDrag: 'none',
userSelect: 'none',
msUserSelect: 'none',
MozUserSelect: 'none',
WebkitUserDrag: 'none',
WebkitUserSelect: 'none',
}}
alt={text.title}
width={width ?? 'auto'}
fallbackSrc={logoFallback[colorMode]}
src={`/images/${logoName[colorMode]}${logoExt[colorMode]}`}
/>
);
};
const LogoSubtitle = ({ text, logo, mediaSize }) => (
<>
<Logo text={text} logo={logo} mediaSize={mediaSize} />
<SubtitleOnly mt={6} text={text.subtitle} />
</>
);
const All = ({ text, logo, mediaSize, showSubtitle }) => (
<>
<Logo text={text} logo={logo} />
<TextOnly mediaSize={mediaSize} showSubtitle={showSubtitle} mt={2} text={text} />
</>
);
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 (
<Button
px={0}
w="100%"
ref={ref}
variant="link"
flexWrap="wrap"
flexDir="column"
onClick={onClick}
_focus={{ boxShadow: 'none' }}
_hover={{ textDecoration: 'none' }}
justifyContent={justifyMap[isSubmitting]}
alignItems={['flex-start', 'flex-start', 'center']}
{...props}>
<MatchedMode
mediaSize={mediaSize}
showSubtitle={!isSubmitting}
text={web.text}
logo={web.logo}
/>
</Button>
);
});

View File

@@ -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 (
<Select
isMulti
size="lg"
options={options}
aria-label={label}
name="query_location"
onChange={handleChange}
closeMenuOnSelect={false}
/>
);
};

View File

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

View File

@@ -0,0 +1 @@
export * from './modal';

View File

@@ -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<MeronexIcon>(() => 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 (
<>
<AnimatePresence>
<motion.div
transition={{ duration: 0.2 }}
exit={{ opacity: 0, scale: 0.3 }}
animate={{ opacity: 1, scale: 1 }}
initial={{ opacity: 0, scale: 0.3 }}>
<IconButton
h={3}
w={3}
mb={1}
ml={1}
maxH={3}
maxW={3}
minH={3}
minW={3}
size="sm"
variant="link"
icon={<Info />}
onClick={onOpen}
colorScheme="primary"
aria-label={`${name}_help`}
/>
</motion.div>
</AnimatePresence>
<Modal isOpen={isOpen} onClose={onClose} size="xl">
<ModalOverlay />
<ModalContent bg={bg} color={color} py={4} borderRadius="md" {...rest}>
<ModalHeader>{item.params.title}</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Markdown content={item.content} />
</ModalBody>
</ModalContent>
</Modal>
</>
);
};

View File

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

View File

@@ -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';

View File

@@ -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}>
<Flex px={2} flex="0 1 auto" flexDirection="column">
<Header resetForm={resetForm} />
</Flex>

View File

@@ -0,0 +1,2 @@
export * from './frame';
export * from './layout';

View File

@@ -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 (
<Frame>
<If
c={
isSubmitting.value &&
all(
formData.query_location.value,
formData.query_target.value,
formData.query_type.value,
formData.query_vrf.value,
)
}>
<Results
queryLocation={formData.query_location.value}
queryType={formData.query_type.value}
queryVrf={formData.query_vrf.value}
queryTarget={formData.query_target.value}
/>
</If>
<AnimatePresence>
<If c={!isSubmitting.value}>
<motion.div
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
exit={{ opacity: 0, x: -300 }}
initial={{ opacity: 0, y: 300 }}>
<HyperglassForm />
</motion.div>
</If>
</AnimatePresence>
</Frame>
);
};

View File

@@ -0,0 +1,3 @@
import type { FlexProps } from '@chakra-ui/react';
export interface TFrame extends FlexProps {}

View File

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

View File

@@ -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';

View File

@@ -32,7 +32,6 @@ export interface TResult {
}
export interface TResults extends BoxProps {
setSubmitting(v: boolean): boolean;
queryType: TQueryTypes;
queryLocation: string[];
queryTarget: string;

View File

@@ -0,0 +1 @@
export * from './select';

View File

@@ -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<TSelectContext>(Object());
export const useSelectContext = () => useContext(SelectContext);
const ReactSelectAsBox = (props: TBoxAsReactSelect) => <Box as={ReactSelect} {...props} />;
export const Select = (props: TSelect) => {
const { ctl, options, multi, onSelect, ...rest } = props;
const [isOpen, setIsOpen] = useState<boolean>(false);
const { colorMode } = useColorMode();
const selectContext = useMemo<TSelectContext>(() => ({ 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 (
<SelectContext.Provider value={selectContext}>
<ReactSelectAsBox
as={ReactSelect}
options={options}
isMulti={multi}
onChange={handleChange}
ref={ctl}
onMenuClose={() => {
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}
/>
</SelectContext.Provider>
);
};

View File

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

View File

@@ -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<TSelectOptionGroup | TSelectOption>;
export type TBoxAsReactSelect = Omit<IReactSelect, 'isMulti' | 'onSelect' | 'onChange'> &
Omit<BoxProps, 'onChange' | 'onSelect'>;
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<Theme, 'borderRadius'> {
borderRadius: string | number;
}
export type TControl = ControlProps<TOptions>;
export type TMenu = MenuProps<TOptions>;
export type TMenuList = MenuListComponentProps<TOptions>;
export type TOption = OptionProps<TOptions>;
export type TMultiValueState = MultiValueProps<TOptions>;
export type TIndicator = IndicatorProps<TOptions>;
export type TPlaceholder = PlaceholderProps<TOptions>;
export type TMultiValue = Pick<TSelectContext, 'colorMode'>;
export type { Styles as TStyles } from 'react-select';

View File

@@ -0,0 +1 @@
export * from './title';

View File

@@ -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 (
<Image
css={{
userDrag: 'none',
userSelect: 'none',
msUserSelect: 'none',
MozUserSelect: 'none',
WebkitUserDrag: 'none',
WebkitUserSelect: 'none',
}}
fallbackSrc={fallbackSrc}
width={width ?? 'auto'}
alt={web.text.title}
src={src}
{...props}
/>
);
};

View File

@@ -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 (
<Heading
as="h3"
whiteSpace="break-spaces"
fontSize={{ base: 'md', lg: 'xl' }}
textAlign={{ base: 'left', xl: 'center' }}
{...props}>
{web.text.subtitle}
</Heading>
);
};

View File

@@ -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 (
<Stack spacing={2} maxW="100%" textAlign={textAlign} {...rest}>
<TitleOnly showSubtitle={showSubtitle} />
<If c={showSubtitle}>
<SubtitleOnly />
</If>
</Stack>
);
};
const LogoSubtitle = () => (
<>
<Logo />
<SubtitleOnly mt={6} />
</>
);
const All = (props: TTextOnly) => {
const { showSubtitle, ...rest } = props;
return (
<>
<Logo />
<TextOnly showSubtitle={showSubtitle} mt={2} {...rest} />
</>
);
};
export const Title = forwardRef<HTMLButtonElement, TTitle>((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 (
<Button
px={0}
w="100%"
ref={ref}
variant="link"
flexWrap="wrap"
flexDir="column"
justifyContent={justify}
_focus={{ boxShadow: 'none' }}
_hover={{ textDecoration: 'none' }}
alignItems={{ base: 'flex-start', lg: 'center' }}
{...props}>
<If c={titleMode === 'text_only'}>
<TextOnly showSubtitle={!isSubmitting.value} />
</If>
<If c={titleMode === 'logo_only'}>
<Logo />
</If>
<If c={titleMode === 'logo_subtitle'}>
<LogoSubtitle />
</If>
<If c={titleMode === 'all'}>
<All showSubtitle={!isSubmitting.value} />
</If>
</Button>
);
});

View File

@@ -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 (
<Heading as="h1" mb={margin} fontSize={fontSize} {...rest}>
{web.text.title}
</Heading>
);
};

View File

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

View File

@@ -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 <MediaContext.Provider value={value}>{children}</MediaContext.Provider>;
};
export const useMedia = () => useContext(MediaContext);

View File

@@ -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 }) => {
))}
</Head>
<Meta />
<LookingGlass />
<Layout />
</>
);
};

View File

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

View File

@@ -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[];

View File

@@ -62,5 +62,5 @@ declare global {
type Animated<T> = Omit<T, keyof MotionProps> &
Omit<MotionProps, keyof T> & { transition?: MotionProps['transition'] };
type MeronexIcon = import('@meronex/icons').IconType;
type MeronexIcon = import('@meronex/icons').IconBaseProps;
}

View File

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

View File

@@ -0,0 +1,8 @@
export function all(...iter: any[]) {
for (let i of iter) {
if (!i) {
return false;
}
}
return true;
}

View File

@@ -1,2 +1,3 @@
export * from './common';
export * from './formatters';
export * from './theme';