ui enhancements

This commit is contained in:
miloschwartz
2025-12-24 15:53:08 -05:00
parent c5ece144d0
commit 9fba9bd6b7
13 changed files with 131 additions and 73 deletions

View File

@@ -1480,7 +1480,7 @@
"IAgreeToThe": "I agree to the", "IAgreeToThe": "I agree to the",
"termsOfService": "terms of service", "termsOfService": "terms of service",
"and": "and", "and": "and",
"privacyPolicy": "privacy policy" "privacyPolicy": "privacy policy."
}, },
"signUpMarketing": { "signUpMarketing": {
"keepMeInTheLoop": "Keep me in the loop with news, updates, and new features by email." "keepMeInTheLoop": "Keep me in the loop with news, updates, and new features by email."

View File

@@ -36,7 +36,7 @@ async function query(clientId?: number, niceId?: string, orgId?: string) {
.select() .select()
.from(clients) .from(clients)
.where(and(eq(clients.niceId, niceId), eq(clients.orgId, orgId))) .where(and(eq(clients.niceId, niceId), eq(clients.orgId, orgId)))
.leftJoin(olms, eq(olms.clientId, olms.clientId)) .leftJoin(olms, eq(clients.clientId, olms.clientId))
.limit(1); .limit(1);
return res; return res;
} }

View File

@@ -285,7 +285,7 @@ export default function Page() {
<Button <Button
variant="outline" variant="outline"
onClick={() => { onClick={() => {
router.push("/admin/idp"); router.push(`/${params.orgId}/settings/idp`);
}} }}
> >
{t("idpSeeAll")} {t("idpSeeAll")}

View File

@@ -1,17 +1,10 @@
import { internal, priv } from "@app/lib/api"; import { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies"; import { authCookieHeader } from "@app/lib/api/cookies";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import IdpTable, { IdpRow } from "@app/components/private/OrgIdpTable"; import IdpTable, { IdpRow } from "@app/components/private/OrgIdpTable";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { Alert, AlertDescription } from "@app/components/ui/alert"; import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { cache } from "react";
import {
GetOrgSubscriptionResponse,
GetOrgTierResponse
} from "@server/routers/billing/types";
import { TierId } from "@server/lib/billing/tiers";
import { build } from "@server/build";
type OrgIdpPageProps = { type OrgIdpPageProps = {
params: Promise<{ orgId: string }>; params: Promise<{ orgId: string }>;
@@ -35,21 +28,6 @@ export default async function OrgIdpPage(props: OrgIdpPageProps) {
const t = await getTranslations(); const t = await getTranslations();
let subscriptionStatus: GetOrgTierResponse | null = null;
try {
const getSubscription = cache(() =>
priv.get<AxiosResponse<GetOrgTierResponse>>(
`/org/${params.orgId}/billing/tier`
)
);
const subRes = await getSubscription();
subscriptionStatus = subRes.data.data;
} catch {}
const subscribed =
build === "enterprise"
? true
: subscriptionStatus?.tier === TierId.STANDARD;
return ( return (
<> <>
<SettingsSectionTitle <SettingsSectionTitle
@@ -57,13 +35,7 @@ export default async function OrgIdpPage(props: OrgIdpPageProps) {
description={t("idpManageDescription")} description={t("idpManageDescription")}
/> />
{build === "saas" && !subscribed ? ( <PaidFeaturesAlert />
<Alert variant="info" className="mb-6">
<AlertDescription>
{t("idpDisabled")} {t("subscriptionRequiredToUse")}
</AlertDescription>
</Alert>
) : null}
<IdpTable idps={idps} orgId={params.orgId} /> <IdpTable idps={idps} orgId={params.orgId} />
</> </>

View File

@@ -338,7 +338,7 @@ function ProxyResourceTargetsForm({
<div <div
className={`flex items-center gap-2 ${status === "healthy" ? "text-green-500" : status === "unhealthy" ? "text-destructive" : ""}`} className={`flex items-center gap-2 ${status === "healthy" ? "text-green-500" : status === "unhealthy" ? "text-destructive" : ""}`}
> >
<Settings className="h-3 w-3" /> <Settings className="h-4 w-4 text-foreground" />
{getStatusText(status)} {getStatusText(status)}
</div> </div>
</Button> </Button>

View File

@@ -178,4 +178,16 @@ p {
.animate-dot-pulse { .animate-dot-pulse {
animation: dot-pulse 1.4s ease-in-out infinite; animation: dot-pulse 1.4s ease-in-out infinite;
} }
/* Use JavaScript-set viewport height for mobile to handle keyboard properly */
.h-screen-safe {
height: 100vh; /* Default for desktop and fallback */
}
/* Only apply custom viewport height on mobile */
@media (max-width: 767px) {
.h-screen-safe {
height: var(--vh, 100vh); /* Use CSS variable set by ViewportHeightFix on mobile */
}
}
} }

View File

@@ -22,6 +22,7 @@ import { TopLoader } from "@app/components/Toploader";
import Script from "next/script"; import Script from "next/script";
import { TanstackQueryProvider } from "@app/components/TanstackQueryProvider"; import { TanstackQueryProvider } from "@app/components/TanstackQueryProvider";
import { TailwindIndicator } from "@app/components/TailwindIndicator"; import { TailwindIndicator } from "@app/components/TailwindIndicator";
import { ViewportHeightFix } from "@app/components/ViewportHeightFix";
export const metadata: Metadata = { export const metadata: Metadata = {
title: `Dashboard - ${process.env.BRANDING_APP_NAME || "Pangolin"}`, title: `Dashboard - ${process.env.BRANDING_APP_NAME || "Pangolin"}`,
@@ -77,7 +78,7 @@ export default async function RootLayout({
return ( return (
<html suppressHydrationWarning lang={locale}> <html suppressHydrationWarning lang={locale}>
<body className={`${font.className} h-screen overflow-hidden`}> <body className={`${font.className} h-screen-safe overflow-hidden`}>
<TopLoader /> <TopLoader />
{build === "saas" && ( {build === "saas" && (
<Script <Script
@@ -86,6 +87,7 @@ export default async function RootLayout({
strategy="afterInteractive" strategy="afterInteractive"
/> />
)} )}
<ViewportHeightFix />
<NextIntlClientProvider> <NextIntlClientProvider>
<ThemeProvider <ThemeProvider
attribute="class" attribute="class"

View File

@@ -37,7 +37,7 @@ export async function Layout({
(sidebarStateCookie !== "expanded" && defaultSidebarCollapsed); (sidebarStateCookie !== "expanded" && defaultSidebarCollapsed);
return ( return (
<div className="flex h-screen overflow-hidden"> <div className="flex h-screen-safe overflow-hidden">
{/* Desktop Sidebar */} {/* Desktop Sidebar */}
{showSidebar && ( {showSidebar && (
<LayoutSidebar <LayoutSidebar
@@ -75,7 +75,7 @@ export async function Layout({
<div <div
className={cn( className={cn(
"container mx-auto max-w-12xl mb-12", "container mx-auto max-w-12xl mb-12",
showHeader && "pt-16 md:pt-16" // Add top padding on mobile and desktop to account for fixed header showHeader && "md:pt-16" // Add top padding only on desktop to account for fixed header
)} )}
> >
{children} {children}

View File

@@ -48,7 +48,7 @@ export function LayoutMobileMenu({
const t = useTranslations(); const t = useTranslations();
return ( return (
<div className="shrink-0 md:hidden fixed top-0 left-0 right-0 z-50 bg-card border-b border-border"> <div className="shrink-0 md:hidden sticky top-0 z-50">
<div className="h-16 flex items-center px-2"> <div className="h-16 flex items-center px-2">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{showSidebar && ( {showSidebar && (

View File

@@ -198,7 +198,7 @@ export default function ProxyResourcesTable({
if (!targets || targets.length === 0) { if (!targets || targets.length === 0) {
return ( return (
<div className="flex items-center gap-2"> <div id="LOOK_FOR_ME" className="flex items-center gap-2">
<StatusIcon status="unknown" /> <StatusIcon status="unknown" />
<span className="text-sm"> <span className="text-sm">
{t("resourcesTableNoTargets")} {t("resourcesTableNoTargets")}

View File

@@ -0,0 +1,79 @@
"use client";
import { useEffect } from "react";
/**
* Fixes mobile viewport height issues when keyboard opens/closes
* by setting a CSS variable with a stable viewport height
* Only applies on mobile devices (< 768px, matching Tailwind's md breakpoint)
*/
export function ViewportHeightFix() {
useEffect(() => {
// Check if we're on mobile (md breakpoint is typically 768px)
const isMobile = () => window.innerWidth < 768;
// On desktop, don't set --vh at all, let CSS use 100vh directly
if (!isMobile()) {
// Remove --vh if it was set, so CSS falls back to 100vh
document.documentElement.style.removeProperty("--vh");
return;
}
// Mobile-specific logic
let maxHeight = window.innerHeight;
let resizeTimer: NodeJS.Timeout;
// Set the viewport height as a CSS variable
const setViewportHeight = (height: number) => {
document.documentElement.style.setProperty("--vh", `${height}px`);
};
// Set initial value
setViewportHeight(maxHeight);
const handleResize = () => {
// If we switched to desktop, remove --vh and stop
if (!isMobile()) {
document.documentElement.style.removeProperty("--vh");
return;
}
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
const currentHeight = window.innerHeight;
// Track the maximum height we've seen (when keyboard is closed)
if (currentHeight > maxHeight) {
maxHeight = currentHeight;
setViewportHeight(maxHeight);
}
// If current height is close to max, update max (keyboard closed)
else if (currentHeight >= maxHeight * 0.9) {
maxHeight = currentHeight;
setViewportHeight(maxHeight);
}
// Otherwise, keep using the max height (keyboard is open)
}, 100);
};
const handleOrientationChange = () => {
// Reset on orientation change
setTimeout(() => {
maxHeight = window.innerHeight;
setViewportHeight(maxHeight);
}, 150);
};
window.addEventListener("resize", handleResize);
window.addEventListener("orientationchange", handleOrientationChange);
return () => {
window.removeEventListener("resize", handleResize);
window.removeEventListener("orientationchange", handleOrientationChange);
clearTimeout(resizeTimer);
};
}, []);
return null;
}

View File

@@ -73,35 +73,30 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
> >
{asChild ? ( {asChild ? (
props.children props.children
) : ( ) : loading ? (
<span className="relative inline-flex items-center justify-center"> <span className="relative inline-flex items-center justify-center">
<span <span className="inline-flex items-center justify-center opacity-0">
className={cn(
"inline-flex items-center justify-center",
loading && "opacity-0"
)}
>
{props.children} {props.children}
</span> </span>
{loading && ( <span className="absolute inset-0 flex items-center justify-center">
<span className="absolute inset-0 flex items-center justify-center"> <span className="flex items-center gap-1.5">
<span className="flex items-center gap-1"> <span
<span className="h-1 w-1 bg-current animate-dot-pulse"
className="h-1 w-1 bg-current animate-dot-pulse" style={{ animationDelay: "0ms" }}
style={{ animationDelay: "0ms" }} />
/> <span
<span className="h-1 w-1 bg-current animate-dot-pulse"
className="h-1 w-1 bg-current animate-dot-pulse" style={{ animationDelay: "200ms" }}
style={{ animationDelay: "200ms" }} />
/> <span
<span className="h-1 w-1 bg-current animate-dot-pulse"
className="h-1 w-1 bg-current animate-dot-pulse" style={{ animationDelay: "400ms" }}
style={{ animationDelay: "400ms" }} />
/>
</span>
</span> </span>
)} </span>
</span> </span>
) : (
props.children
)} )}
</Comp> </Comp>
); );

View File

@@ -14,13 +14,13 @@ const checkboxVariants = cva(
variants: { variants: {
variant: { variant: {
outlinePrimary: outlinePrimary:
"border rounded-[5px] border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground", "border rounded-[5px] border-input data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
outline: outline:
"border rounded-[5px] border-input data-[state=checked]:bg-muted data-[state=checked]:text-accent-foreground", "border rounded-[5px] border-input data-[state=checked]:border-primary data-[state=checked]:bg-muted data-[state=checked]:text-accent-foreground",
outlinePrimarySquare: outlinePrimarySquare:
"border rounded-[5px] border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground", "border rounded-[5px] border-input data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
outlineSquare: outlineSquare:
"border rounded-[5px] border-input data-[state=checked]:bg-muted data-[state=checked]:text-accent-foreground" "border rounded-[5px] border-input data-[state=checked]:border-primary data-[state=checked]:bg-muted data-[state=checked]:text-accent-foreground"
} }
}, },
defaultVariants: { defaultVariants: {
@@ -30,8 +30,7 @@ const checkboxVariants = cva(
); );
interface CheckboxProps interface CheckboxProps
extends extends React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>,
VariantProps<typeof checkboxVariants> {} VariantProps<typeof checkboxVariants> {}
const Checkbox = React.forwardRef< const Checkbox = React.forwardRef<
@@ -50,9 +49,8 @@ const Checkbox = React.forwardRef<
)); ));
Checkbox.displayName = CheckboxPrimitive.Root.displayName; Checkbox.displayName = CheckboxPrimitive.Root.displayName;
interface CheckboxWithLabelProps extends React.ComponentPropsWithoutRef< interface CheckboxWithLabelProps
typeof Checkbox extends React.ComponentPropsWithoutRef<typeof Checkbox> {
> {
label: string; label: string;
} }