chore(ui): improve changes

This commit is contained in:
pedrooot
2026-04-22 08:10:41 +02:00
parent 8795c10f94
commit 6d4edbe4e3
14 changed files with 281 additions and 50 deletions
+8
View File
@@ -2,6 +2,14 @@
All notable changes to the **Prowler UI** are documented in this file.
## [1.25.0] (Prowler UNRELEASED)
### 🚀 Added
- Sign-in and sign-up redesigned with an animated background and a live release highlights panel from GitHub, with adaptive summaries for patch releases [(#10774)](https://github.com/prowler-cloud/prowler/pull/10774)
---
## [1.24.1] (Prowler v5.24.1)
### 🔒 Security
+2
View File
@@ -0,0 +1,2 @@
export * from "./releases";
export * from "./types";
+125
View File
@@ -0,0 +1,125 @@
"use server";
import { unstable_cache } from "next/cache";
import { z } from "zod";
import type { LatestRelease, ReleaseComponent } from "./types";
import { RELEASE_COMPONENTS } from "./types";
const REPO = "prowler-cloud/prowler";
const REPO_URL = `https://github.com/${REPO}`;
const RELEASES_ENDPOINT = `https://api.github.com/repos/${REPO}/releases?per_page=10`;
const MAX_HIGHLIGHTS = 3;
const REVALIDATE_SECONDS = 3600;
const META_HEADINGS = [
/^what'?s changed$/i,
/^new contributors$/i,
/^full changelog$/i,
];
const releaseSchema = z.object({
tag_name: z.string(),
html_url: z.url(),
body: z.string().nullable(),
draft: z.boolean(),
prerelease: z.boolean(),
});
const releaseListSchema = z.array(releaseSchema);
const stripLeadingSymbols = (s: string) =>
s.replace(/^[^A-Za-z0-9]+/, "").trim();
const startsWithNonAscii = (s: string) => {
const first = s.trim().charCodeAt(0);
return Number.isFinite(first) && first > 127;
};
const stripFencedAndDetails = (body: string) =>
body
.replace(/```[\s\S]*?```/g, "")
.replace(/<details[\s\S]*?<\/details>/gi, "");
const parseBody = (
body: string,
): { curated: string[]; components: ReleaseComponent[] } => {
const clean = stripFencedAndDetails(body);
const headings = Array.from(clean.matchAll(/^##\s+(.+?)\s*$/gm)).map((m) =>
m[1].trim(),
);
const curated = headings
.filter(startsWithNonAscii)
.filter((h) => !META_HEADINGS.some((re) => re.test(stripLeadingSymbols(h))))
.slice(0, MAX_HIGHLIGHTS);
const components: ReleaseComponent[] = RELEASE_COMPONENTS.filter((comp) =>
headings.some((h) => h.toUpperCase() === comp),
);
return { curated, components };
};
const buildHeaders = (): HeadersInit => {
const headers: Record<string, string> = {
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
};
if (process.env.GITHUB_TOKEN) {
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
}
return headers;
};
const fetchLatestReleaseWithHighlights = async (): Promise<LatestRelease> => {
const res = await fetch(RELEASES_ENDPOINT, {
headers: buildHeaders(),
next: { revalidate: REVALIDATE_SECONDS },
});
if (!res.ok) {
throw new Error(`GitHub releases API responded with ${res.status}`);
}
const json = await res.json();
const releases = releaseListSchema.parse(json);
for (const release of releases) {
if (release.draft || release.prerelease || !release.body) continue;
const { curated, components } = parseBody(release.body);
const base = {
version: release.tag_name.replace(/^v/, ""),
url: release.html_url,
repoUrl: REPO_URL,
};
if (curated.length > 0) {
return { ...base, kind: "curated", highlights: curated };
}
if (components.length > 0) {
return { ...base, kind: "patch", components };
}
}
throw new Error("No releases with parseable highlights or components");
};
const getCachedLatestRelease = unstable_cache(
fetchLatestReleaseWithHighlights,
["github-latest-release", REPO],
{
revalidate: REVALIDATE_SECONDS,
tags: ["releases"],
},
);
export async function getLatestRelease(): Promise<LatestRelease | null> {
try {
return await getCachedLatestRelease();
} catch (err) {
console.error("[releases] failed to resolve latest release:", err);
return null;
}
}
+21
View File
@@ -0,0 +1,21 @@
export const RELEASE_COMPONENTS = ["UI", "API", "SDK"] as const;
export type ReleaseComponent = (typeof RELEASE_COMPONENTS)[number];
interface LatestReleaseBase {
version: string;
url: string;
repoUrl: string;
}
export interface CuratedRelease extends LatestReleaseBase {
kind: "curated";
highlights: string[];
}
export interface PatchRelease extends LatestReleaseBase {
kind: "patch";
components: ReleaseComponent[];
}
export type LatestRelease = CuratedRelease | PatchRelease;
@@ -1,4 +1,8 @@
import { Suspense } from "react";
import { AuthForm } from "@/components/auth/oss";
import { AuthReleaseHighlightsServer } from "@/components/auth/oss/auth-release-highlights-server";
import { AuthReleaseHighlightsSkeleton } from "@/components/auth/oss/auth-release-highlights-skeleton";
import {
getAuthUrl,
isGithubOAuthEnabled,
@@ -15,6 +19,14 @@ const SignIn = () => {
githubAuthUrl={GITHUB_AUTH_URL}
isGoogleOAuthEnabled={isGoogleOAuthEnabled}
isGithubOAuthEnabled={isGithubOAuthEnabled}
releaseHighlights={
<Suspense
key="auth-release-highlights"
fallback={<AuthReleaseHighlightsSkeleton />}
>
<AuthReleaseHighlightsServer />
</Suspense>
}
/>
);
};
+17 -2
View File
@@ -1,6 +1,13 @@
import { Suspense } from "react";
import { AuthForm } from "@/components/auth/oss";
import { getAuthUrl, isGithubOAuthEnabled } from "@/lib/helper";
import { isGoogleOAuthEnabled } from "@/lib/helper";
import { AuthReleaseHighlightsServer } from "@/components/auth/oss/auth-release-highlights-server";
import { AuthReleaseHighlightsSkeleton } from "@/components/auth/oss/auth-release-highlights-skeleton";
import {
getAuthUrl,
isGithubOAuthEnabled,
isGoogleOAuthEnabled,
} from "@/lib/helper";
import { SearchParamsProps } from "@/types";
const SignUp = async ({
@@ -25,6 +32,14 @@ const SignUp = async ({
githubAuthUrl={GITHUB_AUTH_URL}
isGoogleOAuthEnabled={isGoogleOAuthEnabled}
isGithubOAuthEnabled={isGithubOAuthEnabled}
releaseHighlights={
<Suspense
key="auth-release-highlights"
fallback={<AuthReleaseHighlightsSkeleton />}
>
<AuthReleaseHighlightsServer />
</Suspense>
}
/>
);
};
@@ -10,9 +10,9 @@ const PARTICLE_SPEED = 0.35;
const CONNECTION_DISTANCE = 140;
const CONNECTION_DISTANCE_SQ = CONNECTION_DISTANCE * CONNECTION_DISTANCE;
const LINE_WIDTH = 0.8;
const MOUSE_RADIUS = 160;
const MOUSE_RADIUS = 100;
const MOUSE_RADIUS_SQ = MOUSE_RADIUS * MOUSE_RADIUS;
const MOUSE_FORCE = 2.2;
const MOUSE_FORCE = 1.4;
const PARTICLE_ALPHA = 0.75;
const CONNECTION_MAX_ALPHA = 0.35;
const ALPHA_TIER_COUNT = 3;
+6
View File
@@ -1,3 +1,5 @@
import { ReactNode } from "react";
import { SignInForm } from "@/components/auth/oss/sign-in-form";
import { SignUpForm } from "@/components/auth/oss/sign-up-form";
@@ -8,6 +10,7 @@ export const AuthForm = ({
githubAuthUrl,
isGoogleOAuthEnabled,
isGithubOAuthEnabled,
releaseHighlights,
}: {
type: string;
invitationToken?: string | null;
@@ -15,6 +18,7 @@ export const AuthForm = ({
githubAuthUrl?: string;
isGoogleOAuthEnabled?: boolean;
isGithubOAuthEnabled?: boolean;
releaseHighlights?: ReactNode;
}) => {
if (type === "sign-in") {
return (
@@ -23,6 +27,7 @@ export const AuthForm = ({
githubAuthUrl={githubAuthUrl}
isGoogleOAuthEnabled={isGoogleOAuthEnabled}
isGithubOAuthEnabled={isGithubOAuthEnabled}
releaseHighlights={releaseHighlights}
/>
);
}
@@ -34,6 +39,7 @@ export const AuthForm = ({
githubAuthUrl={githubAuthUrl}
isGoogleOAuthEnabled={isGoogleOAuthEnabled}
isGithubOAuthEnabled={isGithubOAuthEnabled}
releaseHighlights={releaseHighlights}
/>
);
};
+9 -5
View File
@@ -2,24 +2,28 @@ import { ReactNode } from "react";
import { AnimatedDotsBackground } from "@/components/auth/oss/animated-dots-background";
import { AuthCard } from "@/components/auth/oss/auth-card";
import { AuthReleaseHighlights } from "@/components/auth/oss/auth-release-highlights";
import { ProwlerExtended } from "@/components/icons";
import { ThemeSwitch } from "@/components/ThemeSwitch";
interface AuthLayoutProps {
title: string;
children: ReactNode;
releaseHighlights?: ReactNode;
}
export const AuthLayout = ({ title, children }: AuthLayoutProps) => (
export const AuthLayout = ({
title,
children,
releaseHighlights,
}: AuthLayoutProps) => (
<main className="relative min-h-screen w-full overflow-hidden">
<AnimatedDotsBackground />
<div className="relative z-10 mx-auto flex min-h-screen w-full max-w-6xl items-center justify-center px-6 py-10 sm:px-10">
<div className="flex w-full items-center justify-center gap-10 lg:justify-between xl:gap-16">
<div className="flex w-full items-center justify-center gap-10 lg:justify-center xl:gap-16">
<div className="flex w-full max-w-sm flex-col">
<div className="mb-6 flex w-full justify-center">
<ProwlerExtended width={300} className="h-auto w-55 max-w-full" />
<ProwlerExtended width={300} className="h-auto w-56 max-w-full" />
</div>
<AuthCard className="gap-4 px-7 py-9">
@@ -32,7 +36,7 @@ export const AuthLayout = ({ title, children }: AuthLayoutProps) => (
</AuthCard>
</div>
<AuthReleaseHighlights />
{releaseHighlights}
</div>
</div>
</main>
@@ -0,0 +1,8 @@
import { getLatestRelease } from "@/actions/releases/releases";
import { AuthReleaseHighlights } from "@/components/auth/oss/auth-release-highlights";
export const AuthReleaseHighlightsServer = async () => {
const release = await getLatestRelease();
if (!release) return null;
return <AuthReleaseHighlights release={release} />;
};
@@ -0,0 +1,5 @@
export const AuthReleaseHighlightsSkeleton = () => (
<aside aria-hidden="true" className="hidden w-full max-w-md lg:block">
<div className="relative min-h-[380px] animate-pulse overflow-hidden rounded-3xl border border-black/8 bg-white/60 shadow-2xl ring-1 shadow-slate-300/35 ring-white/55 backdrop-blur-2xl dark:border-white/10 dark:bg-black/60 dark:shadow-black/45 dark:ring-white/6" />
</aside>
);
@@ -1,26 +1,35 @@
import { Icon } from "@iconify/react";
import type { LatestRelease, ReleaseComponent } from "@/actions/releases/types";
import { ProwlerShort } from "@/components/icons";
import { Button } from "@/components/shadcn";
const RELEASE = {
version: "5.24.0",
url: "https://github.com/prowler-cloud/prowler/releases/tag/5.24.0",
repoUrl: "https://github.com/prowler-cloud/prowler",
} as const;
interface AuthReleaseHighlightsProps {
release: LatestRelease;
}
const HIGHLIGHTS: readonly string[] = [
"Redesigned resources side drawer",
"New AWS Bedrock & IAM hardening checks",
"New Microsoft 365 Conditional Access coverage",
];
const formatPatchMessage = (components: ReleaseComponent[]): string => {
const [a, b, c] = components;
if (!a) return "";
if (!b) return `This version contains multiple fixes on the ${a} component.`;
if (!c) {
return `This version contains multiple fixes across ${a} and ${b} components.`;
}
return `This version contains multiple fixes across ${a}, ${b} and ${c} components.`;
};
export const AuthReleaseHighlights = () => (
export const AuthReleaseHighlights = ({
release,
}: AuthReleaseHighlightsProps) => (
<aside
aria-label={`Prowler v${RELEASE.version} highlights`}
aria-label={
release.kind === "curated"
? `Prowler v${release.version} release highlights`
: `Prowler v${release.version} patch summary`
}
className="hidden w-full max-w-md lg:block"
>
<div className="relative overflow-hidden rounded-3xl border border-black/8 bg-gradient-to-br from-white/78 via-white/70 to-white/58 p-8 shadow-2xl ring-1 shadow-slate-300/35 ring-white/55 backdrop-blur-2xl dark:border-white/10 dark:from-black/70 dark:via-black/62 dark:to-black/52 dark:shadow-black/45 dark:ring-white/6">
<div className="relative min-h-[380px] overflow-hidden rounded-3xl border border-black/8 bg-gradient-to-br from-white/78 via-white/70 to-white/58 p-8 shadow-2xl ring-1 shadow-slate-300/35 ring-white/55 backdrop-blur-2xl dark:border-white/10 dark:from-black/82 dark:via-black/74 dark:to-black/68 dark:shadow-black/45 dark:ring-white/6">
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 bg-gradient-to-br from-white/55 via-white/10 to-transparent dark:from-white/8 dark:via-transparent"
@@ -39,37 +48,48 @@ export const AuthReleaseHighlights = () => (
aria-hidden="true"
className="flex h-10 w-10 items-center justify-center rounded-xl border border-black/8 bg-emerald-400/18 shadow-lg shadow-emerald-400/15 dark:border-white/10 dark:bg-emerald-400/15 dark:shadow-emerald-950/30"
>
<ProwlerShort width={18} className="text-emerald-400" />
<ProwlerShort
width={18}
className="text-emerald-700 dark:text-emerald-300"
/>
</div>
<div>
<p className="text-xs font-semibold tracking-wide text-emerald-700 uppercase dark:text-emerald-300">
Prowler v{RELEASE.version}
<p className="text-xs font-semibold tracking-wide text-emerald-700 uppercase dark:text-emerald-200">
Prowler v{release.version}
</p>
<h2 className="mt-1 text-2xl leading-tight font-semibold text-slate-950 dark:text-white dark:drop-shadow-sm">
Fresh off the branch
</h2>
<p className="mt-2 max-w-sm text-sm text-slate-600 dark:text-white/72">
A quick look at what we just shipped in v{RELEASE.version}.
<p className="mt-2 max-w-sm text-sm text-slate-600 dark:text-white/85">
A quick look at what we just shipped in v{release.version}.
</p>
</div>
<ul className="flex flex-col gap-3">
{HIGHLIGHTS.map((label) => (
<li
key={label}
className="flex items-start gap-3 text-slate-700 dark:text-white/90"
>
<Icon
aria-hidden="true"
icon="mdi:check-circle"
width={20}
className="mt-0.5 shrink-0 text-emerald-600 drop-shadow-sm dark:text-emerald-300"
/>
<span className="text-sm leading-6">{label}</span>
</li>
))}
</ul>
{release.kind === "curated" && (
<ul className="flex flex-col gap-3">
{release.highlights.map((label) => (
<li
key={label}
className="flex items-start gap-3 text-slate-700 dark:text-white/90"
>
<Icon
aria-hidden="true"
icon="mdi:check-circle"
width={20}
className="mt-0.5 shrink-0 text-emerald-600 drop-shadow-sm dark:text-emerald-300"
/>
<span className="text-sm leading-6">{label}</span>
</li>
))}
</ul>
)}
{release.kind === "patch" && (
<p className="text-sm leading-6 text-slate-700 dark:text-white/90">
{formatPatchMessage(release.components)}
</p>
)}
<div className="flex flex-wrap items-center gap-4">
<Button
@@ -78,19 +98,19 @@ export const AuthReleaseHighlights = () => (
size="sm"
className="border-black/8 bg-white/45 text-slate-900 hover:bg-white/70 dark:border-white/12 dark:bg-white/6 dark:text-white dark:hover:bg-white/10"
>
<a href={RELEASE.repoUrl} target="_blank" rel="noopener noreferrer">
<a href={release.repoUrl} target="_blank" rel="noopener noreferrer">
<Icon aria-hidden="true" icon="mdi:github" width={16} />
GitHub
</a>
</Button>
<a
href={RELEASE.url}
href={release.url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-sm font-medium text-emerald-700 transition-colors hover:text-emerald-800 dark:text-emerald-300 dark:hover:text-emerald-200"
className="inline-flex items-center gap-1 rounded-sm text-sm font-medium text-emerald-700 transition-colors hover:text-emerald-800 focus-visible:ring-2 focus-visible:ring-emerald-400/60 focus-visible:outline-none dark:text-emerald-200 dark:hover:text-emerald-100"
>
See full release notes
<Icon aria-hidden="true" icon="mdi:arrow-top-right" width={14} />
<Icon aria-hidden="true" icon="mdi:arrow-top-right" width={16} />
</a>
</div>
</div>
+4 -2
View File
@@ -3,7 +3,7 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Icon } from "@iconify/react";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect } from "react";
import { ReactNode, useEffect } from "react";
import { useForm } from "react-hook-form";
import { authenticate } from "@/actions/auth";
@@ -23,11 +23,13 @@ export const SignInForm = ({
githubAuthUrl,
isGoogleOAuthEnabled,
isGithubOAuthEnabled,
releaseHighlights,
}: {
googleAuthUrl?: string;
githubAuthUrl?: string;
isGoogleOAuthEnabled?: boolean;
isGithubOAuthEnabled?: boolean;
releaseHighlights?: ReactNode;
}) => {
const router = useRouter();
const searchParams = useSearchParams();
@@ -143,7 +145,7 @@ export const SignInForm = ({
const title = isSamlMode ? "Sign in with SAML SSO" : "Sign in";
return (
<AuthLayout title={title}>
<AuthLayout title={title} releaseHighlights={releaseHighlights}>
<Form {...form}>
<form
noValidate
+4 -1
View File
@@ -3,6 +3,7 @@
import { Checkbox } from "@heroui/checkbox";
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/navigation";
import { ReactNode } from "react";
import { useForm, useWatch } from "react-hook-form";
import { createNewUser } from "@/actions/auth";
@@ -37,12 +38,14 @@ export const SignUpForm = ({
githubAuthUrl,
isGoogleOAuthEnabled,
isGithubOAuthEnabled,
releaseHighlights,
}: {
invitationToken?: string | null;
googleAuthUrl?: string;
githubAuthUrl?: string;
isGoogleOAuthEnabled?: boolean;
isGithubOAuthEnabled?: boolean;
releaseHighlights?: ReactNode;
}) => {
const router = useRouter();
const { toast } = useToast();
@@ -126,7 +129,7 @@ export const SignUpForm = ({
};
return (
<AuthLayout title="Sign up">
<AuthLayout title="Sign up" releaseHighlights={releaseHighlights}>
<Form {...form}>
<form
noValidate