mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-01 00:06:38 +00:00
ui enhancements
This commit is contained in:
@@ -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."
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")}
|
||||||
|
|||||||
@@ -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} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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")}
|
||||||
|
|||||||
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 ? (
|
{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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user