Skip to content

Commit

Permalink
Closes #155: Implement User IP Button
Browse files Browse the repository at this point in the history
  • Loading branch information
thatmattlove committed Oct 19, 2021
1 parent d4db98d commit f6d3dfe
Show file tree
Hide file tree
Showing 14 changed files with 309 additions and 14 deletions.
4 changes: 4 additions & 0 deletions hyperglass/models/config/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
39 changes: 25 additions & 14 deletions hyperglass/ui/components/form/queryTarget.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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>
</>
);
Expand Down
4 changes: 4 additions & 0 deletions hyperglass/ui/components/form/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,7 @@ export interface LocationCardProps {
onChange(a: 'add' | 'remove', v: SingleOption): void;
hasError: boolean;
}

export interface UserIPProps {
setTarget(target: string): void;
}
102 changes: 102 additions & 0 deletions hyperglass/ui/components/form/userIP.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
};
1 change: 1 addition & 0 deletions hyperglass/ui/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
27 changes: 27 additions & 0 deletions hyperglass/ui/components/prompt/desktop.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
};
11 changes: 11 additions & 0 deletions hyperglass/ui/components/prompt/index.tsx
Original file line number Diff line number Diff line change
@@ -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} />;
};
30 changes: 30 additions & 0 deletions hyperglass/ui/components/prompt/mobile.tsx
Original file line number Diff line number Diff line change
@@ -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>
</>
);
};
10 changes: 10 additions & 0 deletions hyperglass/ui/components/prompt/types.ts
Original file line number Diff line number Diff line change
@@ -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;
}
1 change: 1 addition & 0 deletions hyperglass/ui/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export * from './useLGQuery';
export * from './useOpposingColor';
export * from './useStrf';
export * from './useTableToString';
export * from './useWtf';
76 changes: 76 additions & 0 deletions hyperglass/ui/hooks/useWtf.ts
Original file line number Diff line number Diff line change
@@ -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];
}
4 changes: 4 additions & 0 deletions hyperglass/ui/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions hyperglass/ui/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from './guards';
export * from './table';
export * from './theme';
export * from './util';
export * from './wtfismyip';
13 changes: 13 additions & 0 deletions hyperglass/ui/types/wtfismyip.ts
Original file line number Diff line number Diff line change
@@ -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;
}

0 comments on commit f6d3dfe

Please sign in to comment.