Merge branch 'next' into service/lobe-ai-chat

This commit is contained in:
Romain ROCHAS 2025-10-08 21:46:23 +02:00 committed by GitHub
commit bdd078e53d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 1755 additions and 270 deletions

File diff suppressed because it is too large Load diff

View file

@ -35,6 +35,11 @@ protected static function booted()
}); });
} }
public static function ownedByCurrentTeam()
{
return Environment::whereRelation('project.team', 'id', currentTeam()->id)->orderBy('name');
}
public function isEmpty() public function isEmpty()
{ {
return $this->applications()->count() == 0 && return $this->applications()->count() == 0 &&

View file

@ -114,7 +114,7 @@
} }
} }
}" }"
@keydown.escape.window="modalOpen = false; resetModal()" :class="{ 'z-40': modalOpen }" @keydown.escape.window="if (modalOpen) { modalOpen = false; resetModal(); }" :class="{ 'z-40': modalOpen }"
class="relative w-auto h-auto"> class="relative w-auto h-auto">
@if ($customButton) @if ($customButton)
@if ($buttonFullWidth) @if ($buttonFullWidth)

View file

@ -59,25 +59,25 @@
if (this.zoom === '90') { if (this.zoom === '90') {
const style = document.createElement('style'); const style = document.createElement('style');
style.textContent = ` style.textContent = `
html { html {
font-size: 93.75%; font-size: 93.75%;
} }
:root { :root {
--vh: 1vh; --vh: 1vh;
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
html { html {
font-size: 87.5%; font-size: 87.5%;
} }
} }
`; `;
document.head.appendChild(style); document.head.appendChild(style);
} }
} }
}"> }">
<div class="flex pt-6 pb-4 pl-2"> <div class="flex lg:pt-6 pt-4 pb-4 pl-2">
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div class="text-2xl font-bold tracking-wide dark:text-white">Coolify</div> <div class="text-2xl font-bold tracking-wide dark:text-white">Coolify</div>
<x-version /> <x-version />
@ -86,8 +86,8 @@
<!-- Search button that triggers global search modal --> <!-- Search button that triggers global search modal -->
<button @click="$dispatch('open-global-search')" type="button" title="Search (Press / or ⌘K)" <button @click="$dispatch('open-global-search')" type="button" title="Search (Press / or ⌘K)"
class="flex items-center gap-1.5 px-2.5 py-1.5 bg-neutral-100 dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-200 rounded-md hover:bg-neutral-200 dark:hover:bg-coolgray-200 transition-colors"> class="flex items-center gap-1.5 px-2.5 py-1.5 bg-neutral-100 dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-200 rounded-md hover:bg-neutral-200 dark:hover:bg-coolgray-200 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-neutral-500 dark:text-neutral-400" fill="none" <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-neutral-500 dark:text-neutral-400"
viewBox="0 0 24 24" stroke="currentColor"> fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg> </svg>

View file

@ -20,9 +20,9 @@
}" x-cloak class="mx-auto" :class="pageWidth === 'full' ? '' : 'max-w-7xl'"> }" x-cloak class="mx-auto" :class="pageWidth === 'full' ? '' : 'max-w-7xl'">
<div class="relative z-50 lg:hidden" :class="open ? 'block' : 'hidden'" role="dialog" aria-modal="true"> <div class="relative z-50 lg:hidden" :class="open ? 'block' : 'hidden'" role="dialog" aria-modal="true">
<div class="fixed inset-0 bg-black/80" x-on:click="open = false"></div> <div class="fixed inset-0 bg-black/80" x-on:click="open = false"></div>
<div class="fixed h-full flex"> <div class="fixed inset-y-0 right-0 h-full flex">
<div class="relative flex flex-1 w-full max-w-56 "> <div class="relative flex flex-1 w-full max-w-56 ">
<div class="absolute top-0 flex justify-center w-16 pt-5 left-full"> <div class="absolute top-0 flex justify-center w-16 pt-5 right-full">
<button type="button" class="-m-2.5 p-2.5" x-on:click="open = !open"> <button type="button" class="-m-2.5 p-2.5" x-on:click="open = !open">
<span class="sr-only">Close sidebar</span> <span class="sr-only">Close sidebar</span>
<svg class="w-6 h-6 text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5" <svg class="w-6 h-6 text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
@ -45,8 +45,12 @@
</div> </div>
</div> </div>
<div class="sticky top-0 z-40 flex items-center px-4 py-4 gap-x-6 sm:px-6 lg:hidden"> <div class="sticky top-0 z-40 flex items-center justify-between px-4 py-4 gap-x-6 sm:px-6 lg:hidden">
<button type="button" class="-m-2.5 p-2.5 dark:text-warning lg:hidden" x-on:click="open = !open"> <div class="flex items-center gap-3 flex-shrink-0">
<div class="text-xl font-bold tracking-wide dark:text-white">Coolify</div>
<livewire:switch-team />
</div>
<button type="button" class="-m-2.5 p-2.5 dark:text-warning" x-on:click="open = !open">
<span class="sr-only">Open sidebar</span> <span class="sr-only">Open sidebar</span>
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 24 24"> <svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"

View file

@ -21,7 +21,7 @@
<x-modal-input buttonTitle="Add" title="New Project"> <x-modal-input buttonTitle="Add" title="New Project">
<x-slot:content> <x-slot:content>
<button <button
class="flex items-center justify-center size-4 text-white rounded hover:bg-coolgray-400 dark:hover:bg-coolgray-300"> class="flex items-center justify-center size-4 text-white rounded hover:bg-coolgray-400 dark:hover:bg-coolgray-300 cursor-pointer">
<svg class="size-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" <svg class="size-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor"> stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /> <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
@ -87,7 +87,7 @@ class="flex items-center justify-center size-4 text-white rounded hover:bg-coolg
<x-modal-input buttonTitle="Add" title="New Server" :closeOutside="false"> <x-modal-input buttonTitle="Add" title="New Server" :closeOutside="false">
<x-slot:content> <x-slot:content>
<button <button
class="flex items-center justify-center size-4 text-white rounded hover:bg-coolgray-400 dark:hover:bg-coolgray-300"> class="flex items-center justify-center size-4 text-white rounded hover:bg-coolgray-400 dark:hover:bg-coolgray-300 cursor-pointer">
<svg class="size-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" <svg class="size-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor"> stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /> <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />

View file

