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

Closes #155: Implement User IP Button

This commit is contained in:
thatmattlove
2021-10-19 16:23:40 -07:00
parent d4db98da5e
commit f6d3dfe1dc
14 changed files with 309 additions and 14 deletions

View File

@ -153,6 +153,10 @@ class Text(HyperglassModel):
rpki_unknown: StrictStr = "No ROAs Exist"
rpki_unverified: StrictStr = "Not Verified"
no_communities: StrictStr = "No Communities"
ip_error: StrictStr = "Unable to determine IP Address"
no_ip: StrictStr = "No {protocol} Address"
ip_select: StrictStr = "Select an IP Address"
ip_button: StrictStr = "My IP"
@validator("title_mode")
def validate_title_mode(cls, value):

View File

@ -1,10 +1,12 @@
import { useMemo } from 'react';
import { Input, Text } from '@chakra-ui/react';
import { Input, InputGroup, InputRightElement, Text } from '@chakra-ui/react';
import { components } from 'react-select';
import { If, Select } from '~/components';
import { useColorValue } from '~/context';
import { useDirective, useFormState } from '~/hooks';
import { isSelectDirective } from '~/types';
import { UserIP } from './userIP';
import type { OptionProps } from 'react-select';
import type { Directive, SingleOption } from '~/types';
import type { TQueryTarget } from './types';
@ -73,19 +75,28 @@ export const QueryTarget: React.FC<TQueryTarget> = (props: TQueryTarget) => {
/>
</If>
<If c={directive === null || !isSelectDirective(directive)}>
<Input
bg={bg}
size="lg"
color={color}
borderRadius="md"
borderColor={border}
aria-label={placeholder}
placeholder={placeholder}
value={displayTarget}
name="queryTargetDisplay"
onChange={handleInputChange}
_placeholder={{ color: placeholderColor }}
/>
<InputGroup size="lg">
<Input
bg={bg}
color={color}
borderRadius="md"
borderColor={border}
value={displayTarget}
aria-label={placeholder}
placeholder={placeholder}
name="queryTargetDisplay"
onChange={handleInputChange}
_placeholder={{ color: placeholderColor }}
/>
<InputRightElement w="max-content" pr={2}>
<UserIP
setTarget={(target: string) => {
setTarget({ display: target });
onChange({ field: name, value: target });
}}
/>
</InputRightElement>
</InputGroup>
</If>
</>
);

View File

@ -34,3 +34,7 @@ export interface LocationCardProps {
onChange(a: 'add' | 'remove', v: SingleOption): void;
hasError: boolean;
}
export interface UserIPProps {
setTarget(target: string): void;
}

View File

@ -0,0 +1,102 @@
import { useMemo } from 'react';
import dynamic from 'next/dynamic';
import { Button, chakra, Stack, Text, VStack, useDisclosure } from '@chakra-ui/react';
import { Prompt } from '~/components';
import { useConfig, useColorValue } from '~/context';
import { useStrf, useWtf } from '~/hooks';
import type { UserIPProps } from './types';
const RightArrow = chakra(
dynamic<MeronexIcon>(() => import('@meronex/icons/fa').then(i => i.FaArrowCircleRight)),
);
export const UserIP = (props: UserIPProps): JSX.Element => {
const { setTarget } = props;
const { onOpen, ...disclosure } = useDisclosure();
const strF = useStrf();
const { web } = useConfig();
const errorColor = useColorValue('red.500', 'red.300');
const noIPv4 = strF(web.text.noIp, { protocol: 'IPv4' });
const noIPv6 = strF(web.text.noIp, { protocol: 'IPv6' });
const [ipv4, ipv6, query] = useWtf();
const hasResult = useMemo(
() => (!ipv4.isError || !ipv6.isError) && (ipv4.data?.ip !== null || ipv6.data?.ip !== null),
[ipv4, ipv6],
);
const show4 = useMemo(() => !ipv4.isError && ipv4.data?.ip !== null, [ipv4]);
const show6 = useMemo(() => !ipv6.isError && ipv6.data?.ip !== null, [ipv6]);
function handleOpen(): void {
onOpen();
query();
}
return (
<Prompt
trigger={
<Button size="sm" onClick={handleOpen}>
{web.text.ipButton}
</Button>
}
onOpen={handleOpen}
{...disclosure}
>
<VStack w="100%" spacing={4} justify="center">
{hasResult && (
<Text fontSize="sm" textAlign="center">
{web.text.ipSelect}
</Text>
)}
<Stack spacing={2}>
{show4 && (
<Button
size="sm"
fontSize="xs"
fontFamily="mono"
colorScheme="primary"
isDisabled={ipv4.isError}
isLoading={ipv4.isLoading}
justifyContent="space-between"
onClick={() => {
ipv4?.data?.ip && setTarget(ipv4.data.ip);
disclosure.onClose();
}}
rightIcon={<RightArrow boxSize="18px" />}
>
{ipv4?.data?.ip ?? noIPv4}
</Button>
)}
{show6 && (
<Button
size="sm"
fontSize="xs"
fontFamily="mono"
colorScheme="secondary"
isDisabled={ipv6.isError}
isLoading={ipv6.isLoading}
justifyContent="space-between"
onClick={() => {
ipv6?.data?.ip && setTarget(ipv6.data.ip);
disclosure.onClose();
}}
rightIcon={<RightArrow boxSize="18px" />}
>
{ipv6?.data?.ip ?? noIPv6}
</Button>
)}
{!hasResult && (
<Text fontSize="sm" textAlign="center" color={errorColor}>
{web.text.ipError}
</Text>
)}
</Stack>
</VStack>
</Prompt>
);
};

