mirror of
https://github.com/fosrl/pangolin.git
synced 2026-06-23 07:41:50 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19faa3a29c | ||
|
|
c284dc2e83 | ||
|
|
1b634955d8 | ||
|
|
be888c3fc1 | ||
|
|
3f2bb42221 | ||
|
|
5dc3ae4c7f | ||
|
|
ffb6c64de0 | ||
|
|
2cbc6fb128 | ||
|
|
75084028d7 | ||
|
|
f44a7c55dd | ||
|
|
72fa1d6a14 | ||
|
|
7a275c86c2 | ||
|
|
4b703b5c11 | ||
|
|
1b6e9e8cfe | ||
|
|
fe55956079 | ||
|
|
4cd0b9a0bb | ||
|
|
ab4d567af9 | ||
|
|
38203e522b | ||
|
|
13b691fd7d |
@@ -2967,6 +2967,7 @@
|
|||||||
"orgOrDomainIdMissing": "Organization or Domain ID is missing",
|
"orgOrDomainIdMissing": "Organization or Domain ID is missing",
|
||||||
"loadingDNSRecords": "Loading DNS records...",
|
"loadingDNSRecords": "Loading DNS records...",
|
||||||
"olmUpdateAvailableInfo": "An updated version of Olm is available. Please update to the latest version for the best experience.",
|
"olmUpdateAvailableInfo": "An updated version of Olm is available. Please update to the latest version for the best experience.",
|
||||||
|
"updateAvailableInfo": "An updated version is available. Please update to the latest version for the best experience.",
|
||||||
"client": "Client",
|
"client": "Client",
|
||||||
"proxyProtocol": "Proxy Protocol Settings",
|
"proxyProtocol": "Proxy Protocol Settings",
|
||||||
"proxyProtocolDescription": "Configure Proxy Protocol to preserve client IP addresses for TCP services.",
|
"proxyProtocolDescription": "Configure Proxy Protocol to preserve client IP addresses for TCP services.",
|
||||||
|
|||||||
@@ -420,31 +420,6 @@ export async function listUserDevices(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// REMOVING THIS BECAUSE WE HAVE DIFFERENT TYPES OF CLIENTS NOW
|
|
||||||
// // Try to get the latest version, but don't block if it fails
|
|
||||||
// try {
|
|
||||||
// const latestOlmVersion = await getLatestOlmVersion();
|
|
||||||
|
|
||||||
// if (latestOlmVersion) {
|
|
||||||
// olmsWithUpdates.forEach((client) => {
|
|
||||||
// try {
|
|
||||||
// client.olmUpdateAvailable = semver.lt(
|
|
||||||
// client.olmVersion ? client.olmVersion : "",
|
|
||||||
// latestOlmVersion
|
|
||||||
// );
|
|
||||||
// } catch (error) {
|
|
||||||
// client.olmUpdateAvailable = false;
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// } catch (error) {
|
|
||||||
// // Log the error but don't let it block the response
|
|
||||||
// logger.warn(
|
|
||||||
// "Failed to check for OLM updates, continuing without update info:",
|
|
||||||
// error
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
return response<ListUserDevicesResponse>(res, {
|
return response<ListUserDevicesResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
devices: olmsWithUpdates,
|
devices: olmsWithUpdates,
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
import {
|
import {
|
||||||
db,
|
db,
|
||||||
exitNodes,
|
exitNodes,
|
||||||
|
labels,
|
||||||
newts,
|
newts,
|
||||||
orgs,
|
orgs,
|
||||||
remoteExitNodes,
|
remoteExitNodes,
|
||||||
roleSites,
|
roleSites,
|
||||||
|
siteLabels,
|
||||||
siteNetworks,
|
siteNetworks,
|
||||||
siteResources,
|
siteResources,
|
||||||
targets,
|
|
||||||
sites,
|
sites,
|
||||||
|
targets,
|
||||||
userSites,
|
userSites,
|
||||||
labels,
|
|
||||||
siteLabels,
|
|
||||||
type Label
|
type Label
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { regionalCache as cache } from "#dynamic/lib/cache";
|
import { regionalCache as cache } from "#dynamic/lib/cache";
|
||||||
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
@@ -23,102 +24,9 @@ import type { PaginatedResponse } from "@server/types/Pagination";
|
|||||||
import { and, asc, desc, eq, inArray, like, or, sql } from "drizzle-orm";
|
import { and, asc, desc, eq, inArray, like, or, sql } from "drizzle-orm";
|
||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import semver from "semver";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
|
||||||
|
|
||||||
// Stale-while-revalidate: keeps the last successfully fetched version so that
|
|
||||||
// a transient network failure / timeout does not flip every site back to
|
|
||||||
// newtUpdateAvailable: false.
|
|
||||||
let staleNewtVersion: string | null = null;
|
|
||||||
|
|
||||||
async function getLatestNewtVersion(): Promise<string | null> {
|
|
||||||
try {
|
|
||||||
const cachedVersion = await cache.get<string>(
|
|
||||||
"cache:latestNewtVersion"
|
|
||||||
);
|
|
||||||
if (cachedVersion) {
|
|
||||||
return cachedVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 1500);
|
|
||||||
|
|
||||||
const response = await fetch(
|
|
||||||
"https://api.github.com/repos/fosrl/newt/tags",
|
|
||||||
{
|
|
||||||
signal: controller.signal
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
logger.warn(
|
|
||||||
`Failed to fetch latest Newt version from GitHub: ${response.status} ${response.statusText}`
|
|
||||||
);
|
|
||||||
return staleNewtVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
let tags = await response.json();
|
|
||||||
if (!Array.isArray(tags) || tags.length === 0) {
|
|
||||||
logger.warn("No tags found for Newt repository");
|
|
||||||
return staleNewtVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove release-candidates, then sort descending by semver so that
|
|
||||||
// duplicate tags (e.g. "1.10.3" and "v1.10.3") and any ordering quirks
|
|
||||||
// from the GitHub API do not cause an older tag to be selected.
|
|
||||||
tags = tags.filter((tag: any) => !tag.name.includes("rc"));
|
|
||||||
tags.sort((a: any, b: any) => {
|
|
||||||
const va = semver.coerce(a.name);
|
|
||||||
const vb = semver.coerce(b.name);
|
|
||||||
if (!va && !vb) return 0;
|
|
||||||
if (!va) return 1;
|
|
||||||
if (!vb) return -1;
|
|
||||||
return semver.rcompare(va, vb);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Deduplicate: keep only the first (highest) entry per normalised version
|
|
||||||
const seen = new Set<string>();
|
|
||||||
tags = tags.filter((tag: any) => {
|
|
||||||
const normalised = semver.coerce(tag.name)?.version;
|
|
||||||
if (!normalised || seen.has(normalised)) return false;
|
|
||||||
seen.add(normalised);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (tags.length === 0) {
|
|
||||||
logger.warn("No valid semver tags found for Newt repository");
|
|
||||||
return staleNewtVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
const latestVersion = tags[0].name;
|
|
||||||
|
|
||||||
staleNewtVersion = latestVersion;
|
|
||||||
await cache.set("cache:latestNewtVersion", latestVersion, 3600);
|
|
||||||
|
|
||||||
return latestVersion;
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.name === "AbortError") {
|
|
||||||
logger.warn(
|
|
||||||
"Request to fetch latest Newt version timed out (1.5s)"
|
|
||||||
);
|
|
||||||
} else if (error.cause?.code === "UND_ERR_CONNECT_TIMEOUT") {
|
|
||||||
logger.warn(
|
|
||||||
"Connection timeout while fetching latest Newt version"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
logger.warn(
|
|
||||||
"Error fetching latest Newt version:",
|
|
||||||
error.message || error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return staleNewtVersion;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const listSitesParamsSchema = z.strictObject({
|
const listSitesParamsSchema = z.strictObject({
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
@@ -449,9 +357,6 @@ export async function listSites(
|
|||||||
|
|
||||||
const totalCount = Number(countRows[0]?.count ?? 0);
|
const totalCount = Number(countRows[0]?.count ?? 0);
|
||||||
|
|
||||||
// Get latest version asynchronously without blocking the response
|
|
||||||
const latestNewtVersionPromise = getLatestNewtVersion();
|
|
||||||
|
|
||||||
const siteIds = rows.map((site) => site.siteId);
|
const siteIds = rows.map((site) => site.siteId);
|
||||||
|
|
||||||
let labelsForSites: Array<{
|
let labelsForSites: Array<{
|
||||||
@@ -494,36 +399,6 @@ export async function listSites(
|
|||||||
return { ...siteWithUpdate, labels: labelsForSite };
|
return { ...siteWithUpdate, labels: labelsForSite };
|
||||||
});
|
});
|
||||||
|
|
||||||
// Try to get the latest version, but don't block if it fails
|
|
||||||
try {
|
|
||||||
const latestNewtVersion = await latestNewtVersionPromise;
|
|
||||||
|
|
||||||
if (latestNewtVersion) {
|
|
||||||
sitesWithUpdates.forEach((site) => {
|
|
||||||
if (
|
|
||||||
site.type === "newt" &&
|
|
||||||
site.newtVersion &&
|
|
||||||
latestNewtVersion
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
site.newtUpdateAvailable = semver.lt(
|
|
||||||
site.newtVersion,
|
|
||||||
latestNewtVersion
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
site.newtUpdateAvailable = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Log the error but don't let it block the response
|
|
||||||
logger.warn(
|
|
||||||
"Failed to check for Newt updates, continuing without update info:",
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sitesPayload = sitesWithUpdates.map((site) =>
|
const sitesPayload = sitesWithUpdates.map((site) =>
|
||||||
site.type === "local" ? { ...site, online: undefined } : site
|
site.type === "local" ? { ...site, online: undefined } : site
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -41,6 +41,13 @@ import { useParams } from "next/navigation";
|
|||||||
import { FaApple, FaWindows, FaLinux } from "react-icons/fa";
|
import { FaApple, FaWindows, FaLinux } from "react-icons/fa";
|
||||||
import { SiAndroid } from "react-icons/si";
|
import { SiAndroid } from "react-icons/si";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
|
import {
|
||||||
|
productUpdatesQueries,
|
||||||
|
type LatestVersionResponse
|
||||||
|
} from "@app/lib/queries";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import semver from "semver";
|
||||||
|
import { InfoPopup } from "@app/components/ui/info-popup";
|
||||||
|
|
||||||
function formatTimestamp(timestamp: number | null | undefined): string {
|
function formatTimestamp(timestamp: number | null | undefined): string {
|
||||||
if (!timestamp) return "-";
|
if (!timestamp) return "-";
|
||||||
@@ -166,6 +173,34 @@ export default function GeneralPage() {
|
|||||||
}>(null);
|
}>(null);
|
||||||
const [isCheckingCache, setIsCheckingCache] = useState(false);
|
const [isCheckingCache, setIsCheckingCache] = useState(false);
|
||||||
const [isRebuildingCache, setIsRebuildingCache] = useState(false);
|
const [isRebuildingCache, setIsRebuildingCache] = useState(false);
|
||||||
|
const data = useQuery(productUpdatesQueries.latestVersion(true));
|
||||||
|
const latestPlatformVersions = data.data?.data;
|
||||||
|
|
||||||
|
const agentVersionMap: Record<string, string> = {
|
||||||
|
"Pangolin Windows": "windows",
|
||||||
|
"Pangolin Android": "android",
|
||||||
|
"Pangolin iOS": "ios",
|
||||||
|
"Pangolin iPadOS": "ios",
|
||||||
|
"Pangolin macOS": "mac",
|
||||||
|
"Pangolin CLI": "cli",
|
||||||
|
"Olm CLI": "olm"
|
||||||
|
};
|
||||||
|
|
||||||
|
let updateAvailable = false;
|
||||||
|
if (client.agent && client.olmVersion && latestPlatformVersions) {
|
||||||
|
const agent = agentVersionMap[
|
||||||
|
client.agent
|
||||||
|
] as keyof LatestVersionResponse;
|
||||||
|
|
||||||
|
if (agent in latestPlatformVersions) {
|
||||||
|
const agentVersion = latestPlatformVersions[agent];
|
||||||
|
|
||||||
|
updateAvailable = semver.lt(
|
||||||
|
client.olmVersion,
|
||||||
|
agentVersion.latestVersion
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// get "imp" from local storage to determine if we should show the verify button (imp = "1" means show)
|
// get "imp" from local storage to determine if we should show the verify button (imp = "1" means show)
|
||||||
const showVerifyButton =
|
const showVerifyButton =
|
||||||
@@ -451,11 +486,21 @@ export default function GeneralPage() {
|
|||||||
{t("agent")}
|
{t("agent")}
|
||||||
</InfoSectionTitle>
|
</InfoSectionTitle>
|
||||||
<InfoSectionContent>
|
<InfoSectionContent>
|
||||||
<Badge variant="secondary">
|
<div className="flex items-center">
|
||||||
{client.agent +
|
<Badge variant="secondary">
|
||||||
" v" +
|
{client.agent +
|
||||||
client.olmVersion}
|
" v" +
|
||||||
</Badge>
|
client.olmVersion}
|
||||||
|
</Badge>
|
||||||
|
|
||||||
|
{updateAvailable && (
|
||||||
|
<InfoPopup
|
||||||
|
info={t(
|
||||||
|
"updateAvailableInfo"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</InfoSectionContent>
|
</InfoSectionContent>
|
||||||
</InfoSection>
|
</InfoSection>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import Link from "next/link";
|
|||||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useMemo, useState, useTransition } from "react";
|
import { useMemo, useState, useTransition } from "react";
|
||||||
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
|
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
|
||||||
import { build } from "@server/build";
|
|
||||||
import type { QueryRequestAuditLogResponse } from "@server/routers/auditLogs/types";
|
import type { QueryRequestAuditLogResponse } from "@server/routers/auditLogs/types";
|
||||||
import { ColumnFilterButton } from "@app/components/ColumnFilterButton";
|
import { ColumnFilterButton } from "@app/components/ColumnFilterButton";
|
||||||
|
|
||||||
@@ -122,8 +121,7 @@ export default function GeneralPage() {
|
|||||||
...logQueries.requests({
|
...logQueries.requests({
|
||||||
orgId: orgId as string,
|
orgId: orgId as string,
|
||||||
filters: queryFilters
|
filters: queryFilters
|
||||||
}),
|
})
|
||||||
enabled: build !== "oss"
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const rows = isLoading ? generateSampleRequestLogs() : (data?.log ?? []);
|
const rows = isLoading ? generateSampleRequestLogs() : (data?.log ?? []);
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import {
|
|||||||
} from "@app/components/ui/dropdown-menu";
|
} from "@app/components/ui/dropdown-menu";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { useNavigationContext } from "@app/hooks/useNavigationContext";
|
import { useNavigationContext } from "@app/hooks/useNavigationContext";
|
||||||
|
import { useOptimisticLabels } from "@app/hooks/useOptimisticLabels";
|
||||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import { cn } from "@app/lib/cn";
|
|
||||||
import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn";
|
import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import type { PaginationState } from "@tanstack/react-table";
|
import type { PaginationState } from "@tanstack/react-table";
|
||||||
@@ -31,15 +31,18 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { startTransition, useMemo, useState, useTransition } from "react";
|
import { startTransition, useMemo, useState, useTransition } from "react";
|
||||||
import { useDebouncedCallback } from "use-debounce";
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
import z from "zod";
|
|
||||||
import { ColumnFilterButton } from "./ColumnFilterButton";
|
import { ColumnFilterButton } from "./ColumnFilterButton";
|
||||||
import { type SelectedLabel } from "./labels-selector";
|
import { LabelColumnFilterButton } from "./LabelColumnFilterButton";
|
||||||
import { LabelsTableCell } from "./LabelsTableCell";
|
import { LabelsTableCell } from "./LabelsTableCell";
|
||||||
import { Badge } from "./ui/badge";
|
import { Badge } from "./ui/badge";
|
||||||
import { ControlledDataTable } from "./ui/controlled-data-table";
|
import { ControlledDataTable } from "./ui/controlled-data-table";
|
||||||
import { LabelColumnFilterButton } from "./LabelColumnFilterButton";
|
import {
|
||||||
import { useLocalLabels } from "@app/hooks/useLocalLabels";
|
productUpdatesQueries,
|
||||||
import { useOptimisticLabels } from "@app/hooks/useOptimisticLabels";
|
type LatestVersionResponse
|
||||||
|
} from "@app/lib/queries";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import semver from "semver";
|
||||||
|
import { InfoPopup } from "./ui/info-popup";
|
||||||
|
|
||||||
export type ClientRow = {
|
export type ClientRow = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -101,6 +104,9 @@ export default function MachineClientsTable({
|
|||||||
|
|
||||||
const { isPaidUser } = usePaidStatus();
|
const { isPaidUser } = usePaidStatus();
|
||||||
const isLabelFeatureEnabled = isPaidUser(tierMatrix.labels);
|
const isLabelFeatureEnabled = isPaidUser(tierMatrix.labels);
|
||||||
|
const data = useQuery(productUpdatesQueries.latestVersion(true));
|
||||||
|
|
||||||
|
const latestPlatformVersions = data.data?.data;
|
||||||
|
|
||||||
const defaultMachineColumnVisibility = {
|
const defaultMachineColumnVisibility = {
|
||||||
subnet: false,
|
subnet: false,
|
||||||
@@ -375,6 +381,37 @@ export default function MachineClientsTable({
|
|||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const originalRow = row.original;
|
const originalRow = row.original;
|
||||||
|
|
||||||
|
const agentVersionMap: Record<string, string> = {
|
||||||
|
"Pangolin Windows": "windows",
|
||||||
|
"Pangolin Android": "android",
|
||||||
|
"Pangolin iOS": "ios",
|
||||||
|
"Pangolin iPadOS": "ios",
|
||||||
|
"Pangolin macOS": "mac",
|
||||||
|
"Pangolin CLI": "cli",
|
||||||
|
"Olm CLI": "olm"
|
||||||
|
};
|
||||||
|
|
||||||
|
let updateAvailable = false;
|
||||||
|
|
||||||
|
if (
|
||||||
|
originalRow.olmVersion &&
|
||||||
|
originalRow.agent &&
|
||||||
|
latestPlatformVersions
|
||||||
|
) {
|
||||||
|
const agent = agentVersionMap[
|
||||||
|
originalRow.agent
|
||||||
|
] as keyof LatestVersionResponse;
|
||||||
|
|
||||||
|
if (agent in latestPlatformVersions) {
|
||||||
|
const agentVersion = latestPlatformVersions[agent];
|
||||||
|
|
||||||
|
updateAvailable = semver.lt(
|
||||||
|
originalRow.olmVersion,
|
||||||
|
agentVersion.latestVersion
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex items-center space-x-1">
|
||||||
{originalRow.agent && originalRow.olmVersion ? (
|
{originalRow.agent && originalRow.olmVersion ? (
|
||||||
@@ -386,9 +423,9 @@ export default function MachineClientsTable({
|
|||||||
) : (
|
) : (
|
||||||
"-"
|
"-"
|
||||||
)}
|
)}
|
||||||
{/*originalRow.olmUpdateAvailable && (
|
{updateAvailable && (
|
||||||
<InfoPopup info={t("olmUpdateAvailableInfo")} />
|
<InfoPopup info={t("updateAvailableInfo")} />
|
||||||
)*/}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -411,9 +411,9 @@ export function PrivateResourceForm({
|
|||||||
|
|
||||||
type FormData = z.infer<typeof formSchema>;
|
type FormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
const rolesQuery = useQuery(orgQueries.roles({ orgId }));
|
const clientsQuery = useQuery(
|
||||||
const usersQuery = useQuery(orgQueries.users({ orgId }));
|
orgQueries.machineClients({ orgId, perPage: 1 })
|
||||||
const clientsQuery = useQuery(orgQueries.machineClients({ orgId }));
|
);
|
||||||
const resourceRolesQuery = useQuery({
|
const resourceRolesQuery = useQuery({
|
||||||
...resourceQueries.siteResourceRoles({
|
...resourceQueries.siteResourceRoles({
|
||||||
siteResourceId: siteResourceId ?? 0
|
siteResourceId: siteResourceId ?? 0
|
||||||
@@ -433,13 +433,6 @@ export function PrivateResourceForm({
|
|||||||
enabled: siteResourceId != null
|
enabled: siteResourceId != null
|
||||||
});
|
});
|
||||||
|
|
||||||
const allRoles = (rolesQuery.data ?? [])
|
|
||||||
.map((r) => ({ id: r.roleId.toString(), text: r.name }))
|
|
||||||
.filter((r) => r.text !== "Admin");
|
|
||||||
const allUsers = (usersQuery.data ?? []).map((u) => ({
|
|
||||||
id: u.id.toString(),
|
|
||||||
text: `${getUserDisplayName({ email: u.email, username: u.username })}${u.type !== UserType.Internal ? ` (${u.idpName})` : ""}`
|
|
||||||
}));
|
|
||||||
const allClients = (clientsQuery.data ?? [])
|
const allClients = (clientsQuery.data ?? [])
|
||||||
.filter((c) => !c.userId)
|
.filter((c) => !c.userId)
|
||||||
.map((c) => ({ id: c.clientId.toString(), text: c.name }));
|
.map((c) => ({ id: c.clientId.toString(), text: c.name }));
|
||||||
@@ -478,8 +471,6 @@ export function PrivateResourceForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const loadingRolesUsers =
|
const loadingRolesUsers =
|
||||||
rolesQuery.isLoading ||
|
|
||||||
usersQuery.isLoading ||
|
|
||||||
clientsQuery.isLoading ||
|
clientsQuery.isLoading ||
|
||||||
(siteResourceId != null &&
|
(siteResourceId != null &&
|
||||||
(resourceRolesQuery.isLoading ||
|
(resourceRolesQuery.isLoading ||
|
||||||
@@ -488,16 +479,6 @@ export function PrivateResourceForm({
|
|||||||
|
|
||||||
const hasMachineClients = allClients.length > 0;
|
const hasMachineClients = allClients.length > 0;
|
||||||
|
|
||||||
const [activeRolesTagIndex, setActiveRolesTagIndex] = useState<
|
|
||||||
number | null
|
|
||||||
>(null);
|
|
||||||
const [activeUsersTagIndex, setActiveUsersTagIndex] = useState<
|
|
||||||
number | null
|
|
||||||
>(null);
|
|
||||||
const [activeClientsTagIndex, setActiveClientsTagIndex] = useState<
|
|
||||||
number | null
|
|
||||||
>(null);
|
|
||||||
|
|
||||||
const [sshServerMode, setSshServerMode] = useState<"standard" | "native">(
|
const [sshServerMode, setSshServerMode] = useState<"standard" | "native">(
|
||||||
() => {
|
() => {
|
||||||
if (variant === "edit" && resource) {
|
if (variant === "edit" && resource) {
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
|||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import { LabelColumnFilterButton } from "./LabelColumnFilterButton";
|
import { LabelColumnFilterButton } from "./LabelColumnFilterButton";
|
||||||
import { LabelsTableCell } from "./LabelsTableCell";
|
import { LabelsTableCell } from "./LabelsTableCell";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { productUpdatesQueries } from "@app/lib/queries";
|
||||||
|
import semver from "semver";
|
||||||
|
|
||||||
export type SiteRow = {
|
export type SiteRow = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -113,12 +116,11 @@ export default function SitesTable({
|
|||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
// useEffect(() => {
|
const { data: latestVersions } = useQuery(
|
||||||
// const interval = setInterval(() => {
|
productUpdatesQueries.latestVersion(true)
|
||||||
// router.refresh();
|
);
|
||||||
// }, 30_000);
|
|
||||||
// return () => clearInterval(interval);
|
const latestNewtVersion = latestVersions?.data?.newt?.latestVersion;
|
||||||
// }, []);
|
|
||||||
|
|
||||||
const booleanSearchFilterSchema = z
|
const booleanSearchFilterSchema = z
|
||||||
.enum(["true", "false"])
|
.enum(["true", "false"])
|
||||||
@@ -333,6 +335,11 @@ export default function SitesTable({
|
|||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const originalRow = row.original;
|
const originalRow = row.original;
|
||||||
|
|
||||||
|
let updateAvailable =
|
||||||
|
latestNewtVersion &&
|
||||||
|
originalRow.newtVersion &&
|
||||||
|
semver.lt(originalRow.newtVersion, latestNewtVersion);
|
||||||
|
|
||||||
if (originalRow.type === "newt") {
|
if (originalRow.type === "newt") {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex items-center space-x-1">
|
||||||
@@ -346,7 +353,7 @@ export default function SitesTable({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Badge>
|
</Badge>
|
||||||
{originalRow.newtUpdateAvailable && (
|
{updateAvailable && (
|
||||||
<InfoPopup
|
<InfoPopup
|
||||||
info={t("newtUpdateAvailableInfo")}
|
info={t("newtUpdateAvailableInfo")}
|
||||||
/>
|
/>
|
||||||
@@ -561,7 +568,7 @@ export default function SitesTable({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return cols;
|
return cols;
|
||||||
}, [isLabelFeatureEnabled, orgId, t, searchParams]);
|
}, [isLabelFeatureEnabled, orgId, t, searchParams, latestNewtVersion]);
|
||||||
|
|
||||||
function toggleSort(column: string) {
|
function toggleSort(column: string) {
|
||||||
const newSearch = getNextSortOrder(column, searchParams);
|
const newSearch = getNextSortOrder(column, searchParams);
|
||||||
|
|||||||
@@ -38,6 +38,12 @@ import { ColumnFilterButton } from "./ColumnFilterButton";
|
|||||||
import IdpTypeBadge from "./IdpTypeBadge";
|
import IdpTypeBadge from "./IdpTypeBadge";
|
||||||
import { Badge } from "./ui/badge";
|
import { Badge } from "./ui/badge";
|
||||||
import { ControlledDataTable } from "./ui/controlled-data-table";
|
import { ControlledDataTable } from "./ui/controlled-data-table";
|
||||||
|
import {
|
||||||
|
productUpdatesQueries,
|
||||||
|
type LatestVersionResponse
|
||||||
|
} from "@app/lib/queries";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import semver from "semver";
|
||||||
|
|
||||||
export type ClientRow = {
|
export type ClientRow = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -100,6 +106,9 @@ export default function UserDevicesTable({
|
|||||||
searchParams
|
searchParams
|
||||||
} = useNavigationContext();
|
} = useNavigationContext();
|
||||||
const [isRefreshing, startTransition] = useTransition();
|
const [isRefreshing, startTransition] = useTransition();
|
||||||
|
const data = useQuery(productUpdatesQueries.latestVersion(true));
|
||||||
|
|
||||||
|
const latestPlatformVersions = data.data?.data;
|
||||||
|
|
||||||
const defaultUserColumnVisibility = {
|
const defaultUserColumnVisibility = {
|
||||||
subnet: false,
|
subnet: false,
|
||||||
@@ -555,6 +564,37 @@ export default function UserDevicesTable({
|
|||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const originalRow = row.original;
|
const originalRow = row.original;
|
||||||
|
|
||||||
|
const agentVersionMap: Record<string, string> = {
|
||||||
|
"Pangolin Windows": "windows",
|
||||||
|
"Pangolin Android": "android",
|
||||||
|
"Pangolin iOS": "ios",
|
||||||
|
"Pangolin iPadOS": "ios",
|
||||||
|
"Pangolin macOS": "mac",
|
||||||
|
"Pangolin CLI": "cli",
|
||||||
|
"Olm CLI": "olm"
|
||||||
|
};
|
||||||
|
|
||||||
|
let updateAvailable = false;
|
||||||
|
|
||||||
|
if (
|
||||||
|
originalRow.olmVersion &&
|
||||||
|
originalRow.agent &&
|
||||||
|
latestPlatformVersions
|
||||||
|
) {
|
||||||
|
const agent = agentVersionMap[
|
||||||
|
originalRow.agent
|
||||||
|
] as keyof LatestVersionResponse;
|
||||||
|
|
||||||
|
if (agent in latestPlatformVersions) {
|
||||||
|
const agentVersion = latestPlatformVersions[agent];
|
||||||
|
|
||||||
|
updateAvailable = semver.lt(
|
||||||
|
originalRow.olmVersion,
|
||||||
|
agentVersion.latestVersion
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center space-x-1">
|
<div className="flex items-center space-x-1">
|
||||||
{originalRow.agent && originalRow.olmVersion ? (
|
{originalRow.agent && originalRow.olmVersion ? (
|
||||||
@@ -567,9 +607,9 @@ export default function UserDevicesTable({
|
|||||||
"-"
|
"-"
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/*originalRow.olmUpdateAvailable && (
|
{updateAvailable && (
|
||||||
<InfoPopup info={t("olmUpdateAvailableInfo")} />
|
<InfoPopup info={t("updateAvailableInfo")} />
|
||||||
)*/}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -714,7 +754,7 @@ export default function UserDevicesTable({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return allOptions;
|
return allOptions;
|
||||||
}, [t]);
|
}, [t, latestPlatformVersions]);
|
||||||
|
|
||||||
function handleFilterChange(
|
function handleFilterChange(
|
||||||
column: string,
|
column: string,
|
||||||
|
|||||||
@@ -63,6 +63,34 @@ export type LatestVersionResponse = {
|
|||||||
latestVersion: string;
|
latestVersion: string;
|
||||||
releaseNotes: string;
|
releaseNotes: string;
|
||||||
};
|
};
|
||||||
|
newt: {
|
||||||
|
latestVersion: string;
|
||||||
|
releaseNotes: string;
|
||||||
|
};
|
||||||
|
cli: {
|
||||||
|
latestVersion: string;
|
||||||
|
releaseNotes: string;
|
||||||
|
};
|
||||||
|
"panglin-node": {
|
||||||
|
latestVersion: string;
|
||||||
|
releaseNotes: string;
|
||||||
|
};
|
||||||
|
windows: {
|
||||||
|
latestVersion: string;
|
||||||
|
releaseNotes: string;
|
||||||
|
};
|
||||||
|
android: {
|
||||||
|
latestVersion: string;
|
||||||
|
releaseNotes: string;
|
||||||
|
};
|
||||||
|
mac: {
|
||||||
|
latestVersion: string;
|
||||||
|
releaseNotes: string;
|
||||||
|
};
|
||||||
|
ios: {
|
||||||
|
latestVersion: string;
|
||||||
|
releaseNotes: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const productUpdatesQueries = {
|
export const productUpdatesQueries = {
|
||||||
|
|||||||
Reference in New Issue
Block a user