mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-10 04:36:38 +00:00
Pull up downstream changes
This commit is contained in:
430
src/app/[orgId]/settings/domains/CreateDomainForm.tsx
Normal file
430
src/app/[orgId]/settings/domains/CreateDomainForm.tsx
Normal file
@@ -0,0 +1,430 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from "@app/components/ui/form";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import { useToast } from "@app/hooks/useToast";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
Credenza,
|
||||
CredenzaBody,
|
||||
CredenzaClose,
|
||||
CredenzaContent,
|
||||
CredenzaDescription,
|
||||
CredenzaFooter,
|
||||
CredenzaHeader,
|
||||
CredenzaTitle
|
||||
} from "@app/components/Credenza";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
import { CreateDomainResponse } from "@server/routers/domain/createOrgDomain";
|
||||
import { StrategySelect } from "@app/components/StrategySelect";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||
import { InfoIcon, AlertTriangle } from "lucide-react";
|
||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||
import {
|
||||
InfoSection,
|
||||
InfoSectionContent,
|
||||
InfoSections,
|
||||
InfoSectionTitle
|
||||
} from "@app/components/InfoSection";
|
||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||
|
||||
const formSchema = z.object({
|
||||
baseDomain: z.string().min(1, "Domain is required"),
|
||||
type: z.enum(["ns", "cname"])
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
type CreateDomainFormProps = {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
onCreated?: (domain: CreateDomainResponse) => void;
|
||||
};
|
||||
|
||||
export default function CreateDomainForm({
|
||||
open,
|
||||
setOpen,
|
||||
onCreated
|
||||
}: CreateDomainFormProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [createdDomain, setCreatedDomain] =
|
||||
useState<CreateDomainResponse | null>(null);
|
||||
const api = createApiClient(useEnvContext());
|
||||
const t = useTranslations();
|
||||
const { toast } = useToast();
|
||||
const { org } = useOrgContext();
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
baseDomain: "",
|
||||
type: "ns"
|
||||
}
|
||||
});
|
||||
|
||||
function reset() {
|
||||
form.reset();
|
||||
setLoading(false);
|
||||
setCreatedDomain(null);
|
||||
}
|
||||
|
||||
async function onSubmit(values: FormValues) {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await api.put<AxiosResponse<CreateDomainResponse>>(
|
||||
`/org/${org.org.orgId}/domain`,
|
||||
values
|
||||
);
|
||||
const domainData = response.data.data;
|
||||
setCreatedDomain(domainData);
|
||||
toast({
|
||||
title: t("success"),
|
||||
description: t("domainCreatedDescription")
|
||||
});
|
||||
onCreated?.(domainData);
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: t("error"),
|
||||
description: formatAxiosError(e),
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const domainType = form.watch("type");
|
||||
const baseDomain = form.watch("baseDomain");
|
||||
|
||||
return (
|
||||
<Credenza
|
||||
open={open}
|
||||
onOpenChange={(val) => {
|
||||
setOpen(val);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<CredenzaContent>
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>{t("domainAdd")}</CredenzaTitle>
|
||||
<CredenzaDescription>
|
||||
{t("domainAddDescription")}
|
||||
</CredenzaDescription>
|
||||
</CredenzaHeader>
|
||||
<CredenzaBody>
|
||||
{!createdDomain ? (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-4"
|
||||
id="create-domain-form"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="type"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<StrategySelect
|
||||
options={[
|
||||
{
|
||||
id: "ns",
|
||||
title: t(
|
||||
"selectDomainTypeNsName"
|
||||
),
|
||||
description: t(
|
||||
"selectDomainTypeNsDescription"
|
||||
)
|
||||
},
|
||||
{
|
||||
id: "cname",
|
||||
title: t(
|
||||
"selectDomainTypeCnameName"
|
||||
),
|
||||
description: t(
|
||||
"selectDomainTypeCnameDescription"
|
||||
)
|
||||
}
|
||||
]}
|
||||
defaultValue={field.value}
|
||||
onChange={field.onChange}
|
||||
cols={1}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="baseDomain"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("domain")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="example.com"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
<Alert variant="default">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">
|
||||
Add DNS Records
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
Add the following DNS records to your domain
|
||||
provider to complete the setup.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="space-y-4">
|
||||
{domainType === "ns" &&
|
||||
createdDomain.nsRecords && (
|
||||
<div>
|
||||
<h3 className="font-medium mb-3">
|
||||
NS Records
|
||||
</h3>
|
||||
<InfoSections cols={1}>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
Record
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">
|
||||
Type:
|
||||
</span>
|
||||
<span className="text-sm font-mono">
|
||||
NS
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">
|
||||
Name:
|
||||
</span>
|
||||
<span className="text-sm font-mono">
|
||||
{baseDomain}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium">
|
||||
Value:
|
||||
</span>
|
||||
{createdDomain.nsRecords.map(
|
||||
(
|
||||
nsRecord,
|
||||
index
|
||||
) => (
|
||||
<div
|
||||
className="flex justify-between items-center"
|
||||
key={
|
||||
index
|
||||
}
|
||||
>
|
||||
<CopyToClipboard
|
||||
text={
|
||||
nsRecord
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
</InfoSections>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{domainType === "cname" && (
|
||||
<>
|
||||
{createdDomain.cnameRecords &&
|
||||
createdDomain.cnameRecords.length >
|
||||
0 && (
|
||||
<div>
|
||||
<h3 className="font-medium mb-3">
|
||||
CNAME Records
|
||||
</h3>
|
||||
<InfoSections cols={1}>
|
||||
{createdDomain.cnameRecords.map(
|
||||
(
|
||||
cnameRecord,
|
||||
index
|
||||
) => (
|
||||
<InfoSection
|
||||
key={index}
|
||||
>
|
||||
<InfoSectionTitle>
|
||||
Record{" "}
|
||||
{index +
|
||||
1}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">
|
||||
Type:
|
||||
</span>
|
||||
<span className="text-sm font-mono">
|
||||
CNAME
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">
|
||||
Name:
|
||||
</span>
|
||||
<span className="text-sm font-mono">
|
||||
{
|
||||
cnameRecord.baseDomain
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">
|
||||
Value:
|
||||
</span>
|
||||
<CopyToClipboard
|
||||
text={
|
||||
cnameRecord.value
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)
|
||||
)}
|
||||
</InfoSections>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{createdDomain.txtRecords &&
|
||||
createdDomain.txtRecords.length >
|
||||
0 && (
|
||||
<div>
|
||||
<h3 className="font-medium mb-3">
|
||||
TXT Records
|
||||
</h3>
|
||||
<InfoSections cols={1}>
|
||||
{createdDomain.txtRecords.map(
|
||||
(
|
||||
txtRecord,
|
||||
index
|
||||
) => (
|
||||
<InfoSection
|
||||
key={index}
|
||||
>
|
||||
<InfoSectionTitle>
|
||||
Record{" "}
|
||||
{index +
|
||||
1}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">
|
||||
Type:
|
||||
</span>
|
||||
<span className="text-sm font-mono">
|
||||
TXT
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">
|
||||
Name:
|
||||
</span>
|
||||
<span className="text-sm font-mono">
|
||||
{
|
||||
txtRecord.baseDomain
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">
|
||||
Value:
|
||||
</span>
|
||||
<CopyToClipboard
|
||||
text={
|
||||
txtRecord.value
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)
|
||||
)}
|
||||
</InfoSections>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">
|
||||
Save These Records
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
Make sure to save these DNS records as you
|
||||
will not see them again.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Alert variant="info">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">
|
||||
DNS Propagation
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
DNS changes may take some time to propagate
|
||||
across the internet. This can take anywhere
|
||||
from a few minutes to 48 hours, depending on
|
||||
your DNS provider and TTL settings.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<CredenzaClose asChild>
|
||||
<Button variant="outline">{t("close")}</Button>
|
||||
</CredenzaClose>
|
||||
{!createdDomain && (
|
||||
<Button
|
||||
type="submit"
|
||||
form="create-domain-form"
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
>
|
||||
{t("domainCreate")}
|
||||
</Button>
|
||||
)}
|
||||
</CredenzaFooter>
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
);
|
||||
}
|
||||
37
src/app/[orgId]/settings/domains/DomainsDataTable.tsx
Normal file
37
src/app/[orgId]/settings/domains/DomainsDataTable.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { DataTable } from "@app/components/ui/data-table";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
onAdd?: () => void;
|
||||
onRefresh?: () => void;
|
||||
isRefreshing?: boolean;
|
||||
}
|
||||
|
||||
export function DomainsDataTable<TData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
onAdd,
|
||||
onRefresh,
|
||||
isRefreshing
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
title={t("domains")}
|
||||
searchPlaceholder={t("domainsSearch")}
|
||||
searchColumn="baseDomain"
|
||||
addButtonText={t("domainAdd")}
|
||||
onAdd={onAdd}
|
||||
onRefresh={onRefresh}
|
||||
isRefreshing={isRefreshing}
|
||||
/>
|
||||
);
|
||||
}
|
||||
261
src/app/[orgId]/settings/domains/DomainsTable.tsx
Normal file
261
src/app/[orgId]/settings/domains/DomainsTable.tsx
Normal file
@@ -0,0 +1,261 @@
|
||||
"use client";
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { DomainsDataTable } from "./DomainsDataTable";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { ArrowUpDown } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { Badge } from "@app/components/ui/badge";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import CreateDomainForm from "./CreateDomainForm";
|
||||
import { useToast } from "@app/hooks/useToast";
|
||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||
|
||||
export type DomainRow = {
|
||||
domainId: string;
|
||||
baseDomain: string;
|
||||
type: string;
|
||||
verified: boolean;
|
||||
failed: boolean;
|
||||
tries: number;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
domains: DomainRow[];
|
||||
};
|
||||
|
||||
export default function DomainsTable({ domains }: Props) {
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
const [selectedDomain, setSelectedDomain] = useState<DomainRow | null>(
|
||||
null
|
||||
);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [restartingDomains, setRestartingDomains] = useState<Set<string>>(new Set());
|
||||
const api = createApiClient(useEnvContext());
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
const { toast } = useToast();
|
||||
const { org } = useOrgContext();
|
||||
|
||||
const refreshData = async () => {
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: t("error"),
|
||||
description: t("refreshError"),
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteDomain = async (domainId: string) => {
|
||||
try {
|
||||
await api.delete(`/org/${org.org.orgId}/domain/${domainId}`);
|
||||
toast({
|
||||
title: t("success"),
|
||||
description: t("domainDeletedDescription")
|
||||
});
|
||||
setIsDeleteModalOpen(false);
|
||||
refreshData();
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: t("error"),
|
||||
description: formatAxiosError(e),
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const restartDomain = async (domainId: string) => {
|
||||
setRestartingDomains(prev => new Set(prev).add(domainId));
|
||||
try {
|
||||
await api.post(`/org/${org.org.orgId}/domain/${domainId}/restart`);
|
||||
toast({
|
||||
title: t("success"),
|
||||
description: t("domainRestartedDescription", { fallback: "Domain verification restarted successfully" })
|
||||
});
|
||||
refreshData();
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: t("error"),
|
||||
description: formatAxiosError(e),
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
setRestartingDomains(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(domainId);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeDisplay = (type: string) => {
|
||||
switch (type) {
|
||||
case "ns":
|
||||
return t("selectDomainTypeNsName");
|
||||
case "cname":
|
||||
return t("selectDomainTypeCnameName");
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ColumnDef<DomainRow>[] = [
|
||||
{
|
||||
accessorKey: "baseDomain",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t("domain")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "type",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t("type")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const type = row.original.type;
|
||||
return (
|
||||
<Badge variant="secondary">{getTypeDisplay(type)}</Badge>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "verified",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t("status")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const { verified, failed } = row.original;
|
||||
if (verified) {
|
||||
return <Badge variant="green">{t("verified")}</Badge>;
|
||||
} else if (failed) {
|
||||
return <Badge variant="destructive">{t("failed", { fallback: "Failed" })}</Badge>;
|
||||
} else {
|
||||
return <Badge variant="yellow">{t("pending")}</Badge>;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
const domain = row.original;
|
||||
const isRestarting = restartingDomains.has(domain.domainId);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
{domain.failed && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => restartDomain(domain.domainId)}
|
||||
disabled={isRestarting}
|
||||
>
|
||||
{isRestarting ? t("restarting", { fallback: "Restarting..." }) : t("restart", { fallback: "Restart" })}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSelectedDomain(domain);
|
||||
setIsDeleteModalOpen(true);
|
||||
}}
|
||||
>
|
||||
{t("delete")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{selectedDomain && (
|
||||
<ConfirmDeleteDialog
|
||||
open={isDeleteModalOpen}
|
||||
setOpen={(val) => {
|
||||
setIsDeleteModalOpen(val);
|
||||
setSelectedDomain(null);
|
||||
}}
|
||||
dialog={
|
||||
<div className="space-y-4">
|
||||
<p>
|
||||
{t("domainQuestionRemove", {
|
||||
domain: selectedDomain.baseDomain
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
<b>{t("domainMessageRemove")}</b>
|
||||
</p>
|
||||
<p>{t("domainMessageConfirm")}</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={t("domainConfirmDelete")}
|
||||
onConfirm={async () =>
|
||||
deleteDomain(selectedDomain.domainId)
|
||||
}
|
||||
string={selectedDomain.baseDomain}
|
||||
title={t("domainDelete")}
|
||||
/>
|
||||
)}
|
||||
|
||||
<CreateDomainForm
|
||||
open={isCreateModalOpen}
|
||||
setOpen={setIsCreateModalOpen}
|
||||
onCreated={(domain) => {
|
||||
refreshData();
|
||||
}}
|
||||
/>
|
||||
|
||||
<DomainsDataTable
|
||||
columns={columns}
|
||||
data={domains}
|
||||
onAdd={() => setIsCreateModalOpen(true)}
|
||||
onRefresh={refreshData}
|
||||
isRefreshing={isRefreshing}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
60
src/app/[orgId]/settings/domains/page.tsx
Normal file
60
src/app/[orgId]/settings/domains/page.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { internal } from "@app/lib/api";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { AxiosResponse } from "axios";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import DomainsTable, { DomainRow } from "./DomainsTable";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { cache } from "react";
|
||||
import { GetOrgResponse } from "@server/routers/org";
|
||||
import { redirect } from "next/navigation";
|
||||
import OrgProvider from "@app/providers/OrgProvider";
|
||||
import { ListDomainsResponse } from "@server/routers/domain";
|
||||
|
||||
type Props = {
|
||||
params: Promise<{ orgId: string }>;
|
||||
};
|
||||
|
||||
export default async function DomainsPage(props: Props) {
|
||||
const params = await props.params;
|
||||
|
||||
let domains: DomainRow[] = [];
|
||||
try {
|
||||
const res = await internal.get<
|
||||
AxiosResponse<ListDomainsResponse>
|
||||
>(`/org/${params.orgId}/domains`, await authCookieHeader());
|
||||
domains = res.data.data.domains as DomainRow[];
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
let org = null;
|
||||
try {
|
||||
const getOrg = cache(async () =>
|
||||
internal.get<AxiosResponse<GetOrgResponse>>(
|
||||
`/org/${params.orgId}`,
|
||||
await authCookieHeader()
|
||||
)
|
||||
);
|
||||
const res = await getOrg();
|
||||
org = res.data.data;
|
||||
} catch {
|
||||
redirect(`/${params.orgId}`);
|
||||
}
|
||||
|
||||
if (!org) {
|
||||
}
|
||||
|
||||
const t = await getTranslations();
|
||||
|
||||
return (
|
||||
<>
|
||||
<OrgProvider org={org}>
|
||||
<SettingsSectionTitle
|
||||
title={t("domains")}
|
||||
description={t("domainsDescription")}
|
||||
/>
|
||||
<DomainsTable domains={domains} />
|
||||
</OrgProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user