mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-18 00:09:34 +02:00
Improved resource card layout and favicon handling
This commit is contained in:
parent
f6ae379caf
commit
97b267e7ae
2 changed files with 250 additions and 53 deletions
|
@ -9,7 +9,8 @@ import {
|
||||||
roles,
|
roles,
|
||||||
resourcePassword,
|
resourcePassword,
|
||||||
resourcePincode,
|
resourcePincode,
|
||||||
resourceWhitelist
|
resourceWhitelist,
|
||||||
|
sites
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
@ -94,9 +95,11 @@ export async function getUserResources(
|
||||||
enabled: resources.enabled,
|
enabled: resources.enabled,
|
||||||
sso: resources.sso,
|
sso: resources.sso,
|
||||||
protocol: resources.protocol,
|
protocol: resources.protocol,
|
||||||
emailWhitelistEnabled: resources.emailWhitelistEnabled
|
emailWhitelistEnabled: resources.emailWhitelistEnabled,
|
||||||
|
siteName: sites.name
|
||||||
})
|
})
|
||||||
.from(resources)
|
.from(resources)
|
||||||
|
.leftJoin(sites, eq(sites.siteId, resources.siteId))
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
inArray(resources.resourceId, accessibleResourceIds),
|
inArray(resources.resourceId, accessibleResourceIds),
|
||||||
|
@ -124,7 +127,12 @@ export async function getUserResources(
|
||||||
domain: `${resource.ssl ? "https://" : "http://"}${resource.fullDomain}`,
|
domain: `${resource.ssl ? "https://" : "http://"}${resource.fullDomain}`,
|
||||||
enabled: resource.enabled,
|
enabled: resource.enabled,
|
||||||
protected: !!(resource.sso || hasPassword || hasPincode || hasWhitelist),
|
protected: !!(resource.sso || hasPassword || hasPincode || hasWhitelist),
|
||||||
protocol: resource.protocol
|
protocol: resource.protocol,
|
||||||
|
sso: resource.sso,
|
||||||
|
password: hasPassword,
|
||||||
|
pincode: hasPincode,
|
||||||
|
whitelist: hasWhitelist,
|
||||||
|
siteName: resource.siteName
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -7,12 +7,20 @@ import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { ExternalLink, Globe, ShieldCheck, Search, RefreshCw, AlertCircle, Plus, Shield, ShieldOff, ChevronLeft, ChevronRight } from "lucide-react";
|
import { ExternalLink, Globe, ShieldCheck, Search, RefreshCw, AlertCircle, Plus, Shield, ShieldOff, ChevronLeft, ChevronRight, Building2, Key, KeyRound, Fingerprint, AtSign, Copy, InfoIcon } from "lucide-react";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { GetUserResourcesResponse } from "@server/routers/resource/getUserResources";
|
import { GetUserResourcesResponse } from "@server/routers/resource/getUserResources";
|
||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
|
import { useToast } from "@app/hooks/useToast";
|
||||||
|
|
||||||
|
// Update Resource type to include site information
|
||||||
type Resource = {
|
type Resource = {
|
||||||
resourceId: number;
|
resourceId: number;
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -20,6 +28,13 @@ type Resource = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
protected: boolean;
|
protected: boolean;
|
||||||
protocol: string;
|
protocol: string;
|
||||||
|
// Auth method fields
|
||||||
|
sso?: boolean;
|
||||||
|
password?: boolean;
|
||||||
|
pincode?: boolean;
|
||||||
|
whitelist?: boolean;
|
||||||
|
// Site information
|
||||||
|
siteName?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MemberResourcesPortalProps = {
|
type MemberResourcesPortalProps = {
|
||||||
|
@ -66,30 +81,184 @@ const ResourceFavicon = ({ domain, enabled }: { domain: string; enabled: boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
// Enhanced status badge component
|
// Enhanced status badge component
|
||||||
const StatusBadge = ({ enabled, protected: isProtected }: { enabled: boolean; protected: boolean }) => {
|
const StatusBadge = ({ enabled, protected: isProtected, resource }: { enabled: boolean; protected: boolean; resource: Resource }) => {
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return (
|
return (
|
||||||
<Badge variant="secondary" className="gap-1.5 bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-700">
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<div className="h-7 w-7 rounded-full flex items-center justify-center bg-gray-100 dark:bg-gray-800">
|
||||||
<div className="h-2 w-2 bg-gray-400 dark:bg-gray-500 rounded-full"></div>
|
<div className="h-2 w-2 bg-gray-400 dark:bg-gray-500 rounded-full"></div>
|
||||||
Disabled
|
</div>
|
||||||
</Badge>
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Resource Disabled</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isProtected) {
|
if (isProtected) {
|
||||||
return (
|
return (
|
||||||
<Badge variant="secondary" className="gap-1.5 bg-green-50 dark:bg-green-950 text-green-700 dark:text-green-300 border-green-200 dark:border-green-800">
|
<TooltipProvider>
|
||||||
<ShieldCheck className="h-3 w-3" />
|
<Tooltip>
|
||||||
Protected
|
<TooltipTrigger>
|
||||||
</Badge>
|
<div className="h-7 w-7 rounded-full flex items-center justify-center bg-green-50 dark:bg-green-950">
|
||||||
|
<ShieldCheck className="h-3.5 w-3.5 text-green-700 dark:text-green-300" />
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="flex flex-col gap-2">
|
||||||
|
<p className="font-medium">Protected Resource</p>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm font-medium text-muted-foreground">Authentication Methods:</p>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
{resource.sso && (
|
||||||
|
<div className="flex items-center gap-1.5 text-sm">
|
||||||
|
<div className="h-6 w-6 rounded-full flex items-center justify-center bg-blue-50 dark:bg-blue-950">
|
||||||
|
<Key className="h-3 w-3 text-blue-700 dark:text-blue-300" />
|
||||||
|
</div>
|
||||||
|
<span>Single Sign-On (SSO)</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{resource.password && (
|
||||||
|
<div className="flex items-center gap-1.5 text-sm">
|
||||||
|
<div className="h-6 w-6 rounded-full flex items-center justify-center bg-purple-50 dark:bg-purple-950">
|
||||||
|
<KeyRound className="h-3 w-3 text-purple-700 dark:text-purple-300" />
|
||||||
|
</div>
|
||||||
|
<span>Password Protected</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{resource.pincode && (
|
||||||
|
<div className="flex items-center gap-1.5 text-sm">
|
||||||
|
<div className="h-6 w-6 rounded-full flex items-center justify-center bg-emerald-50 dark:bg-emerald-950">
|
||||||
|
<Fingerprint className="h-3 w-3 text-emerald-700 dark:text-emerald-300" />
|
||||||
|
</div>
|
||||||
|
<span>PIN Code</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{resource.whitelist && (
|
||||||
|
<div className="flex items-center gap-1.5 text-sm">
|
||||||
|
<div className="h-6 w-6 rounded-full flex items-center justify-center bg-amber-50 dark:bg-amber-950">
|
||||||
|
<AtSign className="h-3 w-3 text-amber-700 dark:text-amber-300" />
|
||||||
|
</div>
|
||||||
|
<span>Email Whitelist</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge variant="secondary" className="gap-1.5 bg-orange-50 dark:bg-orange-950 text-orange-700 dark:text-orange-300 border-orange-200 dark:border-orange-800">
|
<div className="h-7 w-7 rounded-full flex items-center justify-center bg-orange-50 dark:bg-orange-950">
|
||||||
<ShieldOff className="h-3 w-3" />
|
<ShieldOff className="h-3.5 w-3.5 text-orange-700 dark:text-orange-300" />
|
||||||
Unprotected
|
</div>
|
||||||
</Badge>
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Resource Info component
|
||||||
|
const ResourceInfo = ({ resource }: { resource: Resource }) => {
|
||||||
|
const hasAuthMethods = resource.sso || resource.password || resource.pincode || resource.whitelist;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<div className="h-7 w-7 rounded-full flex items-center justify-center bg-muted hover:bg-muted/80 transition-colors">
|
||||||
|
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="flex flex-col gap-3 w-[280px] p-3 bg-card border-2">
|
||||||
|
{/* Site Information */}
|
||||||
|
{resource.siteName && (
|
||||||
|
<div>
|
||||||
|
<div className="text-xs font-medium mb-1.5">Site</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Building2 className="h-4 w-4 text-foreground shrink-0" />
|
||||||
|
<span className="text-sm">{resource.siteName}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Authentication Methods */}
|
||||||
|
{hasAuthMethods && (
|
||||||
|
<div className={resource.siteName ? "border-t border-border pt-2" : ""}>
|
||||||
|
<div className="text-xs font-medium mb-1.5">Authentication Methods</div>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
{resource.sso && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="h-5 w-5 rounded-full flex items-center justify-center bg-blue-50/50 dark:bg-blue-950/50">
|
||||||
|
<Key className="h-3 w-3 text-blue-700 dark:text-blue-300" />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm">Single Sign-On (SSO)</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{resource.password && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="h-5 w-5 rounded-full flex items-center justify-center bg-purple-50/50 dark:bg-purple-950/50">
|
||||||
|
<KeyRound className="h-3 w-3 text-purple-700 dark:text-purple-300" />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm">Password Protected</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{resource.pincode && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="h-5 w-5 rounded-full flex items-center justify-center bg-emerald-50/50 dark:bg-emerald-950/50">
|
||||||
|
<Fingerprint className="h-3 w-3 text-emerald-700 dark:text-emerald-300" />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm">PIN Code</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{resource.whitelist && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="h-5 w-5 rounded-full flex items-center justify-center bg-amber-50/50 dark:bg-amber-950/50">
|
||||||
|
<AtSign className="h-3 w-3 text-amber-700 dark:text-amber-300" />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm">Email Whitelist</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Resource Status - if disabled */}
|
||||||
|
{!resource.enabled && (
|
||||||
|
<div className={`${(resource.siteName || hasAuthMethods) ? "border-t border-border pt-2" : ""}`}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<AlertCircle className="h-4 w-4 text-destructive shrink-0" />
|
||||||
|
<span className="text-sm text-destructive">Resource Disabled</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Site badge component
|
||||||
|
const SiteBadge = ({ resource }: { resource: Resource }) => {
|
||||||
|
if (!resource.siteName) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<div className="h-7 w-7 rounded-full flex items-center justify-center bg-muted/60 dark:bg-muted/80">
|
||||||
|
<Building2 className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{resource.siteName}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -212,6 +381,7 @@ export default function MemberResourcesPortal({ orgId }: MemberResourcesPortalPr
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
const api = createApiClient({ env });
|
const api = createApiClient({ env });
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
const [resources, setResources] = useState<Resource[]>([]);
|
const [resources, setResources] = useState<Resource[]>([]);
|
||||||
const [filteredResources, setFilteredResources] = useState<Resource[]>([]);
|
const [filteredResources, setFilteredResources] = useState<Resource[]>([]);
|
||||||
|
@ -477,53 +647,72 @@ export default function MemberResourcesPortal({ orgId }: MemberResourcesPortalPr
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Resources Grid */}
|
{/* Resources Grid */}
|
||||||
<div className="grid gap-4 sm:gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 auto-cols-fr">
|
<div className="grid gap-5 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-4 auto-cols-fr">
|
||||||
{paginatedResources.map((resource) => (
|
{paginatedResources.map((resource) => (
|
||||||
<Card key={resource.resourceId} className="rounded-lg bg-card text-card-foreground hover:shadow-lg transition-all duration-200 border-2 hover:border-primary/20 dark:hover:border-primary/30 flex flex-col w-full group">
|
<Card key={resource.resourceId} className="rounded-lg bg-card text-card-foreground hover:shadow-lg transition-all duration-200 border-2 hover:border-primary/20 dark:hover:border-primary/30 flex flex-col w-full group min-h-[200px]">
|
||||||
<CardHeader className="pb-3">
|
<div className="p-6">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<CardTitle className="text-lg font-bold text-foreground truncate mr-2 group-hover:text-primary transition-colors">
|
<div className="flex items-center min-w-0 flex-1 gap-3 overflow-hidden">
|
||||||
{resource.name}
|
<div className="flex-shrink-0">
|
||||||
</CardTitle>
|
<ResourceFavicon domain={resource.domain} enabled={resource.enabled} />
|
||||||
<Badge variant="secondary" className="text-xs shrink-0 bg-muted/60 dark:bg-muted/80 text-muted-foreground">
|
</div>
|
||||||
Your Site
|
<TooltipProvider>
|
||||||
</Badge>
|
<Tooltip>
|
||||||
</div>
|
<TooltipTrigger className="min-w-0 max-w-full">
|
||||||
</CardHeader>
|
<CardTitle className="text-lg font-bold text-foreground truncate group-hover:text-primary transition-colors">
|
||||||
<CardContent className="px-6 pb-6 flex-1 flex flex-col justify-between">
|
{resource.name}
|
||||||
<div className="space-y-4">
|
</CardTitle>
|
||||||
{/* Resource URL with Favicon */}
|
</TooltipTrigger>
|
||||||
<div className="flex items-center space-x-2">
|
<TooltipContent>
|
||||||
<ResourceFavicon domain={resource.domain} enabled={resource.enabled} />
|
<p className="max-w-xs break-words">{resource.name}</p>
|
||||||
<button
|
</TooltipContent>
|
||||||
onClick={() => handleOpenResource(resource)}
|
</Tooltip>
|
||||||
className="text-sm text-blue-500 dark:text-blue-400 font-medium hover:underline text-left truncate transition-colors hover:text-blue-600 dark:hover:text-blue-300"
|
</TooltipProvider>
|
||||||
disabled={!resource.enabled}
|
|
||||||
>
|
|
||||||
{resource.domain.replace(/^https?:\/\//, '')}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Enhanced Status Badge */}
|
<div className="flex-shrink-0">
|
||||||
<div className="flex items-center">
|
<ResourceInfo resource={resource} />
|
||||||
<StatusBadge enabled={resource.enabled} protected={resource.protected} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Open Resource Button */}
|
<div className="flex items-center gap-2 mt-3">
|
||||||
<div className="mt-4">
|
<button
|
||||||
<Button
|
|
||||||
onClick={() => handleOpenResource(resource)}
|
onClick={() => handleOpenResource(resource)}
|
||||||
className="w-full h-8 transition-all group-hover:shadow-sm"
|
className="text-sm text-muted-foreground font-medium text-left truncate flex-1"
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
disabled={!resource.enabled}
|
disabled={!resource.enabled}
|
||||||
>
|
>
|
||||||
<ExternalLink className="h-3 w-3 mr-2" />
|
{resource.domain.replace(/^https?:\/\//, '')}
|
||||||
Open Resource
|
</button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(resource.domain);
|
||||||
|
toast({
|
||||||
|
title: "Copied to clipboard",
|
||||||
|
description: "Resource URL has been copied to your clipboard.",
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 pt-0 mt-auto">
|
||||||
|
<Button
|
||||||
|
onClick={() => handleOpenResource(resource)}
|
||||||
|
className="w-full h-9 transition-all group-hover:shadow-sm"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={!resource.enabled}
|
||||||
|
>
|
||||||
|
<ExternalLink className="h-3.5 w-3.5 mr-2" />
|
||||||
|
Open Resource
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue