Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
838378a409 | ||
|
|
95c90a258f | ||
|
|
20d6dcea45 | ||
|
|
1cb2ff2130 | ||
|
|
df755d521c | ||
|
|
0019b5987d | ||
|
|
41d6405872 | ||
|
|
e0ab5b5961 | ||
|
|
560734d72c | ||
|
|
18589823f3 | ||
|
|
ab2b3fd9ef | ||
|
|
123d9b306f | ||
|
|
ca3445103d | ||
| 18f3de1ba9 | |||
| 52571c651f | |||
|
|
b9712643de | ||
| e954402959 |
@@ -1,2 +1,2 @@
|
||||
parameters:
|
||||
app.version: '0.1.23'
|
||||
app.version: '0.1.28'
|
||||
|
||||
@@ -138,8 +138,23 @@
|
||||
"uptime": "Uptime",
|
||||
"cpu": "CPU",
|
||||
"memory": "Memoire",
|
||||
"ports": "Ports",
|
||||
"noData": "Aucune donnee disponible"
|
||||
},
|
||||
"database": {
|
||||
"title": "Base de donnees",
|
||||
"status": "Statut",
|
||||
"connected": "Connecte",
|
||||
"unreachable": "Injoignable",
|
||||
"name": "Base",
|
||||
"size": "Taille",
|
||||
"tableCount": "Tables",
|
||||
"activeConnections": "Connexions",
|
||||
"cacheHitRatio": "Cache hit",
|
||||
"largestTable": "Plus grosse table",
|
||||
"formLabel": "Nom de la base",
|
||||
"formHint": "Ex : coltura"
|
||||
},
|
||||
"deploy": {
|
||||
"button": "Deployer",
|
||||
"title": "Deployer une version",
|
||||
|
||||
@@ -4,8 +4,8 @@ import { getApplication, updateApplication, deleteApplication } from '~/services
|
||||
import { createEnvironment, updateEnvironment, deleteEnvironment, toggleMaintenance } from '~/services/environments'
|
||||
import type { DeployResult } from '~/services/dto/deploy'
|
||||
import { getAvailableTags, deploy } from '~/services/deploy'
|
||||
import type { EnvironmentHealth } from '~/services/dto/dashboard'
|
||||
import { getEnvironmentHealth } from '~/services/dashboard'
|
||||
import type { EnvironmentHealth, DatabaseInfo } from '~/services/dto/dashboard'
|
||||
import { getEnvironmentHealth, getDatabaseInfo } from '~/services/dashboard'
|
||||
import type { LogOutput } from '~/services/dto/logs'
|
||||
import { getDockerLogs, getSymfonyLog } from '~/services/logs'
|
||||
|
||||
@@ -31,6 +31,9 @@ const deployResult = ref<DeployResult | null>(null)
|
||||
const healthByEnvId = ref<Record<number, EnvironmentHealth>>({})
|
||||
const loadingHealth = ref(false)
|
||||
|
||||
// Database info per env
|
||||
const dbInfoByEnvId = ref<Record<number, DatabaseInfo>>({})
|
||||
|
||||
// Log modals
|
||||
const showLogModal = ref(false)
|
||||
const logContent = ref('')
|
||||
@@ -54,6 +57,7 @@ const envForm = ref<EnvironmentWrite>({
|
||||
deployScriptPath: '',
|
||||
maintenanceFilePath: '',
|
||||
appUrl: '',
|
||||
databaseName: '',
|
||||
logFiles: [],
|
||||
})
|
||||
const isSubmittingEnv = ref(false)
|
||||
@@ -66,6 +70,7 @@ async function loadApplication() {
|
||||
loading.value = false
|
||||
}
|
||||
loadHealthData()
|
||||
loadDatabaseData()
|
||||
}
|
||||
|
||||
// Application edit
|
||||
@@ -103,7 +108,7 @@ async function handleDeleteApp() {
|
||||
// Environment CRUD
|
||||
function openCreateEnvModal() {
|
||||
editingEnvId.value = null
|
||||
envForm.value = { name: '', containerName: '', deployScriptPath: '', maintenanceFilePath: '', appUrl: '', logFiles: [] }
|
||||
envForm.value = { name: '', containerName: '', deployScriptPath: '', maintenanceFilePath: '', appUrl: '', databaseName: '', logFiles: [] }
|
||||
showEnvModal.value = true
|
||||
}
|
||||
|
||||
@@ -115,6 +120,7 @@ function openEditEnvModal(env: Environment) {
|
||||
deployScriptPath: env.deployScriptPath,
|
||||
maintenanceFilePath: env.maintenanceFilePath,
|
||||
appUrl: env.appUrl ?? '',
|
||||
databaseName: env.databaseName ?? '',
|
||||
logFiles: env.logFiles.map(lf => ({ label: lf.label, path: lf.path })),
|
||||
}
|
||||
showEnvModal.value = true
|
||||
@@ -215,6 +221,20 @@ async function loadHealthData() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDatabaseData() {
|
||||
if (!application.value?.environments?.length) return
|
||||
const promises = application.value.environments.map(async (env) => {
|
||||
if (!env.databaseName) return
|
||||
try {
|
||||
const info = await getDatabaseInfo(env.id!)
|
||||
dbInfoByEnvId.value[env.id!] = info
|
||||
} catch {
|
||||
// silently ignore — no database configured or unreachable
|
||||
}
|
||||
})
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
function formatUptime(startedAt: string): string {
|
||||
if (!startedAt) return '-'
|
||||
const start = new Date(startedAt)
|
||||
@@ -427,7 +447,7 @@ onMounted(loadApplication)
|
||||
<!-- Health metrics -->
|
||||
<div v-if="healthByEnvId[env.id!]" class="mt-4 border-t border-neutral-200 py-3">
|
||||
<p class="text-sm font-bold uppercase tracking-wider mb-2">{{ t('environments.health.title') }}</p>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-5 gap-3">
|
||||
<div class="grid grid-cols-2 sm:grid-cols-6 gap-3">
|
||||
<div>
|
||||
<p class="text-xs text-neutral-400">{{ t('environments.health.status') }}</p>
|
||||
<span
|
||||
@@ -456,8 +476,65 @@ onMounted(loadApplication)
|
||||
<span class="text-neutral-400">({{ healthByEnvId[env.id!].memoryPercent }}%)</span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-neutral-400">{{ t('environments.health.ports') }}</p>
|
||||
<div v-if="healthByEnvId[env.id!].ports?.length" class="mt-1 flex flex-wrap gap-1">
|
||||
<span
|
||||
v-for="(p, i) in healthByEnvId[env.id!].ports"
|
||||
:key="i"
|
||||
class="inline-block rounded bg-neutral-100 px-2 py-0.5 text-xs font-mono text-neutral-700"
|
||||
>
|
||||
{{ p.hostPort }}:{{ p.containerPort }}
|
||||
</span>
|
||||
</div>
|
||||
<p v-else class="text-sm text-neutral-400 mt-1">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Database info -->
|
||||
<div v-if="dbInfoByEnvId[env.id!]" class="mt-4 border-t border-neutral-200 py-3">
|
||||
<p class="text-sm font-bold uppercase tracking-wider mb-2">{{ t('environments.database.title') }}</p>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-6 gap-3">
|
||||
<div>
|
||||
<p class="text-xs text-neutral-400">{{ t('environments.database.status') }}</p>
|
||||
<span
|
||||
class="inline-block rounded-full px-2.5 py-0.5 text-xs font-semibold mt-1"
|
||||
:class="dbInfoByEnvId[env.id!].connected
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-red-100 text-red-700'"
|
||||
>
|
||||
{{ dbInfoByEnvId[env.id!].connected
|
||||
? t('environments.database.connected')
|
||||
: t('environments.database.unreachable') }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-neutral-400">{{ t('environments.database.name') }}</p>
|
||||
<p class="text-sm font-mono text-neutral-800 mt-1">{{ dbInfoByEnvId[env.id!].name }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-neutral-400">{{ t('environments.database.size') }}</p>
|
||||
<p class="text-sm text-neutral-800 mt-1">{{ dbInfoByEnvId[env.id!].size || '-' }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-neutral-400">{{ t('environments.database.tableCount') }}</p>
|
||||
<p class="text-sm text-neutral-800 mt-1">{{ dbInfoByEnvId[env.id!].tableCount }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-neutral-400">{{ t('environments.database.activeConnections') }}</p>
|
||||
<p class="text-sm text-neutral-800 mt-1">{{ dbInfoByEnvId[env.id!].activeConnections }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-neutral-400">{{ t('environments.database.cacheHitRatio') }}</p>
|
||||
<p class="text-sm text-neutral-800 mt-1">{{ dbInfoByEnvId[env.id!].cacheHitRatio }}%</p>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="dbInfoByEnvId[env.id!].largestTable && dbInfoByEnvId[env.id!].largestTable !== '-'" class="text-xs text-neutral-500 mt-2">
|
||||
{{ t('environments.database.largestTable') }} : <span class="font-mono">{{ dbInfoByEnvId[env.id!].largestTable }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center gap-4 mt-4">
|
||||
<MalioButton
|
||||
:label="t('environments.editButton')"
|
||||
@@ -564,6 +641,12 @@ onMounted(loadApplication)
|
||||
:label="t('environments.form.appUrl')"
|
||||
groupClass="mt-0"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="envForm.databaseName"
|
||||
:label="t('environments.database.formLabel')"
|
||||
:hint="t('environments.database.formHint')"
|
||||
groupClass="mt-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Log files -->
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { DashboardResponse, EnvironmentHealth } from './dto/dashboard'
|
||||
import type { DashboardResponse, EnvironmentHealth, DatabaseInfo } from './dto/dashboard'
|
||||
|
||||
export function getDashboard(): Promise<DashboardResponse> {
|
||||
return useApi().get<DashboardResponse>('/dashboard', undefined, {
|
||||
@@ -11,3 +11,9 @@ export function getEnvironmentHealth(envId: number): Promise<EnvironmentHealth>
|
||||
toast: false,
|
||||
})
|
||||
}
|
||||
|
||||
export function getDatabaseInfo(envId: number): Promise<DatabaseInfo> {
|
||||
return useApi().get<DatabaseInfo>(`/environments/${envId}/database`, undefined, {
|
||||
toast: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ type Environment = {
|
||||
deployScriptPath: string
|
||||
maintenanceFilePath: string
|
||||
appUrl?: string
|
||||
databaseName?: string
|
||||
logFiles: LogFile[]
|
||||
maintenance: boolean
|
||||
}
|
||||
@@ -22,6 +23,7 @@ type EnvironmentWrite = {
|
||||
deployScriptPath: string
|
||||
maintenanceFilePath: string
|
||||
appUrl?: string
|
||||
databaseName?: string
|
||||
logFiles: LogFile[]
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,22 @@ type DashboardResponse = {
|
||||
applications: DashboardApplication[]
|
||||
}
|
||||
|
||||
type PortMapping = {
|
||||
hostPort: string
|
||||
containerPort: string
|
||||
protocol: string
|
||||
}
|
||||
|
||||
type DatabaseInfo = {
|
||||
connected: boolean
|
||||
name: string
|
||||
size: string
|
||||
tableCount: number
|
||||
activeConnections: number
|
||||
cacheHitRatio: number
|
||||
largestTable: string
|
||||
}
|
||||
|
||||
type EnvironmentHealth = {
|
||||
status: string
|
||||
version: string
|
||||
@@ -24,4 +40,5 @@ type EnvironmentHealth = {
|
||||
memoryUsage: string
|
||||
memoryLimit: string
|
||||
memoryPercent: number
|
||||
ports: PortMapping[]
|
||||
}
|
||||
|
||||
@@ -40,10 +40,16 @@ FROM php:8.4-fpm AS production
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libicu-dev libpq-dev libpng-dev libzip-dev libxml2-dev \
|
||||
nginx supervisor docker.io \
|
||||
nginx supervisor docker.io curl \
|
||||
&& docker-php-ext-install -j$(nproc) intl pdo_pgsql zip gd opcache \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Docker Compose plugin
|
||||
RUN DOCKER_COMPOSE_VERSION=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep '"tag_name"' | sed 's/.*"v\(.*\)".*/\1/') \
|
||||
&& mkdir -p /usr/local/lib/docker/cli-plugins \
|
||||
&& curl -SL "https://github.com/docker/compose/releases/download/v${DOCKER_COMPOSE_VERSION}/docker-compose-linux-x86_64" -o /usr/local/lib/docker/cli-plugins/docker-compose \
|
||||
&& chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
|
||||
|
||||
# PHP production config
|
||||
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
|
||||
|
||||
|
||||
31
migrations/Version20260408135722.php
Normal file
31
migrations/Version20260408135722.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20260408135722 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE environment ADD database_name VARCHAR(255) DEFAULT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE environment DROP database_name');
|
||||
}
|
||||
}
|
||||
29
src/ApiResource/DatabaseInfo.php
Normal file
29
src/ApiResource/DatabaseInfo.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\ApiResource;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use App\State\DatabaseInfoProvider;
|
||||
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Get(
|
||||
uriTemplate: '/environments/{id}/database',
|
||||
security: "is_granted('ROLE_ADMIN')",
|
||||
provider: DatabaseInfoProvider::class,
|
||||
),
|
||||
],
|
||||
)]
|
||||
final class DatabaseInfo
|
||||
{
|
||||
public bool $connected = false;
|
||||
public string $name = '';
|
||||
public string $size = '';
|
||||
public int $tableCount = 0;
|
||||
public int $activeConnections = 0;
|
||||
public float $cacheHitRatio = 0.0;
|
||||
public string $largestTable = '';
|
||||
}
|
||||
@@ -26,4 +26,6 @@ final class EnvironmentHealth
|
||||
public string $memoryUsage = '';
|
||||
public string $memoryLimit = '';
|
||||
public float $memoryPercent = 0.0;
|
||||
/** @var list<array{hostPort: string, containerPort: string, protocol: string}> */
|
||||
public array $ports = [];
|
||||
}
|
||||
|
||||
@@ -73,6 +73,10 @@ class Environment
|
||||
#[Groups(['env:read', 'env:write', 'app:detail'])]
|
||||
private ?string $appUrl = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Groups(['env:read', 'env:write', 'app:detail'])]
|
||||
private ?string $databaseName = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Application::class, inversedBy: 'environments')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?Application $application = null;
|
||||
@@ -155,6 +159,18 @@ class Environment
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDatabaseName(): ?string
|
||||
{
|
||||
return $this->databaseName;
|
||||
}
|
||||
|
||||
public function setDatabaseName(?string $databaseName): static
|
||||
{
|
||||
$this->databaseName = $databaseName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getApplication(): ?Application
|
||||
{
|
||||
return $this->application;
|
||||
|
||||
124
src/Service/DatabaseService.php
Normal file
124
src/Service/DatabaseService.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
|
||||
final class DatabaseService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Connection $connection,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array{connected: bool, name: string, size: string, tableCount: int, activeConnections: int, cacheHitRatio: float, largestTable: string}
|
||||
*/
|
||||
public function getDatabaseInfo(string $databaseName): array
|
||||
{
|
||||
$fallback = [
|
||||
'connected' => false,
|
||||
'name' => $databaseName,
|
||||
'size' => '',
|
||||
'tableCount' => 0,
|
||||
'activeConnections' => 0,
|
||||
'cacheHitRatio' => 0.0,
|
||||
'largestTable' => '',
|
||||
];
|
||||
|
||||
try {
|
||||
// Check database exists
|
||||
$exists = $this->connection->fetchOne(
|
||||
'SELECT 1 FROM pg_database WHERE datname = :dbname',
|
||||
['dbname' => $databaseName]
|
||||
);
|
||||
|
||||
if (!$exists) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
// Database size
|
||||
$sizeBytes = (int) $this->connection->fetchOne(
|
||||
'SELECT pg_database_size(:dbname)',
|
||||
['dbname' => $databaseName]
|
||||
);
|
||||
$size = $this->formatBytes($sizeBytes);
|
||||
|
||||
// Table count
|
||||
$tableCount = (int) $this->connection->fetchOne(
|
||||
"SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_catalog = :dbname",
|
||||
['dbname' => $databaseName]
|
||||
);
|
||||
|
||||
// Active connections
|
||||
$activeConnections = (int) $this->connection->fetchOne(
|
||||
'SELECT count(*) FROM pg_stat_activity WHERE datname = :dbname',
|
||||
['dbname' => $databaseName]
|
||||
);
|
||||
|
||||
// Cache hit ratio
|
||||
$cacheHitRatio = (float) ($this->connection->fetchOne(
|
||||
'SELECT round(100.0 * sum(blks_hit) / nullif(sum(blks_hit + blks_read), 0), 2) FROM pg_stat_database WHERE datname = :dbname',
|
||||
['dbname' => $databaseName]
|
||||
) ?? 0);
|
||||
|
||||
// Largest table — requires querying the target database catalog
|
||||
$largestTable = $this->fetchLargestTable($databaseName);
|
||||
|
||||
return [
|
||||
'connected' => true,
|
||||
'name' => $databaseName,
|
||||
'size' => $size,
|
||||
'tableCount' => $tableCount,
|
||||
'activeConnections' => $activeConnections,
|
||||
'cacheHitRatio' => $cacheHitRatio,
|
||||
'largestTable' => $largestTable,
|
||||
];
|
||||
} catch (\Throwable) {
|
||||
return $fallback;
|
||||
}
|
||||
}
|
||||
|
||||
private function fetchLargestTable(string $databaseName): string
|
||||
{
|
||||
try {
|
||||
$row = $this->connection->fetchAssociative(
|
||||
"SELECT relname, pg_total_relation_size(c.oid) as total_size
|
||||
FROM pg_catalog.pg_class c
|
||||
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE n.nspname = 'public' AND c.relkind = 'r'
|
||||
AND c.relowner = (SELECT oid FROM pg_roles WHERE rolname = current_user)
|
||||
ORDER BY pg_total_relation_size(c.oid) DESC
|
||||
LIMIT 1"
|
||||
);
|
||||
|
||||
if (!$row) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return $row['relname'] . ' (' . $this->formatBytes((int) $row['total_size']) . ')';
|
||||
} catch (\Throwable) {
|
||||
return '-';
|
||||
}
|
||||
}
|
||||
|
||||
private function formatBytes(int $bytes): string
|
||||
{
|
||||
if ($bytes < 1024) {
|
||||
return $bytes . ' B';
|
||||
}
|
||||
|
||||
$units = ['KB', 'MB', 'GB', 'TB'];
|
||||
$value = (float) $bytes;
|
||||
|
||||
foreach ($units as $unit) {
|
||||
$value /= 1024;
|
||||
if ($value < 1024) {
|
||||
return round($value, 1) . ' ' . $unit;
|
||||
}
|
||||
}
|
||||
|
||||
return round($value, 1) . ' TB';
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ final class DockerService
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{status: string, image: string, version: string, startedAt: string}
|
||||
* @return array{status: string, image: string, version: string, startedAt: string, ports: list<array{hostPort: string, containerPort: string, protocol: string}>}
|
||||
*/
|
||||
public function getContainerStatus(string $containerName): array
|
||||
{
|
||||
@@ -33,12 +33,13 @@ final class DockerService
|
||||
'image' => '',
|
||||
'version' => '',
|
||||
'startedAt' => '',
|
||||
'ports' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$process = new Process([
|
||||
'docker', 'inspect',
|
||||
'--format', '{{.State.Status}}||{{.Config.Image}}||{{.State.StartedAt}}',
|
||||
'--format', '{{.State.Status}}||{{.Config.Image}}||{{.State.StartedAt}}||{{json .NetworkSettings.Ports}}',
|
||||
$containerName,
|
||||
]);
|
||||
$process->setTimeout(10);
|
||||
@@ -50,10 +51,11 @@ final class DockerService
|
||||
'image' => '',
|
||||
'version' => '',
|
||||
'startedAt' => '',
|
||||
'ports' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$parts = explode('||', trim($process->getOutput()));
|
||||
$parts = explode('||', trim($process->getOutput()), 4);
|
||||
|
||||
if (\count($parts) < 3) {
|
||||
return [
|
||||
@@ -61,6 +63,7 @@ final class DockerService
|
||||
'image' => '',
|
||||
'version' => '',
|
||||
'startedAt' => '',
|
||||
'ports' => [],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -70,11 +73,32 @@ final class DockerService
|
||||
$version = substr($image, strrpos($image, ':') + 1);
|
||||
}
|
||||
|
||||
$ports = [];
|
||||
if (isset($parts[3])) {
|
||||
$portsJson = json_decode($parts[3], true);
|
||||
if (\is_array($portsJson)) {
|
||||
foreach ($portsJson as $containerPort => $bindings) {
|
||||
if (!\is_array($bindings)) {
|
||||
continue;
|
||||
}
|
||||
[$port, $protocol] = explode('/', $containerPort) + [1 => 'tcp'];
|
||||
foreach ($bindings as $binding) {
|
||||
$ports[] = [
|
||||
'hostPort' => $binding['HostPort'] ?? '',
|
||||
'containerPort' => $port,
|
||||
'protocol' => $protocol,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => $parts[0],
|
||||
'image' => $image,
|
||||
'version' => $version,
|
||||
'startedAt' => $parts[2],
|
||||
'ports' => $ports,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
49
src/State/DatabaseInfoProvider.php
Normal file
49
src/State/DatabaseInfoProvider.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\State;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\ApiResource\DatabaseInfo;
|
||||
use App\Repository\EnvironmentRepository;
|
||||
use App\Service\DatabaseService;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
final readonly class DatabaseInfoProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private EnvironmentRepository $environmentRepository,
|
||||
private DatabaseService $databaseService,
|
||||
) {}
|
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): DatabaseInfo
|
||||
{
|
||||
$id = $uriVariables['id'] ?? null;
|
||||
$environment = $id ? $this->environmentRepository->find($id) : null;
|
||||
|
||||
if (null === $environment) {
|
||||
throw new NotFoundHttpException(sprintf('Environment "%s" not found.', $id));
|
||||
}
|
||||
|
||||
$databaseName = $environment->getDatabaseName();
|
||||
|
||||
if (null === $databaseName || '' === $databaseName) {
|
||||
throw new NotFoundHttpException('No database configured for this environment.');
|
||||
}
|
||||
|
||||
$info = $this->databaseService->getDatabaseInfo($databaseName);
|
||||
|
||||
$dto = new DatabaseInfo();
|
||||
$dto->connected = $info['connected'];
|
||||
$dto->name = $info['name'];
|
||||
$dto->size = $info['size'];
|
||||
$dto->tableCount = $info['tableCount'];
|
||||
$dto->activeConnections = $info['activeConnections'];
|
||||
$dto->cacheHitRatio = $info['cacheHitRatio'];
|
||||
$dto->largestTable = $info['largestTable'];
|
||||
|
||||
return $dto;
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ final readonly class EnvironmentHealthProvider implements ProviderInterface
|
||||
$dto->memoryUsage = $stats['memoryUsage'];
|
||||
$dto->memoryLimit = $stats['memoryLimit'];
|
||||
$dto->memoryPercent = $stats['memoryPercent'];
|
||||
$dto->ports = $status['ports'];
|
||||
|
||||
return $dto;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user