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
33 changes: 23 additions & 10 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 {
mode: FormMode;
typeError?: string; // Error for 'Type' field
defaultDnsRecord?: Record;
Myrfion marked this conversation as resolved.
Show resolved Hide resolved
}

export default function DnsRecordForm({ typeError }: DnsRecordFormProps) {
export default function DnsRecordForm({ typeError, defaultDnsRecord, 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={defaultDnsRecord?.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={defaultDnsRecord?.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={defaultDnsRecord?.value} />
<Tooltip label="Enter DNS Record value">
<InfoIcon />
</Tooltip>
</FormField>

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

<FormField label="Description">
<Textarea rows={10} name="description" />
<Textarea
rows={10}
name="description"
defaultValue={defaultDnsRecord?.description ?? ''}
/>
</FormField>
</VStack>
<Button type="submit" mt="6" rightIcon={<AddIcon boxSize={3.5} mt="0.15rem" />}>
Create
{defaultDnsRecord && <input type="hidden" name="id" value={defaultDnsRecord.id} />}
<Button type="submit" mt="6" rightIcon={<SubmitButtonIcon boxSize={3.5} mt="0.15rem" />}>
{submitButtonText}
</Button>
</Form>
);
Expand Down
66 changes: 37 additions & 29 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',
Myrfion marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -125,13 +128,13 @@ export default function DomainsTable(props: DomainsTableProps) {
</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} />
<DnsRecordName
subdomain={dnsRecord.subdomain}
baseDomain={baseDomain}
/>
<Tooltip label="Copy name 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 defaultDnsRecord={record} mode="EDIT" />
</Container>
);
}
11 changes: 0 additions & 11 deletions app/routes/__index/domains/$domainId.tsx

This file was deleted.

12 changes: 7 additions & 5 deletions app/routes/__index/domains/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import { json } from '@remix-run/node';
import { z } from 'zod';
import { parseFormSafe } from 'zodix';
import { useInterval } from 'react-use';

import DomainsTable from '~/components/domains-table';
import DnsRecordsTable from '~/components/domains-table';
import { getRecordById, getRecordsByUsername, renewDnsRecordById } from '~/models/record.server';
import { requireUsername } from '~/session.server';
import { deleteDnsRequest } from '~/queues/dns/delete-record-flow.server';
Expand Down Expand Up @@ -79,8 +78,11 @@ export const action = async ({ request }: ActionArgs) => {

export default function DomainsIndexRoute() {
const revalidator = useRevalidator();
const domains = useTypedLoaderData<typeof loader>();
const pending = useMemo(() => domains.some((domain) => domain.status === 'pending'), [domains]);
const dnsRecords = useTypedLoaderData<typeof loader>();
const pending = useMemo(
() => dnsRecords.some((dnsRecord) => dnsRecord.status === 'pending'),
[dnsRecords]
);

// Check to see if any change is pending, and if so, reload every 5s until finished
useInterval(
Expand All @@ -106,7 +108,7 @@ export default function DomainsIndexRoute() {
<Button rightIcon={<AddIcon boxSize={3} />}>Create new domain</Button>
</Link>
</Flex>
<DomainsTable domains={domains} />
<DnsRecordsTable dnsRecords={dnsRecords} />
</Flex>
</Container>
);
Expand Down
Loading