Skip to main content

Infrastructure

Process List code

Copy the implementation or inspect every file that belongs to this component unit.

resources/views/components/ui/process-list.blade.php

@props([
    'output' => null,
    'rows' => [],
    'title' => 'Processes by CPU',
    'description' => null,
    'command' => 'ps auxww --sort=-%cpu',
    'capturedAt' => null,
    'host' => null,
    'limit' => null,
    'emptyTitle' => 'No process output',
    'emptyDescription' => 'Capture ps output and pass it to the component to inspect running processes.',
])

@php
    /*
    | Process list viewer for `ps auxww --sort=-%cpu` output. It accepts either
    | the raw command output or normalized row arrays. Shell execution stays with
    | the caller, so this component remains a safe display primitive.
    */
    $parseProcessOutput = function (?string $rawOutput): array {
        if (! is_string($rawOutput) || trim($rawOutput) === '') {
            return [];
        }

        $lines = preg_split('/\R/', trim($rawOutput)) ?: [];
        $processes = [];

        foreach ($lines as $index => $line) {
            $line = trim($line);

            if ($line === '' || ($index === 0 && str_starts_with($line, 'USER'))) {
                continue;
            }

            $columns = preg_split('/\s+/', $line, 11);

            if (! is_array($columns) || count($columns) < 10) {
                continue;
            }

            $processes[] = [
                'user' => $columns[0] ?? '',
                'pid' => $columns[1] ?? '',
                'cpu' => (float) ($columns[2] ?? 0),
                'memory' => (float) ($columns[3] ?? 0),
                'vsz' => $columns[4] ?? '',
                'rss' => $columns[5] ?? '',
                'tty' => $columns[6] ?? '',
                'stat' => $columns[7] ?? '',
                'started' => $columns[8] ?? '',
                'time' => $columns[9] ?? '',
                'command' => $columns[10] ?? '',
            ];
        }

        return $processes;
    };

    $normalizeProcess = function (array $process): array {
        return [
            'user' => (string) ($process['user'] ?? $process['USER'] ?? ''),
            'pid' => (string) ($process['pid'] ?? $process['PID'] ?? ''),
            'cpu' => (float) ($process['cpu'] ?? $process['%cpu'] ?? $process['%CPU'] ?? 0),
            'memory' => (float) ($process['memory'] ?? $process['mem'] ?? $process['%mem'] ?? $process['%MEM'] ?? 0),
            'vsz' => (string) ($process['vsz'] ?? $process['VSZ'] ?? ''),
            'rss' => (string) ($process['rss'] ?? $process['RSS'] ?? ''),
            'tty' => (string) ($process['tty'] ?? $process['TTY'] ?? ''),
            'stat' => (string) ($process['stat'] ?? $process['STAT'] ?? ''),
            'started' => (string) ($process['started'] ?? $process['start'] ?? $process['START'] ?? ''),
            'time' => (string) ($process['time'] ?? $process['TIME'] ?? ''),
            'command' => (string) ($process['command'] ?? $process['COMMAND'] ?? ''),
        ];
    };

    $processes = collect(! empty($rows) ? $rows : $parseProcessOutput($output))
        ->filter(fn ($process) => is_array($process))
        ->map($normalizeProcess)
        ->when(is_numeric($limit) && (int) $limit > 0, fn ($items) => $items->take((int) $limit))
        ->values();

    $formatPercent = function (float $value): string {
        $formatted = number_format($value, $value >= 10 || floor($value) === $value ? 0 : 1);

        return "{$formatted}%";
    };

    $formatKilobytes = function (string $value): string {
        if (! is_numeric($value)) {
            return $value;
        }

        $kilobytes = (float) $value;

        if ($kilobytes >= 1048576) {
            return number_format($kilobytes / 1048576, 1).' GB';
        }

        if ($kilobytes >= 1024) {
            return number_format($kilobytes / 1024, 1).' MB';
        }

        return number_format($kilobytes).' KB';
    };

    $cpuVariant = function (float $cpu): string {
        return match (true) {
            $cpu >= 75 => 'red',
            $cpu >= 40 => 'amber',
            $cpu >= 10 => 'blue',
            default => 'gray',
        };
    };

    $buildProcessDetails = function (array $process) use ($formatPercent, $formatKilobytes): array {
        $details = [
            'PID' => $process['pid'],
            'User' => $process['user'],
            'CPU' => $formatPercent((float) $process['cpu']),
            'Memory' => $formatPercent((float) $process['memory']),
            'VSZ' => $process['vsz'] !== '' ? $formatKilobytes($process['vsz']) : null,
            'RSS' => $process['rss'] !== '' ? $formatKilobytes($process['rss']) : null,
            'TTY' => $process['tty'],
            'State' => $process['stat'],
            'Started' => $process['started'] !== '' ? $process['started'] : null,
            'Time' => $process['time'],
        ];

        return array_filter(
            $details,
            fn (?string $value): bool => is_string($value) && $value !== '',
        );
    };

    $modalPrefix = 'process-'.substr(md5(json_encode([
        $command,
        $capturedAt,
        $host,
        $processes->pluck('pid')->all(),
    ])), 0, 10);

    $visibleProcesses = $processes->count();
    $totalCpu = $processes->sum('cpu');
    $totalMemory = $processes->sum('memory');
    $topProcess = $processes->first();
