mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-27 23:36:39 +00:00
Merge branch 'dev' into clients-pops
This commit is contained in:
@@ -14,7 +14,7 @@ import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { RolesDataTable } from "./RolesDataTable";
|
||||
import { Role } from "@server/db/schemas";
|
||||
import { Role } from "@server/db";
|
||||
import CreateRoleForm from "./CreateRoleForm";
|
||||
import DeleteRoleForm from "./DeleteRoleForm";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
|
||||
@@ -44,7 +44,6 @@ import {
|
||||
CreateOrgApiKeyBody,
|
||||
CreateOrgApiKeyResponse
|
||||
} from "@server/routers/apiKeys";
|
||||
import { ApiKey } from "@server/db/schemas";
|
||||
import {
|
||||
InfoSection,
|
||||
InfoSectionContent,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { ArrowRight, InfoIcon, ShieldCheck, ShieldOff } from "lucide-react";
|
||||
import { InfoIcon, ShieldCheck, ShieldOff } from "lucide-react";
|
||||
import { useResourceContext } from "@app/hooks/useResourceContext";
|
||||
import { Separator } from "@app/components/ui/separator";
|
||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||
import {
|
||||
InfoSection,
|
||||
@@ -11,13 +10,17 @@ import {
|
||||
InfoSections,
|
||||
InfoSectionTitle
|
||||
} from "@app/components/InfoSection";
|
||||
import Link from "next/link";
|
||||
import { Switch } from "@app/components/ui/switch";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useDockerSocket } from "@app/hooks/useDockerSocket";
|
||||
|
||||
type ResourceInfoBoxType = {};
|
||||
|
||||
export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||
const { resource, authInfo } = useResourceContext();
|
||||
const { resource, authInfo, site } = useResourceContext();
|
||||
const api = createApiClient(useEnvContext());
|
||||
|
||||
const { isEnabled, isAvailable } = useDockerSocket(site!);
|
||||
|
||||
let fullUrl = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`;
|
||||
|
||||
@@ -67,6 +70,24 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||
{resource.siteName}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
{/* {isEnabled && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>Socket</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{isAvailable ? (
|
||||
<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>
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)} */}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -92,7 +113,9 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>Visibility</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<span>{resource.enabled ? "Enabled" : "Disabled"}</span>
|
||||
<span>
|
||||
{resource.enabled ? "Enabled" : "Disabled"}
|
||||
</span>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
</InfoSections>
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
} from "@app/components/Credenza";
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { Resource } from "@server/db/schemas";
|
||||
import { Resource } from "@server/db";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
} from "@app/components/Credenza";
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { Resource } from "@server/db/schemas";
|
||||
import { Resource } from "@server/db";
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
|
||||
@@ -13,15 +13,7 @@ import { GetOrgResponse } from "@server/routers/org";
|
||||
import OrgProvider from "@app/providers/OrgProvider";
|
||||
import { cache } from "react";
|
||||
import ResourceInfoBox from "./ResourceInfoBox";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator
|
||||
} from "@app/components/ui/breadcrumb";
|
||||
import Link from "next/link";
|
||||
import { GetSiteResponse } from "@server/routers/site";
|
||||
|
||||
interface ResourceLayoutProps {
|
||||
children: React.ReactNode;
|
||||
@@ -35,6 +27,7 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
|
||||
|
||||
let authInfo = null;
|
||||
let resource = null;
|
||||
let site = null;
|
||||
try {
|
||||
const res = await internal.get<AxiosResponse<GetResourceResponse>>(
|
||||
`/resource/${params.resourceId}`,
|
||||
@@ -49,6 +42,19 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
|
||||
redirect(`/${params.orgId}/settings/resources`);
|
||||
}
|
||||
|
||||
// Fetch site info
|
||||
if (resource.siteId) {
|
||||
try {
|
||||
const res = await internal.get<AxiosResponse<GetSiteResponse>>(
|
||||
`/site/${resource.siteId}`,
|
||||
await authCookieHeader()
|
||||
);
|
||||
site = res.data.data;
|
||||
} catch {
|
||||
redirect(`/${params.orgId}/settings/resources`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await internal.get<
|
||||
AxiosResponse<GetResourceAuthInfoResponse>
|
||||
@@ -110,7 +116,11 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
|
||||
/>
|
||||
|
||||
<OrgProvider org={org}>
|
||||
<ResourceProvider resource={resource} authInfo={authInfo}>
|
||||
<ResourceProvider
|
||||
site={site}
|
||||
resource={resource}
|
||||
authInfo={authInfo}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<ResourceInfoBox />
|
||||
<HorizontalTabs items={navItems}>
|
||||
|
||||
@@ -41,7 +41,6 @@ import {
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
@@ -61,7 +60,8 @@ import {
|
||||
SettingsSectionDescription,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionFooter,
|
||||
SettingsSectionForm
|
||||
SettingsSectionForm,
|
||||
SettingsSectionGrid
|
||||
} from "@app/components/Settings";
|
||||
import { SwitchInput } from "@app/components/SwitchInput";
|
||||
import { useRouter } from "next/navigation";
|
||||
@@ -73,6 +73,8 @@ import {
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger
|
||||
} from "@app/components/ui/collapsible";
|
||||
import { ContainersSelector } from "@app/components/ContainersSelector";
|
||||
import { FaDocker } from "react-icons/fa";
|
||||
|
||||
const addTargetSchema = z.object({
|
||||
ip: z.string().refine(isTargetValid),
|
||||
@@ -163,6 +165,9 @@ export default function ReverseProxyTargets(props: {
|
||||
} as z.infer<typeof addTargetSchema>
|
||||
});
|
||||
|
||||
const watchedIp = addTargetForm.watch("ip");
|
||||
const watchedPort = addTargetForm.watch("port");
|
||||
|
||||
const tlsSettingsForm = useForm<TlsSettingsValues>({
|
||||
resolver: zodResolver(tlsSettingsSchema),
|
||||
defaultValues: {
|
||||
@@ -556,115 +561,6 @@ export default function ReverseProxyTargets(props: {
|
||||
|
||||
return (
|
||||
<SettingsContainer>
|
||||
{resource.http && (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
HTTPS & TLS Settings
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Configure TLS settings for your resource
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
<Form {...tlsSettingsForm}>
|
||||
<form
|
||||
onSubmit={tlsSettingsForm.handleSubmit(
|
||||
saveTlsSettings
|
||||
)}
|
||||
className="space-y-4"
|
||||
id="tls-settings-form"
|
||||
>
|
||||
<FormField
|
||||
control={tlsSettingsForm.control}
|
||||
name="ssl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<SwitchInput
|
||||
id="ssl-toggle"
|
||||
label="Enable SSL (https)"
|
||||
defaultChecked={
|
||||
field.value
|
||||
}
|
||||
onCheckedChange={(
|
||||
val
|
||||
) => {
|
||||
field.onChange(val);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Collapsible
|
||||
open={isAdvancedOpen}
|
||||
onOpenChange={setIsAdvancedOpen}
|
||||
className="space-y-2"
|
||||
>
|
||||
<div className="flex items-center justify-between space-x-4">
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
variant="text"
|
||||
size="sm"
|
||||
className="p-0 flex items-center justify-start gap-2 w-full"
|
||||
>
|
||||
<h4 className="text-sm font-semibold">
|
||||
Advanced TLS Settings
|
||||
</h4>
|
||||
<div>
|
||||
<ChevronsUpDown className="h-4 w-4" />
|
||||
<span className="sr-only">
|
||||
Toggle
|
||||
</span>
|
||||
</div>
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
</div>
|
||||
<CollapsibleContent className="space-y-2">
|
||||
<FormField
|
||||
control={
|
||||
tlsSettingsForm.control
|
||||
}
|
||||
name="tlsServerName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
TLS Server Name
|
||||
(SNI)
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The TLS Server Name
|
||||
to use for SNI.
|
||||
Leave empty to use
|
||||
the default.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
loading={httpsTlsLoading}
|
||||
form="tls-settings-form"
|
||||
>
|
||||
Save Settings
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
)}
|
||||
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
@@ -767,11 +663,31 @@ export default function ReverseProxyTargets(props: {
|
||||
control={addTargetForm.control}
|
||||
name="ip"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormItem className="relative">
|
||||
<FormLabel>IP / Hostname</FormLabel>
|
||||
<FormControl>
|
||||
<Input id="ip" {...field} />
|
||||
</FormControl>
|
||||
{site && site.type == "newt" && (
|
||||
<ContainersSelector
|
||||
site={site}
|
||||
onContainerSelect={(
|
||||
hostname,
|
||||
port
|
||||
) => {
|
||||
addTargetForm.setValue(
|
||||
"ip",
|
||||
hostname
|
||||
);
|
||||
if (port) {
|
||||
addTargetForm.setValue(
|
||||
"port",
|
||||
port
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -798,12 +714,7 @@ export default function ReverseProxyTargets(props: {
|
||||
type="submit"
|
||||
variant="outlinePrimary"
|
||||
className="mt-6"
|
||||
disabled={
|
||||
!(
|
||||
addTargetForm.getValues("ip") &&
|
||||
addTargetForm.getValues("port")
|
||||
)
|
||||
}
|
||||
disabled={!(watchedIp && watchedPort)}
|
||||
>
|
||||
Add Target
|
||||
</Button>
|
||||
@@ -873,59 +784,175 @@ export default function ReverseProxyTargets(props: {
|
||||
</SettingsSection>
|
||||
|
||||
{resource.http && (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
Additional Proxy Settings
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Configure how your resource handles proxy settings
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
<Form {...proxySettingsForm}>
|
||||
<form
|
||||
onSubmit={proxySettingsForm.handleSubmit(
|
||||
saveProxySettings
|
||||
)}
|
||||
className="space-y-4"
|
||||
id="proxy-settings-form"
|
||||
>
|
||||
<FormField
|
||||
control={proxySettingsForm.control}
|
||||
name="setHostHeader"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Custom Host Header
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The host header to set when
|
||||
proxying requests. Leave
|
||||
empty to use the default.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
<SettingsSectionGrid cols={2}>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
Secure Connection Configuration
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Configure SSL/TLS settings for your resource
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
<Form {...tlsSettingsForm}>
|
||||
<form
|
||||
onSubmit={tlsSettingsForm.handleSubmit(
|
||||
saveTlsSettings
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
loading={proxySettingsLoading}
|
||||
form="proxy-settings-form"
|
||||
>
|
||||
Save Proxy Settings
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
className="space-y-4"
|
||||
id="tls-settings-form"
|
||||
>
|
||||
<FormField
|
||||
control={tlsSettingsForm.control}
|
||||
name="ssl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<SwitchInput
|
||||
id="ssl-toggle"
|
||||
label="Enable SSL (https)"
|
||||
defaultChecked={
|
||||
field.value
|
||||
}
|
||||
onCheckedChange={(
|
||||
val
|
||||
) => {
|
||||
field.onChange(
|
||||
val
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Collapsible
|
||||
open={isAdvancedOpen}
|
||||
onOpenChange={setIsAdvancedOpen}
|
||||
className="space-y-2"
|
||||
>
|
||||
<div className="flex items-center justify-between space-x-4">
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
variant="text"
|
||||
size="sm"
|
||||
className="p-0 flex items-center justify-start gap-2 w-full"
|
||||
>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Advanced TLS
|
||||
Settings
|
||||
</p>
|
||||
<div>
|
||||
<ChevronsUpDown className="h-4 w-4" />
|
||||
<span className="sr-only">
|
||||
Toggle
|
||||
</span>
|
||||
</div>
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
</div>
|
||||
<CollapsibleContent className="space-y-2">
|
||||
<FormField
|
||||
control={
|
||||
tlsSettingsForm.control
|
||||
}
|
||||
name="tlsServerName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
TLS Server Name
|
||||
(SNI)
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The TLS Server
|
||||
Name to use for
|
||||
SNI. Leave empty
|
||||
to use the
|
||||
default.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
loading={httpsTlsLoading}
|
||||
form="tls-settings-form"
|
||||
>
|
||||
Save Settings
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
Additional Proxy Settings
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
Configure how your resource handles proxy
|
||||
settings
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
<Form {...proxySettingsForm}>
|
||||
<form
|
||||
onSubmit={proxySettingsForm.handleSubmit(
|
||||
saveProxySettings
|
||||
)}
|
||||
className="space-y-4"
|
||||
id="proxy-settings-form"
|
||||
>
|
||||
<FormField
|
||||
control={proxySettingsForm.control}
|
||||
name="setHostHeader"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Custom Host Header
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The host header to set
|
||||
when proxying requests.
|
||||
Leave empty to use the
|
||||
default.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
loading={proxySettingsLoading}
|
||||
form="proxy-settings-form"
|
||||
>
|
||||
Save Settings
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
</SettingsSectionGrid>
|
||||
)}
|
||||
</SettingsContainer>
|
||||
);
|
||||
|
||||
@@ -32,7 +32,7 @@ import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { Resource } from "@server/db/schemas";
|
||||
import { Resource } from "@server/db";
|
||||
import { StrategySelect } from "@app/components/StrategySelect";
|
||||
import {
|
||||
Select,
|
||||
|
||||
@@ -269,7 +269,7 @@ PersistentKeepalive = 5`
|
||||
- NEWT_ID=${siteDefaults?.newtId}
|
||||
- NEWT_SECRET=${siteDefaults?.newtSecret}`;
|
||||
|
||||
const newtConfigDockerRun = `docker run -it fosrl/newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${env.app.dashboardUrl}`;
|
||||
const newtConfigDockerRun = `docker run -dit fosrl/newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${env.app.dashboardUrl}`;
|
||||
|
||||
return loadingPage ? (
|
||||
<LoaderPlaceholder height="300px" />
|
||||
|
||||
@@ -27,6 +27,7 @@ import { formatAxiosError } from "@app/lib/api";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import CreateSiteFormModal from "./CreateSiteModal";
|
||||
import { parseDataSize } from '@app/lib/dataSize';
|
||||
|
||||
export type SiteRow = {
|
||||
id: number;
|
||||
@@ -198,7 +199,9 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
},
|
||||
sortingFn: (rowA, rowB) =>
|
||||
parseDataSize(rowA.original.mbIn) - parseDataSize(rowB.original.mbIn)
|
||||
},
|
||||
{
|
||||
accessorKey: "mbOut",
|
||||
@@ -214,7 +217,9 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
},
|
||||
sortingFn: (rowA, rowB) =>
|
||||
parseDataSize(rowA.original.mbOut) - parseDataSize(rowB.original.mbOut),
|
||||
},
|
||||
{
|
||||
accessorKey: "type",
|
||||
|
||||
@@ -31,9 +31,13 @@ import { formatAxiosError } from "@app/lib/api";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useState } from "react";
|
||||
import { SwitchInput } from "@app/components/SwitchInput";
|
||||
import Link from "next/link";
|
||||
import { ArrowRight, ExternalLink } from "lucide-react";
|
||||
|
||||
const GeneralFormSchema = z.object({
|
||||
name: z.string().nonempty("Name is required")
|
||||
name: z.string().nonempty("Name is required"),
|
||||
dockerSocketEnabled: z.boolean().optional()
|
||||
});
|
||||
|
||||
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
||||
@@ -50,7 +54,8 @@ export default function GeneralPage() {
|
||||
const form = useForm<GeneralFormValues>({
|
||||
resolver: zodResolver(GeneralFormSchema),
|
||||
defaultValues: {
|
||||
name: site?.name
|
||||
name: site?.name,
|
||||
dockerSocketEnabled: site?.dockerSocketEnabled ?? false
|
||||
},
|
||||
mode: "onChange"
|
||||
});
|
||||
@@ -60,7 +65,8 @@ export default function GeneralPage() {
|
||||
|
||||
await api
|
||||
.post(`/site/${site?.siteId}`, {
|
||||
name: data.name
|
||||
name: data.name,
|
||||
dockerSocketEnabled: data.dockerSocketEnabled
|
||||
})
|
||||
.catch((e) => {
|
||||
toast({
|
||||
@@ -73,7 +79,10 @@ export default function GeneralPage() {
|
||||
});
|
||||
});
|
||||
|
||||
updateSite({ name: data.name });
|
||||
updateSite({
|
||||
name: data.name,
|
||||
dockerSocketEnabled: data.dockerSocketEnabled
|
||||
});
|
||||
|
||||
toast({
|
||||
title: "Site updated",
|
||||
@@ -102,7 +111,7 @@ export default function GeneralPage() {
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-4"
|
||||
className="space-y-6"
|
||||
id="general-settings-form"
|
||||
>
|
||||
<FormField
|
||||
@@ -122,6 +131,45 @@ export default function GeneralPage() {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{site && site.type === "newt" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dockerSocketEnabled"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<SwitchInput
|
||||
id="docker-socket-enabled"
|
||||
label="Enable Docker Socket"
|
||||
defaultChecked={
|
||||
field.value
|
||||
}
|
||||
onCheckedChange={
|
||||
field.onChange
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
Enable Docker Socket
|
||||
discovery for populating
|
||||
container information.
|
||||
Socket path must be provided
|
||||
to Newt.{" "}
|
||||
<a
|
||||
href="https://docs.fossorial.io/Newt/overview#docker-socket-integration"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline inline-flex items-center"
|
||||
>
|
||||
Learn more
|
||||
<ExternalLink className="ml-1 h-4 w-4" />
|
||||
</a>
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionForm>
|
||||
|
||||
@@ -258,7 +258,7 @@ PersistentKeepalive = 5`;
|
||||
- NEWT_SECRET=${secret}`
|
||||
],
|
||||
"Docker Run": [
|
||||
`docker run -it fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}`
|
||||
`docker run -dit fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}`
|
||||
]
|
||||
},
|
||||
podman: {
|
||||
@@ -281,7 +281,7 @@ Restart=always
|
||||
WantedBy=default.target`
|
||||
],
|
||||
"Podman Run": [
|
||||
`podman run -it docker.io/fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}`
|
||||
`podman run -dit docker.io/fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}`
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -395,7 +395,7 @@ export default function PoliciesPage() {
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This expression must return
|
||||
thr org ID or true for the
|
||||
the org ID or true for the
|
||||
user to be allowed to access
|
||||
the organization.
|
||||
</FormDescription>
|
||||
|
||||
@@ -289,17 +289,17 @@ export default function LicensePage() {
|
||||
terms corresponding to the
|
||||
tier associated with your
|
||||
license key.
|
||||
<br />
|
||||
<Link
|
||||
href="https://fossorial.io/license.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
View Fossorial
|
||||
Commercial License &
|
||||
Subscription Terms
|
||||
</Link>
|
||||
{/* <br /> */}
|
||||
{/* <Link */}
|
||||
{/* href="https://fossorial.io/license.html" */}
|
||||
{/* target="_blank" */}
|
||||
{/* rel="noopener noreferrer" */}
|
||||
{/* className="text-primary hover:underline" */}
|
||||
{/* > */}
|
||||
{/* View Fossorial */}
|
||||
{/* Commercial License & */}
|
||||
{/* Subscription Terms */}
|
||||
{/* </Link> */}
|
||||
</FormLabel>
|
||||
<FormMessage />
|
||||
</div>
|
||||
@@ -503,32 +503,32 @@ export default function LicensePage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<SettingsSectionFooter>
|
||||
{!licenseStatus?.isHostLicensed ? (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setPurchaseMode("license");
|
||||
setIsPurchaseModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Purchase License
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setPurchaseMode("additional-sites");
|
||||
setIsPurchaseModalOpen(true);
|
||||
}}
|
||||
>
|
||||
Purchase Additional Sites
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</SettingsSectionFooter>
|
||||
{/* <SettingsSectionFooter> */}
|
||||
{/* {!licenseStatus?.isHostLicensed ? ( */}
|
||||
{/* <> */}
|
||||
{/* <Button */}
|
||||
{/* onClick={() => { */}
|
||||
{/* setPurchaseMode("license"); */}
|
||||
{/* setIsPurchaseModalOpen(true); */}
|
||||
{/* }} */}
|
||||
{/* > */}
|
||||
{/* Purchase License */}
|
||||
{/* </Button> */}
|
||||
{/* </> */}
|
||||
{/* ) : ( */}
|
||||
{/* <> */}
|
||||
{/* <Button */}
|
||||
{/* variant="outline" */}
|
||||
{/* onClick={() => { */}
|
||||
{/* setPurchaseMode("additional-sites"); */}
|
||||
{/* setIsPurchaseModalOpen(true); */}
|
||||
{/* }} */}
|
||||
{/* > */}
|
||||
{/* Purchase Additional Sites */}
|
||||
{/* </Button> */}
|
||||
{/* </> */}
|
||||
{/* )} */}
|
||||
{/* </SettingsSectionFooter> */}
|
||||
</SettingsSection>
|
||||
</SettingsSectionGrid>
|
||||
<LicenseKeysDataTable
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { cookies } from "next/headers";
|
||||
import ValidateOidcToken from "./ValidateOidcToken";
|
||||
import { idp } from "@server/db/schemas";
|
||||
import db from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { cache } from "react";
|
||||
import { priv } from "@app/lib/api";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { GetIdpResponse } from "@server/routers/idp";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{ orgId: string; idpId: string }>;
|
||||
@@ -17,13 +20,14 @@ export default async function Page(props: {
|
||||
const allCookies = await cookies();
|
||||
const stateCookie = allCookies.get("p_oidc_state")?.value;
|
||||
|
||||
// query db directly in server component because just need the name
|
||||
const [idpRes] = await db
|
||||
.select({ name: idp.name })
|
||||
.from(idp)
|
||||
.where(eq(idp.idpId, parseInt(params.idpId!)));
|
||||
|
||||
if (!idpRes) {
|
||||
const idpRes = await cache(
|
||||
async () => await priv.get<AxiosResponse<GetIdpResponse>>(`/idp/${params.idpId}`)
|
||||
)();
|
||||
|
||||
const foundIdp = idpRes.data?.data?.idp;
|
||||
|
||||
if (!foundIdp) {
|
||||
return <div>IdP not found</div>;
|
||||
}
|
||||
|
||||
@@ -35,7 +39,7 @@ export default async function Page(props: {
|
||||
code={searchParams.code}
|
||||
expectedState={searchParams.state}
|
||||
stateCookie={stateCookie}
|
||||
idp={{ name: idpRes.name }}
|
||||
idp={{ name: foundIdp.name }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -45,8 +45,8 @@ export default function DashboardLoginForm({
|
||||
<Image
|
||||
src={`/logo/pangolin_orange.svg`}
|
||||
alt="Pangolin Logo"
|
||||
width="100"
|
||||
height="100"
|
||||
width={100}
|
||||
height={100}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center space-y-1">
|
||||
|
||||
@@ -6,9 +6,11 @@ import DashboardLoginForm from "./DashboardLoginForm";
|
||||
import { Mail } from "lucide-react";
|
||||
import { pullEnv } from "@app/lib/pullEnv";
|
||||
import { cleanRedirect } from "@app/lib/cleanRedirect";
|
||||
import db from "@server/db";
|
||||
import { idp } from "@server/db/schemas";
|
||||
import { idp } from "@server/db";
|
||||
import { LoginFormIDP } from "@app/components/LoginForm";
|
||||
import { priv } from "@app/lib/api";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { ListIdpsResponse } from "@server/routers/idp";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -34,8 +36,10 @@ export default async function Page(props: {
|
||||
redirectUrl = cleanRedirect(searchParams.redirect as string);
|
||||
}
|
||||
|
||||
const idps = await db.select().from(idp);
|
||||
const loginIdps = idps.map((idp) => ({
|
||||
const idpsRes = await cache(
|
||||
async () => await priv.get<AxiosResponse<ListIdpsResponse>>("/idp")
|
||||
)();
|
||||
const loginIdps = idpsRes.data.data.idps.map((idp) => ({
|
||||
idpId: idp.idpId,
|
||||
name: idp.name
|
||||
})) as LoginFormIDP[];
|
||||
|
||||
@@ -14,8 +14,9 @@ import ResourceAccessDenied from "./ResourceAccessDenied";
|
||||
import AccessToken from "./AccessToken";
|
||||
import { pullEnv } from "@app/lib/pullEnv";
|
||||
import { LoginFormIDP } from "@app/components/LoginForm";
|
||||
import db from "@server/db";
|
||||
import { idp } from "@server/db/schemas";
|
||||
import { ListIdpsResponse } from "@server/routers/idp";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function ResourceAuthPage(props: {
|
||||
params: Promise<{ resourceId: number }>;
|
||||
@@ -130,8 +131,10 @@ export default async function ResourceAuthPage(props: {
|
||||
);
|
||||
}
|
||||
|
||||
const idps = await db.select().from(idp);
|
||||
const loginIdps = idps.map((idp) => ({
|
||||
const idpsRes = await cache(
|
||||
async () => await priv.get<AxiosResponse<ListIdpsResponse>>("/idp")
|
||||
)();
|
||||
const loginIdps = idpsRes.data.data.idps.map((idp) => ({
|
||||
idpId: idp.idpId,
|
||||
name: idp.name
|
||||
})) as LoginFormIDP[];
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -101,7 +101,14 @@ export default function StepperForm() {
|
||||
);
|
||||
|
||||
const generateId = (name: string) => {
|
||||
return name.toLowerCase().replace(/\s+/g, "-");
|
||||
// Replace any character that is not a letter, number, space, or hyphen with a hyphen
|
||||
// Also collapse multiple hyphens and trim
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, "-")
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
};
|
||||
|
||||
async function orgSubmit(values: z.infer<typeof orgSchema>) {
|
||||
@@ -227,23 +234,22 @@ export default function StepperForm() {
|
||||
type="text"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
const orgId =
|
||||
generateId(
|
||||
e.target
|
||||
.value
|
||||
);
|
||||
// Prevent "/" in orgName input
|
||||
const sanitizedValue = e.target.value.replace(/\//g, "-");
|
||||
const orgId = generateId(sanitizedValue);
|
||||
orgForm.setValue(
|
||||
"orgId",
|
||||
orgId
|
||||
);
|
||||
orgForm.setValue(
|
||||
"orgName",
|
||||
e.target.value
|
||||
sanitizedValue
|
||||
);
|
||||
debouncedCheckOrgIdAvailability(
|
||||
orgId
|
||||
);
|
||||
}}
|
||||
value={field.value.replace(/\//g, "-")}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
|
||||
Reference in New Issue
Block a user