@ -1,14 +1,82 @@
<div x-data="{ <div x-data="{
modalOpen: false, modalOpen: false,
selectedIndex: -1, selectedIndex: -1,
isSearching: false,
isLoadingInitialData: false,
allSearchableItems: [],
searchQuery: '',
creatableItems: [],
isCreateMode: false,
// Client-side search function
get searchResults() {
if (!this.searchQuery || this.searchQuery.length < 1) {
return [];
}
const query = this.searchQuery.toLowerCase().trim();
const results = this.allSearchableItems.filter(item => {
if (!item.search_text) return false;
return item.search_text.toLowerCase().includes(query);
}).slice(0, 20);
return results;
},
get filteredCreatableItems() {
if (!this.searchQuery || this.searchQuery.length < 1) {
return [];
}
const query = this.searchQuery.toLowerCase().trim();
if (query === 'new') {
return this.creatableItems;
}
return this.creatableItems.filter(item => {
const searchText = `${item.name} ${item.description} ${item.type} ${item.category}`.toLowerCase();
if (query.startsWith('new ')) {
const queryWithoutNew = query.substring(4);
return searchText.includes(queryWithoutNew) || searchText.includes(query);
}
return searchText.includes(query);
});
},
get groupedCreatableItems() {
const grouped = {};
this.filteredCreatableItems.forEach(item => {
const category = item.category || 'Other';
if (!grouped[category]) {
grouped[category] = [];
}
grouped[category].push(item);
});
return grouped;
},
openModal() { openModal() {
this.modalOpen = true; this.modalOpen = true;
this.selectedIndex = -1; this.selectedIndex = -1;
@this.openSearchModal(); this.isLoadingInitialData = true;
this.searchQuery = '';
$wire.openSearchModal().then(() => {
this.allSearchableItems = $wire.allSearchableItems || [];
this.creatableItems = $wire.creatableItems || [];
this.isLoadingInitialData = false;
setTimeout(() => this.$refs.searchInput?.focus(), 50);
});
}, },
closeModal() { closeModal() {
this.modalOpen = false; this.modalOpen = false;
this.selectedIndex = -1; this.selectedIndex = -1;
this.isSearching = false;
this.isLoadingInitialData = false;
this.searchQuery = '';
this.allSearchableItems = [];
// Ensure scroll is restored // Ensure scroll is restored
document.body.style.overflow = ''; document.body.style.overflow = '';
@this.closeSearchModal(); @this.closeSearchModal();
@ -31,19 +99,68 @@
} }
}, },
init() { init() {
// Listen for reset index event from Livewire
Livewire.on('reset-selected-index', () => {
this.selectedIndex = -1;
});
this.$watch('searchQuery', (value) => {
this.selectedIndex = -1;
const trimmed = value.trim().toLowerCase();
if (trimmed === '') {
if ($wire.isSelectingResource) {
$wire.cancelResourceSelection();
}
return;
}
const exactMatchCommands = [
'new project', 'new server', 'new team', 'new storage', 'new s3',
'new private key', 'new privatekey', 'new key',
'new github app', 'new github', 'new source',
'new public', 'new public git', 'new public repo', 'new public repository',
'new private github', 'new private gh', 'new private deploy', 'new deploy key',
'new dockerfile', 'new docker compose', 'new compose', 'new docker image', 'new image',
'new postgresql', 'new postgres', 'new mysql', 'new mariadb',
'new redis', 'new keydb', 'new dragonfly', 'new mongodb', 'new mongo', 'new clickhouse'
];
if (exactMatchCommands.includes(trimmed)) {
const matchingItem = this.creatableItems.find(item => {
const itemSearchText = `new ${item.name}`.toLowerCase();
const itemType = `new ${item.type}`.toLowerCase();
return itemSearchText === trimmed || itemType === trimmed ||
(item.type && trimmed.includes(item.type.replace(/-/g, ' ')));
});
if (matchingItem) {
$wire.navigateToResource(matchingItem.type);
}
}
});
// Create named handlers for proper cleanup // Create named handlers for proper cleanup
const openGlobalSearchHandler = () => this.openModal(); const openGlobalSearchHandler = () => this.openModal();
const slashKeyHandler = (e) => { const slashKeyHandler = (e) => {
if (e.key === '/' && !['INPUT', 'TEXTAREA'].includes(e.target.tagName) && !this.modalOpen) { if (e.key === '/' && !['INPUT', 'TEXTAREA'].includes(e.target.tagName)) {
e.preventDefault(); e.preventDefault();
this.openModal(); if (!this.modalOpen) {
this.openModal();
} else {
// If modal is open, focus the input
this.$refs.searchInput?.focus();
this.selectedIndex = -1;
}
} }
}; };
const cmdKHandler = (e) => { const cmdKHandler = (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') { if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault(); e.preventDefault();
if (this.modalOpen) { if (this.modalOpen) {
this.closeModal(); // If modal is open, focus the input instead of closing
this.$refs.searchInput?.focus();
this.selectedIndex = -1;
} else { } else {
this.openModal(); this.openModal();
} }
@ -51,7 +168,21 @@
}; };
const escapeKeyHandler = (e) => { const escapeKeyHandler = (e) => {
if (e.key === 'Escape' && this.modalOpen) { if (e.key === 'Escape' && this.modalOpen) {
this.closeModal(); // If search query is empty, close the modal
if (!this.searchQuery || this.searchQuery === '') {
// Check if we're in a selection state using Alpine-accessible Livewire state
if ($wire.isSelectingResource) {
$wire.cancelResourceSelection();
setTimeout(() => this.$refs.searchInput?.focus(), 100);
} else {
// Close the modal if in main menu
this.closeModal();
}
} else {
// If search query has text, just clear it
this.searchQuery = '';
setTimeout(() => this.$refs.searchInput?.focus(), 100);
}
} }
}; };
const arrowKeyHandler = (e) => { const arrowKeyHandler = (e) => {
@ -94,13 +225,18 @@
}, 150); }, 150);
} }
}); });
// Listen for closeSearchModal event from backend
window.addEventListener('closeSearchModal', () => {
this.closeModal();
});
} }
}"> }">
<!-- Modal overlay --> <!-- Modal overlay -->
<template x-teleport="body"> <template x-teleport="body">
<div x-show="modalOpen" x-cloak <div x-show="modalOpen" x-cloak
class="fixed top-0 left-0 z-99 flex items-start justify-center w-screen h-screen pt-[20vh]"> class="fixed top-0 left-0 z-99 flex items-start justify-center w-screen h-screen pt-[10vh]">
<div @click="closeModal()" class="absolute inset-0 w-full h-full bg-black/50 backdrop-blur-sm"> <div @click="closeModal()" class="absolute inset-0 w-full h-full bg-black/50 backdrop-blur-sm">
</div> </div>
<div x-show="modalOpen" x-trap.inert="modalOpen" x-init="$watch('modalOpen', value => { document.body.style.overflow = value ? 'hidden' : '' })" <div x-show="modalOpen" x-trap.inert="modalOpen" x-init="$watch('modalOpen', value => { document.body.style.overflow = value ? 'hidden' : '' })"
@ -113,73 +249,456 @@ class="fixed top-0 left-0 z-99 flex items-start justify-center w-screen h-screen
<!-- Search input (always visible) --> <!-- Search input (always visible) -->
<div class="relative"> <div class="relative">
<div class="absolute inset-y-0 left-4 flex items-center pointer-events-none"> <div class="absolute inset-y-0 left-4 flex items-center pointer-events-none">
<svg class="w-5 h-5 text-neutral-400" xmlns="http://www.w3.org/2000/svg" fill="none" <svg x-show="!isLoadingInitialData" class="w-5 h-5 text-neutral-400" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24" stroke="currentColor"> viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg> </svg>
<svg x-show="isLoadingInitialData" x-cloak class="animate-spin h-5 w-5 text-warning"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
</path>
</svg>
</div>
<input type="text" x-model="searchQuery"
placeholder="Search resources (type new for create things)..." x-ref="searchInput"
x-init="$watch('modalOpen', value => { if (value) setTimeout(() => $refs.searchInput.focus(), 100) })" :disabled="isLoadingInitialData"
class="w-full pl-12 pr-32 py-4 text-base bg-white dark:bg-coolgray-100 border-none rounded-lg shadow-xl ring-1 ring-neutral-200 dark:ring-coolgray-300 focus:ring-2 focus:ring-neutral-400 dark:focus:ring-coolgray-300 dark:text-white placeholder-neutral-400 dark:placeholder-neutral-500 disabled:opacity-50 disabled:cursor-not-allowed" />
<div class="absolute inset-y-0 right-2 flex items-center gap-2 pointer-events-none">
<span class="text-xs font-medium text-neutral-400 dark:text-neutral-500">
/ or ⌘K to focus
</span>
<button @click="closeModal()"
class="pointer-events-auto px-2 py-1 text-xs font-medium text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200 rounded">
ESC
</button>
</div> </div>
<input type="text" wire:model.live.debounce.500ms="searchQuery"
placeholder="Search for resources... (Type 'new' to create, or 'new project' to add directly)"
x-ref="searchInput" x-init="$watch('modalOpen', value => { if (value) setTimeout(() => $refs.searchInput.focus(), 100) })"
class="w-full pl-12 pr-12 py-4 text-base bg-white dark:bg-coolgray-100 border-none rounded-lg shadow-xl ring-1 ring-neutral-200 dark:ring-coolgray-300 focus:ring-2 focus:ring-neutral-400 dark:focus:ring-coolgray-300 dark:text-white placeholder-neutral-400 dark:placeholder-neutral-500" />
<button @click="closeModal()"
class="absolute inset-y-0 right-2 flex items-center justify-center px-2 text-xs font-medium text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200 rounded">
ESC
</button>
</div> </div>
<!-- Search results (with background) --> <!-- Debug: Show data loaded (temporary) -->
@if (strlen($searchQuery) >= 1) {{-- <div x-show="!isLoadingInitialData && searchQuery === '' && allSearchableItems.length > 0" x-cloak
<div class="mt-2 bg-white dark:bg-coolgray-100 rounded-lg shadow-xl ring-1 ring-neutral-200 dark:ring-coolgray-300 overflow-hidden p-6">
class="mt-2 bg-white dark:bg-coolgray-100 rounded-lg shadow-xl ring-1 ring-neutral-200 dark:ring-coolgray-300 overflow-hidden"> <div class="text-center">
<!-- Loading indicator --> <p class="text-sm font-semibold text-green-600 dark:text-green-400">
<div wire:loading.flex wire:target="searchQuery" Data loaded successfully!
class="min-h-[200px] items-center justify-center p-8"> </p>
<div class="text-center"> <p class="text-xs text-neutral-500 dark:text-neutral-400 mt-2">
<svg class="animate-spin mx-auto h-8 w-8 text-neutral-400" <span x-text="allSearchableItems.length"></span> searchable items available
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> </p>
<circle class="opacity-25" cx="12" cy="12" r="10" <p class="text-xs text-neutral-400 dark:text-neutral-500 mt-1">
stroke="currentColor" stroke-width="4"></circle> Start typing to search...
<path class="opacity-75" fill="currentColor" </p>
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"> </div>
</path> </div> --}}
</svg>
<p class="mt-2 text-sm text-neutral-600 dark:text-neutral-400">
Searching...
</p>
</div>
</div>
<!-- Results content - hidden while loading --> <!-- Search results (with background) -->
<div wire:loading.remove wire:target="searchQuery" <div x-show="searchQuery.length >= 1" x-cloak
class="max-h-[60vh] overflow-y-auto scrollbar"> class="mt-2 bg-white dark:bg-coolgray-100 rounded-lg shadow-xl ring-1 ring-neutral-200 dark:ring-coolgray-300 overflow-hidden">
@if ($isCreateMode && count($creatableItems) > 0 && !$autoOpenResource) <!-- Results content -->
<!-- Create new resources section --> <div class="max-h-[60vh] overflow-y-auto scrollbar">
<div class="py-2" x-data="{ @if ($isSelectingResource)
openModal(type) { <!-- Resource Selection Flow -->
// Close the parent search modal properly <div class="p-6">
const parentModal = this.$root.closest('[x-data]'); <!-- Server Selection -->
if (parentModal && parentModal.__x) { @if ($selectedServerId === null)
parentModal.__x.$data.closeModal(); <div class="mb-4" x-init="selectedIndex = -1">
} <div class="flex items-center gap-3 mb-3">
// Dispatch event to open creation modal after a short delay <button type="button"
setTimeout(() => { @click="$wire.set('searchQuery', ''); setTimeout(() => $refs.searchInput.focus(), 100)"
this.$dispatch('open-create-modal-' + type); class="text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white">
}, 150); <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none"
} viewBox="0 0 24 24" stroke="currentColor">
}"> <path stroke-linecap="round" stroke-linejoin="round"
<div stroke-width="2" d="M15 19l-7-7 7-7" />
class="px-4 py-2 bg-yellow-50 dark:bg-yellow-900/20 border-b border-yellow-100 dark:border-yellow-800"> </svg>
<h3 class="text-sm font-semibold text-yellow-900 dark:text-yellow-100"> </button>
Create New Resources <div>
</h3> <h2 class="text-base font-semibold text-neutral-900 dark:text-white">
<p class="text-xs text-yellow-700 dark:text-yellow-300 mt-0.5"> Select Server
Click on any item below to create a new resource </h2>
</p> @if ($this->selectedResourceName)
<div class="text-xs text-neutral-500 dark:text-neutral-400">
for {{ $this->selectedResourceName }}
</div>
@endif
</div>
</div>
@if ($loadingServers)
<div
class="flex items-center gap-3 p-3 bg-neutral-50 dark:bg-coolgray-200 rounded-lg">
<svg class="animate-spin h-5 w-5 text-yellow-500"
xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10"
stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
</path>
</svg>
<span class="text-sm text-neutral-600 dark:text-neutral-400">Loading
servers...</span>
</div>
@elseif (count($availableServers) > 0)
@foreach ($availableServers as $index => $server)
<button type="button"
wire:click="selectServer({{ $server['id'] }}, true)"
class="search-result-item w-full text-left block px-4 py-3 min-h-[4rem] hover:bg-yellow-50 dark:hover:bg-yellow-900/20 transition-colors focus:outline-none focus:bg-yellow-100 dark:focus:bg-yellow-900/30 border-b border-neutral-100 dark:border-coolgray-300 last:border-0">
<div class="flex items-center justify-between gap-3 min-h-[2.5rem]">
<div class="flex-1 min-w-0">
<div class="font-medium text-neutral-900 dark:text-white">
{{ $server['name'] }}
</div>
@if (!empty($server['description']))
<div
class="text-xs text-neutral-500 dark:text-neutral-400">
{{ $server['description'] }}
</div>
@else
<div class="text-xs text-transparent select-none">
&nbsp;
</div>
@endif
</div>
<svg xmlns="http://www.w3.org/2000/svg"
class="shrink-0 h-5 w-5 text-yellow-500 dark:text-yellow-400"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</div>
</button>
@endforeach
@else
<div
class="p-3 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-800">
<p class="text-sm text-red-800 dark:text-red-200">No servers
available</p>
</div>
@endif
</div> </div>
@foreach ($creatableItems as $item) @endif
<button type="button" @click="openModal('{{ $item['type'] }}')"
<!-- Destination Selection -->
@if ($selectedServerId !== null && $selectedDestinationUuid === null)
<div class="mb-4" x-init="selectedIndex = -1">
<div class="flex items-center gap-3 mb-3">
<button type="button"
@click="$wire.set('searchQuery', ''); setTimeout(() => $refs.searchInput.focus(), 100)"
class="text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</button>
<div>
<h2 class="text-base font-semibold text-neutral-900 dark:text-white">
Select Destination
</h2>
@if ($this->selectedResourceName)
<div class="text-xs text-neutral-500 dark:text-neutral-400">
for {{ $this->selectedResourceName }}
</div>
@endif
</div>
</div>
@if ($loadingDestinations)
<div
class="flex items-center gap-3 p-3 bg-neutral-50 dark:bg-coolgray-200 rounded-lg">
<svg class="animate-spin h-5 w-5 text-yellow-500"
xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10"
stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
</path>
</svg>
<span class="text-sm text-neutral-600 dark:text-neutral-400">Loading
destinations...</span>
</div>
@elseif (count($availableDestinations) > 0)
@foreach ($availableDestinations as $index => $destination)
<button type="button"
wire:click="selectDestination('{{ $destination['uuid'] }}', true)"
class="search-result-item w-full text-left block px-4 py-3 min-h-[4rem] hover:bg-yellow-50 dark:hover:bg-yellow-900/20 transition-colors focus:outline-none focus:bg-yellow-100 dark:focus:bg-yellow-900/30 border-b border-neutral-100 dark:border-coolgray-300 last:border-0">
<div class="flex items-center justify-between gap-3 min-h-[2.5rem]">
<div class="flex-1 min-w-0">
<div class="font-medium text-neutral-900 dark:text-white">
{{ $destination['name'] }}
</div>
<div
class="text-xs text-neutral-500 dark:text-neutral-400">
Network: {{ $destination['network'] }}
</div>
</div>
<svg xmlns="http://www.w3.org/2000/svg"
class="shrink-0 h-5 w-5 text-yellow-500 dark:text-yellow-400"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</div>
</button>
@endforeach
@else
<div
class="p-3 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-800">
<p class="text-sm text-red-800 dark:text-red-200">No destinations
available</p>
</div>
@endif
</div>
@endif
<!-- Project Selection -->
@if ($selectedDestinationUuid !== null && $selectedProjectUuid === null)
<div class="mb-4" x-init="selectedIndex = -1">
<div class="flex items-center gap-3 mb-3">
<button type="button"
@click="$wire.set('searchQuery', ''); setTimeout(() => $refs.searchInput.focus(), 100)"
class="text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</button>
<div>
<h2 class="text-base font-semibold text-neutral-900 dark:text-white">
Select Project
</h2>
@if ($this->selectedResourceName)
<div class="text-xs text-neutral-500 dark:text-neutral-400">
for {{ $this->selectedResourceName }}
</div>
@endif
</div>
</div>
@if ($loadingProjects)
<div
class="flex items-center gap-3 p-3 bg-neutral-50 dark:bg-coolgray-200 rounded-lg">
<svg class="animate-spin h-5 w-5 text-yellow-500"
xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10"
stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
</path>
</svg>
<span class="text-sm text-neutral-600 dark:text-neutral-400">Loading
projects...</span>
</div>
@elseif (count($availableProjects) > 0)
@foreach ($availableProjects as $index => $project)
<button type="button"
wire:click="selectProject('{{ $project['uuid'] }}', true)"
class="search-result-item w-full text-left block px-4 py-3 min-h-[4rem] hover:bg-yellow-50 dark:hover:bg-yellow-900/20 transition-colors focus:outline-none focus:bg-yellow-100 dark:focus:bg-yellow-900/30 border-b border-neutral-100 dark:border-coolgray-300 last:border-0">
<div class="flex items-center justify-between gap-3 min-h-[2.5rem]">
<div class="flex-1 min-w-0">
<div class="font-medium text-neutral-900 dark:text-white">
{{ $project['name'] }}
</div>
@if (!empty($project['description']))
<div
class="text-xs text-neutral-500 dark:text-neutral-400">
{{ $project['description'] }}
</div>
@else
<div class="text-xs text-transparent select-none">
&nbsp;
</div>
@endif
</div>
<svg xmlns="http://www.w3.org/2000/svg"
class="shrink-0 h-5 w-5 text-yellow-500 dark:text-yellow-400"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</div>
</button>
@endforeach
@else
<div
class="p-3 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-800">
<p class="text-sm text-red-800 dark:text-red-200">No projects
available</p>
</div>
@endif
</div>
@endif
<!-- Environment Selection -->
@if ($selectedProjectUuid !== null && $selectedEnvironmentUuid === null)
<div class="mb-4" x-init="selectedIndex = -1">
<div class="flex items-center gap-3 mb-3">
<button type="button"
@click="$wire.set('searchQuery', ''); setTimeout(() => $refs.searchInput.focus(), 100)"
class="text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-white">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</button>
<div>
<h2 class="text-base font-semibold text-neutral-900 dark:text-white">
Select Environment
</h2>
@if ($this->selectedResourceName)
<div class="text-xs text-neutral-500 dark:text-neutral-400">
for {{ $this->selectedResourceName }}
</div>
@endif
</div>
</div>
@if ($loadingEnvironments)
<div
class="flex items-center gap-3 p-3 bg-neutral-50 dark:bg-coolgray-200 rounded-lg">
<svg class="animate-spin h-5 w-5 text-yellow-500"
xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10"
stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
</path>
</svg>
<span class="text-sm text-neutral-600 dark:text-neutral-400">Loading
environments...</span>
</div>
@elseif (count($availableEnvironments) > 0)
@foreach ($availableEnvironments as $index => $environment)
<button type="button"
wire:click="selectEnvironment('{{ $environment['uuid'] }}', true)"
class="search-result-item w-full text-left block px-4 py-3 min-h-[4rem] hover:bg-yellow-50 dark:hover:bg-yellow-900/20 transition-colors focus:outline-none focus:bg-yellow-100 dark:focus:bg-yellow-900/30 border-b border-neutral-100 dark:border-coolgray-300 last:border-0">
<div class="flex items-center justify-between gap-3 min-h-[2.5rem]">
<div class="flex-1 min-w-0">
<div class="font-medium text-neutral-900 dark:text-white">
{{ $environment['name'] }}
</div>
@if (!empty($environment['description']))
<div
class="text-xs text-neutral-500 dark:text-neutral-400">
{{ $environment['description'] }}
</div>
@else
<div class="text-xs text-transparent select-none">
&nbsp;
</div>
@endif
</div>
<svg xmlns="http://www.w3.org/2000/svg"
class="shrink-0 h-5 w-5 text-yellow-500 dark:text-yellow-400"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</div>
</button>
@endforeach
@else
<div
class="p-3 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-800">
<p class="text-sm text-red-800 dark:text-red-200">No environments
available</p>
</div>
@endif
</div>
@endif
</div>
@elseif ($isCreateMode && count($this->filteredCreatableItems) > 0 && !$autoOpenResource)
<!-- Create new resources section -->
<div class="py-2">
{{-- Show existing resources first if any match --}}
@php
$existingResources = collect($searchResults)
->filter(fn($r) => !isset($r['is_creatable_suggestion']))
->count();
@endphp
@if ($existingResources > 0)
<!-- Existing Resources Section -->
<div class="px-4 pt-3 pb-1">
<h4
class="text-xs font-semibold text-neutral-500 dark:text-neutral-400 uppercase tracking-wider">
Existing Resources
</h4>
</div>
@foreach ($searchResults as $result)
@if (!isset($result['is_creatable_suggestion']))
<a href="{{ $result['link'] ?? '#' }}"
class="search-result-item block px-4 py-3 hover:bg-neutral-50 dark:hover:bg-coolgray-200 transition-colors focus:outline-none focus:bg-yellow-50 dark:focus:bg-yellow-900/20 border-transparent hover:border-coollabs focus:border-yellow-500 dark:focus:border-yellow-400">
<div class="flex items-center justify-between gap-3">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<span
class="font-medium text-neutral-900 dark:text-white truncate">
{{ $result['name'] }}
</span>
<span
class="px-2 py-0.5 text-xs rounded-full bg-neutral-100 dark:bg-coolgray-300 text-neutral-700 dark:text-neutral-300 shrink-0">
@if ($result['type'] === 'application')
Application
@elseif ($result['type'] === 'service')
Service
@elseif ($result['type'] === 'database')
{{ ucfirst($result['subtype'] ?? 'Database') }}
@elseif ($result['type'] === 'server')
Server
@elseif ($result['type'] === 'project')
Project
@elseif ($result['type'] === 'environment')
Environment
@endif
</span>
</div>
@if (!empty($result['project']) && !empty($result['environment']))
<div
class="text-xs text-neutral-500 dark:text-neutral-400 mb-1">
{{ $result['project'] }} /
{{ $result['environment'] }}
</div>
@endif
@if (!empty($result['description']))
<div
class="text-sm text-neutral-600 dark:text-neutral-400">
{{ Str::limit($result['description'], 80) }}
</div>
@endif
</div>
<svg xmlns="http://www.w3.org/2000/svg"
class="shrink-0 h-5 w-5 text-neutral-300 dark:text-neutral-600 self-center"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</div>
</a>
@endif
@endforeach
@endif
@php
$grouped = collect($this->filteredCreatableItems)->groupBy('category');
@endphp
@foreach ($grouped as $category => $items)
<!-- Category Header -->
<div class="px-4 pt-3 pb-1">
<h4
class="text-xs font-semibold text-neutral-500 dark:text-neutral-400 uppercase tracking-wider">
{{ $category }}
</h4>
</div>
<!-- Category Items -->
@foreach ($items as $item)
<button type="button" wire:click="navigateToResource('{{ $item['type'] }}')"
class="search-result-item w-full text-left block px-4 py-3 hover:bg-yellow-50 dark:hover:bg-yellow-900/20 transition-colors focus:outline-none focus:bg-yellow-100 dark:focus:bg-yellow-900/30 border-transparent hover:border-yellow-500 focus:border-yellow-500"> class="search-result-item w-full text-left block px-4 py-3 hover:bg-yellow-50 dark:hover:bg-yellow-900/20 transition-colors focus:outline-none focus:bg-yellow-100 dark:focus:bg-yellow-900/30 border-transparent hover:border-yellow-500 focus:border-yellow-500">
<div class="flex items-center justify-between gap-3"> <div class="flex items-center justify-between gap-3">
<div class="flex items-center gap-3 flex-1 min-w-0"> <div class="flex items-center gap-3 flex-1 min-w-0">
@ -194,16 +713,17 @@ class="h-5 w-5 text-yellow-600 dark:text-yellow-400"
</div> </div>
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1"> <div class="flex items-center gap-2 mb-1">
<span <div
class="font-medium text-neutral-900 dark:text-white truncate"> class="font-medium text-neutral-900 dark:text-white truncate">
{{ $item['name'] }} {{ $item['name'] }}
</span> </div>
<span @if (isset($item['quickcommand']))
class="px-2 py-0.5 text-xs rounded-full bg-yellow-100 dark:bg-yellow-900/40 text-yellow-700 dark:text-yellow-300 shrink-0"> <span
New class="text-xs text-neutral-500 dark:text-neutral-400 shrink-0">{{ $item['quickcommand'] }}</span>
</span> @endif
</div> </div>
<div class="text-sm text-neutral-600 dark:text-neutral-400"> <div
class="text-sm text-neutral-600 dark:text-neutral-400 truncate">
{{ $item['description'] }} {{ $item['description'] }}
</div> </div>
</div> </div>
@ -217,81 +737,140 @@ class="shrink-0 h-5 w-5 text-yellow-500 dark:text-yellow-400 self-center"
</div> </div>
</button> </button>
@endforeach @endforeach
</div> @endforeach
@elseif (strlen($searchQuery) >= 2 && count($searchResults) > 0) </div>
<div class="py-2"> @endif
@foreach ($searchResults as $index => $result)
<a href="{{ $result['link'] ?? '#' }}" <template
class="search-result-item block px-4 py-3 hover:bg-neutral-50 dark:hover:bg-coolgray-200 transition-colors focus:outline-none focus:bg-yellow-50 dark:focus:bg-yellow-900/20 border-transparent hover:border-coollabs focus:border-yellow-500 dark:focus:border-yellow-400"> x-if="searchQuery.length >= 1 && searchResults.length > 0 && !$wire.isSelectingResource">
<div class="flex items-center justify-between gap-3"> <div class="py-2">
<div class="flex-1 min-w-0"> <template x-if="filteredCreatableItems.length > 0">
<div class="flex items-center gap-2 mb-1"> <div class="px-4 pt-3 pb-1">
<span <h4
class="font-medium text-neutral-900 dark:text-white truncate"> class="text-xs font-semibold text-neutral-500 dark:text-neutral-400 uppercase tracking-wider">
{{ $result['name'] }} Existing Resources
</span> </h4>
<span </div>
class="px-2 py-0.5 text-xs rounded-full bg-neutral-100 dark:bg-coolgray-300 text-neutral-700 dark:text-neutral-300 shrink-0"> </template>
@if ($result['type'] === 'application') <template x-for="(result, index) in searchResults" :key="index">
Application <a :href="result.link || '#'"
@elseif ($result['type'] === 'service') class="search-result-item block px-4 py-3 hover:bg-neutral-50 dark:hover:bg-coolgray-200 transition-colors focus:outline-none focus:bg-yellow-50 dark:focus:bg-yellow-900/20 border-transparent hover:border-coollabs focus:border-yellow-500 dark:focus:border-yellow-400">
Service <div class="flex items-center justify-between gap-3">
@elseif ($result['type'] === 'database') <div class="flex-1 min-w-0">
{{ ucfirst($result['subtype'] ?? 'Database') }} <div class="flex items-center gap-2 mb-1">
@elseif ($result['type'] === 'server') <span class="font-medium text-neutral-900 dark:text-white truncate"
Server x-text="result.name">
@elseif ($result['type'] === 'project') </span>
Project <span
@elseif ($result['type'] === 'environment') class="px-2 py-0.5 text-xs rounded-full bg-neutral-100 dark:bg-coolgray-300 text-neutral-700 dark:text-neutral-300 shrink-0">
Environment <span x-show="result.type === 'navigation'">Navigation</span>
@endif <span x-show="result.type === 'application'">Application</span>
</span> <span x-show="result.type === 'service'">Service</span>
</div> <span x-show="result.type === 'database'"
@if (!empty($result['project']) && !empty($result['environment'])) x-text="result.subtype ? result.subtype.charAt(0).toUpperCase() + result.subtype.slice(1) : 'Database'"></span>
<div <span x-show="result.type === 'server'">Server</span>
class="text-xs text-neutral-500 dark:text-neutral-400 mb-1"> <span x-show="result.type === 'project'">Project</span>
{{ $result['project'] }} / {{ $result['environment'] }} <span x-show="result.type === 'environment'">Environment</span>
</div> </span>
@endif
@if (!empty($result['description']))
<div class="text-sm text-neutral-600 dark:text-neutral-400">
{{ Str::limit($result['description'], 80) }}
</div>
@endif
</div> </div>
<svg xmlns="http://www.w3.org/2000/svg" <template x-if="result.project && result.environment">
class="shrink-0 h-5 w-5 text-neutral-300 dark:text-neutral-600 self-center" <div class="text-xs text-neutral-500 dark:text-neutral-400 mb-1">
fill="none" viewBox="0 0 24 24" stroke="currentColor"> <span x-text="result.project"></span> / <span
<path stroke-linecap="round" stroke-linejoin="round" x-text="result.environment"></span>
stroke-width="2" d="M9 5l7 7-7 7" /> </div>
</svg> </template>
<template x-if="result.description">
<div class="text-sm text-neutral-600 dark:text-neutral-400"
x-text="result.description.length > 80 ? result.description.substring(0, 80) + '...' : result.description">
</div>
</template>
</div> </div>
</a> <svg xmlns="http://www.w3.org/2000/svg"
@endforeach class="shrink-0 h-5 w-5 text-neutral-300 dark:text-neutral-600 self-center"
</div> fill="none" viewBox="0 0 24 24" stroke="currentColor">
@elseif (strlen($searchQuery) >= 2 && count($searchResults) === 0 && !$autoOpenResource) <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
<div class="flex items-center justify-center py-12 px-4"> d="M9 5l7 7-7 7" />
<div class="text-center"> </svg>
<p class="mt-4 text-sm font-medium text-neutral-900 dark:text-white"> </div>
No results found </a>
</p> </template>
<p class="mt-1 text-sm text-neutral-500 dark:text-neutral-400"> </div>
Try different keywords or check the spelling </template>
</p>
<template x-if="filteredCreatableItems.length > 0 && !$wire.isSelectingResource">
<div class="py-2">
<template x-for="[categoryName, items] in Object.entries(groupedCreatableItems)"
:key="categoryName">
<div>
<div class="px-4 pt-3 pb-1">
<h4 class="text-xs font-semibold text-neutral-500 dark:text-neutral-400 uppercase tracking-wider"
x-text="categoryName">
</h4>
</div>
<template x-for="item in items" :key="item.type">
<button type="button" @click="$wire.navigateToResource(item.type)"
class="search-result-item w-full text-left block px-4 py-3 hover:bg-yellow-50 dark:hover:bg-yellow-900/20 transition-colors focus:outline-none focus:bg-yellow-100 dark:focus:bg-yellow-900/30 border-transparent hover:border-yellow-500 focus:border-yellow-500">
<div class="flex items-center justify-between gap-3">
<div class="flex items-center gap-3 flex-1 min-w-0">
<div
class="flex-shrink-0 w-10 h-10 rounded-lg bg-yellow-100 dark:bg-yellow-900/40 flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 text-yellow-600 dark:text-yellow-400"
fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<div class="font-medium text-neutral-900 dark:text-white truncate"
x-text="item.name">
</div>
<span
class="text-xs text-neutral-500 dark:text-neutral-400 shrink-0"
x-text="item.quickcommand"
x-show="item.quickcommand">
</span>
</div>
<div class="text-sm text-neutral-600 dark:text-neutral-400 truncate"
x-text="item.description">
</div>
</div>
</div>
<svg xmlns="http://www.w3.org/2000/svg"
class="shrink-0 h-5 w-5 text-yellow-500 dark:text-yellow-400 self-center"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</div>
</button>
</template>
</div> </div>
</template>
</div>
</template>
<template
x-if="searchQuery.length >= 2 && searchResults.length === 0 && filteredCreatableItems.length === 0 && !$wire.isSelectingResource && !$wire.autoOpenResource">
<div class="flex items-center justify-center py-12 px-4">
<div class="text-center">
<p class="mt-4 text-sm font-medium text-neutral-900 dark:text-white">
No results found
</p>
<p class="mt-1 text-sm text-neutral-500 dark:text-neutral-400">
Try different keywords or check the spelling
</p>
<p class="mt-2 text-xs text-neutral-400 dark:text-neutral-500">
💡 Tip: Search for service names like "wordpress", "postgres", or "redis"
</p>
</div> </div>
@elseif (strlen($searchQuery) > 0 && strlen($searchQuery) < 2) </div>
<div class="flex items-center justify-center py-12 px-4"> </template>
<div class="text-center">
<p class="text-sm text-neutral-600 dark:text-neutral-400">
Type at least 2 characters to search
</p>
</div>
</div>
@endif
</div>
</div> </div>
@endif </div>
</div> </div>
</div> </div>
</template> </template>
@ -548,4 +1127,5 @@ class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5
</div> </div>
</template> </template>
</div> </div>
</div> </div>

