refactor(v5): rename Home to Dashboard and add coollabs brand styles

Rename HomeController, Home page, and V5HomeProps type to Dashboard
equivalents. Update route name from 'home' to 'dashboard'.

Add coollabs brand color tokens to CSS theme and a 'coolify' button
variant using those colors. Add Sheet UI component for slide-over panels.
Update navbar with active-route detection and Sheet-based mobile drawer.
Switch cluster action buttons to use the new 'coolify' variant.
This commit is contained in:
Andras Bacsai
2026-06-16 22:23:50 +02:00
parent 4c41817cda
commit 5d57b131ff
10 changed files with 235 additions and 46 deletions
@@ -16,7 +16,7 @@ use Illuminate\Validation\Rule;
use Inertia\Inertia;
use Inertia\Response;
class HomeController extends Controller
class DashboardController extends Controller
{
private const SELECTED_PROJECT_SESSION_KEY = 'v5.selectedProjectUuid';
@@ -28,7 +28,7 @@ class HomeController extends Controller
$projects = $this->projects($currentTeam);
[$selectedProject, $selectedEnvironment] = $this->selectedProjectAndEnvironment($request, $projects);
return Inertia::render('Home', [
return Inertia::render('Dashboard', [
'flux' => $fluxHealth->check(),
'projects' => $projects,
'selectedProjectUuid' => $selectedProject['uuid'] ?? null,
+7 -2
View File
@@ -29,6 +29,11 @@
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-warning: var(--warning);
--color-coollabs-50: #f5f0ff;
--color-coollabs: #6b16ed;
--color-coollabs-100: #7317ff;
--color-coollabs-200: #5a12c7;
--color-coollabs-300: #4a0fa3;
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
@@ -76,7 +81,7 @@
--warning: #fcd452;
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.015 286.067);
--ring: var(--warning);
--chart-1: oklch(0.871 0.006 286.286);
--chart-2: oklch(0.552 0.016 285.938);
--chart-3: oklch(0.442 0.017 285.786);
@@ -112,7 +117,7 @@
--warning: #fcd452;
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.552 0.016 285.938);
--ring: var(--warning);
--chart-1: oklch(0.871 0.006 286.286);
--chart-2: oklch(0.552 0.016 285.938);
--chart-3: oklch(0.442 0.017 285.786);
+4 -4
View File
@@ -14,7 +14,7 @@ import {
DialogTitle,
} from '@/components/ui/dialog';
import { csrfToken } from '@/lib/csrf';
import type { V5Cluster, V5HomeProps } from '@/types';
import type { V5Cluster, V5DashboardProps } from '@/types';
type ClusterFormErrors = {
name?: string[];
@@ -50,7 +50,7 @@ export default function Clusters({
projects = [],
selectedProjectUuid = null,
selectedEnvironmentUuid = null,
}: V5HomeProps) {
}: V5DashboardProps) {
const [clusterList, setClusterList] = useState<V5Cluster[]>(clusters);
const [selectedClusterId, setSelectedClusterId] = useState<string>(clusters[0]?.id ?? '');
const [name, setName] = useState('');
@@ -134,7 +134,7 @@ export default function Clusters({
</div>
<Button
type="button"
variant="outline"
variant="coolify"
size="sm"
aria-label="Create cluster"
onClick={() => setIsCreateDialogOpen(true)}
@@ -323,7 +323,7 @@ export default function Clusters({
<DialogFooter>
<DialogClose render={<Button type="button" variant="outline" />}>Cancel</DialogClose>
<Button type="submit" disabled={isSubmitting}>
<Button type="submit" variant="coolify" disabled={isSubmitting}>
{isSubmitting ? 'Creating...' : 'Create cluster'}
</Button>
</DialogFooter>
@@ -1,17 +1,17 @@
import { Head } from '@inertiajs/react';
import { AppNavbar } from '@/components/app-navbar';
import type { V5HomeProps } from '@/types';
import type { V5DashboardProps } from '@/types';
export default function Home({
export default function Dashboard({
flux,
projects = [],
selectedProjectUuid = null,
selectedEnvironmentUuid = null,
}: V5HomeProps) {
}: V5DashboardProps) {
return (
<>
<Head title="Magic" />
<Head title="Dashboard" />
<div className="h-dvh overflow-hidden bg-background text-foreground">
<AppNavbar
@@ -23,7 +23,7 @@ export default function Home({
<main className="flex h-full min-h-0 items-center justify-center overflow-hidden px-6 pt-16">
<section className="flex w-full max-w-5xl flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-border bg-card px-8 py-24 text-center">
<p className="text-sm font-medium text-foreground">Magic</p>
<p className="text-sm font-medium text-foreground">Dashboard</p>
<p className="max-w-md text-sm text-muted-foreground">This is where the magic happens.</p>
</section>
</main>
+57 -9
View File
@@ -1,9 +1,11 @@
import { Link } from '@inertiajs/react';
import { Link, usePage } from '@inertiajs/react';
import { useMemo, useState } from 'react';
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Sheet, SheetClose, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
import { csrfToken } from '@/lib/csrf';
import type { SelectItemOption, V5HomeProps, V5Project } from '@/types';
import { cn } from '@/lib/utils';
import type { SelectItemOption, V5DashboardProps, V5Project } from '@/types';
function persistSelection(projectUuid: string, environmentUuid: string): void {
void fetch('/v5/selection', {
@@ -21,7 +23,7 @@ function persistSelection(projectUuid: string, environmentUuid: string): void {
});
}
type AppNavbarProps = V5HomeProps;
type AppNavbarProps = V5DashboardProps;
export function AppNavbar({
flux,
@@ -30,6 +32,7 @@ export function AppNavbar({
selectedProjectUuid = null,
selectedEnvironmentUuid = null,
}: AppNavbarProps) {
const { url } = usePage();
const firstProject = projects[0] ?? null;
const [projectUuid, setProjectUuid] = useState<string>(selectedProjectUuid ?? firstProject?.uuid ?? '');
const selectedProject = useMemo<V5Project | null>(
@@ -73,26 +76,27 @@ export function AppNavbar({
label: environment.name,
value: environment.uuid,
}));
const isClustersPage = url.startsWith('/v5/clusters');
return (
<header className="fixed inset-x-0 top-0 z-40 border-b border-border bg-background">
<nav className="flex h-16 items-center gap-4 px-6" aria-label="Main navigation">
<nav className="relative flex h-16 items-center gap-3 px-4 sm:px-6" aria-label="Main navigation">
<Link
href="/v5"
className="flex shrink-0 items-center rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
aria-label="Coolify home"
aria-label="Coolify dashboard"
>
<img src="/coolify-logo.svg" alt="Coolify" className="size-8" />
</Link>
<div className="flex min-w-0 items-center gap-2">
<div className="absolute left-1/2 flex min-w-0 -translate-x-1/2 items-center justify-center gap-1 md:static md:flex-1 md:translate-x-0 md:justify-start md:gap-2">
<Select
items={projectItems}
value={selectedProject?.uuid ?? ''}
onValueChange={selectProject}
disabled={projects.length === 0}
>
<SelectTrigger aria-label="Select a project" variant="ghost" className="max-w-[10rem]">
<SelectTrigger aria-label="Select a project" variant="ghost" className="max-w-[38vw] md:max-w-[10rem]">
<SelectValue placeholder="Select a project" />
</SelectTrigger>
<SelectContent position="popper" align="start" sideOffset={4}>
@@ -114,7 +118,7 @@ export function AppNavbar({
onValueChange={selectEnvironment}
disabled={!selectedProject || (selectedProject.environments ?? []).length === 0}
>
<SelectTrigger aria-label="Select an environment" variant="ghost" className="max-w-[10rem]">
<SelectTrigger aria-label="Select an environment" variant="ghost" className="max-w-[30vw] md:max-w-[10rem]">
<SelectValue placeholder="Select an environment" />
</SelectTrigger>
<SelectContent position="popper" align="start" sideOffset={4}>
@@ -132,7 +136,7 @@ export function AppNavbar({
<div className="ml-auto flex items-center gap-3">
<Link
href="/v5/clusters"
className="rounded-md px-3 py-1 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
className="hidden rounded-md px-3 py-1 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground md:inline-flex"
>
Clusters
</Link>
@@ -143,6 +147,50 @@ export function AppNavbar({
>
Flux: {flux?.label ?? 'Unknown'} · {clusters.length} clusters
</div>
<Sheet>
<SheetTrigger
className="inline-flex rounded-md p-2 text-warning transition-colors hover:bg-muted hover:text-warning md:hidden"
aria-label="Open mobile menu"
>
<svg xmlns="http://www.w3.org/2000/svg" className="size-6" viewBox="0 0 24 24" aria-hidden="true">
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
</SheetTrigger>
<SheetContent side="right" className="w-72 max-w-[85vw] bg-background">
<SheetHeader>
<SheetTitle>Coolify</SheetTitle>
<SheetDescription className="sr-only">Move between Coolify v5 pages.</SheetDescription>
</SheetHeader>
<nav className="flex flex-col gap-1 px-4" aria-label="Mobile navigation">
<SheetClose
render={<Link href="/v5" />}
className={cn(
'rounded-md px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-warning',
!isClustersPage && 'bg-accent text-accent-foreground',
)}
>
Dashboard
</SheetClose>
<SheetClose
render={<Link href="/v5/clusters" />}
className={cn(
'rounded-md px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-warning',
isClustersPage && 'bg-accent text-accent-foreground',
)}
>
Clusters
</SheetClose>
</nav>
</SheetContent>
</Sheet>
</div>
</nav>
</header>
+2
View File
@@ -10,6 +10,8 @@ const buttonVariants = cva(
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/80',
coolify:
'border-coollabs bg-coollabs-50 text-coollabs-200 hover:bg-coollabs hover:text-white dark:border-coollabs-100 dark:bg-coollabs/20 dark:text-white dark:hover:bg-coollabs-100 dark:hover:text-white',
outline:
'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
secondary:
+87
View File
@@ -0,0 +1,87 @@
import { Dialog as SheetPrimitive } from '@base-ui/react/dialog';
import { XIcon } from '@phosphor-icons/react';
import type * as React from 'react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
function Sheet({ ...props }: SheetPrimitive.Root.Props) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
}
function SheetTrigger({ ...props }: SheetPrimitive.Trigger.Props) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
}
function SheetClose({ ...props }: SheetPrimitive.Close.Props) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
}
function SheetPortal({ ...props }: SheetPrimitive.Portal.Props) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
}
function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
return (
<SheetPrimitive.Backdrop
data-slot="sheet-overlay"
className={cn(
'fixed inset-0 z-50 bg-black/10 text-xs/relaxed transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs',
className,
)}
{...props}
/>
);
}
function SheetContent({
className,
children,
side = 'right',
showCloseButton = true,
...props
}: SheetPrimitive.Popup.Props & {
side?: 'top' | 'right' | 'bottom' | 'left';
showCloseButton?: boolean;
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Popup
data-slot="sheet-content"
data-side={side}
className={cn(
'fixed z-50 flex flex-col bg-popover bg-clip-padding text-xs/relaxed text-popover-foreground shadow-lg transition duration-200 ease-in-out data-ending-style:opacity-0 data-starting-style:opacity-0 data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=bottom]:data-ending-style:translate-y-[2.5rem] data-[side=bottom]:data-starting-style:translate-y-[2.5rem] data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=left]:data-ending-style:translate-x-[-2.5rem] data-[side=left]:data-starting-style:translate-x-[-2.5rem] data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=right]:data-ending-style:translate-x-[2.5rem] data-[side=right]:data-starting-style:translate-x-[2.5rem] data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=top]:data-ending-style:translate-y-[-2.5rem] data-[side=top]:data-starting-style:translate-y-[-2.5rem] data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm',
className,
)}
{...props}
>
{children}
{showCloseButton && (
<SheetPrimitive.Close data-slot="sheet-close" render={<Button variant="ghost" className="absolute top-3 right-3" size="icon-sm" />}>
<XIcon />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
)}
</SheetPrimitive.Popup>
</SheetPortal>
);
}
function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) {
return <div data-slot="sheet-header" className={cn('flex flex-col gap-0.5 p-4', className)} {...props} />;
}
function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) {
return <div data-slot="sheet-footer" className={cn('mt-auto flex flex-col gap-2 p-4', className)} {...props} />;
}
function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
return <SheetPrimitive.Title data-slot="sheet-title" className={cn('font-heading text-sm font-medium text-foreground', className)} {...props} />;
}
function SheetDescription({ className, ...props }: SheetPrimitive.Description.Props) {
return <SheetPrimitive.Description data-slot="sheet-description" className={cn('text-xs/relaxed text-muted-foreground', className)} {...props} />;
}
export { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger };
+1 -1
View File
@@ -38,7 +38,7 @@ export type V5Project = {
environments: V5Environment[];
};
export type V5HomeProps = {
export type V5DashboardProps = {
flux: FluxStatus | null;
clusters?: V5Cluster[];
projects?: V5Project[];
+5 -5
View File
@@ -1,11 +1,11 @@
<?php
use App\Http\Controllers\V5\HomeController;
use App\Http\Controllers\V5\DashboardController;
use Illuminate\Support\Facades\Route;
Route::middleware('v5.authenticated')->group(function () {
Route::get('/', HomeController::class)->name('home');
Route::post('/selection', [HomeController::class, 'updateSelection'])->name('selection.update');
Route::get('/clusters', [HomeController::class, 'clustersIndex'])->name('clusters.index');
Route::post('/clusters', [HomeController::class, 'storeCluster'])->name('clusters.store');
Route::get('/', DashboardController::class)->name('dashboard');
Route::post('/selection', [DashboardController::class, 'updateSelection'])->name('selection.update');
Route::get('/clusters', [DashboardController::class, 'clustersIndex'])->name('clusters.index');
Route::post('/clusters', [DashboardController::class, 'storeCluster'])->name('clusters.store');
});
@@ -32,8 +32,9 @@ beforeEach(function () {
Schema::dropIfExists('users');
});
it('registers the v5 home route', function () {
expect(Route::has('v5.home'))->toBeTrue()
it('registers the v5 dashboard route', function () {
expect(Route::has('v5.dashboard'))->toBeTrue()
->and(Route::has('v5.home'))->toBeFalse()
->and(Route::has('v5.selection.update'))->toBeTrue()
->and(Route::has('v5.clusters.index'))->toBeTrue()
->and(Route::has('v5.clusters.store'))->toBeTrue()
@@ -174,7 +175,8 @@ it('serves the v5 inertia shell', function () {
->assertSee('coolify-logo-dev-transparent.png', false)
->assertDontSee('coolify-logo.svg', false)
->assertSee('v5-app', false)
->assertSee('Home', false)
->assertSee('Dashboard', false)
->assertDontSee('Home', false)
->assertDontSee('v5-ready', false)
->assertDontSee('This page is served from Laravel through Inertia and React')
->assertDontSee('Bootstrap server')
@@ -369,7 +371,7 @@ it('allows the same v5 cluster name in another team without leaking it', functio
->assertDontSee('Other team cluster.');
});
it('shares existing projects and environments with the v5 home page', function () {
it('shares existing projects and environments with the v5 dashboard page', function () {
$this->withoutVite();
fakeFluxHealth();
createSharedUserAndTeamTables();
@@ -458,25 +460,28 @@ it('rejects persisted v5 selections outside the current team', function () {
->assertSessionMissing('v5.selectedEnvironmentUuid');
});
it('defines the v5 home page as a shadcn styled canvas shell', function () {
$homePage = file_get_contents(resource_path('js/v5/Pages/Home.tsx'));
it('defines the v5 dashboard page as a shadcn styled canvas shell', function () {
$dashboardPage = file_get_contents(resource_path('js/v5/Pages/Dashboard.tsx'));
$app = file_get_contents(resource_path('js/v5/app.tsx'));
$navbarPath = resource_path('js/v5/components/app-navbar.tsx');
expect(file_exists($navbarPath))->toBeTrue();
$navbar = file_get_contents($navbarPath);
$sheetPath = resource_path('js/v5/components/ui/sheet.tsx');
expect(file_exists($sheetPath))->toBeTrue();
expect($app)
->toContain('progress: {')
->toContain('delay: 250')
->toContain('delay: 10')
->toContain("color: '#fcd452'")
->toContain('showSpinner: false')
->not->toContain('TopNavigationLoadingIndicator')
->not->toContain('withApp:');
expect($homePage)
->toContain('Magic')
expect($dashboardPage)
->toContain('Dashboard')
->toContain("import { AppNavbar } from '@/components/app-navbar';")
->not->toContain('function csrfToken()')
->not->toContain("import { csrfToken } from '@/lib/csrf';")
@@ -494,7 +499,9 @@ it('defines the v5 home page as a shadcn styled canvas shell', function () {
->not->toContain("fetch('/v5/selection'");
expect($navbar)
->toContain("import { Link } from '@inertiajs/react';")
->toContain("import { Link, usePage } from '@inertiajs/react';")
->toContain("import { cn } from '@/lib/utils';")
->toContain("import { Sheet, SheetClose, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';")
->toContain("import { csrfToken } from '@/lib/csrf';")
->not->toContain('function csrfToken()')
->toContain('export function AppNavbar')
@@ -506,16 +513,44 @@ it('defines the v5 home page as a shadcn styled canvas shell', function () {
->toContain('text-muted-foreground')
->toContain('SelectGroup')
->toContain('variant="ghost"')
->toContain('className="max-w-[10rem]"')
->toContain('position="popper"')
->toContain('sideOffset={4}')
->toContain('Select a project')
->toContain('Select an environment')
->toContain('const { url } = usePage();')
->toContain("const isClustersPage = url.startsWith('/v5/clusters');")
->toContain('href="/v5"')
->toContain('href="/v5/clusters"')
->toContain('Clusters')
->toContain('className="relative flex h-16 items-center gap-3 px-4 sm:px-6"')
->toContain('className="absolute left-1/2 flex min-w-0 -translate-x-1/2 items-center justify-center gap-1 md:static md:flex-1 md:translate-x-0 md:justify-start md:gap-2"')
->toContain('className="max-w-[38vw] md:max-w-[10rem]"')
->toContain('className="max-w-[30vw] md:max-w-[10rem]"')
->toContain('aria-label="Open mobile menu"')
->toContain('<Sheet>')
->toContain('<SheetTrigger')
->toContain('<SheetContent side="right" className="w-72 max-w-[85vw] bg-background"')
->toContain('<SheetHeader>')
->toContain('<SheetTitle>Coolify</SheetTitle>')
->toContain('<SheetDescription className="sr-only">')
->toContain('<SheetClose')
->toContain('Move between Coolify v5 pages.')
->not->toContain('<SheetTitle className="sr-only">Navigation</SheetTitle>')
->toContain('className="hidden rounded-md px-3 py-1 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground md:inline-flex"')
->toContain('className="inline-flex rounded-md p-2 text-warning transition-colors hover:bg-muted hover:text-warning md:hidden"')
->toContain('Dashboard')
->not->toContain('Home')
->toContain("fetch('/v5/selection'")
->toContain("'X-CSRF-TOKEN': csrfToken()")
->not->toContain('isMobileMenuOpen')
->not->toContain('setIsMobileMenuOpen')
->not->toContain('isProjectEnvironmentMenuOpen')
->not->toContain('setIsProjectEnvironmentMenuOpen')
->not->toContain('Open project and environment selector')
->not->toContain('Close project and environment selector')
->not->toContain('DropdownMenu')
->not->toContain('fixed inset-0 bg-black/80')
->not->toContain('fixed inset-y-0 right-0')
->not->toContain('<a')
->not->toContain("import { Button } from '@/components/ui/button';")
->not->toContain('<Button')
@@ -526,9 +561,7 @@ it('defines the v5 home page as a shadcn styled canvas shell', function () {
->not->toContain('bg-background/95')
->not->toContain('backdrop-blur')
->not->toContain('supports-[backdrop-filter]')
->not->toContain('bg-coolgray')
->not->toContain('border-coolgray')
->not->toContain('text-warning')
->not->toContain('className="w-[12rem]"')
->not->toContain('<h1>Coolify v5</h1>')
->not->toContain('<h2 id="clusters-heading">Clusters</h2>');
@@ -539,6 +572,7 @@ it('defines the v5 home page as a shadcn styled canvas shell', function () {
it('defines the v5 cluster management page and create cluster form', function () {
$clustersPagePath = resource_path('js/v5/Pages/Clusters.tsx');
$clustersPage = file_get_contents($clustersPagePath);
$buttonComponent = file_get_contents(resource_path('js/v5/components/ui/button.tsx'));
$types = file_get_contents(resource_path('js/v5/types.ts'));
expect(file_exists($clustersPagePath))->toBeTrue();
@@ -551,6 +585,7 @@ it('defines the v5 cluster management page and create cluster form', function ()
->toContain('aria-label="Create cluster"')
->toContain('setIsCreateDialogOpen(true)')
->toContain('Add cluster')
->toContain('variant="coolify"')
->toContain('border-warning bg-warning/10 text-foreground')
->not->toContain('border-primary bg-primary/10 text-foreground')
->toContain('<Dialog')
@@ -571,14 +606,26 @@ it('defines the v5 cluster management page and create cluster form', function ()
->not->toContain('<aside className="rounded-lg border border-border bg-card p-5">')
->not->toContain('This is where the magic happens.');
expect(substr_count($clustersPage, 'variant="coolify"'))->toBe(2)
->and($buttonComponent)
->toContain('coolify:')
->toContain('bg-coollabs-50')
->toContain('hover:bg-coollabs');
expect(file_get_contents(resource_path('js/v5/components/ui/dialog.tsx')))
->toContain('@base-ui/react/dialog')
->toContain('DialogTitle')
->toContain('DialogDescription');
expect(file_get_contents(resource_path('css/v5/app.css')))
$v5Css = file_get_contents(resource_path('css/v5/app.css'));
expect($v5Css)
->toContain('--color-warning: var(--warning);')
->toContain('--warning: #fcd452;');
->toContain('--warning: #fcd452;')
->not->toContain('--ring: oklch(0.705 0.015 286.067);')
->not->toContain('--ring: oklch(0.552 0.016 285.938);');
expect(substr_count($v5Css, '--ring: var(--warning);'))->toBe(2);
expect($types)
->toContain('sshUser: string;')
@@ -722,10 +769,10 @@ it('shows when flux is unavailable', function () {
->assertSee('Flux socket was not found.');
});
it('does not include coolify version controls on the v5 home page', function () {
$homePage = file_get_contents(resource_path('js/v5/Pages/Home.tsx'));
it('does not include coolify version controls on the v5 dashboard page', function () {
$dashboardPage = file_get_contents(resource_path('js/v5/Pages/Dashboard.tsx'));
expect($homePage)
expect($dashboardPage)
->not->toContain('Check coolify version')
->not->toContain('/v5/coolify/version')
->not->toContain('Installed version:');