Merge remote-tracking branch 'upstream/dev' into feature-i18n

This commit is contained in:
Marvin
2025-06-05 04:41:28 +00:00
228 changed files with 1963 additions and 847 deletions

View File

@@ -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";

View File

@@ -44,7 +44,7 @@ import {
CreateOrgApiKeyBody,
CreateOrgApiKeyResponse
} from "@server/routers/apiKeys";
import { ApiKey } from "@server/db/schemas";
import { ApiKey } from "@server/db";
import {
InfoSection,
InfoSectionContent,

View File

@@ -21,7 +21,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
const { resource, authInfo, site } = useResourceContext();
const api = createApiClient(useEnvContext());
const { isEnabled, isAvailable } = useDockerSocket(resource.siteId);
const { isEnabled, isAvailable } = useDockerSocket(site!);
const t = useTranslations();
let fullUrl = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`;
@@ -72,7 +72,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
{resource.siteName}
</InfoSectionContent>
</InfoSection>
{isEnabled && (
{/* {isEnabled && (
<InfoSection>
<InfoSectionTitle>Socket</InfoSectionTitle>
<InfoSectionContent>
@@ -89,7 +89,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
)}
</InfoSectionContent>
</InfoSection>
)}
)} */}
</>
) : (
<>

View File

@@ -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";
import { useTranslations } from "next-intl";

View File

@@ -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,

View File

@@ -771,7 +771,7 @@ export default function ReverseProxyTargets(props: {
<Input id="ip" {...field} />
</FormControl>
<FormMessage />
{site && (
{site && site.type == 'newt' && (
<ContainersSelector
site={site}
onContainerSelect={(

View File

@@ -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,

View File

@@ -130,31 +130,35 @@ export default function GeneralPage() {
</FormItem>
)}
/>
<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, useful in resource
targets.
</FormDescription>
</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,
useful in resource targets.
</FormDescription>
</FormItem>
)}
/>
)}
</form>
</Form>
</SettingsSectionForm>

View File

@@ -1,7 +1,7 @@
import { cookies } from "next/headers";
import ValidateOidcToken from "./ValidateOidcToken";
import { idp } from "@server/db/schemas";
import db from "@server/db";
import { idp } from "@server/db";
import { db } from "@server/db";
import { eq } from "drizzle-orm";
import { getTranslations } from "next-intl/server";

View File

@@ -6,8 +6,8 @@ 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 { db } from "@server/db";
import { idp } from "@server/db";
import { LoginFormIDP } from "@app/components/LoginForm";
import { getTranslations } from "next-intl/server";

View File

@@ -14,8 +14,8 @@ 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 { db } from "@server/db";
import { idp } from "@server/db";
export default async function ResourceAuthPage(props: {
params: Promise<{ resourceId: number }>;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -68,8 +68,9 @@ export const ContainersSelector: FC<ContainerSelectorProps> = ({
}) => {
const [open, setOpen] = useState(false);
const isDesktop = useMediaQuery("(min-width: 768px)");
const { isAvailable, containers, fetchContainers } = useDockerSocket(
site.siteId
site
);
useEffect(() => {

View File

@@ -172,6 +172,8 @@ export function Layout({
alt="Pangolin Logo"
width={110}
height={25}
priority={true}
quality={25}
/>
)}
</Link>

View File

@@ -4,21 +4,18 @@ import { useEnvContext } from "./useEnvContext";
import {
Container,
GetDockerStatusResponse,
GetSiteResponse,
ListContainersResponse,
TriggerFetchResponse
} from "@server/routers/site";
import { AxiosResponse } from "axios";
import { toast } from "./useToast";
import { Site } from "@server/db";
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
export function useDockerSocket(siteId: number) {
if (!siteId) {
throw new Error("Site ID is required to use Docker Socket");
}
export function useDockerSocket(site: Site) {
console.log(`useDockerSocket initialized for site ID: ${site.siteId}`);
const [site, setSite] = useState<GetSiteResponse>();
const [dockerSocket, setDockerSocket] = useState<GetDockerStatusResponse>();
const [containers, setContainers] = useState<Container[]>([]);
@@ -27,40 +24,18 @@ export function useDockerSocket(siteId: number) {
const { dockerSocketEnabled: isEnabled = true } = site || {};
const { isAvailable = false, socketPath } = dockerSocket || {};
const fetchSite = useCallback(async () => {
try {
const res = await api.get<AxiosResponse<GetSiteResponse>>(
`/site/${siteId}`
);
if (res.status === 200) {
setSite(res.data.data);
}
} catch (err) {
console.error(err);
toast({
variant: "destructive",
title: "Failed to fetch resource",
description: formatAxiosError(
err,
"An error occurred while fetching resource"
)
});
}
}, [api, siteId]);
const checkDockerSocket = useCallback(async () => {
if (!isEnabled) {
console.warn("Docker socket is not enabled for this site.");
return;
}
try {
const res = await api.post(`/site/${siteId}/docker/check`);
const res = await api.post(`/site/${site.siteId}/docker/check`);
console.log("Docker socket check response:", res);
} catch (error) {
console.error("Failed to check Docker socket:", error);
}
}, [api, siteId, isEnabled]);
}, [api, site.siteId, isEnabled]);
const getDockerSocketStatus = useCallback(async () => {
if (!isEnabled) {
@@ -70,7 +45,7 @@ export function useDockerSocket(siteId: number) {
try {
const res = await api.get<AxiosResponse<GetDockerStatusResponse>>(
`/site/${siteId}/docker/status`
`/site/${site.siteId}/docker/status`
);
if (res.status === 200) {
@@ -92,7 +67,7 @@ export function useDockerSocket(siteId: number) {
description: "An error occurred while fetching Docker status."
});
}
}, [api, siteId, isEnabled]);
}, [api, site.siteId, isEnabled]);
const getContainers = useCallback(
async (maxRetries: number = 3) => {
@@ -111,18 +86,15 @@ export function useDockerSocket(siteId: number) {
try {
const res = await api.get<
AxiosResponse<ListContainersResponse>
>(`/site/${siteId}/docker/containers`);
>(`/site/${site.siteId}/docker/containers`);
setContainers(res.data.data);
return;
return res.data.data;
} catch (error: any) {
attempt++;
// Check if the error is a 425 (Too Early) status
if (error?.response?.status === 425) {
if (attempt < maxRetries) {
// Ask the newt server to check containers
await fetchContainerList();
console.log(
`Containers not ready yet (attempt ${attempt}/${maxRetries}). Retrying in 250ms...`
);
@@ -160,7 +132,7 @@ export function useDockerSocket(siteId: number) {
try {
const res = await api.post<AxiosResponse<TriggerFetchResponse>>(
`/site/${siteId}/docker/trigger`
`/site/${site.siteId}/docker/trigger`
);
// TODO: identify a way to poll the server for latest container list periodically?
await fetchContainerList();
@@ -169,13 +141,9 @@ export function useDockerSocket(siteId: number) {
console.error("Failed to trigger Docker containers:", error);
}
},
[api, siteId, isEnabled, isAvailable]
[api, site.siteId, isEnabled, isAvailable]
);
useEffect(() => {
fetchSite();
}, [fetchSite]);
// 2. Docker socket status monitoring
useEffect(() => {
if (!isEnabled || isAvailable) {