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:
@ -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):
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -34,3 +34,7 @@ export interface LocationCardProps {
|
||||
onChange(a: 'add' | 'remove', v: SingleOption): void;
|
||||
hasError: boolean;
|
||||
}
|
||||
|
||||
export interface UserIPProps {
|
||||
setTarget(target: string): void;
|
||||
}
|
||||
|
102
hyperglass/ui/components/form/userIP.tsx
Normal file
102
hyperglass/ui/components/form/userIP.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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';
|
||||
|
27
hyperglass/ui/components/prompt/desktop.tsx
Normal file
27
hyperglass/ui/components/prompt/desktop.tsx
Normal 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>
|
||||
);
|
||||
};
|
11
hyperglass/ui/components/prompt/index.tsx
Normal file
11
hyperglass/ui/components/prompt/index.tsx
Normal 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} />;
|
||||
};
|
30
hyperglass/ui/components/prompt/mobile.tsx
Normal file
30
hyperglass/ui/components/prompt/mobile.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
10
hyperglass/ui/components/prompt/types.ts
Normal file
10
hyperglass/ui/components/prompt/types.ts
Normal 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;
|
||||
}
|
@ -11,3 +11,4 @@ export * from './useLGQuery';
|
||||
export * from './useOpposingColor';
|
||||
export * from './useStrf';
|
||||
export * from './useTableToString';
|
||||
export * from './useWtf';
|
||||
|
76
hyperglass/ui/hooks/useWtf.ts
Normal file
76
hyperglass/ui/hooks/useWtf.ts
Normal 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];
|
||||
}
|
@ -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 {
|
||||
|
@ -7,3 +7,4 @@ export * from './guards';
|
||||
export * from './table';
|
||||
export * from './theme';
|
||||
export * from './util';
|
||||
export * from './wtfismyip';
|
||||
|
13
hyperglass/ui/types/wtfismyip.ts
Normal file
13
hyperglass/ui/types/wtfismyip.ts
Normal 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;
|
||||
}
|
Reference in New Issue
Block a user