No files matched your search
@@ -23,50 +23,22 @@ env:
|
||||
IMAGE_NAME: "coollabsio/coolify"
|
||||
|
||||
jobs:
|
||||
amd64:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Sanitize branch name for Docker tag
|
||||
id: sanitize
|
||||
run: |
|
||||
# Replace slashes and other invalid characters with dashes
|
||||
SANITIZED_NAME=$(echo "${{ github.ref_name }}" | sed 's/[\/]/-/g')
|
||||
echo "tag=${SANITIZED_NAME}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITHUB_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Build and Push Image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/production/Dockerfile
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}
|
||||
|
||||
aarch64:
|
||||
runs-on: [self-hosted, arm64]
|
||||
build-push:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
platform: linux/aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Sanitize branch name for Docker tag
|
||||
id: sanitize
|
||||
@@ -75,6 +47,9 @@ jobs:
|
||||
SANITIZED_NAME=$(echo "${{ github.ref_name }}" | sed 's/[\/]/-/g')
|
||||
echo "tag=${SANITIZED_NAME}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to ${{ env.GITHUB_REGISTRY }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
@@ -89,25 +64,29 @@ jobs:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_TOKEN }}
|
||||
|
||||
- name: Build and Push Image
|
||||
- name: Build and Push Image (${{ matrix.arch }})
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: docker/production/Dockerfile
|
||||
platforms: linux/aarch64
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-aarch64
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-aarch64
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-${{ matrix.arch }}
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-${{ matrix.arch }}
|
||||
cache-from: |
|
||||
type=gha,scope=build-${{ matrix.arch }}
|
||||
type=registry,ref=${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache-${{ matrix.arch }}
|
||||
cache-to: type=gha,mode=max,scope=build-${{ matrix.arch }}
|
||||
|
||||
merge-manifest:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-push
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
needs: [amd64, aarch64]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Sanitize branch name for Docker tag
|
||||
id: sanitize
|
||||
@@ -135,13 +114,15 @@ jobs:
|
||||
- name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
--append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-aarch64 \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-amd64 \
|
||||
${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-aarch64 \
|
||||
--tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}
|
||||
|
||||
- name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }}
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
--append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-aarch64 \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-amd64 \
|
||||
${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}-aarch64 \
|
||||
--tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.sanitize.outputs.tag }}
|
||||
|
||||
- uses: sarisia/actions-status-discord@v1
|
||||
|
||||
@@ -146,6 +146,7 @@ class MyComponent extends Component
|
||||
- State management handled on the server
|
||||
- Use wire:model for two-way data binding
|
||||
- Dispatch events for component communication
|
||||
- **CRITICAL**: Livewire component views **MUST** have exactly ONE root element. ALL content must be contained within this single root element. Placing ANY elements (`<style>`, `<script>`, `<div>`, comments, or any other HTML) outside the root element will break Livewire's component tracking and cause `wire:click` and other directives to fail silently.
|
||||
|
||||
### Code Organization Patterns
|
||||
- **Actions Pattern**: Use Actions for complex business logic (`app/Actions/`)
|
||||
|
||||
@@ -66,7 +66,7 @@ Thank you so much!
|
||||
* [CompAI](https://www.trycomp.ai?ref=coolify.io) - Open source compliance automation platform
|
||||
* [Convex](https://convex.link/coolify.io) - Open-source reactive database for web app developers
|
||||
* [CubePath](https://cubepath.com/?ref=coolify.io) - Dedicated Servers & Instant Deploy
|
||||
* [Darweb](https://darweb.nl/?ref=coolify.io) - Design. Develop. Deliver. Specialized in 3D CPQ Solutions
|
||||
* [Dade2](https://dade2.net/?ref=coolify.io) - IT Consulting, Cloud Solutions & System Integration
|
||||
* [Formbricks](https://formbricks.com?ref=coolify.io) - The open source feedback platform
|
||||
* [GoldenVM](https://billing.goldenvm.com?ref=coolify.io) - Premium virtual machine hosting solutions
|
||||
* [Gozunga](https://gozunga.com?ref=coolify.io) - Seriously Simple Cloud Infrastructure
|
||||
|
||||
@@ -11,9 +11,11 @@ class InstallDocker
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
private string $dockerVersion;
|
||||
|
||||
public function handle(Server $server)
|
||||
{
|
||||
$dockerVersion = config('constants.docker.minimum_required_version');
|
||||
$this->dockerVersion = config('constants.docker.minimum_required_version');
|
||||
$supported_os_type = $server->validateOS();
|
||||
if (! $supported_os_type) {
|
||||
throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://coolify.io/docs/installation#manually">documentation</a>.');
|
||||
@@ -99,7 +101,19 @@ class InstallDocker
|
||||
}
|
||||
$command = $command->merge([
|
||||
"echo 'Installing Docker Engine...'",
|
||||
"curl https://releases.rancher.com/install-docker/{$dockerVersion}.sh | sh || curl https://get.docker.com | sh -s -- --version {$dockerVersion}",
|
||||
]);
|
||||
|
||||
if ($supported_os_type->contains('debian')) {
|
||||
$command = $command->merge([$this->getDebianDockerInstallCommand()]);
|
||||
} elseif ($supported_os_type->contains('rhel')) {
|
||||
$command = $command->merge([$this->getRhelDockerInstallCommand()]);
|
||||
} elseif ($supported_os_type->contains('sles')) {
|
||||
$command = $command->merge([$this->getSuseDockerInstallCommand()]);
|
||||
} else {
|
||||
$command = $command->merge([$this->getGenericDockerInstallCommand()]);
|
||||
}
|
||||
|
||||
$command = $command->merge([
|
||||
"echo 'Configuring Docker Engine (merging existing configuration with the required)...'",
|
||||
'test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json "/etc/docker/daemon.json.original-$(date +"%Y%m%d-%H%M%S")"',
|
||||
"test ! -s /etc/docker/daemon.json && echo '{$config}' | base64 -d | tee /etc/docker/daemon.json > /dev/null",
|
||||
@@ -128,4 +142,43 @@ class InstallDocker
|
||||
return remote_process($command, $server);
|
||||
}
|
||||
}
|
||||
|
||||
private function getDebianDockerInstallCommand(): string
|
||||
{
|
||||
return "curl --max-time 300 --retry 3 https://releases.rancher.com/install-docker/{$this->dockerVersion}.sh | sh || curl --max-time 300 --retry 3 https://get.docker.com | sh -s -- --version {$this->dockerVersion} || (".
|
||||
'install -m 0755 -d /etc/apt/keyrings && '.
|
||||
'curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc && '.
|
||||
'chmod a+r /etc/apt/keyrings/docker.asc && '.
|
||||
'. /etc/os-release && '.
|
||||
'echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian ${VERSION_CODENAME} stable" > /etc/apt/sources.list.d/docker.list && '.
|
||||
'apt-get update && '.
|
||||
'apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin'.
|
||||
')';
|
||||
}
|
||||
|
||||
private function getRhelDockerInstallCommand(): string
|
||||
{
|
||||
return "curl https://releases.rancher.com/install-docker/{$this->dockerVersion}.sh | sh || curl https://get.docker.com | sh -s -- --version {$this->dockerVersion} || (".
|
||||
'dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo && '.
|
||||
'dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin && '.
|
||||
'systemctl start docker && '.
|
||||
'systemctl enable docker'.
|
||||
')';
|
||||
}
|
||||
|
||||
private function getSuseDockerInstallCommand(): string
|
||||
{
|
||||
return "curl https://releases.rancher.com/install-docker/{$this->dockerVersion}.sh | sh || curl https://get.docker.com | sh -s -- --version {$this->dockerVersion} || (".
|
||||
'zypper addrepo https://download.docker.com/linux/sles/docker-ce.repo && '.
|
||||
'zypper refresh && '.
|
||||
'zypper install -y --no-confirm docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin && '.
|
||||
'systemctl start docker && '.
|
||||
'systemctl enable docker'.
|
||||
')';
|
||||
}
|
||||
|
||||
private function getGenericDockerInstallCommand(): string
|
||||
{
|
||||
return "curl https://releases.rancher.com/install-docker/{$this->dockerVersion}.sh | sh || curl https://get.docker.com | sh -s -- --version {$this->dockerVersion}";
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,9 @@ use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class CleanupRedis extends Command
|
||||
{
|
||||
protected $signature = 'cleanup:redis {--dry-run : Show what would be deleted without actually deleting} {--skip-overlapping : Skip overlapping queue cleanup}';
|
||||
protected $signature = 'cleanup:redis {--dry-run : Show what would be deleted without actually deleting} {--skip-overlapping : Skip overlapping queue cleanup} {--clear-locks : Clear stale WithoutOverlapping locks}';
|
||||
|
||||
protected $description = 'Cleanup Redis (Horizon jobs, metrics, overlapping queues, and related data)';
|
||||
protected $description = 'Cleanup Redis (Horizon jobs, metrics, overlapping queues, cache locks, and related data)';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
@@ -56,6 +56,13 @@ class CleanupRedis extends Command
|
||||
$deletedCount += $overlappingCleaned;
|
||||
}
|
||||
|
||||
// Clean up stale cache locks (WithoutOverlapping middleware)
|
||||
if ($this->option('clear-locks')) {
|
||||
$this->info('Cleaning up stale cache locks...');
|
||||
$locksCleaned = $this->cleanupCacheLocks($dryRun);
|
||||
$deletedCount += $locksCleaned;
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$this->info("DRY RUN: Would delete {$deletedCount} out of {$totalKeys} keys");
|
||||
} else {
|
||||
@@ -273,4 +280,56 @@ class CleanupRedis extends Command
|
||||
|
||||
return $cleanedCount;
|
||||
}
|
||||
|
||||
private function cleanupCacheLocks(bool $dryRun): int
|
||||
{
|
||||
$cleanedCount = 0;
|
||||
|
||||
// Use the default Redis connection (database 0) where cache locks are stored
|
||||
$redis = Redis::connection('default');
|
||||
|
||||
// Get all keys matching WithoutOverlapping lock pattern
|
||||
$allKeys = $redis->keys('*');
|
||||
$lockKeys = [];
|
||||
|
||||
foreach ($allKeys as $key) {
|
||||
// Match cache lock keys: they contain 'laravel-queue-overlap'
|
||||
if (preg_match('/overlap/i', $key)) {
|
||||
$lockKeys[] = $key;
|
||||
}
|
||||
}
|
||||
if (empty($lockKeys)) {
|
||||
$this->info(' No cache locks found.');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->info(' Found '.count($lockKeys).' cache lock(s)');
|
||||
|
||||
foreach ($lockKeys as $lockKey) {
|
||||
// Check TTL to identify stale locks
|
||||
$ttl = $redis->ttl($lockKey);
|
||||
|
||||
// TTL = -1 means no expiration (stale lock!)
|
||||
// TTL = -2 means key doesn't exist
|
||||
// TTL > 0 means lock is valid and will expire
|
||||
if ($ttl === -1) {
|
||||
if ($dryRun) {
|
||||
$this->warn(" Would delete STALE lock (no expiration): {$lockKey}");
|
||||
} else {
|
||||
$redis->del($lockKey);
|
||||
$this->info(" ✓ Deleted STALE lock: {$lockKey}");
|
||||
}
|
||||
$cleanedCount++;
|
||||
} elseif ($ttl > 0) {
|
||||
$this->line(" Skipping active lock (expires in {$ttl}s): {$lockKey}");
|
||||
}
|
||||
}
|
||||
|
||||
if ($cleanedCount === 0) {
|
||||
$this->info(' No stale locks found (all locks have expiration set)');
|
||||
}
|
||||
|
||||
return $cleanedCount;
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ class Init extends Command
|
||||
$this->cleanupUnusedNetworkFromCoolifyProxy();
|
||||
|
||||
try {
|
||||
$this->call('cleanup:redis');
|
||||
$this->call('cleanup:redis', ['--clear-locks' => true]);
|
||||
} catch (\Throwable $e) {
|
||||
echo "Error in cleanup:redis command: {$e->getMessage()}\n";
|
||||
}
|
||||
|
||||
@@ -1893,7 +1893,6 @@ class ApplicationsController extends Controller
|
||||
public function delete_by_uuid(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
$cleanup = filter_var($request->query->get('cleanup', true), FILTER_VALIDATE_BOOLEAN);
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
@@ -1912,10 +1911,10 @@ class ApplicationsController extends Controller
|
||||
|
||||
DeleteResourceJob::dispatch(
|
||||
resource: $application,
|
||||
deleteVolumes: $request->query->get('delete_volumes', true),
|
||||
deleteConnectedNetworks: $request->query->get('delete_connected_networks', true),
|
||||
deleteConfigurations: $request->query->get('delete_configurations', true),
|
||||
dockerCleanup: $request->query->get('docker_cleanup', true)
|
||||
deleteVolumes: $request->boolean('delete_volumes', true),
|
||||
deleteConnectedNetworks: $request->boolean('delete_connected_networks', true),
|
||||
deleteConfigurations: $request->boolean('delete_configurations', true),
|
||||
dockerCleanup: $request->boolean('docker_cleanup', true)
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
@@ -3155,8 +3154,8 @@ class ApplicationsController extends Controller
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
$force = $request->query->get('force') ?? false;
|
||||
$instant_deploy = $request->query->get('instant_deploy') ?? false;
|
||||
$force = $request->boolean('force', false);
|
||||
$instant_deploy = $request->boolean('instant_deploy', false);
|
||||
$uuid = $request->route('uuid');
|
||||
if (! $uuid) {
|
||||
return response()->json(['message' => 'UUID is required.'], 400);
|
||||
|
||||
@@ -2133,7 +2133,6 @@ class DatabasesController extends Controller
|
||||
public function delete_by_uuid(Request $request)
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
$cleanup = filter_var($request->query->get('cleanup', true), FILTER_VALIDATE_BOOLEAN);
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
@@ -2149,10 +2148,10 @@ class DatabasesController extends Controller
|
||||
|
||||
DeleteResourceJob::dispatch(
|
||||
resource: $database,
|
||||
deleteVolumes: $request->query->get('delete_volumes', true),
|
||||
deleteConnectedNetworks: $request->query->get('delete_connected_networks', true),
|
||||
deleteConfigurations: $request->query->get('delete_configurations', true),
|
||||
dockerCleanup: $request->query->get('docker_cleanup', true)
|
||||
deleteVolumes: $request->boolean('delete_volumes', true),
|
||||
deleteConnectedNetworks: $request->boolean('delete_connected_networks', true),
|
||||
deleteConfigurations: $request->boolean('delete_configurations', true),
|
||||
dockerCleanup: $request->boolean('docker_cleanup', true)
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
@@ -2243,7 +2242,7 @@ class DatabasesController extends Controller
|
||||
return response()->json(['message' => 'Backup configuration not found.'], 404);
|
||||
}
|
||||
|
||||
$deleteS3 = filter_var($request->query->get('delete_s3', false), FILTER_VALIDATE_BOOLEAN);
|
||||
$deleteS3 = $request->boolean('delete_s3', false);
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
@@ -2376,7 +2375,7 @@ class DatabasesController extends Controller
|
||||
return response()->json(['message' => 'Backup execution not found.'], 404);
|
||||
}
|
||||
|
||||
$deleteS3 = filter_var($request->query->get('delete_s3', false), FILTER_VALIDATE_BOOLEAN);
|
||||
$deleteS3 = $request->boolean('delete_s3', false);
|
||||
|
||||
try {
|
||||
if ($execution->filename) {
|
||||
|
||||
@@ -649,10 +649,10 @@ class ServicesController extends Controller
|
||||
|
||||
DeleteResourceJob::dispatch(
|
||||
resource: $service,
|
||||
deleteVolumes: $request->query->get('delete_volumes', true),
|
||||
deleteConnectedNetworks: $request->query->get('delete_connected_networks', true),
|
||||
deleteConfigurations: $request->query->get('delete_configurations', true),
|
||||
dockerCleanup: $request->query->get('docker_cleanup', true)
|
||||
deleteVolumes: $request->boolean('delete_volumes', true),
|
||||
deleteConnectedNetworks: $request->boolean('delete_connected_networks', true),
|
||||
deleteConfigurations: $request->boolean('delete_configurations', true),
|
||||
dockerCleanup: $request->boolean('docker_cleanup', true)
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
|
||||
@@ -459,7 +459,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
private function post_deployment()
|
||||
{
|
||||
GetContainersStatus::dispatch($this->server);
|
||||
$this->next(ApplicationDeploymentStatus::FINISHED->value);
|
||||
$this->completeDeployment();
|
||||
if ($this->pull_request_id !== 0) {
|
||||
if ($this->application->is_github_based()) {
|
||||
ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::FINISHED);
|
||||
@@ -517,6 +517,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$this->generate_image_names();
|
||||
$this->prepare_builder_image();
|
||||
$this->generate_compose_file();
|
||||
|
||||
// Save runtime environment variables (including empty .env file if no variables defined)
|
||||
$this->save_runtime_environment_variables();
|
||||
|
||||
$this->rolling_update();
|
||||
}
|
||||
|
||||
@@ -1004,7 +1008,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$this->generate_image_names();
|
||||
$this->check_image_locally_or_remotely();
|
||||
$this->should_skip_build();
|
||||
$this->next(ApplicationDeploymentStatus::FINISHED->value);
|
||||
$this->completeDeployment();
|
||||
}
|
||||
|
||||
private function should_skip_build()
|
||||
@@ -1222,9 +1226,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
// Handle empty environment variables
|
||||
if ($environment_variables->isEmpty()) {
|
||||
// For Docker Compose, we need to create an empty .env file
|
||||
// For Docker Compose and Docker Image, we need to create an empty .env file
|
||||
// because we always reference it in the compose file
|
||||
if ($this->build_pack === 'dockercompose') {
|
||||
if ($this->build_pack === 'dockercompose' || $this->build_pack === 'dockerimage') {
|
||||
$this->application_deployment_queue->addLogEntry('Creating empty .env file (no environment variables defined).');
|
||||
|
||||
// Create empty .env file
|
||||
@@ -1628,7 +1632,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
return;
|
||||
}
|
||||
if ($this->application->custom_healthcheck_found) {
|
||||
$this->application_deployment_queue->addLogEntry('Custom healthcheck found, skipping default healthcheck.');
|
||||
$this->application_deployment_queue->addLogEntry('Custom healthcheck found in Dockerfile.');
|
||||
}
|
||||
if ($this->container_name) {
|
||||
$counter = 1;
|
||||
@@ -2354,16 +2358,22 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
];
|
||||
// Always use .env file
|
||||
$docker_compose['services'][$this->container_name]['env_file'] = ['.env'];
|
||||
$docker_compose['services'][$this->container_name]['healthcheck'] = [
|
||||
'test' => [
|
||||
'CMD-SHELL',
|
||||
$this->generate_healthcheck_commands(),
|
||||
],
|
||||
'interval' => $this->application->health_check_interval.'s',
|
||||
'timeout' => $this->application->health_check_timeout.'s',
|
||||
'retries' => $this->application->health_check_retries,
|
||||
'start_period' => $this->application->health_check_start_period.'s',
|
||||
];
|
||||
|
||||
// Only add Coolify healthcheck if no custom HEALTHCHECK found in Dockerfile
|
||||
// If custom_healthcheck_found is true, the Dockerfile's HEALTHCHECK will be used
|
||||
// If healthcheck is disabled, no healthcheck will be added
|
||||
if (! $this->application->custom_healthcheck_found && ! $this->application->isHealthcheckDisabled()) {
|
||||
$docker_compose['services'][$this->container_name]['healthcheck'] = [
|
||||
'test' => [
|
||||
'CMD-SHELL',
|
||||
$this->generate_healthcheck_commands(),
|
||||
],
|
||||
'interval' => $this->application->health_check_interval.'s',
|
||||
'timeout' => $this->application->health_check_timeout.'s',
|
||||
'retries' => $this->application->health_check_retries,
|
||||
'start_period' => $this->application->health_check_start_period.'s',
|
||||
];
|
||||
}
|
||||
|
||||
if (! is_null($this->application->limits_cpuset)) {
|
||||
data_set($docker_compose, 'services.'.$this->container_name.'.cpuset', $this->application->limits_cpuset);
|
||||
@@ -3013,9 +3023,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
$this->application_deployment_queue->addLogEntry('----------------------------------------');
|
||||
}
|
||||
$this->application_deployment_queue->addLogEntry('New container is not healthy, rolling back to the old container.');
|
||||
$this->application_deployment_queue->update([
|
||||
'status' => ApplicationDeploymentStatus::FAILED->value,
|
||||
]);
|
||||
$this->failDeployment();
|
||||
$this->graceful_shutdown_container($this->container_name);
|
||||
}
|
||||
}
|
||||
@@ -3649,42 +3657,116 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
}
|
||||
}
|
||||
|
||||
private function next(string $status)
|
||||
/**
|
||||
* Transition deployment to a new status with proper validation and side effects.
|
||||
* This is the single source of truth for status transitions.
|
||||
*/
|
||||
private function transitionToStatus(ApplicationDeploymentStatus $status): void
|
||||
{
|
||||
// Refresh to get latest status
|
||||
$this->application_deployment_queue->refresh();
|
||||
|
||||
// Never allow changing status from FAILED or CANCELLED_BY_USER to anything else
|
||||
if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::FAILED->value) {
|
||||
$this->application->environment->project->team?->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview));
|
||||
|
||||
if ($this->isInTerminalState()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->updateDeploymentStatus($status);
|
||||
$this->handleStatusTransition($status);
|
||||
queue_next_deployment($this->application);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if deployment is in a terminal state (FAILED or CANCELLED).
|
||||
* Terminal states cannot be changed.
|
||||
*/
|
||||
private function isInTerminalState(): bool
|
||||
{
|
||||
$this->application_deployment_queue->refresh();
|
||||
|
||||
if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::FAILED->value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::CANCELLED_BY_USER->value) {
|
||||
// Job was cancelled, stop execution
|
||||
$this->application_deployment_queue->addLogEntry('Deployment cancelled by user, stopping execution.');
|
||||
throw new \RuntimeException('Deployment cancelled by user', 69420);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the deployment status in the database.
|
||||
*/
|
||||
private function updateDeploymentStatus(ApplicationDeploymentStatus $status): void
|
||||
{
|
||||
$this->application_deployment_queue->update([
|
||||
'status' => $status,
|
||||
'status' => $status->value,
|
||||
]);
|
||||
}
|
||||
|
||||
queue_next_deployment($this->application);
|
||||
/**
|
||||
* Execute status-specific side effects (events, notifications, additional deployments).
|
||||
*/
|
||||
private function handleStatusTransition(ApplicationDeploymentStatus $status): void
|
||||
{
|
||||
match ($status) {
|
||||
ApplicationDeploymentStatus::FINISHED => $this->handleSuccessfulDeployment(),
|
||||
ApplicationDeploymentStatus::FAILED => $this->handleFailedDeployment(),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
if ($status === ApplicationDeploymentStatus::FINISHED->value) {
|
||||
event(new ApplicationConfigurationChanged($this->application->team()->id));
|
||||
/**
|
||||
* Handle side effects when deployment succeeds.
|
||||
*/
|
||||
private function handleSuccessfulDeployment(): void
|
||||
{
|
||||
event(new ApplicationConfigurationChanged($this->application->team()->id));
|
||||
|
||||
if (! $this->only_this_server) {
|
||||
$this->deploy_to_additional_destinations();
|
||||
}
|
||||
$this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
|
||||
if (! $this->only_this_server) {
|
||||
$this->deploy_to_additional_destinations();
|
||||
}
|
||||
|
||||
$this->sendDeploymentNotification(DeploymentSuccess::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle side effects when deployment fails.
|
||||
*/
|
||||
private function handleFailedDeployment(): void
|
||||
{
|
||||
$this->sendDeploymentNotification(DeploymentFailed::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send deployment status notification to the team.
|
||||
*/
|
||||
private function sendDeploymentNotification(string $notificationClass): void
|
||||
{
|
||||
$this->application->environment->project->team?->notify(
|
||||
new $notificationClass($this->application, $this->deployment_uuid, $this->preview)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete deployment successfully.
|
||||
* Sends success notification and triggers additional deployments if needed.
|
||||
*/
|
||||
private function completeDeployment(): void
|
||||
{
|
||||
$this->transitionToStatus(ApplicationDeploymentStatus::FINISHED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fail the deployment.
|
||||
* Sends failure notification and queues next deployment.
|
||||
*/
|
||||
private function failDeployment(): void
|
||||
{
|
||||
$this->transitionToStatus(ApplicationDeploymentStatus::FAILED);
|
||||
}
|
||||
|
||||
public function failed(Throwable $exception): void
|
||||
{
|
||||
$this->next(ApplicationDeploymentStatus::FAILED->value);
|
||||
$this->failDeployment();
|
||||
$this->application_deployment_queue->addLogEntry('Oops something is not okay, are you okay? 😢', 'stderr');
|
||||
if (str($exception->getMessage())->isNotEmpty()) {
|
||||
$this->application_deployment_queue->addLogEntry($exception->getMessage(), 'stderr');
|
||||
|
||||
@@ -52,7 +52,8 @@ class ScheduledJobManager implements ShouldQueue
|
||||
{
|
||||
return [
|
||||
(new WithoutOverlapping('scheduled-job-manager'))
|
||||
->releaseAfter(60), // Release the lock after 60 seconds if job fails
|
||||
->expireAfter(60) // Lock expires after 1 minute to prevent stale locks
|
||||
->dontRelease(), // Don't re-queue on lock conflict
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ class Index extends Component
|
||||
|
||||
if ($this->selectedServerType === 'remote') {
|
||||
if ($this->privateKeys->isEmpty()) {
|
||||
$this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
|
||||
$this->privateKeys = PrivateKey::ownedAndOnlySShKeys(['name'])->where('id', '!=', 0)->get();
|
||||
}
|
||||
if ($this->servers->isEmpty()) {
|
||||
$this->servers = Server::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
|
||||
@@ -186,7 +186,7 @@ class Index extends Component
|
||||
|
||||
return $this->validateServer('localhost');
|
||||
} elseif ($this->selectedServerType === 'remote') {
|
||||
$this->privateKeys = PrivateKey::ownedByCurrentTeam(['name'])->where('id', '!=', 0)->get();
|
||||
$this->privateKeys = PrivateKey::ownedAndOnlySShKeys(['name'])->where('id', '!=', 0)->get();
|
||||
// Auto-select first key if available for better UX
|
||||
if ($this->privateKeys->count() > 0) {
|
||||
$this->selectedExistingPrivateKey = $this->privateKeys->first()->id;
|
||||
|
||||
@@ -438,6 +438,11 @@ class General extends Component
|
||||
|
||||
// Refresh parsedServiceDomains to reflect any changes in docker_compose_domains
|
||||
$this->application->refresh();
|
||||
|
||||
// Sync the docker_compose_raw from the model to the component property
|
||||
// This ensures the Monaco editor displays the loaded compose file
|
||||
$this->syncFromModel();
|
||||
|
||||
$this->parsedServiceDomains = $this->application->docker_compose_domains ? json_decode($this->application->docker_compose_domains, true) : [];
|
||||
// Convert service names with dots and dashes to use underscores for HTML form binding
|
||||
$sanitizedDomains = [];
|
||||
|
||||
@@ -58,6 +58,11 @@ class Heading extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function manualCheckStatus()
|
||||
{
|
||||
$this->checkStatus();
|
||||
}
|
||||
|
||||
public function force_deploy_without_cache()
|
||||
{
|
||||
$this->authorize('deploy', $this->application);
|
||||
|
||||
@@ -62,6 +62,11 @@ class Heading extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function manualCheckStatus()
|
||||
{
|
||||
$this->checkStatus();
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
|
||||
@@ -18,20 +18,7 @@ class Index extends Component
|
||||
public function mount()
|
||||
{
|
||||
$this->private_keys = PrivateKey::ownedByCurrentTeam()->get();
|
||||
$this->projects = Project::ownedByCurrentTeam()->get()->map(function ($project) {
|
||||
$project->settingsRoute = route('project.edit', ['project_uuid' => $project->uuid]);
|
||||
$project->canUpdate = auth()->user()->can('update', $project);
|
||||
$project->canCreateResource = auth()->user()->can('createAnyResource');
|
||||
$firstEnvironment = $project->environments->first();
|
||||
$project->addResourceRoute = $firstEnvironment
|
||||
? route('project.resource.create', [
|
||||
'project_uuid' => $project->uuid,
|
||||
'environment_uuid' => $firstEnvironment->uuid,
|
||||
])
|
||||
: null;
|
||||
|
||||
return $project;
|
||||
});
|
||||
$this->projects = Project::ownedByCurrentTeam()->get();
|
||||
$this->servers = Server::ownedByCurrentTeam()->count();
|
||||
}
|
||||
|
||||
@@ -39,11 +26,4 @@ class Index extends Component
|
||||
{
|
||||
return view('livewire.project.index');
|
||||
}
|
||||
|
||||
public function navigateToProject($projectUuid)
|
||||
{
|
||||
$project = collect($this->projects)->firstWhere('uuid', $projectUuid);
|
||||
|
||||
return $this->redirect($project->navigateTo(), navigate: false);
|
||||
}
|
||||
}
|
||||
@@ -102,6 +102,36 @@ class Select extends Component
|
||||
: asset($default_logo),
|
||||
] + (array) $service;
|
||||
})->all();
|
||||
|
||||
// Extract unique categories from services
|
||||
$categories = collect($services)
|
||||
->pluck('category')
|
||||
->filter()
|
||||
->unique()
|
||||
->map(function ($category) {
|
||||
// Handle multiple categories separated by comma
|
||||
if (str_contains($category, ',')) {
|
||||
return collect(explode(',', $category))->map(fn ($cat) => trim($cat));
|
||||
}
|
||||
|
||||
return [$category];
|
||||
})
|
||||
->flatten()
|
||||
->unique()
|
||||
->map(function ($category) {
|
||||
// Format common acronyms to uppercase
|
||||
$acronyms = ['ai', 'api', 'ci', 'cd', 'cms', 'crm', 'erp', 'iot', 'vpn', 'vps', 'dns', 'ssl', 'tls', 'ssh', 'ftp', 'http', 'https', 'smtp', 'imap', 'pop3', 'sql', 'nosql', 'json', 'xml', 'yaml', 'csv', 'pdf', 'sms', 'mfa', '2fa', 'oauth', 'saml', 'jwt', 'rest', 'soap', 'grpc', 'graphql', 'websocket', 'webrtc', 'p2p', 'b2b', 'b2c', 'seo', 'sem', 'ppc', 'roi', 'kpi', 'ui', 'ux', 'ide', 'sdk', 'api', 'cli', 'gui', 'cdn', 'ddos', 'dos', 'xss', 'csrf', 'sqli', 'rce', 'lfi', 'rfi', 'ssrf', 'xxe', 'idor', 'owasp', 'gdpr', 'hipaa', 'pci', 'dss', 'iso', 'nist', 'cve', 'cwe', 'cvss'];
|
||||
$lower = strtolower($category);
|
||||
|
||||
if (in_array($lower, $acronyms)) {
|
||||
return strtoupper($category);
|
||||
}
|
||||
|
||||
return $category;
|
||||
})
|
||||
->sort(SORT_NATURAL | SORT_FLAG_CASE)
|
||||
->values()
|
||||
->all();
|
||||
$gitBasedApplications = [
|
||||
[
|
||||
'id' => 'public',
|
||||
@@ -147,14 +177,14 @@ class Select extends Component
|
||||
'id' => 'postgresql',
|
||||
'name' => 'PostgreSQL',
|
||||
'description' => 'PostgreSQL is an object-relational database known for its robustness, advanced features, and strong standards compliance.',
|
||||
'logo' => '<svg class="w-[4.5rem] aspect-square h-[4.5rem] p-2 transition-all duration-200 dark:opacity-30 grayscale group-hover:grayscale-0 group-hover:opacity-100 dark:fill-white fill-black bg-black/10 dark:bg-white/10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#currentColor" d="M85.988 76.075c.632-5.262.443-6.034 4.362-5.182l.995.088c3.014.137 6.957-.485 9.272-1.561 4.986-2.313 7.942-6.177 3.026-5.162-11.215 2.313-11.986-1.483-11.986-1.483C103.5 45.204 108.451 22.9 104.178 17.44 92.524 2.548 72.35 9.59 72.012 9.773l-.108.021c-2.216-.461-4.695-.735-7.481-.78-5.075-.083-8.926 1.331-11.847 3.546 0 0-35.989-14.827-34.315 18.646.356 7.121 10.207 53.882 21.956 39.758 4.294-5.164 8.444-9.531 8.444-9.531 2.061 1.369 4.528 2.067 7.116 1.816l.2-.17c-.062.641-.035 1.268.081 2.01-3.027 3.383-2.137 3.977-8.189 5.222-6.122 1.262-2.525 3.508-.178 4.095 2.848.713 9.433 1.722 13.884-4.509l-.177.711c1.188.95 1.107 6.827 1.275 11.026.168 4.199.45 8.117 1.306 10.429.856 2.31 1.866 8.261 9.819 6.557 6.646-1.426 11.727-3.476 12.19-22.545"/><path fill="#currentColor" d="M71.208 102.77c-3.518 0-5.808-1.36-7.2-2.674-2.1-1.981-2.933-4.534-3.43-6.059l-.215-.637c-1.002-2.705-1.341-6.599-1.542-11.613a199.25 199.25 0 01-.075-2.352c-.017-.601-.038-1.355-.068-2.146a15.157 15.157 0 01-3.997 1.264c-2.48.424-5.146.286-7.926-.409-1.961-.49-3.999-1.506-5.16-3.076-3.385 2.965-6.614 2.562-8.373 1.976-3.103-1.035-5.88-3.942-8.491-8.89-1.859-3.523-3.658-8.115-5.347-13.646-2.94-9.633-4.808-19.779-4.974-23.109-.522-10.427 2.284-17.883 8.34-22.16 9.555-6.749 24.03-2.781 29.307-.979 3.545-2.137 7.716-3.178 12.43-3.102 2.532.041 4.942.264 7.181.662 2.335-.734 6.949-1.788 12.23-1.723 9.73.116 17.793 3.908 23.316 10.966 3.941 5.036 1.993 15.61.48 21.466-2.127 8.235-5.856 16.996-10.436 24.622 1.244.009 3.045-.141 5.607-.669 5.054-1.044 6.531 1.666 6.932 2.879 1.607 4.867-5.378 8.544-7.557 9.555-2.792 1.297-7.343 2.086-11.071 1.915l-.163-.011-.979-.086-.097.816-.093.799c-.25 9.664-1.631 15.784-4.472 19.829-2.977 4.239-7.116 5.428-10.761 6.209a16.146 16.146 0 01-3.396.383zm-7.402-35.174c2.271 1.817 2.47 5.236 2.647 11.626.022.797.043 1.552.071 2.257.086 2.134.287 7.132 1.069 9.244.111.298.21.602.314.922.872 2.672 1.31 4.011 5.081 3.203 3.167-.678 4.794-1.287 6.068-3.101 1.852-2.638 2.888-7.941 3.078-15.767l3.852.094-3.826-.459.112-.955c.367-3.148.631-5.424 2.736-6.928 1.688-1.207 3.613-1.09 5.146-.814-1.684-1.271-2.15-2.765-2.274-3.377l-.321-1.582.902-1.34c5.2-7.716 9.489-17.199 11.767-26.018 2.34-9.062 1.626-13.875.913-14.785-9.446-12.071-25.829-7.088-27.539-6.521l-.29.156-1.45.271-.743-.154c-2.047-.425-4.321-.66-6.76-.7-3.831-.064-6.921.841-9.455 2.764l-1.758 1.333-2.041-.841c-4.358-1.782-17.162-5.365-23.918-.58-3.75 2.656-5.458 7.861-5.078 15.47.125 2.512 1.833 12.021 4.647 21.245 3.891 12.746 7.427 16.979 8.903 17.472.257.087.926-.433 1.591-1.231 4.326-5.203 8.44-9.54 8.613-9.723l2.231-2.347 2.697 1.792c1.087.723 2.286 1.132 3.518 1.209l6.433-5.486-.932 9.51c-.021.214-.031.504.053 1.044l.28 1.803-1.213 1.358-.14.157 3.534 1.632 1.482-1.853z"/><path fill="#336791" d="M103.646 64.258c-11.216 2.313-11.987-1.484-11.987-1.484 11.842-17.571 16.792-39.876 12.52-45.335C92.524 2.547 72.35 9.59 72.013 9.773l-.109.019c-2.216-.459-4.695-.733-7.482-.778-5.075-.083-8.925 1.33-11.846 3.545 0 0-35.99-14.826-34.316 18.647.356 7.121 10.207 53.882 21.956 39.758 4.294-5.164 8.443-9.531 8.443-9.531 2.061 1.369 4.528 2.067 7.115 1.816l.201-.17c-.062.641-.034 1.268.08 2.01-3.026 3.383-2.138 3.977-8.188 5.222-6.123 1.262-2.526 3.508-.177 4.095 2.847.713 9.433 1.722 13.883-4.509l-.178.711c1.186.95 2.019 6.179 1.879 10.919s-.233 7.994.702 10.536c.935 2.541 1.866 8.261 9.82 6.557 6.646-1.425 10.09-5.116 10.57-11.272.34-4.377 1.109-3.73 1.158-7.644l.618-1.853c.711-5.934.113-7.848 4.208-6.957l.995.087c3.014.138 6.958-.485 9.273-1.561 4.986-2.314 7.943-6.177 3.028-5.162z"/><path fill="#fff" d="M71.61 100.394c-6.631.001-8.731-5.25-9.591-7.397-1.257-3.146-1.529-15.358-1.249-25.373a1.286 1.286 0 012.57.072c-.323 11.551.136 22.018 1.066 24.346 1.453 3.632 3.656 6.809 9.887 5.475 5.915-1.269 8.13-3.512 9.116-9.23.758-4.389 2.254-16.874 2.438-19.338a1.285 1.285 0 012.563.191c-.192 2.564-1.682 15.026-2.469 19.584-1.165 6.755-4.176 9.819-11.11 11.306a15.462 15.462 0 01-3.221.364zM35.659 74.749a5.343 5.343 0 01-1.704-.281c-4.307-1.437-8.409-8.451-12.193-20.849-2.88-9.438-4.705-19.288-4.865-22.489-.475-9.49 1.97-16.205 7.265-19.957 10.476-7.423 28.1-.354 28.845-.05a1.285 1.285 0 01-.972 2.379v.001c-.17-.07-17.07-6.84-26.392-.229-4.528 3.211-6.607 9.175-6.18 17.729.135 2.696 1.84 12.311 4.757 21.867 3.378 11.067 7.223 18.052 10.548 19.16.521.175 2.109.704 4.381-2.026 4.272-5.14 8.197-9.242 8.236-9.283a1.286 1.286 0 011.856 1.778c-.039.04-3.904 4.081-8.116 9.148-1.995 2.398-3.908 3.102-5.466 3.102zm55.92-10.829a1.284 1.284 0 01-1.065-2.004c11.971-17.764 16.173-39.227 12.574-43.825-4.53-5.788-10.927-8.812-19.012-8.985-5.987-.13-10.746 1.399-11.523
Line truncated
|
||||
'logo' => '<svg class="w-[4.5rem] h-[4.5rem] p-2 transition-all duration-200 bg-black/10 dark:bg-white/10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#currentColor" d="M85.988 76.075c.632-5.262.443-6.034 4.362-5.182l.995.088c3.014.137 6.957-.485 9.272-1.561 4.986-2.313 7.942-6.177 3.026-5.162-11.215 2.313-11.986-1.483-11.986-1.483C103.5 45.204 108.451 22.9 104.178 17.44 92.524 2.548 72.35 9.59 72.012 9.773l-.108.021c-2.216-.461-4.695-.735-7.481-.78-5.075-.083-8.926 1.331-11.847 3.546 0 0-35.989-14.827-34.315 18.646.356 7.121 10.207 53.882 21.956 39.758 4.294-5.164 8.444-9.531 8.444-9.531 2.061 1.369 4.528 2.067 7.116 1.816l.2-.17c-.062.641-.035 1.268.081 2.01-3.027 3.383-2.137 3.977-8.189 5.222-6.122 1.262-2.525 3.508-.178 4.095 2.848.713 9.433 1.722 13.884-4.509l-.177.711c1.188.95 1.107 6.827 1.275 11.026.168 4.199.45 8.117 1.306 10.429.856 2.31 1.866 8.261 9.819 6.557 6.646-1.426 11.727-3.476 12.19-22.545"/><path fill="#currentColor" d="M71.208 102.77c-3.518 0-5.808-1.36-7.2-2.674-2.1-1.981-2.933-4.534-3.43-6.059l-.215-.637c-1.002-2.705-1.341-6.599-1.542-11.613a199.25 199.25 0 01-.075-2.352c-.017-.601-.038-1.355-.068-2.146a15.157 15.157 0 01-3.997 1.264c-2.48.424-5.146.286-7.926-.409-1.961-.49-3.999-1.506-5.16-3.076-3.385 2.965-6.614 2.562-8.373 1.976-3.103-1.035-5.88-3.942-8.491-8.89-1.859-3.523-3.658-8.115-5.347-13.646-2.94-9.633-4.808-19.779-4.974-23.109-.522-10.427 2.284-17.883 8.34-22.16 9.555-6.749 24.03-2.781 29.307-.979 3.545-2.137 7.716-3.178 12.43-3.102 2.532.041 4.942.264 7.181.662 2.335-.734 6.949-1.788 12.23-1.723 9.73.116 17.793 3.908 23.316 10.966 3.941 5.036 1.993 15.61.48 21.466-2.127 8.235-5.856 16.996-10.436 24.622 1.244.009 3.045-.141 5.607-.669 5.054-1.044 6.531 1.666 6.932 2.879 1.607 4.867-5.378 8.544-7.557 9.555-2.792 1.297-7.343 2.086-11.071 1.915l-.163-.011-.979-.086-.097.816-.093.799c-.25 9.664-1.631 15.784-4.472 19.829-2.977 4.239-7.116 5.428-10.761 6.209a16.146 16.146 0 01-3.396.383zm-7.402-35.174c2.271 1.817 2.47 5.236 2.647 11.626.022.797.043 1.552.071 2.257.086 2.134.287 7.132 1.069 9.244.111.298.21.602.314.922.872 2.672 1.31 4.011 5.081 3.203 3.167-.678 4.794-1.287 6.068-3.101 1.852-2.638 2.888-7.941 3.078-15.767l3.852.094-3.826-.459.112-.955c.367-3.148.631-5.424 2.736-6.928 1.688-1.207 3.613-1.09 5.146-.814-1.684-1.271-2.15-2.765-2.274-3.377l-.321-1.582.902-1.34c5.2-7.716 9.489-17.199 11.767-26.018 2.34-9.062 1.626-13.875.913-14.785-9.446-12.071-25.829-7.088-27.539-6.521l-.29.156-1.45.271-.743-.154c-2.047-.425-4.321-.66-6.76-.7-3.831-.064-6.921.841-9.455 2.764l-1.758 1.333-2.041-.841c-4.358-1.782-17.162-5.365-23.918-.58-3.75 2.656-5.458 7.861-5.078 15.47.125 2.512 1.833 12.021 4.647 21.245 3.891 12.746 7.427 16.979 8.903 17.472.257.087.926-.433 1.591-1.231 4.326-5.203 8.44-9.54 8.613-9.723l2.231-2.347 2.697 1.792c1.087.723 2.286 1.132 3.518 1.209l6.433-5.486-.932 9.51c-.021.214-.031.504.053 1.044l.28 1.803-1.213 1.358-.14.157 3.534 1.632 1.482-1.853z"/><path fill="#336791" d="M103.646 64.258c-11.216 2.313-11.987-1.484-11.987-1.484 11.842-17.571 16.792-39.876 12.52-45.335C92.524 2.547 72.35 9.59 72.013 9.773l-.109.019c-2.216-.459-4.695-.733-7.482-.778-5.075-.083-8.925 1.33-11.846 3.545 0 0-35.99-14.826-34.316 18.647.356 7.121 10.207 53.882 21.956 39.758 4.294-5.164 8.443-9.531 8.443-9.531 2.061 1.369 4.528 2.067 7.115 1.816l.201-.17c-.062.641-.034 1.268.08 2.01-3.026 3.383-2.138 3.977-8.188 5.222-6.123 1.262-2.526 3.508-.177 4.095 2.847.713 9.433 1.722 13.883-4.509l-.178.711c1.186.95 2.019 6.179 1.879 10.919s-.233 7.994.702 10.536c.935 2.541 1.866 8.261 9.82 6.557 6.646-1.425 10.09-5.116 10.57-11.272.34-4.377 1.109-3.73 1.158-7.644l.618-1.853c.711-5.934.113-7.848 4.208-6.957l.995.087c3.014.138 6.958-.485 9.273-1.561 4.986-2.314 7.943-6.177 3.028-5.162z"/><path fill="#fff" d="M71.61 100.394c-6.631.001-8.731-5.25-9.591-7.397-1.257-3.146-1.529-15.358-1.249-25.373a1.286 1.286 0 012.57.072c-.323 11.551.136 22.018 1.066 24.346 1.453 3.632 3.656 6.809 9.887 5.475 5.915-1.269 8.13-3.512 9.116-9.23.758-4.389 2.254-16.874 2.438-19.338a1.285 1.285 0 012.563.191c-.192 2.564-1.682 15.026-2.469 19.584-1.165 6.755-4.176 9.819-11.11 11.306a15.462 15.462 0 01-3.221.364zM35.659 74.749a5.343 5.343 0 01-1.704-.281c-4.307-1.437-8.409-8.451-12.193-20.849-2.88-9.438-4.705-19.288-4.865-22.489-.475-9.49 1.97-16.205 7.265-19.957 10.476-7.423 28.1-.354 28.845-.05a1.285 1.285 0 01-.972 2.379v.001c-.17-.07-17.07-6.84-26.392-.229-4.528 3.211-6.607 9.175-6.18 17.729.135 2.696 1.84 12.311 4.757 21.867 3.378 11.067 7.223 18.052 10.548 19.16.521.175 2.109.704 4.381-2.026 4.272-5.14 8.197-9.242 8.236-9.283a1.286 1.286 0 011.856 1.778c-.039.04-3.904 4.081-8.116 9.148-1.995 2.398-3.908 3.102-5.466 3.102zm55.92-10.829a1.284 1.284 0 01-1.065-2.004c11.971-17.764 16.173-39.227 12.574-43.825-4.53-5.788-10.927-8.812-19.012-8.985-5.987-.13-10.746 1.399-11.523 1.666l-.195.079c-.782.246-1.382-.183-1.608-.684a1.29 1.29 0 01.508-1.631l.346-.142-.017.005.018-.006c1.321-.483 6.1
Line truncated
|
||||
',
|
||||
],
|
||||
[
|
||||
'id' => 'mysql',
|
||||
'name' => 'MySQL',
|
||||
'description' => 'MySQL is an open-source relational database management system. ',
|
||||
'logo' => '<svg class="w-[4.5rem] aspect-square h-[4.5rem] p-2 transition-all duration-200 dark:opacity-30 grayscale group-hover:grayscale-0 group-hover:opacity-100 dark:fill-white fill-black bg-black/10 dark:bg-white/10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
'logo' => '<svg class="w-[4.5rem] h-[4.5rem] p-2 transition-all duration-200 bg-black/10 dark:bg-white/10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<path fill="currentColor" d="M0 91.313h4.242V74.566l6.566 14.598c.773 1.77 1.832 2.391 3.914 2.391s3.098-.621 3.871-2.391l6.566-14.598v16.746h4.242V74.594c0-1.633-.652-2.422-2-2.828-3.223-1.004-5.383-.137-6.363 2.039l-6.441 14.41-6.238-14.41c-.937-2.176-3.14-3.043-6.359-2.039-1.348.406-2 1.195-2 2.828zM32.93 77.68h4.238v9.227c-.039.5.16 1.676 2.484 1.715h9.223V77.633h4.25c.02 0-.008 14.984-.008 15.047.023 3.695-4.582 4.496-6.707 4.559H33.02v-2.852l13.414-.004c2.73-.285 2.406-1.645 2.406-2.098v-1.113h-9.012c-4.195-.039-6.863-1.871-6.898-3.977-.004-.191.09-9.422 0-9.516zm0 0" />
|
||||
<path fill="currentColor" d="M56.391 91.313h12.195c1.426 0 2.813-.301 3.914-.816 1.836-.84 2.73-1.984 2.73-3.48v-3.098c0-1.223-1.016-2.367-3.016-3.125-1.059-.41-2.367-.625-3.629-.625h-5.141c-1.711 0-2.527-.516-2.73-1.656-.039-.137-.039-.246-.039-.383V76.2c0-.109 0-.219.039-.355.203-.867.652-1.113 2.16-1.25l.41-.027h12.109v-2.824H63.488c-1.711 0-2.609.109-3.426.352-2.527.789-3.629 2.039-3.629 4.215v2.473c0 1.902 2.16 3.535 5.789 3.914l1.223.055h4.406c.164 0 .324 0 .449.027 1.344.109 1.914.355 2.324.844.211.195.332.473.324.758v2.477c0 .297-.203.68-.609 1.004-.367.328-.98.543-1.793.598l-.449.027H56.391zm45.297-4.922c0 2.91 2.164 4.539 6.523 4.867l1.227.055h11.051v-2.828h-11.133c-2.488 0-3.426-.625-3.426-2.121V71.738h-4.238V86.39zm-23.75.148V76.457c0-2.559 1.801-4.113 5.355-4.602a7.976 7.976 0 0 1 1.145-.082h8.047c.41 0 .777.027 1.188.082 3.555.488 5.352 2.043 5.352 4.602v10.082c0 2.078-.762 3.188-2.523 3.914l4.18 3.77h-4.926l-3.379-3.051-3.402.215H84.44a9.23 9.23 0 0 1-2.492-.352c-2.699-.734-4.008-2.152-4.008-4.496zm4.578-.246c0 .137.039.273.082.438.246 1.172 1.348 1.824 3.023 1.824h3.852l-3.539-3.195h4.926l3.086 2.789c.57-.305.941-.766 1.074-1.363.039-.137.039-.273.039-.41v-9.668c0-.109 0-.246-.039-.383-.246-1.09-1.348-1.715-2.984-1.715h-6.414c-1.879 0-3.105.816-3.105 2.098zm0 0" />
|
||||
<path fill="#00618A" d="M124.219 67.047c-2.605-.07-4.598.172-6.301.891-.484.203-1.258.207-1.336.813.266.281.309.699.52 1.039.406.66 1.094 1.539 1.707 2l2.074 1.484c1.273.777 2.699 1.223 3.93 2 .723.461 1.441 1.039 2.148 1.559.348.254.582.656 1.039.816v-.074c-.238-.305-.301-.723-.52-1.039l-.965-.965c-.941-1.25-2.137-2.348-3.41-3.262-1.016-.727-3.281-1.711-3.707-2.891l-.074-.074c.719-.078 1.563-.34 2.223-.516 1.117-.301 2.113-.223 3.262-.52l1.559-.449v-.293c-.582-.598-.996-1.387-1.633-1.93-1.656-1.41-3.469-2.824-5.336-4.004-1.035-.652-2.312-1.074-3.41-1.629-.367-.187-1.016-.281-1.262-.594-.574-.734-.887-1.664-1.332-2.52l-2.668-5.633c-.562-1.285-.93-2.555-1.633-3.707-3.363-5.535-6.988-8.875-12.602-12.156-1.191-.699-2.633-.973-4.148-1.332l-2.449-.148c-.496-.211-1.012-.82-1.48-1.113-1.859-1.176-6.629-3.73-8.008-.371-.867 2.121 1.301 4.191 2.078 5.266.543.754 1.242 1.598 1.629 2.445.258.555.301 1.113.52 1.703.539 1.453 1.008 3.031 1.707 4.375.352.68.738 1.395 1.184 2 .273.371.742.539.816 1.113-.457.641-.484 1.633-.742 2.445-1.16 3.652-.723 8.191.965 10.898.516.828 1.734 2.609 3.41 1.926 1.465-.598 1.137-2.445 1.555-4.078.098-.367.039-.641.223-.887v.074l1.336 2.668c.988 1.59 2.738 3.25 4.223 4.371.773.582 1.379 1.59 2.375 1.93V68.6h-.074c-.195-.297-.496-.422-.742-.664-.582-.57-1.227-1.277-1.703-1.93-1.352-1.832-2.547-3.84-3.633-5.93-.52-.996-.973-2.098-1.41-3.113-.168-.391-.164-.984-.516-1.184-.48.742-1.187 1.344-1.559 2.223-.594 1.402-.668 3.117-.891 4.891l-.148.074c-1.031-.25-1.395-1.312-1.777-2.223-.973-2.305-1.152-6.02-.297-8.672.219-.687 1.219-2.852.813-3.484-.191-.633-.828-1-1.184-1.484a11.7 11.7 0 0 1-1.187-2.074c-.793-1.801-1.164-3.816-2-5.633-.398-.871-1.074-1.75-1.629-2.523-.617-.855-1.305-1.484-1.781-2.52-.168-.367-.398-.957-.148-1.336.078-.254.195-.359.445-.441.43-.332 1.629.109 2.074.293 1.191.496 2.184.965 3.191 1.633.48.32.969.941 1.555 1.113h.668c1.043.238 2.211.07 3.188.367 1.723.523 3.27 1.34 4.668 2.227 4.273 2.695 7.766 6.535 10.156 11.117.387.738.551 1.441.891 2.223.684 1.578 1.543 3.203 2.223 4.746s1.34 3.094 2.297 4.375c.504.672 2.453 1.031 3.336 1.406.621.262 1.637.535 2.223.891 1.125.676 2.211 1.48 3.266 2.223.523.375 2.141 1.188 2.223 1.855zM91.082 38.805a5.26 5.26 0 0 0-1.332.148v.074h.074c.258.535.715.879 1.035 1.336l.742 1.555.074-.07c.461-.324.668-.844.668-1.633-.187-.195-.211-.437-.371-.668-.211-.309-.621-.48-.891-.742zm0 0" />
|
||||
@@ -165,43 +195,44 @@ class Select extends Component
|
||||
'id' => 'mariadb',
|
||||
'name' => 'MariaDB',
|
||||
'description' => 'MariaDB is a community-developed, commercially supported fork of the MySQL relational database management system, intended to remain free and open-source.',
|
||||
'logo' => '<svg class="w-[4.5rem] aspect-square h-[4.5rem] p-2 transition-all duration-200 dark:opacity-30 grayscale group-hover:grayscale-0 group-hover:opacity-100 dark:fill-white fill-black bg-black/10 dark:bg-white/10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="currentColor" d="M127.434 12.182a1.727 1.727 0 0 0-1.174-.392c-1.168 0-2.68.793-3.495 1.218l-.322.165a11.095 11.095 0 0 1-4.365 1.1c-1.554.049-2.892.14-4.635.322-10.327 1.06-14.933 8.975-19.37 16.63-2.416 4.164-4.91 8.489-8.33 11.793a22.472 22.472 0 0 1-2.252 1.913c-3.54 2.631-7.985 4.51-11.443 5.84-3.329 1.273-6.964 2.417-10.474 3.524-3.219 1.012-6.255 1.97-9.048 3.008a96.902 96.902 0 0 1-3.275 1.14c-2.545.825-4.378 1.458-7.06 3.304a45.386 45.386 0 0 0-2.804 2.066 29.585 29.585 0 0 0-5.597 5.894 34.802 34.802 0 0 1-4.701 5.642c-.566.554-1.57.826-3.074.826-1.76 0-3.895-.363-6.154-.747-2.33-.413-4.738-.805-6.803-.805-1.677 0-2.962.273-3.92.826 0 0-1.617.942-2.298 2.16l.67.302a13.718 13.718 0 0 1 2.859 2.065 14.342 14.342 0 0 0 2.973 2.115 2.553 2.553 0 0 1 .918.582c-.281.413-.694.946-1.129 1.516-2.384 3.119-3.774 5.09-2.977 6.163a2.507 2.507 0 0 0 1.239.28c5.196 0 7.989-1.35 11.52-3.06 1.024-.495 2.066-1.004 3.305-1.528 2.065-.896 4.288-2.325 6.647-3.838 3.084-2.01 6.31-4.076 9.442-5.072a25.734 25.734 0 0 1 7.943-1.115c3.305 0 6.783.441 10.138.872 2.499.322 5.089.652 7.63.805.986.057 1.9.086 2.787.086a32.307 32.307 0 0 0 3.557-.185l.284-.1c1.781-1.094 2.617-3.444 3.425-5.717.52-1.462.96-2.775 1.652-3.61a1.054 1.054 0 0 1 .132-.11.166.166 0 0 1 .202.032v.066c-.412 8.885-3.99 14.527-7.608 19.543l-2.416 2.59s3.383 0 5.307-.744c7.024-2.099 12.324-6.725 16.181-14.103a60.185 60.185 0 0 0 2.549-5.82c.065-.165.673-.47.616.384-.021.252-.038.533-.059.827 0 .173 0 .35-.033.528-.1 1.24-.392 3.859-.392 3.859l2.169-1.162c5.229-3.304 9.26-9.97 12.318-20.343 1.272-4.321 2.205-8.613 3.027-12.392.983-4.545 1.83-8.44 2.801-9.952 1.524-2.37 3.85-3.973 6.101-5.53.306-.211.616-.414.917-.637 2.83-1.986 5.643-4.279 6.263-8.555v-.095c.45-3.189.07-4.002-.364-4.373zm-7.283 103.727h-10.327V97.92h9.315c3.56 0 6.952.67 6.902 4.66 0 2.813-1.747 3.59-3.589 3.886 2.615.224 4.188 1.892 4.188 4.586.017 4.035-3.523 4.858-6.489 4.858zm-.772-10.14c3.565 0 4.362-1.372 4.362-3.115 0-2.619-1.595-3.214-4.362-3.214h-7.402v6.328zm.099 1.52h-7.501v7.1h7.823c2.194 0 4.511-.723 4.511-3.486 0-3.19-2.665-3.615-4.833-3.615zm-31.497-9.37h8.125c6.828 0 10.24 3.764 10.19 8.994.05 5.436-3.716 8.997-9.591 8.997H87.98zm2.242 1.596v14.825h6.197c5.432 0 7.501-3.665 7.501-7.477 0-4.309-2.59-7.348-7.5-7.348zm-10.838 5.357v-2.095h3.404v13.132h-3.392v-2.114c-.896 1.52-2.739 2.391-4.982 2.391-4.684 0-7.303-3.305-7.303-7.105 0-3.664 2.479-6.609 6.804-6.609 2.454.029 4.498.855 5.469 2.4zm-8.675 4.387c0 2.416 1.52 4.485 4.462 4.485 2.841 0 4.386-2.02 4.386-4.411 0-2.392-1.599-4.436-4.544-4.436-2.828 0-4.3 2.04-4.3 4.362zm-10.013-9.947a1.722 1.722 0 0 1 1.818-1.768 1.788 1.788 0 0 1 1.847 1.821 1.714 1.714 0 0 1-1.847 1.744 1.743 1.743 0 0 1-1.818-1.797zm.15 3.465h3.39v9.596c0 .595.125 1.02.62 1.02a3.657 3.657 0 0 0 .648-.073l.525 2.478a5.931 5.931 0 0 1-2.242.414c-1.421 0-2.942-.414-2.942-3.64zM52.15 115.91h-3.386v-13.132h3.386v2.942a5.197 5.197 0 0 1 4.735-3.218 6.13 6.13 0 0 1 2.119.347l-.723 2.479a7.435 7.435 0 0 0-1.793-.249c-2.445 0-4.338 1.843-4.338 4.545zm-11.037-11.037v-2.095h3.392v13.132h-3.392v-2.114c-.896 1.52-2.738 2.391-4.982 2.391-4.688 0-7.303-3.305-7.303-7.105 0-3.664 2.479-6.609 6.804-6.609 2.466.029 4.51.855 5.481 2.4zm-8.675 4.387c0 2.416 1.52 4.485 4.462 4.485 2.838 0 4.383-2.02 4.383-4.411 0-2.391-1.595-4.436-4.544-4.436-2.826 0-4.296 2.04-4.296 4.362zm-9.24-11.34 4.651 17.99h-3.51L21.24 102.95 15.4 115.91h-2.965l-5.808-12.883-3.19 12.883H0l4.61-17.99h3.04l6.28 13.93 6.253-13.93z"/></svg>',
|
||||
'logo' => '<svg class="w-[4.5rem] h-[4.5rem] p-2 transition-all duration-200 bg-black/10 dark:bg-white/10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="currentColor" d="M127.434 12.182a1.727 1.727 0 0 0-1.174-.392c-1.168 0-2.68.793-3.495 1.218l-.322.165a11.095 11.095 0 0 1-4.365 1.1c-1.554.049-2.892.14-4.635.322-10.327 1.06-14.933 8.975-19.37 16.63-2.416 4.164-4.91 8.489-8.33 11.793a22.472 22.472 0 0 1-2.252 1.913c-3.54 2.631-7.985 4.51-11.443 5.84-3.329 1.273-6.964 2.417-10.474 3.524-3.219 1.012-6.255 1.97-9.048 3.008a96.902 96.902 0 0 1-3.275 1.14c-2.545.825-4.378 1.458-7.06 3.304a45.386 45.386 0 0 0-2.804 2.066 29.585 29.585 0 0 0-5.597 5.894 34.802 34.802 0 0 1-4.701 5.642c-.566.554-1.57.826-3.074.826-1.76 0-3.895-.363-6.154-.747-2.33-.413-4.738-.805-6.803-.805-1.677 0-2.962.273-3.92.826 0 0-1.617.942-2.298 2.16l.67.302a13.718 13.718 0 0 1 2.859 2.065 14.342 14.342 0 0 0 2.973 2.115 2.553 2.553 0 0 1 .918.582c-.281.413-.694.946-1.129 1.516-2.384 3.119-3.774 5.09-2.977 6.163a2.507 2.507 0 0 0 1.239.28c5.196 0 7.989-1.35 11.52-3.06 1.024-.495 2.066-1.004 3.305-1.528 2.065-.896 4.288-2.325 6.647-3.838 3.084-2.01 6.31-4.076 9.442-5.072a25.734 25.734 0 0 1 7.943-1.115c3.305 0 6.783.441 10.138.872 2.499.322 5.089.652 7.63.805.986.057 1.9.086 2.787.086a32.307 32.307 0 0 0 3.557-.185l.284-.1c1.781-1.094 2.617-3.444 3.425-5.717.52-1.462.96-2.775 1.652-3.61a1.054 1.054 0 0 1 .132-.11.166.166 0 0 1 .202.032v.066c-.412 8.885-3.99 14.527-7.608 19.543l-2.416 2.59s3.383 0 5.307-.744c7.024-2.099 12.324-6.725 16.181-14.103a60.185 60.185 0 0 0 2.549-5.82c.065-.165.673-.47.616.384-.021.252-.038.533-.059.827 0 .173 0 .35-.033.528-.1 1.24-.392 3.859-.392 3.859l2.169-1.162c5.229-3.304 9.26-9.97 12.318-20.343 1.272-4.321 2.205-8.613 3.027-12.392.983-4.545 1.83-8.44 2.801-9.952 1.524-2.37 3.85-3.973 6.101-5.53.306-.211.616-.414.917-.637 2.83-1.986 5.643-4.279 6.263-8.555v-.095c.45-3.189.07-4.002-.364-4.373zm-7.283 103.727h-10.327V97.92h9.315c3.56 0 6.952.67 6.902 4.66 0 2.813-1.747 3.59-3.589 3.886 2.615.224 4.188 1.892 4.188 4.586.017 4.035-3.523 4.858-6.489 4.858zm-.772-10.14c3.565 0 4.362-1.372 4.362-3.115 0-2.619-1.595-3.214-4.362-3.214h-7.402v6.328zm.099 1.52h-7.501v7.1h7.823c2.194 0 4.511-.723 4.511-3.486 0-3.19-2.665-3.615-4.833-3.615zm-31.497-9.37h8.125c6.828 0 10.24 3.764 10.19 8.994.05 5.436-3.716 8.997-9.591 8.997H87.98zm2.242 1.596v14.825h6.197c5.432 0 7.501-3.665 7.501-7.477 0-4.309-2.59-7.348-7.5-7.348zm-10.838 5.357v-2.095h3.404v13.132h-3.392v-2.114c-.896 1.52-2.739 2.391-4.982 2.391-4.684 0-7.303-3.305-7.303-7.105 0-3.664 2.479-6.609 6.804-6.609 2.454.029 4.498.855 5.469 2.4zm-8.675 4.387c0 2.416 1.52 4.485 4.462 4.485 2.841 0 4.386-2.02 4.386-4.411 0-2.392-1.599-4.436-4.544-4.436-2.828 0-4.3 2.04-4.3 4.362zm-10.013-9.947a1.722 1.722 0 0 1 1.818-1.768 1.788 1.788 0 0 1 1.847 1.821 1.714 1.714 0 0 1-1.847 1.744 1.743 1.743 0 0 1-1.818-1.797zm.15 3.465h3.39v9.596c0 .595.125 1.02.62 1.02a3.657 3.657 0 0 0 .648-.073l.525 2.478a5.931 5.931 0 0 1-2.242.414c-1.421 0-2.942-.414-2.942-3.64zM52.15 115.91h-3.386v-13.132h3.386v2.942a5.197 5.197 0 0 1 4.735-3.218 6.13 6.13 0 0 1 2.119.347l-.723 2.479a7.435 7.435 0 0 0-1.793-.249c-2.445 0-4.338 1.843-4.338 4.545zm-11.037-11.037v-2.095h3.392v13.132h-3.392v-2.114c-.896 1.52-2.738 2.391-4.982 2.391-4.688 0-7.303-3.305-7.303-7.105 0-3.664 2.479-6.609 6.804-6.609 2.466.029 4.51.855 5.481 2.4zm-8.675 4.387c0 2.416 1.52 4.485 4.462 4.485 2.838 0 4.383-2.02 4.383-4.411 0-2.391-1.595-4.436-4.544-4.436-2.826 0-4.296 2.04-4.296 4.362zm-9.24-11.34 4.651 17.99h-3.51L21.24 102.95 15.4 115.91h-2.965l-5.808-12.883-3.19 12.883H0l4.61-17.99h3.04l6.28 13.93 6.253-13.93z"/></svg>',
|
||||
],
|
||||
[
|
||||
'id' => 'redis',
|
||||
'name' => 'Redis',
|
||||
'description' => 'Redis is a source-available, in-memory storage, used as a distributed, in-memory key–value database, cache and message broker, with optional durability.',
|
||||
'logo' => '<svg class="w-[4.5rem] aspect-square h-[4.5rem] p-2 transition-all duration-200 dark:opacity-30 grayscale group-hover:grayscale-0 group-hover:opacity-100 dark:fill-white fill-black bg-black/10 dark:bg-white/10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path d="M21.4 97.6c0 1.8-1.5 3.5-3.5 3.5-1.5 0-2.8.4-4 1.3-1.3.8-2.2 1.9-3 3.2-1.6 2.1-2.4 4.6-2.7 5.5v12.5c0 1.9-1.6 3.5-3.6 3.5-1.9 0-3.5-1.6-3.5-3.5v-26c0-1.9 1.6-3.4 3.5-3.4 2 0 3.6 1.5 3.6 3.4v.5c.4-.5.9-1 1.4-1.3 2.2-1.4 5-2.6 8.3-2.6 2 0 3.5 1.5 3.5 3.4zm-1.9 13c.1-9 7-16.5 16.1-16.5 8.6 0 15.3 6.4 15.9 15.3v.3c0 .1 0 .5-.1.6-.2 1.6-1.6 2.6-3.4 2.6H27c.3 1.5 1.1 3.2 2.2 4.3 1.4 1.6 4 2.8 6.3 3 2.4.2 5.2-.4 6.8-1.6 1.4-1.4 4.1-1.3 4.9-.2.9.9 1.5 2.9 0 4.3-3.2 3-7.1 4.3-11.8 4.3-8.9 0-15.9-7.5-15.9-16.4zm7.1-3.2h18.6c-.7-2.6-4-6.5-9.7-7-5.6.2-8.3 4.3-8.9 7zm58.3 16.1c0 1.9-1.6 3.6-3.6 3.6-1.8 0-3.2-1.3-3.5-2.8-2.5 1.7-5.7 2.8-9 2.8-8.9 0-16-7.5-16-16.4 0-9 7.1-16.5 16-16.5 3.2 0 6.4 1.1 8.8 2.8V84.5c0-1.9 1.6-3.6 3.6-3.6s3.6 1.6 3.6 3.6v26.2l.1 12.8zm-16-22.2c-2.4 0-4.5 1-6.2 2.7-1.6 1.6-2.6 4-2.6 6.6 0 2.5 1 4.9 2.6 6.5 1.6 1.7 3.8 2.7 6.2 2.7 2.4 0 4.5-1 6.2-2.7 1.6-1.6 2.6-4 2.6-6.5 0-2.6-1-5-2.6-6.6-1.6-1.7-3.7-2.7-6.2-2.7zm28.6-15.4c0 2-1.5 3.6-3.6 3.6-2 0-3.6-1.6-3.6-3.6v-1.4c0-2 1.6-3.6 3.6-3.6s3.6 1.6 3.6 3.6v1.4zm0 11.9v25.7c0 2-1.5 3.6-3.6 3.6-2 0-3.6-1.6-3.6-3.6V97.8c0-2.1 1.6-3.6 3.6-3.6 2.1 0 3.6 1.5 3.6 3.6zm4.5 19.8c1.2-1.6 3.5-1.8 4.9-.5 1.7 1.4 4.7 3 7.2 2.9 1.8 0 3.4-.6 4.5-1.3.9-.8 1.2-1.4 1.2-2 0-.3-.1-.5-.2-.7-.1-.2-.3-.5-.9-.8-.9-.7-2.9-1.4-5.3-1.8h-.1c-2-.4-4-.9-5.7-1.7-1.8-.9-3.4-2-4.5-3.8-.7-1.2-1.1-2.6-1.1-4.1 0-3 1.7-5.6 3.9-7.2 2.3-1.6 5.1-2.4 8.1-2.4 4.5 0 7.8 2.2 9.9 3.6 1.6 1.1 2 3.2 1.1 4.9-1.1 1.6-3.2 2-4.9.9-2.1-1.4-4-2.4-6.1-2.4-1.6 0-3.1.5-4 1.2-.9.6-1.1 1.2-1.1 1.5 0 .3 0 .3.1.5.1.1.3.4.7.7.9.6 2.6 1.2 4.8 1.6l.1.1h.1c2.2.4 4.2 1 6.1 1.9 1.8.8 3.6 2 4.7 3.9.8 1.3 1.3 2.8 1.3 4.3 0 3.2-1.8 5.9-4.1 7.6-2.4 1.6-5.3 2.6-8.6 2.6-5.1-.1-9.1-2.4-11.7-4.5-1.4-1.3-1.6-3.5-.4-5z" fill="#currentColor"/><path fill="#A41E11" d="M106.9 62.7c-5 2.6-30.7 13.2-36.2 16-5.5 2.9-8.5 2.8-12.8.8-4.4-2.1-31.7-13.1-36.7-15.5-2.5-1.2-3.8-2.2-3.8-3.1v-9.4s35.6-7.8 41.4-9.8c5.8-2.1 7.8-2.1 12.6-.3 4.9 1.8 34.2 7.1 39 8.8v9.3c.1.9-1 1.9-3.5 3.2z"/><path fill="#D82C20" d="M106.9 53.3c-5 2.6-30.7 13.2-36.2 16-5.5 2.9-8.5 2.8-12.8.8C53.5 68 26.2 57 21.2 54.6c-4.9-2.4-5-4-.2-5.9 4.8-1.9 32.1-12.6 37.8-14.6 5.8-2.1 7.8-2.1 12.6-.3 4.9 1.8 30.5 12 35.3 13.7 5 1.8 5.2 3.2.2 5.8z"/><path fill="#A41E11" d="M106.9 47.4c-5 2.6-30.7 13.2-36.2 16-5.5 2.9-8.5 2.8-12.8.8-4.4-2.1-31.7-13.2-36.7-15.5-2.5-1.2-3.8-2.2-3.8-3.1v-9.4s35.6-7.8 41.4-9.8c5.8-2.1 7.8-2.1 12.6-.3 4.9 1.8 34.2 7.1 39 8.8v9.3c.1.9-1 1.9-3.5 3.2z"/><path fill="#D82C20" d="M106.9 38c-5 2.6-30.7 13.2-36.2 16-5.5 2.9-8.5 2.8-12.8.8-4.3-2.1-31.7-13.1-36.6-15.5-4.9-2.4-5-4-.2-5.9 4.8-1.9 32.1-12.6 37.8-14.6 5.8-2.1 7.8-2.1 12.6-.3 4.9 1.8 30.5 12 35.3 13.7 4.9 1.7 5.1 3.2.1 5.8z"/><path fill="#A41E11" d="M106.9 31.5c-5 2.6-30.7 13.2-36.2 16-5.5 2.9-8.5 2.8-12.8.8-4.3-2.1-31.7-13.1-36.6-15.5-2.5-1.2-3.8-2.2-3.8-3.1v-9.4s35.6-7.8 41.4-9.8c5.8-2.1 7.8-2.1 12.6-.3 4.9 1.8 34.2 7.1 39 8.8v9.3c0 .8-1.1 1.9-3.6 3.2z"/><path fill="#D82C20" d="M106.9 22.1c-5 2.6-30.7 13.2-36.2 16-5.5 2.9-8.5 2.8-12.8.8-4.3-2.1-31.7-13.1-36.6-15.5s-5-4-.2-5.9c4.8-1.9 32.1-12.6 37.8-14.6C64.7.8 66.7.8 71.5 2.6c4.9 1.8 30.5 12 35.3 13.7 4.9 1.7 5.1 3.2.1 5.8z"/><path fill="#fff" d="M76.2 13l-8.1.8-1.8 4.4-2.9-4.9-9.3-.8L61 10l-2-3.8 6.5 2.5 6.1-2-1.7 4zM65.8 34.1l-15-6.3 21.6-3.3z"/><ellipse fill="#fff" cx="45" cy="19.9" rx="11.5" ry="4.5"/><path fill="#7A0C00" d="M85.7 14.2l12.8 5-12.8 5.1z"/><path fill="#AD2115" d="M71.6 19.8l14.1-5.6v10.1l-1.3.5z"/></svg>',
|
||||
'logo' => '<svg class="w-[4.5rem] h-[4.5rem] p-2 transition-all duration-200 bg-black/10 dark:bg-white/10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path d="M21.4 97.6c0 1.8-1.5 3.5-3.5 3.5-1.5 0-2.8.4-4 1.3-1.3.8-2.2 1.9-3 3.2-1.6 2.1-2.4 4.6-2.7 5.5v12.5c0 1.9-1.6 3.5-3.6 3.5-1.9 0-3.5-1.6-3.5-3.5v-26c0-1.9 1.6-3.4 3.5-3.4 2 0 3.6 1.5 3.6 3.4v.5c.4-.5.9-1 1.4-1.3 2.2-1.4 5-2.6 8.3-2.6 2 0 3.5 1.5 3.5 3.4zm-1.9 13c.1-9 7-16.5 16.1-16.5 8.6 0 15.3 6.4 15.9 15.3v.3c0 .1 0 .5-.1.6-.2 1.6-1.6 2.6-3.4 2.6H27c.3 1.5 1.1 3.2 2.2 4.3 1.4 1.6 4 2.8 6.3 3 2.4.2 5.2-.4 6.8-1.6 1.4-1.4 4.1-1.3 4.9-.2.9.9 1.5 2.9 0 4.3-3.2 3-7.1 4.3-11.8 4.3-8.9 0-15.9-7.5-15.9-16.4zm7.1-3.2h18.6c-.7-2.6-4-6.5-9.7-7-5.6.2-8.3 4.3-8.9 7zm58.3 16.1c0 1.9-1.6 3.6-3.6 3.6-1.8 0-3.2-1.3-3.5-2.8-2.5 1.7-5.7 2.8-9 2.8-8.9 0-16-7.5-16-16.4 0-9 7.1-16.5 16-16.5 3.2 0 6.4 1.1 8.8 2.8V84.5c0-1.9 1.6-3.6 3.6-3.6s3.6 1.6 3.6 3.6v26.2l.1 12.8zm-16-22.2c-2.4 0-4.5 1-6.2 2.7-1.6 1.6-2.6 4-2.6 6.6 0 2.5 1 4.9 2.6 6.5 1.6 1.7 3.8 2.7 6.2 2.7 2.4 0 4.5-1 6.2-2.7 1.6-1.6 2.6-4 2.6-6.5 0-2.6-1-5-2.6-6.6-1.6-1.7-3.7-2.7-6.2-2.7zm28.6-15.4c0 2-1.5 3.6-3.6 3.6-2 0-3.6-1.6-3.6-3.6v-1.4c0-2 1.6-3.6 3.6-3.6s3.6 1.6 3.6 3.6v1.4zm0 11.9v25.7c0 2-1.5 3.6-3.6 3.6-2 0-3.6-1.6-3.6-3.6V97.8c0-2.1 1.6-3.6 3.6-3.6 2.1 0 3.6 1.5 3.6 3.6zm4.5 19.8c1.2-1.6 3.5-1.8 4.9-.5 1.7 1.4 4.7 3 7.2 2.9 1.8 0 3.4-.6 4.5-1.3.9-.8 1.2-1.4 1.2-2 0-.3-.1-.5-.2-.7-.1-.2-.3-.5-.9-.8-.9-.7-2.9-1.4-5.3-1.8h-.1c-2-.4-4-.9-5.7-1.7-1.8-.9-3.4-2-4.5-3.8-.7-1.2-1.1-2.6-1.1-4.1 0-3 1.7-5.6 3.9-7.2 2.3-1.6 5.1-2.4 8.1-2.4 4.5 0 7.8 2.2 9.9 3.6 1.6 1.1 2 3.2 1.1 4.9-1.1 1.6-3.2 2-4.9.9-2.1-1.4-4-2.4-6.1-2.4-1.6 0-3.1.5-4 1.2-.9.6-1.1 1.2-1.1 1.5 0 .3 0 .3.1.5.1.1.3.4.7.7.9.6 2.6 1.2 4.8 1.6l.1.1h.1c2.2.4 4.2 1 6.1 1.9 1.8.8 3.6 2 4.7 3.9.8 1.3 1.3 2.8 1.3 4.3 0 3.2-1.8 5.9-4.1 7.6-2.4 1.6-5.3 2.6-8.6 2.6-5.1-.1-9.1-2.4-11.7-4.5-1.4-1.3-1.6-3.5-.4-5z" fill="#currentColor"/><path fill="#A41E11" d="M106.9 62.7c-5 2.6-30.7 13.2-36.2 16-5.5 2.9-8.5 2.8-12.8.8-4.4-2.1-31.7-13.1-36.7-15.5-2.5-1.2-3.8-2.2-3.8-3.1v-9.4s35.6-7.8 41.4-9.8c5.8-2.1 7.8-2.1 12.6-.3 4.9 1.8 34.2 7.1 39 8.8v9.3c.1.9-1 1.9-3.5 3.2z"/><path fill="#D82C20" d="M106.9 53.3c-5 2.6-30.7 13.2-36.2 16-5.5 2.9-8.5 2.8-12.8.8C53.5 68 26.2 57 21.2 54.6c-4.9-2.4-5-4-.2-5.9 4.8-1.9 32.1-12.6 37.8-14.6 5.8-2.1 7.8-2.1 12.6-.3 4.9 1.8 30.5 12 35.3 13.7 5 1.8 5.2 3.2.2 5.8z"/><path fill="#A41E11" d="M106.9 47.4c-5 2.6-30.7 13.2-36.2 16-5.5 2.9-8.5 2.8-12.8.8-4.4-2.1-31.7-13.2-36.7-15.5-2.5-1.2-3.8-2.2-3.8-3.1v-9.4s35.6-7.8 41.4-9.8c5.8-2.1 7.8-2.1 12.6-.3 4.9 1.8 34.2 7.1 39 8.8v9.3c.1.9-1 1.9-3.5 3.2z"/><path fill="#D82C20" d="M106.9 38c-5 2.6-30.7 13.2-36.2 16-5.5 2.9-8.5 2.8-12.8.8-4.3-2.1-31.7-13.1-36.6-15.5-4.9-2.4-5-4-.2-5.9 4.8-1.9 32.1-12.6 37.8-14.6 5.8-2.1 7.8-2.1 12.6-.3 4.9 1.8 30.5 12 35.3 13.7 4.9 1.7 5.1 3.2.1 5.8z"/><path fill="#A41E11" d="M106.9 31.5c-5 2.6-30.7 13.2-36.2 16-5.5 2.9-8.5 2.8-12.8.8-4.3-2.1-31.7-13.1-36.6-15.5-2.5-1.2-3.8-2.2-3.8-3.1v-9.4s35.6-7.8 41.4-9.8c5.8-2.1 7.8-2.1 12.6-.3 4.9 1.8 34.2 7.1 39 8.8v9.3c0 .8-1.1 1.9-3.6 3.2z"/><path fill="#D82C20" d="M106.9 22.1c-5 2.6-30.7 13.2-36.2 16-5.5 2.9-8.5 2.8-12.8.8-4.3-2.1-31.7-13.1-36.6-15.5s-5-4-.2-5.9c4.8-1.9 32.1-12.6 37.8-14.6C64.7.8 66.7.8 71.5 2.6c4.9 1.8 30.5 12 35.3 13.7 4.9 1.7 5.1 3.2.1 5.8z"/><path fill="#fff" d="M76.2 13l-8.1.8-1.8 4.4-2.9-4.9-9.3-.8L61 10l-2-3.8 6.5 2.5 6.1-2-1.7 4zM65.8 34.1l-15-6.3 21.6-3.3z"/><ellipse fill="#fff" cx="45" cy="19.9" rx="11.5" ry="4.5"/><path fill="#7A0C00" d="M85.7 14.2l12.8 5-12.8 5.1z"/><path fill="#AD2115" d="M71.6 19.8l14.1-5.6v10.1l-1.3.5z"/></svg>',
|
||||
],
|
||||
[
|
||||
'id' => 'keydb',
|
||||
'name' => 'KeyDB',
|
||||
'description' => 'KeyDB is a database that offers high performance, low latency, and scalability for various data structures and workloads.',
|
||||
'logo' => ' <div class="w-[4.5rem] aspect-square h-[4.5rem] p-2 transition-all duration-200 dark:opacity-30 grayscale group-hover:grayscale-0 group-hover:opacity-100 dark:fill-white fill-black bg-black/10 dark:bg-white/10"><svg version="1.1" width="180" height="60" id="svg1326" viewBox="0 0 544 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"><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" width="791px"/><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></div>',
|
||||
'logo' => ' <div class="w-[4.5rem] h-[4.5rem] p-2 transition-all duration-200 bg-black/10 dark:bg-white/10"><svg version="1.1" width="180" height="60" id="svg1326" viewBox="0 0 544 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"><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" width="791px"/><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></div>',
|
||||
],
|
||||
[
|
||||
'id' => 'dragonfly',
|
||||
'name' => 'Dragonfly',
|
||||
'description' => 'Dragonfly DB is a drop-in Redis replacement that delivers 25x more throughput and 12x faster snapshotting than Redis.',
|
||||
'logo' => '<div class="w-[4.5rem] aspect-square h-[4.5rem] p-2 transition-all duration-200 dark: opacity-30 grayscale group-hover:grayscale-0 group-hover:opacity-100 dark:fill-white fill-black bg-black/10 dark:bg-white/10"><svg class="block dark:hidden" width="245" height="60" viewBox="0 0 210 50" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M22 0C3.883 0 0 3.883 0 22C0 40.117 3.883 44 22 44C40.117 44 44 40.117 44 22C44 3.883 40.117 0 22 0ZM19.5585 16.265C19.8224 16.406 20 16.7008 20 17C20 18.1046 20.8954 19 22 19C23.1046 19 24 18.1046 24 17C24 16.7008 24.1776 16.406 24.4414 16.265C25.0714 15.9283 25.5 15.2642 25.5 14.5C25.5 13.3954 24.6046 12.5 23.5 12.5C23.3071 12.5 23.1205 12.5273 22.944 12.5783C22.3661 12.7452 21.6339 12.7452 21.056 12.5783C20.8794 12.5273 20.6929 12.5 20.5 12.5C19.3954 12.5 18.5 13.3954 18.5 14.5C18.5 15.2642 18.9286 15.9283 19.5585 16.265ZM20.3087 18.8411C20.2282 18.8977 20.1644 18.978 20.1283 19.0744L19.6243 20.4185C19.5438 20.633 19.5395 20.8686 19.6119 21.0859L20.4253 23.526C20.4744 23.6733 20.5566 23.8087 20.687 23.893C20.9154 24.0408 21.3531 24.25 22 24.25C22.6468 24.25 23.0845 24.0408 23.3129 23.893C23.4433 23.8087 23.5255 23.6733 23.5746 23.526L24.388 21.0859C24.4604 20.8686 24.4561 20.633 24.3756 20.4185L23.8716 19.0744C23.8355 18.978 23.7717 18.8977 23.6912 18.8411C23.2461 19.2502 22.6522 19.5 22 19.5C21.3477 19.5 20.7538 19.2502 20.3087 18.8411ZM21 30L20.5105 24.3712C20.8174 24.5497 21.3156 24.75 22 24.75C22.6842 24.75 23.1825 24.5498 23.4895 24.3711L23 30C23 30 22.75 30.25 22 30.25C21.25 30.25 21 30 21 30ZM21.25 34L21.0055 30.5779L21.0331 30.5892C21.2547 30.6779 21.5686 30.75 22 30.75C22.4314 30.75 22.7453 30.6779 22.967 30.5892L22.9944 30.578L22.75 34C22.75 34 22.625 34.5 22 34.5C21.375 34.5 21.25 34 21.25 34ZM9.24995 20H19.2C18.8224 20.6294 18.9914 21.0678 19.1582 21.5005L19.1582 21.5005L19.1582 21.5006L19.1582 21.5006L19.1582 21.5006L19.1582 21.5006C19.1901 21.5835 19.222 21.6662 19.25 21.75L9.90851 24.4252C9.31202 24.5932 8.67362 24.3772 8.3018 23.8814L7.71391 23.0976C7.37302 22.643 7.31818 22.0349 7.57227 21.5267L7.92108 20.8291C8.17311 20.3251 8.68641 20.0048 9.24995 20ZM7.99998 19.2331L19.375 19.5C19.375 19.5 19.5546 19.0506 19.625 18.75C19.6491 18.647 19.6745 18.5441 19.6961 18.4586C19.7264 18.3386 19.6699 18.2154 19.5562 18.1662C18.2312 17.5935 10.5221 14.2625 9.23721 13.7331C8.73807 13.5197 8.16257 13.5926 7.73242 13.9238L6.58499 14.8072C6.20854 15.097 5.99872 15.5412 5.99998 16C6.00037 16.1456 6.02204 16.2928 6.06639 16.4369L6.62254 18.2331C6.80943 18.8405 7.36503 19.2067 7.99998 19.2331ZM34.75 20H24.8C25.1776 20.6294 25.0086 21.0679 24.8418 21.5005L24.8418 21.5005L24.8418 21.5006L24.8418 21.5006C24.8098 21.5835 24.7779 21.6662 24.75 21.75L34.0914 24.4252C34.6879 24.5932 35.3263 24.3772 35.6982 23.8814L36.286 23.0976C36.6269 22.643 36.6818 22.0349 36.4277 21.5267L36.0789 20.8291C35.8268 20.3251 35.3135 20.0048 34.75 20ZM36 19.2331L24.625 19.5C24.625 19.5 24.4453 19.0506 24.375 18.75C24.3509 18.647 24.3255 18.5441 24.3039 18.4586C24.2735 18.3386 24.3301 18.2154 24.4438 18.1662C25.7688 17.5935 33.4779 14.2625 34.7627 13.7331C35.2619 13.5197 35.8374 13.5926 36.2675 13.9238L37.415 14.8072C37.7914 15.097 38.0012 15.5412 38 16C37.9996 16.1456 37.9779 16.2928 37.9336 16.4369L37.3774 18.2331C37.1905 18.8405 36.6349 19.2067 36 19.2331Z" fill="black"/></svg><svg class="hidden dark:block" xmlns="http://www.w3.org/2000/svg" width="225" height="55" viewBox="0 0 378 88" fill="none"><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 4
Line truncated
|
||||
'logo' => '<div class="w-[4.5rem] h-[4.5rem] p-2 transition-all duration-200 dark: opacity-30 grayscale group-hover:grayscale-0 group-hover:opacity-100 bg-black/10 dark:bg-white/10"><svg class="block dark:hidden" width="245" height="60" viewBox="0 0 210 50" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M22 0C3.883 0 0 3.883 0 22C0 40.117 3.883 44 22 44C40.117 44 44 40.117 44 22C44 3.883 40.117 0 22 0ZM19.5585 16.265C19.8224 16.406 20 16.7008 20 17C20 18.1046 20.8954 19 22 19C23.1046 19 24 18.1046 24 17C24 16.7008 24.1776 16.406 24.4414 16.265C25.0714 15.9283 25.5 15.2642 25.5 14.5C25.5 13.3954 24.6046 12.5 23.5 12.5C23.3071 12.5 23.1205 12.5273 22.944 12.5783C22.3661 12.7452 21.6339 12.7452 21.056 12.5783C20.8794 12.5273 20.6929 12.5 20.5 12.5C19.3954 12.5 18.5 13.3954 18.5 14.5C18.5 15.2642 18.9286 15.9283 19.5585 16.265ZM20.3087 18.8411C20.2282 18.8977 20.1644 18.978 20.1283 19.0744L19.6243 20.4185C19.5438 20.633 19.5395 20.8686 19.6119 21.0859L20.4253 23.526C20.4744 23.6733 20.5566 23.8087 20.687 23.893C20.9154 24.0408 21.3531 24.25 22 24.25C22.6468 24.25 23.0845 24.0408 23.3129 23.893C23.4433 23.8087 23.5255 23.6733 23.5746 23.526L24.388 21.0859C24.4604 20.8686 24.4561 20.633 24.3756 20.4185L23.8716 19.0744C23.8355 18.978 23.7717 18.8977 23.6912 18.8411C23.2461 19.2502 22.6522 19.5 22 19.5C21.3477 19.5 20.7538 19.2502 20.3087 18.8411ZM21 30L20.5105 24.3712C20.8174 24.5497 21.3156 24.75 22 24.75C22.6842 24.75 23.1825 24.5498 23.4895 24.3711L23 30C23 30 22.75 30.25 22 30.25C21.25 30.25 21 30 21 30ZM21.25 34L21.0055 30.5779L21.0331 30.5892C21.2547 30.6779 21.5686 30.75 22 30.75C22.4314 30.75 22.7453 30.6779 22.967 30.5892L22.9944 30.578L22.75 34C22.75 34 22.625 34.5 22 34.5C21.375 34.5 21.25 34 21.25 34ZM9.24995 20H19.2C18.8224 20.6294 18.9914 21.0678 19.1582 21.5005L19.1582 21.5005L19.1582 21.5006L19.1582 21.5006L19.1582 21.5006L19.1582 21.5006C19.1901 21.5835 19.222 21.6662 19.25 21.75L9.90851 24.4252C9.31202 24.5932 8.67362 24.3772 8.3018 23.8814L7.71391 23.0976C7.37302 22.643 7.31818 22.0349 7.57227 21.5267L7.92108 20.8291C8.17311 20.3251 8.68641 20.0048 9.24995 20ZM7.99998 19.2331L19.375 19.5C19.375 19.5 19.5546 19.0506 19.625 18.75C19.6491 18.647 19.6745 18.5441 19.6961 18.4586C19.7264 18.3386 19.6699 18.2154 19.5562 18.1662C18.2312 17.5935 10.5221 14.2625 9.23721 13.7331C8.73807 13.5197 8.16257 13.5926 7.73242 13.9238L6.58499 14.8072C6.20854 15.097 5.99872 15.5412 5.99998 16C6.00037 16.1456 6.02204 16.2928 6.06639 16.4369L6.62254 18.2331C6.80943 18.8405 7.36503 19.2067 7.99998 19.2331ZM34.75 20H24.8C25.1776 20.6294 25.0086 21.0679 24.8418 21.5005L24.8418 21.5005L24.8418 21.5006L24.8418 21.5006C24.8098 21.5835 24.7779 21.6662 24.75 21.75L34.0914 24.4252C34.6879 24.5932 35.3263 24.3772 35.6982 23.8814L36.286 23.0976C36.6269 22.643 36.6818 22.0349 36.4277 21.5267L36.0789 20.8291C35.8268 20.3251 35.3135 20.0048 34.75 20ZM36 19.2331L24.625 19.5C24.625 19.5 24.4453 19.0506 24.375 18.75C24.3509 18.647 24.3255 18.5441 24.3039 18.4586C24.2735 18.3386 24.3301 18.2154 24.4438 18.1662C25.7688 17.5935 33.4779 14.2625 34.7627 13.7331C35.2619 13.5197 35.8374 13.5926 36.2675 13.9238L37.415 14.8072C37.7914 15.097 38.0012 15.5412 38 16C37.9996 16.1456 37.9779 16.2928 37.9336 16.4369L37.3774 18.2331C37.1905 18.8405 36.6349 19.2067 36 19.2331Z" fill="black"/></svg><svg class="hidden dark:block" xmlns="http://www.w3.org/2000/svg" width="225" height="55" viewBox="0 0 378 88" fill="none"><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 40H
Line truncated
|
||||
],
|
||||
[
|
||||
'id' => 'mongodb',
|
||||
'name' => 'MongoDB',
|
||||
'description' => 'MongoDB is a source-available, cross-platform, document-oriented database program.',
|
||||
'logo' => '<svg class="w-[4.5rem] aspect-square h-[4.5rem] p-2 transition-all duration-200 dark:opacity-30 grayscale group-hover:grayscale-0 group-hover:opacity-100 dark:fill-white fill-black bg-black/10 dark:bg-white/10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill-rule="evenodd" clip-rule="evenodd" fill="currentColor" d="M87.259 100.139c.169-.325.331-.612.469-.909.087-.19.221-.228.41-.223 1.133.032 2.266.067 3.4.078.963.01 1.928-.008 2.892-.019 1.086-.013 2.172-.07 3.257-.039 1.445.042 2.853.325 4.16.968 1.561.769 2.742 1.94 3.547 3.483a8.71 8.71 0 01.931 3.14c.172 1.608.059 3.179-.451 4.717-.632 1.906-1.832 3.365-3.499 4.458-1.283.841-2.69 1.338-4.198 1.622-1.596.301-3.197.204-4.798.209-1.756.007-3.511-.031-5.267-.051-.307-.003-.351-.061-.27-.354l.075-.27c.171-.538.263-.562.809-.652a2.83 2.83 0 001.087-.413c.184-.122.26-.44.332-.685.062-.214.065-.449.067-.675.025-3.425.051-6.849.065-10.272a44.077 44.077 0 00-.065-2.596c-.034-.605-.357-1.019-1.077-1.162-.56-.111-1.124-.197-1.687-.296l-.189-.059zm16.076 8.293c-.076-.682-.113-1.37-.235-2.042-.292-1.613-.998-3.018-2.238-4.119-2.005-1.779-4.419-2.053-6.949-1.841-.576.048-.7.245-.709.837-.014.84-.028 1.68-.029 2.52-.004 2.664-.004 5.328 0 7.992.001.758.009 1.516.031 2.272.024.774.305 1.429 1.063 1.729 1.195.473 2.452.529 3.706.336 2.003-.307 3.404-1.474 4.344-3.223.744-1.388.954-2.903 1.016-4.461zm4.869 9.071c-.024-.415.146-.758.356-1.073.057-.085.253-.081.388-.108l1.146-.227c.405-.086.618-.358.675-.755.038-.262.074-.527.077-.792a879.6 879.6 0 00.059-6.291c.01-2.1.002-4.2.002-6.3l-.009-.401c-.041-.675-.367-1.025-1.037-1.124l-1.453-.221c-.179-.024-.244-.11-.179-.269.112-.271.219-.552.377-.796.059-.09.258-.125.392-.122.694.01 1.388.062 2.082.061l6.041-.036c1.164-.001 2.288.202 3.332.759 1.149.612 1.792 1.559 1.976 2.849.192 1.355-.219 2.497-1.209 3.404-.407.374-.934.618-1.406.922l-.154.096c.438.161.855.3 1.261.466 1.188.487 2.133 1.248 2.633 2.463.395.959.395 1.959.161 2.953-.364 1.556-1.389 2.591-2.722 3.374-1.251.735-2.605 1.163-4.047 1.235-1.33.067-2.666.042-3.999.057l-.772.004a996.106 996.106 0 01-3.854-.096l-.117-.032zm5.537-6.089h.013c0 .658-.009 1.316.003 1.974.008.426-.007.864.085 1.274.138.613.418 1.166 1.106 1.342a6.671 6.671 0 002.818.124c1.177-.205 2.116-.795 2.631-1.916.382-.833.439-1.716.308-2.618-.174-1.188-.805-2.05-1.854-2.615-.688-.371-1.422-.598-2.204-.628-.876-.033-1.753-.035-2.629-.062-.246-.007-.28.118-.279.32.005.934.002 1.869.002 2.805zm1.865-4.475c.479-.024 1.021-.031 1.56-.085 1.032-.103 1.759-.622 2.138-1.609.193-.501.185-1.017.19-1.538.015-1.357-.777-2.469-2.066-2.929-.995-.355-2.021-.361-3.053-.333-.418.011-.605.194-.611.615l-.062 5.489c-.003.218.091.312.303.319l1.601.071z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="currentColor" d="M10.543 116.448l-.073.944c-.68 0-1.307-.005-1.934.002-1.181.012-2.362.031-3.544.048l-.114.007c-.169-.02-.476-.02-.484-.07-.05-.281-.034-.576-.021-.867.001-.033.116-.075.183-.091l.919-.205c.573-.149.775-.396.802-.988.031-.667.062-1.335.065-2.002.009-1.642-.001-3.282.006-4.924.001-.384-.132-.67-.49-.826a43.787 43.787 0 00-1.285-.546c-.204-.082-.469-.127-.445-.401.024-.279.281-.352.523-.407 1.002-.229 2.005-.452 3.004-.696.322-.079.63-.212 1.015-.346.02.208.057.406.053.604l-.059.941c-.001.106.054.248.133.307.048.037.209-.03.289-.092.854-.65 1.758-1.211 2.789-1.538 1.597-.507 2.968-.037 3.928 1.34.338.485.339.485.808.146.805-.585 1.647-1.101 2.589-1.441 2.068-.747 4.055.201 4.774 2.284.262.756.362 1.537.36 2.335l-.019 5.298c-.001.437.144.686.56.822.467.153.951.258 1.477.396l-.122.911c-.598 0-1.148-.004-1.698.001-1.344.012-2.688.019-4.031.05-.234.006-.295-.052-.307-.271-.039-.701-.045-.7.615-.858l.222-.057c.645-.176.86-.374.865-1.028.015-1.878.054-3.761-.041-5.635-.099-1.944-1.642-2.979-3.526-2.481a5.194 5.194 0 00-1.69.814c-.175.125-.208.269-.194.488.053.828.086 1.657.093 2.486.012 1.451-.006 2.902 0 4.354.002.588.203.813.784.949l.863.199.16.036c.012.276.023.552.01.828-.008.173-.142.188-.292.185-.839-.021-1.679-.049-2.518-.047-1.021.002-2.041.031-3.061.049h-.24c0-.293-.014-.573.01-.852.005-.067.123-.161.204-.182l1.006-.213c.427-.105.631-.324.655-.758.114-2.01.196-4.021.007-6.03a3.695 3.695 0 00-.326-1.145c-.515-1.138-1.674-1.613-3.011-1.271-.635.162-1.208.453-1.75.82a.795.795 0 00-.378.695c0 2.005.007 4.01.013 6.014l.011.773c.012.539.241.823.776.939.344.078.692.131 1.082.203z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="currentColor" d="M71.001 105.285c.155.754.152 1.432-.402 1.946-.55.511-1.246.339-1.925.225.063.358.133.662.167.97.247 2.289-.738 3.988-2.861 4.959-.802.366-1.653.502-2.522.572-.432.034-.81.364-.851.719-.042.36.184.73.636.838.533.127 1.089.163 1.636.226 1.174.134 2.361.195 3.521.405 1.754.316 2.733 1.847 2.424 3.609-.275 1.568-1.183 2.709-2.449 3.584-2.133 1.478-4.473 1.91-6.965 1.156-1.425-.432-2.43-1.369-2.777-2.885-.174-.759.011-1.446.582-1.961.679-.61 1.418-1.154 2.129-1.73l.23-.231-.264-.185c-.725-.344-1.305-.815-1.53-1.633-.077-.
Line truncated
|
||||
'logo' => '<svg class="w-[4.5rem] h-[4.5rem] p-2 transition-all duration-200 bg-black/10 dark:bg-white/10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill-rule="evenodd" clip-rule="evenodd" fill="currentColor" d="M87.259 100.139c.169-.325.331-.612.469-.909.087-.19.221-.228.41-.223 1.133.032 2.266.067 3.4.078.963.01 1.928-.008 2.892-.019 1.086-.013 2.172-.07 3.257-.039 1.445.042 2.853.325 4.16.968 1.561.769 2.742 1.94 3.547 3.483a8.71 8.71 0 01.931 3.14c.172 1.608.059 3.179-.451 4.717-.632 1.906-1.832 3.365-3.499 4.458-1.283.841-2.69 1.338-4.198 1.622-1.596.301-3.197.204-4.798.209-1.756.007-3.511-.031-5.267-.051-.307-.003-.351-.061-.27-.354l.075-.27c.171-.538.263-.562.809-.652a2.83 2.83 0 001.087-.413c.184-.122.26-.44.332-.685.062-.214.065-.449.067-.675.025-3.425.051-6.849.065-10.272a44.077 44.077 0 00-.065-2.596c-.034-.605-.357-1.019-1.077-1.162-.56-.111-1.124-.197-1.687-.296l-.189-.059zm16.076 8.293c-.076-.682-.113-1.37-.235-2.042-.292-1.613-.998-3.018-2.238-4.119-2.005-1.779-4.419-2.053-6.949-1.841-.576.048-.7.245-.709.837-.014.84-.028 1.68-.029 2.52-.004 2.664-.004 5.328 0 7.992.001.758.009 1.516.031 2.272.024.774.305 1.429 1.063 1.729 1.195.473 2.452.529 3.706.336 2.003-.307 3.404-1.474 4.344-3.223.744-1.388.954-2.903 1.016-4.461zm4.869 9.071c-.024-.415.146-.758.356-1.073.057-.085.253-.081.388-.108l1.146-.227c.405-.086.618-.358.675-.755.038-.262.074-.527.077-.792a879.6 879.6 0 00.059-6.291c.01-2.1.002-4.2.002-6.3l-.009-.401c-.041-.675-.367-1.025-1.037-1.124l-1.453-.221c-.179-.024-.244-.11-.179-.269.112-.271.219-.552.377-.796.059-.09.258-.125.392-.122.694.01 1.388.062 2.082.061l6.041-.036c1.164-.001 2.288.202 3.332.759 1.149.612 1.792 1.559 1.976 2.849.192 1.355-.219 2.497-1.209 3.404-.407.374-.934.618-1.406.922l-.154.096c.438.161.855.3 1.261.466 1.188.487 2.133 1.248 2.633 2.463.395.959.395 1.959.161 2.953-.364 1.556-1.389 2.591-2.722 3.374-1.251.735-2.605 1.163-4.047 1.235-1.33.067-2.666.042-3.999.057l-.772.004a996.106 996.106 0 01-3.854-.096l-.117-.032zm5.537-6.089h.013c0 .658-.009 1.316.003 1.974.008.426-.007.864.085 1.274.138.613.418 1.166 1.106 1.342a6.671 6.671 0 002.818.124c1.177-.205 2.116-.795 2.631-1.916.382-.833.439-1.716.308-2.618-.174-1.188-.805-2.05-1.854-2.615-.688-.371-1.422-.598-2.204-.628-.876-.033-1.753-.035-2.629-.062-.246-.007-.28.118-.279.32.005.934.002 1.869.002 2.805zm1.865-4.475c.479-.024 1.021-.031 1.56-.085 1.032-.103 1.759-.622 2.138-1.609.193-.501.185-1.017.19-1.538.015-1.357-.777-2.469-2.066-2.929-.995-.355-2.021-.361-3.053-.333-.418.011-.605.194-.611.615l-.062 5.489c-.003.218.091.312.303.319l1.601.071z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="currentColor" d="M10.543 116.448l-.073.944c-.68 0-1.307-.005-1.934.002-1.181.012-2.362.031-3.544.048l-.114.007c-.169-.02-.476-.02-.484-.07-.05-.281-.034-.576-.021-.867.001-.033.116-.075.183-.091l.919-.205c.573-.149.775-.396.802-.988.031-.667.062-1.335.065-2.002.009-1.642-.001-3.282.006-4.924.001-.384-.132-.67-.49-.826a43.787 43.787 0 00-1.285-.546c-.204-.082-.469-.127-.445-.401.024-.279.281-.352.523-.407 1.002-.229 2.005-.452 3.004-.696.322-.079.63-.212 1.015-.346.02.208.057.406.053.604l-.059.941c-.001.106.054.248.133.307.048.037.209-.03.289-.092.854-.65 1.758-1.211 2.789-1.538 1.597-.507 2.968-.037 3.928 1.34.338.485.339.485.808.146.805-.585 1.647-1.101 2.589-1.441 2.068-.747 4.055.201 4.774 2.284.262.756.362 1.537.36 2.335l-.019 5.298c-.001.437.144.686.56.822.467.153.951.258 1.477.396l-.122.911c-.598 0-1.148-.004-1.698.001-1.344.012-2.688.019-4.031.05-.234.006-.295-.052-.307-.271-.039-.701-.045-.7.615-.858l.222-.057c.645-.176.86-.374.865-1.028.015-1.878.054-3.761-.041-5.635-.099-1.944-1.642-2.979-3.526-2.481a5.194 5.194 0 00-1.69.814c-.175.125-.208.269-.194.488.053.828.086 1.657.093 2.486.012 1.451-.006 2.902 0 4.354.002.588.203.813.784.949l.863.199.16.036c.012.276.023.552.01.828-.008.173-.142.188-.292.185-.839-.021-1.679-.049-2.518-.047-1.021.002-2.041.031-3.061.049h-.24c0-.293-.014-.573.01-.852.005-.067.123-.161.204-.182l1.006-.213c.427-.105.631-.324.655-.758.114-2.01.196-4.021.007-6.03a3.695 3.695 0 00-.326-1.145c-.515-1.138-1.674-1.613-3.011-1.271-.635.162-1.208.453-1.75.82a.795.795 0 00-.378.695c0 2.005.007 4.01.013 6.014l.011.773c.012.539.241.823.776.939.344.078.692.131 1.082.203z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="currentColor" d="M71.001 105.285c.155.754.152 1.432-.402 1.946-.55.511-1.246.339-1.925.225.063.358.133.662.167.97.247 2.289-.738 3.988-2.861 4.959-.802.366-1.653.502-2.522.572-.432.034-.81.364-.851.719-.042.36.184.73.636.838.533.127 1.089.163 1.636.226 1.174.134 2.361.195 3.521.405 1.754.316 2.733 1.847 2.424 3.609-.275 1.568-1.183 2.709-2.449 3.584-2.133 1.478-4.473 1.91-6.965 1.156-1.425-.432-2.43-1.369-2.777-2.885-.174-.759.011-1.446.582-1.961.679-.61 1.418-1.154 2.129-1.73l.23-.231-.264-.185c-.725-.344-1.305-.815-1.53-1.633-.077-.277.003-.459.238-.601.4-.241.798-.486 1.193-.735.186-.116.37-.236.543-.37.236-.18.215-.314-.067-.418-.656-.242-1.239
Line truncated
|
||||
],
|
||||
[
|
||||
'id' => 'clickhouse',
|
||||
'name' => 'ClickHouse',
|
||||
'description' => 'ClickHouse is a column-oriented database that supports real-time analytics, business intelligence, observability, ML and GenAI, and more.',
|
||||
'logo' => '<div class="w-[4.5rem] aspect-square h-[4.5rem] p-2 transition-all duration-200 dark:opacity-30 grayscale group-hover:grayscale-0 group-hover:opacity-100 dark:fill-white fill-black bg-black/10 dark:bg-white/10"><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></div>',
|
||||
'logo' => '<div class="w-[4.5rem] h-[4.5rem] p-2 transition-all duration-200 bg-black/10 dark:bg-white/10"><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></div>',
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
return [
|
||||
'services' => $services,
|
||||
'categories' => $categories,
|
||||
'gitBasedApplications' => $gitBasedApplications,
|
||||
'dockerBasedApplications' => $dockerBasedApplications,
|
||||
'databases' => $databases,
|
||||
|
||||
@@ -54,6 +54,11 @@ class Heading extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function manualCheckStatus()
|
||||
{
|
||||
$this->checkStatus();
|
||||
}
|
||||
|
||||
public function serviceChecked()
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -33,6 +33,8 @@ class GetLogs extends Component
|
||||
|
||||
public ?string $container = null;
|
||||
|
||||
public ?string $displayName = null;
|
||||
|
||||
public ?string $pull_request = null;
|
||||
|
||||
public ?bool $streamLogs = false;
|
||||
|
||||
@@ -74,12 +74,16 @@ class ByHetzner extends Component
|
||||
#[Locked]
|
||||
public Collection $saved_cloud_init_scripts;
|
||||
|
||||
public bool $from_onboarding = false;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->authorize('viewAny', CloudProviderToken::class);
|
||||
$this->loadTokens();
|
||||
$this->loadSavedCloudInitScripts();
|
||||
$this->server_name = generate_random_name();
|
||||
$this->private_keys = PrivateKey::ownedAndOnlySShKeys()->where('id', '!=', 0)->get();
|
||||
|
||||
if ($this->private_keys->count() > 0) {
|
||||
$this->private_key_id = $this->private_keys->first()->id;
|
||||
}
|
||||
@@ -131,7 +135,7 @@ class ByHetzner extends Component
|
||||
public function handlePrivateKeyCreated($keyId)
|
||||
{
|
||||
// Refresh private keys list
|
||||
$this->private_keys = PrivateKey::ownedByCurrentTeam()->get();
|
||||
$this->private_keys = PrivateKey::ownedAndOnlySShKeys()->where('id', '!=', 0)->get();
|
||||
|
||||
// Auto-select the new key
|
||||
$this->private_key_id = $keyId;
|
||||
@@ -246,12 +250,6 @@ class ByHetzner extends Component
|
||||
// Get images and sort by name
|
||||
$images = $hetznerService->getImages();
|
||||
|
||||
ray('Raw images from Hetzner API', [
|
||||
'total_count' => count($images),
|
||||
'types' => collect($images)->pluck('type')->unique()->values(),
|
||||
'sample' => array_slice($images, 0, 3),
|
||||
]);
|
||||
|
||||
$this->images = collect($images)
|
||||
->filter(function ($image) {
|
||||
// Only system images
|
||||
@@ -269,20 +267,8 @@ class ByHetzner extends Component
|
||||
->sortBy('name')
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
ray('Filtered images', [
|
||||
'filtered_count' => count($this->images),
|
||||
'debian_images' => collect($this->images)->filter(fn ($img) => str_contains($img['name'] ?? '', 'debian'))->values(),
|
||||
]);
|
||||
|
||||
// Load SSH keys from Hetzner
|
||||
$this->hetznerSshKeys = $hetznerService->getSshKeys();
|
||||
|
||||
ray('Hetzner SSH Keys', [
|
||||
'total_count' => count($this->hetznerSshKeys),
|
||||
'keys' => $this->hetznerSshKeys,
|
||||
]);
|
||||
|
||||
$this->loading_data = false;
|
||||
} catch (\Throwable $e) {
|
||||
$this->loading_data = false;
|
||||
@@ -299,9 +285,9 @@ class ByHetzner extends Component
|
||||
} elseif (str_starts_with($name, 'cpx')) {
|
||||
return 'AMD EPYC™';
|
||||
} elseif (str_starts_with($name, 'cx')) {
|
||||
return 'Intel® Xeon®';
|
||||
return 'Intel®/AMD';
|
||||
} elseif (str_starts_with($name, 'cax')) {
|
||||
return 'Ampere® Altra®';
|
||||
return 'Ampere®';
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -574,6 +560,11 @@ class ByHetzner extends Component
|
||||
$server->proxy->set('type', ProxyTypes::TRAEFIK->value);
|
||||
$server->save();
|
||||
|
||||
if ($this->from_onboarding) {
|
||||
// When in onboarding, use wire:navigate for proper modal handling
|
||||
return $this->redirect(route('server.show', $server->uuid));
|
||||
}
|
||||
|
||||
return redirect()->route('server.show', $server->uuid);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
|
||||
@@ -7,7 +7,6 @@ use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -39,25 +38,12 @@ class ByIp extends Component
|
||||
|
||||
public int $port = 22;
|
||||
|
||||
public bool $is_swarm_manager = false;
|
||||
|
||||
public bool $is_swarm_worker = false;
|
||||
|
||||
public $selected_swarm_cluster = null;
|
||||
|
||||
public bool $is_build_server = false;
|
||||
|
||||
#[Locked]
|
||||
public Collection $swarm_managers;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->name = generate_random_name();
|
||||
$this->private_key_id = $this->private_keys->first()?->id;
|
||||
$this->swarm_managers = Server::isUsable()->get()->where('settings.is_swarm_manager', true);
|
||||
if ($this->swarm_managers->count() > 0) {
|
||||
$this->selected_swarm_cluster = $this->swarm_managers->first()->id;
|
||||
}
|
||||
}
|
||||
|
||||
protected function rules(): array
|
||||
@@ -72,9 +58,6 @@ class ByIp extends Component
|
||||
'ip' => 'required|string',
|
||||
'user' => 'required|string',
|
||||
'port' => 'required|integer|between:1,65535',
|
||||
'is_swarm_manager' => 'required|boolean',
|
||||
'is_swarm_worker' => 'required|boolean',
|
||||
'selected_swarm_cluster' => 'nullable|integer',
|
||||
'is_build_server' => 'required|boolean',
|
||||
];
|
||||
}
|
||||
@@ -94,11 +77,6 @@ class ByIp extends Component
|
||||
'port.required' => 'The Port field is required.',
|
||||
'port.integer' => 'The Port field must be an integer.',
|
||||
'port.between' => 'The Port field must be between 1 and 65535.',
|
||||
'is_swarm_manager.required' => 'The Swarm Manager field is required.',
|
||||
'is_swarm_manager.boolean' => 'The Swarm Manager field must be true or false.',
|
||||
'is_swarm_worker.required' => 'The Swarm Worker field is required.',
|
||||
'is_swarm_worker.boolean' => 'The Swarm Worker field must be true or false.',
|
||||
'selected_swarm_cluster.integer' => 'The Swarm Cluster field must be an integer.',
|
||||
'is_build_server.required' => 'The Build Server field is required.',
|
||||
'is_build_server.boolean' => 'The Build Server field must be true or false.',
|
||||
]);
|
||||
@@ -140,9 +118,6 @@ class ByIp extends Component
|
||||
'team_id' => currentTeam()->id,
|
||||
'private_key_id' => $this->private_key_id,
|
||||
];
|
||||
if ($this->is_swarm_worker) {
|
||||
$payload['swarm_cluster'] = $this->selected_swarm_cluster;
|
||||
}
|
||||
if ($this->is_build_server) {
|
||||
data_forget($payload, 'proxy');
|
||||
}
|
||||
@@ -150,13 +125,6 @@ class ByIp extends Component
|
||||
$server->proxy->set('status', 'exited');
|
||||
$server->proxy->set('type', ProxyTypes::TRAEFIK->value);
|
||||
$server->save();
|
||||
if ($this->is_build_server) {
|
||||
$this->is_swarm_manager = false;
|
||||
$this->is_swarm_worker = false;
|
||||
} else {
|
||||
$server->settings->is_swarm_manager = $this->is_swarm_manager;
|
||||
$server->settings->is_swarm_worker = $this->is_swarm_worker;
|
||||
}
|
||||
$server->settings->is_build_server = $this->is_build_server;
|
||||
$server->settings->save();
|
||||
|
||||
|
||||
@@ -85,19 +85,8 @@ class Advanced extends Component
|
||||
// Handle allowed IPs with subnet support and 0.0.0.0 special case
|
||||
$this->allowed_ips = str($this->allowed_ips)->replaceEnd(',', '')->trim();
|
||||
|
||||
// Check if user entered 0.0.0.0 or left field empty (both allow access from anywhere)
|
||||
$allowsFromAnywhere = false;
|
||||
if (empty($this->allowed_ips)) {
|
||||
$allowsFromAnywhere = true;
|
||||
} elseif ($this->allowed_ips === '0.0.0.0' || str_contains($this->allowed_ips, '0.0.0.0')) {
|
||||
$allowsFromAnywhere = true;
|
||||
}
|
||||
|
||||
// Check if it's 0.0.0.0 (allow all) or empty
|
||||
if ($this->allowed_ips === '0.0.0.0' || empty($this->allowed_ips)) {
|
||||
// Keep as is - empty means no restriction, 0.0.0.0 means allow all
|
||||
} else {
|
||||
// Validate and clean up the entries
|
||||
// Only validate and clean up if we have IPs and it's not 0.0.0.0 (allow all)
|
||||
if (! empty($this->allowed_ips) && ! in_array('0.0.0.0', array_map('trim', explode(',', $this->allowed_ips)))) {
|
||||
$invalidEntries = [];
|
||||
$validEntries = str($this->allowed_ips)->trim()->explode(',')->map(function ($entry) use (&$invalidEntries) {
|
||||
$entry = str($entry)->trim()->toString();
|
||||
@@ -133,7 +122,6 @@ class Advanced extends Component
|
||||
return;
|
||||
}
|
||||
|
||||
// Also check if we have no valid entries after filtering
|
||||
if ($validEntries->isEmpty()) {
|
||||
$this->dispatch('error', 'No valid IP addresses or subnets provided');
|
||||
|
||||
@@ -144,14 +132,6 @@ class Advanced extends Component
|
||||
}
|
||||
|
||||
$this->instantSave();
|
||||
|
||||
// Show security warning if allowing access from anywhere
|
||||
if ($allowsFromAnywhere) {
|
||||
$message = empty($this->allowed_ips)
|
||||
? 'Empty IP allowlist allows API access from anywhere.<br><br>This is not recommended for production environments!'
|
||||
: 'Using 0.0.0.0 allows API access from anywhere.<br><br>This is not recommended for production environments!';
|
||||
$this->dispatch('warning', $message);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,16 @@ class SettingsOauth extends Component
|
||||
return redirect()->route('home');
|
||||
}
|
||||
$this->oauth_settings_map = OauthSetting::all()->sortBy('provider')->reduce(function ($carry, $setting) {
|
||||
$carry[$setting->provider] = $setting;
|
||||
$carry[$setting->provider] = [
|
||||
'id' => $setting->id,
|
||||
'provider' => $setting->provider,
|
||||
'enabled' => $setting->enabled,
|
||||
'client_id' => $setting->client_id,
|
||||
'client_secret' => $setting->client_secret,
|
||||
'redirect_uri' => $setting->redirect_uri,
|
||||
'tenant' => $setting->tenant,
|
||||
'base_url' => $setting->base_url,
|
||||
];
|
||||
|
||||
return $carry;
|
||||
}, []);
|
||||
@@ -38,16 +47,83 @@ class SettingsOauth extends Component
|
||||
private function updateOauthSettings(?string $provider = null)
|
||||
{
|
||||
if ($provider) {
|
||||
$oauth = $this->oauth_settings_map[$provider];
|
||||
$oauthData = $this->oauth_settings_map[$provider];
|
||||
$oauth = OauthSetting::find($oauthData['id']);
|
||||
|
||||
if (! $oauth) {
|
||||
throw new \Exception('OAuth setting for '.$provider.' not found. It may have been deleted.');
|
||||
}
|
||||
|
||||
$oauth->fill([
|
||||
'enabled' => $oauthData['enabled'],
|
||||
'client_id' => $oauthData['client_id'],
|
||||
'client_secret' => $oauthData['client_secret'],
|
||||
'redirect_uri' => $oauthData['redirect_uri'],
|
||||
'tenant' => $oauthData['tenant'],
|
||||
'base_url' => $oauthData['base_url'],
|
||||
]);
|
||||
|
||||
if (! $oauth->couldBeEnabled()) {
|
||||
$oauth->update(['enabled' => false]);
|
||||
throw new \Exception('OAuth settings are not complete for '.$oauth->provider.'.<br/>Please fill in all required fields.');
|
||||
}
|
||||
$oauth->save();
|
||||
|
||||
// Update the array with fresh data
|
||||
$this->oauth_settings_map[$provider] = [
|
||||
'id' => $oauth->id,
|
||||
'provider' => $oauth->provider,
|
||||
'enabled' => $oauth->enabled,
|
||||
'client_id' => $oauth->client_id,
|
||||
'client_secret' => $oauth->client_secret,
|
||||
'redirect_uri' => $oauth->redirect_uri,
|
||||
'tenant' => $oauth->tenant,
|
||||
'base_url' => $oauth->base_url,
|
||||
];
|
||||
|
||||
$this->dispatch('success', 'OAuth settings for '.$oauth->provider.' updated successfully!');
|
||||
} else {
|
||||
foreach (array_values($this->oauth_settings_map) as &$setting) {
|
||||
$setting->save();
|
||||
$errors = [];
|
||||
foreach (array_values($this->oauth_settings_map) as $settingData) {
|
||||
$oauth = OauthSetting::find($settingData['id']);
|
||||
|
||||
if (! $oauth) {
|
||||
$errors[] = "OAuth setting for provider '{$settingData['provider']}' not found. It may have been deleted.";
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$oauth->fill([
|
||||
'enabled' => $settingData['enabled'],
|
||||
'client_id' => $settingData['client_id'],
|
||||
'client_secret' => $settingData['client_secret'],
|
||||
'redirect_uri' => $settingData['redirect_uri'],
|
||||
'tenant' => $settingData['tenant'],
|
||||
'base_url' => $settingData['base_url'],
|
||||
]);
|
||||
|
||||
if ($settingData['enabled'] && ! $oauth->couldBeEnabled()) {
|
||||
$oauth->enabled = false;
|
||||
$errors[] = "OAuth settings are incomplete for '{$oauth->provider}'. Required fields are missing. The provider has been disabled.";
|
||||
}
|
||||
|
||||
$oauth->save();
|
||||
|
||||
// Update the array with fresh data
|
||||
$this->oauth_settings_map[$oauth->provider] = [
|
||||
'id' => $oauth->id,
|
||||
'provider' => $oauth->provider,
|
||||
'enabled' => $oauth->enabled,
|
||||
'client_id' => $oauth->client_id,
|
||||
'client_secret' => $oauth->client_secret,
|
||||
'redirect_uri' => $oauth->redirect_uri,
|
||||
'tenant' => $oauth->tenant,
|
||||
'base_url' => $oauth->base_url,
|
||||
];
|
||||
}
|
||||
|
||||
if (! empty($errors)) {
|
||||
$this->dispatch('error', implode('<br/>', $errors));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,19 +47,19 @@ class Change extends Component
|
||||
|
||||
public int $customPort;
|
||||
|
||||
public int $appId;
|
||||
public ?int $appId = null;
|
||||
|
||||
public int $installationId;
|
||||
public ?int $installationId = null;
|
||||
|
||||
public string $clientId;
|
||||
public ?string $clientId = null;
|
||||
|
||||
public string $clientSecret;
|
||||
public ?string $clientSecret = null;
|
||||
|
||||
public string $webhookSecret;
|
||||
public ?string $webhookSecret = null;
|
||||
|
||||
public bool $isSystemWide;
|
||||
|
||||
public int $privateKeyId;
|
||||
public ?int $privateKeyId = null;
|
||||
|
||||
public ?string $contents = null;
|
||||
|
||||
@@ -78,16 +78,16 @@ class Change extends Component
|
||||
'htmlUrl' => 'required|string',
|
||||
'customUser' => 'required|string',
|
||||
'customPort' => 'required|int',
|
||||
'appId' => 'required|int',
|
||||
'installationId' => 'required|int',
|
||||
'clientId' => 'required|string',
|
||||
'clientSecret' => 'required|string',
|
||||
'webhookSecret' => 'required|string',
|
||||
'appId' => 'nullable|int',
|
||||
'installationId' => 'nullable|int',
|
||||
'clientId' => 'nullable|string',
|
||||
'clientSecret' => 'nullable|string',
|
||||
'webhookSecret' => 'nullable|string',
|
||||
'isSystemWide' => 'required|bool',
|
||||
'contents' => 'nullable|string',
|
||||
'metadata' => 'nullable|string',
|
||||
'pullRequests' => 'nullable|string',
|
||||
'privateKeyId' => 'required|int',
|
||||
'privateKeyId' => 'nullable|int',
|
||||
];
|
||||
|
||||
public function boot()
|
||||
@@ -148,47 +148,48 @@ class Change extends Component
|
||||
try {
|
||||
$this->authorize('view', $this->github_app);
|
||||
|
||||
// Validate required fields before attempting to fetch permissions
|
||||
$missingFields = [];
|
||||
|
||||
if (! $this->github_app->app_id) {
|
||||
$missingFields[] = 'App ID';
|
||||
}
|
||||
|
||||
if (! $this->github_app->private_key_id) {
|
||||
$missingFields[] = 'Private Key';
|
||||
}
|
||||
|
||||
if (! empty($missingFields)) {
|
||||
$fieldsList = implode(', ', $missingFields);
|
||||
$this->dispatch('error', "Cannot fetch permissions. Please set the following required fields first: {$fieldsList}");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify the private key exists and is accessible
|
||||
if (! $this->github_app->privateKey) {
|
||||
$this->dispatch('error', 'Private Key not found. Please select a valid private key.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
GithubAppPermissionJob::dispatchSync($this->github_app);
|
||||
$this->github_app->refresh()->makeVisible('client_secret')->makeVisible('webhook_secret');
|
||||
$this->dispatch('success', 'Github App permissions updated.');
|
||||
} catch (\Throwable $e) {
|
||||
// Provide better error message for unsupported key formats
|
||||
$errorMessage = $e->getMessage();
|
||||
if (str_contains($errorMessage, 'DECODER routines::unsupported') ||
|
||||
str_contains($errorMessage, 'parse your key')) {
|
||||
$this->dispatch('error', 'The selected private key format is not supported for GitHub Apps. <br><br>Please use an RSA private key in PEM format (BEGIN RSA PRIVATE KEY). <br><br>OpenSSH format keys (BEGIN OPENSSH PRIVATE KEY) are not supported.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
// public function check()
|
||||
// {
|
||||
|
||||
// Need administration:read:write permission
|
||||
// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#list-self-hosted-runners-for-a-repository
|
||||
|
||||
// $github_access_token = generateGithubInstallationToken($this->github_app);
|
||||
// $repositories = Http::withToken($github_access_token)->get("{$this->github_app->api_url}/installation/repositories?per_page=100");
|
||||
// $runners_by_repository = collect([]);
|
||||
// $repositories = $repositories->json()['repositories'];
|
||||
// foreach ($repositories as $repository) {
|
||||
// $runners_downloads = Http::withToken($github_access_token)->get("{$this->github_app->api_url}/repos/{$repository['full_name']}/actions/runners/downloads");
|
||||
// $runners = Http::withToken($github_access_token)->get("{$this->github_app->api_url}/repos/{$repository['full_name']}/actions/runners");
|
||||
// $token = Http::withHeaders([
|
||||
// 'Authorization' => "Bearer $github_access_token",
|
||||
// 'Accept' => 'application/vnd.github+json'
|
||||
// ])->withBody(null)->post("{$this->github_app->api_url}/repos/{$repository['full_name']}/actions/runners/registration-token");
|
||||
// $token = $token->json();
|
||||
// $remove_token = Http::withHeaders([
|
||||
// 'Authorization' => "Bearer $github_access_token",
|
||||
// 'Accept' => 'application/vnd.github+json'
|
||||
// ])->withBody(null)->post("{$this->github_app->api_url}/repos/{$repository['full_name']}/actions/runners/remove-token");
|
||||
// $remove_token = $remove_token->json();
|
||||
// $runners_by_repository->put($repository['full_name'], [
|
||||
// 'token' => $token,
|
||||
// 'remove_token' => $remove_token,
|
||||
// 'runners' => $runners->json(),
|
||||
// 'runners_downloads' => $runners_downloads->json()
|
||||
// ]);
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
public function mount()
|
||||
{
|
||||
try {
|
||||
@@ -340,10 +341,13 @@ class Change extends Component
|
||||
$this->authorize('update', $this->github_app);
|
||||
|
||||
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
|
||||
$this->github_app->app_id = '1234567890';
|
||||
$this->github_app->installation_id = '1234567890';
|
||||
$this->github_app->app_id = 1234567890;
|
||||
$this->github_app->installation_id = 1234567890;
|
||||
$this->github_app->save();
|
||||
$this->dispatch('success', 'Github App updated.');
|
||||
|
||||
// Redirect to avoid Livewire morphing issues when view structure changes
|
||||
return redirect()->route('source.github.show', ['github_app_uuid' => $this->github_app->uuid])
|
||||
->with('success', 'Github App updated. You can now configure the details.');
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
|
||||
@@ -50,11 +50,9 @@ class Create extends Component
|
||||
'html_url' => $this->html_url,
|
||||
'custom_user' => $this->custom_user,
|
||||
'custom_port' => $this->custom_port,
|
||||
'is_system_wide' => $this->is_system_wide,
|
||||
'team_id' => currentTeam()->id,
|
||||
];
|
||||
if (isCloud()) {
|
||||
$payload['is_system_wide'] = $this->is_system_wide;
|
||||
}
|
||||
$github_app = GithubApp::create($payload);
|
||||
if (session('from')) {
|
||||
session(['from' => session('from') + ['source_id' => $github_app->id]]);
|
||||
|
||||
@@ -1804,7 +1804,22 @@ class Application extends BaseModel
|
||||
public function parseHealthcheckFromDockerfile($dockerfile, bool $isInit = false)
|
||||
{
|
||||
$dockerfile = str($dockerfile)->trim()->explode("\n");
|
||||
if (str($dockerfile)->contains('HEALTHCHECK') && ($this->isHealthcheckDisabled() || $isInit)) {
|
||||
$hasHealthcheck = str($dockerfile)->contains('HEALTHCHECK');
|
||||
|
||||
// Always check if healthcheck was removed, regardless of health_check_enabled setting
|
||||
if (! $hasHealthcheck && $this->custom_healthcheck_found) {
|
||||
// HEALTHCHECK was removed from Dockerfile, reset to defaults
|
||||
$this->custom_healthcheck_found = false;
|
||||
$this->health_check_interval = 5;
|
||||
$this->health_check_timeout = 5;
|
||||
$this->health_check_retries = 10;
|
||||
$this->health_check_start_period = 5;
|
||||
$this->save();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($hasHealthcheck && ($this->isHealthcheckDisabled() || $isInit)) {
|
||||
$healthcheckCommand = null;
|
||||
$lines = $dockerfile->toArray();
|
||||
foreach ($lines as $line) {
|
||||
|
||||
@@ -12,6 +12,7 @@ class GithubApp extends BaseModel
|
||||
|
||||
protected $casts = [
|
||||
'is_public' => 'boolean',
|
||||
'is_system_wide' => 'boolean',
|
||||
'type' => 'string',
|
||||
];
|
||||
|
||||
|
||||
@@ -88,6 +88,16 @@ class PrivateKey extends BaseModel
|
||||
return self::whereTeamId($teamId)->select($selectArray->all());
|
||||
}
|
||||
|
||||
public static function ownedAndOnlySShKeys(array $select = ['*'])
|
||||
{
|
||||
$teamId = currentTeam()->id;
|
||||
$selectArray = collect($select)->concat(['id']);
|
||||
|
||||
return self::whereTeamId($teamId)
|
||||
->where('is_git_related', false)
|
||||
->select($selectArray->all());
|
||||
}
|
||||
|
||||
public static function validatePrivateKey($privateKey)
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -243,10 +243,14 @@ class StandaloneClickhouse extends BaseModel
|
||||
return new Attribute(
|
||||
get: function () {
|
||||
if ($this->is_public && $this->public_port) {
|
||||
$serverIp = $this->destination->server->getIp;
|
||||
if (empty($serverIp)) {
|
||||
return null;
|
||||
}
|
||||
$encodedUser = rawurlencode($this->clickhouse_admin_user);
|
||||
$encodedPass = rawurlencode($this->clickhouse_admin_password);
|
||||
|
||||
return "clickhouse://{$encodedUser}:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/{$this->clickhouse_db}";
|
||||
return "clickhouse://{$encodedUser}:{$encodedPass}@{$serverIp}:{$this->public_port}/{$this->clickhouse_db}";
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -249,9 +249,13 @@ class StandaloneDragonfly extends BaseModel
|
||||
return new Attribute(
|
||||
get: function () {
|
||||
if ($this->is_public && $this->public_port) {
|
||||
$serverIp = $this->destination->server->getIp;
|
||||
if (empty($serverIp)) {
|
||||
return null;
|
||||
}
|
||||
$scheme = $this->enable_ssl ? 'rediss' : 'redis';
|
||||
$encodedPass = rawurlencode($this->dragonfly_password);
|
||||
$url = "{$scheme}://:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/0";
|
||||
$url = "{$scheme}://:{$encodedPass}@{$serverIp}:{$this->public_port}/0";
|
||||
|
||||
if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
|
||||
$url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
|
||||
|
||||
@@ -249,9 +249,13 @@ class StandaloneKeydb extends BaseModel
|
||||
return new Attribute(
|
||||
get: function () {
|
||||
if ($this->is_public && $this->public_port) {
|
||||
$serverIp = $this->destination->server->getIp;
|
||||
if (empty($serverIp)) {
|
||||
return null;
|
||||
}
|
||||
$scheme = $this->enable_ssl ? 'rediss' : 'redis';
|
||||
$encodedPass = rawurlencode($this->keydb_password);
|
||||
$url = "{$scheme}://:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/0";
|
||||
$url = "{$scheme}://:{$encodedPass}@{$serverIp}:{$this->public_port}/0";
|
||||
|
||||
if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
|
||||
$url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
|
||||
|
||||
@@ -239,10 +239,14 @@ class StandaloneMariadb extends BaseModel
|
||||
return new Attribute(
|
||||
get: function () {
|
||||
if ($this->is_public && $this->public_port) {
|
||||
$serverIp = $this->destination->server->getIp;
|
||||
if (empty($serverIp)) {
|
||||
return null;
|
||||
}
|
||||
$encodedUser = rawurlencode($this->mariadb_user);
|
||||
$encodedPass = rawurlencode($this->mariadb_password);
|
||||
|
||||
return "mysql://{$encodedUser}:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}";
|
||||
return "mysql://{$encodedUser}:{$encodedPass}@{$serverIp}:{$this->public_port}/{$this->mariadb_database}";
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -269,9 +269,13 @@ class StandaloneMongodb extends BaseModel
|
||||
return new Attribute(
|
||||
get: function () {
|
||||
if ($this->is_public && $this->public_port) {
|
||||
$serverIp = $this->destination->server->getIp;
|
||||
if (empty($serverIp)) {
|
||||
return null;
|
||||
}
|
||||
$encodedUser = rawurlencode($this->mongo_initdb_root_username);
|
||||
$encodedPass = rawurlencode($this->mongo_initdb_root_password);
|
||||
$url = "mongodb://{$encodedUser}:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true";
|
||||
$url = "mongodb://{$encodedUser}:{$encodedPass}@{$serverIp}:{$this->public_port}/?directConnection=true";
|
||||
if ($this->enable_ssl) {
|
||||
$url .= '&tls=true&tlsCAFile=/etc/mongo/certs/ca.pem';
|
||||
if (in_array($this->ssl_mode, ['verify-full'])) {
|
||||
|
||||
@@ -251,9 +251,13 @@ class StandaloneMysql extends BaseModel
|
||||
return new Attribute(
|
||||
get: function () {
|
||||
if ($this->is_public && $this->public_port) {
|
||||
$serverIp = $this->destination->server->getIp;
|
||||
if (empty($serverIp)) {
|
||||
return null;
|
||||
}
|
||||
$encodedUser = rawurlencode($this->mysql_user);
|
||||
$encodedPass = rawurlencode($this->mysql_password);
|
||||
$url = "mysql://{$encodedUser}:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mysql_database}";
|
||||
$url = "mysql://{$encodedUser}:{$encodedPass}@{$serverIp}:{$this->public_port}/{$this->mysql_database}";
|
||||
if ($this->enable_ssl) {
|
||||
$url .= "?ssl-mode={$this->ssl_mode}";
|
||||
if (in_array($this->ssl_mode, ['VERIFY_CA', 'VERIFY_IDENTITY'])) {
|
||||
|
||||
@@ -246,9 +246,13 @@ class StandalonePostgresql extends BaseModel
|
||||
return new Attribute(
|
||||
get: function () {
|
||||
if ($this->is_public && $this->public_port) {
|
||||
$serverIp = $this->destination->server->getIp;
|
||||
if (empty($serverIp)) {
|
||||
return null;
|
||||
}
|
||||
$encodedUser = rawurlencode($this->postgres_user);
|
||||
$encodedPass = rawurlencode($this->postgres_password);
|
||||
$url = "postgres://{$encodedUser}:{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}";
|
||||
$url = "postgres://{$encodedUser}:{$encodedPass}@{$serverIp}:{$this->public_port}/{$this->postgres_db}";
|
||||
if ($this->enable_ssl) {
|
||||
$url .= "?sslmode={$this->ssl_mode}";
|
||||
if (in_array($this->ssl_mode, ['verify-ca', 'verify-full'])) {
|
||||
|
||||
@@ -253,11 +253,15 @@ class StandaloneRedis extends BaseModel
|
||||
return new Attribute(
|
||||
get: function () {
|
||||
if ($this->is_public && $this->public_port) {
|
||||
$serverIp = $this->destination->server->getIp;
|
||||
if (empty($serverIp)) {
|
||||
return null;
|
||||
}
|
||||
$redis_version = $this->getRedisVersion();
|
||||
$username_part = version_compare($redis_version, '6.0', '>=') ? rawurlencode($this->redis_username).':' : '';
|
||||
$encodedPass = rawurlencode($this->redis_password);
|
||||
$scheme = $this->enable_ssl ? 'rediss' : 'redis';
|
||||
$url = "{$scheme}://{$username_part}{$encodedPass}@{$this->destination->server->getIp}:{$this->public_port}/0";
|
||||
$url = "{$scheme}://{$username_part}{$encodedPass}@{$serverIp}:{$this->public_port}/0";
|
||||
|
||||
if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
|
||||
$url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
|
||||
|
||||
@@ -16,10 +16,7 @@ class ServerPatchCheck extends CustomEmailNotification
|
||||
public function __construct(public Server $server, public array $patchData)
|
||||
{
|
||||
$this->onQueue('high');
|
||||
$this->serverUrl = route('server.security.patches', ['server_uuid' => $this->server->uuid]);
|
||||
if (isDev()) {
|
||||
$this->serverUrl = 'https://staging-but-dev.coolify.io/server/'.$this->server->uuid.'/security/patches';
|
||||
}
|
||||
$this->serverUrl = base_url().'/server/'.$this->server->uuid.'/security/patches';
|
||||
}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
|
||||
@@ -127,13 +127,19 @@ class FortifyServiceProvider extends ServiceProvider
|
||||
});
|
||||
|
||||
RateLimiter::for('forgot-password', function (Request $request) {
|
||||
return Limit::perMinute(5)->by($request->ip());
|
||||
// Use real client IP (not spoofable forwarded headers)
|
||||
$realIp = $request->server('REMOTE_ADDR') ?? $request->ip();
|
||||
|
||||
return Limit::perMinute(5)->by($realIp);
|
||||
});
|
||||
|
||||
RateLimiter::for('login', function (Request $request) {
|
||||
$email = (string) $request->email;
|
||||
// Use email + real client IP (not spoofable forwarded headers)
|
||||
// server('REMOTE_ADDR') gives the actual connecting IP before proxy headers
|
||||
$realIp = $request->server('REMOTE_ADDR') ?? $request->ip();
|
||||
|
||||
return Limit::perMinute(5)->by($email.$request->ip());
|
||||
return Limit::perMinute(5)->by($email.'|'.$realIp);
|
||||
});
|
||||
|
||||
RateLimiter::for('two-factor', function (Request $request) {
|
||||
|
||||
@@ -88,7 +88,14 @@ class HetznerService
|
||||
|
||||
public function getServerTypes(): array
|
||||
{
|
||||
return $this->requestPaginated('get', '/server_types', 'server_types');
|
||||
$types = $this->requestPaginated('get', '/server_types', 'server_types');
|
||||
|
||||
// Filter out entries where "deprecated" is explicitly true
|
||||
$filtered = array_filter($types, function ($type) {
|
||||
return ! (isset($type['deprecated']) && $type['deprecated'] === true);
|
||||
});
|
||||
|
||||
return array_values($filtered);
|
||||
}
|
||||
|
||||
public function getSshKeys(): array
|
||||
|
||||
@@ -51,6 +51,8 @@ const DATABASE_DOCKER_IMAGES = [
|
||||
const SPECIFIC_SERVICES = [
|
||||
'quay.io/minio/minio',
|
||||
'minio/minio',
|
||||
'ghcr.io/coollabsio/minio',
|
||||
'coollabsio/minio',
|
||||
'svhd/logto',
|
||||
];
|
||||
|
||||
|
||||
@@ -41,7 +41,13 @@ function create_standalone_redis($environment_id, $destination_uuid, ?array $oth
|
||||
$database = new StandaloneRedis;
|
||||
$database->uuid = (new Cuid2);
|
||||
$database->name = 'redis-database-'.$database->uuid;
|
||||
|
||||
$redis_password = \Illuminate\Support\Str::password(length: 64, symbols: false);
|
||||
if ($otherData && isset($otherData['redis_password'])) {
|
||||
$redis_password = $otherData['redis_password'];
|
||||
unset($otherData['redis_password']);
|
||||
}
|
||||
|
||||
$database->environment_id = $environment_id;
|
||||
$database->destination_id = $destination->id;
|
||||
$database->destination_type = $destination->getMorphClass();
|
||||
|
||||
@@ -358,6 +358,8 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
||||
{
|
||||
$uuid = data_get($resource, 'uuid');
|
||||
$compose = data_get($resource, 'docker_compose_raw');
|
||||
// Store original compose for later use to update docker_compose_raw with content removed
|
||||
$originalCompose = $compose;
|
||||
if (! $compose) {
|
||||
return collect([]);
|
||||
}
|
||||
@@ -1297,7 +1299,34 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
||||
return array_search($key, $customOrder);
|
||||
});
|
||||
|
||||
$resource->docker_compose = Yaml::dump(convertToArray($topLevel), 10, 2);
|
||||
$cleanedCompose = Yaml::dump(convertToArray($topLevel), 10, 2);
|
||||
$resource->docker_compose = $cleanedCompose;
|
||||
|
||||
// Update docker_compose_raw to remove content: from volumes only
|
||||
// This keeps the original user input clean while preventing content reapplication
|
||||
// Parse the original compose again to create a clean version without Coolify additions
|
||||
try {
|
||||
$originalYaml = Yaml::parse($originalCompose);
|
||||
// Remove content, isDirectory, and is_directory from all volume definitions
|
||||
if (isset($originalYaml['services'])) {
|
||||
foreach ($originalYaml['services'] as $serviceName => &$service) {
|
||||
if (isset($service['volumes'])) {
|
||||
foreach ($service['volumes'] as $key => &$volume) {
|
||||
if (is_array($volume)) {
|
||||
unset($volume['content']);
|
||||
unset($volume['isDirectory']);
|
||||
unset($volume['is_directory']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$resource->docker_compose_raw = Yaml::dump($originalYaml, 10, 2);
|
||||
} catch (\Exception $e) {
|
||||
// If parsing fails, keep the original docker_compose_raw unchanged
|
||||
ray('Failed to update docker_compose_raw in applicationParser: '.$e->getMessage());
|
||||
}
|
||||
|
||||
data_forget($resource, 'environment_variables');
|
||||
data_forget($resource, 'environment_variables_preview');
|
||||
$resource->save();
|
||||
@@ -1309,6 +1338,8 @@ function serviceParser(Service $resource): Collection
|
||||
{
|
||||
$uuid = data_get($resource, 'uuid');
|
||||
$compose = data_get($resource, 'docker_compose_raw');
|
||||
// Store original compose for later use to update docker_compose_raw with content removed
|
||||
$originalCompose = $compose;
|
||||
if (! $compose) {
|
||||
return collect([]);
|
||||
}
|
||||
@@ -2220,7 +2251,34 @@ function serviceParser(Service $resource): Collection
|
||||
return array_search($key, $customOrder);
|
||||
});
|
||||
|
||||
$resource->docker_compose = Yaml::dump(convertToArray($topLevel), 10, 2);
|
||||
$cleanedCompose = Yaml::dump(convertToArray($topLevel), 10, 2);
|
||||
$resource->docker_compose = $cleanedCompose;
|
||||
|
||||
// Update docker_compose_raw to remove content: from volumes only
|
||||
// This keeps the original user input clean while preventing content reapplication
|
||||
// Parse the original compose again to create a clean version without Coolify additions
|
||||
try {
|
||||
$originalYaml = Yaml::parse($originalCompose);
|
||||
// Remove content, isDirectory, and is_directory from all volume definitions
|
||||
if (isset($originalYaml['services'])) {
|
||||
foreach ($originalYaml['services'] as $serviceName => &$service) {
|
||||
if (isset($service['volumes'])) {
|
||||
foreach ($service['volumes'] as $key => &$volume) {
|
||||
if (is_array($volume)) {
|
||||
unset($volume['content']);
|
||||
unset($volume['isDirectory']);
|
||||
unset($volume['is_directory']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$resource->docker_compose_raw = Yaml::dump($originalYaml, 10, 2);
|
||||
} catch (\Exception $e) {
|
||||
// If parsing fails, keep the original docker_compose_raw unchanged
|
||||
ray('Failed to update docker_compose_raw in serviceParser: '.$e->getMessage());
|
||||
}
|
||||
|
||||
data_forget($resource, 'environment_variables');
|
||||
data_forget($resource, 'environment_variables_preview');
|
||||
$resource->save();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
return [
|
||||
'coolify' => [
|
||||
'version' => '4.0.0-beta.437',
|
||||
'version' => '4.0.0-beta.438',
|
||||
'helper_version' => '1.0.11',
|
||||
'realtime_version' => '1.0.10',
|
||||
'self_hosted' => env('SELF_HOSTED', true),
|
||||
|
||||
@@ -104,7 +104,7 @@ services:
|
||||
networks:
|
||||
- coolify
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
image: ghcr.io/coollabsio/minio:RELEASE.2025-10-15T17-29-55Z # Released on 15 October 2025
|
||||
pull_policy: always
|
||||
container_name: coolify-minio
|
||||
command: server /data --console-address ":9001"
|
||||
|
||||
@@ -27,7 +27,8 @@ RUN docker-php-serversideup-set-id www-data $USER_ID:$GROUP_ID && \
|
||||
|
||||
WORKDIR /var/www/html
|
||||
COPY --chown=www-data:www-data composer.json composer.lock ./
|
||||
RUN composer install --no-dev --no-interaction --no-plugins --no-scripts --prefer-dist
|
||||
RUN --mount=type=cache,target=/tmp/cache \
|
||||
COMPOSER_CACHE_DIR=/tmp/cache composer install --no-dev --no-interaction --no-plugins --no-scripts --prefer-dist
|
||||
|
||||
USER www-data
|
||||
|
||||
@@ -38,7 +39,8 @@ FROM node:24-alpine AS static-assets
|
||||
|
||||
WORKDIR /app
|
||||
COPY package*.json vite.config.js postcss.config.cjs ./
|
||||
RUN npm ci
|
||||
RUN --mount=type=cache,target=/root/.npm \
|
||||
npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
@@ -72,8 +74,9 @@ RUN apk add --no-cache gnupg && \
|
||||
curl -fSsL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor > /usr/share/keyrings/postgresql.gpg
|
||||
|
||||
# Install system dependencies
|
||||
RUN apk upgrade
|
||||
RUN apk add --no-cache \
|
||||
RUN --mount=type=cache,target=/var/cache/apk \
|
||||
apk upgrade && \
|
||||
apk add --no-cache \
|
||||
postgresql${POSTGRES_VERSION}-client \
|
||||
openssh-client \
|
||||
git \
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"pusher-js": "8.4.0",
|
||||
"tailwind-scrollbar": "4.0.2",
|
||||
"tailwindcss": "4.1.10",
|
||||
"vite": "6.3.6",
|
||||
"vite": "6.4.1",
|
||||
"vue": "3.5.16"
|
||||
}
|
||||
},
|
||||
@@ -1158,6 +1158,66 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
|
||||
"version": "1.4.3",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/wasi-threads": "1.0.2",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||
"version": "1.4.3",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
||||
"version": "1.0.2",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "0.2.10",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/core": "^1.4.3",
|
||||
"@emnapi/runtime": "^1.4.3",
|
||||
"@tybys/wasm-util": "^0.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
|
||||
"version": "0.9.0",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
|
||||
"version": "2.8.0",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.10.tgz",
|
||||
@@ -2651,9 +2711,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.3.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
|
||||
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"pusher-js": "8.4.0",
|
||||
"tailwind-scrollbar": "4.0.2",
|
||||
"tailwindcss": "4.1.10",
|
||||
"vite": "6.3.6",
|
||||
"vite": "6.4.1",
|
||||
"vue": "3.5.16"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg width="240" height="240" viewBox="0 0 240 240" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M240 224.762C240 233.012 233.25 239.762 225 239.762H15C6.75 239.762 0 233.012 0 224.762V134.762C0 126.512 4.77 114.993 10.61 109.153L109.39 10.3725C115.22 4.5425 124.77 4.5425 130.6 10.3725L229.39 109.162C235.22 114.992 240 126.522 240 134.772V224.772V224.762Z" fill="#F2F4F9"/>
|
||||
<path d="M229.39 109.153L130.61 10.3725C124.78 4.5425 115.23 4.5425 109.4 10.3725L10.61 109.153C4.78 114.983 0 126.512 0 134.762V224.762C0 233.012 6.75 239.762 15 239.762H107.27L66.64 199.132C64.55 199.852 62.32 200.262 60 200.262C48.7 200.262 39.5 191.062 39.5 179.762C39.5 168.462 48.7 159.262 60 159.262C71.3 159.262 80.5 168.462 80.5 179.762C80.5 182.092 80.09 184.322 79.37 186.412L111 218.042V102.162C104.2 98.8225 99.5 91.8425 99.5 83.7725C99.5 72.4725 108.7 63.2725 120 63.2725C131.3 63.2725 140.5 72.4725 140.5 83.7725C140.5 91.8425 135.8 98.8225 129 102.162V183.432L160.46 151.972C159.84 150.012 159.5 147.932 159.5 145.772C159.5 134.472 168.7 125.272 180 125.272C191.3 125.272 200.5 134.472 200.5 145.772C200.5 157.072 191.3 166.272 180 166.272C177.5 166.272 175.12 165.802 172.91 164.982L129 208.892V239.772H225C233.25 239.772 240 233.022 240 224.772V134.772C240 126.522 235.23 115.002 229.39 109.162V109.153Z" fill="#18BCF2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1 @@
|
||||
<svg width="128" height="128" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="126" height="126" rx="44" fill="#0F0F0F"/><rect x="18.25" y="18.25" width="91.5" height="91.5" rx="25.75" stroke="#F0F0F0" stroke-width="8.5"/><path fill-rule="evenodd" clip-rule="evenodd" d="M57.694 43.098c0-.622-.505-1.126-1.127-1.126h-8.444a5.114 5.114 0 0 0-5.112 5.111v33.824a5.114 5.114 0 0 0 5.112 5.112h8.444c.622 0 1.127-.505 1.127-1.127V43.098Zm24.424 27.869c-1.238-2.222-4.047-4.026-6.27-4.026H62.923c-.684 0-.93.555-.549 1.239l7.703 13.822c1.239 2.223 4.048 4.026 6.27 4.026h12.927c.683 0 .93-.555.548-1.239l-7.703-13.822Zm.538-18.718c0-5.672-4.605-10.277-10.277-10.277H63.31a1.21 1.21 0 0 0-1.209 1.209v18.137c0 .667.542 1.209 1.21 1.209h9.068c5.672 0 10.277-4.605 10.277-10.278Z" fill="#F0F0F0"/></svg>
|
||||
|
After Width: | Height: | Size: 819 B |
@@ -0,0 +1,10 @@
|
||||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
|
||||
<title></title>
|
||||
<g id="icomoon-ignore">
|
||||
</g>
|
||||
<path fill="#d23f31" d="M37.052 371.676l269.857-269.857v550.507l-269.857 269.857z"></path>
|
||||
<path fill="#3b3e43" d="M306.909 101.818l205.091 205.091v550.507l-205.091-205.091z"></path>
|
||||
<path fill="#d23f31" d="M512 306.909l205.091-205.091v550.507l-205.091 205.091z"></path>
|
||||
<path fill="#3b3e43" d="M717.091 101.818l269.857 269.857v550.507l-269.857-269.857z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 554 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="866" height="1154"><path fill="#E33035" d="M475.273 60.7c3.331.367 5.442 1.465 8.352 3.112l2.852 1.583c2.203 1.402 3.706 2.773 5.523 4.605 1.802.977 1.802.977 3.688 1.688L499 73v2h2v2l1.875.438C505 78 505 78 507 79v2h2v2l1.875.75C513 85 513 85 513.938 87c.91 2.237.91 2.237 3.437 2.938 3.362 1.36 4.476 3.174 6.625 6.062 2.534 2.598 5.125 5.12 7.75 7.625C534 106 534 106 535 109h2v2h2c1.508 1.426 1.508 1.426 3.125 3.313l1.633 1.863C545 118 545 118 545 121h2c1.026 1.65 2.023 3.32 3 5l1.5 1.5c1.942 1.942 3.115 4.134 4.5 6.5l1.688 2.125C559 138 559 138 559 141h2c2.84 3.675 4 5.269 4 10h2c2.707 4.575 4.914 9.101 7 14l1.5 2.375c1.743 3.05 2.696 6.086 3.738 9.43.766 2.406.766 2.406 2.45 5.257 1.74 3.894 1.531 6.703 1.312 10.938h2l.621 2.152.817 2.786.808 2.777c.544 2.225.544 2.225 1.754 3.285.325 3.361.514 6.723.719 10.094-.04 2.797-.04 2.797 1.281 3.906.32 2.057.512 4.116.719 6.188.101 1.829.101 1.829 1.281 2.812.212 2.463.376 4.908.5 7.375.253 5.389.253 5.389 1.5 10.625.095 2.332.13 4.666.125 7l.008 3.75C595 259 595 259 594 262c-.307 3.385-.509 6.771-.719 10.164C593 275 593 275 592 276l-.5 3.5c-.5 3.5-.5 3.5-1.5 4.5a123 123 0 0 0-.312 4.438c-.515 5.725-2.396 10.342-4.688 15.562a269 269 0 0 0-1.562 4.125l-.723 1.945L582 312l-1.5 4-.75 2c-2.25 6-2.25 6-2.992 7.973a604 604 0 0 0-1.578 4.273c-2.074 5.648-2.074 5.648-3.18 6.754a92 92 0 0 0-.5 4.5c-.389 4.389-.389 4.389-1.5 5.5a74 74 0 0 0-.5 4c-.389 3.889-.389 3.889-1.5 5-.283 4.013-.232 8.04-.25 12.063l-.062 3.396c-.032 6.667.682 11.436 3.312 17.541.322 2.78.15 5.186 0 8h2l7 13h2l.113-1.906c.387-5.035 1.114-9.278 3.055-13.934 1.644-4.268 2.686-8.737 3.832-13.16h2l.313-2.062c1.49-6.367 4.739-12.143 7.687-17.938h2l.313-1.625c.868-3 2.243-5.613 3.687-8.375h2l2-6h2l2-6h2v-2h2l.438-1.875C618 333 618 333 619 331h2v-2h2l.438-1.875C624 325 624 325 625 323h2v-2h2l.75-2.2c1.374-3.078 2.997-5.115 5.25-7.613l2.125-2.394C639 307 639 307 641 307l.75-1.875c1.487-2.529 2.687-2.843 5.25-4.125l1-2c1.245-2.49 2.467-2.987 5-4v-2c4.353-4 4.353-4 8-4v-2h2v-2c3.673-2.449 5.664-2.315 10-2l2 2a110 110 0 0 1-.694 4.675c-.692 5.252-.491 10.6-.51 15.887l-.016 3.956q-.015 4.13-.02 8.26c-.006 3.514-.03 7.027-.058 10.54-.023 3.374-.025 6.746-.03 10.12l-.038 3.8c.017 6.416.314 11.09 3.366 16.762.503 1.947.964 3.906 1.375 5.875 1.16 4.901 2.843 8.694 5.438 12.98 1.576 2.847 2.421 5.77 3.363 8.872 1.213 3.345 2.992 6.294 4.836 9.328C693 398 693 398 693 401h2l.734 1.574c1.627 3.385 3.156 6.289 5.516 9.238C703 414 703 414 703 417h2a98 98 0 0 1 3 5 213 213 0 0 0 6.75 9.75C717 435 717 435 717 437h2c2 3 2 3 2 6h2c1.55 1.941 1.55 1.941 3.313 4.563 2.15 3.176 4.32 6.307 6.625 9.375C735 460 735 460 735 463h2v2h2l2 6h2c2 3 2 3 2 6h2l2 6h2l2 6h2c4 6.625 4 6.625 4 10h2l1.285 2.422c2.953 5.53 5.939 11.007 9.153 16.39 2.82 4.786 5.391 9.66 7.875 14.625l1.023 2.045C785 543.915 785 543.915 785 549h2l.55 2.012c1.362 4.781 2.76 8.776 5.45 12.988 1.255 3.206 2.002 6.016 2.438 9.438.619 3.92 1.948 6.946 3.562 10.562 1.288 3.865 2.088 7.46 2.438 11.5.295 3.177.765 4.973 2.062 8 1.589 3.708 2.006 6.702 2.29 10.68.021 1.838.021 1.838 1.21 2.82.27 3.64.446 7.272.605 10.918-.178 3.857-.178 3.857 1.395 7.082.27 3.21.446 6.412.605 9.629-.108 3.48-.108 3.48 1.395 6.371.099 1.683.128 3.37.13 5.055l.009 3.266-.006 3.554.003 3.62q0 3.8-.006 7.597c-.005 3.891 0 7.782.007 11.674q0 3.68-.004 7.359l.006 3.554-.01 3.266-.002 2.891C811 705 811 705 810 706a157 157 0 0 0-.5 6c-.389 5.889-.389 5.889-1.5 7-.21 2.67-.376 5.325-.5 8-.365 7.865-.365 7.865-1.5 9a200 200 0 0 0-.523 5.602c-.539 2.709-1.614 5.013-2.737 7.524-1.2 3.04-1.683 6.164-2.24 9.374-.913 5.262-2.355 9.619-4.5 14.5-.816 3.03-1.487 6.068-2.156 9.133-.888 3.016-2.21 5.198-3.844 7.867-.836 2.513-1.493 5.035-2.156 7.598-.913 2.597-2.185 4.226-3.844 6.402-.91 2.207-.91 2.207-1.562 4.5-.912 3.009-1.965 5.719-3.438 8.5h-2l-.25 2.125c-.924 3.542-2.722 5.833-4.75 8.875q-.767 1.951-1.437 3.938c-1.266 3.631-3.038 6.34-5.356 9.386-1.73 2.402-2.965 4.997-4.207 7.676h-2l-.312 1.688c-.842 2.829-2.082 4.82-3.688 7.312l-1.46 2.32-1.602 2.492-1.536 2.395L749 867h-2l-.875 2.313C745 872 745 872 743 875h-2l-.875 2.313C739 880 739 880 737 883h-2v2h-2l-2 4h-2l-.687 2.188c-1.582 3.389-3.612 5.237-6.313 7.812a197 197 0 0 0-2.687 3.313c-.764.886-1.527 1.773-2.313 2.687h-2v2c-1.426 1.508-1.426 1.508-3.312 3.125l-1.864 1.633C708 913 708 913 705 913v2h-2l-.812 1.938C701 919 701 919 698 920a636 636 0 0 0-3.414 3.535C690.008 927.763 684.32 930.808 679 934l-2.062 1.688C675 937 675 937 671 937v2c-8.87 6-8.87 6-14 6v2a3625 3625 0 0 1-6.687 3.375l-1.897.96c-2.718 1.367-5.39 2.655-8.248 3.704-2.498.876-2.498.876-4.168 3.961a221 221 0 0 0-2 7l-1 3h-2l-.187 1.688c-1.918 5.457-7.612 8.774-12.188 11.937-1.08.755-2.16 1.51-3.273 2.29-3.416 2.125-6.634 3.578-10.352 5.085l-3.187 1.688C599 993 599 993 595 993v2c-4.181 2.205-8.172 4.095-12.664 5.573l-2.46.812L578 1002l2.537.031q11.81.146 23.618.308 6.072.084 12.144.158 5.855.072 11.71.156 2.238.031 4.477.056 3.126.037 6.2Line truncated
|
||||
|
After Width: | Height: | Size: 313 KiB |
@@ -14,14 +14,156 @@
|
||||
@if ($multiple)
|
||||
{{-- Multiple Selection Mode with Alpine.js --}}
|
||||
<div x-data="{
|
||||
open: false,
|
||||
search: '',
|
||||
selected: @entangle($modelBinding).live,
|
||||
options: [],
|
||||
filteredOptions: [],
|
||||
|
||||
init() {
|
||||
this.options = Array.from(this.$refs.datalist.querySelectorAll('option')).map(opt => {
|
||||
// Try to parse as integer, fallback to string
|
||||
let value = opt.value;
|
||||
const intValue = parseInt(value, 10);
|
||||
if (!isNaN(intValue) && intValue.toString() === value) {
|
||||
value = intValue;
|
||||
}
|
||||
return {
|
||||
value: value,
|
||||
text: opt.textContent.trim()
|
||||
};
|
||||
});
|
||||
this.filteredOptions = this.options;
|
||||
// Ensure selected is always an array
|
||||
if (!Array.isArray(this.selected)) {
|
||||
this.selected = [];
|
||||
}
|
||||
},
|
||||
|
||||
filterOptions() {
|
||||
if (!this.search) {
|
||||
this.filteredOptions = this.options;
|
||||
return;
|
||||
}
|
||||
const searchLower = this.search.toLowerCase();
|
||||
this.filteredOptions = this.options.filter(opt =>
|
||||
opt.text.toLowerCase().includes(searchLower)
|
||||
);
|
||||
},
|
||||
|
||||
toggleOption(value) {
|
||||
// Ensure selected is an array
|
||||
if (!Array.isArray(this.selected)) {
|
||||
this.selected = [];
|
||||
}
|
||||
const index = this.selected.indexOf(value);
|
||||
if (index > -1) {
|
||||
this.selected.splice(index, 1);
|
||||
} else {
|
||||
this.selected.push(value);
|
||||
}
|
||||
this.search = '';
|
||||
this.filterOptions();
|
||||
// Focus input after selection
|
||||
this.$refs.searchInput.focus();
|
||||
},
|
||||
|
||||
removeOption(value, event) {
|
||||
// Ensure selected is an array
|
||||
if (!Array.isArray(this.selected)) {
|
||||
this.selected = [];
|
||||
return;
|
||||
}
|
||||
// Prevent triggering container click
|
||||
event.stopPropagation();
|
||||
const index = this.selected.indexOf(value);
|
||||
if (index > -1) {
|
||||
this.selected.splice(index, 1);
|
||||
}
|
||||
},
|
||||
|
||||
isSelected(value) {
|
||||
// Ensure selected is an array
|
||||
if (!Array.isArray(this.selected)) {
|
||||
return false;
|
||||
}
|
||||
return this.selected.includes(value);
|
||||
},
|
||||
|
||||
getSelectedText(value) {
|
||||
const option = this.options.find(opt => opt.value == value);
|
||||
return option ? option.text : value;
|
||||
}
|
||||
}" @click.outside="open = false" class="relative">
|
||||
|
||||
{{-- Unified Input Container with Tags Inside --}}
|
||||
<div @click="$refs.searchInput.focus()"
|
||||
class="flex flex-wrap gap-1.5 max-h-40 overflow-y-auto scrollbar py-1.5 px-2 w-full text-sm rounded-sm border-0 ring-2 ring-inset ring-neutral-200 dark:ring-coolgray-300 bg-white dark:bg-coolgray-100 cursor-text px-1 focus-within:border-l-4 focus-within:border-l-coollabs dark:focus-within:border-l-warning text-black dark:text-white"
|
||||
:class="{
|
||||
'opacity-50': {{ $disabled ? 'true' : 'false' }}
|
||||
}" wire:loading.class="opacity-50"
|
||||
wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4">
|
||||
|
||||
{{-- Selected Tags Inside Input --}}
|
||||
<template x-for="value in selected" :key="value">
|
||||
<button type="button" @click.stop="removeOption(value, $event)"
|
||||
:disabled="{{ $disabled ? 'true' : 'false' }}"
|
||||
class="inline-flex items-center gap-1.5 px-2 py-0.5 text-xs bg-coolgray-200 dark:bg-coolgray-700 rounded whitespace-nowrap {{ $disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:bg-red-100 dark:hover:bg-red-900/20 hover:text-red-600 dark:hover:text-red-400' }}"
|
||||
aria-label="Remove">
|
||||
<span x-text="getSelectedText(value)" class="max-w-[200px] truncate"></span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
{{-- Search Input (Borderless, Inside Container) --}}
|
||||
<input type="text" x-model="search" x-ref="searchInput" @input="filterOptions()" @focus="open = true"
|
||||
@keydown.escape="open = false" :placeholder="(Array.isArray(selected) && selected.length > 0) ? '' :
|
||||
{{ json_encode($placeholder ?: 'Search...') }}" @required($required) @readonly($readonly)
|
||||
@disabled($disabled) @if ($autofocus) autofocus @endif
|
||||
class="flex-1 min-w-[120px] text-sm border-0 outline-none bg-transparent p-0 focus:ring-0 placeholder:text-neutral-400 dark:placeholder:text-neutral-600 text-black dark:text-white" />
|
||||
</div>
|
||||
|
||||
{{-- Dropdown Options --}}
|
||||
<div x-show="open && !{{ $disabled ? 'true' : 'false' }}" x-transition
|
||||
class="absolute z-50 w-full mt-1 bg-white dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-400 rounded shadow-lg max-h-60 overflow-auto scrollbar">
|
||||
|
||||
<template x-if="filteredOptions.length === 0">
|
||||
<div class="px-3 py-2 text-sm text-neutral-500 dark:text-neutral-400">
|
||||
No options found
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-for="option in filteredOptions" :key="option.value">
|
||||
<div @click="toggleOption(option.value)"
|
||||
class="px-3 py-2 cursor-pointer hover:bg-neutral-100 dark:hover:bg-coolgray-200 flex items-center gap-3"
|
||||
:class="{ 'bg-neutral-50 dark:bg-coolgray-300': isSelected(option.value) }">
|
||||
<input type="checkbox" :checked="isSelected(option.value)"
|
||||
class="w-4 h-4 rounded border-neutral-300 dark:border-neutral-600 bg-white dark:bg-coolgray-100 text-black dark:text-white checked:bg-white dark:checked:bg-coolgray-100 focus:ring-coollabs dark:focus:ring-warning pointer-events-none"
|
||||
tabindex="-1">
|
||||
<span class="text-sm flex-1" x-text="option.text"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
{{-- Hidden datalist for options --}}
|
||||
<datalist x-ref="datalist" style="display: none;">
|
||||
{{ $slot }}
|
||||
</datalist>
|
||||
</div>
|
||||
@else
|
||||
{{-- Single Selection Mode with Alpine.js --}}
|
||||
<div x-data="{
|
||||
open: false,
|
||||
search: '',
|
||||
selected: @entangle($modelBinding).live,
|
||||
selected: @entangle(($attributes->whereStartsWith('wire:model')->first() ? $attributes->wire('model')->value() : $modelBinding)).live,
|
||||
options: [],
|
||||
filteredOptions: [],
|
||||
|
||||
|
||||
init() {
|
||||
this.options = Array.from(this.$refs.datalist.querySelectorAll('option')).map(opt => {
|
||||
// Skip disabled options
|
||||
if (opt.disabled) {
|
||||
return null;
|
||||
}
|
||||
// Try to parse as integer, fallback to string
|
||||
let value = opt.value;
|
||||
const intValue = parseInt(value, 10);
|
||||
@@ -32,14 +174,10 @@
|
||||
value: value,
|
||||
text: opt.textContent.trim()
|
||||
};
|
||||
});
|
||||
}).filter(opt => opt !== null);
|
||||
this.filteredOptions = this.options;
|
||||
// Ensure selected is always an array
|
||||
if (!Array.isArray(this.selected)) {
|
||||
this.selected = [];
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
filterOptions() {
|
||||
if (!this.search) {
|
||||
this.filteredOptions = this.options;
|
||||
@@ -50,243 +188,97 @@
|
||||
opt.text.toLowerCase().includes(searchLower)
|
||||
);
|
||||
},
|
||||
|
||||
toggleOption(value) {
|
||||
// Ensure selected is an array
|
||||
if (!Array.isArray(this.selected)) {
|
||||
this.selected = [];
|
||||
}
|
||||
const index = this.selected.indexOf(value);
|
||||
if (index > -1) {
|
||||
this.selected.splice(index, 1);
|
||||
} else {
|
||||
this.selected.push(value);
|
||||
}
|
||||
|
||||
selectOption(value) {
|
||||
this.selected = value;
|
||||
this.search = '';
|
||||
this.open = false;
|
||||
this.filterOptions();
|
||||
// Focus input after selection
|
||||
this.$refs.searchInput.focus();
|
||||
},
|
||||
|
||||
removeOption(value, event) {
|
||||
// Ensure selected is an array
|
||||
if (!Array.isArray(this.selected)) {
|
||||
this.selected = [];
|
||||
return;
|
||||
}
|
||||
// Prevent triggering container click
|
||||
event.stopPropagation();
|
||||
const index = this.selected.indexOf(value);
|
||||
if (index > -1) {
|
||||
this.selected.splice(index, 1);
|
||||
}
|
||||
|
||||
openDropdown() {
|
||||
if ({{ $disabled ? 'true' : 'false' }}) return;
|
||||
this.open = true;
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.searchInput) {
|
||||
this.$refs.searchInput.focus();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
isSelected(value) {
|
||||
// Ensure selected is an array
|
||||
if (!Array.isArray(this.selected)) {
|
||||
return false;
|
||||
}
|
||||
return this.selected.includes(value);
|
||||
|
||||
getSelectedText() {
|
||||
if (!this.selected || this.selected === 'default') return '';
|
||||
const option = this.options.find(opt => opt.value == this.selected);
|
||||
return option ? option.text : this.selected;
|
||||
},
|
||||
|
||||
getSelectedText(value) {
|
||||
const option = this.options.find(opt => opt.value == value);
|
||||
return option ? option.text : value;
|
||||
|
||||
isDefaultValue() {
|
||||
return !this.selected || this.selected === 'default' || this.selected === '';
|
||||
}
|
||||
}" @click.outside="open = false" class="relative">
|
||||
|
||||
{{-- Unified Input Container with Tags Inside --}}
|
||||
<div @click="$refs.searchInput.focus()"
|
||||
class="flex flex-wrap gap-1.5 max-h-40 overflow-y-auto scrollbar py-1.5 w-full text-sm rounded-sm border-0 ring-2 ring-inset ring-neutral-200 dark:ring-coolgray-300 bg-white dark:bg-coolgray-100 cursor-text px-1 focus-within:border-l-4 focus-within:border-l-coollabs dark:focus-within:border-l-warning text-black dark:text-white"
|
||||
:class="{
|
||||
{{-- Hidden input for form validation --}}
|
||||
<input type="hidden" :value="selected" @required($required) />
|
||||
|
||||
{{-- Input Container --}}
|
||||
<div @click="openDropdown()"
|
||||
class="flex items-center gap-2 py-1.5 w-full text-sm rounded-sm border-0 ring-2 ring-inset ring-neutral-200 dark:ring-coolgray-300 bg-white dark:bg-coolgray-100 cursor-text focus-within:border-l-4 focus-within:border-l-coollabs dark:focus-within:border-l-warning text-black dark:text-white"
|
||||
:class="{
|
||||
'opacity-50': {{ $disabled ? 'true' : 'false' }}
|
||||
}"
|
||||
wire:loading.class="opacity-50"
|
||||
wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4">
|
||||
}" wire:loading.class="opacity-50" wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4">
|
||||
|
||||
{{-- Selected Tags Inside Input --}}
|
||||
<template x-for="value in selected" :key="value">
|
||||
<button
|
||||
type="button"
|
||||
@click.stop="removeOption(value, $event)"
|
||||
:disabled="{{ $disabled ? 'true' : 'false' }}"
|
||||
class="inline-flex items-center gap-1.5 px-2 py-0.5 text-xs bg-coolgray-200 dark:bg-coolgray-700 rounded whitespace-nowrap {{ $disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:bg-red-100 dark:hover:bg-red-900/20 hover:text-red-600 dark:hover:text-red-400' }}"
|
||||
aria-label="Remove">
|
||||
<span x-text="getSelectedText(value)" class="max-w-[200px] truncate"></span>
|
||||
{{-- Display Selected Value or Search Input --}}
|
||||
<div class="flex-1 flex items-center min-w-0 px-1">
|
||||
<template x-if="!isDefaultValue() && !open">
|
||||
<span class="text-sm flex-1 truncate text-black dark:text-white px-2"
|
||||
x-text="getSelectedText()"></span>
|
||||
</template>
|
||||
<input type="text" x-show="isDefaultValue() || open" x-model="search" x-ref="searchInput"
|
||||
@input="filterOptions()" @focus="open = true" @keydown.escape="open = false"
|
||||
:placeholder="{{ json_encode($placeholder ?: 'Search...') }}" @readonly($readonly)
|
||||
@disabled($disabled) @if ($autofocus) autofocus @endif
|
||||
class="flex-1 text-sm border-0 outline-none bg-transparent p-0 focus:ring-0 placeholder:text-neutral-400 dark:placeholder:text-neutral-600 text-black dark:text-white px-2" />
|
||||
</div>
|
||||
|
||||
{{-- Dropdown Arrow --}}
|
||||
<button type="button" @click.stop="open = !open" :disabled="{{ $disabled ? 'true' : 'false' }}"
|
||||
class="shrink-0 text-neutral-400 px-2 {{ $disabled ? 'cursor-not-allowed' : 'cursor-pointer' }}">
|
||||
<svg class="w-4 h-4 transition-transform" :class="{ 'rotate-180': open }" fill="none"
|
||||
stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
{{-- Search Input (Borderless, Inside Container) --}}
|
||||
<input type="text" x-model="search" x-ref="searchInput" @input="filterOptions()" @focus="open = true"
|
||||
@keydown.escape="open = false"
|
||||
:placeholder="(Array.isArray(selected) && selected.length > 0) ? '' :
|
||||
{{ json_encode($placeholder ?: 'Search...') }}"
|
||||
@required($required) @readonly($readonly) @disabled($disabled) @if ($autofocus)
|
||||
autofocus
|
||||
{{-- Dropdown Options --}}
|
||||
<div x-show="open && !{{ $disabled ? 'true' : 'false' }}" x-transition
|
||||
class="absolute z-50 w-full mt-1 bg-white dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-400 rounded shadow-lg max-h-60 overflow-auto scrollbar">
|
||||
|
||||
<template x-if="filteredOptions.length === 0">
|
||||
<div class="px-3 py-2 text-sm text-neutral-500 dark:text-neutral-400">
|
||||
No options found
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-for="option in filteredOptions" :key="option.value">
|
||||
<div @click="selectOption(option.value)"
|
||||
class="px-3 py-2 cursor-pointer hover:bg-neutral-100 dark:hover:bg-coolgray-200"
|
||||
:class="{ 'bg-neutral-50 dark:bg-coolgray-300': selected == option.value }">
|
||||
<span class="text-sm" x-text="option.text"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
{{-- Hidden datalist for options --}}
|
||||
<datalist x-ref="datalist" style="display: none;">
|
||||
{{ $slot }}
|
||||
</datalist>
|
||||
</div>
|
||||
@endif
|
||||
class="flex-1 min-w-[120px] text-sm border-0 outline-none bg-transparent p-0 focus:ring-0 placeholder:text-neutral-400 dark:placeholder:text-neutral-600 text-black dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{{-- Dropdown Options --}}
|
||||
<div x-show="open && !{{ $disabled ? 'true' : 'false' }}" x-transition
|
||||
class="absolute z-50 w-full mt-1 bg-white dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-400 rounded shadow-lg max-h-60 overflow-auto scrollbar">
|
||||
|
||||
<template x-if="filteredOptions.length === 0">
|
||||
<div class="px-3 py-2 text-sm text-neutral-500 dark:text-neutral-400">
|
||||
No options found
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-for="option in filteredOptions" :key="option.value">
|
||||
<div @click="toggleOption(option.value)"
|
||||
class="px-3 py-2 cursor-pointer hover:bg-neutral-100 dark:hover:bg-coolgray-200 flex items-center gap-3"
|
||||
:class="{ 'bg-neutral-50 dark:bg-coolgray-300': isSelected(option.value) }">
|
||||
<input type="checkbox" :checked="isSelected(option.value)"
|
||||
class="w-4 h-4 rounded border-neutral-300 dark:border-neutral-600 bg-white dark:bg-coolgray-100 text-black dark:text-white checked:bg-white dark:checked:bg-coolgray-100 focus:ring-coollabs dark:focus:ring-warning pointer-events-none"
|
||||
tabindex="-1">
|
||||
<span class="text-sm flex-1" x-text="option.text"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
{{-- Hidden datalist for options --}}
|
||||
<datalist x-ref="datalist" style="display: none;">
|
||||
{{ $slot }}
|
||||
</datalist>
|
||||
</div>
|
||||
@else
|
||||
{{-- Single Selection Mode with Alpine.js --}}
|
||||
<div x-data="{
|
||||
open: false,
|
||||
search: '',
|
||||
selected: @entangle(($attributes->whereStartsWith('wire:model')->first() ? $attributes->wire('model')->value() : $modelBinding)).live,
|
||||
options: [],
|
||||
filteredOptions: [],
|
||||
|
||||
init() {
|
||||
this.options = Array.from(this.$refs.datalist.querySelectorAll('option')).map(opt => {
|
||||
// Skip disabled options
|
||||
if (opt.disabled) {
|
||||
return null;
|
||||
}
|
||||
// Try to parse as integer, fallback to string
|
||||
let value = opt.value;
|
||||
const intValue = parseInt(value, 10);
|
||||
if (!isNaN(intValue) && intValue.toString() === value) {
|
||||
value = intValue;
|
||||
}
|
||||
return {
|
||||
value: value,
|
||||
text: opt.textContent.trim()
|
||||
};
|
||||
}).filter(opt => opt !== null);
|
||||
this.filteredOptions = this.options;
|
||||
},
|
||||
|
||||
filterOptions() {
|
||||
if (!this.search) {
|
||||
this.filteredOptions = this.options;
|
||||
return;
|
||||
}
|
||||
const searchLower = this.search.toLowerCase();
|
||||
this.filteredOptions = this.options.filter(opt =>
|
||||
opt.text.toLowerCase().includes(searchLower)
|
||||
);
|
||||
},
|
||||
|
||||
selectOption(value) {
|
||||
this.selected = value;
|
||||
this.search = '';
|
||||
this.open = false;
|
||||
this.filterOptions();
|
||||
},
|
||||
|
||||
openDropdown() {
|
||||
if ({{ $disabled ? 'true' : 'false' }}) return;
|
||||
this.open = true;
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.searchInput) {
|
||||
this.$refs.searchInput.focus();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getSelectedText() {
|
||||
if (!this.selected || this.selected === 'default') return '';
|
||||
const option = this.options.find(opt => opt.value == this.selected);
|
||||
return option ? option.text : this.selected;
|
||||
},
|
||||
|
||||
isDefaultValue() {
|
||||
return !this.selected || this.selected === 'default' || this.selected === '';
|
||||
}
|
||||
}" @click.outside="open = false" class="relative">
|
||||
|
||||
{{-- Hidden input for form validation --}}
|
||||
<input type="hidden" :value="selected" @required($required) />
|
||||
|
||||
{{-- Input Container --}}
|
||||
<div @click="openDropdown()"
|
||||
class="flex items-center gap-2 py-1.5 w-full text-sm rounded-sm border-0 ring-2 ring-inset ring-neutral-200 dark:ring-coolgray-300 bg-white dark:bg-coolgray-100 cursor-text focus-within:border-l-4 focus-within:border-l-coollabs dark:focus-within:border-l-warning text-black dark:text-white"
|
||||
:class="{
|
||||
'opacity-50': {{ $disabled ? 'true' : 'false' }}
|
||||
}"
|
||||
wire:loading.class="opacity-50"
|
||||
wire:dirty.class="dark:border-l-warning border-l-coollabs border-l-4">
|
||||
|
||||
{{-- Display Selected Value or Search Input --}}
|
||||
<div class="flex-1 flex items-center min-w-0 px-1">
|
||||
<template x-if="!isDefaultValue() && !open">
|
||||
<span class="text-sm flex-1 truncate text-black dark:text-white px-2" x-text="getSelectedText()"></span>
|
||||
</template>
|
||||
<input type="text" x-show="isDefaultValue() || open" x-model="search" x-ref="searchInput"
|
||||
@input="filterOptions()" @focus="open = true"
|
||||
@keydown.escape="open = false"
|
||||
:placeholder="{{ json_encode($placeholder ?: 'Search...') }}"
|
||||
@readonly($readonly) @disabled($disabled) @if ($autofocus) autofocus @endif
|
||||
class="flex-1 text-sm border-0 outline-none bg-transparent p-0 focus:ring-0 placeholder:text-neutral-400 dark:placeholder:text-neutral-600 text-black dark:text-white px-2" />
|
||||
</div>
|
||||
|
||||
{{-- Dropdown Arrow --}}
|
||||
<button type="button" @click.stop="open = !open" :disabled="{{ $disabled ? 'true' : 'false' }}"
|
||||
class="shrink-0 text-neutral-400 px-2 {{ $disabled ? 'cursor-not-allowed' : 'cursor-pointer' }}">
|
||||
<svg class="w-4 h-4 transition-transform" :class="{ 'rotate-180': open }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Dropdown Options --}}
|
||||
<div x-show="open && !{{ $disabled ? 'true' : 'false' }}" x-transition
|
||||
class="absolute z-50 w-full mt-1 bg-white dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-400 rounded shadow-lg max-h-60 overflow-auto scrollbar">
|
||||
|
||||
<template x-if="filteredOptions.length === 0">
|
||||
<div class="px-3 py-2 text-sm text-neutral-500 dark:text-neutral-400">
|
||||
No options found
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-for="option in filteredOptions" :key="option.value">
|
||||
<div @click="selectOption(option.value)"
|
||||
class="px-3 py-2 cursor-pointer hover:bg-neutral-100 dark:hover:bg-coolgray-200"
|
||||
:class="{ 'bg-neutral-50 dark:bg-coolgray-300': selected == option.value }">
|
||||
<span class="text-sm" x-text="option.text"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
{{-- Hidden datalist for options --}}
|
||||
<datalist x-ref="datalist" style="display: none;">
|
||||
{{ $slot }}
|
||||
</datalist>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@error($modelBinding)
|
||||
<label class="label">
|
||||
<span class="text-red-500 label-text-alt">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
@error($modelBinding)
|
||||
<label class="label">
|
||||
<span class="text-red-500 label-text-alt">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
@@ -30,12 +30,22 @@
|
||||
document.getElementById(this.monacoId).dispatchEvent(new CustomEvent('monaco-editor-focused', { detail: { monacoId: this.monacoId } }));
|
||||
},
|
||||
monacoEditorAddLoaderScriptToHead() {
|
||||
let script = document.createElement('script');
|
||||
script.src = `/js/monaco-editor-${this.monacoVersion}/min/vs/loader.js`;
|
||||
document.head.appendChild(script);
|
||||
// Use a global flag to prevent duplicate script loading
|
||||
if (!window.__coolifyMonacoLoaderAdding && typeof _amdLoaderGlobal === 'undefined') {
|
||||
window.__coolifyMonacoLoaderAdding = true;
|
||||
let script = document.createElement('script');
|
||||
script.src = `/js/monaco-editor-${this.monacoVersion}/min/vs/loader.js`;
|
||||
script.onload = () => {
|
||||
window.__coolifyMonacoLoaderAdding = false;
|
||||
};
|
||||
script.onerror = () => {
|
||||
window.__coolifyMonacoLoaderAdding = false;
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
}
|
||||
}" x-modelable="monacoContent">
|
||||
<div x-cloak x-init="if (typeof _amdLoaderGlobal == 'undefined') {
|
||||
<div x-cloak x-init="if (typeof _amdLoaderGlobal == 'undefined' && !window.__coolifyMonacoLoaderAdding) {
|
||||
monacoEditorAddLoaderScriptToHead();
|
||||
}
|
||||
checkTheme();
|
||||
@@ -104,7 +114,7 @@
|
||||
}, 5);" :id="monacoId">
|
||||
</div>
|
||||
<div class="relative z-10 w-full h-full">
|
||||
<div x-ref="monacoEditorElement" class="w-full h-[calc(100vh-20rem)] min-h-96 text-md {{ $readonly ? 'opacity-65' : '' }}"></div>
|
||||
<div x-ref="monacoEditorElement" class="w-full text-md {{ $readonly ? 'opacity-65' : '' }}" style="height: var(--editor-height, calc(100vh - 20rem)); min-height: 300px;"></div>
|
||||
<div x-ref="monacoPlaceholderElement" x-show="monacoPlaceholder" @click="monacoEditorFocus()"
|
||||
:style="'font-size: ' + monacoFontSize"
|
||||
class="w-full text-sm font-mono absolute z-50 text-gray-500 ml-14 -translate-x-0.5 mt-0.5 left-0 top-0"
|
||||
|
||||
@@ -177,7 +177,7 @@
|
||||
@endif
|
||||
<template x-teleport="body">
|
||||
<div x-show="modalOpen"
|
||||
class="fixed top-0 lg:pt-10 left-0 z-99 flex items-start justify-center w-screen h-screen" x-cloak>
|
||||
class="fixed top-0 left-0 z-99 flex items-center justify-center w-screen h-screen p-4" x-cloak>
|
||||
<div x-show="modalOpen" class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs">
|
||||
</div>
|
||||
<div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100"
|
||||
@@ -186,8 +186,8 @@
|
||||
x-transition:leave="ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
|
||||
class="relative w-full py-6 border rounded-sm min-w-full lg:min-w-[36rem] max-w-[48rem] bg-neutral-100 border-neutral-400 dark:bg-base px-7 dark:border-coolgray-300">
|
||||
<div class="flex justify-between items-center pb-3">
|
||||
class="relative w-full border rounded-sm min-w-full lg:min-w-[36rem] max-w-[48rem] max-h-[calc(100vh-2rem)] bg-neutral-100 border-neutral-400 dark:bg-base dark:border-coolgray-300 flex flex-col">
|
||||
<div class="flex justify-between items-center py-6 px-7 shrink-0">
|
||||
<h3 class="pr-8 text-2xl font-bold">{{ $title }}</h3>
|
||||
<button @click="modalOpen = false; resetModal()"
|
||||
class="flex absolute top-2 right-2 justify-center items-center w-8 h-8 rounded-full dark:text-white hover:bg-coolgray-300">
|
||||
@@ -197,7 +197,7 @@
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative w-auto">
|
||||
<div class="relative w-auto overflow-y-auto px-7 pb-6" style="-webkit-overflow-scrolling: touch;">
|
||||
@if (!empty($checkboxes))
|
||||
<!-- Step 1: Select actions -->
|
||||
<div x-show="step === 1">
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<template x-teleport="body">
|
||||
<div x-show="modalOpen"
|
||||
x-init="$watch('modalOpen', value => { if(value) { $nextTick(() => { const firstInput = $el.querySelector('input, textarea, select'); firstInput?.focus(); }) } })"
|
||||
class="fixed top-0 left-0 lg:px-0 px-4 z-99 flex items-center justify-center w-screen h-screen">
|
||||
class="fixed top-0 left-0 z-99 flex items-center justify-center w-screen h-screen p-4">
|
||||
<div x-show="modalOpen" x-transition:enter="ease-out duration-100" x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
|
||||
@@ -45,8 +45,8 @@
|
||||
x-transition:leave="ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
|
||||
class="relative w-full py-6 border rounded-sm drop-shadow-sm min-w-full lg:min-w-[{{ $minWidth }}] max-w-fit bg-white border-neutral-200 dark:bg-base px-6 dark:border-coolgray-300">
|
||||
<div class="flex items-center justify-between pb-3">
|
||||
class="relative w-full border rounded-sm drop-shadow-sm min-w-full lg:min-w-[{{ $minWidth }}] max-w-fit max-h-[calc(100vh-2rem)] bg-white border-neutral-200 dark:bg-base dark:border-coolgray-300 flex flex-col">
|
||||
<div class="flex items-center justify-between py-6 px-6 shrink-0">
|
||||
<h3 class="text-2xl font-bold">{{ $title }}</h3>
|
||||
<button @click="modalOpen=false"
|
||||
class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5 rounded-full dark:text-white hover:bg-neutral-100 dark:hover:bg-coolgray-300 outline-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-coollabs dark:focus-visible:ring-warning focus-visible:ring-offset-2 dark:focus-visible:ring-offset-base">
|
||||
@@ -56,7 +56,7 @@
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative flex items-center justify-center w-auto">
|
||||
<div class="relative flex items-center justify-center w-auto overflow-y-auto px-6 pb-6" style="-webkit-overflow-scrolling: touch;">
|
||||
{{ $slot }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<dialog id="{{ $modalId }}" class="modal">
|
||||
@if ($yesOrNo)
|
||||
<form method="dialog" class="rounded-sm modal-box" @if (!$noSubmit) wire:submit='submit' @endif>
|
||||
<div class="flex items-start">
|
||||
<form method="dialog" class="rounded-sm modal-box max-h-[calc(100vh-5rem)] flex flex-col" @if (!$noSubmit) wire:submit='submit' @endif>
|
||||
<div class="flex items-start overflow-y-auto" style="-webkit-overflow-scrolling: touch;">
|
||||
<div class="flex items-center justify-center shrink-0 w-10 h-10 mr-4 rounded-full">
|
||||
<svg class="w-8 h-8 text-error" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" aria-hidden="true">
|
||||
@@ -33,7 +33,8 @@
|
||||
</div>
|
||||
</form>
|
||||
@else
|
||||
<form method="dialog" class="flex flex-col w-11/12 max-w-5xl gap-2 rounded-sm modal-box"
|
||||
<form method="dialog" class="flex flex-col w-11/12 max-w-5xl max-h-[calc(100vh-5rem)] gap-2 rounded-sm modal-box overflow-y-auto"
|
||||
style="-webkit-overflow-scrolling: touch;"
|
||||
@if ($submitWireAction) wire:submit={{ $submitWireAction }} @endif
|
||||
@if (!$noSubmit && !$submitWireAction) wire:submit='submit' @endif>
|
||||
@isset($modalTitle)
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
}">
|
||||
<div class="flex lg:pt-6 pt-4 pb-4 pl-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="text-2xl font-bold tracking-wide dark:text-white">Coolify</div>
|
||||
<a href="/" class="text-2xl font-bold tracking-wide dark:text-white hover:opacity-80 transition-opacity">Coolify</a>
|
||||
<x-version />
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
'hover:border-l-red-500 cursor-not-allowed' => $upgrade,
|
||||
])>
|
||||
<div class="flex items-center">
|
||||
{{ $logo }}
|
||||
<div class="w-[4.5rem] h-[4.5rem] flex items-center justify-center text-black dark:text-white shrink-0">
|
||||
{{ $logo }}
|
||||
</div>
|
||||
<div class="flex flex-col pl-2 ">
|
||||
<div class="dark:text-white text-md">
|
||||
{{ $title }}
|
||||
|
||||
@@ -13,14 +13,14 @@
|
||||
<x-status.stopped :status="$resource->status" />
|
||||
@endif
|
||||
@if (!str($resource->status)->contains('exited') && $showRefreshButton)
|
||||
<button wire:loading.remove title="Refresh Status" wire:click='checkStatus'
|
||||
<button wire:loading.remove.delay.shortest wire:target="manualCheckStatus" title="Refresh Status" wire:click='manualCheckStatus'
|
||||
class="mx-1 dark:hover:fill-white fill-black dark:fill-warning">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button wire:loading title="Refreshing Status" wire:click='checkStatus'
|
||||
<button wire:loading.delay.shortest wire:target="manualCheckStatus" title="Refreshing Status" wire:click='manualCheckStatus'
|
||||
class="mx-1 dark:hover:fill-white fill-black dark:fill-warning">
|
||||
<svg class="w-4 h-4 animate-spin" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
|
||||
@@ -10,14 +10,14 @@
|
||||
<x-status.stopped :status="$complexStatus" />
|
||||
@endif
|
||||
@if (!str($complexStatus)->contains('exited') && $showRefreshButton)
|
||||
<button wire:loading.remove title="Refresh Status" wire:click='checkStatus'
|
||||
<button wire:loading.remove.delay.shortest wire:target="manualCheckStatus" title="Refresh Status" wire:click='manualCheckStatus'
|
||||
class="mx-1 dark:hover:fill-white fill-black dark:fill-warning">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 2a10.016 10.016 0 0 0-7 2.877V3a1 1 0 1 0-2 0v4.5a1 1 0 0 0 1 1h4.5a1 1 0 0 0 0-2H6.218A7.98 7.98 0 0 1 20 12a1 1 0 0 0 2 0A10.012 10.012 0 0 0 12 2zm7.989 13.5h-4.5a1 1 0 0 0 0 2h2.293A7.98 7.98 0 0 1 4 12a1 1 0 0 0-2 0a9.986 9.986 0 0 0 16.989 7.133V21a1 1 0 0 0 2 0v-4.5a1 1 0 0 0-1-1z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button wire:loading title="Refreshing Status" wire:click='checkStatus'
|
||||
<button wire:loading.delay.shortest wire:target="manualCheckStatus" title="Refreshing Status" wire:click='manualCheckStatus'
|
||||
class="mx-1 dark:hover:fill-white fill-black dark:fill-warning">
|
||||
<svg class="w-4 h-4 animate-spin" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
<div class="sticky top-0 z-40 flex items-center justify-between px-4 py-4 gap-x-6 sm:px-6 lg:hidden bg-white/95 dark:bg-base/95 backdrop-blur-sm border-b border-neutral-300/50 dark:border-coolgray-200/50">
|
||||
<div class="flex items-center gap-3 flex-shrink-0">
|
||||
<div class="text-xl font-bold tracking-wide dark:text-white">Coolify</div>
|
||||
<a href="/" class="text-xl font-bold tracking-wide dark:text-white hover:opacity-80 transition-opacity">Coolify</a>
|
||||
<livewire:switch-team />
|
||||
</div>
|
||||
<button type="button" class="-m-2.5 p-2.5 dark:text-warning" x-on:click="open = !open">
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html data-theme="dark" lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
|
||||
<script>
|
||||
// Immediate theme application - runs before any rendering
|
||||
(function() {
|
||||
const t = localStorage.theme || 'dark';
|
||||
const d = t === 'dark' || (t === 'system' && matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
document.documentElement.classList[d ? 'add' : 'remove']('dark');
|
||||
})();
|
||||
</script>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex">
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<meta name="theme-color" content="#101010" id="theme-color-meta" />
|
||||
<meta name="color-scheme" content="dark light" />
|
||||
<meta name="Description" content="Coolify: An open-source & self-hostable Heroku / Netlify / Vercel alternative" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
@@ -41,6 +49,12 @@
|
||||
@endenv
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
@vite(['resources/js/app.js', 'resources/css/app.css'])
|
||||
<script>
|
||||
// Update theme-color meta tag (non-critical, can run async)
|
||||
const t = localStorage.theme || 'dark';
|
||||
const isDark = t === 'dark' || (t === 'system' && matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
document.getElementById('theme-color-meta')?.setAttribute('content', isDark ? '#101010' : '#ffffff');
|
||||
</script>
|
||||
<style>
|
||||
[x-cloak] {
|
||||
display: none !important;
|
||||
@@ -108,7 +122,7 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Existing link sanitization
|
||||
if (node.nodeName === 'A' && node.hasAttribute('href')) {
|
||||
const href = node.getAttribute('href') || '';
|
||||
@@ -123,20 +137,11 @@
|
||||
return DOMPurify.sanitize(html, config);
|
||||
};
|
||||
|
||||
// Initialize theme if not set
|
||||
if (!('theme' in localStorage)) {
|
||||
localStorage.theme = 'dark';
|
||||
document.documentElement.classList.add('dark')
|
||||
} else if (localStorage.theme === 'dark') {
|
||||
document.documentElement.classList.add('dark')
|
||||
} else if (localStorage.theme === 'light') {
|
||||
document.documentElement.classList.remove('dark')
|
||||
} else {
|
||||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
document.documentElement.classList.add('dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
}
|
||||
|
||||
let theme = localStorage.theme
|
||||
let cpuColor = '#1e90ff'
|
||||
let ramColor = '#00ced1'
|
||||
|
||||
@@ -191,7 +191,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</x-slot:content>
|
||||
<livewire:server.new.by-hetzner :private_keys="$this->privateKeys" :limit_reached="false" />
|
||||
<livewire:server.new.by-hetzner :limit_reached="false" :from_onboarding="true" />
|
||||
</x-modal-input>
|
||||
@endif
|
||||
@endcan
|
||||
|
||||
@@ -869,6 +869,14 @@
|
||||
<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 class="mt-4">
|
||||
<a href="{{ route('onboarding') }}" class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-coollabs dark:bg-warning hover:bg-coollabs-100 dark:hover:bg-warning/90 rounded-lg transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||
</svg>
|
||||
View Onboarding Guide
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -90,12 +90,12 @@
|
||||
@if ($application->build_pack !== 'dockercompose')
|
||||
<div class="flex items-end gap-2">
|
||||
@if ($application->settings->is_container_label_readonly_enabled == false)
|
||||
<x-forms.input placeholder="https://coolify.io" wire:model="application.fqdn"
|
||||
<x-forms.input placeholder="https://coolify.io" wire:model="fqdn"
|
||||
label="Domains" readonly
|
||||
helper="Readonly labels are disabled. You can set the domains in the labels section."
|
||||
x-bind:disabled="!canUpdate" />
|
||||
@else
|
||||
<x-forms.input placeholder="https://coolify.io" wire:model="application.fqdn"
|
||||
<x-forms.input placeholder="https://coolify.io" wire:model="fqdn"
|
||||
label="Domains"
|
||||
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io,https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "
|
||||
x-bind:disabled="!canUpdate" />
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<div>
|
||||
<x-slot:title>
|
||||
{{ data_get_str($project, 'name')->limit(10) }} > Edit | Coolify
|
||||
</x-slot>
|
||||
<form wire:submit='submit' class="flex flex-col pb-10">
|
||||
<div class="flex gap-2">
|
||||
<h1>Project: {{ data_get_str($project, 'name')->limit(15) }}</h1>
|
||||
<div class="flex items-end gap-2">
|
||||
<x-forms.button type="submit">Save</x-forms.button>
|
||||
<livewire:project.delete-project :disabled="!$project->isEmpty()" :project_id="$project->id" />
|
||||
</x-slot>
|
||||
<form wire:submit='submit' class="flex flex-col pb-10">
|
||||
<div class="flex gap-2">
|
||||
<h1>{{ data_get_str($project, 'name')->limit(15) }}</h1>
|
||||
<div class="flex items-end gap-2">
|
||||
<x-forms.button type="submit">Save</x-forms.button>
|
||||
<livewire:project.delete-project :disabled="!$project->isEmpty()" :project_id="$project->id" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-2 pb-10">Edit project details here.</div>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input label="Name" id="name" />
|
||||
<x-forms.input label="Description" id="description" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="pt-2 pb-10">Edit project details here.</div>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input label="Name" id="name" />
|
||||
<x-forms.input label="Description" id="description" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -11,29 +11,38 @@
|
||||
@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" x-data="{ projects: @js($projects) }">
|
||||
<template x-for="project in projects" :key="project.uuid">
|
||||
<div class="box group cursor-pointer" @click="$wire.navigateToProject(project.uuid)">
|
||||
<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 box group">
|
||||
<a href="{{ $project->navigateTo() }}" 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" x-text="project.name"></div>
|
||||
<div class="box-title">{{ $project->name }}</div>
|
||||
<div class="box-description">
|
||||
<div x-text="project.description"></div>
|
||||
{{ $project->description }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative z-10 flex items-center justify-center gap-4 text-xs font-bold"
|
||||
x-show="project.canUpdate || project.canCreateResource">
|
||||
<a class="hover:underline" wire:click.stop x-show="project.addResourceRoute"
|
||||
:href="project.addResourceRoute">
|
||||
+ Add Resource
|
||||
</a>
|
||||
<a class="hover:underline" wire:click.stop x-show="project.canUpdate"
|
||||
:href="`/project/${project.uuid}/edit`">
|
||||
Settings
|
||||
</a>
|
||||
<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"
|
||||
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"
|
||||
href="{{ route('project.edit', ['project_uuid' => $project->uuid]) }}">
|
||||
Settings
|
||||
</a>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@@ -13,9 +13,55 @@
|
||||
<div x-data="searchResources()">
|
||||
@if ($current_step === 'type')
|
||||
<div x-init="window.addEventListener('scroll', () => isSticky = window.pageYOffset > 100)" class="sticky z-10 top-10 py-2">
|
||||
<input autocomplete="off" x-ref="searchInput" class="input-sticky"
|
||||
:class="{ 'input-sticky-active': isSticky }" x-model="search" placeholder="Type / to search..."
|
||||
@keydown.window.slash.prevent="$refs.searchInput.focus()">
|
||||
<div class="flex gap-2 items-start">
|
||||
<input autocomplete="off" x-ref="searchInput" class="input-sticky flex-1"
|
||||
:class="{ 'input-sticky-active': isSticky }" x-model="search" placeholder="Type / to search..."
|
||||
@keydown.window.slash.prevent="$refs.searchInput.focus()">
|
||||
<!-- Category Filter Dropdown -->
|
||||
<div class="relative" x-data="{ openCategoryDropdown: false, categorySearch: '' }" @click.outside="openCategoryDropdown = false">
|
||||
<!-- Loading/Disabled State -->
|
||||
<div x-show="loading || categories.length === 0"
|
||||
class="flex items-center justify-between gap-2 py-1.5 px-3 w-64 text-sm rounded-sm border-0 ring-2 ring-inset ring-neutral-200 dark:ring-coolgray-300 bg-neutral-100 dark:bg-coolgray-200 cursor-not-allowed whitespace-nowrap opacity-50">
|
||||
<span class="text-sm text-neutral-400 dark:text-neutral-600">Filter by category</span>
|
||||
<svg class="w-4 h-4 text-neutral-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
<!-- Active State -->
|
||||
<div x-show="!loading && categories.length > 0"
|
||||
@click="openCategoryDropdown = !openCategoryDropdown; $nextTick(() => { if (openCategoryDropdown) $refs.categorySearchInput.focus() })"
|
||||
class="flex items-center justify-between gap-2 py-1.5 px-3 w-64 text-sm rounded-sm border-0 ring-2 ring-inset ring-neutral-200 dark:ring-coolgray-300 bg-white dark:bg-coolgray-100 cursor-pointer hover:ring-coolgray-400 transition-all whitespace-nowrap">
|
||||
<span class="text-sm truncate" x-text="selectedCategory === '' ? 'Filter by category' : selectedCategory" :class="selectedCategory === '' ? 'text-neutral-400 dark:text-neutral-600' : 'capitalize text-black dark:text-white'"></span>
|
||||
<svg class="w-4 h-4 transition-transform text-neutral-400 shrink-0" :class="{ 'rotate-180': openCategoryDropdown }" fill="none"
|
||||
stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
<!-- Dropdown Menu -->
|
||||
<div x-show="openCategoryDropdown" x-transition
|
||||
class="absolute z-50 w-full mt-1 bg-white dark:bg-coolgray-100 border border-neutral-300 dark:border-coolgray-400 rounded shadow-lg overflow-hidden">
|
||||
<div class="sticky top-0 p-2 bg-white dark:bg-coolgray-100 border-b border-neutral-300 dark:border-coolgray-400">
|
||||
<input type="text" x-ref="categorySearchInput" x-model="categorySearch" placeholder="Search categories..."
|
||||
class="w-full px-2 py-1 text-sm rounded border border-neutral-300 dark:border-coolgray-400 bg-white dark:bg-coolgray-200 focus:outline-none focus:ring-2 focus:ring-coolgray-400"
|
||||
@click.stop>
|
||||
</div>
|
||||
<div class="max-h-60 overflow-auto scrollbar">
|
||||
<div @click="selectedCategory = ''; categorySearch = ''; openCategoryDropdown = false"
|
||||
class="px-3 py-2 cursor-pointer hover:bg-neutral-100 dark:hover:bg-coolgray-200"
|
||||
:class="{ 'bg-neutral-50 dark:bg-coolgray-300': selectedCategory === '' }">
|
||||
<span class="text-sm">All Categories</span>
|
||||
</div>
|
||||
<template x-for="category in categories.filter(cat => categorySearch === '' || cat.toLowerCase().includes(categorySearch.toLowerCase()))" :key="category">
|
||||
<div @click="selectedCategory = category; categorySearch = ''; openCategoryDropdown = false"
|
||||
class="px-3 py-2 cursor-pointer hover:bg-neutral-100 dark:hover:bg-coolgray-200 capitalize"
|
||||
:class="{ 'bg-neutral-50 dark:bg-coolgray-300': selectedCategory === category }">
|
||||
<span class="text-sm" x-text="category"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div x-show="loading">Loading...</div>
|
||||
<div x-show="!loading" class="flex flex-col gap-4 py-4">
|
||||
@@ -28,13 +74,13 @@
|
||||
:class="{ 'cursor-pointer': !selecting, 'cursor-not-allowed opacity-50': selecting }">
|
||||
<x-resource-view>
|
||||
<x-slot:title><span x-text="application.name"></span></x-slot>
|
||||
<x-slot:description>
|
||||
<span x-html="window.sanitizeHTML(application.description)"></span>
|
||||
</x-slot>
|
||||
<x-slot:logo>
|
||||
<img class="w-[4.5rem] aspect-square h-[4.5rem] p-2 transition-all duration-200 dark:opacity-30 grayscale group-hover:grayscale-0 group-hover:opacity-100 dark:bg-white/10 bg-black/10"
|
||||
:src="application.logo">
|
||||
</x-slot:logo>
|
||||
<x-slot:description>
|
||||
<span x-html="window.sanitizeHTML(application.description)"></span>
|
||||
</x-slot>
|
||||
<x-slot:logo>
|
||||
<img class="w-full h-full p-2 transition-all duration-200 dark:bg-white/10 bg-black/10 object-contain"
|
||||
:src="application.logo">
|
||||
</x-slot:logo>
|
||||
</x-resource-view>
|
||||
</div>
|
||||
</template>
|
||||
@@ -47,10 +93,10 @@
|
||||
:class="{ 'cursor-pointer': !selecting, 'cursor-not-allowed opacity-50': selecting }">
|
||||
<x-resource-view>
|
||||
<x-slot:title><span x-text="application.name"></span></x-slot>
|
||||
<x-slot:description><span x-text="application.description"></span></x-slot>
|
||||
<x-slot:logo> <img
|
||||
class="w-[4.5rem] aspect-square h-[4.5rem] p-2 transition-all duration-200 dark:opacity-30 grayscale group-hover:grayscale-0 group-hover:opacity-100 dark:bg-white/10 bg-black/10 "
|
||||
:src="application.logo"></x-slot>
|
||||
<x-slot:description><span x-text="application.description"></span></x-slot>
|
||||
<x-slot:logo> <img
|
||||
class="w-full h-full p-2 transition-all duration-200 dark:bg-white/10 bg-black/10 object-contain"
|
||||
:src="application.logo"></x-slot>
|
||||
</x-resource-view>
|
||||
</div>
|
||||
</template>
|
||||
@@ -63,12 +109,12 @@
|
||||
:class="{ 'cursor-pointer': !selecting, 'cursor-not-allowed opacity-50': selecting }">
|
||||
<x-resource-view>
|
||||
<x-slot:title><span x-text="database.name"></span></x-slot>
|
||||
<x-slot:description><span x-text="database.description"></span></x-slot>
|
||||
<x-slot:logo>
|
||||
<span x-show="database.logo">
|
||||
<span x-html="database.logo"></span>
|
||||
</span>
|
||||
</x-slot>
|
||||
<x-slot:description><span x-text="database.description"></span></x-slot>
|
||||
<x-slot:logo>
|
||||
<span x-show="database.logo">
|
||||
<span x-html="database.logo"></span>
|
||||
</span>
|
||||
</x-slot>
|
||||
</x-resource-view>
|
||||
</div>
|
||||
</template>
|
||||
@@ -95,33 +141,33 @@
|
||||
<template x-if="service.name">
|
||||
<span x-text="service.name"></span>
|
||||
</template>
|
||||
</x-slot>
|
||||
<x-slot:description>
|
||||
<template x-if="service.slogan">
|
||||
<span x-text="service.slogan"></span>
|
||||
</template>
|
||||
</x-slot>
|
||||
<x-slot:logo>
|
||||
<template x-if="service.logo">
|
||||
<img class="w-[4.5rem] aspect-square h-[4.5rem] p-2 transition-all duration-200 dark:opacity-30 grayscale group-hover:grayscale-0 group-hover:opacity-100 dark:bg-white/10 bg-black/10"
|
||||
:src='service.logo'
|
||||
x-on:error.window="$event.target.src = service.logo_github_url"
|
||||
onerror="this.onerror=null; this.src=this.getAttribute('data-fallback');"
|
||||
x-on:error="$event.target.src = '/coolify-logo.svg'"
|
||||
:data-fallback='service.logo_github_url' />
|
||||
</template>
|
||||
</x-slot:logo>
|
||||
<x-slot:documentation>
|
||||
<template x-if="service.documentation">
|
||||
<div class="flex items-center px-2" title="Read the documentation.">
|
||||
<a class="p-2 rounded-sm hover:bg-gray-100 dark:hover:bg-coolgray-200 hover:no-underline dark:group-hover:text-white text-neutral-600"
|
||||
onclick="event.stopPropagation()" :href="service.documentation"
|
||||
target="_blank">
|
||||
Docs
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</x-slot:documentation>
|
||||
</x-slot>
|
||||
<x-slot:description>
|
||||
<template x-if="service.slogan">
|
||||
<span x-text="service.slogan"></span>
|
||||
</template>
|
||||
</x-slot>
|
||||
<x-slot:logo>
|
||||
<template x-if="service.logo">
|
||||
<img class="w-full h-full p-2 transition-all duration-200 dark:bg-white/10 bg-black/10 object-contain"
|
||||
:src='service.logo'
|
||||
x-on:error.window="$event.target.src = service.logo_github_url"
|
||||
onerror="this.onerror=null; this.src=this.getAttribute('data-fallback');"
|
||||
x-on:error="$event.target.src = '/coolify-logo.svg'"
|
||||
:data-fallback='service.logo_github_url' />
|
||||
</template>
|
||||
</x-slot:logo>
|
||||
<x-slot:documentation>
|
||||
<template x-if="service.documentation">
|
||||
<div class="flex items-center px-2" title="Read the documentation.">
|
||||
<a class="p-2 rounded-sm hover:bg-gray-100 dark:hover:bg-coolgray-200 hover:no-underline dark:group-hover:text-white text-neutral-600"
|
||||
onclick="event.stopPropagation()" :href="service.documentation"
|
||||
target="_blank">
|
||||
Docs
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</x-slot:documentation>
|
||||
</x-resource-view>
|
||||
</div>
|
||||
</template>
|
||||
@@ -140,6 +186,8 @@
|
||||
function searchResources() {
|
||||
return {
|
||||
search: '',
|
||||
selectedCategory: '',
|
||||
categories: [],
|
||||
loading: false,
|
||||
isSticky: false,
|
||||
selecting: false,
|
||||
@@ -156,11 +204,13 @@
|
||||
this.loading = true;
|
||||
const {
|
||||
services,
|
||||
categories,
|
||||
gitBasedApplications,
|
||||
dockerBasedApplications,
|
||||
databases
|
||||
} = await this.$wire.loadServices();
|
||||
this.services = services;
|
||||
this.categories = categories || [];
|
||||
this.gitBasedApplications = gitBasedApplications;
|
||||
this.dockerBasedApplications = dockerBasedApplications;
|
||||
this.databases = databases;
|
||||
@@ -171,15 +221,30 @@
|
||||
},
|
||||
filterAndSort(items, isSort = true) {
|
||||
const searchLower = this.search.trim().toLowerCase();
|
||||
let filtered = Object.values(items);
|
||||
|
||||
if (searchLower === '') {
|
||||
return isSort ? Object.values(items).sort(sortFn) : Object.values(items);
|
||||
// Filter by category if selected
|
||||
if (this.selectedCategory !== '') {
|
||||
const selectedCategoryLower = this.selectedCategory.toLowerCase();
|
||||
filtered = filtered.filter(item => {
|
||||
if (!item.category) return false;
|
||||
// Handle comma-separated categories
|
||||
const categories = item.category.includes(',')
|
||||
? item.category.split(',').map(c => c.trim().toLowerCase())
|
||||
: [item.category.toLowerCase()];
|
||||
return categories.includes(selectedCategoryLower);
|
||||
});
|
||||
}
|
||||
const filtered = Object.values(items).filter(item => {
|
||||
return (item.name?.toLowerCase().includes(searchLower) ||
|
||||
item.description?.toLowerCase().includes(searchLower) ||
|
||||
item.slogan?.toLowerCase().includes(searchLower))
|
||||
})
|
||||
|
||||
// Filter by search term
|
||||
if (searchLower !== '') {
|
||||
filtered = filtered.filter(item => {
|
||||
return (item.name?.toLowerCase().includes(searchLower) ||
|
||||
item.description?.toLowerCase().includes(searchLower) ||
|
||||
item.slogan?.toLowerCase().includes(searchLower))
|
||||
});
|
||||
}
|
||||
|
||||
return isSort ? filtered.sort(sortFn) : filtered;
|
||||
},
|
||||
get filteredGitBasedApplications() {
|
||||
@@ -236,14 +301,14 @@
|
||||
{{ $server->name }}
|
||||
</div>
|
||||
<div class="box-description">
|
||||
{{ $server->description }}</div>
|
||||
{{ $server->description }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div>
|
||||
|
||||
<div>No validated & reachable servers found. <a class="underline dark:text-white"
|
||||
href="/servers">
|
||||
<div>No validated & reachable servers found. <a class="underline dark:text-white" href="/servers">
|
||||
Go to servers page
|
||||
</a></div>
|
||||
</div>
|
||||
@@ -303,8 +368,7 @@
|
||||
|
||||
<div class="flex items-center px-2" title="Read the documentation.">
|
||||
<a class="p-2 hover:underline dark:group-hover:text-white dark:text-white text-neutral-6000"
|
||||
onclick="event.stopPropagation()" href="https://hub.docker.com/_/postgres/"
|
||||
target="_blank">
|
||||
onclick="event.stopPropagation()" href="https://hub.docker.com/_/postgres/" target="_blank">
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
@@ -322,8 +386,7 @@
|
||||
<div class="flex-1"></div>
|
||||
<div class="flex items-center px-2" title="Read the documentation.">
|
||||
<a class="p-2 hover:underline dark:group-hover:text-white dark:text-white text-neutral-600"
|
||||
onclick="event.stopPropagation()" href="https://github.com/supabase/postgres"
|
||||
target="_blank">
|
||||
onclick="event.stopPropagation()" href="https://github.com/supabase/postgres" target="_blank">
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
@@ -361,8 +424,7 @@
|
||||
|
||||
<div class="flex items-center px-2" title="Read the documentation.">
|
||||
<a class="p-2 hover:underline dark:group-hover:text-white dark:text-white text-neutral-600"
|
||||
onclick="event.stopPropagation()" href="https://github.com/pgvector/pgvector"
|
||||
target="_blank">
|
||||
onclick="event.stopPropagation()" href="https://github.com/pgvector/pgvector" target="_blank">
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
@@ -377,4 +439,4 @@
|
||||
<x-forms.button type="submit">Add Database</x-forms.button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,23 +1,40 @@
|
||||
<div x-data="{ raw: true, showNormalTextarea: false }">
|
||||
<div x-data="{
|
||||
raw: true,
|
||||
showNormalTextarea: false,
|
||||
editorHeight: 400,
|
||||
calculateEditorHeight() {
|
||||
// Get viewport height
|
||||
const viewportHeight = window.innerHeight;
|
||||
// Modal max height is calc(100vh - 2rem) = viewport - 32px
|
||||
const modalMaxHeight = viewportHeight - 32;
|
||||
// Account for: modal header (~80px) + info text (~60px) + checkboxes (~80px) + buttons (~80px) + padding (~48px)
|
||||
const fixedElementsHeight = 348;
|
||||
// Calculate available height for editor
|
||||
const availableHeight = modalMaxHeight - fixedElementsHeight;
|
||||
// Set minimum height of 300px and maximum of available space
|
||||
this.editorHeight = Math.max(300, Math.min(availableHeight, viewportHeight - 200));
|
||||
}
|
||||
}" x-init="calculateEditorHeight(); window.addEventListener('resize', () => calculateEditorHeight())">
|
||||
<div class="pb-4">Volume names are updated upon save. The service UUID will be added as a prefix to all volumes, to
|
||||
prevent
|
||||
name collision. <br>To see the actual volume names, check the Deployable Compose file, or go to Storage
|
||||
menu.</div>
|
||||
|
||||
<div x-cloak x-show="raw" class="font-mono">
|
||||
<div x-cloak x-show="showNormalTextarea">
|
||||
<x-forms.textarea rows="20" id="dockerComposeRaw">
|
||||
<div class="compose-editor-container" x-bind:style="`--editor-height: ${editorHeight}px`">
|
||||
<div x-cloak x-show="raw" class="font-mono">
|
||||
<div x-cloak x-show="showNormalTextarea">
|
||||
<x-forms.textarea x-bind:style="`height: ${editorHeight}px`" id="dockerComposeRaw">
|
||||
</x-forms.textarea>
|
||||
</div>
|
||||
<div x-cloak x-show="!showNormalTextarea">
|
||||
<x-forms.textarea allowTab useMonacoEditor monacoEditorLanguage="yaml" id="dockerComposeRaw">
|
||||
</x-forms.textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div x-cloak x-show="raw === false" class="font-mono">
|
||||
<x-forms.textarea x-bind:style="`height: ${editorHeight}px`" readonly id="dockerCompose">
|
||||
</x-forms.textarea>
|
||||
</div>
|
||||
<div x-cloak x-show="!showNormalTextarea">
|
||||
<x-forms.textarea allowTab useMonacoEditor monacoEditorLanguage="yaml" rows="20"
|
||||
id="dockerComposeRaw">
|
||||
</x-forms.textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div x-cloak x-show="raw === false" class="font-mono">
|
||||
<x-forms.textarea rows="20" readonly id="dockerCompose">
|
||||
</x-forms.textarea>
|
||||
</div>
|
||||
<div class="pt-2 flex gap-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
@@ -46,4 +63,4 @@
|
||||
Save
|
||||
</x-forms.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -5,7 +5,8 @@
|
||||
@if (isDev())
|
||||
<div>{{ $service->compose_parsing_version }}</div>
|
||||
@endif
|
||||
<x-forms.button canGate="update" :canResource="$service" wire:target='submit' type="submit">Save</x-forms.button>
|
||||
<x-forms.button canGate="update" :canResource="$service" wire:target='submit'
|
||||
type="submit">Save</x-forms.button>
|
||||
@can('update', $service)
|
||||
<x-modal-input buttonTitle="Edit Compose File" title="Edit Docker Compose" :closeOutside="false">
|
||||
<livewire:project.service.edit-compose serviceId="{{ $service->id }}" />
|
||||
@@ -15,11 +16,13 @@
|
||||
<div>Configuration</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input canGate="update" :canResource="$service" id="name" required label="Service Name" placeholder="My super WordPress site" />
|
||||
<x-forms.input canGate="update" :canResource="$service" id="name" required label="Service Name"
|
||||
placeholder="My super WordPress site" />
|
||||
<x-forms.input canGate="update" :canResource="$service" id="description" label="Description" />
|
||||
</div>
|
||||
<div class="w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$service" instantSave id="connectToDockerNetwork" label="Connect To Predefined Network"
|
||||
<x-forms.checkbox canGate="update" :canResource="$service" instantSave id="connectToDockerNetwork"
|
||||
label="Connect To Predefined Network"
|
||||
helper="By default, you do not reach the Coolify defined networks.<br>Starting a docker compose based resource will have an internal network. <br>If you connect to a Coolify defined network, you maybe need to use different internal DNS names to connect to a resource.<br><br>For more information, check <a class='underline dark:text-white' target='_blank' href='https://coolify.io/docs/knowledge-base/docker/compose#connect-to-predefined-networks'>this</a>." />
|
||||
</div>
|
||||
@if ($fields->count() > 0)
|
||||
@@ -36,10 +39,11 @@
|
||||
<x-helper helper="Variable name: {{ $serviceName }}" />
|
||||
@endif
|
||||
</div>
|
||||
<x-forms.input canGate="update" :canResource="$service" type="{{ data_get($field, 'isPassword') ? 'password' : 'text' }}"
|
||||
<x-forms.input canGate="update" :canResource="$service"
|
||||
type="{{ data_get($field, 'isPassword') ? 'password' : 'text' }}"
|
||||
required="{{ str(data_get($field, 'rules'))?->contains('required') }}"
|
||||
id="fields.{{ $serviceName }}.value"></x-forms.input>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</form>
|
||||
</form>
|
||||
@@ -23,7 +23,7 @@
|
||||
</div>
|
||||
@can('update', $this->env)
|
||||
<div class="flex flex-col w-full gap-3">
|
||||
<div class="flex w-full items-center gap-4 overflow-x-auto whitespace-nowrap">
|
||||
<div class="flex flex-wrap w-full items-center gap-4">
|
||||
@if (!$is_redis_credential)
|
||||
@if ($type === 'service')
|
||||
<x-forms.checkbox instantSave id="is_buildtime"
|
||||
@@ -69,7 +69,7 @@
|
||||
</div>
|
||||
@else
|
||||
<div class="flex flex-col w-full gap-3">
|
||||
<div class="flex w-full items-center gap-4 overflow-x-auto whitespace-nowrap">
|
||||
<div class="flex flex-wrap w-full items-center gap-4">
|
||||
@if (!$is_redis_credential)
|
||||
@if ($type === 'service')
|
||||
<x-forms.checkbox disabled id="is_buildtime"
|
||||
@@ -145,7 +145,7 @@
|
||||
@endcan
|
||||
@can('update', $this->env)
|
||||
<div class="flex flex-col w-full gap-3">
|
||||
<div class="flex w-full items-center gap-4 overflow-x-auto whitespace-nowrap">
|
||||
<div class="flex flex-wrap w-full items-center gap-4">
|
||||
@if (!$is_redis_credential)
|
||||
@if ($type === 'service')
|
||||
<x-forms.checkbox instantSave id="is_buildtime"
|
||||
@@ -213,7 +213,7 @@
|
||||
</div>
|
||||
@else
|
||||
<div class="flex flex-col w-full gap-3">
|
||||
<div class="flex w-full items-center gap-4 overflow-x-auto whitespace-nowrap">
|
||||
<div class="flex flex-wrap w-full items-center gap-4">
|
||||
@if (!$is_redis_credential)
|
||||
@if ($type === 'service')
|
||||
<x-forms.checkbox disabled id="is_buildtime"
|
||||
|
||||
@@ -34,7 +34,9 @@
|
||||
}
|
||||
}">
|
||||
<div class="flex gap-2 items-center">
|
||||
@if ($resource?->type() === 'application' || str($resource?->type())->startsWith('standalone'))
|
||||
@if ($displayName)
|
||||
<h4>{{ $displayName }}</h4>
|
||||
@elseif ($resource?->type() === 'application' || str($resource?->type())->startsWith('standalone'))
|
||||
<h4>{{ $container }}</h4>
|
||||
@else
|
||||
<h4>{{ str($container)->beforeLast('-')->headline() }}</h4>
|
||||
@@ -46,17 +48,19 @@
|
||||
<x-loading wire:poll.2000ms='getLogs(true)' />
|
||||
@endif
|
||||
</div>
|
||||
<form wire:submit='getLogs(true)' class="flex gap-2 items-end">
|
||||
<div class="w-96">
|
||||
<form wire:submit='getLogs(true)' class="flex flex-col gap-4">
|
||||
<div class="w-full sm:w-96">
|
||||
<x-forms.input label="Only Show Number of Lines" placeholder="100" type="number" required
|
||||
id="numberOfLines" :readonly="$streamLogs"></x-forms.input>
|
||||
</div>
|
||||
<x-forms.button type="submit">Refresh</x-forms.button>
|
||||
<x-forms.checkbox instantSave label="Stream Logs" id="streamLogs"></x-forms.checkbox>
|
||||
<x-forms.checkbox instantSave label="Include Timestamps" id="showTimeStamps"></x-forms.checkbox>
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:flex-wrap sm:gap-2 sm:items-center">
|
||||
<x-forms.button type="submit">Refresh</x-forms.button>
|
||||
<x-forms.checkbox instantSave label="Stream Logs" id="streamLogs"></x-forms.checkbox>
|
||||
<x-forms.checkbox instantSave label="Include Timestamps" id="showTimeStamps"></x-forms.checkbox>
|
||||
</div>
|
||||
</form>
|
||||
<div :class="fullscreen ? 'fullscreen' : 'relative w-full py-4 mx-auto'">
|
||||
<div class="flex overflow-y-auto flex-col-reverse px-4 py-2 w-full bg-white dark:text-white dark:bg-coolgray-100 scrollbar dark:border-coolgray-300 border-neutral-200"
|
||||
<div class="flex overflow-y-auto overflow-x-hidden flex-col-reverse px-4 py-2 w-full min-w-0 bg-white dark:text-white dark:bg-coolgray-100 scrollbar dark:border-coolgray-300 border-neutral-200"
|
||||
:class="fullscreen ? '' : 'max-h-96 border border-solid rounded-sm'">
|
||||
<div :class="fullscreen ? 'fixed top-4 right-4' : 'absolute top-6 right-0'">
|
||||
<div class="flex justify-end gap-4" :class="fullscreen ? 'fixed' : ''"
|
||||
@@ -98,9 +102,9 @@
|
||||
</div>
|
||||
</div>
|
||||
@if ($outputs)
|
||||
<pre id="logs" class="font-mono whitespace-pre-wrap">{{ $outputs }}</pre>
|
||||
<pre id="logs" class="font-mono whitespace-pre-wrap break-all max-w-full">{{ $outputs }}</pre>
|
||||
@else
|
||||
<pre id="logs" class="font-mono whitespace-pre-wrap">Refresh to get the logs...</pre>
|
||||
<pre id="logs" class="font-mono whitespace-pre-wrap break-all max-w-full">Refresh to get the logs...</pre>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
submitAction="saveCaCertificate" :actions="[
|
||||
'This will overwrite the existing CA certificate at /data/coolify/ssl/coolify-ca.crt with your custom CA certificate.',
|
||||
'This will regenerate all SSL certificates for databases on this server and it will sign them with your custom CA.',
|
||||
'You must manually redeploy all your databases on this server so that they use the new SSL certificates singned with your new CA certificate.',
|
||||
'You must manually redeploy all your databases on this server so that they use the new SSL certificates signed with your new CA certificate.',
|
||||
'Because of caching, you probably also need to redeploy all your resources on this server that are using this CA certificate.',
|
||||
]"
|
||||
confirmationText="/data/coolify/ssl/coolify-ca.crt" shortConfirmationLabel="CA Certificate Path"
|
||||
@@ -24,7 +24,7 @@
|
||||
submitAction="regenerateCaCertificate" :actions="[
|
||||
'This will generate a new CA certificate at /data/coolify/ssl/coolify-ca.crt and replace the existing one.',
|
||||
'This will regenerate all SSL certificates for databases on this server and it will sign them with the new CA certificate.',
|
||||
'You must manually redeploy all your databases on this server so that they use the new SSL certificates singned with the new CA certificate.',
|
||||
'You must manually redeploy all your databases on this server so that they use the new SSL certificates signed with the new CA certificate.',
|
||||
'Because of caching, you probably also need to redeploy all your resources on this server that are using this CA certificate.',
|
||||
]"
|
||||
confirmationText="/data/coolify/ssl/coolify-ca.crt" shortConfirmationLabel="CA Certificate Path"
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
|
||||
<div>
|
||||
<x-forms.select label="Server Type" id="selected_server_type" wire:model.live="selected_server_type"
|
||||
helper="Learn more about <a class='inline-block underline dark:text-white' href='https://www.hetzner.com/cloud/' target='_blank'>Hetzner server types</a>"
|
||||
required :disabled="!$selected_location">
|
||||
<option value="">
|
||||
{{ $selected_location ? 'Select a server type...' : 'Select a location first' }}
|
||||
|
||||
@@ -31,45 +31,9 @@
|
||||
helper="Build servers are used to build your applications, so you cannot deploy applications to it."
|
||||
label="Use it as a build server?" />
|
||||
</div>
|
||||
<div class="">
|
||||
<h3 class="pt-6">Swarm <span class="text-xs text-neutral-500">(experimental)</span></h3>
|
||||
<div class="pb-4">Read the docs <a class='underline dark:text-white'
|
||||
href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>.</div>
|
||||
@if ($is_swarm_worker || $is_build_server)
|
||||
<x-forms.checkbox disabled instantSave type="checkbox" id="is_swarm_manager"
|
||||
helper="For more information, please read the documentation <a class='dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>."
|
||||
label="Is it a Swarm Manager?" />
|
||||
@else
|
||||
<x-forms.checkbox type="checkbox" instantSave id="is_swarm_manager"
|
||||
helper="For more information, please read the documentation <a class='dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>."
|
||||
label="Is it a Swarm Manager?" />
|
||||
@endif
|
||||
@if ($is_swarm_manager || $is_build_server)
|
||||
<x-forms.checkbox disabled instantSave type="checkbox" id="is_swarm_worker"
|
||||
helper="For more information, please read the documentation <a class='dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>."
|
||||
label="Is it a Swarm Worker?" />
|
||||
@else
|
||||
<x-forms.checkbox type="checkbox" instantSave id="is_swarm_worker"
|
||||
helper="For more information, please read the documentation <a class='dark:text-white' href='https://coolify.io/docs/knowledge-base/docker/swarm' target='_blank'>here</a>."
|
||||
label="Is it a Swarm Worker?" />
|
||||
@endif
|
||||
@if ($is_swarm_worker && count($swarm_managers) > 0)
|
||||
<div class="py-4">
|
||||
<x-forms.select label="Select a Swarm Cluster" id="selected_swarm_cluster" required>
|
||||
@foreach ($swarm_managers as $server)
|
||||
@if ($loop->first)
|
||||
<option selected value="{{ $server->id }}">{{ $server->name }}</option>
|
||||
@else
|
||||
<option value="{{ $server->id }}">{{ $server->name }}</option>
|
||||
@endif
|
||||
@endforeach
|
||||
</x-forms.select>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<x-forms.button type="submit">
|
||||
Continue
|
||||
</x-forms.button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -7,7 +7,7 @@
|
||||
<x-server.sidebar-proxy :server="$server" :parameters="$parameters" />
|
||||
<div class="w-full">
|
||||
<h2 class="pb-4">Logs</h2>
|
||||
<livewire:project.shared.get-logs :server="$server" container="coolify-proxy" />
|
||||
<livewire:project.shared.get-logs :server="$server" container="coolify-proxy" displayName="Coolify Proxy" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -56,48 +56,53 @@
|
||||
step2ButtonText="Update All
|
||||
Packages" />
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Package</th>
|
||||
@if ($packageManager !== 'dnf')
|
||||
<th>Current Version</th>
|
||||
@endif
|
||||
<th>New Version</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($updates as $update)
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="inline-flex gap-2 justify-center items-center">
|
||||
@if (data_get_str($update, 'package')->contains('docker') || data_get_str($update, 'package')->contains('kernel'))
|
||||
<x-helper :helper="'This package will restart your currently running containers'">
|
||||
<x-slot:icon>
|
||||
<svg class="w-4 h-4 text-red-500 block"
|
||||
viewBox="0 0 256 256"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="currentColor"
|
||||
d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16">
|
||||
</path>
|
||||
</svg>
|
||||
</x-slot:icon>
|
||||
</x-helper>
|
||||
@endif
|
||||
{{ data_get($update, 'package') }}
|
||||
</td>
|
||||
@if ($packageManager !== 'dnf')
|
||||
<td>{{ data_get($update, 'current_version') }}</td>
|
||||
@endif
|
||||
<td>{{ data_get($update, 'new_version') }}</td>
|
||||
<td>
|
||||
<x-forms.button type="button"
|
||||
wire:click="$dispatch('updatePackage', { package: '{{ data_get($update, 'package') }}' })">Update</x-forms.button>
|
||||
</td>
|
||||
<th>Package</th>
|
||||
<th>Version</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($updates as $update)
|
||||
<tr>
|
||||
<td>
|
||||
<div class="flex gap-2 items-center">
|
||||
@if (data_get_str($update, 'package')->contains('docker') || data_get_str($update, 'package')->contains('kernel'))
|
||||
<x-helper :helper="'This package will restart your currently running containers'">
|
||||
<x-slot:icon>
|
||||
<svg class="w-4 h-4 text-red-500 block flex-shrink-0"
|
||||
viewBox="0 0 256 256"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="currentColor"
|
||||
d="M240.26 186.1L152.81 34.23a28.74 28.74 0 0 0-49.62 0L15.74 186.1a27.45 27.45 0 0 0 0 27.71A28.31 28.31 0 0 0 40.55 228h174.9a28.31 28.31 0 0 0 24.79-14.19a27.45 27.45 0 0 0 .02-27.71m-20.8 15.7a4.46 4.46 0 0 1-4 2.2H40.55a4.46 4.46 0 0 1-4-2.2a3.56 3.56 0 0 1 0-3.73L124 46.2a4.77 4.77 0 0 1 8 0l87.44 151.87a3.56 3.56 0 0 1 .02 3.73M116 136v-32a12 12 0 0 1 24 0v32a12 12 0 0 1-24 0m28 40a16 16 0 1 1-16-16a16 16 0 0 1 16 16">
|
||||
</path>
|
||||
</svg>
|
||||
</x-slot:icon>
|
||||
</x-helper>
|
||||
@endif
|
||||
<span class="break-all">{{ data_get($update, 'package') }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="whitespace-nowrap">
|
||||
<div class="flex gap-1 items-center">
|
||||
<span>{{ data_get($update, 'new_version') }}</span>
|
||||
@if ($packageManager !== 'dnf' && data_get($update, 'current_version'))
|
||||
<x-helper helper="Current: {{ data_get($update, 'current_version') }}" />
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
<td class="whitespace-nowrap">
|
||||
<x-forms.button type="button"
|
||||
wire:click="$dispatch('updatePackage', { package: '{{ data_get($update, 'package') }}' })">Update</x-forms.button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -337,7 +337,7 @@
|
||||
<x-slot:title>Sentinel Logs</x-slot:title>
|
||||
<x-slot:content>
|
||||
<livewire:project.shared.get-logs :server="$server"
|
||||
container="coolify-sentinel" lazy />
|
||||
container="coolify-sentinel" displayName="Sentinel" lazy />
|
||||
</x-slot:content>
|
||||
<x-forms.button @click="slideOverOpen=true"
|
||||
:disabled="$isValidating">Logs</x-forms.button>
|
||||
@@ -353,7 +353,7 @@
|
||||
<x-slot:title>Sentinel Logs</x-slot:title>
|
||||
<x-slot:content>
|
||||
<livewire:project.shared.get-logs :server="$server"
|
||||
container="coolify-sentinel" lazy />
|
||||
container="coolify-sentinel" displayName="Sentinel" lazy />
|
||||
</x-slot:content>
|
||||
<x-forms.button @click="slideOverOpen=true"
|
||||
:disabled="$isValidating">Logs</x-forms.button>
|
||||
|
||||
@@ -49,21 +49,30 @@
|
||||
localStorage.setItem('theme', userSettings);
|
||||
|
||||
const themeMetaTag = document.querySelector('meta[name=theme-color]');
|
||||
let isDark = false;
|
||||
|
||||
if (userSettings === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
themeMetaTag.setAttribute('content', this.darkColorContent);
|
||||
this.theme = 'dark';
|
||||
isDark = true;
|
||||
} else if (userSettings === 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
themeMetaTag.setAttribute('content', this.whiteColorContent);
|
||||
this.theme = 'light';
|
||||
} else if (darkModePreference) {
|
||||
isDark = false;
|
||||
} else if (userSettings === 'system') {
|
||||
this.theme = 'system';
|
||||
document.documentElement.classList.add('dark');
|
||||
} else if (!darkModePreference) {
|
||||
this.theme = 'system';
|
||||
document.documentElement.classList.remove('dark');
|
||||
if (darkModePreference) {
|
||||
document.documentElement.classList.add('dark');
|
||||
isDark = true;
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
isDark = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Update theme-color meta tag
|
||||
if (themeMetaTag) {
|
||||
themeMetaTag.setAttribute('content', isDark ? '#101010' : '#ffffff');
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -16,33 +16,33 @@
|
||||
<div class="flex flex-col gap-2 pt-4">
|
||||
@foreach ($oauth_settings_map as $oauth_setting)
|
||||
<div class="p-4 border dark:border-coolgray-300 border-neutral-200">
|
||||
<h3>{{ ucfirst($oauth_setting->provider) }}</h3>
|
||||
<h3>{{ ucfirst($oauth_setting['provider']) }}</h3>
|
||||
<div class="w-32">
|
||||
<x-forms.checkbox instantSave="instantSave('{{ $oauth_setting->provider }}')"
|
||||
id="oauth_settings_map.{{ $oauth_setting->provider }}.enabled" label="Enabled" />
|
||||
<x-forms.checkbox instantSave="instantSave('{{ $oauth_setting['provider'] }}')"
|
||||
id="oauth_settings_map.{{ $oauth_setting['provider'] }}.enabled" label="Enabled" />
|
||||
</div>
|
||||
<div class="flex flex-col w-full gap-2 xl:flex-row">
|
||||
<x-forms.input id="oauth_settings_map.{{ $oauth_setting->provider }}.client_id"
|
||||
<x-forms.input id="oauth_settings_map.{{ $oauth_setting['provider'] }}.client_id"
|
||||
label="Client ID" />
|
||||
<x-forms.input id="oauth_settings_map.{{ $oauth_setting->provider }}.client_secret"
|
||||
<x-forms.input id="oauth_settings_map.{{ $oauth_setting['provider'] }}.client_secret"
|
||||
type="password" label="Client Secret" autocomplete="new-password" />
|
||||
<x-forms.input id="oauth_settings_map.{{ $oauth_setting->provider }}.redirect_uri"
|
||||
placeholder="{{ route('auth.callback', $oauth_setting->provider) }}" label="Redirect URI" />
|
||||
@if ($oauth_setting->provider == 'azure')
|
||||
<x-forms.input id="oauth_settings_map.{{ $oauth_setting->provider }}.tenant"
|
||||
<x-forms.input id="oauth_settings_map.{{ $oauth_setting['provider'] }}.redirect_uri"
|
||||
placeholder="{{ route('auth.callback', $oauth_setting['provider']) }}" label="Redirect URI" />
|
||||
@if ($oauth_setting['provider'] == 'azure')
|
||||
<x-forms.input id="oauth_settings_map.{{ $oauth_setting['provider'] }}.tenant"
|
||||
label="Tenant" />
|
||||
@endif
|
||||
@if ($oauth_setting->provider == 'google')
|
||||
<x-forms.input id="oauth_settings_map.{{ $oauth_setting->provider }}.tenant"
|
||||
@if ($oauth_setting['provider'] == 'google')
|
||||
<x-forms.input id="oauth_settings_map.{{ $oauth_setting['provider'] }}.tenant"
|
||||
helper="Optional parameter that supplies a hosted domain (HD) to Google, which<br>triggers a login hint to be displayed on the OAuth screen with this domain.<br><br><a class='underline dark:text-warning text-coollabs' href='https://developers.google.com/identity/openid-connect/openid-connect#hd-param' target='_blank'>Google Documentation</a>"
|
||||
label="Tenant" />
|
||||
@endif
|
||||
@if (
|
||||
$oauth_setting->provider == 'authentik' ||
|
||||
$oauth_setting->provider == 'clerk' ||
|
||||
$oauth_setting->provider == 'zitadel' ||
|
||||
$oauth_setting->provider == 'gitlab')
|
||||
<x-forms.input id="oauth_settings_map.{{ $oauth_setting->provider }}.base_url"
|
||||
$oauth_setting['provider'] == 'authentik' ||
|
||||
$oauth_setting['provider'] == 'clerk' ||
|
||||
$oauth_setting['provider'] == 'zitadel' ||
|
||||
$oauth_setting['provider'] == 'gitlab')
|
||||
<x-forms.input id="oauth_settings_map.{{ $oauth_setting['provider'] }}.base_url"
|
||||
label="Base URL" />
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -1,77 +1,93 @@
|
||||
<div>
|
||||
<x-slot:title>
|
||||
Advanced Settings | Coolify
|
||||
</x-slot>
|
||||
<x-settings.navbar />
|
||||
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }" class="flex flex-col h-full gap-8 sm:flex-row">
|
||||
<x-settings.sidebar activeMenu="advanced" />
|
||||
<form wire:submit='submit' class="flex flex-col">
|
||||
<div class="flex items-center gap-2">
|
||||
<h2>Advanced</h2>
|
||||
<x-forms.button type="submit">
|
||||
Save
|
||||
</x-forms.button>
|
||||
</div>
|
||||
<div class="pb-4">Advanced settings for your Coolify instance.</div>
|
||||
|
||||
<div class="flex flex-col gap-1 md:w-96">
|
||||
<x-forms.checkbox instantSave id="is_registration_enabled"
|
||||
helper="If enabled, users can register themselves. If disabled, only administrators can create new users."
|
||||
label="Registration Allowed" />
|
||||
<x-forms.checkbox instantSave id="do_not_track"
|
||||
helper="If enabled, Coolify will not track any data. This is useful if you are concerned about privacy."
|
||||
label="Do Not Track" />
|
||||
<h4 class="pt-4">DNS Settings</h4>
|
||||
<x-forms.checkbox instantSave id="is_dns_validation_enabled"
|
||||
helper="If you set a custom domain, Coolify will validate the domain in your DNS provider."
|
||||
label="DNS Validation" />
|
||||
<x-forms.input id="custom_dns_servers" label="Custom DNS Servers"
|
||||
helper="DNS servers to validate domains against. A comma separated list of DNS servers."
|
||||
placeholder="1.1.1.1,8.8.8.8" />
|
||||
<h4 class="pt-4">API Settings</h4>
|
||||
<x-forms.checkbox instantSave id="is_api_enabled" label="API Access"
|
||||
helper="If enabled, the API will be enabled. If disabled, the API will be disabled." />
|
||||
<x-forms.input id="allowed_ips" label="Allowed IPs for API Access"
|
||||
helper="Allowed IP addresses or subnets for API access.<br>Supports single IPs (192.168.1.100) and CIDR notation (192.168.1.0/24).<br>Use comma to separate multiple entries.<br>Use 0.0.0.0 or leave empty to allow from anywhere."
|
||||
placeholder="192.168.1.100,10.0.0.0/8,203.0.113.0/24" />
|
||||
<h4 class="pt-4">Confirmation Settings</h4>
|
||||
<div class="md:w-96 pb-1">
|
||||
<x-forms.checkbox instantSave id="is_sponsorship_popup_enabled" label="Show Sponsorship Popup"
|
||||
helper="When enabled, sponsorship popups will be shown monthly to users. When disabled, the sponsorship popup will be permanently hidden for all users." />
|
||||
</x-slot>
|
||||
<x-settings.navbar />
|
||||
<div x-data="{ activeTab: window.location.hash ? window.location.hash.substring(1) : 'general' }"
|
||||
class="flex flex-col h-full gap-8 sm:flex-row">
|
||||
<x-settings.sidebar activeMenu="advanced" />
|
||||
<form wire:submit='submit' class="flex flex-col w-full">
|
||||
<div class="flex items-center gap-2">
|
||||
<h2>Advanced</h2>
|
||||
<x-forms.button type="submit">
|
||||
Save
|
||||
</x-forms.button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
@if ($disable_two_step_confirmation)
|
||||
<div class="md:w-96 pb-4" wire:key="two-step-confirmation-enabled">
|
||||
<x-forms.checkbox instantSave id="disable_two_step_confirmation"
|
||||
label="Disable Two Step Confirmation"
|
||||
helper="When disabled, you will not need to confirm actions with a text and user password. This significantly reduces security and may lead to accidental deletions or unwanted changes. Use with extreme caution, especially on production servers." />
|
||||
<div class="pb-4">Advanced settings for your Coolify instance.</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="md:w-96">
|
||||
<x-forms.checkbox instantSave id="is_registration_enabled"
|
||||
helper="If enabled, users can register themselves. If disabled, only administrators can create new users."
|
||||
label="Registration Allowed" />
|
||||
</div>
|
||||
@else
|
||||
<div class="md:w-96 pb-4 flex items-center justify-between gap-2"
|
||||
wire:key="two-step-confirmation-disabled">
|
||||
<label class="flex items-center gap-2">
|
||||
Disable Two Step Confirmation
|
||||
<x-helper
|
||||
helper="When disabled, you will not need to confirm actions with a text and user password. This significantly reduces security and may lead to accidental deletions or unwanted changes. Use with extreme caution, especially on production servers.">
|
||||
</x-helper>
|
||||
</label>
|
||||
<x-modal-confirmation title="Disable Two Step Confirmation?" buttonTitle="Disable" isErrorButton
|
||||
submitAction="toggleTwoStepConfirmation" :actions="[
|
||||
'Two Step confirmation will be disabled globally.',
|
||||
'Disabling two step confirmation reduces security (as anyone can easily delete anything).',
|
||||
'The risk of accidental actions will increase.',
|
||||
]"
|
||||
confirmationText="DISABLE TWO STEP CONFIRMATION"
|
||||
confirmationLabel="Please type the confirmation text to disable two step confirmation."
|
||||
shortConfirmationLabel="Confirmation text" />
|
||||
<div class="md:w-96">
|
||||
<x-forms.checkbox instantSave id="do_not_track"
|
||||
helper="If enabled, Coolify will not track any data. This is useful if you are concerned about privacy."
|
||||
label="Do Not Track" />
|
||||
</div>
|
||||
<x-callout type="danger" title="Warning!" class="mb-4">
|
||||
Disabling two step confirmation reduces security (as anyone can easily delete anything) and
|
||||
increases the risk of accidental actions. This is not recommended for production servers.
|
||||
</x-callout>
|
||||
@endif
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<h4 class="pt-4">DNS Settings</h4>
|
||||
<div class="md:w-96">
|
||||
<x-forms.checkbox instantSave id="is_dns_validation_enabled"
|
||||
helper="If you set a custom domain, Coolify will validate the domain in your DNS provider."
|
||||
label="DNS Validation" />
|
||||
</div>
|
||||
|
||||
<x-forms.input id="custom_dns_servers" label="Custom DNS Servers"
|
||||
helper="DNS servers to validate domains against. A comma separated list of DNS servers."
|
||||
placeholder="1.1.1.1,8.8.8.8" />
|
||||
<h4 class="pt-4">API Settings</h4>
|
||||
<div class="md:w-96">
|
||||
<x-forms.checkbox instantSave id="is_api_enabled" label="API Access"
|
||||
helper="If enabled, the API will be enabled. If disabled, the API will be disabled." />
|
||||
</div>
|
||||
<x-forms.input id="allowed_ips" label="Allowed IPs for API Access"
|
||||
helper="Allowed IP addresses or subnets for API access.<br>Supports single IPs (192.168.1.100) and CIDR notation (192.168.1.0/24).<br>Use comma to separate multiple entries.<br>Use 0.0.0.0 or leave empty to allow from anywhere."
|
||||
placeholder="192.168.1.100,10.0.0.0/8,203.0.113.0/24" />
|
||||
@if (empty($allowed_ips) || in_array('0.0.0.0', array_map('trim', explode(',', $allowed_ips ?? ''))))
|
||||
<x-callout type="warning" title="Warning" class="mt-2">
|
||||
Using 0.0.0.0 (or empty) allows API access from anywhere. This is not recommended for production
|
||||
environments!
|
||||
</x-callout>
|
||||
@endif
|
||||
<h4 class="pt-4">Confirmation Settings</h4>
|
||||
<div class="md:w-96">
|
||||
<x-forms.checkbox instantSave id=" is_sponsorship_popup_enabled" label="Show Sponsorship Popup"
|
||||
helper="When enabled, sponsorship popups will be shown monthly to users. When disabled, the sponsorship popup will be permanently hidden for all users." />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
@if ($disable_two_step_confirmation)
|
||||
<div class="pb-4 md:w-96" wire:key="two-step-confirmation-enabled">
|
||||
<x-forms.checkbox instantSave id="disable_two_step_confirmation"
|
||||
label="Disable Two Step Confirmation"
|
||||
helper="When disabled, you will not need to confirm actions with a text and user password. This significantly reduces security and may lead to accidental deletions or unwanted changes. Use with extreme caution, especially on production servers." />
|
||||
</div>
|
||||
@else
|
||||
<div class="pb-4 flex items-center justify-between gap-2 md:w-96"
|
||||
wire:key="two-step-confirmation-disabled">
|
||||
<label class="flex items-center gap-2">
|
||||
Disable Two Step Confirmation
|
||||
<x-helper
|
||||
helper="When disabled, you will not need to confirm actions with a text and user password. This significantly reduces security and may lead to accidental deletions or unwanted changes. Use with extreme caution, especially on production servers.">
|
||||
</x-helper>
|
||||
</label>
|
||||
<x-modal-confirmation title="Disable Two Step Confirmation?" buttonTitle="Disable" isErrorButton
|
||||
submitAction="toggleTwoStepConfirmation" :actions="[
|
||||
'Two Step confirmation will be disabled globally.',
|
||||
'Disabling two step confirmation reduces security (as anyone can easily delete anything).',
|
||||
'The risk of accidental actions will increase.',
|
||||
]"
|
||||
confirmationText="DISABLE TWO STEP CONFIRMATION"
|
||||
confirmationLabel="Please type the confirmation text to disable two step confirmation."
|
||||
shortConfirmationLabel="Confirmation text" />
|
||||
</div>
|
||||
<x-callout type="danger" title="Warning!" class="mb-4">
|
||||
Disabling two step confirmation reduces security (as anyone can easily delete anything) and
|
||||
increases the risk of accidental actions. This is not recommended for production servers.
|
||||
</x-callout>
|
||||
@endif
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,7 +1,7 @@
|
||||
<div>
|
||||
@if (data_get($github_app, 'app_id'))
|
||||
<form wire:submit='submit'>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center gap-2">
|
||||
<h1>GitHub App</h1>
|
||||
<div class="flex gap-2">
|
||||
@if (data_get($github_app, 'installation_id'))
|
||||
@@ -40,8 +40,8 @@
|
||||
</a>
|
||||
@else
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex gap-2">
|
||||
<div class="flex items-end gap-2 w-full">
|
||||
<div class="flex flex-col sm:flex-row gap-2">
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-end gap-2 w-full">
|
||||
<x-forms.input canGate="update" :canResource="$github_app" id="name" label="App Name" />
|
||||
<x-forms.button canGate="update" :canResource="$github_app" wire:click.prevent="updateGithubAppName">
|
||||
Sync Name
|
||||
@@ -72,24 +72,29 @@
|
||||
helper="If checked, this GitHub App will be available for everyone in this Coolify instance."
|
||||
instantSave id="isSystemWide" />
|
||||
</div>
|
||||
@if ($isSystemWide)
|
||||
<x-callout type="warning" title="Not Recommended">
|
||||
System-wide GitHub Apps are shared across all teams on this Coolify instance. This means any team can use this GitHub App to deploy applications from your repositories. For better security and isolation, it's recommended to create team-specific GitHub Apps instead.
|
||||
</x-callout>
|
||||
@endif
|
||||
@endif
|
||||
<div class="flex gap-2">
|
||||
<div class="flex flex-col sm:flex-row gap-2">
|
||||
<x-forms.input canGate="update" :canResource="$github_app" id="htmlUrl" label="HTML Url" />
|
||||
<x-forms.input canGate="update" :canResource="$github_app" id="apiUrl" label="API Url" />
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="flex flex-col sm:flex-row gap-2">
|
||||
<x-forms.input canGate="update" :canResource="$github_app" id="customUser" label="User"
|
||||
required />
|
||||
<x-forms.input canGate="update" :canResource="$github_app" type="number" id="customPort"
|
||||
label="Port" required />
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="flex flex-col sm:flex-row gap-2">
|
||||
<x-forms.input canGate="update" :canResource="$github_app" type="number" id="appId"
|
||||
label="App Id" required />
|
||||
<x-forms.input canGate="update" :canResource="$github_app" type="number"
|
||||
id="installationId" label="Installation Id" required />
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="flex flex-col sm:flex-row gap-2">
|
||||
<x-forms.input canGate="update" :canResource="$github_app" id="clientId" label="Client Id"
|
||||
type="password" required />
|
||||
<x-forms.input canGate="update" :canResource="$github_app" id="clientSecret"
|
||||
@@ -108,7 +113,7 @@
|
||||
@endforeach
|
||||
</x-forms.select>
|
||||
</div>
|
||||
<div class="flex items-end gap-2 ">
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-end gap-2">
|
||||
<h2 class="pt-4">Permissions</h2>
|
||||
@can('view', $github_app)
|
||||
<x-forms.button wire:click.prevent="checkPermissions">Refetch</x-forms.button>
|
||||
@@ -120,7 +125,7 @@
|
||||
</a>
|
||||
@endcan
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="flex flex-col sm:flex-row gap-2">
|
||||
<x-forms.input id="contents" helper="read - mandatory." label="Content" readonly
|
||||
placeholder="N/A" />
|
||||
<x-forms.input id="metadata" helper="read - mandatory." label="Metadata" readonly
|
||||
@@ -144,56 +149,61 @@
|
||||
</div>
|
||||
<div class="pb-4 title">Here you can find all resources that are using this source.</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
@if ($applications->isEmpty())
|
||||
<div class="py-4 text-sm opacity-70">
|
||||
No resources are currently using this GitHub App.
|
||||
</div>
|
||||
@else
|
||||
<div class="flex flex-col">
|
||||
<div class="overflow-x-auto">
|
||||
<div class="inline-block min-w-full">
|
||||
<div class="overflow-hidden">
|
||||
<table class="min-w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-5 py-3 text-xs font-medium text-left uppercase">
|
||||
Project
|
||||
</th>
|
||||
<th class="px-5 py-3 text-xs font-medium text-left uppercase">
|
||||
Environment</th>
|
||||
<th class="px-5 py-3 text-xs font-medium text-left uppercase">Name
|
||||
</th>
|
||||
<th class="px-5 py-3 text-xs font-medium text-left uppercase">Type
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y">
|
||||
@forelse ($applications->sortBy('name',SORT_NATURAL) as $resource)
|
||||
<div class="flex flex-col">
|
||||
<div class="overflow-x-auto">
|
||||
<div class="inline-block min-w-full">
|
||||
<div class="overflow-hidden">
|
||||
<table class="min-w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="px-5 py-4 text-sm whitespace-nowrap">
|
||||
{{ data_get($resource->project(), 'name') }}
|
||||
</td>
|
||||
<td class="px-5 py-4 text-sm whitespace-nowrap">
|
||||
{{ data_get($resource, 'environment.name') }}
|
||||
</td>
|
||||
<td class="px-5 py-4 text-sm whitespace-nowrap"><a
|
||||
class=""
|
||||
href="{{ $resource->link() }}">{{ $resource->name }}
|
||||
<x-internal-link /></a>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-sm whitespace-nowrap">
|
||||
{{ str($resource->type())->headline() }}</td>
|
||||
<th class="px-5 py-3 text-xs font-medium text-left uppercase">
|
||||
Project
|
||||
</th>
|
||||
<th class="px-5 py-3 text-xs font-medium text-left uppercase">
|
||||
Environment</th>
|
||||
<th class="px-5 py-3 text-xs font-medium text-left uppercase">Name
|
||||
</th>
|
||||
<th class="px-5 py-3 text-xs font-medium text-left uppercase">Type
|
||||
</th>
|
||||
</tr>
|
||||
@empty
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody class="divide-y">
|
||||
@foreach ($applications->sortBy('name',SORT_NATURAL) as $resource)
|
||||
<tr>
|
||||
<td class="px-5 py-4 text-sm whitespace-nowrap">
|
||||
{{ data_get($resource->project(), 'name') }}
|
||||
</td>
|
||||
<td class="px-5 py-4 text-sm whitespace-nowrap">
|
||||
{{ data_get($resource, 'environment.name') }}
|
||||
</td>
|
||||
<td class="px-5 py-4 text-sm whitespace-nowrap"><a
|
||||
class=""
|
||||
href="{{ $resource->link() }}">{{ $resource->name }}
|
||||
<x-internal-link /></a>
|
||||
</td>
|
||||
<td class="px-5 py-4 text-sm whitespace-nowrap">
|
||||
{{ str($resource->type())->headline() }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<div class="flex items-center gap-2 pb-4">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center gap-2 pb-4">
|
||||
<h1>GitHub App</h1>
|
||||
<div class="flex gap-2">
|
||||
@can('delete', $github_app)
|
||||
@@ -228,7 +238,7 @@
|
||||
<div class="pb-10">
|
||||
@can('create', $github_app)
|
||||
@if (!isCloud() || isDev())
|
||||
<div class="flex items-end gap-2">
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-end gap-2">
|
||||
<x-forms.select wire:model.live='webhook_endpoint' label="Webhook Endpoint"
|
||||
helper="All Git webhooks will be sent to this endpoint. <br><br>If you would like to use domain instead of IP address, set your Coolify instance's FQDN in the Settings menu.">
|
||||
@if ($ipv4)
|
||||
@@ -250,7 +260,7 @@
|
||||
</x-forms.button>
|
||||
</div>
|
||||
@else
|
||||
<div class="flex gap-2">
|
||||
<div class="flex flex-col sm:flex-row gap-2">
|
||||
<h2>Register a GitHub App</h2>
|
||||
<x-forms.button isHighlighted
|
||||
x-on:click.prevent="createGithubApp('{{ $webhook_endpoint }}','{{ $preview_deployment_permissions }}',{{ $administration }})">
|
||||
@@ -261,11 +271,11 @@
|
||||
@endif
|
||||
|
||||
<div class="flex flex-col gap-2 pt-4 w-96">
|
||||
<x-forms.checkbox disabled instantSave id="default_permissions" label="Mandatory"
|
||||
<x-forms.checkbox disabled id="default_permissions" label="Mandatory"
|
||||
helper="Contents: read<br>Metadata: read<br>Email: read" />
|
||||
<x-forms.checkbox instantSave id="preview_deployment_permissions" label="Preview Deployments "
|
||||
<x-forms.checkbox id="preview_deployment_permissions" label="Preview Deployments "
|
||||
helper="Necessary for updating pull requests with useful comments (deployment status, links, etc.)<br><br>Pull Request: read & write" />
|
||||
{{-- <x-forms.checkbox instantSave id="administration" label="Administration (for Github Runners)"
|
||||
{{-- <x-forms.checkbox id="administration" label="Administration (for Github Runners)"
|
||||
helper="Necessary for adding Github Runners to repositories.<br><br>Administration: read & write" /> --}}
|
||||
</div>
|
||||
@else
|
||||
|
||||
@@ -9,17 +9,28 @@
|
||||
placeholder="If empty, your GitHub user will be used." id="organization" label="Organization (on GitHub)" />
|
||||
</div>
|
||||
@if (!isCloud())
|
||||
<div class="w-48">
|
||||
<x-forms.checkbox id="is_system_wide" label="System Wide"
|
||||
helper="If checked, this GitHub App will be available for everyone in this Coolify instance." />
|
||||
<div x-data="{ showWarning: @entangle('is_system_wide') }">
|
||||
<div class="w-48">
|
||||
<x-forms.checkbox id="is_system_wide" label="System Wide"
|
||||
helper="If checked, this GitHub App will be available for everyone in this Coolify instance." />
|
||||
</div>
|
||||
<div x-show="showWarning" x-transition x-cloak class="w-full max-w-2xl mx-auto pt-2">
|
||||
<x-callout type="warning" title="Not Recommended">
|
||||
<div class="whitespace-normal break-words">
|
||||
System-wide GitHub Apps are shared across all teams on this Coolify instance. This means any team
|
||||
can use this GitHub App to deploy applications from your repositories. For better security and
|
||||
isolation, it's recommended to create team-specific GitHub Apps instead.
|
||||
</div>
|
||||
</x-callout>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
<div x-data="{
|
||||
activeAccordion: '',
|
||||
setActiveAccordion(id) {
|
||||
this.activeAccordion = (this.activeAccordion == id) ? '' : id
|
||||
}
|
||||
}" class="relative w-full py-2 mx-auto overflow-hidden text-sm font-normal rounded-md">
|
||||
activeAccordion: '',
|
||||
setActiveAccordion(id) {
|
||||
this.activeAccordion = (this.activeAccordion == id) ? '' : id
|
||||
}
|
||||
}" class="relative w-full py-2 mx-auto overflow-hidden text-sm font-normal rounded-md">
|
||||
<div x-data="{ id: $id('accordion') }" class="cursor-pointer">
|
||||
<button @click="setActiveAccordion(id)"
|
||||
class="flex items-center justify-between w-full px-1 py-2 text-left select-none dark:hover:text-white hover:bg-white/5"
|
||||
@@ -55,4 +66,4 @@
|
||||
<x-callout type="warning" title="Permission Required">
|
||||
You don't have permission to create new GitHub Apps. Please contact your team administrator for access.
|
||||
</x-callout>
|
||||
@endcan
|
||||
@endcan
|
||||
@@ -1,4 +1,4 @@
|
||||
# documentation: https://appwrite.io
|
||||
# documentation: https://appwrite.io/docs
|
||||
# slogan: A backend-as-a-service platform that simplifies the web & mobile app development.
|
||||
# category: backend
|
||||
# tags: backend, backend-as-a-service, platform
|
||||
@@ -139,12 +139,22 @@ services:
|
||||
- _APP_DATABASE_SHARED_NAMESPACE=${_APP_DATABASE_SHARED_NAMESPACE}
|
||||
- _APP_FUNCTIONS_CREATION_ABUSE_LIMIT=${_APP_FUNCTIONS_CREATION_ABUSE_LIMIT}
|
||||
- _APP_CUSTOM_DOMAIN_DENY_LIST=${_APP_CUSTOM_DOMAIN_DENY_LIST}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -fsI http://localhost:80 | head -n 1 | grep -E '^HTTP/.* 3[0-9]{2} ' || exit 1"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
appwrite-console:
|
||||
image: appwrite/console:6.1.28
|
||||
container_name: appwrite-console
|
||||
environment:
|
||||
- SERVICE_URL_APPWRITE=/console
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -fsI http://localhost:80 | head -n 1 | grep -E '^HTTP/.* 3[0-9]{2} ' || exit 1"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
appwrite-realtime:
|
||||
image: appwrite/appwrite:1.7.4
|
||||
@@ -172,6 +182,11 @@ services:
|
||||
- _APP_USAGE_STATS=${_APP_USAGE_STATS:-enabled}
|
||||
- _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG}
|
||||
- _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -s localhost > /dev/null || exit 1"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
appwrite-worker-audits:
|
||||
image: appwrite/appwrite:1.7.4
|
||||
@@ -195,6 +210,12 @@ services:
|
||||
- _APP_DB_PASS=$SERVICE_PASSWORD_MARIADB
|
||||
- _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG}
|
||||
- _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "ps aux | grep -q '[w]orker-audits' || exit 1"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
|
||||
appwrite-worker-webhooks:
|
||||
image: appwrite/appwrite:1.7.4
|
||||
@@ -221,6 +242,11 @@ services:
|
||||
- _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG}
|
||||
- _APP_WEBHOOK_MAX_FAILED_ATTEMPTS=${_APP_WEBHOOK_MAX_FAILED_ATTEMPTS}
|
||||
- _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "ps aux | grep -q '[w]orker-webhooks' || exit 1"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
appwrite-worker-deletes:
|
||||
image: appwrite/appwrite:1.7.4
|
||||
@@ -279,6 +305,11 @@ services:
|
||||
- _APP_EMAIL_CERTIFICATES=${_APP_EMAIL_CERTIFICATES}
|
||||
- _APP_MAINTENANCE_RETENTION_AUDIT=${_APP_MAINTENANCE_RETENTION_AUDIT:-1209600}
|
||||
- _APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE=${_APP_MAINTENANCE_RETENTION_AUDIT_CONSOLE}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "ps aux | grep -q '[w]orker-deletes' || exit 1"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
appwrite-worker-databases:
|
||||
image: appwrite/appwrite:1.7.4
|
||||
@@ -304,6 +335,12 @@ services:
|
||||
- _APP_WORKERS_NUM=${_APP_WORKERS_NUM}
|
||||
- _APP_QUEUE_NAME=${_APP_QUEUE_NAME}
|
||||
- _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "ps aux | grep -q '[w]orker-databases' || exit 1"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
|
||||
appwrite-worker-builds:
|
||||
image: appwrite/appwrite:1.7.4
|
||||
@@ -371,6 +408,11 @@ services:
|
||||
- _APP_DOMAIN_SITES=${_APP_DOMAIN_SITES:-sites.$SERVICE_FQDN_APPWRITE}
|
||||
- _APP_BROWSER_HOST=${_APP_BROWSER_HOST}
|
||||
- _APP_CONSOLE_DOMAIN=${_APP_CONSOLE_DOMAIN}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "ps aux | grep -q '[w]orker-builds' || exit 1"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
appwrite-worker-certificates:
|
||||
image: appwrite/appwrite:1.7.4
|
||||
@@ -405,6 +447,11 @@ services:
|
||||
- _APP_DB_PASS=$SERVICE_PASSWORD_MARIADB
|
||||
- _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG}
|
||||
- _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "ps aux | grep -q '[w]orker-certificates' || exit 1"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
appwrite-worker-functions:
|
||||
image: appwrite/appwrite:1.7.4
|
||||
@@ -442,6 +489,11 @@ services:
|
||||
- _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG}
|
||||
- _APP_LOGGING_PROVIDER=${_APP_LOGGING_PROVIDER}
|
||||
- _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "ps aux | grep -q '[w]orker-functions' || exit 1"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
appwrite-worker-mails:
|
||||
image: appwrite/appwrite:1.7.4
|
||||
@@ -474,6 +526,12 @@ services:
|
||||
- _APP_DOMAIN=${_APP_DOMAIN:-$SERVICE_FQDN_APPWRITE}
|
||||
- _APP_OPTIONS_FORCE_HTTPS=${_APP_OPTIONS_FORCE_HTTPS:-disabled}
|
||||
- _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "ps aux | grep -q '[w]orker-mails' || exit 1"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
|
||||
appwrite-worker-messaging:
|
||||
image: appwrite/appwrite:1.7.4
|
||||
@@ -523,7 +581,12 @@ services:
|
||||
- _APP_STORAGE_WASABI_REGION=${_APP_STORAGE_WASABI_REGION:-eu-central-1}
|
||||
- _APP_STORAGE_WASABI_BUCKET=${_APP_STORAGE_WASABI_BUCKET}
|
||||
- _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "ps aux | grep -q '[w]orker-messaging' || exit 1"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
appwrite-worker-migrations:
|
||||
image: appwrite/appwrite:1.7.4
|
||||
entrypoint: worker-migrations
|
||||
@@ -556,6 +619,7 @@ services:
|
||||
- _APP_MIGRATIONS_FIREBASE_CLIENT_ID=${_APP_MIGRATIONS_FIREBASE_CLIENT_ID}
|
||||
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET=${_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET}
|
||||
- _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
|
||||
exclude_from_hc: true
|
||||
|
||||
appwrite-task-maintenance:
|
||||
image: appwrite/appwrite:1.7.4
|
||||
@@ -593,6 +657,12 @@ services:
|
||||
- _APP_MAINTENANCE_RETENTION_SCHEDULES=${_APP_MAINTENANCE_RETENTION_SCHEDULES:-86400}
|
||||
- _APP_MAINTENANCE_START_TIME=${_APP_MAINTENANCE_START_TIME}
|
||||
- _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "ps aux | grep -q '[m]aintenance' || exit 1"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
|
||||
appwrite-task-stats-resources:
|
||||
image: appwrite/appwrite:1.7.4
|
||||
@@ -618,6 +688,11 @@ services:
|
||||
- _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG}
|
||||
- _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
|
||||
- _APP_STATS_RESOURCES_INTERVAL=${_APP_STATS_RESOURCES_INTERVAL}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "ps aux | grep -q '[s]tats-resources' || exit 1"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
appwrite-worker-stats-resources:
|
||||
image: appwrite/appwrite:1.7.4
|
||||
@@ -642,6 +717,11 @@ services:
|
||||
- _APP_USAGE_STATS=${_APP_USAGE_STATS:-enabled}
|
||||
- _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG}
|
||||
- _APP_STATS_RESOURCES_INTERVAL=${_APP_STATS_RESOURCES_INTERVAL}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "ps aux | grep -q '[w]orker-stats-resources' || exit 1"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
appwrite-worker-stats-usage:
|
||||
image: appwrite/appwrite:1.7.4
|
||||
@@ -667,6 +747,12 @@ services:
|
||||
- _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG}
|
||||
- _APP_USAGE_AGGREGATION_INTERVAL=${_APP_USAGE_AGGREGATION_INTERVAL:-30}
|
||||
- _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "ps aux | grep -q '[w]orker-stats-usage' || exit 1"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
|
||||
appwrite-task-scheduler-functions:
|
||||
image: appwrite/appwrite:1.7.4
|
||||
@@ -689,6 +775,11 @@ services:
|
||||
- _APP_DB_USER=$SERVICE_USER_MARIADB
|
||||
- _APP_DB_PASS=$SERVICE_PASSWORD_MARIADB
|
||||
- _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "ps aux | grep -q '[s]chedule-functi' || exit 1"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
appwrite-task-scheduler-executions:
|
||||
image: appwrite/appwrite:1.7.4
|
||||
@@ -711,6 +802,11 @@ services:
|
||||
- _APP_DB_USER=$SERVICE_USER_MARIADB
|
||||
- _APP_DB_PASS=$SERVICE_PASSWORD_MARIADB
|
||||
- _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "ps aux | grep -q '[s]chedule-execut' || exit 1"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
appwrite-task-scheduler-messages:
|
||||
image: appwrite/appwrite:1.7.4
|
||||
@@ -733,17 +829,33 @@ services:
|
||||
- _APP_DB_USER=$SERVICE_USER_MARIADB
|
||||
- _APP_DB_PASS=$SERVICE_PASSWORD_MARIADB
|
||||
- _APP_DATABASE_SHARED_TABLES=${_APP_DATABASE_SHARED_TABLES}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "ps aux | grep -q '[s]chedule-messag' || exit 1"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
appwrite-assistant:
|
||||
image: appwrite/assistant:0.8.3
|
||||
container_name: appwrite-assistant
|
||||
environment:
|
||||
- _APP_ASSISTANT_OPENAI_API_KEY=${_APP_ASSISTANT_OPENAI_API_KEY}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget --spider -q http://127.0.0.1:3003 || exit 0"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
appwrite-browser:
|
||||
image: appwrite/browser:0.2.4
|
||||
container_name: appwrite-browser
|
||||
hostname: appwrite-browser
|
||||
healthcheck:
|
||||
test: ["CMD", "node", "-e", "import('http').then(http => http.get('http://localhost:3000', res => process.exit(0)).on('error', () => process.exit(1)))"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
|
||||
openruntimes-executor:
|
||||
container_name: openruntimes-executor
|
||||
@@ -805,6 +917,11 @@ services:
|
||||
- MYSQL_PASSWORD=$SERVICE_PASSWORD_MARIADB
|
||||
- MARIADB_AUTO_UPGRADE=1
|
||||
command: 'mysqld --innodb-flush-method=fsync'
|
||||
healthcheck:
|
||||
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
||||
interval: 20s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
|
||||
appwrite-redis:
|
||||
image: redis:7.2.4-alpine
|
||||
@@ -816,19 +933,12 @@ services:
|
||||
--maxmemory-samples 5
|
||||
volumes:
|
||||
- appwrite-redis:/data:rw
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 20s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
|
||||
networks:
|
||||
runtimes:
|
||||
name: runtimes
|
||||
|
||||
volumes:
|
||||
appwrite-mariadb:
|
||||
appwrite-redis:
|
||||
appwrite-cache:
|
||||
appwrite-uploads:
|
||||
appwrite-imports:
|
||||
appwrite-certificates:
|
||||
appwrite-functions:
|
||||
appwrite-sites:
|
||||
appwrite-builds:
|
||||
appwrite-config:
|
||||
name: runtimes
|
||||
@@ -22,7 +22,7 @@ services:
|
||||
retries: 10
|
||||
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
image: ghcr.io/coollabsio/minio:RELEASE.2025-10-15T17-29-55Z # Released on 15 October 2025
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
- MINIO_SERVER_URL=$MINIO_SERVER_URL
|
||||
@@ -32,7 +32,7 @@ services:
|
||||
volumes:
|
||||
- azimutt-minio-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
test: ["CMD", "mc", "ready", "local"]
|
||||
interval: 5s
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
|
||||
@@ -66,7 +66,7 @@ services:
|
||||
start_period: 10s
|
||||
|
||||
minio-service:
|
||||
image: minio/minio
|
||||
image: ghcr.io/coollabsio/minio:RELEASE.2025-10-15T17-29-55Z # Released on 15 October 2025
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
environment:
|
||||
@@ -75,10 +75,10 @@ services:
|
||||
- MINIO_BROWSER=off
|
||||
command: server /data --console-address ":9001"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
interval: 30s
|
||||
test: ["CMD", "mc", "ready", "local"]
|
||||
interval: 5s
|
||||
timeout: 20s
|
||||
retries: 3
|
||||
retries: 10
|
||||
|
||||
proxy-service:
|
||||
image: budibase/proxy
|
||||
|
||||
@@ -86,7 +86,7 @@ services:
|
||||
retries: 5
|
||||
|
||||
minio:
|
||||
image: 'quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z' # Released at 2025-09-07T16-13-09Z
|
||||
image: ghcr.io/coollabsio/minio:RELEASE.2025-10-15T17-29-55Z # Released on 15 October 2025
|
||||
command: 'server /data --console-address ":9001"'
|
||||
environment:
|
||||
- MINIO_SERVER_URL=$MINIO_SERVER_URL
|
||||
@@ -96,11 +96,7 @@ services:
|
||||
volumes:
|
||||
- 'minio-data:/data'
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- mc
|
||||
- ready
|
||||
- local
|
||||
test: ["CMD", "mc", "ready", "local"]
|
||||
interval: 5s
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# documentation: https://doc.evolution-api.com/v1/pt/get-started/introduction
|
||||
# slogan: Evolution API Installation with Postgres and Redis
|
||||
# documentation: https://doc.evolution-api.com/v2/en/get-started/introduction
|
||||
# slogan: Multi-platform messaging (whatsapp and more) integration API
|
||||
# category: backend
|
||||
# tags: evolution-api,evo-api,evolution,whatsapp,api,postgres,redis
|
||||
# logo: svgs/evolution-api.svg
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# documentation: https://www.home-assistant.io/installation/linux#docker-compose
|
||||
# slogan: Open source home automation that puts local control and privacy first.
|
||||
# category: automation
|
||||
# tags: home-automation,iot,smart-home,automation,domotics,mqtt,zigbee,zwave
|
||||
# logo: svgs/home-assistant.svg
|
||||
# port: 8123
|
||||
|
||||
services:
|
||||
homeassistant:
|
||||
image: ghcr.io/home-assistant/home-assistant:2025.10.2
|
||||
environment:
|
||||
- SERVICE_URL_HOMEASSISTANT_8123
|
||||
- TZ=${TZ:-UTC}
|
||||
- DISABLE_JEMALLOC=${DISABLE_JEMALLOC:-false}
|
||||
volumes:
|
||||
- homeassistant-config:/config
|
||||
- /run/dbus:/run/dbus:ro
|
||||
- type: bind
|
||||
source: ./configuration.yaml
|
||||
target: /config/configuration.yaml
|
||||
content: |
|
||||
# Loads default set of integrations. Do not remove.
|
||||
default_config:
|
||||
|
||||
# Configuration for reverse proxy support (required for Coolify)
|
||||
http:
|
||||
use_x_forwarded_for: true
|
||||
trusted_proxies:
|
||||
- 10.0.0.0/8
|
||||
- 172.16.0.0/12
|
||||
- 192.168.0.0/16
|
||||
ip_ban_enabled: true
|
||||
login_attempts_threshold: 5
|
||||
privileged: true
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8123"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
@@ -15,10 +15,15 @@ services:
|
||||
volumes:
|
||||
- huly-db:/data/db
|
||||
minio:
|
||||
image: "minio/minio"
|
||||
image: ghcr.io/coollabsio/minio:RELEASE.2025-10-15T17-29-55Z # Released on 15 October 2025
|
||||
command: server /data --address ":9000" --console-address ":9001"
|
||||
volumes:
|
||||
- huly-files:/data
|
||||
- huly-files:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "mc", "ready", "local"]
|
||||
interval: 5s
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
elastic:
|
||||
image: "elasticsearch:7.14.2"
|
||||
command: |
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# documentation: https://github.com/metatool-ai/metamcp
|
||||
# slogan: MCP Aggregator, Orchestrator, Middleware, Gateway in one app
|
||||
# tags: mcp, ai, sse, aggregator, orchestrator, middleware
|
||||
# category: mcp
|
||||
# logo: svgs/metamcp.png
|
||||
# port: 12008
|
||||
|
||||
services:
|
||||
app:
|
||||
image: ghcr.io/metatool-ai/metamcp:2.4
|
||||
environment:
|
||||
- SERVICE_URL_METAMCP_12008
|
||||
- POSTGRES_HOST=${POSTGRES_HOST:-postgres}
|
||||
- POSTGRES_PORT=${POSTGRES_PORT:-5432}
|
||||
- POSTGRES_USER=${SERVICE_USER_POSTGRES}
|
||||
- POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES}
|
||||
- POSTGRES_DB=${POSTGRES_DB:-metamcp_db}
|
||||
- DATABASE_URL=postgresql://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@${POSTGRES_HOST:-postgres}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-metamcp_db}
|
||||
- APP_URL=${SERVICE_URL_METAMCP}
|
||||
- NEXT_PUBLIC_APP_URL=${SERVICE_URL_METAMCP}
|
||||
- BETTER_AUTH_SECRET=${SERVICE_PASSWORD_AUTH}
|
||||
- TRANSFORM_LOCALHOST_TO_DOCKER_INTERNAL=${TRANSFORM_LOCALHOST_TO_DOCKER_INTERNAL:-true}
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- curl
|
||||
- '-f'
|
||||
- 'http://localhost:12008/health'
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
- POSTGRES_DB=${POSTGRES_DB:-metamcp_db}
|
||||
- POSTGRES_USER=${SERVICE_USER_POSTGRES}
|
||||
- POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD-SHELL
|
||||
- 'pg_isready -U ${SERVICE_USER_POSTGRES} -d ${POSTGRES_DB:-metamcp_db}'
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
@@ -0,0 +1,22 @@
|
||||
# documentation: https://github.com/coollabsio/minio?tab=readme-ov-file#minio-docker-images
|
||||
# slogan: MinIO is a high performance object storage server compatible with Amazon S3 APIs.
|
||||
# category: storage
|
||||
# tags: object, storage, server, s3, api
|
||||
# logo: svgs/minio.svg
|
||||
|
||||
services:
|
||||
minio:
|
||||
image: ghcr.io/coollabsio/minio:RELEASE.2025-10-15T17-29-55Z # Released on 15 October 2025
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
- MINIO_SERVER_URL=$MINIO_SERVER_URL
|
||||
- MINIO_BROWSER_REDIRECT_URL=$MINIO_BROWSER_REDIRECT_URL
|
||||
- MINIO_ROOT_USER=$SERVICE_USER_MINIO
|
||||
- MINIO_ROOT_PASSWORD=$SERVICE_PASSWORD_MINIO
|
||||
volumes:
|
||||
- minio-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "mc", "ready", "local"]
|
||||
interval: 5s
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
@@ -1,3 +1,4 @@
|
||||
# ignore: true
|
||||
# documentation: https://min.io/docs/minio/container/index.html
|
||||
# slogan: MinIO is a high performance object storage server compatible with Amazon S3 APIs.
|
||||
# category: storage
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# ignore: true
|
||||
# documentation: https://github.com/stonith404/pingvin-share
|
||||
# slogan: A self-hosted file sharing platform that combines lightness and beauty, perfect for seamless and efficient file sharing.
|
||||
# category: storage
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# ignore: true
|
||||
# documentation: https://github.com/stonith404/pingvin-share
|
||||
# slogan: A self-hosted file sharing platform that combines lightness and beauty, perfect for seamless and efficient file sharing.
|
||||
# category: storage
|
||||
|
||||
@@ -4,72 +4,77 @@
|
||||
# tags: plane,project-management,tool,open,source,api,nextjs,redis,postgresql,django,pm
|
||||
# logo: svgs/plane.svg
|
||||
|
||||
x-app-env: &app-env
|
||||
environment:
|
||||
- APP_RELEASE=${APP_RELEASE:-v0.25.2}
|
||||
- WEB_URL=${SERVICE_URL_PLANE}
|
||||
- DEBUG=${DEBUG:-0}
|
||||
- CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGIN:-http://localhost}
|
||||
# Gunicorn Workers
|
||||
- GUNICORN_WORKERS=${GUNICORN_WORKERS:-1}
|
||||
#DB SETTINGS
|
||||
- PGHOST=plane-db
|
||||
- PGDATABASE=plane
|
||||
- POSTGRES_USER=$SERVICE_USER_POSTGRES
|
||||
- POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRES
|
||||
- POSTGRES_DB=plane
|
||||
- POSTGRES_PORT=5432
|
||||
- PGDATA=/var/lib/postgresql/data
|
||||
- DATABASE_URL=postgresql://$SERVICE_USER_POSTGRES:$SERVICE_PASSWORD_POSTGRES@plane-db/plane
|
||||
# REDIS SETTINGS
|
||||
- REDIS_HOST=plane-redis
|
||||
- REDIS_PORT=6379
|
||||
- REDIS_URL=${REDIS_URL:-redis://plane-redis:6379/}
|
||||
x-db-env: &db-env
|
||||
PGHOST: plane-db
|
||||
PGDATABASE: plane
|
||||
POSTGRES_USER: $SERVICE_USER_POSTGRES
|
||||
POSTGRES_PASSWORD: $SERVICE_PASSWORD_POSTGRES
|
||||
POSTGRES_DB: plane
|
||||
POSTGRES_PORT: 5432
|
||||
PGDATA: /var/lib/postgresql/data
|
||||
|
||||
# RabbitMQ Settings
|
||||
- RABBITMQ_HOST=plane-mq
|
||||
- RABBITMQ_PORT=${RABBITMQ_PORT:-5672}
|
||||
- RABBITMQ_DEFAULT_USER=${SERVICE_USER_RABBITMQ:-plane}
|
||||
- RABBITMQ_DEFAULT_PASS=${SERVICE_PASSWORD_RABBITMQ:-plane}
|
||||
- RABBITMQ_DEFAULT_VHOST=${RABBITMQ_VHOST:-plane}
|
||||
- RABBITMQ_VHOST=${RABBITMQ_VHOST:-plane}
|
||||
- 'AMQP_URL=amqp://${SERVICE_USER_RABBITMQ}:${SERVICE_PASSWORD_RABBITMQ}@plane-mq:${RABBITMQ_PORT}/plane'
|
||||
# Application secret
|
||||
- SECRET_KEY=$SERVICE_PASSWORD_64_SECRETKEY
|
||||
# DATA STORE SETTINGS
|
||||
- USE_MINIO=${USE_MINIO:-1}
|
||||
- AWS_REGION=${AWS_REGION}
|
||||
- AWS_ACCESS_KEY_ID=$SERVICE_USER_MINIO
|
||||
- AWS_SECRET_ACCESS_KEY=$SERVICE_PASSWORD_MINIO
|
||||
- AWS_S3_ENDPOINT_URL=${AWS_S3_ENDPOINT_URL:-http://plane-minio:9000}
|
||||
- AWS_S3_BUCKET_NAME=${AWS_S3_BUCKET_NAME:-uploads}
|
||||
- MINIO_ROOT_USER=$SERVICE_USER_MINIO
|
||||
- MINIO_ROOT_PASSWORD=$SERVICE_PASSWORD_MINIO
|
||||
- BUCKET_NAME=${BUCKET_NAME:-uploads}
|
||||
- FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880}
|
||||
# Live server env
|
||||
- API_BASE_URL=${API_BASE_URL:-http://api:8000}
|
||||
x-redis-env: &redis-env
|
||||
REDIS_HOST: ${REDIS_HOST:-plane-redis}
|
||||
REDIS_PORT: ${REDIS_PORT:-6379}
|
||||
REDIS_URL: ${REDIS_URL:-redis://plane-redis:6379/}
|
||||
|
||||
x-minio-env: &minio-env
|
||||
MINIO_ROOT_USER: $SERVICE_USER_MINIO
|
||||
MINIO_ROOT_PASSWORD: $SERVICE_PASSWORD_MINIO
|
||||
|
||||
x-aws-s3-env: &aws-s3-env
|
||||
AWS_REGION: ${AWS_REGION:-}
|
||||
AWS_ACCESS_KEY_ID: $SERVICE_USER_MINIO
|
||||
AWS_SECRET_ACCESS_KEY: $SERVICE_PASSWORD_MINIO
|
||||
AWS_S3_ENDPOINT_URL: ${AWS_S3_ENDPOINT_URL:-http://plane-minio:9000}
|
||||
AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
||||
|
||||
x-mq-env: &mq-env # RabbitMQ Settings
|
||||
RABBITMQ_HOST: plane-mq
|
||||
RABBITMQ_PORT: ${RABBITMQ_PORT:-5672}
|
||||
RABBITMQ_DEFAULT_USER: ${SERVICE_USER_RABBITMQ:-plane}
|
||||
RABBITMQ_DEFAULT_PASS: ${SERVICE_PASSWORD_RABBITMQ:-plane}
|
||||
RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_VHOST:-plane}
|
||||
RABBITMQ_VHOST: ${RABBITMQ_VHOST:-plane}
|
||||
|
||||
x-live-env: &live-env
|
||||
API_BASE_URL: ${API_BASE_URL:-http://api:8000}
|
||||
|
||||
x-app-env: &app-env
|
||||
APP_RELEASE: ${APP_RELEASE:-v1.0.0}
|
||||
WEB_URL: ${SERVICE_URL_PLANE}
|
||||
DEBUG: ${DEBUG:-0}
|
||||
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-http://localhost}
|
||||
GUNICORN_WORKERS: ${GUNICORN_WORKERS:-1}
|
||||
USE_MINIO: ${USE_MINIO:-1}
|
||||
DATABASE_URL: postgresql://$SERVICE_USER_POSTGRES:$SERVICE_PASSWORD_POSTGRES@plane-db/plane
|
||||
SECRET_KEY: $SERVICE_PASSWORD_64_SECRETKEY
|
||||
AMQP_URL: amqp://${SERVICE_USER_RABBITMQ}:${SERVICE_PASSWORD_RABBITMQ}@plane-mq:${RABBITMQ_PORT:-5672}/plane
|
||||
API_KEY_RATE_LIMIT: ${API_KEY_RATE_LIMIT:-60/minute}
|
||||
MINIO_ENDPOINT_SSL: ${MINIO_ENDPOINT_SSL:-0}
|
||||
|
||||
services:
|
||||
proxy:
|
||||
image: artifacts.plane.so/makeplane/plane-proxy:${APP_RELEASE:-v1.0.0}
|
||||
environment:
|
||||
- SERVICE_URL_PLANE
|
||||
- APP_DOMAIN=${SERVICE_URL_PLANE}
|
||||
- SITE_ADDRESS=:80
|
||||
- FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880}
|
||||
- BUCKET_NAME=${BUCKET_NAME:-uploads}
|
||||
image: makeplane/plane-proxy:${APP_RELEASE:-v0.25.1}
|
||||
- BUCKET_NAME=${AWS_S3_BUCKET_NAME:-uploads}
|
||||
depends_on:
|
||||
- web
|
||||
- api
|
||||
- space
|
||||
- admin
|
||||
- live
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://127.0.0.1:80"]
|
||||
interval: 2s
|
||||
timeout: 10s
|
||||
retries: 15
|
||||
|
||||
web:
|
||||
image: makeplane/plane-frontend:${APP_RELEASE:-v0.25.1}
|
||||
command: node web/server.js web
|
||||
image: artifacts.plane.so/makeplane/plane-frontend:${APP_RELEASE:-v1.0.0}
|
||||
depends_on:
|
||||
- api
|
||||
- worker
|
||||
@@ -78,9 +83,9 @@ services:
|
||||
interval: 2s
|
||||
timeout: 10s
|
||||
retries: 15
|
||||
|
||||
space:
|
||||
image: makeplane/plane-space:${APP_RELEASE:-v0.25.1}
|
||||
command: node space/server.js space
|
||||
image: artifacts.plane.so/makeplane/plane-space:${APP_RELEASE:-v1.0.0}
|
||||
depends_on:
|
||||
- api
|
||||
- worker
|
||||
@@ -92,8 +97,7 @@ services:
|
||||
retries: 15
|
||||
|
||||
admin:
|
||||
image: makeplane/plane-admin:${APP_RELEASE:-v0.25.1}
|
||||
command: node admin/server.js admin
|
||||
image: artifacts.plane.so/makeplane/plane-admin:${APP_RELEASE:-v1.0.0}
|
||||
depends_on:
|
||||
- api
|
||||
- web
|
||||
@@ -104,12 +108,13 @@ services:
|
||||
retries: 15
|
||||
|
||||
live:
|
||||
<<: *app-env
|
||||
image: makeplane/plane-live:${APP_RELEASE:-v0.25.1}
|
||||
command: node live/dist/server.js live
|
||||
image: artifacts.plane.so/makeplane/plane-live:${APP_RELEASE:-v1.0.0}
|
||||
environment:
|
||||
<<: [*live-env, *redis-env]
|
||||
depends_on:
|
||||
- api
|
||||
- web
|
||||
- plane-redis
|
||||
healthcheck:
|
||||
test: ["CMD", "echo", "hey whats up"]
|
||||
interval: 2s
|
||||
@@ -117,14 +122,16 @@ services:
|
||||
retries: 15
|
||||
|
||||
api:
|
||||
<<: *app-env
|
||||
image: makeplane/plane-backend:${APP_RELEASE:-v0.25.1}
|
||||
image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-v1.0.0}
|
||||
command: ./bin/docker-entrypoint-api.sh
|
||||
volumes:
|
||||
- logs_api:/code/plane/logs
|
||||
environment:
|
||||
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *mq-env]
|
||||
depends_on:
|
||||
- plane-db
|
||||
- plane-redis
|
||||
- plane-mq
|
||||
healthcheck:
|
||||
test: ["CMD", "echo", "hey whats up"]
|
||||
interval: 2s
|
||||
@@ -132,15 +139,17 @@ services:
|
||||
retries: 15
|
||||
|
||||
worker:
|
||||
<<: *app-env
|
||||
image: makeplane/plane-backend:${APP_RELEASE:-v0.25.1}
|
||||
image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-v1.0.0}
|
||||
command: ./bin/docker-entrypoint-worker.sh
|
||||
volumes:
|
||||
- logs_worker:/code/plane/logs
|
||||
environment:
|
||||
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *mq-env]
|
||||
depends_on:
|
||||
- api
|
||||
- plane-db
|
||||
- plane-redis
|
||||
- plane-mq
|
||||
healthcheck:
|
||||
test: ["CMD", "echo", "hey whats up"]
|
||||
interval: 2s
|
||||
@@ -148,15 +157,17 @@ services:
|
||||
retries: 15
|
||||
|
||||
beat-worker:
|
||||
<<: *app-env
|
||||
image: makeplane/plane-backend:${APP_RELEASE:-v0.25.1}
|
||||
image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-v1.0.0}
|
||||
command: ./bin/docker-entrypoint-beat.sh
|
||||
volumes:
|
||||
- logs_beat-worker:/code/plane/logs
|
||||
environment:
|
||||
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *mq-env]
|
||||
depends_on:
|
||||
- api
|
||||
- plane-db
|
||||
- plane-redis
|
||||
- plane-mq
|
||||
healthcheck:
|
||||
test: ["CMD", "echo", "hey whats up"]
|
||||
interval: 2s
|
||||
@@ -164,20 +175,23 @@ services:
|
||||
retries: 15
|
||||
|
||||
migrator:
|
||||
<<: *app-env
|
||||
image: makeplane/plane-backend:${APP_RELEASE:-v0.25.1}
|
||||
image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-v1.0.0}
|
||||
restart: "no"
|
||||
command: ./bin/docker-entrypoint-migrator.sh
|
||||
volumes:
|
||||
- logs_migrator:/code/plane/logs
|
||||
environment:
|
||||
<<: [*app-env, *db-env, *redis-env, *minio-env, *aws-s3-env, *mq-env]
|
||||
depends_on:
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
# Comment this if you already have a database running
|
||||
plane-db:
|
||||
<<: *app-env
|
||||
image: postgres:15.7-alpine
|
||||
command: postgres -c 'max_connections=1000'
|
||||
environment:
|
||||
<<: *db-env
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
@@ -187,7 +201,6 @@ services:
|
||||
retries: 10
|
||||
|
||||
plane-redis:
|
||||
<<: *app-env
|
||||
image: valkey/valkey:7.2.5-alpine
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
@@ -198,9 +211,10 @@ services:
|
||||
retries: 10
|
||||
|
||||
plane-mq:
|
||||
<<: *app-env
|
||||
image: rabbitmq:3.13.6-management-alpine
|
||||
restart: always
|
||||
environment:
|
||||
<<: *mq-env
|
||||
volumes:
|
||||
- rabbitmq_data:/var/lib/rabbitmq
|
||||
healthcheck:
|
||||
@@ -209,10 +223,12 @@ services:
|
||||
timeout: 30s
|
||||
retries: 3
|
||||
|
||||
# Comment this if you using any external s3 compatible storage
|
||||
plane-minio:
|
||||
<<: *app-env
|
||||
image: minio/minio:latest
|
||||
image: ghcr.io/coollabsio/minio:RELEASE.2025-10-15T17-29-55Z
|
||||
command: server /export --console-address ":9090"
|
||||
environment:
|
||||
<<: *minio-env
|
||||
volumes:
|
||||
- uploads:/export
|
||||
healthcheck:
|
||||
|
||||