367 lines
17 KiB
PHP
367 lines
17 KiB
PHP
@php
|
|
$diff = $getState() ?? ['summary' => [], 'added' => [], 'removed' => [], 'changed' => []];
|
|
$summary = $diff['summary'] ?? [];
|
|
$policyType = $diff['policy_type'] ?? null;
|
|
|
|
$groupByBlock = static function (array $items): array {
|
|
$groups = [];
|
|
|
|
foreach ($items as $path => $value) {
|
|
if (! is_string($path) || $path === '') {
|
|
continue;
|
|
}
|
|
|
|
$parts = explode(' > ', $path, 2);
|
|
|
|
if (count($parts) === 2) {
|
|
[$group, $label] = $parts;
|
|
} else {
|
|
$group = 'Other';
|
|
$label = $path;
|
|
}
|
|
|
|
$groups[$group][$label] = $value;
|
|
}
|
|
|
|
ksort($groups);
|
|
|
|
return $groups;
|
|
};
|
|
|
|
$stringify = static function (mixed $value): string {
|
|
if ($value === null) {
|
|
return '—';
|
|
}
|
|
|
|
if (is_bool($value)) {
|
|
return $value ? 'Enabled' : 'Disabled';
|
|
}
|
|
|
|
if (is_scalar($value)) {
|
|
return (string) $value;
|
|
}
|
|
|
|
return json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: '';
|
|
};
|
|
|
|
$isExpandable = static function (mixed $value): bool {
|
|
if (is_array($value)) {
|
|
return true;
|
|
}
|
|
|
|
return is_string($value) && strlen($value) > 160;
|
|
};
|
|
|
|
$isScriptKey = static function (mixed $name): bool {
|
|
return in_array((string) $name, ['scriptContent', 'detectionScriptContent', 'remediationScriptContent'], true);
|
|
};
|
|
|
|
$canHighlightScripts = static function (?string $policyType): bool {
|
|
return (bool) config('tenantpilot.display.show_script_content', false)
|
|
&& in_array($policyType, ['deviceManagementScript', 'deviceShellScript', 'deviceHealthScript'], true);
|
|
};
|
|
|
|
$selectGrammar = static function (?string $policyType, string $code): string {
|
|
if ($policyType === 'deviceShellScript') {
|
|
$firstLine = strtok($code, "\n") ?: '';
|
|
$shebang = trim($firstLine);
|
|
|
|
if (str_starts_with($shebang, '#!')) {
|
|
if (str_contains($shebang, 'zsh')) {
|
|
return 'zsh';
|
|
}
|
|
|
|
if (str_contains($shebang, 'bash')) {
|
|
return 'bash';
|
|
}
|
|
|
|
return 'sh';
|
|
}
|
|
|
|
return 'sh';
|
|
}
|
|
|
|
return 'powershell';
|
|
};
|
|
|
|
$highlight = static function (?string $policyType, string $code, string $fallbackClass = '') use ($selectGrammar): ?string {
|
|
if (! class_exists(\Torchlight\Engine\Engine::class)) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return (new \Torchlight\Engine\Engine())->codeToHtml(
|
|
code: $code,
|
|
grammar: $selectGrammar($policyType, $code),
|
|
theme: [
|
|
'light' => 'github-light',
|
|
'dark' => 'github-dark',
|
|
],
|
|
withGutter: false,
|
|
withWrapper: true,
|
|
);
|
|
} catch (\Throwable $e) {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
$splitLines = static function (string $text): array {
|
|
$text = str_replace(["\r\n", "\r"], "\n", $text);
|
|
|
|
return $text === '' ? [] : explode("\n", $text);
|
|
};
|
|
|
|
$myersLineDiff = static function (array $a, array $b): array {
|
|
$n = count($a);
|
|
$m = count($b);
|
|
$max = $n + $m;
|
|
|
|
$v = [1 => 0];
|
|
$trace = [];
|
|
|
|
for ($d = 0; $d <= $max; $d++) {
|
|
$trace[$d] = $v;
|
|
|
|
for ($k = -$d; $k <= $d; $k += 2) {
|
|
$kPlus = $v[$k + 1] ?? 0;
|
|
$kMinus = $v[$k - 1] ?? 0;
|
|
|
|
if ($k === -$d || ($k !== $d && $kMinus < $kPlus)) {
|
|
$x = $kPlus;
|
|
} else {
|
|
$x = $kMinus + 1;
|
|
}
|
|
|
|
$y = $x - $k;
|
|
|
|
while ($x < $n && $y < $m && $a[$x] === $b[$y]) {
|
|
$x++;
|
|
$y++;
|
|
}
|
|
|
|
$v[$k] = $x;
|
|
|
|
if ($x >= $n && $y >= $m) {
|
|
break 2;
|
|
}
|
|
}
|
|
}
|
|
|
|
$ops = [];
|
|
$x = $n;
|
|
$y = $m;
|
|
|
|
for ($d = count($trace) - 1; $d >= 0; $d--) {
|
|
$v = $trace[$d];
|
|
$k = $x - $y;
|
|
|
|
$kPlus = $v[$k + 1] ?? 0;
|
|
$kMinus = $v[$k - 1] ?? 0;
|
|
|
|
if ($k === -$d || ($k !== $d && $kMinus < $kPlus)) {
|
|
$prevK = $k + 1;
|
|
} else {
|
|
$prevK = $k - 1;
|
|
}
|
|
|
|
$prevX = $v[$prevK] ?? 0;
|
|
$prevY = $prevX - $prevK;
|
|
|
|
while ($x > $prevX && $y > $prevY) {
|
|
$ops[] = ['type' => 'equal', 'line' => $a[$x - 1]];
|
|
$x--;
|
|
$y--;
|
|
}
|
|
|
|
if ($d === 0) {
|
|
break;
|
|
}
|
|
|
|
if ($x === $prevX) {
|
|
$ops[] = ['type' => 'insert', 'line' => $b[$y - 1] ?? ''];
|
|
$y--;
|
|
} else {
|
|
$ops[] = ['type' => 'delete', 'line' => $a[$x - 1] ?? ''];
|
|
$x--;
|
|
}
|
|
}
|
|
|
|
return array_reverse($ops);
|
|
};
|
|
|
|
$scriptLineDiff = static function (string $fromText, string $toText) use ($splitLines, $myersLineDiff): array {
|
|
return $myersLineDiff($splitLines($fromText), $splitLines($toText));
|
|
};
|
|
@endphp
|
|
|
|
<div class="space-y-4">
|
|
<x-filament::section
|
|
heading="Normalized diff"
|
|
:description="$summary['message'] ?? sprintf('%d added, %d removed, %d changed', $summary['added'] ?? 0, $summary['removed'] ?? 0, $summary['changed'] ?? 0)"
|
|
>
|
|
<div class="flex flex-wrap gap-2">
|
|
<x-filament::badge color="success">
|
|
{{ (int) ($summary['added'] ?? 0) }} added
|
|
</x-filament::badge>
|
|
<x-filament::badge color="danger">
|
|
{{ (int) ($summary['removed'] ?? 0) }} removed
|
|
</x-filament::badge>
|
|
<x-filament::badge color="warning">
|
|
{{ (int) ($summary['changed'] ?? 0) }} changed
|
|
</x-filament::badge>
|
|
</div>
|
|
</x-filament::section>
|
|
|
|
@foreach (['changed' => ['label' => 'Changed', 'collapsed' => false], 'added' => ['label' => 'Added', 'collapsed' => true], 'removed' => ['label' => 'Removed', 'collapsed' => true]] as $key => $meta)
|
|
@php
|
|
$items = $diff[$key] ?? [];
|
|
$groups = $groupByBlock(is_array($items) ? $items : []);
|
|
@endphp
|
|
|
|
@if ($groups !== [])
|
|
<x-filament::section
|
|
:heading="$meta['label']"
|
|
collapsible
|
|
:collapsed="$meta['collapsed']"
|
|
>
|
|
<div class="space-y-6">
|
|
@foreach ($groups as $group => $groupItems)
|
|
<div>
|
|
<div class="flex items-center justify-between">
|
|
<div class="text-sm font-medium text-gray-900 dark:text-white">
|
|
{{ $group }}
|
|
</div>
|
|
<x-filament::badge size="sm" color="gray">
|
|
{{ count($groupItems) }}
|
|
</x-filament::badge>
|
|
</div>
|
|
|
|
<div class="mt-2 divide-y divide-gray-200 rounded-lg border border-gray-200 dark:divide-white/10 dark:border-white/10">
|
|
@foreach ($groupItems as $name => $value)
|
|
<div class="px-4 py-3">
|
|
@if ($key === 'changed' && is_array($value) && array_key_exists('from', $value) && array_key_exists('to', $value))
|
|
@php
|
|
$from = $value['from'];
|
|
$to = $value['to'];
|
|
$fromText = $stringify($from);
|
|
$toText = $stringify($to);
|
|
|
|
$isScriptContent = $canHighlightScripts($policyType) && $isScriptKey($name);
|
|
$ops = $isScriptContent ? $scriptLineDiff((string) $fromText, (string) $toText) : [];
|
|
@endphp
|
|
<div class="grid grid-cols-1 gap-2 sm:grid-cols-3">
|
|
<div class="text-sm font-medium text-gray-900 dark:text-white">
|
|
{{ (string) $name }}
|
|
</div>
|
|
|
|
@if ($isScriptContent)
|
|
<div class="text-sm text-gray-600 dark:text-gray-300 sm:col-span-2">
|
|
<span class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Diff</span>
|
|
<details class="mt-1">
|
|
<summary class="cursor-pointer text-sm text-gray-700 dark:text-gray-200">
|
|
View
|
|
</summary>
|
|
|
|
<div class="mt-2 overflow-x-auto font-mono text-xs text-gray-800 dark:text-gray-200 whitespace-pre">
|
|
@php
|
|
$count = count($ops);
|
|
@endphp
|
|
|
|
@for ($i = 0; $i < $count; $i++)
|
|
@php
|
|
$op = $ops[$i];
|
|
$next = $ops[$i + 1] ?? null;
|
|
@endphp
|
|
|
|
@if ($op['type'] === 'equal')
|
|
{{ $op['line'] }}
|
|
@elseif ($op['type'] === 'delete' && is_array($next) && ($next['type'] ?? null) === 'insert')
|
|
<span class="block bg-danger-50 text-danger-700 dark:bg-danger-950/40 dark:text-danger-200">- {{ $op['line'] }}</span>
|
|
<span class="block bg-success-50 text-success-700 dark:bg-success-950/40 dark:text-success-200">+ {{ $next['line'] }}</span>
|
|
@php
|
|
$i++;
|
|
@endphp
|
|
@elseif ($op['type'] === 'delete')
|
|
<span class="block bg-danger-50 text-danger-700 dark:bg-danger-950/40 dark:text-danger-200">- {{ $op['line'] }}</span>
|
|
@elseif ($op['type'] === 'insert')
|
|
<span class="block bg-success-50 text-success-700 dark:bg-success-950/40 dark:text-success-200">+ {{ $op['line'] }}</span>
|
|
@endif
|
|
@endfor
|
|
</div>
|
|
</details>
|
|
</div>
|
|
@else
|
|
<div class="text-sm text-gray-600 dark:text-gray-300">
|
|
<span class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">From</span>
|
|
@if ($isExpandable($from))
|
|
<details class="mt-1">
|
|
<summary class="cursor-pointer text-sm text-gray-700 dark:text-gray-200">
|
|
View
|
|
</summary>
|
|
<pre class="mt-2 overflow-x-auto text-xs text-gray-800 dark:text-gray-200">{{ $fromText }}</pre>
|
|
</details>
|
|
@else
|
|
<div class="mt-1">{{ $fromText }}</div>
|
|
@endif
|
|
</div>
|
|
<div class="text-sm text-gray-600 dark:text-gray-300">
|
|
<span class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">To</span>
|
|
@if ($isExpandable($to))
|
|
<details class="mt-1">
|
|
<summary class="cursor-pointer text-sm text-gray-700 dark:text-gray-200">
|
|
View
|
|
</summary>
|
|
<pre class="mt-2 overflow-x-auto text-xs text-gray-800 dark:text-gray-200">{{ $toText }}</pre>
|
|
</details>
|
|
@else
|
|
<div class="mt-1">{{ $toText }}</div>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@else
|
|
@php
|
|
$text = $stringify($value);
|
|
@endphp
|
|
<div class="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
|
|
<div class="text-sm font-medium text-gray-900 dark:text-white">
|
|
{{ (string) $name }}
|
|
</div>
|
|
<div class="text-sm text-gray-700 dark:text-gray-200 sm:max-w-[70%]">
|
|
@if ($isExpandable($value))
|
|
<details>
|
|
<summary class="cursor-pointer text-sm text-gray-700 dark:text-gray-200">
|
|
View
|
|
</summary>
|
|
@php
|
|
$isScriptContent = $canHighlightScripts($policyType) && $isScriptKey($name);
|
|
$highlighted = $isScriptContent ? $highlight($policyType, (string) $text) : null;
|
|
@endphp
|
|
|
|
@if (is_string($highlighted) && $highlighted !== '')
|
|
@once
|
|
@include('filament.partials.torchlight-dark-overrides')
|
|
@endonce
|
|
|
|
<div class="mt-2 overflow-x-auto">{!! $highlighted !!}</div>
|
|
@else
|
|
<pre class="mt-2 overflow-x-auto text-xs text-gray-800 dark:text-gray-200">{{ $text }}</pre>
|
|
@endif
|
|
</details>
|
|
@else
|
|
<div class="break-words">{{ $text }}</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</x-filament::section>
|
|
@endif
|
|
@endforeach
|
|
</div>
|