Merge branch 'no-gerbil' into dev

This commit is contained in:
Owen Schwartz
2025-01-12 15:58:29 -05:00
9 changed files with 369 additions and 211 deletions

View File

@@ -49,7 +49,7 @@ const createSiteFormSchema = z.object({
.max(30, {
message: "Name must not be longer than 30 characters."
}),
method: z.enum(["wireguard", "newt"])
method: z.enum(["wireguard", "newt", "local"])
});
type CreateSiteFormValues = z.infer<typeof createSiteFormSchema>;
@@ -79,17 +79,16 @@ export default function CreateSiteForm({
const [isLoading, setIsLoading] = useState(false);
const [isChecked, setIsChecked] = useState(false);
const router = useRouter();
const [keypair, setKeypair] = useState<{
publicKey: string;
privateKey: string;
} | null>(null);
const [siteDefaults, setSiteDefaults] =
useState<PickSiteDefaultsResponse | null>(null);
const handleCheckboxChange = (checked: boolean) => {
setChecked?.(checked);
// setChecked?.(checked);
setIsChecked(checked);
};
@@ -98,6 +97,17 @@ export default function CreateSiteForm({
defaultValues
});
const nameField = form.watch("name");
const methodField = form.watch("method");
useEffect(() => {
const nameIsValid = nameField?.length >= 2 && nameField?.length <= 30;
const isFormValid = methodField === "local" || isChecked;
// Only set checked to true if name is valid AND (method is local OR checkbox is checked)
setChecked?.(nameIsValid && isFormValid);
}, [nameField, methodField, isChecked, setChecked]);
useEffect(() => {
if (!open) return;
@@ -114,11 +124,8 @@ export default function CreateSiteForm({
api.get(`/org/${orgId}/pick-site-defaults`)
.catch((e) => {
toast({
variant: "destructive",
title: "Error picking site defaults",
description: formatAxiosError(e)
});
// update the default value of the form to be local method
form.setValue("method", "local");
})
.then((res) => {
if (res && res.status === 200) {
@@ -130,24 +137,54 @@ export default function CreateSiteForm({
async function onSubmit(data: CreateSiteFormValues) {
setLoading?.(true);
setIsLoading(true);
if (!siteDefaults || !keypair) {
return;
}
let payload: CreateSiteBody = {
name: data.name,
subnet: siteDefaults.subnet,
exitNodeId: siteDefaults.exitNodeId,
pubKey: keypair.publicKey,
type: data.method
};
if (data.method === "newt") {
payload.secret = siteDefaults.newtSecret;
payload.newtId = siteDefaults.newtId;
if (data.method == "wireguard") {
if (!keypair || !siteDefaults) {
toast({
variant: "destructive",
title: "Error creating site",
description: "Key pair or site defaults not found"
});
setLoading?.(false);
setIsLoading(false);
return;
}
payload = {
...payload,
subnet: siteDefaults.subnet,
exitNodeId: siteDefaults.exitNodeId,
pubKey: keypair.publicKey
};
}
if (data.method === "newt") {
if (!siteDefaults) {
toast({
variant: "destructive",
title: "Error creating site",
description: "Site defaults not found"
});
setLoading?.(false);
setIsLoading(false);
return;
}
payload = {
...payload,
secret: siteDefaults.newtSecret,
newtId: siteDefaults.newtId
};
}
const res = await api
.put<
AxiosResponse<CreateSiteResponse>
>(`/org/${orgId}/site/`, payload)
.put<AxiosResponse<CreateSiteResponse>>(
`/org/${orgId}/site/`,
payload
)
.catch((e) => {
toast({
variant: "destructive",
@@ -157,18 +194,20 @@ export default function CreateSiteForm({
});
if (res && res.status === 201) {
const niceId = res.data.data.niceId;
// navigate to the site page
// router.push(`/${orgId}/settings/sites/${niceId}`);
const data = res.data.data;
onCreate?.({
name: data.name,
id: data.siteId,
nice: data.niceId.toString(),
mbIn: "0 MB",
mbOut: "0 MB",
mbIn:
data.type == "wireguard" || data.type == "newt"
? "0 MB"
: "--",
mbOut:
data.type == "wireguard" || data.type == "newt"
? "0 MB"
: "--",
orgId: orgId as string,
type: data.type as any,
online: false
@@ -245,12 +284,21 @@ PersistentKeepalive = 5`
<SelectValue placeholder="Select method" />
</SelectTrigger>
<SelectContent>
<SelectItem value="wireguard">
WireGuard
<SelectItem value="local">
Local
</SelectItem>
<SelectItem value="newt">
<SelectItem
value="newt"
disabled={!siteDefaults}
>
Newt
</SelectItem>
<SelectItem
value="wireguard"
disabled={!siteDefaults}
>
WireGuard
</SelectItem>
</SelectContent>
</Select>
</FormControl>
@@ -264,50 +312,76 @@ PersistentKeepalive = 5`
<div className="w-full">
{form.watch("method") === "wireguard" && !isLoading ? (
<CopyTextBox text={wgConfig} />
<>
<CopyTextBox text={wgConfig} />
<span className="text-sm text-muted-foreground">
You will only be able to see the
configuration once.
</span>
</>
) : form.watch("method") === "wireguard" &&
isLoading ? (
<p>Loading WireGuard configuration...</p>
) : (
<CopyTextBox text={newtConfig} wrapText={false} />
)}
) : form.watch("method") === "newt" ? (
<>
<CopyTextBox
text={newtConfig}
wrapText={false}
/>
<span className="text-sm text-muted-foreground">
You will only be able to see the
configuration once.
</span>
</>
) : null}
</div>
<span className="text-sm text-muted-foreground">
You will only be able to see the configuration once.
</span>
{form.watch("method") === "newt" && (
<>
<br />
<Link
className="text-sm text-primary flex items-center gap-1"
href="https://docs.fossorial.io/Newt/install"
target="_blank"
rel="noopener noreferrer"
>
<span>
{" "}
Learn how to install Newt on your system
</span>
<SquareArrowOutUpRight size={14} />
</Link>
</>
<Link
className="text-sm text-primary flex items-center gap-1"
href="https://docs.fossorial.io/Newt/install"
target="_blank"
rel="noopener noreferrer"
>
<span>
{" "}
Learn how to install Newt on your system
</span>
<SquareArrowOutUpRight size={14} />
</Link>
)}
<div className="flex items-center space-x-2">
<Checkbox
id="terms"
checked={isChecked}
onCheckedChange={handleCheckboxChange}
/>
<label
htmlFor="terms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
{form.watch("method") === "local" && (
<Link
className="text-sm text-primary flex items-center gap-1"
href="https://docs.fossorial.io/Pangolin/without-tunneling"
target="_blank"
rel="noopener noreferrer"
>
I have copied the config
</label>
</div>
<span>
{" "}
Local sites do not tunnel, learn more
</span>
<SquareArrowOutUpRight size={14} />
</Link>
)}
{(form.watch("method") === "newt" ||
form.watch("method") === "wireguard") && (
<div className="flex items-center space-x-2">
<Checkbox
id="terms"
checked={isChecked}
onCheckedChange={handleCheckboxChange}
/>
<label
htmlFor="terms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
I have copied the config
</label>
</div>
)}
</form>
</Form>
</div>

View File

@@ -23,7 +23,7 @@ import { useState } from "react";
import CreateSiteForm from "./CreateSiteForm";
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { useToast } from "@app/hooks/useToast";
import { formatAxiosError } from "@app/lib/api";;
import { formatAxiosError } from "@app/lib/api";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import CreateSiteFormModal from "./CreateSiteModal";
@@ -146,21 +146,27 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
},
cell: ({ row }) => {
const originalRow = row.original;
if (originalRow.online) {
return (
<span className="text-green-500 flex items-center space-x-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>Online</span>
</span>
);
if (
originalRow.type == "newt" ||
originalRow.type == "wireguard"
) {
if (originalRow.online) {
return (
<span className="text-green-500 flex items-center space-x-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>Online</span>
</span>
);
} else {
return (
<span className="text-neutral-500 flex items-center space-x-2">
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
<span>Offline</span>
</span>
);
}
} else {
return (
<span className="text-neutral-500 flex items-center space-x-2">
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
<span>Offline</span>
</span>
);
return <span>--</span>;
}
}
},
@@ -245,6 +251,14 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
</div>
);
}
if (originalRow.type === "local") {
return (
<div className="flex items-center space-x-2">
<span>Local</span>
</div>
);
}
}
},
{

View File

@@ -16,37 +16,50 @@ type SiteInfoCardProps = {};
export default function SiteInfoCard({}: SiteInfoCardProps) {
const { site, updateSite } = useSiteContext();
const getConnectionTypeString = (type: string) => {
if (type === "newt") {
return "Newt";
} else if (type === "wireguard") {
return "WireGuard";
} else if (type === "local") {
return "Local";
} else {
return "Unknown";
}
};
return (
<Alert>
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">Site Information</AlertTitle>
<AlertDescription className="mt-4">
<InfoSections>
<InfoSection>
<InfoSectionTitle>Status</InfoSectionTitle>
<InfoSectionContent>
{site.online ? (
<div className="text-green-500 flex items-center space-x-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>Online</span>
</div>
) : (
<div className="text-neutral-500 flex items-center space-x-2">
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
<span>Offline</span>
</div>
)}
</InfoSectionContent>
</InfoSection>
<Separator orientation="vertical" />
{(site.type == "newt" || site.type == "wireguard") && (
<>
<InfoSection>
<InfoSectionTitle>Status</InfoSectionTitle>
<InfoSectionContent>
{site.online ? (
<div className="text-green-500 flex items-center space-x-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>Online</span>
</div>
) : (
<div className="text-neutral-500 flex items-center space-x-2">
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
<span>Offline</span>
</div>
)}
</InfoSectionContent>
</InfoSection>
<Separator orientation="vertical" />
</>
)}
<InfoSection>
<InfoSectionTitle>Connection Type</InfoSectionTitle>
<InfoSectionContent>
{site.type === "newt"
? "Newt"
: site.type === "wireguard"
? "WireGuard"
: "Unknown"}
{getConnectionTypeString(site.type)}
</InfoSectionContent>
</InfoSection>
</InfoSections>

View File

@@ -23,7 +23,10 @@ export default async function SitesPage(props: SitesPageProps) {
sites = res.data.data.sites;
} catch (e) {}
function formatSize(mb: number): string {
function formatSize(mb: number, type: string): string {
if (type === "local") {
return "--"; // because we are not able to track the data use in a local site right now
}
if (mb >= 1024 * 1024) {
return `${(mb / (1024 * 1024)).toFixed(2)} TB`;
} else if (mb >= 1024) {
@@ -38,8 +41,8 @@ export default async function SitesPage(props: SitesPageProps) {
name: site.name,
id: site.siteId,
nice: site.niceId.toString(),
mbIn: formatSize(site.megabytesIn || 0),
mbOut: formatSize(site.megabytesOut || 0),
mbIn: formatSize(site.megabytesIn || 0, site.type),
mbOut: formatSize(site.megabytesOut || 0, site.type),
orgId: params.orgId,
type: site.type as any,
online: site.online