@endphp

<section {{ $attributes->merge(['class' => 'overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900']) }}>
    <div class="flex flex-col gap-4 px-5 py-4 sm:flex-row sm:items-start sm:justify-between sm:px-6">
        <div class="min-w-0">
            <h3 class="text-base font-semibold text-gray-900 dark:text-white">{{ $title }}</h3>
            @if ($description)
                <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ $description }}</p>
            @endif
        </div>

        <div class="flex shrink-0 flex-wrap items-center gap-2">
            <x-ui.badge variant="gray" :pill="false" icon="terminal-window" icon-weight="regular">
                <span class="font-mono">{{ $command }}</span>
            </x-ui.badge>
            @if ($capturedAt)
                <x-ui.badge variant="gray" :pill="false" icon="clock" icon-weight="regular">{{ $capturedAt }}</x-ui.badge>
            @endif
        </div>
    </div>

    @if ($visibleProcesses > 0)
        <div class="grid gap-4 border-b border-gray-200 px-5 py-4 dark:border-gray-800 sm:grid-cols-2 lg:grid-cols-4 sm:px-6">
            <div class="min-w-0">
                <p class="text-xs font-medium text-gray-500 dark:text-gray-400">Processes</p>
                <p class="mt-1 text-lg font-semibold tabular-nums text-gray-900 dark:text-white">{{ number_format($visibleProcesses) }}</p>
            </div>
            <div class="min-w-0">
                <p class="text-xs font-medium text-gray-500 dark:text-gray-400">Visible CPU</p>
                <p class="mt-1 text-lg font-semibold tabular-nums text-gray-900 dark:text-white">{{ $formatPercent($totalCpu) }}</p>
            </div>
            <div class="min-w-0">
                <p class="text-xs font-medium text-gray-500 dark:text-gray-400">Visible memory</p>
                <p class="mt-1 text-lg font-semibold tabular-nums text-gray-900 dark:text-white">{{ $formatPercent($totalMemory) }}</p>
            </div>
            <div class="min-w-0">
                <p class="text-xs font-medium text-gray-500 dark:text-gray-400">{{ $host ? 'Host' : 'Top command' }}</p>
                <p class="mt-1 truncate text-sm font-semibold text-gray-900 dark:text-white" title="{{ $host ?: ($topProcess['command'] ?? '') }}">
                    {{ $host ?: \Illuminate\Support\Str::limit($topProcess['command'] ?? 'Unknown', 44) }}
                </p>
            </div>
        </div>

        <div class="overflow-x-auto">
            <table class="min-w-[58rem] divide-y divide-gray-200 text-sm dark:divide-gray-800">
                <caption class="sr-only">{{ $title }} process list</caption>
                <thead class="bg-gray-50 dark:bg-gray-900/60">
                    <tr>
                        <th scope="col" class="px-5 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 sm:px-6">PID</th>
                        <th scope="col" class="px-5 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">User</th>
                        <th scope="col" class="px-5 py-3 text-right text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">CPU</th>
                        <th scope="col" class="px-5 py-3 text-right text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Memory</th>
                        <th scope="col" class="px-5 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">State</th>
                        <th scope="col" class="px-5 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Time</th>
                        <th scope="col" class="px-5 py-3 text-left text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 sm:px-6">Command</th>
                    </tr>
                </thead>
                <tbody class="divide-y divide-gray-200 bg-white dark:divide-gray-800 dark:bg-gray-900">
                    @foreach ($processes as $process)
                        @php
                            $cpu = (float) $process['cpu'];
                            $memory = (float) $process['memory'];
                            $cpuWidth = max(1, min(100, (int) round($cpu)));
                            $memoryWidth = max(1, min(100, (int) round($memory)));
                            $modalName = $modalPrefix.'-'.$process['pid'];
                        @endphp

                        <tr class="transition-colors hover:bg-gray-50 dark:hover:bg-gray-800/40">
                            <td class="whitespace-nowrap px-5 py-4 sm:px-6">
                                <button
                                    type="button"
                                    class="font-mono text-xs tabular-nums text-brand-600 underline-offset-2 transition hover:text-brand-500 hover:underline dark:text-brand-400"
                                    aria-label="View details for process {{ $process['pid'] }}"
                                    x-on:click="$dispatch('open-modal', @js($modalName))"
                                >
                                    {{ $process['pid'] }}
                                </button>
                            </td>
                            <td class="whitespace-nowrap px-5 py-4 text-gray-700 dark:text-gray-200">{{ $process['user'] }}</td>
                            <td class="whitespace-nowrap px-5 py-4 text-right">
                                <div class="ml-auto flex w-28 items-center justify-end gap-2">
                                    <div class="h-1.5 w-14 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-800" role="presentation" aria-hidden="true">
                                        <div class="h-full rounded-full bg-brand-500 dark:bg-brand-400" style="width: {{ $cpuWidth }}%"></div>
                                    </div>
                                    <x-ui.badge :variant="$cpuVariant($cpu)" size="sm" :pill="false" aria-label="CPU usage {{ $formatPercent($cpu) }}">{{ $formatPercent($cpu) }}</x-ui.badge>
                                </div>
                            </td>
                            <td class="whitespace-nowrap px-5 py-4 text-right">
                                <div class="ml-auto flex w-32 items-center justify-end gap-2">
                                    <div class="h-1.5 w-14 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-800" role="presentation" aria-hidden="true">
                                        <div class="h-full rounded-full bg-gray-400 dark:bg-gray-500" style="width: {{ $memoryWidth }}%"></div>
                                    </div>
                                    <span class="font-mono text-xs tabular-nums text-gray-600 dark:text-gray-300" aria-label="Memory usage {{ $formatPercent($memory) }}">{{ $formatPercent($memory) }}</span>
                                </div>
                            </td>
                            <td class="whitespace-nowrap px-5 py-4 text-gray-500 dark:text-gray-400">
                                <span class="font-mono text-xs">{{ $process['stat'] }}</span>
                                <span class="ml-2 text-xs">{{ $process['tty'] }}</span>
                            </td>
                            <td class="whitespace-nowrap px-5 py-4 font-mono text-xs text-gray-500 dark:text-gray-400">
                                <span>{{ $process['time'] }}</span>
                                @if ($process['rss'] !== '')
                                    <span class="ml-2 text-gray-400 dark:text-gray-500">{{ $formatKilobytes($process['rss']) }} RSS</span>
                                @endif
                            </td>
                            <td class="max-w-md px-5 py-4 sm:px-6">
                                <code class="block truncate font-mono text-xs text-gray-800 dark:text-gray-100" title="{{ $process['command'] }}">
                                    {{ $process['command'] }}
                                </code>
                            </td>
                        </tr>
                    @endforeach
                </tbody>
            </table>
        </div>

        @foreach ($processes as $process)
            @php
                $modalName = $modalPrefix.'-'.$process['pid'];
                $processDetails = $buildProcessDetails($process);
            @endphp

            <x-ui.modal
                :name="$modalName"
                :title="'Process '.$process['pid']"
                :description="\Illuminate\Support\Str::limit($process['command'], 72)"
                maxWidth="lg"
            >
                <x-ui.description-list :items="[]">
                    @foreach ($processDetails as $label => $value)
                        <x-ui.description-list.item :label="$label">{{ $value }}</x-ui.description-list.item>
                    @endforeach
                    <x-ui.description-list.item label="Command">
                        <code class="block break-all rounded-md bg-gray-100 px-3 py-2 font-mono text-xs text-gray-800 dark:bg-gray-800 dark:text-gray-100">{{ $process['command'] }}</code>
                    </x-ui.description-list.item>
                </x-ui.description-list>

                <x-slot:footer>
                    <x-ui.button variant="outline" type="button" @click="close()">
                        Close
                    </x-ui.button>
                </x-slot:footer>
            </x-ui.modal>
        @endforeach
    @else
        <x-ui.empty-state :title="$emptyTitle" :description="$emptyDescription" icon="terminal-window" class="rounded-none border-0 shadow-none" />
    @endif
</section>