mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-28 22:00:51 +00:00
ui enhancements
This commit is contained in:
committed by
Owen Schwartz
parent
284cccbe17
commit
c0c0d48edf
@@ -1480,7 +1480,7 @@
|
||||
"IAgreeToThe": "I agree to the",
|
||||
"termsOfService": "terms of service",
|
||||
"and": "and",
|
||||
"privacyPolicy": "privacy policy"
|
||||
"privacyPolicy": "privacy policy."
|
||||
},
|
||||
"signUpMarketing": {
|
||||
"keepMeInTheLoop": "Keep me in the loop with news, updates, and new features by email."
|
||||
|
||||
@@ -36,7 +36,7 @@ async function query(clientId?: number, niceId?: string, orgId?: string) {
|
||||
.select()
|
||||
.from(clients)
|
||||
.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);
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -285,7 +285,7 @@ export default function Page() {
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
router.push("/admin/idp");
|
||||
router.push(`/${params.orgId}/settings/idp`);
|
||||
}}
|
||||
>
|
||||
{t("idpSeeAll")}
|
||||
|
||||
@@ -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 { AxiosResponse } from "axios";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import IdpTable, { IdpRow } from "@app/components/private/OrgIdpTable";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||
import { cache } from "react";
|
||||
import {
|
||||
GetOrgSubscriptionResponse,
|
||||
GetOrgTierResponse
|
||||
} from "@server/routers/billing/types";
|
||||
import { TierId } from "@server/lib/billing/tiers";
|
||||
import { build } from "@server/build";
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
|
||||
type OrgIdpPageProps = {
|
||||
params: Promise<{ orgId: string }>;
|
||||
@@ -35,21 +28,6 @@ export default async function OrgIdpPage(props: OrgIdpPageProps) {
|
||||
|
||||
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 (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
@@ -57,13 +35,7 @@ export default async function OrgIdpPage(props: OrgIdpPageProps) {
|
||||
description={t("idpManageDescription")}
|
||||
/>
|
||||
|
||||
{build === "saas" && !subscribed ? (
|
||||
<Alert variant="info" className="mb-6">
|
||||
<AlertDescription>
|
||||
{t("idpDisabled")} {t("subscriptionRequiredToUse")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
<PaidFeaturesAlert />
|
||||
|
||||
<IdpTable idps={idps} orgId={params.orgId} />
|
||||
</>
|
||||
|
||||
@@ -338,7 +338,7 @@ function ProxyResourceTargetsForm({
|
||||
<div
|
||||
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)}
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
@@ -178,4 +178,16 @@ p {
|
||||
.animate-dot-pulse {
|
||||
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 */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { TopLoader } from "@app/components/Toploader";
|
||||
import Script from "next/script";
|
||||
import { TanstackQueryProvider } from "@app/components/TanstackQueryProvider";
|
||||
import { TailwindIndicator } from "@app/components/TailwindIndicator";
|
||||
import { ViewportHeightFix } from "@app/components/ViewportHeightFix";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Dashboard - ${process.env.BRANDING_APP_NAME || "Pangolin"}`,
|
||||
@@ -77,7 +78,7 @@ export default async function RootLayout({
|
||||
|
||||
return (
|
||||
<html suppressHydrationWarning lang={locale}>
|
||||
<body className={`${font.className} h-screen overflow-hidden`}>
|
||||
<body className={`${font.className} h-screen-safe overflow-hidden`}>
|
||||
<TopLoader />
|
||||
{build === "saas" && (
|
||||
<Script
|
||||
@@ -86,6 +87,7 @@ export default async function RootLayout({
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
)}
|
||||
<ViewportHeightFix />
|
||||
<NextIntlClientProvider>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
|
||||
@@ -37,7 +37,7 @@ export async function Layout({
|
||||
(sidebarStateCookie !== "expanded" && defaultSidebarCollapsed);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden">
|
||||
<div className="flex h-screen-safe overflow-hidden">
|
||||
{/* Desktop Sidebar */}
|
||||
{showSidebar && (
|
||||
<LayoutSidebar
|
||||
@@ -75,7 +75,7 @@ export async function Layout({
|
||||
<div
|
||||
className={cn(
|
||||
"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}
|
||||
|
||||
@@ -48,7 +48,7 @@ export function LayoutMobileMenu({
|
||||
const t = useTranslations();
|
||||
|
||||
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="flex items-center gap-4">
|
||||
{showSidebar && (
|
||||
|
||||
@@ -198,7 +198,7 @@ export default function ProxyResourcesTable({
|
||||
|
||||
if (!targets || targets.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div id="LOOK_FOR_ME" className="flex items-center gap-2">
|
||||
<StatusIcon status="unknown" />
|
||||
<span className="text-sm">
|
||||
{t("resourcesTableNoTargets")}
|
||||
|
||||
79
src/components/ViewportHeightFix.tsx
Normal file
79
src/components/ViewportHeightFix.tsx
Normal 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;
|
||||
}
|
||||
|
||||
@@ -73,35 +73,30 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
>
|
||||
{asChild ? (
|
||||
props.children
|
||||
) : (
|
||||
) : loading ? (
|
||||
<span className="relative inline-flex items-center justify-center">
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center",
|
||||
loading && "opacity-0"
|
||||
)}
|
||||
>
|
||||
<span className="inline-flex items-center justify-center opacity-0">
|
||||
{props.children}
|
||||
</span>
|
||||
{loading && (
|
||||
<span className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="flex items-center gap-1">
|
||||
<span
|
||||
className="h-1 w-1 bg-current animate-dot-pulse"
|
||||
style={{ animationDelay: "0ms" }}
|
||||
/>
|
||||
<span
|
||||
className="h-1 w-1 bg-current animate-dot-pulse"
|
||||
style={{ animationDelay: "200ms" }}
|
||||
/>
|
||||
<span
|
||||
className="h-1 w-1 bg-current animate-dot-pulse"
|
||||
style={{ animationDelay: "400ms" }}
|
||||
/>
|
||||
</span>
|
||||
<span className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span
|
||||
className="h-1 w-1 bg-current animate-dot-pulse"
|
||||
style={{ animationDelay: "0ms" }}
|
||||
/>
|
||||
<span
|
||||
className="h-1 w-1 bg-current animate-dot-pulse"
|
||||
style={{ animationDelay: "200ms" }}
|
||||
/>
|
||||
<span
|
||||
className="h-1 w-1 bg-current animate-dot-pulse"
|
||||
style={{ animationDelay: "400ms" }}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
props.children
|
||||
)}
|
||||
</Comp>
|
||||
);
|
||||
|
||||
@@ -14,13 +14,13 @@ const checkboxVariants = cva(
|
||||
variants: {
|
||||
variant: {
|
||||
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:
|
||||
"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:
|
||||
"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:
|
||||
"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: {
|
||||
@@ -30,8 +30,7 @@ const checkboxVariants = cva(
|
||||
);
|
||||
|
||||
interface CheckboxProps
|
||||
extends
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>,
|
||||
extends React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>,
|
||||
VariantProps<typeof checkboxVariants> {}
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
@@ -50,9 +49,8 @@ const Checkbox = React.forwardRef<
|
||||
));
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||
|
||||
interface CheckboxWithLabelProps extends React.ComponentPropsWithoutRef<
|
||||
typeof Checkbox
|
||||
> {
|
||||
interface CheckboxWithLabelProps
|
||||
extends React.ComponentPropsWithoutRef<typeof Checkbox> {
|
||||
label: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user