forked from mirrors/thatmattlove-hyperglass
start: add DNS resolution
This commit is contained in:
parent
bc91fe6f29
commit
03c58ceb76
5 changed files with 258 additions and 28 deletions
|
|
@ -1,8 +1,19 @@
|
|||
import React from "react";
|
||||
import { Flex, FormControl, FormLabel, FormErrorMessage, useColorMode } from "@chakra-ui/core";
|
||||
import HelpModal from "~/components/HelpModal";
|
||||
|
||||
export default ({ label, name, error, hiddenLabels, helpIcon, children, ...props }) => {
|
||||
export default ({
|
||||
label,
|
||||
name,
|
||||
error,
|
||||
hiddenLabels,
|
||||
helpIcon,
|
||||
targetInfo,
|
||||
setTarget,
|
||||
labelAddOn,
|
||||
fieldAddOn,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const { colorMode } = useColorMode();
|
||||
const labelColor = { dark: "whiteAlpha.600", light: "blackAlpha.600" };
|
||||
return (
|
||||
|
|
@ -22,11 +33,20 @@ export default ({ label, name, error, hiddenLabels, helpIcon, children, ...props
|
|||
color={labelColor[colorMode]}
|
||||
pl={1}
|
||||
opacity={hiddenLabels ? 0 : null}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
pr={0}
|
||||
>
|
||||
{label}
|
||||
{helpIcon?.enable && <HelpModal item={helpIcon} name={name} />}
|
||||
{labelAddOn || null}
|
||||
</FormLabel>
|
||||
{children}
|
||||
{fieldAddOn && (
|
||||
<Flex justifyContent="flex-end" pt={3}>
|
||||
{fieldAddOn}
|
||||
</Flex>
|
||||
)}
|
||||
<FormErrorMessage opacity={hiddenLabels ? 0 : null}>
|
||||
{error && error.message}
|
||||
</FormErrorMessage>
|
||||
|
|
|
|||
|
|
@ -5,10 +5,12 @@ import lodash from "lodash";
|
|||
import * as yup from "yup";
|
||||
import format from "string-format";
|
||||
import FormField from "~/components/FormField";
|
||||
import HelpModal from "~/components/HelpModal";
|
||||
import QueryLocation from "~/components/QueryLocation";
|
||||
import QueryType from "~/components/QueryType";
|
||||
import QueryTarget from "~/components/QueryTarget";
|
||||
import QueryVrf from "~/components/QueryVrf";
|
||||
import ResolvedTarget from "~/components/ResolvedTarget";
|
||||
import SubmitButton from "~/components/SubmitButton";
|
||||
import useConfig from "~/components/HyperglassProvider";
|
||||
|
||||
|
|
@ -49,11 +51,16 @@ const HyperglassForm = React.forwardRef(
|
|||
const { handleSubmit, register, setValue, errors } = useForm({
|
||||
validationSchema: formSchema(config)
|
||||
});
|
||||
|
||||
const [queryLocation, setQueryLocation] = useState([]);
|
||||
const [queryType, setQueryType] = useState("");
|
||||
const [queryVrf, setQueryVrf] = useState("");
|
||||
const [queryTarget, setQueryTarget] = useState("");
|
||||
const [availVrfs, setAvailVrfs] = useState([]);
|
||||
const [fqdnTarget, setFqdnTarget] = useState("");
|
||||
const [displayTarget, setDisplayTarget] = useState("");
|
||||
const onSubmit = values => {
|
||||
console.log(values);
|
||||
setFormData(values);
|
||||
setSubmitting(true);
|
||||
};
|
||||
|
|
@ -81,17 +88,28 @@ const HyperglassForm = React.forwardRef(
|
|||
? setQueryType(e.value)
|
||||
: e.field === "query_vrf"
|
||||
? setQueryVrf(e.value)
|
||||
: e.field === "query_target"
|
||||
? setQueryTarget(e.value)
|
||||
: null;
|
||||
};
|
||||
|
||||
const vrfContent = config.content.vrf[queryVrf]?.[queryType];
|
||||
const validFqdnQueryType =
|
||||
["ping", "traceroute", "bgp_route"].includes(queryType) &&
|
||||
fqdnTarget &&
|
||||
queryVrf === "default"
|
||||
? fqdnTarget
|
||||
: null;
|
||||
|
||||
useEffect(() => {
|
||||
register({ name: "query_location" });
|
||||
register({ name: "query_target" });
|
||||
register({ name: "query_type" });
|
||||
register({ name: "query_vrf" });
|
||||
});
|
||||
return (
|
||||
<Box
|
||||
maxW={["100%", "100%", "75%", "50%"]}
|
||||
maxW={["100%", "100%", "75%", "75%"]}
|
||||
w="100%"
|
||||
p={0}
|
||||
mx="auto"
|
||||
|
|
@ -113,7 +131,9 @@ const HyperglassForm = React.forwardRef(
|
|||
label={config.branding.text.query_type}
|
||||
name="query_type"
|
||||
error={errors.query_type}
|
||||
helpIcon={config.content.vrf[queryVrf]?.[queryType] ?? null}
|
||||
labelAddOn={
|
||||
vrfContent && <HelpModal item={vrfContent} name="query_type" />
|
||||
}
|
||||
>
|
||||
<QueryType onChange={handleChange} queryTypes={config.queries} />
|
||||
</FormField>
|
||||
|
|
@ -136,10 +156,28 @@ const HyperglassForm = React.forwardRef(
|
|||
label={config.branding.text.query_target}
|
||||
name="query_target"
|
||||
error={errors.query_target}
|
||||
fieldAddOn={
|
||||
validFqdnQueryType && (
|
||||
<ResolvedTarget
|
||||
formQueryTarget={queryTarget}
|
||||
target={validFqdnQueryType}
|
||||
setTarget={handleChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
<QueryTarget
|
||||
name="query_target"
|
||||
placeholder={config.branding.text.query_target}
|
||||
register={register}
|
||||
resolveTarget={["ping", "traceroute", "bgp_route"].includes(
|
||||
queryType
|
||||
)}
|
||||
value={queryTarget}
|
||||
setFqdn={setFqdnTarget}
|
||||
setValue={handleChange}
|
||||
displayValue={displayTarget}
|
||||
setDisplayValue={setDisplayTarget}
|
||||
/>
|
||||
</FormField>
|
||||
</FormRow>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import styled from "@emotion/styled";
|
||||
import { Input, useColorMode, useTheme } from "@chakra-ui/core";
|
||||
import { Input, useColorMode } from "@chakra-ui/core";
|
||||
|
||||
const StyledInput = styled(Input)`
|
||||
&::placeholder {
|
||||
|
|
@ -8,26 +8,63 @@ const StyledInput = styled(Input)`
|
|||
}
|
||||
`;
|
||||
|
||||
export default ({ placeholder, register }) => {
|
||||
const theme = useTheme();
|
||||
const fqdnPattern = /(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{1,63}(?<!-)\.)+[a-zA-Z]{2,63}$)/gim;
|
||||
|
||||
const bg = { dark: "whiteAlpha.100", light: "white" };
|
||||
const color = { dark: "whiteAlpha.800", light: "gray.400" };
|
||||
const border = { dark: "whiteAlpha.50", light: "gray.100" };
|
||||
const placeholderColor = { dark: "whiteAlpha.400", light: "gray.400" };
|
||||
|
||||
const QueryTarget = ({
|
||||
placeholder,
|
||||
register,
|
||||
setFqdn,
|
||||
name,
|
||||
value,
|
||||
setValue,
|
||||
resolveTarget,
|
||||
displayValue,
|
||||
setDisplayValue
|
||||
}) => {
|
||||
const { colorMode } = useColorMode();
|
||||
const bg = colorMode === "dark" ? theme.colors.whiteAlpha[100] : theme.colors.white;
|
||||
const color = colorMode === "dark" ? theme.colors.whiteAlpha[800] : theme.colors.gray[400];
|
||||
const border = colorMode === "dark" ? theme.colors.whiteAlpha[50] : theme.colors.gray[100];
|
||||
const borderRadius = theme.space[1];
|
||||
const placeholderColor =
|
||||
colorMode === "dark" ? theme.colors.whiteAlpha[400] : theme.colors.gray[400];
|
||||
|
||||
const handleBlur = () => {
|
||||
if (resolveTarget && displayValue && fqdnPattern.test(displayValue)) {
|
||||
setFqdn(displayValue);
|
||||
} else if (resolveTarget && !displayValue) {
|
||||
setFqdn(false);
|
||||
}
|
||||
};
|
||||
const handleChange = e => {
|
||||
setDisplayValue(e.target.value);
|
||||
setValue({ field: name, value: e.target.value });
|
||||
};
|
||||
const handleKeyDown = e => {
|
||||
if ([9, 13].includes(e.keyCode)) {
|
||||
handleBlur();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<StyledInput
|
||||
name="query_target"
|
||||
ref={register}
|
||||
placeholder={placeholder}
|
||||
placeholderColor={placeholderColor}
|
||||
size="lg"
|
||||
bg={bg}
|
||||
color={color}
|
||||
borderColor={border}
|
||||
borderRadius={borderRadius}
|
||||
/>
|
||||
<>
|
||||
<input hidden readOnly name={name} ref={register} value={value} />
|
||||
<StyledInput
|
||||
size="lg"
|
||||
name="query_target_display"
|
||||
bg={bg[colorMode]}
|
||||
onBlur={handleBlur}
|
||||
onFocus={handleBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
value={displayValue}
|
||||
borderRadius="0.25rem"
|
||||
onChange={handleChange}
|
||||
color={color[colorMode]}
|
||||
placeholder={placeholder}
|
||||
borderColor={border[colorMode]}
|
||||
placeholderColor={placeholderColor[colorMode]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
QueryTarget.displayName = "QueryTarget";
|
||||
export default QueryTarget;
|
||||
|
|
|
|||
132
ui/components/ResolvedTarget.js
Normal file
132
ui/components/ResolvedTarget.js
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { Button, Icon, Spinner, Stack, Tag, Text, Tooltip, useColorMode } from "@chakra-ui/core";
|
||||
import useAxios from "axios-hooks";
|
||||
import format from "string-format";
|
||||
import useConfig from "~/components/HyperglassProvider";
|
||||
|
||||
format.extend(String.prototype, {});
|
||||
|
||||
const labelBg = { dark: "secondary", light: "secondary" };
|
||||
const labelBgSuccess = { dark: "success", light: "success" };
|
||||
|
||||
const ResolvedTarget = React.forwardRef(({ target, setTarget, formQueryTarget }, ref) => {
|
||||
const { colorMode } = useColorMode();
|
||||
const config = useConfig();
|
||||
const labelBgStatus = { true: labelBgSuccess[colorMode], false: labelBg[colorMode] };
|
||||
const params4 = {
|
||||
url: "https://cloudflare-dns.com/dns-query",
|
||||
params: { name: target, type: "A" },
|
||||
headers: { accept: "application/dns-json" },
|
||||
timeout: 1000
|
||||
};
|
||||
const params6 = {
|
||||
url: "https://cloudflare-dns.com/dns-query",
|
||||
params: { name: target, type: "AAAA" },
|
||||
headers: { accept: "application/dns-json" },
|
||||
timeout: 1000
|
||||
};
|
||||
|
||||
const [{ data: data4, loading: loading4, error: error4 }] = useAxios(params4);
|
||||
const [{ data: data6, loading: loading6, error: error6 }] = useAxios(params6);
|
||||
|
||||
const [data, setData] = useState("");
|
||||
|
||||
data && setTarget({ field: "query_target", value: data });
|
||||
|
||||
const handleOverride = overridden => {
|
||||
setData(overridden);
|
||||
setTarget({ field: "query_target", value: overridden });
|
||||
};
|
||||
|
||||
const isSelected = value => {
|
||||
console.log("value: ", value, "formQuerytarget: ", formQueryTarget, "target: ", target);
|
||||
return labelBgStatus[value === formQueryTarget];
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (data6 && data6.Answer && data6.Answer[0].type === 28 && data === "") {
|
||||
handleOverride(data6.Answer[0].data);
|
||||
}
|
||||
}, [data6, data]);
|
||||
useEffect(() => {
|
||||
if (data4 && data4.Answer && data4.Answer[0].type === 28 && data === "") {
|
||||
handleOverride(data4.Answer[0].data);
|
||||
}
|
||||
}, [data4, data]);
|
||||
return (
|
||||
<Stack
|
||||
ref={ref}
|
||||
isInline
|
||||
w="100%"
|
||||
justifyContent={data4?.Answer && data6?.Answer ? "space-between" : "flex-end"}
|
||||
>
|
||||
{loading4 ||
|
||||
error4 ||
|
||||
(data4?.Answer?.[0] && (
|
||||
<Tag>
|
||||
<Tooltip
|
||||
hasArrow
|
||||
label={config.branding.text.fqdn_tooltip.format({ protocol: "IPv4" })}
|
||||
placement="bottom"
|
||||
>
|
||||
<Button
|
||||
height="unset"
|
||||
minW="unset"
|
||||
fontSize="xs"
|
||||
py="0.1rem"
|
||||
px={2}
|
||||
mr={2}
|
||||
variantColor={isSelected(data4.Answer[0].data)}
|
||||
borderRadius="md"
|
||||
onClick={() => handleOverride(data4.Answer[0].data)}
|
||||
>
|
||||
IPv4
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{loading4 && <Spinner />}
|
||||
{error4 && <Icon name="warning" />}
|
||||
{data4?.Answer?.[0] && (
|
||||
<Text fontSize="xs" fontFamily="mono" as="span" fontWeight={400}>
|
||||
{data4.Answer[0].data}
|
||||
</Text>
|
||||
)}
|
||||
</Tag>
|
||||
))}
|
||||
{loading6 ||
|
||||
error6 ||
|
||||
(data6?.Answer?.[0] && (
|
||||
<Tag>
|
||||
<Tooltip
|
||||
hasArrow
|
||||
label={config.branding.text.fqdn_tooltip.format({ protocol: "IPv6" })}
|
||||
placement="bottom"
|
||||
>
|
||||
<Button
|
||||
height="unset"
|
||||
minW="unset"
|
||||
fontSize="xs"
|
||||
py="0.1rem"
|
||||
px={2}
|
||||
mr={2}
|
||||
variantColor={isSelected(data6.Answer[0].data)}
|
||||
borderRadius="md"
|
||||
onClick={() => handleOverride(data6.Answer[0].data)}
|
||||
>
|
||||
IPv6
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{loading6 && <Spinner />}
|
||||
{error6 && <Icon name="warning" />}
|
||||
{data6?.Answer?.[0] && (
|
||||
<Text fontSize="xs" fontFamily="mono" as="span" fontWeight={400}>
|
||||
{data6.Answer[0].data}
|
||||
</Text>
|
||||
)}
|
||||
</Tag>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
});
|
||||
|
||||
ResolvedTarget.displayName = "ResolvedTarget";
|
||||
export default ResolvedTarget;
|
||||
|
|
@ -4,7 +4,6 @@ import {
|
|||
AccordionHeader,
|
||||
AccordionPanel,
|
||||
Alert,
|
||||
AlertDescription,
|
||||
Box,
|
||||
ButtonGroup,
|
||||
css,
|
||||
|
|
@ -30,7 +29,7 @@ const FormattedError = ({ keywords, message }) => {
|
|||
{match}
|
||||
</Text>
|
||||
));
|
||||
return <Text>{errorFmt}</Text>;
|
||||
return <Text>{keywords.length !== 0 ? errorFmt : message}</Text>;
|
||||
};
|
||||
|
||||
const AccordionHeaderWrapper = styled(Flex)`
|
||||
|
|
@ -79,12 +78,16 @@ const Result = React.forwardRef(
|
|||
errorMsg = error.response.data.output;
|
||||
} else if (error && error.message.startsWith("timeout")) {
|
||||
errorMsg = config.messages.request_timeout;
|
||||
} else if (error?.response?.statusText) {
|
||||
errorMsg = startCase(error.response.statusText);
|
||||
} else if (error && error.message) {
|
||||
errorMsg = startCase(error.message);
|
||||
} else {
|
||||
errorMsg = config.messages.general;
|
||||
}
|
||||
|
||||
error && console.dir(error);
|
||||
|
||||
const errorLevel =
|
||||
(error?.response?.data?.level && statusMap[error.response?.data?.level]) ?? "error";
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue