mirror of
https://github.com/coollabsio/coolify.git
synced 2026-06-14 03:19:51 +00:00
Merge remote-tracking branch 'origin/next' into api-sensitive-data-scrubber
This commit is contained in:
@@ -4,6 +4,7 @@ namespace App\Console\Commands\Generate;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class Services extends Command
|
||||
@@ -77,6 +78,7 @@ class Services extends Command
|
||||
'category' => $data->get('category'),
|
||||
'logo' => $data->get('logo', 'svgs/default.webp'),
|
||||
'minversion' => $data->get('minversion', '0.0.0'),
|
||||
'template_last_updated_at' => $this->templateLastUpdatedAt($file),
|
||||
];
|
||||
|
||||
if ($port = $data->get('port')) {
|
||||
@@ -99,6 +101,26 @@ class Services extends Command
|
||||
return $payload;
|
||||
}
|
||||
|
||||
private function templateLastUpdatedAt(string $file): ?string
|
||||
{
|
||||
$process = Process::path(base_path())->run([
|
||||
'git',
|
||||
'log',
|
||||
'-1',
|
||||
'--format=%cI',
|
||||
'--',
|
||||
"templates/compose/{$file}",
|
||||
]);
|
||||
|
||||
if ($process->failed()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$timestamp = trim($process->output());
|
||||
|
||||
return $timestamp === '' ? null : $timestamp;
|
||||
}
|
||||
|
||||
private function generateServiceTemplatesWithFqdn(): void
|
||||
{
|
||||
$serviceTemplatesWithFqdn = collect(array_merge(
|
||||
@@ -155,6 +177,7 @@ class Services extends Command
|
||||
'category' => $data->get('category'),
|
||||
'logo' => $data->get('logo', 'svgs/default.webp'),
|
||||
'minversion' => $data->get('minversion', '0.0.0'),
|
||||
'template_last_updated_at' => $this->templateLastUpdatedAt($file),
|
||||
];
|
||||
|
||||
if ($port = $data->get('port')) {
|
||||
@@ -232,6 +255,7 @@ class Services extends Command
|
||||
'category' => $data->get('category'),
|
||||
'logo' => $data->get('logo', 'svgs/default.webp'),
|
||||
'minversion' => $data->get('minversion', '0.0.0'),
|
||||
'template_last_updated_at' => $this->templateLastUpdatedAt($file),
|
||||
];
|
||||
|
||||
if ($port = $data->get('port')) {
|
||||
|
||||
@@ -1053,6 +1053,7 @@ class GlobalSearch extends Component
|
||||
'quickcommand' => '(type: new postgresql)',
|
||||
'type' => 'postgresql',
|
||||
'category' => 'Databases',
|
||||
'logo' => 'svgs/postgresql.svg',
|
||||
'resourceType' => 'database',
|
||||
]);
|
||||
|
||||
@@ -1062,6 +1063,7 @@ class GlobalSearch extends Component
|
||||
'quickcommand' => '(type: new mysql)',
|
||||
'type' => 'mysql',
|
||||
'category' => 'Databases',
|
||||
'logo' => 'svgs/mysql.svg',
|
||||
'resourceType' => 'database',
|
||||
]);
|
||||
|
||||
@@ -1071,6 +1073,7 @@ class GlobalSearch extends Component
|
||||
'quickcommand' => '(type: new mariadb)',
|
||||
'type' => 'mariadb',
|
||||
'category' => 'Databases',
|
||||
'logo' => 'svgs/mariadb.svg',
|
||||
'resourceType' => 'database',
|
||||
]);
|
||||
|
||||
@@ -1080,6 +1083,7 @@ class GlobalSearch extends Component
|
||||
'quickcommand' => '(type: new redis)',
|
||||
'type' => 'redis',
|
||||
'category' => 'Databases',
|
||||
'logo' => 'svgs/redis.svg',
|
||||
'resourceType' => 'database',
|
||||
]);
|
||||
|
||||
@@ -1089,6 +1093,7 @@ class GlobalSearch extends Component
|
||||
'quickcommand' => '(type: new keydb)',
|
||||
'type' => 'keydb',
|
||||
'category' => 'Databases',
|
||||
'logo' => 'svgs/keydb.svg',
|
||||
'resourceType' => 'database',
|
||||
]);
|
||||
|
||||
@@ -1098,6 +1103,7 @@ class GlobalSearch extends Component
|
||||
'quickcommand' => '(type: new dragonfly)',
|
||||
'type' => 'dragonfly',
|
||||
'category' => 'Databases',
|
||||
'logo' => 'svgs/dragonfly.svg',
|
||||
'resourceType' => 'database',
|
||||
]);
|
||||
|
||||
@@ -1107,6 +1113,7 @@ class GlobalSearch extends Component
|
||||
'quickcommand' => '(type: new mongodb)',
|
||||
'type' => 'mongodb',
|
||||
'category' => 'Databases',
|
||||
'logo' => 'svgs/mongodb.svg',
|
||||
'resourceType' => 'database',
|
||||
]);
|
||||
|
||||
@@ -1116,6 +1123,7 @@ class GlobalSearch extends Component
|
||||
'quickcommand' => '(type: new clickhouse)',
|
||||
'type' => 'clickhouse',
|
||||
'category' => 'Databases',
|
||||
'logo' => 'svgs/clickhouse-icon.svg',
|
||||
'resourceType' => 'database',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Livewire\Component;
|
||||
|
||||
class Select extends Component
|
||||
@@ -107,7 +106,7 @@ class Select extends Component
|
||||
public function loadServices()
|
||||
{
|
||||
$services = get_service_templates();
|
||||
$templateLastUpdatedMap = $this->serviceTemplateLastUpdatedMap($services->keys());
|
||||
$templateLastUpdatedMap = $this->serviceTemplateLastUpdatedMap($services);
|
||||
|
||||
$services = collect($services)->map(function ($service, $key) use ($templateLastUpdatedMap) {
|
||||
$default_logo = 'images/default.webp';
|
||||
@@ -279,19 +278,31 @@ class Select extends Component
|
||||
return $this->formatLastModified($this->serviceTemplatesPath());
|
||||
}
|
||||
|
||||
private function serviceTemplateLastUpdatedMap(Collection $serviceNames): array
|
||||
private function serviceTemplateLastUpdatedMap(Collection $services): array
|
||||
{
|
||||
$bundleMtime = file_exists($this->serviceTemplatesPath()) ? filemtime($this->serviceTemplatesPath()) : 0;
|
||||
return $services
|
||||
->mapWithKeys(fn ($service, $serviceName) => [
|
||||
(string) $serviceName => $this->serviceTemplateLastUpdatedFromPayload($service)
|
||||
?? $this->serviceTemplateLastUpdated((string) $serviceName),
|
||||
])
|
||||
->all();
|
||||
}
|
||||
|
||||
return Cache::remember(
|
||||
"service-template-last-updated-map:{$bundleMtime}",
|
||||
now()->addDay(),
|
||||
fn () => $serviceNames
|
||||
->mapWithKeys(fn ($serviceName) => [
|
||||
(string) $serviceName => $this->serviceTemplateLastUpdated((string) $serviceName),
|
||||
])
|
||||
->all()
|
||||
);
|
||||
private function serviceTemplateLastUpdatedFromPayload(mixed $service): ?string
|
||||
{
|
||||
$timestamp = data_get($service, 'template_last_updated_at');
|
||||
|
||||
if (! is_string($timestamp) || $timestamp === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return CarbonImmutable::parse($timestamp)
|
||||
->timezone(config('app.timezone'))
|
||||
->format('M j, Y H:i');
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function serviceTemplateLastUpdated(string $serviceName): ?string
|
||||
|
||||
@@ -1057,7 +1057,6 @@ function sslip(Server $server)
|
||||
|
||||
function get_service_templates(bool $force = false): Collection
|
||||
{
|
||||
|
||||
if ($force) {
|
||||
try {
|
||||
$response = Http::retry(3, 1000)->get(config('constants.services.official'));
|
||||
@@ -1068,15 +1067,16 @@ function get_service_templates(bool $force = false): Collection
|
||||
|
||||
return collect($services);
|
||||
} catch (Throwable) {
|
||||
$services = File::get(base_path('templates/'.config('constants.services.file_name')));
|
||||
|
||||
return collect(json_decode($services))->sortKeys();
|
||||
return get_service_templates();
|
||||
}
|
||||
} else {
|
||||
$services = File::get(base_path('templates/'.config('constants.services.file_name')));
|
||||
|
||||
return collect(json_decode($services))->sortKeys();
|
||||
}
|
||||
|
||||
$path = base_path('templates/'.config('constants.services.file_name'));
|
||||
$mtime = filemtime($path) ?: 0;
|
||||
|
||||
return Cache::remember("service-templates:{$mtime}", now()->addDay(), function () use ($path) {
|
||||
return collect(json_decode(File::get($path)))->sortKeys();
|
||||
});
|
||||
}
|
||||
|
||||
function getResourceByUuid(string $uuid, ?int $teamId = null)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<svg viewBox="1.70837 1.875 22.25025 22.2493" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>svg{color:#d4d4d4}</style>
|
||||
<rect x="2.70837" y="2.875" width="2.24992" height="20.2493" rx="0.236664" />
|
||||
<rect x="7.2085" y="2.875" width="2.24992" height="20.2493" rx="0.236664" />
|
||||
<rect x="11.7086" y="2.875" width="2.24992" height="20.2493" rx="0.236664" />
|
||||
<rect x="16.2076" y="2.875" width="2.24992" height="20.2493" rx="0.236664" />
|
||||
<rect x="20.7087" y="10.7502" width="2.24992" height="4.49985" rx="0.236664" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 534 B |
@@ -1 +1,8 @@
|
||||
<svg width="215" height="90" viewBox="0 0 100 43" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0_378_10860)"><rect x="2.70837" y="2.875" width="2.24992" height="20.2493" rx="0.236664" fill="currentColor" /><rect x="7.2085" y="2.875" width="2.24992" height="20.2493" rx="0.236664" fill="currentColor" /><rect x="11.7086" y="2.875" width="2.24992" height="20.2493" rx="0.236664" fill="currentColor" /><rect x="16.2076" y="2.875" width="2.24992" height="20.2493" rx="0.236664" fill="currentColor" /><rect x="20.7087" y="10.7502" width="2.24992" height="4.49985" rx="0.236664" fill="currentColor" /></g></svg>
|
||||
<svg viewBox="1.70837 1.875 22.25025 22.2493" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>svg{color:#d4d4d4}</style>
|
||||
<rect x="2.70837" y="2.875" width="2.24992" height="20.2493" rx="0.236664" />
|
||||
<rect x="7.2085" y="2.875" width="2.24992" height="20.2493" rx="0.236664" />
|
||||
<rect x="11.7086" y="2.875" width="2.24992" height="20.2493" rx="0.236664" />
|
||||
<rect x="16.2076" y="2.875" width="2.24992" height="20.2493" rx="0.236664" />
|
||||
<rect x="20.7087" y="10.7502" width="2.24992" height="4.49985" rx="0.236664" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 642 B After Width: | Height: | Size: 534 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 88 88" fill="none"><style>svg{color:#d4d4d4}</style><path fill-rule="evenodd" clip-rule="evenodd" d="M44 0C7.766 0 0 7.766 0 44C0 80.234 7.766 88 44 88C80.234 88 88 80.234 88 44C88 7.766 80.234 0 44 0ZM39.1171 32.53C39.6448 32.8121 40 33.4016 40 34C40 36.2091 41.7909 38 44 38C46.2091 38 48 36.2091 48 34C48 33.4016 48.3552 32.8121 48.8829 32.53C50.1428 31.8566 51 30.5284 51 29C51 26.7909 49.2091 25 47 25C46.6142 25 46.2411 25.0546 45.8881 25.1566C44.7322 25.4904 43.2678 25.4904 42.1119 25.1566C41.7589 25.0546 41.3858 25 41 25C38.7909 25 37 26.7909 37 29C37 30.5284 37.8572 31.8566 39.1171 32.53ZM40.6174 37.6822C40.4565 37.7954 40.3289 37.9561 40.2566 38.1489L39.2486 40.837C39.0877 41.266 39.079 41.7371 39.2238 42.1717L40.8506 47.0521C40.9488 47.3466 41.1133 47.6174 41.374 47.786C41.8309 48.0815 42.7062 48.5 43.9999 48.5C45.2937 48.5 46.169 48.0815 46.6259 47.786C46.8866 47.6174 47.0511 47.3466 47.1492 47.0521L48.776 42.1717C48.9209 41.7371 48.9122 41.266 48.7513 40.837L47.7433 38.1489C47.671 37.9561 47.5434 37.7954 47.3825 37.6822C46.4922 38.5005 45.3044 39 43.9999 39C42.6955 39 41.5077 38.5005 40.6174 37.6822ZM42 60L41.0211 48.7423C41.6348 49.0994 42.6312 49.5 44 49.5C45.3684 49.5 46.365 49.0996 46.979 48.7423L46 60C46 60 45.5 60.5 44 60.5C42.5 60.5 42 60 42 60ZM42.5 68L42.0111 61.1559C42.0291 61.1635 42.0474 61.171 42.0662 61.1785C42.5095 61.3558 43.1373 61.5 44 61.5C44.8628 61.5 45.4906 61.3558 45.9339 61.1785C45.9526 61.171 45.9709 61.1635 45.9888 61.156L45.5 68C45.5 68 45.25 69 44 69C42.75 69 42.5 68 42.5 68ZM18.4999 40H38.4C37.6448 41.2587 37.9828 42.1357 38.3163 43.001L38.3164 43.0012L38.3164 43.0013L38.3165 43.0014L38.3165 43.0015L38.3166 43.0017C38.3804 43.1673 38.4441 43.3326 38.4999 43.5L19.817 48.8504C18.6241 49.1865 17.3473 48.7543 16.6036 47.7628L15.4279 46.1951C14.7461 45.2861 14.6364 44.0698 15.1446 43.0535L15.8422 41.6582C16.3462 40.6501 17.3729 40.0096 18.4999 40ZM16 38.4663L38.7499 39C38.7499 39 39.1093 38.1012 39.2499 37.5C39.2981 37.294 39.349 37.0881 39.3922 36.9172C39.4529 36.6771 39.3398 36.4307 39.1124 36.3324C36.4624 35.187 21.0442 28.5249 18.4745 27.4663C17.4762 27.0394 16.3252 27.1853 15.4649 27.8476L13.17 29.6144C12.4171 30.1941 11.9975 31.0823 12 32C12.0008 32.2913 12.0441 32.5855 12.1328 32.8738L13.2451 36.4663C13.6189 37.6811 14.7301 38.4133 16 38.4663ZM69.5 40H49.6C50.3552 41.2587 50.0172 42.1357 49.6836 43.001L49.6836 43.0012L49.6835 43.0013L49.6835 43.0014C49.6196 43.1672 49.5559 43.3325 49.5 43.5L68.1829 48.8504C69.3759 49.1865 70.6527 48.7543 71.3963 47.7628L72.5721 46.1951C73.2539 45.2861 73.3636 44.0698 72.8554 43.0535L72.1578 41.6583C71.6537 40.6501 70.6271 40.0096 69.5 40ZM72 38.4663L49.25 39C49.25 39 48.8907 38.1012 48.75 37.5C48.7018 37.294 48.6509 37.0881 48.6077 36.9173C48.547 36.6771 48.6602 36.4307 48.8875 36.3325C51.5376 35.187 66.9558 28.5249 69.5255 27.4663C70.5238 27.0394 71.6748 27.1853 72.5351 27.8476L74.83 29.6144C75.5829 30.1941 76.0025 31.0823 76 32C75.9992 32.2913 75.9559 32.5855 75.8672 32.8738L74.7549 36.4663C74.3811 37.6811 73.2699 38.4133 72 38.4663Z" fill="currentColor"/></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1 @@
|
||||
<svg version="1.1" id="svg1326" viewBox="0 0 160 182" sodipodi:docname="keydb.svg" inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"><style>svg{color:#d4d4d4}</style><defs id="defs1330"/><sodipodi:namedview id="namedview1328" pagecolor="currentColor" bordercolor="#666666" borderopacity="1.0" inkscape:pageshadow="2" inkscape:pageopacity="0.00784314" inkscape:pagecheckerboard="true" showgrid="false" inkscape:zoom="1.5064209" inkscape:cx="374.06544" inkscape:cy="123.80338" inkscape:window-width="1536" inkscape:window-height="889" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="svg1326"/><path d="M 78.589334,169.85617 12.63903,131.77977 c -1.0281,-0.591 -1.6035,-1.6659 -1.6046,-2.7722 h -0.0134 V 52.833267 c 0,-1.2966 0.7688,-2.4135 1.8749,-2.9206 l 65.739904,-37.9545 c 1.0386,-0.5966 2.2717,-0.5456 3.2321,0.0264 l 65.951096,38.0761 c 1.0282,0.591 1.6042,1.6659 1.6047,2.7726 h 0.0134 v 76.174303 c 0,1.2962 -0.7685,2.4137 -1.8743,2.9202 l -65.740496,37.9554 c -1.0389,0.5964 -2.2717,0.5453 -3.233,-0.027 z M 17.44353,127.16987 v 0 l 62.785704,36.2488 62.785996,-36.2488 V 54.671267 l -62.785996,-36.2489 -62.785704,36.2489 z" style="fill:currentColor;fill-rule:evenodd" id="path1290"/><path d="M 80.229234,14.730167 14.23273,129.00757 h 131.9933 z" style="opacity:.88;fill:#ff0;fill-opacity:1;fill-rule:evenodd" id="path1292"/><path d="M 80.229234,21.136467 19.78603,125.79677 h 120.8861 z M 11.45983,127.40197 v 0 L 77.433934,13.163767 c 0.2713,-0.4856 0.6733,-0.9068 1.1895,-1.2056 1.531,-0.8864 3.4908,-0.3645 4.3778,1.1671 L 148.88863,127.21177 c 0.3464,0.5125 0.5485,1.1308 0.5485,1.7958 0,1.7733 -1.4378,3.2111 -3.2108,3.2111 H 14.23213 v -0.007 c -0.5459,5e-4 -1.099,-0.1386 -1.6052,-0.4318 -1.531,-0.8863 -2.0537,-2.8465 -1.1671,-4.3778 z" style="fill:currentColor;fill-rule:evenodd" id="path1294"/><path d="m 12.63903,55.605567 c -1.5309,-0.8799 -2.059,-2.8347 -1.1792,-4.3657 0.8802,-1.531 2.8347,-2.0591 4.3657,-1.1792 L 80.229234,87.229065 144.63323,50.060667 c 1.531,-0.8799 3.4855,-0.3518 4.3651,1.1792 0.8796,1.531 0.3517,3.4858 -1.1793,4.3657 L 81.867334,93.666065 c -0.9603,0.5723 -2.1931,0.6227 -3.2315,0.026 z" style="opacity:1;fill:currentColor;fill-rule:evenodd" id="path1296"/><path d="m 83.440034,167.11087 c 0,1.773 -1.4377,3.2111 -3.2108,3.2111 -1.7736,0 -3.2114,-1.4381 -3.2114,-3.2111 V 90.920065 c 0,-1.773 1.4378,-3.2111 3.2114,-3.2111 1.7731,0 3.2108,1.4381 3.2108,3.2111 z" style="fill:currentColor;fill-rule:evenodd" id="path1298"/><path d="m 146.22633,137.25697 c 4.5555,0 8.2491,-3.693 8.2491,-8.2494 0,-4.5564 -3.6936,-8.2491 -8.2491,-8.2491 -4.5564,0 -8.2503,3.6927 -8.2503,8.2491 0,4.5564 3.6939,8.2494 8.2503,8.2494 z" style="fill:currentColor;fill-rule:evenodd" id="path1300"/><path d="m 146.22633,61.082867 c 4.5555,0 8.2491,-3.6938 8.2491,-8.2496 0,-4.5562 -3.6936,-8.2494 -8.2491,-8.2494 -4.5564,0 -8.2503,3.6932 -8.2503,8.2494 0,4.5558 3.6939,8.2496 8.2503,8.2496 z" style="fill:currentColor;fill-rule:evenodd" id="path1302"/><path d="m 14.23213,61.082867 c 4.5561,0 8.25,-3.6938 8.25,-8.2496 0,-4.5562 -3.6939,-8.2494 -8.25,-8.2494 -4.5555003,0 -8.2494003,3.6932 -8.2494003,8.2494 0,4.5558 3.6939,8.2496 8.2494003,8.2496 z" style="fill:currentColor;fill-rule:evenodd" id="path1304"/><path d="m 14.23213,137.25697 c 4.5561,0 8.25,-3.693 8.25,-8.2494 0,-4.5564 -3.6939,-8.2491 -8.25,-8.2491 -4.5555003,0 -8.2494003,3.6927 -8.2494003,8.2491 0,4.5564 3.6939,8.2494 8.2494003,8.2494 z" style="fill:currentColor;fill-rule:evenodd" id="path1306"/><path d="m 80.229234,175.36027 c 4.5558,0 8.2497,-3.6933 8.2497,-8.2494 0,-4.5562 -3.6939,-8.2494 -8.2497,-8.2494 -4.5564,0 -8.2497,3.6932 -8.2497,8.2494 0,4.5561 3.6933,8.2494 8.2497,8.2494 z" style="fill:currentColor;fill-rule:evenodd" id="path1308"/><path d="m 80.229234,99.170065 c 4.5558,0 8.2497,-3.6938 8.2497,-8.25 0,-4.5561 -3.6939,-8.2494 -8.2497,-8.2494 -4.5564,0 -8.2497,3.6933 -8.2497,8.2494 0,4.5562 3.6933,8.25 8.2497,8.25 z" style="fill:currentColor;fill-rule:evenodd" id="path1310"/><path d="m 80.229234,22.979567 c 4.5558,0 8.2497,-3.6932 8.2497,-8.2494 0,-4.5561 -3.6939,-8.2492996 -8.2497,-8.2492996 -4.5564,0 -8.2497,3.6931996 -8.2497,8.2492996 0,4.5562 3.6933,8.2494 8.2497,8.2494 z" style="fill:currentColor;fill-rule:evenodd" id="path1312"/></svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
@@ -632,7 +632,7 @@
|
||||
@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-warning-50 dark:focus:bg-warning-900/20 border-transparent hover:border-coollabs focus:border-warning-500 dark:focus:border-warning-400">
|
||||
class="search-result-item block px-4 py-3 hover:bg-neutral-100 dark:hover:bg-coolgray-200 transition-colors focus:outline-none focus:bg-neutral-100 dark:focus:bg-coolgray-200 focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-coollabs dark:focus-visible:ring-warning">
|
||||
<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">
|
||||
@@ -696,12 +696,12 @@
|
||||
<!-- 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-warning-50 dark:hover:bg-warning-900/20 transition-colors focus:outline-none focus:bg-warning-100 dark:focus:bg-warning-900/30 border-transparent hover:border-warning-500 focus:border-warning-500">
|
||||
class="search-result-item w-full text-left block px-4 py-3 hover:bg-neutral-100 dark:hover:bg-coolgray-200 transition-colors focus:outline-none focus:bg-neutral-100 dark:focus:bg-coolgray-200 focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-coollabs dark:focus-visible:ring-warning">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3 flex-1 min-w-0">
|
||||
@if (! empty($item['logo']))
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center overflow-hidden">
|
||||
<img src="{{ asset($item['logo']) }}" alt="{{ $item['name'] }}" class="w-7 h-7 object-contain">
|
||||
<img src="{{ asset($item['logo']) }}" alt="{{ $item['name'] }}" class="w-8 h-8 object-contain">
|
||||
</div>
|
||||
@else
|
||||
<div
|
||||
@@ -755,7 +755,7 @@
|
||||
</template>
|
||||
<template x-for="(result, index) in searchResults" :key="index">
|
||||
<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-warning-50 dark:focus:bg-warning-900/20 border-transparent hover:border-coollabs focus:border-warning-500 dark:focus:border-warning-400">
|
||||
class="search-result-item block px-4 py-3 hover:bg-neutral-100 dark:hover:bg-coolgray-200 transition-colors focus:outline-none focus:bg-neutral-100 dark:focus:bg-coolgray-200 focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-coollabs dark:focus-visible:ring-warning">
|
||||
<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">
|
||||
@@ -811,12 +811,12 @@
|
||||
|
||||
<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-warning-50 dark:hover:bg-warning-900/20 transition-colors focus:outline-none focus:bg-warning-100 dark:focus:bg-warning-900/30 border-transparent hover:border-warning-500 focus:border-warning-500">
|
||||
class="search-result-item w-full text-left block px-4 py-3 hover:bg-neutral-100 dark:hover:bg-coolgray-200 transition-colors focus:outline-none focus:bg-neutral-100 dark:focus:bg-coolgray-200 focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-coollabs dark:focus-visible:ring-warning">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3 flex-1 min-w-0">
|
||||
<template x-if="item.logo">
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-neutral-100 dark:bg-neutral-800 flex items-center justify-center overflow-hidden">
|
||||
<img :src="'/' + item.logo" :alt="item.name" class="w-7 h-7 object-contain">
|
||||
<img :src="'/' + item.logo" :alt="item.name" class="w-8 h-8 object-contain">
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="!item.logo">
|
||||
|
||||
@@ -11,38 +11,55 @@
|
||||
@endcan
|
||||
</div>
|
||||
<div class="subtitle">All your projects are here.</div>
|
||||
<div class="grid grid-cols-1 gap-4 xl:grid-cols-2 -mt-1">
|
||||
@foreach ($projects as $project)
|
||||
<div class="relative gap-2 cursor-pointer coolbox group">
|
||||
<a href="{{ $project->navigateTo() }}" {{ wireNavigate() }} class="absolute inset-0"></a>
|
||||
<div class="flex flex-1 mx-6">
|
||||
<div class="flex flex-col justify-center flex-1">
|
||||
<div class="box-title">{{ $project->name }}</div>
|
||||
<div class="box-description">
|
||||
{{ $project->description }}
|
||||
@if ($projects->count() > 0)
|
||||
<div class="grid grid-cols-1 gap-4 xl:grid-cols-2 -mt-1">
|
||||
@foreach ($projects as $project)
|
||||
<div class="relative gap-2 cursor-pointer coolbox group">
|
||||
<a href="{{ $project->navigateTo() }}" {{ wireNavigate() }} class="absolute inset-0"></a>
|
||||
<div class="flex flex-1 mx-6">
|
||||
<div class="flex flex-col justify-center flex-1">
|
||||
<div class="box-title">{{ $project->name }}</div>
|
||||
<div class="box-description">
|
||||
{{ $project->description }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative z-10 flex items-center justify-center gap-4 text-xs font-bold">
|
||||
@if ($project->environments->first())
|
||||
@can('createAnyResource')
|
||||
<div class="relative z-10 flex items-center justify-center gap-4 text-xs font-bold">
|
||||
@if ($project->environments->first())
|
||||
@can('createAnyResource')
|
||||
<a class="hover:underline" {{ wireNavigate() }}
|
||||
href="{{ route('project.resource.create', [
|
||||
'project_uuid' => $project->uuid,
|
||||
'environment_uuid' => $project->environments->first()->uuid,
|
||||
]) }}">
|
||||
+ Add Resource
|
||||
</a>
|
||||
@endcan
|
||||
@endif
|
||||
@can('update', $project)
|
||||
<a class="hover:underline" {{ wireNavigate() }}
|
||||
href="{{ route('project.resource.create', [
|
||||
'project_uuid' => $project->uuid,
|
||||
'environment_uuid' => $project->environments->first()->uuid,
|
||||
]) }}">
|
||||
+ Add Resource
|
||||
href="{{ route('project.edit', ['project_uuid' => $project->uuid]) }}">
|
||||
Settings
|
||||
</a>
|
||||
@endcan
|
||||
@endif
|
||||
@can('update', $project)
|
||||
<a class="hover:underline" {{ wireNavigate() }}
|
||||
href="{{ route('project.edit', ['project_uuid' => $project->uuid]) }}">
|
||||
Settings
|
||||
</a>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class='font-bold dark:text-warning'>No projects found.</div>
|
||||
<div class="flex items-center gap-1">
|
||||
@can('createAnyResource')
|
||||
<x-modal-input buttonTitle="Add" title="New Project">
|
||||
<livewire:project.add-empty />
|
||||
</x-modal-input> your first project or
|
||||
@else
|
||||
Create your first project or
|
||||
@endcan
|
||||
go to the <a class="underline dark:text-white" href="{{ route('onboarding') }}"
|
||||
{{ wireNavigate() }}>onboarding</a> page.
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
it('shows an empty state when there are no projects', function () {
|
||||
$this->view('livewire.project.index', [
|
||||
'projects' => collect(),
|
||||
])
|
||||
->assertSee('No projects found.')
|
||||
->assertSee('onboarding');
|
||||
});
|
||||
|
||||
it('does not show the empty state when projects exist', function () {
|
||||
$project = new class
|
||||
{
|
||||
public string $name = 'Test Project';
|
||||
|
||||
public string $description = 'A project description';
|
||||
|
||||
public string $uuid = 'test-project-uuid';
|
||||
|
||||
public $environments;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->environments = collect();
|
||||
}
|
||||
|
||||
public function navigateTo(): string
|
||||
{
|
||||
return '#';
|
||||
}
|
||||
};
|
||||
|
||||
$this->view('livewire.project.index', [
|
||||
'projects' => collect([$project]),
|
||||
])
|
||||
->assertSee('Test Project')
|
||||
->assertDontSee('No projects found.');
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use App\Console\Commands\Generate\Services;
|
||||
use Illuminate\Console\OutputStyle;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Output\NullOutput;
|
||||
|
||||
it('adds the last git commit timestamp to generated service template payloads', function () {
|
||||
$command = new Services;
|
||||
$command->setOutput(new OutputStyle(new ArrayInput([]), new NullOutput));
|
||||
$method = new ReflectionMethod($command, 'processFile');
|
||||
|
||||
$payload = $method->invoke($command, 'activepieces.yaml');
|
||||
|
||||
$expectedTimestamp = Process::run([
|
||||
'git',
|
||||
'log',
|
||||
'-1',
|
||||
'--format=%cI',
|
||||
'--',
|
||||
'templates/compose/activepieces.yaml',
|
||||
])->throw()->output();
|
||||
|
||||
expect($payload)
|
||||
->toHaveKey('template_last_updated_at')
|
||||
->and($payload['template_last_updated_at'])
|
||||
->toBe(trim($expectedTimestamp));
|
||||
});
|
||||
@@ -3,6 +3,7 @@
|
||||
use App\Livewire\Project\New\Select;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Support\ViewErrorBag;
|
||||
|
||||
@@ -22,38 +23,56 @@ it('returns the service templates bundle last updated timestamp', function () {
|
||||
->toBe(CarbonImmutable::createFromTimestamp(filemtime($templatePath))->timezone(config('app.timezone'))->format('M j, Y H:i'));
|
||||
});
|
||||
|
||||
it('returns each service template last updated timestamp', function () {
|
||||
it('returns each service template last updated timestamp from the generated bundle', function () {
|
||||
$component = new Select;
|
||||
$templatePath = base_path('templates/compose/activepieces.yaml');
|
||||
$templates = json_decode(file_get_contents(base_path('templates/'.config('constants.services.file_name'))), true);
|
||||
$templateTimestamp = $templates['activepieces']['template_last_updated_at'];
|
||||
|
||||
$resources = $component->loadServices();
|
||||
|
||||
expect($resources['services']['activepieces'])
|
||||
->toHaveKey('templateLastUpdated')
|
||||
->and($resources['services']['activepieces']['templateLastUpdated'])
|
||||
->toBe(CarbonImmutable::createFromTimestamp(filemtime($templatePath))->timezone(config('app.timezone'))->format('M j, Y H:i'));
|
||||
->toBe(CarbonImmutable::parse($templateTimestamp)->timezone(config('app.timezone'))->format('M j, Y H:i'));
|
||||
});
|
||||
|
||||
it('uses a service template timestamp cache keyed by bundle mtime', function () {
|
||||
$bundleMtime = filemtime(base_path('templates/'.config('constants.services.file_name')));
|
||||
Cache::put("service-template-last-updated-map:{$bundleMtime}", [
|
||||
'activepieces' => 'Cached timestamp',
|
||||
], now()->addDay());
|
||||
it('prefers embedded service template git timestamps from the templates bundle', function () {
|
||||
File::shouldReceive('get')
|
||||
->with(base_path('templates/'.config('constants.services.file_name')))
|
||||
->andReturn(json_encode([
|
||||
'activepieces' => [
|
||||
'documentation' => 'https://coolify.io/docs',
|
||||
'slogan' => 'Open source no-code business automation.',
|
||||
'compose' => '',
|
||||
'tags' => null,
|
||||
'category' => 'automation',
|
||||
'logo' => 'images/default.webp',
|
||||
'minversion' => '0.0.0',
|
||||
'template_last_updated_at' => '2026-05-31T12:34:56+00:00',
|
||||
],
|
||||
]));
|
||||
|
||||
$resources = (new Select)->loadServices();
|
||||
|
||||
expect($resources['services']['activepieces']['templateLastUpdated'])->toBe('Cached timestamp');
|
||||
expect($resources['services']['activepieces']['templateLastUpdated'])->toBe('May 31, 2026 12:34');
|
||||
});
|
||||
|
||||
it('does not use stale service template timestamp cache entries from another bundle mtime', function () {
|
||||
$bundleMtime = filemtime(base_path('templates/'.config('constants.services.file_name')));
|
||||
Cache::put('service-template-last-updated-map:'.($bundleMtime - 1), [
|
||||
'activepieces' => 'Stale cached timestamp',
|
||||
], now()->addDay());
|
||||
it('caches parsed local service templates by bundle mtime', function () {
|
||||
Cache::flush();
|
||||
|
||||
$resources = (new Select)->loadServices();
|
||||
$path = base_path('templates/'.config('constants.services.file_name'));
|
||||
$json = file_get_contents($path);
|
||||
|
||||
expect($resources['services']['activepieces']['templateLastUpdated'])->not->toBe('Stale cached timestamp');
|
||||
File::partialMock()
|
||||
->shouldReceive('get')
|
||||
->once()
|
||||
->with($path)
|
||||
->andReturn($json);
|
||||
|
||||
$first = get_service_templates();
|
||||
$second = get_service_templates();
|
||||
|
||||
expect($first->keys()->all())->toBe($second->keys()->all());
|
||||
});
|
||||
|
||||
it('renders the service templates last updated hint placeholder', function () {
|
||||
|
||||
@@ -25,12 +25,12 @@ it('ensures GlobalSearch clears search query when starting resource creation', f
|
||||
->toContain('$this->searchQuery = \'\'');
|
||||
});
|
||||
|
||||
it('ensures GlobalSearch uses Livewire redirect method', function () {
|
||||
it('ensures GlobalSearch uses redirect route helper', function () {
|
||||
$globalSearchFile = file_get_contents(__DIR__.'/../../app/Livewire/GlobalSearch.php');
|
||||
|
||||
// Check that completeResourceCreation uses $this->redirect()
|
||||
// Check that completeResourceCreation uses the shared redirect route helper
|
||||
expect($globalSearchFile)
|
||||
->toContain('$this->redirect(route(\'project.resource.create\'');
|
||||
->toContain('redirectRoute($this, \'project.resource.create\'');
|
||||
});
|
||||
|
||||
it('ensures docker-image item has quickcommand with new image', function () {
|
||||
@@ -42,3 +42,103 @@ it('ensures docker-image item has quickcommand with new image', function () {
|
||||
->toContain("'quickcommand' => '(type: new image)'")
|
||||
->toContain("'type' => 'docker-image'");
|
||||
});
|
||||
|
||||
it('uses neutral hover styling for GlobalSearch quick action rows', function () {
|
||||
$bladeFile = file_get_contents(__DIR__.'/../../resources/views/livewire/global-search.blade.php');
|
||||
|
||||
preg_match_all('/<button[^>]+(?:wire:click="navigateToResource|@click="\\$wire\\.navigateToResource)[^>]+>/s', $bladeFile, $quickActionButtons);
|
||||
|
||||
expect($quickActionButtons[0])->not->toBeEmpty();
|
||||
|
||||
foreach ($quickActionButtons[0] as $quickActionButton) {
|
||||
expect($quickActionButton)
|
||||
->toContain('hover:bg-neutral-100 dark:hover:bg-coolgray-200')
|
||||
->toContain('focus:bg-neutral-100 dark:focus:bg-coolgray-200')
|
||||
->toContain('focus-visible:ring-coollabs dark:focus-visible:ring-warning')
|
||||
->not->toContain('hover:bg-warning-50')
|
||||
->not->toContain('hover:border-warning-500');
|
||||
}
|
||||
});
|
||||
|
||||
it('uses product logos for GlobalSearch database quick actions', function () {
|
||||
$globalSearchFile = file_get_contents(__DIR__.'/../../app/Livewire/GlobalSearch.php');
|
||||
$bladeFile = file_get_contents(__DIR__.'/../../resources/views/livewire/global-search.blade.php');
|
||||
|
||||
expect($bladeFile)
|
||||
->toContain('asset($item[\'logo\'])')
|
||||
->toContain(":src=\"'/' + item.logo\"");
|
||||
|
||||
foreach ([
|
||||
'postgresql' => 'svgs/postgresql.svg',
|
||||
'mysql' => 'svgs/mysql.svg',
|
||||
'mariadb' => 'svgs/mariadb.svg',
|
||||
'redis' => 'svgs/redis.svg',
|
||||
'keydb' => 'svgs/keydb.svg',
|
||||
'dragonfly' => 'svgs/dragonfly.svg',
|
||||
'mongodb' => 'svgs/mongodb.svg',
|
||||
'clickhouse' => 'svgs/clickhouse-icon.svg',
|
||||
] as $type => $logo) {
|
||||
expect($globalSearchFile)
|
||||
->toContain("'type' => '{$type}'")
|
||||
->toContain("'logo' => '{$logo}'");
|
||||
|
||||
expect(file_exists(__DIR__.'/../../public/'.$logo))->toBeTrue();
|
||||
}
|
||||
});
|
||||
|
||||
it('uses neutral hover styling for GlobalSearch existing resource rows', function () {
|
||||
$bladeFile = file_get_contents(__DIR__.'/../../resources/views/livewire/global-search.blade.php');
|
||||
|
||||
preg_match_all('/<a[^>]+(?:href="{{ \\$result\\[\'link\'\\]|:href="result\\.link)[^>]+>/s', $bladeFile, $existingResourceLinks);
|
||||
|
||||
expect($existingResourceLinks[0])->not->toBeEmpty();
|
||||
|
||||
foreach ($existingResourceLinks[0] as $existingResourceLink) {
|
||||
expect($existingResourceLink)
|
||||
->toContain('hover:bg-neutral-100 dark:hover:bg-coolgray-200')
|
||||
->toContain('focus:bg-neutral-100 dark:focus:bg-coolgray-200')
|
||||
->toContain('focus-visible:ring-coollabs dark:focus-visible:ring-warning')
|
||||
->not->toContain('hover:bg-neutral-50')
|
||||
->not->toContain('focus:bg-warning')
|
||||
->not->toContain('hover:border-coollabs')
|
||||
->not->toContain('focus:border-warning');
|
||||
}
|
||||
});
|
||||
|
||||
it('uses visible cropped SVG marks for wide database logos', function () {
|
||||
$keydbLogo = file_get_contents(__DIR__.'/../../public/svgs/keydb.svg');
|
||||
$dragonflyLogo = file_get_contents(__DIR__.'/../../public/svgs/dragonfly.svg');
|
||||
$clickhouseLogo = file_get_contents(__DIR__.'/../../public/svgs/clickhouse-icon.svg');
|
||||
|
||||
expect($keydbLogo)
|
||||
->toContain('viewBox="0 0 160 182"')
|
||||
->toContain('svg{color:#d4d4d4}')
|
||||
->not->toContain('prefers-color-scheme');
|
||||
|
||||
expect($dragonflyLogo)
|
||||
->toContain('viewBox="0 0 88 88"')
|
||||
->toContain('svg{color:#d4d4d4}')
|
||||
->not->toContain('prefers-color-scheme');
|
||||
|
||||
expect($clickhouseLogo)
|
||||
->toContain('viewBox="1.70837 1.875 22.25025 22.2493"')
|
||||
->toContain('svg{color:#d4d4d4}')
|
||||
->toContain('x="20.7087"')
|
||||
->toContain('width="2.24992"')
|
||||
->not->toContain('viewBox="0 0 100 43"')
|
||||
->not->toContain('width="215"')
|
||||
->not->toContain('height="90"');
|
||||
});
|
||||
|
||||
it('uses cropped image assets instead of inline wide logos for GlobalSearch database icons', function () {
|
||||
$globalSearchFile = file_get_contents(__DIR__.'/../../app/Livewire/GlobalSearch.php');
|
||||
$bladeFile = file_get_contents(__DIR__.'/../../resources/views/livewire/global-search.blade.php');
|
||||
|
||||
expect($bladeFile)
|
||||
->toContain('class="w-8 h-8 object-contain"')
|
||||
->toContain('class="w-8 h-8 object-contain"')
|
||||
->not->toContain('$item[\'logo_html\']')
|
||||
->not->toContain('x-html="item.logo_html"');
|
||||
|
||||
expect($globalSearchFile)->not->toContain("'logo_html' =>");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user