Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add integration for editing dns records #369

Merged
merged 11 commits into from
Mar 19, 2023
11 changes: 5 additions & 6 deletions app/components/dns-record/dns-record-name.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { Flex, Text } from '@chakra-ui/react';

interface DnsRecordNameProps {
name: string;
subdomain: string;
baseDomain: string;
}

const DnsRecordName = ({ name }: DnsRecordNameProps) => {
const [nameBase, ...restOfName] = name.split('.');

const DnsRecordName = ({ subdomain, baseDomain }: DnsRecordNameProps) => {
return (
<Flex alignItems="flex-end" flexDirection="row">
<Text>
<Text as="span" sx={{ fontWeight: 'medium' }}>
{nameBase}
{subdomain}
</Text>
<Text as="span" color="gray.500">
.{restOfName}
.{baseDomain}
</Text>
</Text>
</Flex>
Expand Down
31 changes: 20 additions & 11 deletions app/components/dns-record/form.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AddIcon, InfoIcon } from '@chakra-ui/icons';
import { AddIcon, InfoIcon, EditIcon } from '@chakra-ui/icons';
import {
Button,
Input,
Expand All @@ -10,23 +10,31 @@ import {
VStack,
} from '@chakra-ui/react';
import { Form } from '@remix-run/react';

import type { Record } from '@prisma/client';
import { useUser } from '~/utils';
import FormField from './form-field';
import { useMemo } from 'react';

type FormMode = 'CREATE' | 'EDIT';

interface DnsRecordFormProps {
interface dnsRecordFormProps {
mode: FormMode;
typeError?: string; // Error for 'Type' field
dnsRecord?: Record;
}

export default function DnsRecordForm({ typeError }: DnsRecordFormProps) {
export default function DomainForm({ typeError, dnsRecord, mode }: dnsRecordFormProps) {
const user = useUser();

const submitButtonText = useMemo(() => (mode === 'CREATE' ? 'Create' : 'Update'), [mode]);
const SubmitButtonIcon = useMemo(() => (mode === 'CREATE' ? AddIcon : EditIcon), [mode]);

return (
<Form className="domain-form" method="post">
<VStack maxW="xl" spacing="2">
<FormField label="Record Name" isRequired={true}>
<InputGroup>
<Input name="name" />
<Input name="subdomain" defaultValue={dnsRecord?.subdomain} />
<InputRightAddon children={`.${user.baseDomain}`} />
</InputGroup>
<Tooltip label="Enter a name for the DNS Record: name">
Expand All @@ -35,7 +43,7 @@ export default function DnsRecordForm({ typeError }: DnsRecordFormProps) {
</FormField>

<FormField label="Type" isRequired={true} error={typeError}>
<Select placeholder="Select a type" name="type">
<Select placeholder="Select a type" name="type" defaultValue={dnsRecord?.type}>
<option value="A">A</option>
<option value="AAAA">AAAA</option>
<option value="CNAME">CNAME</option>
Expand All @@ -47,14 +55,14 @@ export default function DnsRecordForm({ typeError }: DnsRecordFormProps) {
</FormField>

<FormField label="Value" isRequired={true}>
<Input name="value" />
<Input name="value" defaultValue={dnsRecord?.value} />
<Tooltip label="Enter DNS Record value">
<InfoIcon />
</Tooltip>
</FormField>

<FormField label="Ports">
<Input name="ports" />
<Input name="ports" defaultValue={dnsRecord?.ports ?? ''} />
<Tooltip label="Enter port(s) separated by commas (E.g. 8080, 1234)">
<InfoIcon />
</Tooltip>
Expand All @@ -68,11 +76,12 @@ export default function DnsRecordForm({ typeError }: DnsRecordFormProps) {
</FormField>

<FormField label="Description">
<Textarea rows={10} name="description" />
<Textarea rows={10} name="description" defaultValue={dnsRecord?.description ?? ''} />
</FormField>
</VStack>
<Button type="submit" mt="6" rightIcon={<AddIcon boxSize={3.5} mt="0.15rem" />}>
Create
{dnsRecord && <input type="hidden" name="id" value={dnsRecord.id} />}
<Button type="submit" mt="6" rightIcon={<SubmitButtonIcon boxSize={3.5} mt="0.15rem" />}>
{submitButtonText}
</Button>
</Form>
);
Expand Down
72 changes: 40 additions & 32 deletions app/components/domains-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,16 @@ import RecordDeleteAlertDialog from './record-delete-alert-dialog';

import { Form, useNavigate, useTransition } from '@remix-run/react';
import DnsRecordName from './dns-record/dns-record-name';
import { useUser } from '~/utils';

interface DomainsTableProps {
domains: Record[];
interface DnsRecordsTableProps {
dnsRecords: Record[];
}

export default function DomainsTable(props: DomainsTableProps) {
const { domains } = props;
export default function DnsRecordsTable(props: DnsRecordsTableProps) {
const { dnsRecords } = props;

const { baseDomain } = useUser();

const toast = useToast();
const navigate = useNavigate();
Expand All @@ -46,18 +49,18 @@ export default function DomainsTable(props: DomainsTableProps) {
onOpen: onDeleteAlertDialogOpen,
onClose: onDeleteAlertDialogClose,
} = useDisclosure();
const [domainToDelete, setDomainToDelete] = useState<Record | undefined>();
const [dnsRecordToDelete, setDnsRecordToDelete] = useState<Record | undefined>();

function onCopyNameToClipboard(name: string) {
navigator.clipboard.writeText(name);
function onCopyNameToClipboard(subdomain: string) {
navigator.clipboard.writeText(subdomain);
toast({
title: 'Name was copied to clipboard',
title: 'Subdomain was copied to clipboard',
position: 'bottom-right',
status: 'success',
});
}

function renderDomainStatus(action: RecordStatus) {
function renderDnsRecordStatus(action: RecordStatus) {
if (action === 'active') {
return (
<Tooltip label="Domain is live">
Expand Down Expand Up @@ -90,19 +93,19 @@ export default function DomainsTable(props: DomainsTableProps) {
}
}

function onDeleteDomainOpen(domain: Record) {
function onDeleteDnsRecordOpen(dnsRecord: Record) {
onDeleteAlertDialogOpen();
setDomainToDelete(domain);
setDnsRecordToDelete(dnsRecord);
}

function onDomainDeleteCancel() {
function onDnsRecordDeleteCancel() {
onDeleteAlertDialogClose();
setDomainToDelete(undefined);
setDnsRecordToDelete(undefined);
}

function onDomainDeleteConfirm() {
function onDnsRecordDeleteConfirm() {
onDeleteAlertDialogClose();
setDomainToDelete(undefined);
setDnsRecordToDelete(undefined);
}

function onDnsRecordEdit(dnsRecord: Record) {
Expand All @@ -117,21 +120,21 @@ export default function DomainsTable(props: DomainsTableProps) {
<Thead>
<Tr>
<Th />
<Th>Name</Th>
<Th>Subdomain</Th>
<Th>Type</Th>
<Th>Value</Th>
<Th>Expiration date</Th>
<Th />
</Tr>
</Thead>
<Tbody>
{domains.map((domain) => {
{dnsRecords.map((dnsRecord) => {
const isLoading =
transition.state === 'submitting' &&
Number(transition.submission.formData.get('id')) === domain.id;
Number(transition.submission.formData.get('id')) === dnsRecord.id;

return (
<Tr key={domain.id}>
<Tr key={dnsRecord.id}>
{isLoading ? (
<Td py="8" colSpan={7}>
<Flex justifyContent="center">
Expand All @@ -140,28 +143,33 @@ export default function DomainsTable(props: DomainsTableProps) {
</Td>
) : (
<>
<Td>{renderDomainStatus(domain.status)}</Td>
<Td>{renderDnsRecordStatus(dnsRecord.status)}</Td>
<Td>
<Flex justifyContent="space-between" alignItems="center">
<DnsRecordName name={domain.subdomain} />
<Tooltip label="Copy name to clipboard">
<DnsRecordName
subdomain={dnsRecord.subdomain}
baseDomain={baseDomain}
/>
<Tooltip label="Copy subdomain to clipboard">
<IconButton
icon={<CopyIcon color="black" boxSize="5" />}
aria-label="Refresh domain"
variant="ghost"
ml="2"
onClick={() => onCopyNameToClipboard(domain.subdomain)}
onClick={() =>
onCopyNameToClipboard(`${dnsRecord.subdomain}.${baseDomain}`)
}
/>
</Tooltip>
</Flex>
</Td>
<Td>{domain.type}</Td>
<Td>{domain.value}</Td>
<Td>{dnsRecord.type}</Td>
<Td>{dnsRecord.value}</Td>
<Td>
<Flex justifyContent="space-between" alignItems="center">
{domain.expiresAt.toLocaleDateString('en-US')}
{dnsRecord.expiresAt.toLocaleDateString('en-US')}
<Form method="patch" style={{ margin: 0 }}>
<input type="hidden" name="id" value={domain.id} />
<input type="hidden" name="id" value={dnsRecord.id} />
<input type="hidden" name="intent" value="renew-record" />
<Tooltip label="Renew domain">
<IconButton
Expand All @@ -178,7 +186,7 @@ export default function DomainsTable(props: DomainsTableProps) {
<Flex>
<Tooltip label="Edit domain">
<IconButton
onClick={() => onDnsRecordEdit(domain)}
onClick={() => onDnsRecordEdit(dnsRecord)}
icon={<EditIcon color="black" boxSize={5} />}
aria-label="Edit domain"
variant="ghost"
Expand All @@ -187,7 +195,7 @@ export default function DomainsTable(props: DomainsTableProps) {
</Tooltip>
<Tooltip label="Delete domain">
<IconButton
onClick={() => onDeleteDomainOpen(domain)}
onClick={() => onDeleteDnsRecordOpen(dnsRecord)}
icon={<DeleteIcon color="black" boxSize={5} />}
aria-label="Delete domain"
variant="ghost"
Expand All @@ -207,9 +215,9 @@ export default function DomainsTable(props: DomainsTableProps) {
</Card>
<RecordDeleteAlertDialog
isOpen={isDeleteAlertDialogOpen}
onCancel={onDomainDeleteCancel}
onConfirm={onDomainDeleteConfirm}
dnsRecord={domainToDelete}
onCancel={onDnsRecordDeleteCancel}
onConfirm={onDnsRecordDeleteConfirm}
dnsRecord={dnsRecordToDelete}
/>
</>
);
Expand Down
80 changes: 80 additions & 0 deletions app/routes/__index/domains/$dnsRecordId.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Container, Heading, Text } from '@chakra-ui/react';
import { RecordType } from '@prisma/client';
import { redirect, typedjson, useTypedLoaderData } from 'remix-typedjson';
import { z } from 'zod';
import { parseFormSafe } from 'zodix';
import type { ActionArgs, LoaderArgs } from '@remix-run/node';
import DnsRecordForm from '~/components/dns-record/form';
import { requireUser } from '~/session.server';
import { getRecordById } from '~/models/record.server';
import { updateDnsRequest } from '~/queues/dns/update-record-flow.server';

export const loader = async ({ request, params }: LoaderArgs) => {
await requireUser(request);
const { dnsRecordId } = params;
if (!dnsRecordId) {
throw new Response('dnsRecordId should be string', {
status: 400,
});
}

const record = await getRecordById(Number(dnsRecordId));
if (!record) {
throw new Response('The record is not found', {
status: 404,
});
}

return typedjson(record);
};

export const action = async ({ request }: ActionArgs) => {
const user = await requireUser(request);

const DnsRecord = z.object({
id: z.string(),
subdomain: z.string().min(1), // We do not want to consider '' a valid string
type: z.nativeEnum(RecordType),
value: z.string().min(1),
ports: z.string().optional(),
course: z.string().optional(),
description: z.string().optional(),
});

const updatedDnsRecordParams = await parseFormSafe(request, DnsRecord);

if (updatedDnsRecordParams.success === false) {
throw new Response(updatedDnsRecordParams.error.message, {
status: 400,
});
}

const { data } = updatedDnsRecordParams;

await updateDnsRequest({
id: Number(data.id),
username: user.username,
type: data.type,
subdomain: data.subdomain,
value: data.value,
});

return redirect(`/domains`);
};

export default function DomainRoute() {
const record = useTypedLoaderData<typeof loader>();

return (
<Container maxW="container.xl" ml={[null, null, '10vw']}>
<Heading as="h1" size="xl" mt="8">
Edit DNS Record
</Heading>
<Text maxW="lg" mb="3" mt="2">
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has
been the industry's standard dummy text ever since the 1500s
</Text>
<DnsRecordForm dnsRecord={record} mode="EDIT" />
</Container>
);
}
11 changes: 0 additions & 11 deletions app/routes/__index/domains/$domainId.tsx

This file was deleted.

Loading