View File

@ -16,6 +16,7 @@ export * from './markdown';
export * from './meta';
export * from './output';
export * from './path';
export * from './prompt';
export * from './results';
export * from './select';
export * from './submit';

View File

@ -0,0 +1,27 @@
import {
Popover,
PopoverBody,
PopoverArrow,
PopoverTrigger,
PopoverContent,
PopoverCloseButton,
} from '@chakra-ui/react';
import { useColorValue } from '~/context';
import type { PromptProps } from './types';
export const DesktopPrompt = (props: PromptProps): JSX.Element => {
const { trigger, children, ...disclosure } = props;
const bg = useColorValue('white', 'gray.900');
return (
<Popover closeOnBlur={false} {...disclosure}>
<PopoverTrigger>{trigger}</PopoverTrigger>
<PopoverContent bg={bg}>
<PopoverArrow bg={bg} />
<PopoverCloseButton />
<PopoverBody p={6}>{children}</PopoverBody>
</PopoverContent>
</Popover>
);
};

View File

@ -0,0 +1,11 @@
import { useMobile } from '~/context';
import { DesktopPrompt } from './desktop';
import { MobilePrompt } from './mobile';
import type { PromptProps } from './types';
export const Prompt = (props: PromptProps): JSX.Element => {
const isMobile = useMobile();
return isMobile ? <MobilePrompt {...props} /> : <DesktopPrompt {...props} />;
};

View File

@ -0,0 +1,30 @@
import { Modal, ModalBody, ModalOverlay, ModalContent, ModalCloseButton } from '@chakra-ui/react';
import { useColorValue } from '~/context';
import type { PromptProps } from './types';
export const MobilePrompt = (props: PromptProps): JSX.Element => {
const { children, trigger, ...disclosure } = props;
const bg = useColorValue('white', 'gray.900');
return (
<>
{trigger}
<Modal
size="xs"
isCentered
closeOnEsc={false}
closeOnOverlayClick={false}
motionPreset="slideInBottom"
{...disclosure}
>
<ModalOverlay />
<ModalContent bg={bg}>
<ModalCloseButton />
<ModalBody px={4} py={10}>
{children}
</ModalBody>
</ModalContent>
</Modal>
</>
);
};

View File

@ -0,0 +1,10 @@
import type { UseDisclosureReturn } from '@chakra-ui/react';
type PromptPropsBase = React.PropsWithChildren<
Omit<Partial<UseDisclosureReturn>, 'isOpen' | 'onClose'> &
Pick<UseDisclosureReturn, 'isOpen' | 'onClose'>
>;
export interface PromptProps extends PromptPropsBase {
trigger?: JSX.Element;
}

View File

@ -11,3 +11,4 @@ export * from './useLGQuery';
export * from './useOpposingColor';
export * from './useStrf';
export * from './useTableToString';
export * from './useWtf';

View File

@ -0,0 +1,76 @@
import { useQuery } from 'react-query';
import { fetchWithTimeout } from '~/util';
import type {
QueryFunction,
QueryFunctionContext,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import type { WtfIsMyIP } from '~/types';
const URL_IP4 = 'https://ipv4.json.myip.wtf';
const URL_IP6 = 'https://ipv6.json.myip.wtf';
interface WtfIndividual {
ip: string;
isp: string;
location: string;
country: string;
}
type Wtf = [UseQueryResult<WtfIndividual>, UseQueryResult<WtfIndividual>, () => Promise<void>];
function transform(wtf: WtfIsMyIP): WtfIndividual {
const { YourFuckingIPAddress, YourFuckingISP, YourFuckingLocation, YourFuckingCountryCode } = wtf;
return {
ip: YourFuckingIPAddress,
isp: YourFuckingISP,
location: YourFuckingLocation,
country: YourFuckingCountryCode,
};
}
const query: QueryFunction<WtfIndividual, string> = async (ctx: QueryFunctionContext<string>) => {
const controller = new AbortController();
const [url] = ctx.queryKey;
const res = await fetchWithTimeout(
url,
{
headers: { accept: 'application/json' },
mode: 'cors',
},
5000,
controller,
);
const data = await res.json();
return transform(data);
};
const common: UseQueryOptions<WtfIndividual, unknown, WtfIndividual, string> = {
queryFn: query,
enabled: false,
refetchInterval: false,
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
cacheTime: 120 * 1_000, // 2 minutes
};
export function useWtf(): Wtf {
const ipv4 = useQuery<WtfIndividual, unknown, WtfIndividual, string>({
queryKey: URL_IP4,
...common,
});
const ipv6 = useQuery<WtfIndividual, unknown, WtfIndividual, string>({
queryKey: URL_IP6,
...common,
});
async function refetch(): Promise<void> {
await ipv4.refetch();
await ipv6.refetch();
}
return [ipv4, ipv6, refetch];
}

View File

@ -48,6 +48,10 @@ interface _Text {
rpki_unknown: string;
rpki_unverified: string;
no_communities: string;
ip_error: string;
no_ip: string;
ip_select: string;
ip_button: string;
}
interface _Greeting {

View File

@ -7,3 +7,4 @@ export * from './guards';
export * from './table';
export * from './theme';
export * from './util';
export * from './wtfismyip';

View File

@ -0,0 +1,13 @@
/**
* myip.wtf response.
*
* @see https://github.com/wtfismyip/wtfismyip
* @see https://wtfismyip.com/automation
*/
export interface WtfIsMyIP {
YourFuckingIPAddress: string;
YourFuckingLocation: string;
YourFuckingISP: string;
YourFuckingTorExit: boolean;
YourFuckingCountryCode: string;
}