diff --git a/server/routers/siteResource/listAllSiteResourcesByOrg.ts b/server/routers/siteResource/listAllSiteResourcesByOrg.ts
index 3495d9767..de9083c2c 100644
--- a/server/routers/siteResource/listAllSiteResourcesByOrg.ts
+++ b/server/routers/siteResource/listAllSiteResourcesByOrg.ts
@@ -76,6 +76,7 @@ export type ListAllSiteResourcesByOrgResponse = PaginatedResponse<{
siteName: string;
siteNiceId: string;
siteAddress: string | null;
+ siteOnline: boolean;
})[];
}>;
@@ -106,7 +107,8 @@ function querySiteResourcesBase() {
fullDomain: siteResources.fullDomain,
siteName: sites.name,
siteNiceId: sites.niceId,
- siteAddress: sites.address
+ siteAddress: sites.address,
+ siteOnline: sites.online
})
.from(siteResources)
.innerJoin(sites, eq(siteResources.siteId, sites.siteId));
diff --git a/src/app/[orgId]/settings/resources/client/page.tsx b/src/app/[orgId]/settings/resources/client/page.tsx
index 4d3b48c6c..f63563cc9 100644
--- a/src/app/[orgId]/settings/resources/client/page.tsx
+++ b/src/app/[orgId]/settings/resources/client/page.tsx
@@ -73,7 +73,8 @@ export default async function ClientResourcesPage(
{
siteId: siteResource.siteId,
siteName: siteResource.siteName,
- siteNiceId: siteResource.siteNiceId
+ siteNiceId: siteResource.siteNiceId,
+ online: siteResource.siteOnline
}
],
siteName: siteResource.siteName,
diff --git a/src/components/ClientResourcesTable.tsx b/src/components/ClientResourcesTable.tsx
index 4822f358e..fc1a6a6f3 100644
--- a/src/components/ClientResourcesTable.tsx
+++ b/src/components/ClientResourcesTable.tsx
@@ -20,7 +20,7 @@ import {
ArrowDown01Icon,
ArrowUp10Icon,
ArrowUpDown,
- ArrowUpRight,
+ ChevronDown,
ChevronsUpDownIcon,
MoreHorizontal
} from "lucide-react";
@@ -38,16 +38,13 @@ import { ControlledDataTable } from "./ui/controlled-data-table";
import { useNavigationContext } from "@app/hooks/useNavigationContext";
import { useDebouncedCallback } from "use-debounce";
import { ColumnFilterButton } from "./ColumnFilterButton";
-import {
- Popover,
- PopoverContent,
- PopoverTrigger
-} from "@app/components/ui/popover";
+import { cn } from "@app/lib/cn";
export type InternalResourceSiteRow = {
siteId: number;
siteName: string;
siteNiceId: string;
+ online: boolean;
};
export type InternalResourceRow = {
@@ -113,99 +110,106 @@ function isSafeUrlForLink(href: string): boolean {
}
}
-const MAX_SITE_LINKS = 3;
+type AggregateSitesStatus = "allOnline" | "partial" | "allOffline";
-function ClientResourceSiteLinks({
- orgId,
- sites
-}: {
- orgId: string;
- sites: InternalResourceSiteRow[];
-}) {
- if (sites.length === 0) {
- return -;
+function aggregateSitesStatus(
+ resourceSites: InternalResourceSiteRow[]
+): AggregateSitesStatus {
+ if (resourceSites.length === 0) {
+ return "allOffline";
}
- const visible = sites.slice(0, MAX_SITE_LINKS);
- const overflow = sites.slice(MAX_SITE_LINKS);
-
- return (
-
- {visible.map((site) => (
-
-
-
- ))}
- {overflow.length > 0 ? (
-
- ) : null}
-
- );
+ const onlineCount = resourceSites.filter((rs) => rs.online).length;
+ if (onlineCount === resourceSites.length) return "allOnline";
+ if (onlineCount > 0) return "partial";
+ return "allOffline";
}
-function OverflowSitesPopover({
+function aggregateStatusDotClass(status: AggregateSitesStatus): string {
+ switch (status) {
+ case "allOnline":
+ return "bg-green-500";
+ case "partial":
+ return "bg-yellow-500";
+ case "allOffline":
+ default:
+ return "bg-gray-500";
+ }
+}
+
+function ClientResourceSitesStatusCell({
orgId,
- sites
+ resourceSites
}: {
orgId: string;
- sites: InternalResourceSiteRow[];
+ resourceSites: InternalResourceSiteRow[];
}) {
- const [open, setOpen] = useState(false);
+ const t = useTranslations();
+
+ if (resourceSites.length === 0) {
+ return -;
+ }
+
+ const aggregate = aggregateSitesStatus(resourceSites);
+ const countLabel = t("multiSitesSelectorSitesCount", {
+ count: resourceSites.length
+ });
return (
-
-
+
+
-
- setOpen(true)}
- onMouseLeave={() => setOpen(false)}
- >
-
- {sites.map((site) => (
- -
+
+
+ {resourceSites.map((site) => {
+ const isOnline = site.online;
+ return (
+
-
- ))}
-
-
-
+
+ );
+ })}
+
+
);
}
@@ -243,8 +247,6 @@ export default function ClientResourcesTable({
useState();
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
- const { data: sites = [] } = useQuery(orgQueries.sites({ orgId }));
-
const [isRefreshing, startTransition] = useTransition();
const refreshData = () => {
@@ -339,9 +341,9 @@ export default function ClientResourcesTable({
cell: ({ row }) => {
const resourceRow = row.original;
return (
-
);
}
@@ -599,7 +601,6 @@ export default function ClientResourcesTable({
setOpen={setIsEditDialogOpen}
resource={editingResource}
orgId={orgId}
- sites={sites}
onSuccess={() => {
// Delay refresh to allow modal to close smoothly
setTimeout(() => {
@@ -614,7 +615,6 @@ export default function ClientResourcesTable({
open={isCreateDialogOpen}
setOpen={setIsCreateDialogOpen}
orgId={orgId}
- sites={sites}
onSuccess={() => {
// Delay refresh to allow modal to close smoothly
setTimeout(() => {
diff --git a/src/components/CreateInternalResourceDialog.tsx b/src/components/CreateInternalResourceDialog.tsx
index 1ad7b3632..c0483e35d 100644
--- a/src/components/CreateInternalResourceDialog.tsx
+++ b/src/components/CreateInternalResourceDialog.tsx
@@ -31,7 +31,6 @@ type CreateInternalResourceDialogProps = {
open: boolean;
setOpen: (val: boolean) => void;
orgId: string;
- sites: Site[];
onSuccess?: () => void;
};
@@ -39,7 +38,6 @@ export default function CreateInternalResourceDialog({
open,
setOpen,
orgId,
- sites,
onSuccess
}: CreateInternalResourceDialogProps) {
const t = useTranslations();
@@ -155,7 +153,6 @@ export default function CreateInternalResourceDialog({
void;
resource: InternalResourceData;
orgId: string;
- sites: Site[];
onSuccess?: () => void;
};
@@ -43,7 +42,6 @@ export default function EditInternalResourceDialog({
setOpen,
resource,
orgId,
- sites,
onSuccess
}: EditInternalResourceDialogProps) {
const t = useTranslations();
@@ -174,7 +172,6 @@ export default function EditInternalResourceDialog({
variant="edit"
open={open}
resource={resource}
- sites={sites}
orgId={orgId}
siteResourceId={resource.id}
formId="edit-internal-resource-form"
diff --git a/src/components/InternalResourceForm.tsx b/src/components/InternalResourceForm.tsx
index 6bc807046..0d98fb30b 100644
--- a/src/components/InternalResourceForm.tsx
+++ b/src/components/InternalResourceForm.tsx
@@ -159,18 +159,7 @@ const tagSchema = z.object({ id: z.string(), text: z.string() });
function buildSelectedSitesForResource(
resource: InternalResourceData,
- catalog: Site[]
): Selectedsite[] {
- const fromCatalog = catalog.find((s) => s.siteId === resource.siteId);
- if (fromCatalog) {
- return [
- {
- name: fromCatalog.name,
- siteId: fromCatalog.siteId,
- type: fromCatalog.type
- }
- ];
- }
return [
{
name: resource.siteName,
@@ -207,7 +196,6 @@ type InternalResourceFormProps = {
variant: "create" | "edit";
resource?: InternalResourceData;
open?: boolean;
- sites: Site[];
orgId: string;
siteResourceId?: number;
formId: string;
@@ -218,7 +206,6 @@ export function InternalResourceForm({
variant,
resource,
open,
- sites,
orgId,
siteResourceId,
formId,
@@ -375,8 +362,6 @@ export function InternalResourceForm({
type FormData = z.infer;
- const availableSites = sites.filter((s) => s.type === "newt");
-
const rolesQuery = useQuery(orgQueries.roles({ orgId }));
const usersQuery = useQuery(orgQueries.users({ orgId }));
const clientsQuery = useQuery(orgQueries.machineClients({ orgId }));
@@ -517,7 +502,7 @@ export function InternalResourceForm({
}
: {
name: "",
- siteIds: availableSites[0] ? [availableSites[0].siteId] : [],
+ siteIds: [],
mode: "host",
destination: "",
alias: null,
@@ -539,16 +524,8 @@ export function InternalResourceForm({
const [selectedSites, setSelectedSites] = useState(() =>
variant === "edit" && resource
- ? buildSelectedSitesForResource(resource, sites)
- : availableSites[0]
- ? [
- {
- name: availableSites[0].name,
- siteId: availableSites[0].siteId,
- type: availableSites[0].type
- }
- ]
- : []
+ ? buildSelectedSitesForResource(resource)
+ : []
);
const form = useForm({
@@ -580,7 +557,7 @@ export function InternalResourceForm({
if (variant === "create" && open) {
form.reset({
name: "",
- siteIds: availableSites[0] ? [availableSites[0].siteId] : [],
+ siteIds: [],
mode: "host",
destination: "",
alias: null,
@@ -599,23 +576,13 @@ export function InternalResourceForm({
users: [],
clients: []
});
- setSelectedSites(
- availableSites[0]
- ? [
- {
- name: availableSites[0].name,
- siteId: availableSites[0].siteId,
- type: availableSites[0].type
- }
- ]
- : []
- );
+ setSelectedSites([]);
setTcpPortMode("all");
setUdpPortMode("all");
setTcpCustomPorts("");
setUdpCustomPorts("");
}
- }, [variant, open, form, sites]);
+ }, [variant, open, form]);
// Reset when edit dialog opens / resource changes
useEffect(() => {
@@ -644,7 +611,7 @@ export function InternalResourceForm({
clients: []
});
setSelectedSites(
- buildSelectedSitesForResource(resource, sites)
+ buildSelectedSitesForResource(resource)
);
setTcpPortMode(
getPortModeFromString(resource.tcpPortRangeString)
@@ -667,7 +634,7 @@ export function InternalResourceForm({
previousResourceId.current = resource.id;
}
}
- }, [variant, resource, form, sites]);
+ }, [variant, resource, form]);
// When edit dialog closes, clear previousResourceId so next open (for any resource) resets from fresh data
useEffect(() => {