mirror of
https://github.com/Xevion/rdap.git
synced 2025-12-06 01:16:00 -06:00
feat: enhance RDAP component rendering with comprehensive data display
Major improvements to RDAP card components: - Add tooltips to action buttons in AbstractCard for better UX - Implement dedicated section components (EntitiesSection, LinksSection, NameserversSection, RemarksSection, SecureDNSSection, VCardDisplay) - Add conditional rendering for optional fields across all card types - Enhance Entity and Nameserver cards with full data display - Add WHOIS server (port43) display to relevant cards - Improve visual hierarchy with nested entity displays - Fix autodetection to only run when in autodetect mode - Add proper null/undefined checks throughout components
This commit is contained in:
@@ -2,7 +2,7 @@ import type { FunctionComponent, ReactNode } from "react";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useBoolean } from "usehooks-ts";
|
import { useBoolean } from "usehooks-ts";
|
||||||
import { Link2Icon, CodeIcon, DownloadIcon, ClipboardIcon } from "@radix-ui/react-icons";
|
import { Link2Icon, CodeIcon, DownloadIcon, ClipboardIcon } from "@radix-ui/react-icons";
|
||||||
import { Card, Flex, Box, IconButton, Code, ScrollArea } from "@radix-ui/themes";
|
import { Card, Flex, Box, IconButton, Code, ScrollArea, Tooltip } from "@radix-ui/themes";
|
||||||
|
|
||||||
type AbstractCardProps = {
|
type AbstractCardProps = {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
@@ -38,72 +38,85 @@ const AbstractCard: FunctionComponent<AbstractCardProps> = ({
|
|||||||
</Flex>
|
</Flex>
|
||||||
<Flex gap="2" align="center">
|
<Flex gap="2" align="center">
|
||||||
{url != undefined && (
|
{url != undefined && (
|
||||||
<IconButton variant="ghost" size="2" asChild>
|
<Tooltip content="Open in new tab">
|
||||||
<a
|
<IconButton variant="ghost" size="2" asChild>
|
||||||
href={url}
|
<a
|
||||||
target="_blank"
|
href={url}
|
||||||
rel="noreferrer"
|
target="_blank"
|
||||||
aria-label="Open RDAP URL"
|
rel="noreferrer"
|
||||||
>
|
aria-label="Open RDAP URL"
|
||||||
<Link2Icon width="18" height="18" />
|
>
|
||||||
</a>
|
<Link2Icon width="18" height="18" />
|
||||||
</IconButton>
|
</a>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{data != undefined && (
|
{data != undefined && (
|
||||||
<>
|
<>
|
||||||
<IconButton
|
<Tooltip content="Copy JSON to clipboard">
|
||||||
variant="ghost"
|
<IconButton
|
||||||
size="2"
|
variant="ghost"
|
||||||
onClick={() => {
|
size="2"
|
||||||
navigator.clipboard
|
onClick={() => {
|
||||||
.writeText(JSON.stringify(data, null, 4))
|
navigator.clipboard
|
||||||
.then(
|
.writeText(JSON.stringify(data, null, 4))
|
||||||
() => {
|
.then(
|
||||||
// Successfully copied to clipboard
|
() => {
|
||||||
},
|
// Successfully copied to clipboard
|
||||||
(err) => {
|
},
|
||||||
if (err instanceof Error)
|
(err) => {
|
||||||
console.error(
|
if (err instanceof Error)
|
||||||
`Failed to copy to clipboard (${err.toString()}).`
|
console.error(
|
||||||
);
|
`Failed to copy to clipboard (${err.toString()}).`
|
||||||
else
|
);
|
||||||
console.error(
|
else
|
||||||
"Failed to copy to clipboard."
|
console.error(
|
||||||
);
|
"Failed to copy to clipboard."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
aria-label="Copy JSON to clipboard"
|
||||||
|
>
|
||||||
|
<ClipboardIcon width="18" height="18" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content="Download JSON">
|
||||||
|
<IconButton
|
||||||
|
variant="ghost"
|
||||||
|
size="2"
|
||||||
|
onClick={() => {
|
||||||
|
const file = new Blob(
|
||||||
|
[JSON.stringify(data, null, 4)],
|
||||||
|
{
|
||||||
|
type: "application/json",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}}
|
|
||||||
aria-label="Copy JSON to clipboard"
|
|
||||||
>
|
|
||||||
<ClipboardIcon width="18" height="18" />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
variant="ghost"
|
|
||||||
size="2"
|
|
||||||
onClick={() => {
|
|
||||||
const file = new Blob([JSON.stringify(data, null, 4)], {
|
|
||||||
type: "application/json",
|
|
||||||
});
|
|
||||||
|
|
||||||
const anchor = document.createElement("a");
|
const anchor = document.createElement("a");
|
||||||
anchor.href = URL.createObjectURL(file);
|
anchor.href = URL.createObjectURL(file);
|
||||||
anchor.download = "response.json";
|
anchor.download = "response.json";
|
||||||
anchor.click();
|
anchor.click();
|
||||||
}}
|
}}
|
||||||
aria-label="Download JSON"
|
aria-label="Download JSON"
|
||||||
|
>
|
||||||
|
<DownloadIcon width="18" height="18" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip
|
||||||
|
content={showRaw ? "Show formatted view" : "Show raw JSON"}
|
||||||
>
|
>
|
||||||
<DownloadIcon width="18" height="18" />
|
<IconButton
|
||||||
</IconButton>
|
variant="ghost"
|
||||||
<IconButton
|
size="2"
|
||||||
variant="ghost"
|
onClick={toggleRaw}
|
||||||
size="2"
|
aria-label={
|
||||||
onClick={toggleRaw}
|
showRaw ? "Show formatted view" : "Show raw JSON"
|
||||||
aria-label={
|
}
|
||||||
showRaw ? "Show formatted view" : "Show raw JSON"
|
>
|
||||||
}
|
<CodeIcon width="18" height="18" />
|
||||||
>
|
</IconButton>
|
||||||
<CodeIcon width="18" height="18" />
|
</Tooltip>
|
||||||
</IconButton>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -70,11 +70,14 @@ const Index: NextPage = () => {
|
|||||||
setTarget(target);
|
setTarget(target);
|
||||||
setTargetType(targetType);
|
setTargetType(targetType);
|
||||||
|
|
||||||
const detectResult = await getType(target);
|
// Only run autodetection when in autodetect mode (targetType is null)
|
||||||
if (detectResult.isOk) {
|
if (targetType === null) {
|
||||||
setDetectedType(Maybe.just(detectResult.value));
|
const detectResult = await getType(target);
|
||||||
} else {
|
if (detectResult.isOk) {
|
||||||
setDetectedType(Maybe.nothing());
|
setDetectedType(Maybe.just(detectResult.value));
|
||||||
|
} else {
|
||||||
|
setDetectedType(Maybe.nothing());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onSubmit={async function (props) {
|
onSubmit={async function (props) {
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import Property from "@/components/Property";
|
|||||||
import CopyButton from "@/components/CopyButton";
|
import CopyButton from "@/components/CopyButton";
|
||||||
import StatusBadge from "@/components/StatusBadge";
|
import StatusBadge from "@/components/StatusBadge";
|
||||||
import AbstractCard from "@/components/AbstractCard";
|
import AbstractCard from "@/components/AbstractCard";
|
||||||
|
import EntitiesSection from "@/rdap/components/EntitiesSection";
|
||||||
|
import LinksSection from "@/rdap/components/LinksSection";
|
||||||
|
import RemarksSection from "@/rdap/components/RemarksSection";
|
||||||
import { Flex, Text, DataList, Badge, Code } from "@radix-ui/themes";
|
import { Flex, Text, DataList, Badge, Code } from "@radix-ui/themes";
|
||||||
|
|
||||||
export type AutnumCardProps = {
|
export type AutnumCardProps = {
|
||||||
@@ -50,21 +53,51 @@ const AutnumCard: FunctionComponent<AutnumCardProps> = ({ data, url }: AutnumCar
|
|||||||
</Flex>
|
</Flex>
|
||||||
</DataList.Value>
|
</DataList.Value>
|
||||||
</DataList.Item>
|
</DataList.Item>
|
||||||
<Property title="Type">{data.type}</Property>
|
{data.type && <Property title="Type">{data.type}</Property>}
|
||||||
<Property title="Country">{data.country.toUpperCase()}</Property>
|
{data.country && <Property title="Country">{data.country.toUpperCase()}</Property>}
|
||||||
<Property title="Events">
|
{data.status && data.status.length > 0 && (
|
||||||
<Events key={0} data={data.events} />
|
<DataList.Item>
|
||||||
</Property>
|
<DataList.Label>Status</DataList.Label>
|
||||||
<DataList.Item align="center">
|
<DataList.Value>
|
||||||
<DataList.Label>Status</DataList.Label>
|
<Flex gap="2" wrap="wrap">
|
||||||
<DataList.Value>
|
{data.status.map((status, index) => (
|
||||||
<Flex gap="2" wrap="wrap">
|
<StatusBadge key={index} status={status} />
|
||||||
{data.status.map((status, index) => (
|
))}
|
||||||
<StatusBadge key={index} status={status} />
|
</Flex>
|
||||||
))}
|
</DataList.Value>
|
||||||
</Flex>
|
</DataList.Item>
|
||||||
</DataList.Value>
|
)}
|
||||||
</DataList.Item>
|
{data.port43 && (
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label>WHOIS Server</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
<Flex align="center" gap="2">
|
||||||
|
<Code variant="ghost">{data.port43}</Code>
|
||||||
|
<CopyButton value={data.port43} />
|
||||||
|
</Flex>
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
)}
|
||||||
|
{data.entities && data.entities.length > 0 && (
|
||||||
|
<Property title="Entities">
|
||||||
|
<EntitiesSection entities={data.entities} />
|
||||||
|
</Property>
|
||||||
|
)}
|
||||||
|
{data.events && data.events.length > 0 && (
|
||||||
|
<Property title="Events">
|
||||||
|
<Events data={data.events} />
|
||||||
|
</Property>
|
||||||
|
)}
|
||||||
|
{data.links && data.links.length > 0 && (
|
||||||
|
<Property title="Links">
|
||||||
|
<LinksSection links={data.links} />
|
||||||
|
</Property>
|
||||||
|
)}
|
||||||
|
{data.remarks && data.remarks.length > 0 && (
|
||||||
|
<Property title="Remarks">
|
||||||
|
<RemarksSection remarks={data.remarks} />
|
||||||
|
</Property>
|
||||||
|
)}
|
||||||
</DataList.Root>
|
</DataList.Root>
|
||||||
</AbstractCard>
|
</AbstractCard>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ import Property from "@/components/Property";
|
|||||||
import CopyButton from "@/components/CopyButton";
|
import CopyButton from "@/components/CopyButton";
|
||||||
import StatusBadge from "@/components/StatusBadge";
|
import StatusBadge from "@/components/StatusBadge";
|
||||||
import AbstractCard from "@/components/AbstractCard";
|
import AbstractCard from "@/components/AbstractCard";
|
||||||
|
import NameserversSection from "@/rdap/components/NameserversSection";
|
||||||
|
import SecureDNSSection from "@/rdap/components/SecureDNSSection";
|
||||||
|
import EntitiesSection from "@/rdap/components/EntitiesSection";
|
||||||
|
import LinksSection from "@/rdap/components/LinksSection";
|
||||||
|
import RemarksSection from "@/rdap/components/RemarksSection";
|
||||||
import { Flex, Text, DataList, Badge, Code } from "@radix-ui/themes";
|
import { Flex, Text, DataList, Badge, Code } from "@radix-ui/themes";
|
||||||
|
|
||||||
export type DomainProps = {
|
export type DomainProps = {
|
||||||
@@ -48,28 +53,75 @@ const DomainCard: FunctionComponent<DomainProps> = ({ data, url }: DomainProps)
|
|||||||
</Flex>
|
</Flex>
|
||||||
</DataList.Value>
|
</DataList.Value>
|
||||||
</DataList.Item>
|
</DataList.Item>
|
||||||
<DataList.Item>
|
{data.handle != undefined ? (
|
||||||
<DataList.Label>Handle</DataList.Label>
|
<DataList.Item>
|
||||||
<DataList.Value>
|
<DataList.Label>Handle</DataList.Label>
|
||||||
<Flex align="center" gap="2">
|
<DataList.Value>
|
||||||
<Code variant="ghost">{data.handle}</Code>
|
<Flex align="center" gap="2">
|
||||||
<CopyButton value={data.handle} />
|
<Code variant="ghost">{data.handle}</Code>
|
||||||
</Flex>
|
<CopyButton value={data.handle} />
|
||||||
</DataList.Value>
|
</Flex>
|
||||||
</DataList.Item>
|
</DataList.Value>
|
||||||
<Property title="Events">
|
</DataList.Item>
|
||||||
<Events key={0} data={data.events} />
|
) : null}
|
||||||
</Property>
|
{data.status && data.status.length > 0 && (
|
||||||
<DataList.Item align="center">
|
<DataList.Item>
|
||||||
<DataList.Label>Status</DataList.Label>
|
<DataList.Label>Status</DataList.Label>
|
||||||
<DataList.Value>
|
<DataList.Value>
|
||||||
<Flex gap="2" wrap="wrap">
|
<Flex gap="2" wrap="wrap">
|
||||||
{data.status.map((statusKey, index) => (
|
{data.status.map((statusKey, index) => (
|
||||||
<StatusBadge key={index} status={statusKey} />
|
<StatusBadge key={index} status={statusKey} />
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
</DataList.Value>
|
</DataList.Value>
|
||||||
</DataList.Item>
|
</DataList.Item>
|
||||||
|
)}
|
||||||
|
{data.port43 && (
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label>WHOIS Server</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
<Flex align="center" gap="2">
|
||||||
|
<Code variant="ghost">{data.port43}</Code>
|
||||||
|
<CopyButton value={data.port43} />
|
||||||
|
</Flex>
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
)}
|
||||||
|
{data.nameservers && data.nameservers.length > 0 && (
|
||||||
|
<Property title="Nameservers">
|
||||||
|
<NameserversSection nameservers={data.nameservers} />
|
||||||
|
</Property>
|
||||||
|
)}
|
||||||
|
{data.secureDNS && (
|
||||||
|
<Property title="DNSSEC">
|
||||||
|
<SecureDNSSection secureDNS={data.secureDNS} />
|
||||||
|
</Property>
|
||||||
|
)}
|
||||||
|
{data.entities && data.entities.length > 0 && (
|
||||||
|
<Property title="Entities">
|
||||||
|
<EntitiesSection entities={data.entities} />
|
||||||
|
</Property>
|
||||||
|
)}
|
||||||
|
{data.events && data.events.length > 0 && (
|
||||||
|
<Property title="Events">
|
||||||
|
<Events data={data.events} />
|
||||||
|
</Property>
|
||||||
|
)}
|
||||||
|
{data.links && data.links.length > 0 && (
|
||||||
|
<Property title="Links">
|
||||||
|
<LinksSection links={data.links} />
|
||||||
|
</Property>
|
||||||
|
)}
|
||||||
|
{data.notices && data.notices.length > 0 && (
|
||||||
|
<Property title="Notices">
|
||||||
|
<RemarksSection remarks={data.notices} />
|
||||||
|
</Property>
|
||||||
|
)}
|
||||||
|
{data.remarks && data.remarks.length > 0 && (
|
||||||
|
<Property title="Remarks">
|
||||||
|
<RemarksSection remarks={data.remarks} />
|
||||||
|
</Property>
|
||||||
|
)}
|
||||||
</DataList.Root>
|
</DataList.Root>
|
||||||
</AbstractCard>
|
</AbstractCard>
|
||||||
);
|
);
|
||||||
|
|||||||
147
src/rdap/components/EntitiesSection.tsx
Normal file
147
src/rdap/components/EntitiesSection.tsx
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import type { FunctionComponent } from "react";
|
||||||
|
import React from "react";
|
||||||
|
import type { Entity, RdapStatusType } from "@/rdap/schemas";
|
||||||
|
import { Box, Flex, Badge, Text, Code, DataList, Table } from "@radix-ui/themes";
|
||||||
|
import VCardDisplay from "@/rdap/components/VCardDisplay";
|
||||||
|
import CopyButton from "@/components/CopyButton";
|
||||||
|
import StatusBadge from "@/components/StatusBadge";
|
||||||
|
|
||||||
|
export type EntitiesSectionProps = {
|
||||||
|
entities: Entity[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const EntitiesSection: FunctionComponent<EntitiesSectionProps> = ({ entities }) => {
|
||||||
|
if (!entities || entities.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction="column" gap="3">
|
||||||
|
{entities.map((entity, index) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={index}
|
||||||
|
p="3"
|
||||||
|
style={{
|
||||||
|
border: "1px solid var(--gray-a5)",
|
||||||
|
borderRadius: "var(--radius-3)",
|
||||||
|
backgroundColor: "var(--gray-a2)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex direction="column" gap="3">
|
||||||
|
<DataList.Root
|
||||||
|
orientation={{ initial: "vertical", sm: "horizontal" }}
|
||||||
|
size="2"
|
||||||
|
>
|
||||||
|
{entity.handle && (
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label>Handle</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
<Flex align="center" gap="2">
|
||||||
|
<Code variant="ghost">{entity.handle}</Code>
|
||||||
|
<CopyButton value={entity.handle} />
|
||||||
|
</Flex>
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
)}
|
||||||
|
{entity.roles && entity.roles.length > 0 && (
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label>Roles</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
<Flex gap="2" wrap="wrap">
|
||||||
|
{entity.roles.map(
|
||||||
|
(role: string, roleIndex: number) => (
|
||||||
|
<Badge
|
||||||
|
key={roleIndex}
|
||||||
|
variant="soft"
|
||||||
|
size="1"
|
||||||
|
>
|
||||||
|
{role}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
)}
|
||||||
|
{entity.status && entity.status.length > 0 && (
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label>Status</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
<Flex gap="2" wrap="wrap">
|
||||||
|
{entity.status.map(
|
||||||
|
(
|
||||||
|
status: RdapStatusType,
|
||||||
|
statusIndex: number
|
||||||
|
) => (
|
||||||
|
<StatusBadge
|
||||||
|
key={statusIndex}
|
||||||
|
status={status}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
)}
|
||||||
|
</DataList.Root>
|
||||||
|
|
||||||
|
{entity.vcardArray && (
|
||||||
|
<Flex direction="column" gap="2">
|
||||||
|
<VCardDisplay vcardArray={entity.vcardArray} />
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{entity.publicIds && entity.publicIds.length > 0 && (
|
||||||
|
<Table.Root size="1" variant="surface">
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.ColumnHeaderCell>
|
||||||
|
Public ID Type
|
||||||
|
</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>
|
||||||
|
Identifier
|
||||||
|
</Table.ColumnHeaderCell>
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body>
|
||||||
|
{entity.publicIds.map(
|
||||||
|
(
|
||||||
|
publicId: { type: string; identifier: string },
|
||||||
|
publicIdIndex: number
|
||||||
|
) => (
|
||||||
|
<Table.Row key={publicIdIndex}>
|
||||||
|
<Table.Cell>{publicId.type}</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Flex align="center" gap="2">
|
||||||
|
<Code variant="ghost">
|
||||||
|
{publicId.identifier}
|
||||||
|
</Code>
|
||||||
|
<CopyButton
|
||||||
|
value={publicId.identifier}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{entity.port43 && (
|
||||||
|
<Flex align="center" gap="2">
|
||||||
|
<Text size="2" weight="medium">
|
||||||
|
WHOIS Server:
|
||||||
|
</Text>
|
||||||
|
<Code variant="ghost">{entity.port43}</Code>
|
||||||
|
<CopyButton value={entity.port43} />
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EntitiesSection;
|
||||||
@@ -1,8 +1,15 @@
|
|||||||
import type { FunctionComponent } from "react";
|
import type { FunctionComponent } from "react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import type { Entity } from "@/rdap/schemas";
|
import type { Entity, RdapStatusType } from "@/rdap/schemas";
|
||||||
import CopyButton from "@/components/CopyButton";
|
import CopyButton from "@/components/CopyButton";
|
||||||
|
import StatusBadge from "@/components/StatusBadge";
|
||||||
import AbstractCard from "@/components/AbstractCard";
|
import AbstractCard from "@/components/AbstractCard";
|
||||||
|
import Property from "@/components/Property";
|
||||||
|
import VCardDisplay from "@/rdap/components/VCardDisplay";
|
||||||
|
import Events from "@/rdap/components/Events";
|
||||||
|
import LinksSection from "@/rdap/components/LinksSection";
|
||||||
|
import RemarksSection from "@/rdap/components/RemarksSection";
|
||||||
|
import EntitiesSection from "@/rdap/components/EntitiesSection";
|
||||||
import { Flex, DataList, Badge, Text, Code } from "@radix-ui/themes";
|
import { Flex, DataList, Badge, Text, Code } from "@radix-ui/themes";
|
||||||
|
|
||||||
export type EntityCardProps = {
|
export type EntityCardProps = {
|
||||||
@@ -17,7 +24,9 @@ const EntityCard: FunctionComponent<EntityCardProps> = ({ data, url }: EntityCar
|
|||||||
url={url}
|
url={url}
|
||||||
header={
|
header={
|
||||||
<Flex gap="2" align="center" wrap="wrap">
|
<Flex gap="2" align="center" wrap="wrap">
|
||||||
<Text size="5">{data.handle || data.roles.join(", ")}</Text>
|
<Text size="5">
|
||||||
|
{data.handle || (data.roles && data.roles.join(", ")) || "Entity"}
|
||||||
|
</Text>
|
||||||
<Badge color="gray">ENTITY</Badge>
|
<Badge color="gray">ENTITY</Badge>
|
||||||
</Flex>
|
</Flex>
|
||||||
}
|
}
|
||||||
@@ -34,35 +43,90 @@ const EntityCard: FunctionComponent<EntityCardProps> = ({ data, url }: EntityCar
|
|||||||
</DataList.Value>
|
</DataList.Value>
|
||||||
</DataList.Item>
|
</DataList.Item>
|
||||||
)}
|
)}
|
||||||
<DataList.Item align="center">
|
{data.roles && data.roles.length > 0 && (
|
||||||
<DataList.Label>Roles</DataList.Label>
|
<DataList.Item>
|
||||||
<DataList.Value>
|
<DataList.Label>Roles</DataList.Label>
|
||||||
<Flex gap="2" wrap="wrap">
|
|
||||||
{data.roles.map((role, index) => (
|
|
||||||
<Badge key={index} color="gray" variant="soft" radius="full">
|
|
||||||
{role}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</Flex>
|
|
||||||
</DataList.Value>
|
|
||||||
</DataList.Item>
|
|
||||||
{data.publicIds && data.publicIds.length > 0 && (
|
|
||||||
<DataList.Item align="center">
|
|
||||||
<DataList.Label>Public IDs</DataList.Label>
|
|
||||||
<DataList.Value>
|
<DataList.Value>
|
||||||
<Flex direction="column" gap="2">
|
<Flex gap="2" wrap="wrap">
|
||||||
{data.publicIds.map((publicId, index) => (
|
{data.roles.map((role: string, index: number) => (
|
||||||
<Flex key={index} align="center" gap="2">
|
<Badge key={index} color="gray" variant="soft" radius="full">
|
||||||
<Code variant="ghost">
|
{role}
|
||||||
{publicId.identifier} ({publicId.type})
|
</Badge>
|
||||||
</Code>
|
|
||||||
<CopyButton value={publicId.identifier} />
|
|
||||||
</Flex>
|
|
||||||
))}
|
))}
|
||||||
</Flex>
|
</Flex>
|
||||||
</DataList.Value>
|
</DataList.Value>
|
||||||
</DataList.Item>
|
</DataList.Item>
|
||||||
)}
|
)}
|
||||||
|
{data.status && data.status.length > 0 && (
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label>Status</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
<Flex gap="2" wrap="wrap">
|
||||||
|
{data.status.map((status: RdapStatusType, index: number) => (
|
||||||
|
<StatusBadge key={index} status={status} />
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
)}
|
||||||
|
{data.publicIds && data.publicIds.length > 0 && (
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label>Public IDs</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
<Flex direction="column" gap="2">
|
||||||
|
{data.publicIds.map(
|
||||||
|
(
|
||||||
|
publicId: { type: string; identifier: string },
|
||||||
|
index: number
|
||||||
|
) => (
|
||||||
|
<Flex key={index} align="center" gap="2">
|
||||||
|
<Code variant="ghost">
|
||||||
|
{publicId.identifier} ({publicId.type})
|
||||||
|
</Code>
|
||||||
|
<CopyButton value={publicId.identifier} />
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
)}
|
||||||
|
{data.port43 && (
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label>WHOIS Server</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
<Flex align="center" gap="2">
|
||||||
|
<Code variant="ghost">{data.port43}</Code>
|
||||||
|
<CopyButton value={data.port43} />
|
||||||
|
</Flex>
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
)}
|
||||||
|
{data.vcardArray && (
|
||||||
|
<Property title="Contact Information">
|
||||||
|
<VCardDisplay vcardArray={data.vcardArray} />
|
||||||
|
</Property>
|
||||||
|
)}
|
||||||
|
{data.entities && data.entities.length > 0 && (
|
||||||
|
<Property title="Associated Entities">
|
||||||
|
<EntitiesSection entities={data.entities} />
|
||||||
|
</Property>
|
||||||
|
)}
|
||||||
|
{data.events && data.events.length > 0 && (
|
||||||
|
<Property title="Events">
|
||||||
|
<Events data={data.events} />
|
||||||
|
</Property>
|
||||||
|
)}
|
||||||
|
{data.links && data.links.length > 0 && (
|
||||||
|
<Property title="Links">
|
||||||
|
<LinksSection links={data.links} />
|
||||||
|
</Property>
|
||||||
|
)}
|
||||||
|
{data.remarks && data.remarks.length > 0 && (
|
||||||
|
<Property title="Remarks">
|
||||||
|
<RemarksSection remarks={data.remarks} />
|
||||||
|
</Property>
|
||||||
|
)}
|
||||||
</DataList.Root>
|
</DataList.Root>
|
||||||
</AbstractCard>
|
</AbstractCard>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const Events: FunctionComponent<EventsProps> = ({ data }) => {
|
|||||||
{eventActor}
|
{eventActor}
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<Text size="2" style={{ color: "var(--gray-a6)" }}>
|
<Text size="2" style={{ color: "var(--gray-a4)" }}>
|
||||||
—
|
—
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import Property from "@/components/Property";
|
|||||||
import CopyButton from "@/components/CopyButton";
|
import CopyButton from "@/components/CopyButton";
|
||||||
import StatusBadge from "@/components/StatusBadge";
|
import StatusBadge from "@/components/StatusBadge";
|
||||||
import AbstractCard from "@/components/AbstractCard";
|
import AbstractCard from "@/components/AbstractCard";
|
||||||
|
import EntitiesSection from "@/rdap/components/EntitiesSection";
|
||||||
|
import LinksSection from "@/rdap/components/LinksSection";
|
||||||
|
import RemarksSection from "@/rdap/components/RemarksSection";
|
||||||
import { Flex, Text, DataList, Badge, Code } from "@radix-ui/themes";
|
import { Flex, Text, DataList, Badge, Code } from "@radix-ui/themes";
|
||||||
|
|
||||||
export type IPCardProps = {
|
export type IPCardProps = {
|
||||||
@@ -57,7 +60,7 @@ const IPCard: FunctionComponent<IPCardProps> = ({ data, url }: IPCardProps) => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
</DataList.Value>
|
</DataList.Value>
|
||||||
</DataList.Item>
|
</DataList.Item>
|
||||||
<Property title="Type">{data.type}</Property>
|
{data.type && <Property title="Type">{data.type}</Property>}
|
||||||
{data.country && <Property title="Country">{data.country}</Property>}
|
{data.country && <Property title="Country">{data.country}</Property>}
|
||||||
{data.parentHandle && (
|
{data.parentHandle && (
|
||||||
<DataList.Item>
|
<DataList.Item>
|
||||||
@@ -70,19 +73,49 @@ const IPCard: FunctionComponent<IPCardProps> = ({ data, url }: IPCardProps) => {
|
|||||||
</DataList.Value>
|
</DataList.Value>
|
||||||
</DataList.Item>
|
</DataList.Item>
|
||||||
)}
|
)}
|
||||||
<Property title="Events">
|
{data.status && data.status.length > 0 && (
|
||||||
<Events key={0} data={data.events} />
|
<DataList.Item>
|
||||||
</Property>
|
<DataList.Label>Status</DataList.Label>
|
||||||
<DataList.Item align="center">
|
<DataList.Value>
|
||||||
<DataList.Label>Status</DataList.Label>
|
<Flex gap="2" wrap="wrap">
|
||||||
<DataList.Value>
|
{data.status.map((status, index) => (
|
||||||
<Flex gap="2" wrap="wrap">
|
<StatusBadge key={index} status={status} />
|
||||||
{data.status.map((status, index) => (
|
))}
|
||||||
<StatusBadge key={index} status={status} />
|
</Flex>
|
||||||
))}
|
</DataList.Value>
|
||||||
</Flex>
|
</DataList.Item>
|
||||||
</DataList.Value>
|
)}
|
||||||
</DataList.Item>
|
{data.port43 && (
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label>WHOIS Server</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
<Flex align="center" gap="2">
|
||||||
|
<Code variant="ghost">{data.port43}</Code>
|
||||||
|
<CopyButton value={data.port43} />
|
||||||
|
</Flex>
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
)}
|
||||||
|
{data.entities && data.entities.length > 0 && (
|
||||||
|
<Property title="Entities">
|
||||||
|
<EntitiesSection entities={data.entities} />
|
||||||
|
</Property>
|
||||||
|
)}
|
||||||
|
{data.events && data.events.length > 0 && (
|
||||||
|
<Property title="Events">
|
||||||
|
<Events data={data.events} />
|
||||||
|
</Property>
|
||||||
|
)}
|
||||||
|
{data.links && data.links.length > 0 && (
|
||||||
|
<Property title="Links">
|
||||||
|
<LinksSection links={data.links} />
|
||||||
|
</Property>
|
||||||
|
)}
|
||||||
|
{data.remarks && data.remarks.length > 0 && (
|
||||||
|
<Property title="Remarks">
|
||||||
|
<RemarksSection remarks={data.remarks} />
|
||||||
|
</Property>
|
||||||
|
)}
|
||||||
</DataList.Root>
|
</DataList.Root>
|
||||||
</AbstractCard>
|
</AbstractCard>
|
||||||
);
|
);
|
||||||
|
|||||||
69
src/rdap/components/LinksSection.tsx
Normal file
69
src/rdap/components/LinksSection.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import type { FunctionComponent } from "react";
|
||||||
|
import React from "react";
|
||||||
|
import type { Link as RdapLink } from "@/rdap/schemas";
|
||||||
|
import { Table, Link, Text, Badge } from "@radix-ui/themes";
|
||||||
|
|
||||||
|
export type LinksSectionProps = {
|
||||||
|
links: RdapLink[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const LinksSection: FunctionComponent<LinksSectionProps> = ({ links }) => {
|
||||||
|
if (!links || links.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table.Root size="1" variant="surface" layout="auto">
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.ColumnHeaderCell>URL</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>Relation</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>Title</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>Type</Table.ColumnHeaderCell>
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body>
|
||||||
|
{links.map((link, index) => (
|
||||||
|
<Table.Row key={index}>
|
||||||
|
<Table.Cell>
|
||||||
|
<Link href={link.href} target="_blank" rel="noreferrer" size="2">
|
||||||
|
{link.value || link.href}
|
||||||
|
</Link>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
{link.rel ? (
|
||||||
|
<Badge variant="soft" size="1">
|
||||||
|
{link.rel}
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Text size="2" style={{ color: "var(--gray-a6)" }}>
|
||||||
|
—
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
{link.title ? (
|
||||||
|
<Text size="2">{link.title}</Text>
|
||||||
|
) : (
|
||||||
|
<Text size="2" style={{ color: "var(--gray-a6)" }}>
|
||||||
|
—
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
{link.type ? (
|
||||||
|
<Text size="2" color="gray">
|
||||||
|
{link.type}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text size="2" style={{ color: "var(--gray-a6)" }}>
|
||||||
|
—
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LinksSection;
|
||||||
@@ -2,7 +2,13 @@ import type { FunctionComponent } from "react";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import type { Nameserver } from "@/rdap/schemas";
|
import type { Nameserver } from "@/rdap/schemas";
|
||||||
import CopyButton from "@/components/CopyButton";
|
import CopyButton from "@/components/CopyButton";
|
||||||
|
import StatusBadge from "@/components/StatusBadge";
|
||||||
import AbstractCard from "@/components/AbstractCard";
|
import AbstractCard from "@/components/AbstractCard";
|
||||||
|
import Property from "@/components/Property";
|
||||||
|
import Events from "@/rdap/components/Events";
|
||||||
|
import LinksSection from "@/rdap/components/LinksSection";
|
||||||
|
import RemarksSection from "@/rdap/components/RemarksSection";
|
||||||
|
import EntitiesSection from "@/rdap/components/EntitiesSection";
|
||||||
import { Flex, DataList, Badge, Text, Code } from "@radix-ui/themes";
|
import { Flex, DataList, Badge, Text, Code } from "@radix-ui/themes";
|
||||||
|
|
||||||
export type NameserverCardProps = {
|
export type NameserverCardProps = {
|
||||||
@@ -26,8 +32,19 @@ const NameserverCard: FunctionComponent<NameserverCardProps> = ({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<DataList.Root orientation={{ initial: "vertical", sm: "horizontal" }} size="2">
|
<DataList.Root orientation={{ initial: "vertical", sm: "horizontal" }} size="2">
|
||||||
|
{data.unicodeName && data.unicodeName !== data.ldhName && (
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label>Unicode Name</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
<Flex align="center" gap="2">
|
||||||
|
<Code variant="ghost">{data.unicodeName}</Code>
|
||||||
|
<CopyButton value={data.unicodeName} />
|
||||||
|
</Flex>
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
)}
|
||||||
<DataList.Item>
|
<DataList.Item>
|
||||||
<DataList.Label>LDH Name</DataList.Label>
|
<DataList.Label>{data.unicodeName ? "LDH Name" : "Name"}</DataList.Label>
|
||||||
<DataList.Value>
|
<DataList.Value>
|
||||||
<Flex align="center" gap="2">
|
<Flex align="center" gap="2">
|
||||||
<Code variant="ghost">{data.ldhName}</Code>
|
<Code variant="ghost">{data.ldhName}</Code>
|
||||||
@@ -35,6 +52,90 @@ const NameserverCard: FunctionComponent<NameserverCardProps> = ({
|
|||||||
</Flex>
|
</Flex>
|
||||||
</DataList.Value>
|
</DataList.Value>
|
||||||
</DataList.Item>
|
</DataList.Item>
|
||||||
|
{data.handle && (
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label>Handle</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
<Flex align="center" gap="2">
|
||||||
|
<Code variant="ghost">{data.handle}</Code>
|
||||||
|
<CopyButton value={data.handle} />
|
||||||
|
</Flex>
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
)}
|
||||||
|
{data.ipAddresses?.v4 && data.ipAddresses.v4.length > 0 && (
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label>IPv4 Addresses</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
<Flex direction="column" gap="1">
|
||||||
|
{data.ipAddresses.v4.map((ip, index) => (
|
||||||
|
<Flex key={index} align="center" gap="2">
|
||||||
|
<Code variant="ghost">{ip}</Code>
|
||||||
|
<CopyButton value={ip} />
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
)}
|
||||||
|
{data.ipAddresses?.v6 && data.ipAddresses.v6.length > 0 && (
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label>IPv6 Addresses</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
<Flex direction="column" gap="1">
|
||||||
|
{data.ipAddresses.v6.map((ip, index) => (
|
||||||
|
<Flex key={index} align="center" gap="2">
|
||||||
|
<Code variant="ghost">{ip}</Code>
|
||||||
|
<CopyButton value={ip} />
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
)}
|
||||||
|
{data.status && data.status.length > 0 && (
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label>Status</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
<Flex gap="2" wrap="wrap">
|
||||||
|
{data.status.map((status, index) => (
|
||||||
|
<StatusBadge key={index} status={status} />
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
)}
|
||||||
|
{data.port43 && (
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label>WHOIS Server</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
<Flex align="center" gap="2">
|
||||||
|
<Code variant="ghost">{data.port43}</Code>
|
||||||
|
<CopyButton value={data.port43} />
|
||||||
|
</Flex>
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
)}
|
||||||
|
{data.entities && data.entities.length > 0 && (
|
||||||
|
<Property title="Entities">
|
||||||
|
<EntitiesSection entities={data.entities} />
|
||||||
|
</Property>
|
||||||
|
)}
|
||||||
|
{data.events && data.events.length > 0 && (
|
||||||
|
<Property title="Events">
|
||||||
|
<Events data={data.events} />
|
||||||
|
</Property>
|
||||||
|
)}
|
||||||
|
{data.links && data.links.length > 0 && (
|
||||||
|
<Property title="Links">
|
||||||
|
<LinksSection links={data.links} />
|
||||||
|
</Property>
|
||||||
|
)}
|
||||||
|
{data.remarks && data.remarks.length > 0 && (
|
||||||
|
<Property title="Remarks">
|
||||||
|
<RemarksSection remarks={data.remarks} />
|
||||||
|
</Property>
|
||||||
|
)}
|
||||||
</DataList.Root>
|
</DataList.Root>
|
||||||
</AbstractCard>
|
</AbstractCard>
|
||||||
);
|
);
|
||||||
|
|||||||
85
src/rdap/components/NameserversSection.tsx
Normal file
85
src/rdap/components/NameserversSection.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import type { FunctionComponent } from "react";
|
||||||
|
import React from "react";
|
||||||
|
import type { Nameserver } from "@/rdap/schemas";
|
||||||
|
import { Table, Text, Code, Flex, Badge } from "@radix-ui/themes";
|
||||||
|
import CopyButton from "@/components/CopyButton";
|
||||||
|
|
||||||
|
export type NameserversSectionProps = {
|
||||||
|
nameservers: Nameserver[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const NameserversSection: FunctionComponent<NameserversSectionProps> = ({ nameservers }) => {
|
||||||
|
if (!nameservers || nameservers.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table.Root size="1" variant="surface" layout="auto">
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.ColumnHeaderCell>Nameserver</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>IPv4 Addresses</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>IPv6 Addresses</Table.ColumnHeaderCell>
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body>
|
||||||
|
{nameservers.map((ns, index) => (
|
||||||
|
<Table.Row key={index}>
|
||||||
|
<Table.Cell>
|
||||||
|
<Flex align="center" gap="2">
|
||||||
|
<Code variant="ghost">{ns.ldhName}</Code>
|
||||||
|
<CopyButton value={ns.ldhName} />
|
||||||
|
</Flex>
|
||||||
|
{ns.unicodeName && ns.unicodeName !== ns.ldhName && (
|
||||||
|
<Flex align="center" gap="2" mt="1">
|
||||||
|
<Badge variant="soft" size="1">
|
||||||
|
Unicode
|
||||||
|
</Badge>
|
||||||
|
<Code variant="ghost" size="1">
|
||||||
|
{ns.unicodeName}
|
||||||
|
</Code>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
{ns.ipAddresses?.v4 && ns.ipAddresses.v4.length > 0 ? (
|
||||||
|
<Flex direction="column" gap="1">
|
||||||
|
{ns.ipAddresses.v4.map((ip, ipIndex) => (
|
||||||
|
<Flex key={ipIndex} align="center" gap="2">
|
||||||
|
<Code variant="ghost" size="1">
|
||||||
|
{ip}
|
||||||
|
</Code>
|
||||||
|
<CopyButton value={ip} />
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
) : (
|
||||||
|
<Text size="2" style={{ color: "var(--gray-a4)" }}>
|
||||||
|
—
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
{ns.ipAddresses?.v6 && ns.ipAddresses.v6.length > 0 ? (
|
||||||
|
<Flex direction="column" gap="1">
|
||||||
|
{ns.ipAddresses.v6.map((ip, ipIndex) => (
|
||||||
|
<Flex key={ipIndex} align="center" gap="2">
|
||||||
|
<Code variant="ghost" size="1">
|
||||||
|
{ip}
|
||||||
|
</Code>
|
||||||
|
<CopyButton value={ip} />
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
) : (
|
||||||
|
<Text size="2" style={{ color: "var(--gray-a4)" }}>
|
||||||
|
—
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NameserversSection;
|
||||||
61
src/rdap/components/RemarksSection.tsx
Normal file
61
src/rdap/components/RemarksSection.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import type { FunctionComponent } from "react";
|
||||||
|
import React from "react";
|
||||||
|
import type { Remark, Notice } from "@/rdap/schemas";
|
||||||
|
import { Box, Text, Heading, Flex, Badge } from "@radix-ui/themes";
|
||||||
|
import LinksSection from "@/rdap/components/LinksSection";
|
||||||
|
|
||||||
|
export type RemarksSectionProps = {
|
||||||
|
remarks?: Remark[] | Notice[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const RemarksSection: FunctionComponent<RemarksSectionProps> = ({ remarks }) => {
|
||||||
|
if (!remarks || remarks.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction="column" gap="3">
|
||||||
|
{remarks.map((remark, index) => (
|
||||||
|
<Box
|
||||||
|
key={index}
|
||||||
|
p="3"
|
||||||
|
style={{
|
||||||
|
borderLeft: "3px solid var(--accent-a5)",
|
||||||
|
backgroundColor: "var(--gray-a2)",
|
||||||
|
borderRadius: "var(--radius-3)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex direction="column" gap="2">
|
||||||
|
{remark.title && (
|
||||||
|
<Flex align="center" gap="2">
|
||||||
|
<Heading size="3">{remark.title}</Heading>
|
||||||
|
{remark.type && (
|
||||||
|
<Badge variant="soft" size="1">
|
||||||
|
{remark.type}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
{remark.description && remark.description.length > 0 && (
|
||||||
|
<Flex direction="column" gap="1">
|
||||||
|
{remark.description.map((desc, descIndex) => (
|
||||||
|
<Text key={descIndex} size="2" style={{ lineHeight: "1.6" }}>
|
||||||
|
{desc}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
{remark.links && remark.links.length > 0 && (
|
||||||
|
<Box mt="2">
|
||||||
|
<Text size="2" weight="medium" mb="1" style={{ display: "block" }}>
|
||||||
|
Related Links
|
||||||
|
</Text>
|
||||||
|
<LinksSection links={remark.links} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RemarksSection;
|
||||||
152
src/rdap/components/SecureDNSSection.tsx
Normal file
152
src/rdap/components/SecureDNSSection.tsx
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import CopyButton from "@/components/CopyButton";
|
||||||
|
import type { SecureDNS } from "@/rdap/schemas";
|
||||||
|
import { Badge, Box, Code, DataList, Flex, Table, Text } from "@radix-ui/themes";
|
||||||
|
import type { FunctionComponent } from "react";
|
||||||
|
|
||||||
|
export type SecureDNSSectionProps = {
|
||||||
|
secureDNS: SecureDNS;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SecureDNSSection: FunctionComponent<SecureDNSSectionProps> = ({ secureDNS }) => {
|
||||||
|
const hasData =
|
||||||
|
secureDNS.zoneSigned !== undefined ||
|
||||||
|
secureDNS.delegationSigned !== undefined ||
|
||||||
|
secureDNS.maxSigLife !== undefined ||
|
||||||
|
(secureDNS.dsData && secureDNS.dsData.length > 0) ||
|
||||||
|
(secureDNS.keyData && secureDNS.keyData.length > 0);
|
||||||
|
|
||||||
|
if (!hasData) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<DataList.Root orientation={{ initial: "vertical", sm: "horizontal" }} size="2">
|
||||||
|
{secureDNS.zoneSigned !== undefined && (
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label>Zone Signed</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
<Badge color={secureDNS.zoneSigned ? "green" : "gray"} variant="soft">
|
||||||
|
{secureDNS.zoneSigned ? "Yes" : "No"}
|
||||||
|
</Badge>
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{secureDNS.delegationSigned !== undefined && (
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label>Delegation Signed</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
<Badge
|
||||||
|
color={secureDNS.delegationSigned ? "green" : "gray"}
|
||||||
|
variant="soft"
|
||||||
|
>
|
||||||
|
{secureDNS.delegationSigned ? "Yes" : "No"}
|
||||||
|
</Badge>
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{secureDNS.maxSigLife !== undefined && (
|
||||||
|
<DataList.Item>
|
||||||
|
<DataList.Label>Max Signature Life</DataList.Label>
|
||||||
|
<DataList.Value>
|
||||||
|
<Text>{secureDNS.maxSigLife} seconds</Text>
|
||||||
|
</DataList.Value>
|
||||||
|
</DataList.Item>
|
||||||
|
)}
|
||||||
|
</DataList.Root>
|
||||||
|
|
||||||
|
{secureDNS.dsData && secureDNS.dsData.length > 0 && (
|
||||||
|
<Table.Root size="1" variant="surface" mt="3">
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.ColumnHeaderCell>Key Tag</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>Algorithm</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>Digest Type</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>Digest</Table.ColumnHeaderCell>
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body>
|
||||||
|
{secureDNS.dsData.map((ds, index) => (
|
||||||
|
<Table.Row key={index}>
|
||||||
|
<Table.Cell>
|
||||||
|
<Code variant="ghost">{ds.keyTag}</Code>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Code variant="ghost">{ds.algorithm}</Code>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Code variant="ghost">{ds.digestType}</Code>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Flex align="center" gap="2">
|
||||||
|
<Code
|
||||||
|
variant="ghost"
|
||||||
|
style={{
|
||||||
|
maxWidth: "300px",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ds.digest}
|
||||||
|
</Code>
|
||||||
|
<CopyButton value={ds.digest} />
|
||||||
|
</Flex>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{secureDNS.keyData && secureDNS.keyData.length > 0 && (
|
||||||
|
<Box mt="4">
|
||||||
|
<Text size="3" weight="medium" mb="2" style={{ display: "block" }}>
|
||||||
|
Key Data
|
||||||
|
</Text>
|
||||||
|
<Table.Root size="1" variant="surface">
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.ColumnHeaderCell>Flags</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>Protocol</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>Algorithm</Table.ColumnHeaderCell>
|
||||||
|
<Table.ColumnHeaderCell>Public Key</Table.ColumnHeaderCell>
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body>
|
||||||
|
{secureDNS.keyData.map((key, index) => (
|
||||||
|
<Table.Row key={index}>
|
||||||
|
<Table.Cell>
|
||||||
|
<Code variant="ghost">{key.flags}</Code>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Code variant="ghost">{key.protocol}</Code>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Code variant="ghost">{key.algorithm}</Code>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Flex align="center" gap="2">
|
||||||
|
<Code
|
||||||
|
variant="ghost"
|
||||||
|
style={{
|
||||||
|
maxWidth: "300px",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{key.publicKey}
|
||||||
|
</Code>
|
||||||
|
<CopyButton value={key.publicKey} />
|
||||||
|
</Flex>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
))}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SecureDNSSection;
|
||||||
239
src/rdap/components/VCardDisplay.tsx
Normal file
239
src/rdap/components/VCardDisplay.tsx
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
import type { FunctionComponent } from "react";
|
||||||
|
import React from "react";
|
||||||
|
import type { VCardArray } from "@/rdap/schemas";
|
||||||
|
import { Table, Flex, Code, Link, Text } from "@radix-ui/themes";
|
||||||
|
import CopyButton from "@/components/CopyButton";
|
||||||
|
|
||||||
|
export type VCardDisplayProps = {
|
||||||
|
vcardArray: VCardArray;
|
||||||
|
};
|
||||||
|
|
||||||
|
type VCardProperty = [string, Record<string, string>, string, string];
|
||||||
|
|
||||||
|
type StructuredName = {
|
||||||
|
family: string;
|
||||||
|
given: string;
|
||||||
|
additional: string;
|
||||||
|
prefix: string;
|
||||||
|
suffix: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Address = {
|
||||||
|
street: string;
|
||||||
|
locality: string;
|
||||||
|
region: string;
|
||||||
|
postal: string;
|
||||||
|
country: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ParsedVCard = {
|
||||||
|
fn?: string;
|
||||||
|
name?: StructuredName;
|
||||||
|
org?: string;
|
||||||
|
emails?: string[];
|
||||||
|
phones?: string[];
|
||||||
|
addresses?: Address[];
|
||||||
|
urls?: string[];
|
||||||
|
title?: string;
|
||||||
|
role?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a vCard (jCard) array and extracts common contact properties.
|
||||||
|
* jCard format: ["vcard", [[prop_name, params, value_type, value], ...]]
|
||||||
|
*/
|
||||||
|
const parseVCard = (vcardArray: VCardArray): ParsedVCard => {
|
||||||
|
const [, properties] = vcardArray;
|
||||||
|
const result: ParsedVCard = {};
|
||||||
|
|
||||||
|
properties.forEach((prop: VCardProperty) => {
|
||||||
|
if (!Array.isArray(prop) || prop.length < 4) return;
|
||||||
|
|
||||||
|
const [name, , , value] = prop as VCardProperty;
|
||||||
|
const nameLower = name.toLowerCase();
|
||||||
|
|
||||||
|
switch (nameLower) {
|
||||||
|
case "fn": // Formatted name
|
||||||
|
result.fn = value;
|
||||||
|
break;
|
||||||
|
case "n": // Structured name [family, given, additional, prefix, suffix]
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
result.name = {
|
||||||
|
family: value[0],
|
||||||
|
given: value[1],
|
||||||
|
additional: value[2],
|
||||||
|
prefix: value[3],
|
||||||
|
suffix: value[4],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "org": // Organization
|
||||||
|
result.org = Array.isArray(value) ? value.join(" > ") : value;
|
||||||
|
break;
|
||||||
|
case "email":
|
||||||
|
if (!result.emails) result.emails = [];
|
||||||
|
result.emails.push(value);
|
||||||
|
break;
|
||||||
|
case "tel": // Telephone
|
||||||
|
if (!result.phones) result.phones = [];
|
||||||
|
result.phones.push(value);
|
||||||
|
break;
|
||||||
|
case "adr": // Address [PO, extended, street, locality, region, postal, country]
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
const address = {
|
||||||
|
street: value[2],
|
||||||
|
locality: value[3],
|
||||||
|
region: value[4],
|
||||||
|
postal: value[5],
|
||||||
|
country: value[6],
|
||||||
|
};
|
||||||
|
if (!result.addresses) result.addresses = [];
|
||||||
|
result.addresses.push(address);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "url":
|
||||||
|
if (!result.urls) result.urls = [];
|
||||||
|
result.urls.push(value);
|
||||||
|
break;
|
||||||
|
case "title":
|
||||||
|
result.title = value;
|
||||||
|
break;
|
||||||
|
case "role":
|
||||||
|
result.role = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const VCardDisplay: FunctionComponent<VCardDisplayProps> = ({ vcardArray }) => {
|
||||||
|
const vcard = parseVCard(vcardArray);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table.Root size="2" variant="surface">
|
||||||
|
<Table.Body>
|
||||||
|
{vcard.fn && (
|
||||||
|
<Table.Row>
|
||||||
|
<Table.RowHeaderCell>Name</Table.RowHeaderCell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Flex gap="2" align="center">
|
||||||
|
<Text>{vcard.fn}</Text>
|
||||||
|
<CopyButton value={vcard.fn} />
|
||||||
|
</Flex>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{vcard.org && (
|
||||||
|
<Table.Row>
|
||||||
|
<Table.RowHeaderCell>Organization</Table.RowHeaderCell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Flex align="center" gap="2">
|
||||||
|
<Code variant="ghost">{vcard.org}</Code>
|
||||||
|
<CopyButton value={vcard.org} />
|
||||||
|
</Flex>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{vcard.title && (
|
||||||
|
<Table.Row>
|
||||||
|
<Table.RowHeaderCell>Title</Table.RowHeaderCell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Text>{vcard.title}</Text>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{vcard.role && (
|
||||||
|
<Table.Row>
|
||||||
|
<Table.RowHeaderCell>Role</Table.RowHeaderCell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Text>{vcard.role}</Text>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{vcard.emails && vcard.emails.length > 0 && (
|
||||||
|
<Table.Row>
|
||||||
|
<Table.RowHeaderCell>Email</Table.RowHeaderCell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Flex direction="column" gap="1">
|
||||||
|
{vcard.emails.map((email: string, index: number) => (
|
||||||
|
<Flex key={index} align="center" gap="2">
|
||||||
|
<Link href={`mailto:${email}`}>
|
||||||
|
<Code variant="ghost">{email}</Code>
|
||||||
|
</Link>
|
||||||
|
<CopyButton value={email} />
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{vcard.phones && vcard.phones.length > 0 && (
|
||||||
|
<Table.Row>
|
||||||
|
<Table.RowHeaderCell>Phone</Table.RowHeaderCell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Flex direction="column" gap="1">
|
||||||
|
{vcard.phones.map((phone: string, index: number) => (
|
||||||
|
<Flex key={index} align="center" gap="2">
|
||||||
|
<Link href={`tel:${phone}`}>
|
||||||
|
<Code variant="ghost">{phone}</Code>
|
||||||
|
</Link>
|
||||||
|
<CopyButton value={phone} />
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{vcard.addresses && vcard.addresses.length > 0 && (
|
||||||
|
<Table.Row>
|
||||||
|
<Table.RowHeaderCell>Address</Table.RowHeaderCell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Flex direction="column" gap="2">
|
||||||
|
{vcard.addresses.map((addr: Address, index: number) => (
|
||||||
|
<Text key={index} size="2">
|
||||||
|
{[
|
||||||
|
addr.street,
|
||||||
|
addr.locality,
|
||||||
|
addr.region,
|
||||||
|
addr.postal,
|
||||||
|
addr.country,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(", ")}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{vcard.urls && vcard.urls.length > 0 && (
|
||||||
|
<Table.Row>
|
||||||
|
<Table.RowHeaderCell>URL</Table.RowHeaderCell>
|
||||||
|
<Table.Cell>
|
||||||
|
<Flex direction="column" gap="1">
|
||||||
|
{vcard.urls.map((url: string, index: number) => (
|
||||||
|
<Flex key={index} align="center" gap="2">
|
||||||
|
<Link href={url} target="_blank" rel="noreferrer">
|
||||||
|
<Code variant="ghost">{url}</Code>
|
||||||
|
</Link>
|
||||||
|
<CopyButton value={url} />
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
)}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VCardDisplay;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { getType } from "@/rdap/utils";
|
import { getType, validateInputForType } from "@/rdap/utils";
|
||||||
import type { AutonomousNumber, Domain, IpNetwork, SubmitProps, TargetType } from "@/rdap/schemas";
|
import type { AutonomousNumber, Domain, IpNetwork, SubmitProps, TargetType } from "@/rdap/schemas";
|
||||||
import {
|
import {
|
||||||
AutonomousNumberSchema,
|
AutonomousNumberSchema,
|
||||||
@@ -75,35 +75,56 @@ const useLookup = (warningHandler?: WarningHandler) => {
|
|||||||
if (target == null || target.length == 0)
|
if (target == null || target.length == 0)
|
||||||
return Result.err(new Error("A target must be given in order to execute a lookup."));
|
return Result.err(new Error("A target must be given in order to execute a lookup."));
|
||||||
|
|
||||||
const targetType = await getTypeEasy(target);
|
let targetType: TargetType;
|
||||||
|
|
||||||
if (targetType.isErr) {
|
if (currentType != null) {
|
||||||
return Result.err(
|
// User has explicitly selected a type
|
||||||
new Error("Unable to determine type, unable to send query", {
|
targetType = currentType;
|
||||||
cause: targetType.error,
|
|
||||||
})
|
// Validate the input matches the selected type
|
||||||
);
|
const validation = await validateInputForType(target, currentType, getRegistry);
|
||||||
|
if (validation.isErr) {
|
||||||
|
// Show warning but proceed with user's selection
|
||||||
|
if (warningHandler != undefined) {
|
||||||
|
warningHandler({
|
||||||
|
message: `Warning: ${validation.error}. Proceeding with selected type "${currentType}".`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Autodetect mode
|
||||||
|
const detectedType = await getTypeEasy(target);
|
||||||
|
|
||||||
|
if (detectedType.isErr) {
|
||||||
|
return Result.err(
|
||||||
|
new Error("Unable to determine type, unable to send query", {
|
||||||
|
cause: detectedType.error,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
targetType = detectedType.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (targetType.value) {
|
switch (targetType) {
|
||||||
// Block scoped case to allow url const reuse
|
// Block scoped case to allow url const reuse
|
||||||
case "ip4": {
|
case "ip4": {
|
||||||
await loadBootstrap("ip4");
|
await loadBootstrap("ip4");
|
||||||
const url = getRegistryURL(targetType.value, target);
|
const url = getRegistryURL(targetType, target);
|
||||||
const result = await getAndParse<IpNetwork>(url, IpNetworkSchema);
|
const result = await getAndParse<IpNetwork>(url, IpNetworkSchema);
|
||||||
if (result.isErr) return Result.err(result.error);
|
if (result.isErr) return Result.err(result.error);
|
||||||
return Result.ok({ data: result.value, url });
|
return Result.ok({ data: result.value, url });
|
||||||
}
|
}
|
||||||
case "ip6": {
|
case "ip6": {
|
||||||
await loadBootstrap("ip6");
|
await loadBootstrap("ip6");
|
||||||
const url = getRegistryURL(targetType.value, target);
|
const url = getRegistryURL(targetType, target);
|
||||||
const result = await getAndParse<IpNetwork>(url, IpNetworkSchema);
|
const result = await getAndParse<IpNetwork>(url, IpNetworkSchema);
|
||||||
if (result.isErr) return Result.err(result.error);
|
if (result.isErr) return Result.err(result.error);
|
||||||
return Result.ok({ data: result.value, url });
|
return Result.ok({ data: result.value, url });
|
||||||
}
|
}
|
||||||
case "domain": {
|
case "domain": {
|
||||||
await loadBootstrap("domain");
|
await loadBootstrap("domain");
|
||||||
const url = getRegistryURL(targetType.value, target);
|
const url = getRegistryURL(targetType, target);
|
||||||
|
|
||||||
// HTTP
|
// HTTP
|
||||||
if (url.startsWith("http://") && url != repeatableRef.current) {
|
if (url.startsWith("http://") && url != repeatableRef.current) {
|
||||||
@@ -123,7 +144,7 @@ const useLookup = (warningHandler?: WarningHandler) => {
|
|||||||
}
|
}
|
||||||
case "autnum": {
|
case "autnum": {
|
||||||
await loadBootstrap("autnum");
|
await loadBootstrap("autnum");
|
||||||
const url = getRegistryURL(targetType.value, target);
|
const url = getRegistryURL(targetType, target);
|
||||||
const result = await getAndParse<AutonomousNumber>(url, AutonomousNumberSchema);
|
const result = await getAndParse<AutonomousNumber>(url, AutonomousNumberSchema);
|
||||||
if (result.isErr) return Result.err(result.error);
|
if (result.isErr) return Result.err(result.error);
|
||||||
return Result.ok({ data: result.value, url });
|
return Result.ok({ data: result.value, url });
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Enums
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
export const TargetTypeEnum = z.enum([
|
export const TargetTypeEnum = z.enum([
|
||||||
"autnum",
|
"autnum",
|
||||||
"domain",
|
"domain",
|
||||||
@@ -55,10 +51,6 @@ export const StatusEnum = z.enum([
|
|||||||
"transfer period",
|
"transfer period",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Schemas
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
export const LinkSchema = z.object({
|
export const LinkSchema = z.object({
|
||||||
value: z.string().optional(), // de-facto optional
|
value: z.string().optional(), // de-facto optional
|
||||||
rel: z.string().optional(), // de-facto optional
|
rel: z.string().optional(), // de-facto optional
|
||||||
@@ -69,25 +61,6 @@ export const LinkSchema = z.object({
|
|||||||
type: z.string().optional(),
|
type: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const EntitySchema = z.object({
|
|
||||||
objectClassName: z.literal("entity"),
|
|
||||||
handle: z.string().optional(),
|
|
||||||
roles: z.array(z.string()),
|
|
||||||
publicIds: z
|
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
type: z.string(),
|
|
||||||
identifier: z.string(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const NameserverSchema = z.object({
|
|
||||||
objectClassName: z.literal("nameserver"),
|
|
||||||
ldhName: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const EventSchema = z.object({
|
export const EventSchema = z.object({
|
||||||
eventAction: z.string(),
|
eventAction: z.string(),
|
||||||
eventActor: z.string().optional(),
|
eventActor: z.string().optional(),
|
||||||
@@ -97,25 +70,116 @@ export const EventSchema = z.object({
|
|||||||
export const NoticeSchema = z.object({
|
export const NoticeSchema = z.object({
|
||||||
description: z.string().array(), // de jure required
|
description: z.string().array(), // de jure required
|
||||||
title: z.string().optional(),
|
title: z.string().optional(),
|
||||||
|
type: z.string().optional(),
|
||||||
links: z.array(LinkSchema).optional(),
|
links: z.array(LinkSchema).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const RemarkSchema = z.object({
|
||||||
|
description: z.string().array(), // de jure required
|
||||||
|
title: z.string().optional(),
|
||||||
|
type: z.string().optional(),
|
||||||
|
links: z.array(LinkSchema).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// vCard 4.0 in jCard format (RFC 7095)
|
||||||
|
// Simplified schema - full vCard is complex, so we use a loose schema
|
||||||
|
// Format: ["vcard", [properties...]]
|
||||||
|
export const VCardArraySchema = z.array(z.any());
|
||||||
|
|
||||||
|
export const IPAddressesSchema = z.object({
|
||||||
|
v4: z.array(z.string()).optional(),
|
||||||
|
v6: z.array(z.string()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const DSDataSchema = z.object({
|
||||||
|
keyTag: z.number(),
|
||||||
|
algorithm: z.number(),
|
||||||
|
digest: z.string(),
|
||||||
|
digestType: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const KeyDataSchema = z.object({
|
||||||
|
flags: z.number(),
|
||||||
|
protocol: z.number(),
|
||||||
|
publicKey: z.string(),
|
||||||
|
algorithm: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SecureDNSSchema = z.object({
|
||||||
|
zoneSigned: z.boolean().optional(),
|
||||||
|
delegationSigned: z.boolean().optional(),
|
||||||
|
maxSigLife: z.number().optional(),
|
||||||
|
dsData: z.array(DSDataSchema).optional(),
|
||||||
|
keyData: z.array(KeyDataSchema).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const VariantSchema = z.object({
|
||||||
|
relation: z.array(z.string()).optional(),
|
||||||
|
idnTable: z.string().optional(),
|
||||||
|
variantNames: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
ldhName: z.string(),
|
||||||
|
unicodeName: z.string().optional(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const NameserverSchema = z.object({
|
||||||
|
objectClassName: z.literal("nameserver"),
|
||||||
|
ldhName: z.string(),
|
||||||
|
unicodeName: z.string().optional(),
|
||||||
|
handle: z.string().optional(),
|
||||||
|
ipAddresses: IPAddressesSchema.optional(),
|
||||||
|
status: z.array(StatusEnum).optional(),
|
||||||
|
events: z.array(EventSchema).optional(),
|
||||||
|
links: z.array(LinkSchema).optional(),
|
||||||
|
remarks: z.array(RemarkSchema).optional(),
|
||||||
|
port43: z.string().optional(),
|
||||||
|
entities: z.lazy(() => z.array(EntitySchema)).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const IpNetworkSchema = z.object({
|
export const IpNetworkSchema = z.object({
|
||||||
objectClassName: z.literal("ip network"),
|
objectClassName: z.literal("ip network"),
|
||||||
handle: z.string(),
|
handle: z.string(),
|
||||||
startAddress: z.string(),
|
startAddress: z.string(),
|
||||||
endAddress: z.string(),
|
endAddress: z.string(),
|
||||||
ipVersion: z.enum(["v4", "v6"]),
|
ipVersion: z.enum(["v4", "v6"]),
|
||||||
name: z.string(),
|
name: z.string().optional(),
|
||||||
type: z.string(),
|
type: z.string().optional(),
|
||||||
country: z.string().optional(),
|
country: z.string().optional(),
|
||||||
parentHandle: z.string().optional(),
|
parentHandle: z.string().optional(),
|
||||||
status: z.array(StatusEnum),
|
status: z.array(StatusEnum).optional(),
|
||||||
entities: z.array(EntitySchema).optional(),
|
remarks: z.array(RemarkSchema).optional(),
|
||||||
remarks: z.any().optional(),
|
links: z.array(LinkSchema).optional(),
|
||||||
links: z.any().optional(),
|
port43: z.string().optional(),
|
||||||
port43: z.any().optional(),
|
events: z.array(EventSchema).optional(),
|
||||||
events: z.array(EventSchema),
|
// Required for circular reference
|
||||||
|
get entities() {
|
||||||
|
return z.array(EntitySchema).optional();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Forward declaration for circular Entity reference
|
||||||
|
const BaseEntitySchema = z.object({
|
||||||
|
objectClassName: z.literal("entity"),
|
||||||
|
handle: z.string().optional(),
|
||||||
|
vcardArray: VCardArraySchema.optional(),
|
||||||
|
roles: z.array(z.string()).optional(),
|
||||||
|
publicIds: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
type: z.string(),
|
||||||
|
identifier: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
status: z.array(StatusEnum).optional(),
|
||||||
|
events: z.array(EventSchema).optional(),
|
||||||
|
links: z.array(LinkSchema).optional(),
|
||||||
|
remarks: z.array(RemarkSchema).optional(),
|
||||||
|
port43: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const AutonomousNumberSchema = z.object({
|
export const AutonomousNumberSchema = z.object({
|
||||||
@@ -128,24 +192,40 @@ export const AutonomousNumberSchema = z.object({
|
|||||||
status: z.array(StatusEnum),
|
status: z.array(StatusEnum),
|
||||||
country: z.string().length(2),
|
country: z.string().length(2),
|
||||||
events: z.array(EventSchema),
|
events: z.array(EventSchema),
|
||||||
entities: z.array(EntitySchema),
|
get entities() {
|
||||||
roles: z.array(z.string()),
|
return z.array(EntitySchema).optional();
|
||||||
links: z.array(LinkSchema),
|
},
|
||||||
|
links: z.array(LinkSchema).optional(),
|
||||||
|
remarks: z.array(RemarkSchema).optional(),
|
||||||
|
port43: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Full Entity schema with circular references
|
||||||
|
export const EntitySchema = BaseEntitySchema.extend({
|
||||||
|
networks: z.lazy(() => z.array(IpNetworkSchema)).optional(),
|
||||||
|
autnums: z.lazy(() => z.array(AutonomousNumberSchema)).optional(),
|
||||||
|
asEventActor: z.array(EventSchema).optional(),
|
||||||
|
get entities() {
|
||||||
|
return z.array(EntitySchema).optional();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const DomainSchema = z.object({
|
export const DomainSchema = z.object({
|
||||||
objectClassName: z.literal("domain"),
|
objectClassName: z.literal("domain"),
|
||||||
handle: z.string(),
|
handle: z.string().optional(),
|
||||||
ldhName: z.string(),
|
ldhName: z.string(),
|
||||||
unicodeName: z.string().optional(),
|
unicodeName: z.string().optional(),
|
||||||
|
variants: z.array(VariantSchema).optional(),
|
||||||
links: z.array(LinkSchema).optional(),
|
links: z.array(LinkSchema).optional(),
|
||||||
status: z.array(StatusEnum),
|
status: z.array(StatusEnum).optional(),
|
||||||
entities: z.array(EntitySchema),
|
entities: z.lazy(() => z.array(EntitySchema)).optional(),
|
||||||
events: z.array(EventSchema),
|
events: z.array(EventSchema).optional(),
|
||||||
secureDNS: z.any(), // TODO: Complete schema
|
secureDNS: SecureDNSSchema.optional(),
|
||||||
nameservers: z.array(NameserverSchema),
|
nameservers: z.array(NameserverSchema).optional(),
|
||||||
rdapConformance: z.string().array(), // TODO: Complete
|
rdapConformance: z.string().array().optional(),
|
||||||
notices: z.array(NoticeSchema),
|
notices: z.array(NoticeSchema).optional(),
|
||||||
|
remarks: z.array(RemarkSchema).optional(),
|
||||||
|
port43: z.string().optional(),
|
||||||
network: IpNetworkSchema.optional(),
|
network: IpNetworkSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -166,10 +246,6 @@ export const RegisterSchema = z.object({
|
|||||||
version: z.string(),
|
version: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// TypeScript Types
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
// All precise target types that can be placed in the search bar.
|
// All precise target types that can be placed in the search bar.
|
||||||
export type TargetType = z.infer<typeof TargetTypeEnum>;
|
export type TargetType = z.infer<typeof TargetTypeEnum>;
|
||||||
|
|
||||||
@@ -185,6 +261,13 @@ export type Entity = z.infer<typeof EntitySchema>;
|
|||||||
export type Nameserver = z.infer<typeof NameserverSchema>;
|
export type Nameserver = z.infer<typeof NameserverSchema>;
|
||||||
export type Event = z.infer<typeof EventSchema>;
|
export type Event = z.infer<typeof EventSchema>;
|
||||||
export type Notice = z.infer<typeof NoticeSchema>;
|
export type Notice = z.infer<typeof NoticeSchema>;
|
||||||
|
export type Remark = z.infer<typeof RemarkSchema>;
|
||||||
|
export type VCardArray = z.infer<typeof VCardArraySchema>;
|
||||||
|
export type IPAddresses = z.infer<typeof IPAddressesSchema>;
|
||||||
|
export type DSData = z.infer<typeof DSDataSchema>;
|
||||||
|
export type KeyData = z.infer<typeof KeyDataSchema>;
|
||||||
|
export type SecureDNS = z.infer<typeof SecureDNSSchema>;
|
||||||
|
export type Variant = z.infer<typeof VariantSchema>;
|
||||||
export type IpNetwork = z.infer<typeof IpNetworkSchema>;
|
export type IpNetwork = z.infer<typeof IpNetworkSchema>;
|
||||||
export type AutonomousNumber = z.infer<typeof AutonomousNumberSchema>;
|
export type AutonomousNumber = z.infer<typeof AutonomousNumberSchema>;
|
||||||
export type Register = z.infer<typeof RegisterSchema>;
|
export type Register = z.infer<typeof RegisterSchema>;
|
||||||
|
|||||||
@@ -176,3 +176,34 @@ export async function getType(
|
|||||||
|
|
||||||
return Result.err(new Error("No patterns matched the input"));
|
return Result.err(new Error("No patterns matched the input"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates if a given input matches a specific target type.
|
||||||
|
* Used to warn users when their explicit type selection doesn't match the input format.
|
||||||
|
*
|
||||||
|
* @param value - The input value to validate
|
||||||
|
* @param type - The expected target type
|
||||||
|
* @param getRegistry - Function to fetch registry data
|
||||||
|
* @returns A Result containing true if valid, or an error message if invalid
|
||||||
|
*/
|
||||||
|
export async function validateInputForType(
|
||||||
|
value: string,
|
||||||
|
type: TargetType,
|
||||||
|
getRegistry: (type: RootRegistryType) => Promise<Register>
|
||||||
|
): Promise<Result<true, string>> {
|
||||||
|
const validator = TypeValidators.get(type);
|
||||||
|
if (!validator) {
|
||||||
|
return Result.err(`Unknown type: ${type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await validator({ value, getRegistry });
|
||||||
|
|
||||||
|
if (result === true) {
|
||||||
|
return Result.ok(true);
|
||||||
|
} else if (result === false) {
|
||||||
|
return Result.err(`Input "${value}" does not match the format for type "${type}"`);
|
||||||
|
} else {
|
||||||
|
// result is an error message string
|
||||||
|
return Result.err(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user