View file

@ -1,4 +1,4 @@
<x-forms.select wire:model.live="selectedTeamId" label="Current Team"> <x-forms.select wire:model.live="selectedTeamId">
<option value="default" disabled selected>Switch team</option> <option value="default" disabled selected>Switch team</option>
@foreach (auth()->user()->teams as $team) @foreach (auth()->user()->teams as $team)
<option value="{{ $team->id }}">{{ $team->name }}</option> <option value="{{ $team->id }}">{{ $team->name }}</option>

View file

@ -7,107 +7,123 @@
services: services:
museum: museum:
image: ghcr.io/ente-io/server:latest image: 'ghcr.io/ente-io/server:613c6a96390d7a624cf30b946955705d632423cc' # Released at 2025-09-14T22:16:37-07:00
environment: environment:
- SERVICE_URL_MUSEUM_8080 - SERVICE_URL_MUSEUM_8080
- ENTE_HTTP_USE_TLS=${ENTE_HTTP_USE_TLS:-false} - ENTE_DB_HOST=postgres
- ENTE_DB_PORT=5432
- ENTE_APPS_PUBLIC_ALBUMS=${SERVICE_URL_WEB_3002} - 'ENTE_DB_NAME=${POSTGRES_DB:-ente_db}'
- ENTE_APPS_CAST=${SERVICE_URL_WEB_3004} - 'ENTE_DB_USER=${SERVICE_USER_POSTGRES}'
- ENTE_APPS_ACCOUNTS=${SERVICE_URL_WEB_3001} - 'ENTE_DB_PASSWORD=${SERVICE_PASSWORD_POSTGRES}'
- 'ENTE_HTTP_USE_TLS=${ENTE_HTTP_USE_TLS:-false}'
- ENTE_DB_HOST=${ENTE_DB_HOST:-postgres} - ENTE_S3_ARE_LOCAL_BUCKETS=false
- ENTE_DB_PORT=${ENTE_DB_PORT:-5432} - ENTE_S3_USE_PATH_STYLE_URLS=true
- ENTE_DB_NAME=${ENTE_DB_NAME:-ente_db} - 'ENTE_S3_B2_EU_CEN_KEY=${SERVICE_USER_MINIO}'
- ENTE_DB_USER=${SERVICE_USER_POSTGRES:-pguser} - 'ENTE_S3_B2_EU_CEN_SECRET=${SERVICE_PASSWORD_MINIO}'
- ENTE_DB_PASSWORD=${SERVICE_PASSWORD_POSTGRES} - 'ENTE_S3_B2_EU_CEN_ENDPOINT=${SERVICE_FQDN_MINIO_9000}'
- ENTE_S3_B2_EU_CEN_REGION=eu-central-2
- ENTE_KEY_ENCRYPTION=${SERVICE_REALBASE64_ENCRYPTION} - ENTE_S3_B2_EU_CEN_BUCKET=b2-eu-cen
- ENTE_KEY_HASH=${SERVICE_REALBASE64_64_HASH} - 'ENTE_KEY_ENCRYPTION=${SERVICE_REALBASE64_ENCRYPTION}'
- 'ENTE_KEY_HASH=${SERVICE_REALBASE64_64_HASH}'
- ENTE_JWT_SECRET=${SERVICE_REALBASE64_JWT} - 'ENTE_JWT_SECRET=${SERVICE_REALBASE64_JWT}'
- 'ENTE_INTERNAL_ADMIN=${ENTE_INTERNAL_ADMIN:-1580559962386438}'
- ENTE_INTERNAL_ADMIN=${ENTE_INTERNAL_ADMIN:-1580559962386438} - 'ENTE_INTERNAL_DISABLE_REGISTRATION=${ENTE_INTERNAL_DISABLE_REGISTRATION:-false}'
- ENTE_INTERNAL_DISABLE_REGISTRATION=${ENTE_INTERNAL_DISABLE_REGISTRATION:-false}
# S3/MinIO configuration
- S3_ARE_LOCAL_BUCKETS=true
- S3_USE_PATH_STYLE_URLS=true
- S3_B2_EU_CEN_KEY=${SERVICE_USER_MINIO}
- S3_B2_EU_CEN_SECRET=${SERVICE_PASSWORD_MINIO}
- S3_B2_EU_CEN_ENDPOINT=${SERVICE_URL_MINIO_3200}
- S3_B2_EU_CEN_REGION=eu-central-2
- S3_B2_EU_CEN_BUCKET=b2-eu-cen
volumes: volumes:
- museum-data:/data - 'museum-data:/data'
- museum-config:/config - 'museum-config:/config'
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
minio: minio:
condition: service_started condition: service_started
healthcheck: healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/ping"] test:
interval: 5s - CMD
timeout: 5s - wget
retries: 10 - '--spider'
- 'http://127.0.0.1:8080/ping'
interval: 30s
timeout: 10s
retries: 3
web: web:
image: ghcr.io/ente-io/web image: 'ghcr.io/ente-io/web:ca03165f5e7f2a50105e6e40019c17ae6cdd934f' # Released at 2025-10-08T00:57:05-07:00
environment: environment:
- SERVICE_URL_WEB_3000 - SERVICE_URL_WEB_3000
- ENTE_API_ORIGIN=${SERVICE_URL_MUSEUM} - 'ENTE_API_ORIGIN=${SERVICE_URL_MUSEUM}'
- ENTE_ALBUMS_ORIGIN=${SERVICE_URL_WEB_3002}
healthcheck: healthcheck:
test: ["CMD", "curl", "--fail", "http://127.0.0.1:3000"] test:
interval: 5s - CMD
timeout: 5s - curl
retries: 10 - '--fail'
- 'http://localhost:3000'
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
postgres: postgres:
image: postgres:15-alpine image: 'postgres:15-alpine'
environment: environment:
- POSTGRES_USER=${SERVICE_USER_POSTGRES} - 'POSTGRES_USER=${SERVICE_USER_POSTGRES}'
- POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} - 'POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES}'
- POSTGRES_DB=${POSTGRES_DB:-ente_db} - 'POSTGRES_DB=${POSTGRES_DB:-ente_db}'
volumes: volumes:
- postgres-data:/var/lib/postgresql/data - 'postgres-data:/var/lib/postgresql/data'
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] test:
interval: 5s - CMD-SHELL
- 'pg_isready -U ${SERVICE_USER_POSTGRES} -d ${POSTGRES_DB:-ente_db}'
interval: 10s
timeout: 5s timeout: 5s
retries: 10 retries: 5
minio: minio:
image: quay.io/minio/minio:latest image: 'quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z' # Released at 2025-09-07T16-13-09Z
command: 'server /data --console-address ":9001"'
environment: environment:
- SERVICE_URL_MINIO_9000 - MINIO_SERVER_URL=$MINIO_SERVER_URL
- MINIO_ROOT_USER=${SERVICE_USER_MINIO} - MINIO_BROWSER_REDIRECT_URL=$MINIO_BROWSER_REDIRECT_URL
- MINIO_ROOT_PASSWORD=${SERVICE_PASSWORD_MINIO} - MINIO_ROOT_USER=$SERVICE_USER_MINIO
command: server /data --address ":9000" --console-address ":9001" - MINIO_ROOT_PASSWORD=$SERVICE_PASSWORD_MINIO
volumes: volumes:
- minio-data:/data - 'minio-data:/data'
healthcheck: healthcheck:
test: ["CMD", "mc", "ready", "local"] test:
- CMD
- mc
- ready
- local
interval: 5s interval: 5s
timeout: 20s timeout: 20s
retries: 10 retries: 10
minio-init: minio-init:
image: minio/mc:latest image: 'minio/mc:RELEASE.2025-08-13T08-35-41Z' # Released at 2025-08-13T08-35-41Z
exclude_from_hc: true
restart: no
depends_on: depends_on:
minio: minio:
condition: service_healthy condition: service_started
restart: on-failure
exclude_from_hc: true
environment: environment:
- MINIO_ROOT_USER=${SERVICE_USER_MINIO} - 'MINIO_ROOT_USER=${SERVICE_USER_MINIO}'
- MINIO_ROOT_PASSWORD=${SERVICE_PASSWORD_MINIO} - 'MINIO_ROOT_PASSWORD=${SERVICE_PASSWORD_MINIO}'
entrypoint: > - 'MINIO_CORS_URLS=$SERVICE_URL_MUSEUM,$SERVICE_URL_WEB'
entrypoint: |-
/bin/sh -c " /bin/sh -c "
mc alias set minio http://minio:9000 $${MINIO_ROOT_USER} $${MINIO_ROOT_PASSWORD}; echo \"MINIO_CORS_URLS: \$${MINIO_CORS_URLS}\";
mc mb minio/b2-eu-cen --ignore-existing; sleep 5;
mc mb minio/wasabi-eu-central-2-v3 --ignore-existing; until mc alias set minio http://minio:9000 \$${MINIO_ROOT_USER} \$${MINIO_ROOT_PASSWORD}; do
mc mb minio/scw-eu-fr-v3 --ignore-existing; echo 'Waiting for MinIO...';
echo 'MinIO buckets created successfully'; sleep 2;
done;
mc admin config set minio api cors_allow_origin='$MINIO_CORS_URLS' || true;
mc mb minio/b2-eu-cen --ignore-existing;
mc mb minio/wasabi-eu-central-2-v3 --ignore-existing;
mc mb minio/scw-eu-fr-v3 --ignore-existing;
echo 'MinIO buckets and CORS configured';
" "